@cohortapp/agent-sdk 2.17.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 (526) 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.mjs +8 -6
  23. package/lib/collective/global-config.mjs +63 -1
  24. package/lib/collective/presence.mjs +142 -5
  25. package/lib/comms/send-gate.mjs +559 -1
  26. package/lib/diagnostics/alerts.mjs +49 -0
  27. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  28. package/lib/engine/agents/definitions.mjs +343 -0
  29. package/lib/engine/agents/persist.mjs +275 -0
  30. package/lib/engine/agents/runtime.mjs +748 -0
  31. package/lib/engine/agents/usage.mjs +95 -0
  32. package/lib/engine/auth-status.mjs +139 -0
  33. package/lib/engine/budget.mjs +194 -0
  34. package/lib/engine/cli.mjs +1204 -0
  35. package/lib/engine/commands/index.mjs +269 -0
  36. package/lib/engine/context/budget.mjs +219 -0
  37. package/lib/engine/context/cache.mjs +125 -0
  38. package/lib/engine/context/child-env.mjs +215 -0
  39. package/lib/engine/context/compaction.mjs +342 -0
  40. package/lib/engine/context/images.mjs +90 -0
  41. package/lib/engine/context/instructions.mjs +327 -0
  42. package/lib/engine/context/lazy-instructions.mjs +169 -0
  43. package/lib/engine/context/manager.mjs +182 -0
  44. package/lib/engine/context/real-path.mjs +91 -0
  45. package/lib/engine/context/secret-values.mjs +163 -0
  46. package/lib/engine/context/settings.mjs +274 -0
  47. package/lib/engine/context/stream-input.mjs +159 -0
  48. package/lib/engine/guard.mjs +152 -0
  49. package/lib/engine/hooks.mjs +713 -0
  50. package/lib/engine/loop.mjs +560 -0
  51. package/lib/engine/mcp/client.mjs +254 -0
  52. package/lib/engine/mcp/config.mjs +301 -0
  53. package/lib/engine/mcp/http.mjs +201 -0
  54. package/lib/engine/mcp/index.mjs +146 -0
  55. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  56. package/lib/engine/mcp/naming.mjs +66 -0
  57. package/lib/engine/mcp/resources.mjs +89 -0
  58. package/lib/engine/mcp/results.mjs +133 -0
  59. package/lib/engine/mcp/stdio.mjs +137 -0
  60. package/lib/engine/mcp/supervisor.mjs +116 -0
  61. package/lib/engine/messages.mjs +104 -0
  62. package/lib/engine/output/json.mjs +164 -0
  63. package/lib/engine/output/stream-json.mjs +266 -0
  64. package/lib/engine/permissions.mjs +845 -0
  65. package/lib/engine/process-identity.mjs +164 -0
  66. package/lib/engine/process-tree.mjs +551 -0
  67. package/lib/engine/prompt.mjs +60 -0
  68. package/lib/engine/session/store.mjs +299 -0
  69. package/lib/engine/session-runtime/args.mjs +97 -0
  70. package/lib/engine/session-runtime/host.mjs +143 -0
  71. package/lib/engine/session-runtime/inbox.mjs +122 -0
  72. package/lib/engine/session-runtime/notifications.mjs +129 -0
  73. package/lib/engine/session-runtime/registry.mjs +328 -0
  74. package/lib/engine/session-runtime/runner.mjs +344 -0
  75. package/lib/engine/session-runtime/socket.mjs +212 -0
  76. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  77. package/lib/engine/skills/index.mjs +321 -0
  78. package/lib/engine/tools/bash-background.mjs +533 -0
  79. package/lib/engine/tools/bash.mjs +216 -0
  80. package/lib/engine/tools/edit.mjs +97 -0
  81. package/lib/engine/tools/glob.mjs +81 -0
  82. package/lib/engine/tools/grep.mjs +224 -0
  83. package/lib/engine/tools/index.mjs +84 -0
  84. package/lib/engine/tools/list-agents.mjs +32 -0
  85. package/lib/engine/tools/ls.mjs +127 -0
  86. package/lib/engine/tools/monitor.mjs +82 -0
  87. package/lib/engine/tools/notebook-edit.mjs +218 -0
  88. package/lib/engine/tools/read.mjs +103 -0
  89. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  90. package/lib/engine/tools/schema.mjs +144 -0
  91. package/lib/engine/tools/send-message.mjs +77 -0
  92. package/lib/engine/tools/session.mjs +70 -0
  93. package/lib/engine/tools/todo.mjs +144 -0
  94. package/lib/engine/tools/toolsearch.mjs +217 -0
  95. package/lib/engine/tools/walk.mjs +193 -0
  96. package/lib/engine/tools/web-switch.mjs +31 -0
  97. package/lib/engine/tools/webfetch-html.mjs +387 -0
  98. package/lib/engine/tools/webfetch-net.mjs +340 -0
  99. package/lib/engine/tools/webfetch.mjs +198 -0
  100. package/lib/engine/tools/websearch.mjs +91 -0
  101. package/lib/engine/tools/workflow.mjs +95 -0
  102. package/lib/engine/tools/write.mjs +76 -0
  103. package/lib/engine/tui/line-editor.mjs +137 -0
  104. package/lib/engine/tui/render.mjs +86 -0
  105. package/lib/engine/tui/tui.mjs +274 -0
  106. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  107. package/lib/engine/wire/effort.mjs +36 -0
  108. package/lib/engine/wire/errors.mjs +496 -0
  109. package/lib/engine/wire/http.mjs +441 -0
  110. package/lib/engine/wire/index.mjs +76 -0
  111. package/lib/engine/wire/openai-chat.mjs +332 -0
  112. package/lib/engine/wire/prompt-cache.mjs +79 -0
  113. package/lib/engine/wire/search.mjs +140 -0
  114. package/lib/engine/wire/sse.mjs +114 -0
  115. package/lib/engine/wire/stall.mjs +349 -0
  116. package/lib/engine/wire/token-provider.mjs +175 -0
  117. package/lib/engine/wire/usage.mjs +192 -0
  118. package/lib/engine/workflow/host.mjs +524 -0
  119. package/lib/engine/workflow/journal.mjs +188 -0
  120. package/lib/engine/workflow/json-schema.mjs +171 -0
  121. package/lib/engine/workflow/meta.mjs +329 -0
  122. package/lib/engine/workflow/notifications.mjs +52 -0
  123. package/lib/engine/workflow/runtime.mjs +447 -0
  124. package/lib/engine/workflow/sandbox.mjs +534 -0
  125. package/lib/engine/workflow/worker.mjs +141 -0
  126. package/lib/engine/workflow/worktree.mjs +74 -0
  127. package/lib/execution/disposition.mjs +1 -1
  128. package/lib/execution/intake.mjs +10 -0
  129. package/lib/execution/surface-policy.mjs +15 -0
  130. package/lib/learning/curator.mjs +8 -6
  131. package/lib/learning/reflect.mjs +8 -6
  132. package/lib/model-router/catalog/cohort.yaml +137 -0
  133. package/lib/model-router/catalog.mjs +118 -1
  134. package/lib/model-router/failover.mjs +67 -16
  135. package/lib/model-router/llm-task.mjs +39 -3
  136. package/lib/model-router/resolve.mjs +89 -3
  137. package/lib/model-router/spawn.mjs +46 -47
  138. package/lib/model-router/taxonomy.mjs +126 -4
  139. package/lib/org/cost-sync.mjs +141 -11
  140. package/lib/org/inbound/broadcast.mjs +289 -0
  141. package/lib/org/inbound/collective.mjs +375 -0
  142. package/lib/org/inbound/directedness.mjs +96 -8
  143. package/lib/org/inbound/facts.mjs +78 -2
  144. package/lib/org/inbound/project.mjs +22 -0
  145. package/lib/org/inbound/surfaces.mjs +14 -0
  146. package/lib/org/llm-token.mjs +879 -0
  147. package/lib/org/mesh.mjs +61 -0
  148. package/lib/org/messaging.mjs +3 -1
  149. package/lib/org/protocol.checksum +1 -1
  150. package/lib/org/protocol.mjs +15 -0
  151. package/lib/org/quota.mjs +520 -0
  152. package/lib/org/tool-surface.mjs +104 -16
  153. package/lib/org/ui-parity.mjs +16 -1
  154. package/lib/org/work-ledger.mjs +37 -6
  155. package/lib/rate-guard.mjs +114 -1
  156. package/lib/resource-governor.mjs +41 -6
  157. package/lib/runtime/adapter.mjs +823 -0
  158. package/lib/runtime/child-env.mjs +191 -0
  159. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  160. package/lib/runtime/seat-engine.mjs +162 -0
  161. package/lib/session/ask-ledger.mjs +271 -0
  162. package/lib/session/current-work.mjs +676 -0
  163. package/lib/session/feed-core.mjs +40 -3
  164. package/lib/session/launch-args.mjs +56 -4
  165. package/lib/session/status-summary.mjs +26 -9
  166. package/lib/session/upgrade-notice.mjs +42 -0
  167. package/lib/setup/claude-probe.mjs +117 -13
  168. package/lib/setup/enrich.mjs +13 -10
  169. package/lib/setup/sections/model.mjs +39 -13
  170. package/lib/telemetry/collect.mjs +208 -9
  171. package/lib/upgrade/ignored-drift.mjs +105 -0
  172. package/lib/voice/post-call-brief.mjs +30 -17
  173. package/package.json +13 -3
  174. package/plugins/maestro-skills/skills/board-work.md +5 -0
  175. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  176. package/plugins/maestro-skills/skills/main-session.md +18 -7
  177. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  178. package/scripts/ci/run-tests.mjs +47 -19
  179. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  180. package/scripts/collective/hook-runner.mjs +29 -2
  181. package/scripts/continuous-monitor.sh +13 -0
  182. package/scripts/cost/track-claude-usage.mjs +15 -0
  183. package/scripts/daemon/agent-daemon.mjs +408 -20
  184. package/scripts/daemon/assurance.mjs +48 -12
  185. package/scripts/daemon/cadence-consumer.mjs +218 -68
  186. package/scripts/daemon/cadence-handlers.mjs +73 -4
  187. package/scripts/daemon/classifier.mjs +75 -26
  188. package/scripts/daemon/context-compiler.mjs +51 -37
  189. package/scripts/daemon/deliver.mjs +30 -1
  190. package/scripts/daemon/dispatcher.mjs +595 -149
  191. package/scripts/daemon/health.mjs +14 -1
  192. package/scripts/daemon/maestro-daemon.mjs +11 -0
  193. package/scripts/daemon/prompt-builder.mjs +24 -0
  194. package/scripts/daemon/responder.mjs +246 -79
  195. package/scripts/daemon/sdk-version.mjs +98 -16
  196. package/scripts/eval/probe-gateway.mjs +635 -0
  197. package/scripts/eval/replay/extract.mjs +270 -0
  198. package/scripts/eval/replay/grade.mjs +260 -0
  199. package/scripts/eval/replay/lib/config.mjs +50 -0
  200. package/scripts/eval/replay/lib/effects.mjs +65 -0
  201. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  202. package/scripts/eval/replay/lib/judge.mjs +72 -0
  203. package/scripts/eval/replay/lib/redact.mjs +136 -0
  204. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  205. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  206. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  207. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  208. package/scripts/eval/replay/report.mjs +185 -0
  209. package/scripts/eval/replay/run.mjs +404 -0
  210. package/scripts/fleet/rollout.mjs +1094 -0
  211. package/scripts/hooks/pre-send-audit.sh +36 -245
  212. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  213. package/scripts/hooks/validate-state-yaml.sh +190 -0
  214. package/scripts/huddle/huddle-llm.mjs +361 -0
  215. package/scripts/huddle/huddle-server.mjs +46 -121
  216. package/scripts/local-triggers/autoupdate.sh +448 -78
  217. package/scripts/local-triggers/run-trigger.sh +13 -0
  218. package/scripts/maintenance/pin-integrity.mjs +364 -0
  219. package/scripts/poll-slack-events.sh +41 -9
  220. package/scripts/poller/slack-socket-mode.mjs +28 -3
  221. package/scripts/session/supervisor.mjs +80 -13
  222. package/scripts/spawn-session.sh +13 -0
  223. package/bin/maestro.test.mjs +0 -1574
  224. package/lib/action-executor.test.mjs +0 -871
  225. package/lib/archetype.test.mjs +0 -132
  226. package/lib/assurance/plan-note.test.mjs +0 -234
  227. package/lib/assurance/room-budget.test.mjs +0 -486
  228. package/lib/assurance/tier.test.mjs +0 -174
  229. package/lib/autonomy.test.mjs +0 -66
  230. package/lib/backlog.test.mjs +0 -302
  231. package/lib/backup/policy.test.mjs +0 -305
  232. package/lib/budget-escalate.test.mjs +0 -232
  233. package/lib/budget-guard.envelope.test.mjs +0 -476
  234. package/lib/budget-guard.test.mjs +0 -427
  235. package/lib/cadence-bus-requeue.test.mjs +0 -83
  236. package/lib/cadence-bus-schedule.test.mjs +0 -194
  237. package/lib/cadence-bus.test.mjs +0 -720
  238. package/lib/cadences.test.mjs +0 -230
  239. package/lib/capability/inventory.test.mjs +0 -232
  240. package/lib/capability.test.mjs +0 -78
  241. package/lib/channels/base-adapter.test.mjs +0 -590
  242. package/lib/channels/channels.test.mjs +0 -371
  243. package/lib/channels/contract.test.mjs +0 -162
  244. package/lib/channels/inbox-item.test.mjs +0 -368
  245. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  246. package/lib/channels/pairing.test.mjs +0 -270
  247. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  248. package/lib/channels/slack-adapter.test.mjs +0 -212
  249. package/lib/channels/telegram-adapter.test.mjs +0 -306
  250. package/lib/channels/voice/adapter.test.mjs +0 -278
  251. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  252. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  253. package/lib/charter.test.mjs +0 -89
  254. package/lib/claude-bin.test.mjs +0 -131
  255. package/lib/cli/board.test.mjs +0 -227
  256. package/lib/cli/design.test.mjs +0 -270
  257. package/lib/cli/doctor-checks.test.mjs +0 -336
  258. package/lib/cli/global-setup-extras.test.mjs +0 -462
  259. package/lib/cli/inbox.test.mjs +0 -230
  260. package/lib/cli/session-ack.test.mjs +0 -63
  261. package/lib/cli/session.test.mjs +0 -613
  262. package/lib/collective/capture.test.mjs +0 -121
  263. package/lib/collective/cards.test.mjs +0 -114
  264. package/lib/collective/config.test.mjs +0 -123
  265. package/lib/collective/global-config.test.mjs +0 -220
  266. package/lib/collective/global-skills.test.mjs +0 -126
  267. package/lib/collective/presence.test.mjs +0 -95
  268. package/lib/collective/recall.test.mjs +0 -116
  269. package/lib/collective/vendor-skills.test.mjs +0 -306
  270. package/lib/comms/send-gate.test.mjs +0 -770
  271. package/lib/comms.test.mjs +0 -41
  272. package/lib/context/budget.test.mjs +0 -252
  273. package/lib/context/history-scope.test.mjs +0 -79
  274. package/lib/cost/ledger-row.test.mjs +0 -183
  275. package/lib/design/design-md.test.mjs +0 -318
  276. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  277. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  278. package/lib/design/fixtures/foundation.json +0 -133
  279. package/lib/design/refresh-gate.test.mjs +0 -144
  280. package/lib/design/write.test.mjs +0 -241
  281. package/lib/diagnostics/alerts.test.mjs +0 -318
  282. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  283. package/lib/diagnostics/counters.test.mjs +0 -206
  284. package/lib/diagnostics/events.test.mjs +0 -290
  285. package/lib/diagnostics/otel.test.mjs +0 -196
  286. package/lib/diagnostics/trace.test.mjs +0 -251
  287. package/lib/env-compat.test.mjs +0 -104
  288. package/lib/execution/disposition.test.mjs +0 -553
  289. package/lib/execution/drive.test.mjs +0 -270
  290. package/lib/execution/effects.test.mjs +0 -344
  291. package/lib/execution/intake.test.mjs +0 -389
  292. package/lib/execution/journal.test.mjs +0 -261
  293. package/lib/execution/match.test.mjs +0 -235
  294. package/lib/execution/pipeline.test.mjs +0 -392
  295. package/lib/execution/route.test.mjs +0 -186
  296. package/lib/execution/surface-policy.test.mjs +0 -162
  297. package/lib/fs-atomic.test.mjs +0 -72
  298. package/lib/fs-ownership.test.mjs +0 -158
  299. package/lib/goals/admission.test.mjs +0 -164
  300. package/lib/goals/classify.test.mjs +0 -167
  301. package/lib/goals/collaborate.test.mjs +0 -336
  302. package/lib/goals/gaps.test.mjs +0 -284
  303. package/lib/goals/loop.test.mjs +0 -845
  304. package/lib/hooks/bus.test.mjs +0 -387
  305. package/lib/identity/persona.test.mjs +0 -142
  306. package/lib/kpi-sensors.test.mjs +0 -278
  307. package/lib/kpi.test.mjs +0 -244
  308. package/lib/learning/config.test.mjs +0 -75
  309. package/lib/learning/counters.test.mjs +0 -69
  310. package/lib/learning/curator-consolidate.test.mjs +0 -238
  311. package/lib/learning/curator.test.mjs +0 -106
  312. package/lib/learning/reflect.test.mjs +0 -0
  313. package/lib/learning/session-index.test.mjs +0 -125
  314. package/lib/learning/skill-writer.test.mjs +0 -210
  315. package/lib/mandate/audit.test.mjs +0 -195
  316. package/lib/mandate/contract.test.mjs +0 -185
  317. package/lib/mandate/derive.test.mjs +0 -274
  318. package/lib/mandate/model.test.mjs +0 -164
  319. package/lib/mandate/refresh.test.mjs +0 -389
  320. package/lib/mcp/server.test.mjs +0 -426
  321. package/lib/model-router/auth-profiles.test.mjs +0 -580
  322. package/lib/model-router/catalog.test.mjs +0 -385
  323. package/lib/model-router/economics.test.mjs +0 -438
  324. package/lib/model-router/failover.test.mjs +0 -439
  325. package/lib/model-router/health.test.mjs +0 -338
  326. package/lib/model-router/integration-coverage.test.mjs +0 -831
  327. package/lib/model-router/integration.test.mjs +0 -564
  328. package/lib/model-router/ledger.test.mjs +0 -415
  329. package/lib/model-router/llm-task.test.mjs +0 -392
  330. package/lib/model-router/org-credentials.test.mjs +0 -265
  331. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  332. package/lib/model-router/reconcile.test.mjs +0 -316
  333. package/lib/model-router/repair.test.mjs +0 -180
  334. package/lib/model-router/spawn.test.mjs +0 -446
  335. package/lib/model-router/taxonomy.test.mjs +0 -410
  336. package/lib/model-router.test.mjs +0 -1207
  337. package/lib/org/activity.test.mjs +0 -134
  338. package/lib/org/approvals.test.mjs +0 -216
  339. package/lib/org/awareness.test.mjs +0 -159
  340. package/lib/org/board-mine-cache.test.mjs +0 -53
  341. package/lib/org/board.test.mjs +0 -187
  342. package/lib/org/bootstrap-context.test.mjs +0 -153
  343. package/lib/org/client.test.mjs +0 -1206
  344. package/lib/org/cohort-client.test.mjs +0 -126
  345. package/lib/org/cost-sync.test.mjs +0 -153
  346. package/lib/org/doctor.test.mjs +0 -346
  347. package/lib/org/engagement-ledger.test.mjs +0 -112
  348. package/lib/org/engagement.test.mjs +0 -739
  349. package/lib/org/handoff.test.mjs +0 -269
  350. package/lib/org/inbound/directedness.test.mjs +0 -668
  351. package/lib/org/inbound/facts.test.mjs +0 -471
  352. package/lib/org/inbound/hydrate.test.mjs +0 -908
  353. package/lib/org/inbound/index.test.mjs +0 -429
  354. package/lib/org/inbound/project.test.mjs +0 -287
  355. package/lib/org/integration-tools.test.mjs +0 -160
  356. package/lib/org/keys.test.mjs +0 -92
  357. package/lib/org/knowledge.test.mjs +0 -326
  358. package/lib/org/leases.test.mjs +0 -235
  359. package/lib/org/mesh-directives.test.mjs +0 -110
  360. package/lib/org/mesh-integration.test.mjs +0 -127
  361. package/lib/org/mesh.test.mjs +0 -400
  362. package/lib/org/messaging.test.mjs +0 -471
  363. package/lib/org/param-contract.test.mjs +0 -477
  364. package/lib/org/policy.test.mjs +0 -237
  365. package/lib/org/protocol.checksum.test.mjs +0 -90
  366. package/lib/org/protocol.test.mjs +0 -323
  367. package/lib/org/push.test.mjs +0 -792
  368. package/lib/org/registry.test.mjs +0 -100
  369. package/lib/org/resource-tools.test.mjs +0 -361
  370. package/lib/org/tool-access.test.mjs +0 -144
  371. package/lib/org/tool-surface-integration.test.mjs +0 -120
  372. package/lib/org/tool-surface.test.mjs +0 -1268
  373. package/lib/org/typing.test.mjs +0 -291
  374. package/lib/org/ui-parity.test.mjs +0 -560
  375. package/lib/org/verify.test.mjs +0 -194
  376. package/lib/org/work-ledger.test.mjs +0 -273
  377. package/lib/plan/adoption-e2e.test.mjs +0 -366
  378. package/lib/plan/budget-enforcement.test.mjs +0 -400
  379. package/lib/plan/compile.test.mjs +0 -382
  380. package/lib/plan/emit.test.mjs +0 -269
  381. package/lib/plan/explain.test.mjs +0 -188
  382. package/lib/prompts/parallelism.test.mjs +0 -177
  383. package/lib/rag/rag.test.mjs +0 -505
  384. package/lib/rate-guard.test.mjs +0 -272
  385. package/lib/reactive-gate.test.mjs +0 -57
  386. package/lib/render.test.mjs +0 -68
  387. package/lib/resource-governor.test.mjs +0 -488
  388. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  389. package/lib/scheduling/jitter.test.mjs +0 -140
  390. package/lib/secrets/broker.test.mjs +0 -280
  391. package/lib/secrets/providers.test.mjs +0 -274
  392. package/lib/security/audit-engine.test.mjs +0 -424
  393. package/lib/security/coerce-args.test.mjs +0 -281
  394. package/lib/security/dangerous-tools.test.mjs +0 -68
  395. package/lib/security/external-content.test.mjs +0 -84
  396. package/lib/security/redact.test.mjs +0 -441
  397. package/lib/security/secret-equal.test.mjs +0 -55
  398. package/lib/session/config.test.mjs +0 -92
  399. package/lib/session/feed-core.test.mjs +0 -198
  400. package/lib/session/first-run.test.mjs +0 -121
  401. package/lib/session/frontdoor.test.mjs +0 -205
  402. package/lib/session/handoffs.test.mjs +0 -183
  403. package/lib/session/identity.test.mjs +0 -180
  404. package/lib/session/inbox-claims.test.mjs +0 -286
  405. package/lib/session/launch-args.test.mjs +0 -157
  406. package/lib/session/liveness.test.mjs +0 -100
  407. package/lib/session/status-summary.test.mjs +0 -118
  408. package/lib/session-permissions.test.mjs +0 -120
  409. package/lib/setup/claude-probe.test.mjs +0 -187
  410. package/lib/setup/completeness.test.mjs +0 -110
  411. package/lib/setup/context-pack.test.mjs +0 -89
  412. package/lib/setup/enrich.test.mjs +0 -115
  413. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  414. package/lib/setup/integration.test.mjs +0 -162
  415. package/lib/setup/io.test.mjs +0 -77
  416. package/lib/setup/runner.test.mjs +0 -132
  417. package/lib/setup/sections/identity.test.mjs +0 -234
  418. package/lib/setup/sections/inventory.test.mjs +0 -198
  419. package/lib/setup/sections/learning.test.mjs +0 -81
  420. package/lib/setup/sections/mandate.test.mjs +0 -388
  421. package/lib/setup/sections/messaging.test.mjs +0 -127
  422. package/lib/setup/sections/model.test.mjs +0 -240
  423. package/lib/setup/sections/org.test.mjs +0 -346
  424. package/lib/setup/sections/orgmail.test.mjs +0 -118
  425. package/lib/setup/sections/recovery.test.mjs +0 -98
  426. package/lib/setup/sections/subagents.test.mjs +0 -429
  427. package/lib/setup/sections/verify.test.mjs +0 -175
  428. package/lib/setup/sot.test.mjs +0 -81
  429. package/lib/setup/state.test.mjs +0 -115
  430. package/lib/singleton.test.mjs +0 -151
  431. package/lib/subagents/cli.test.mjs +0 -389
  432. package/lib/subagents/client.test.mjs +0 -309
  433. package/lib/subagents/gap.test.mjs +0 -234
  434. package/lib/subagents/lock.test.mjs +0 -248
  435. package/lib/subagents/manifest.test.mjs +0 -175
  436. package/lib/subagents/refs.test.mjs +0 -204
  437. package/lib/subagents/resolve.test.mjs +0 -422
  438. package/lib/subagents/schema.test.mjs +0 -328
  439. package/lib/telemetry/alerts.test.mjs +0 -109
  440. package/lib/telemetry/collect.test.mjs +0 -1274
  441. package/lib/tool-definitions-integration.test.mjs +0 -83
  442. package/lib/tool-definitions.test.mjs +0 -437
  443. package/lib/upgrade/global-refresh.test.mjs +0 -65
  444. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  445. package/lib/upgrade/post-steps.test.mjs +0 -200
  446. package/lib/upgrade/verify.test.mjs +0 -164
  447. package/lib/util/fetch-timeout.test.mjs +0 -202
  448. package/lib/util/reconnect.test.mjs +0 -369
  449. package/lib/util/unhandled.test.mjs +0 -216
  450. package/lib/voice/outbound.test.mjs +0 -69
  451. package/lib/voice/session-rotation.test.mjs +0 -114
  452. package/lib/voice/stt.test.mjs +0 -226
  453. package/lib/voice/voice.test.mjs +0 -990
  454. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  455. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  456. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  457. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  458. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  459. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  460. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  461. package/scripts/ci/check.test.mjs +0 -194
  462. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  463. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  464. package/scripts/collective/hook-runner.test.mjs +0 -173
  465. package/scripts/cost/fleet-digest.test.mjs +0 -207
  466. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  467. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  468. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  469. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  470. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  471. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  472. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  473. package/scripts/daemon/assurance.test.mjs +0 -1791
  474. package/scripts/daemon/board-mirror.test.mjs +0 -165
  475. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  476. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  477. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  478. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  479. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  480. package/scripts/daemon/classifier.test.mjs +0 -266
  481. package/scripts/daemon/classify-kind.test.mjs +0 -40
  482. package/scripts/daemon/context-compiler.test.mjs +0 -406
  483. package/scripts/daemon/deliver.test.mjs +0 -564
  484. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  485. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  486. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  487. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  488. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  489. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  490. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  491. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  492. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  493. package/scripts/daemon/integration.test.mjs +0 -149
  494. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  495. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  496. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  497. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  498. package/scripts/daemon/responder-cost.test.mjs +0 -68
  499. package/scripts/daemon/responder-history.test.mjs +0 -221
  500. package/scripts/daemon/sdk-version.test.mjs +0 -31
  501. package/scripts/daemon/session-lock.test.mjs +0 -252
  502. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  503. package/scripts/daemon/typing-registry.test.mjs +0 -102
  504. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  505. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  506. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  507. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  508. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  509. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  510. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  511. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  512. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  513. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  514. package/scripts/poller-launchd/install.test.mjs +0 -243
  515. package/scripts/restore-from-backup.test.mjs +0 -181
  516. package/scripts/session/feed.test.mjs +0 -196
  517. package/scripts/session/supervisor-sh.test.mjs +0 -218
  518. package/scripts/session/supervisor.test.mjs +0 -482
  519. package/scripts/setup/configure-macos.test.mjs +0 -306
  520. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  521. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  522. package/scripts/setup/generate-capability.test.mjs +0 -134
  523. package/scripts/setup/init-agent.test.mjs +0 -370
  524. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  525. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  526. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -1,1791 +0,0 @@
1
- /**
2
- * assurance.test.mjs — the four ways an ask used to end in silence, and the
3
- * message the human now gets instead of each of them.
4
- *
5
- * Run: node --test scripts/daemon/assurance.test.mjs
6
- *
7
- * Every test here uses an INJECTED FAKE for transport. Nothing in this file
8
- * touches Slack, Gmail, hq or the live workspace, and no test spawns a model.
9
- * That is not only hygiene — it is the point being proven: the whole
10
- * acknowledgement path is now deterministic string work, so a test can assert
11
- * the exact words a human would read.
12
- */
13
-
14
- import { test, describe, before, beforeEach, after } from "node:test";
15
- import assert from "node:assert/strict";
16
- import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
17
- import { tmpdir } from "node:os";
18
- import { join } from "node:path";
19
-
20
- // Point the whole module tree at a scratch root BEFORE importing anything that
21
- // resolves AGENT_DIR at module load.
22
- const ROOT = mkdtempSync(join(tmpdir(), "assurance-test-"));
23
- process.env.AGENT_DIR = ROOT;
24
- process.env.AGENT_ROOT = ROOT;
25
- mkdirSync(join(ROOT, "state"), { recursive: true });
26
-
27
- const assurance = await import("./assurance.mjs");
28
- const receipts = await import("../../lib/comms/receipts.mjs");
29
-
30
- const {
31
- shouldAcknowledge,
32
- generateAck,
33
- sanitiseAckText,
34
- composeFailure,
35
- composeSilentSuccess,
36
- isOwnNarratorText,
37
- SILENT_SUCCESS_EXCERPT_LIMIT,
38
- classifyFailure,
39
- openAndAcknowledge,
40
- settleSession,
41
- sweepObligations,
42
- readObligation,
43
- openObligations,
44
- _resetObligations,
45
- ACK_AFTER_MS,
46
- ACK_MIN_CHARS,
47
- RETIRED_NARRATOR_TEMPLATES,
48
- notePlan,
49
- NOTICE,
50
- STALE_AFTER_MS,
51
- INTERRUPT_GRACE_MS,
52
- ACK_MAX_ATTEMPTS,
53
- ACK_GEN_MAX_FAILURES,
54
- RETRY_MAX,
55
- } = assurance;
56
-
57
- after(() => { try { rmSync(ROOT, { recursive: true, force: true }); } catch { /* */ } });
58
-
59
- // ---------------------------------------------------------------------------
60
- // Fakes
61
- // ---------------------------------------------------------------------------
62
-
63
- /** A transport that records what a human would have read, and can be told to fail.
64
- * The ackSender mirrors responder.sendHoldingMessage's NEW contract exactly:
65
- * a generated bespoke line on success, and `{sent:false, holdingText:null,
66
- * genFailed:true}` — i.e. deliberate silence — when generation fails. */
67
- const FAKE_ACK_LINE = "Give me a few minutes on the git fixes.";
68
- function fakeTransport(opts = {}) {
69
- const sent = [];
70
- let failures = opts.failTimes || 0;
71
- const impl = async (item, text, o = {}) => {
72
- if (failures > 0) { failures--; return { sent: false, via: null, error: opts.error || "network unreachable" }; }
73
- sent.push({ to: item.sender, channel: item.channel_id || item.channel, kind: o.kind || "reply", text });
74
- return { sent: true, via: "fake", channel: item.channel_id || item.channel };
75
- };
76
- return { sent, impl, ackSender: async (item, cr) => {
77
- if (opts.genFail) return { sent: false, holdingText: null, error: "ack_generation_failed", genFailed: true };
78
- const text = opts.ackText || FAKE_ACK_LINE;
79
- const r = await impl(item, text, { kind: "ack" });
80
- return { sent: r.sent, holdingText: text, error: r.error };
81
- } };
82
- }
83
-
84
- const ITEM = {
85
- id: "msg-1001",
86
- raw_ref: "cohort:cmql18026:1001",
87
- message_id: "1001",
88
- service: "cohort",
89
- channel_id: "cmql1802601iihhzpf774vwbk",
90
- sender: "Dana Whitfield",
91
- content: "with that in mind, fix these issues you just noted please and push their fixes to git",
92
- };
93
-
94
- const CLASS_COMPLEX = { action: "respond", priority: "critical", model: "opus", summary: "Fix the noted issues and push the fixes to git" };
95
-
96
- beforeEach(() => { _resetObligations(); });
97
-
98
- // ---------------------------------------------------------------------------
99
- // 1. THE JUDGEMENT — simple asks must NOT get "I'll look into it"
100
- // ---------------------------------------------------------------------------
101
-
102
- describe("shouldAcknowledge — the simple/complex distinction", () => {
103
- test("a fast ask answered in this turn gets NO acknowledgement", () => {
104
- // The quick path already sent the answer. An ack here is pure noise.
105
- const v = shouldAcknowledge({ willSpawnSession: false, item: ITEM, source: "inbox" });
106
- assert.equal(v.ack, false);
107
- assert.equal(v.reason, "answered-in-turn");
108
- });
109
-
110
- test("a slow ask that spawns a session DOES get one", () => {
111
- const v = shouldAcknowledge({ willSpawnSession: true, item: ITEM, source: "inbox" });
112
- assert.equal(v.ack, true);
113
- });
114
-
115
- test("REGRESSION: a `queue` action still gets one", () => {
116
- // classifier.mjs's heuristic fallback (used when the LLM classifier itself
117
- // fails) emits action:"queue". The old gate was
118
- // `respond || draft || research`, so exactly when classification degraded, a
119
- // directed DM got a 45-minute session and total silence. The gate no longer
120
- // consults the classifier at all.
121
- const v = shouldAcknowledge({ willSpawnSession: true, item: ITEM, source: "inbox", classResult: { action: "queue" } });
122
- assert.equal(v.ack, true, "a degraded classification must not cost the human their acknowledgement");
123
- });
124
-
125
- test("backlog work, which nobody is waiting on, gets none", () => {
126
- assert.equal(shouldAcknowledge({ willSpawnSession: true, item: ITEM, source: "backlog" }).ack, false);
127
- });
128
-
129
- test("an item with no resolvable reply target gets none (nowhere to send it)", () => {
130
- const orphan = { ...ITEM, channel_id: null, channel: null };
131
- assert.equal(shouldAcknowledge({ willSpawnSession: true, item: orphan, source: "inbox" }).ack, false);
132
- });
133
-
134
- test("REGRESSION: the agent's OWN echoed message is never acknowledged (self-echo guard #2)", async () => {
135
- // 25/27/30 Aug 2026: the poll lane re-ingested the agent's own outbound
136
- // sends and the daemon said "Looking now." to itself — in shared DMs two
137
- // agents traded canned acks at each other. Ingestion now drops echoes
138
- // (agent-daemon + self-echo.mjs); this is the second, independent guard on
139
- // the ack decision itself.
140
- const { writeFileSync } = await import("node:fs");
141
- const selfEcho = await import("./lib/self-echo.mjs");
142
- mkdirSync(join(ROOT, "config"), { recursive: true });
143
- writeFileSync(join(ROOT, "config", "agent.json"), JSON.stringify({
144
- firstName: "Isla", fullName: "Isla Roselli", memberId: "cmqh0tcml004ihhl6zev6ocb5",
145
- }));
146
- selfEcho._resetSelfIdentity();
147
- try {
148
- // The REAL echoed shape: display name verbatim, spaces and all.
149
- const echo = { ...ITEM, sender: "Isla Roselli" };
150
- const v = shouldAcknowledge({ willSpawnSession: true, item: echo, source: "inbox" });
151
- assert.equal(v.ack, false);
152
- assert.equal(v.reason, "self-sender");
153
- // The old filter's normalised form must ALSO match — it is how the echo
154
- // slipped through three times.
155
- assert.equal(shouldAcknowledge({ willSpawnSession: true, item: { ...ITEM, sender: "isla-roselli" }, source: "inbox" }).ack, false);
156
- // And a genuine human sender still gets their acknowledgement.
157
- assert.equal(shouldAcknowledge({ willSpawnSession: true, item: ITEM, source: "inbox" }).ack, true);
158
- } finally {
159
- rmSync(join(ROOT, "config", "agent.json"), { force: true });
160
- selfEcho._resetSelfIdentity();
161
- }
162
- });
163
- });
164
-
165
- // ---------------------------------------------------------------------------
166
- // 1b. THE DISPATCH GATE READS THE INBOUND — a seat must not ack (and its
167
- // caller must not spawn) on output its own emitter class produces.
168
- // ---------------------------------------------------------------------------
169
-
170
- describe("shouldAcknowledge — the dispatch gate reads item.content", () => {
171
- // A peer AGENT seat, identified structurally (seat-slug id / agent↔agent DM),
172
- // never by name.
173
- const PEER_ACK = {
174
- ...ITEM,
175
- sender: "Piotr Novak",
176
- sender_id: "A039",
177
- content: "On it — taking a look now.",
178
- };
179
-
180
- test("REGRESSION: an ack-shaped one-liner from a peer agent seat gets NO ack and signals NO spawn", () => {
181
- const v = shouldAcknowledge({ willSpawnSession: true, item: PEER_ACK, source: "inbox" });
182
- assert.equal(v.ack, false);
183
- assert.equal(v.reason, "peer-ack-shaped");
184
- assert.equal(v.emitterClass, true, "the caller reads this to skip the session spawn too");
185
- });
186
-
187
- test("the SHAPE matches, not a phrase list — every 'Looking now' variant from a seat is refused", () => {
188
- for (const content of ["On it.", "Looking now", "Taking the git fixes now — push coming.", "Still digging into this."]) {
189
- const v = shouldAcknowledge({ willSpawnSession: true, item: { ...PEER_ACK, content }, source: "inbox" });
190
- assert.equal(v.ack, false, `"${content}" is ack-shaped`);
191
- assert.equal(v.emitterClass, true);
192
- }
193
- });
194
-
195
- test("an agent↔agent DM channel is an agent-seat signal too", () => {
196
- const v = shouldAcknowledge({
197
- willSpawnSession: true,
198
- item: { ...ITEM, sender: "Piotr Novak", channel: "dm-a016-a039", content: "On it." },
199
- source: "inbox",
200
- });
201
- assert.equal(v.ack, false);
202
- assert.equal(v.reason, "peer-ack-shaped");
203
- });
204
-
205
- test("the SAME short line from a HUMAN sender passes — shape alone is not enough", () => {
206
- // No seat-slug id, no agent DM channel: treated as human, and a human's
207
- // terse "On it" must never be met with silence.
208
- const v = shouldAcknowledge({ willSpawnSession: true, item: { ...ITEM, content: "On it — will do." }, source: "inbox" });
209
- assert.equal(v.ack, true);
210
- });
211
-
212
- test("a QUESTION or a REQUEST from a peer agent seat still passes the gate", () => {
213
- for (const content of [
214
- "did the migration land cleanly?",
215
- "please push the fix to git",
216
- "can you rerun the failing suite",
217
- "fix the flaky poller test and push it",
218
- ]) {
219
- const v = shouldAcknowledge({ willSpawnSession: true, item: { ...PEER_ACK, content }, source: "inbox" });
220
- assert.equal(v.ack, true, `"${content}" is substantive and must pass`);
221
- }
222
- });
223
-
224
- test("an INFORMATION-BEARING terse line from a peer agent seat passes — facts, links, decisions and completions are not acks", () => {
225
- // Re-review finding on d5abda3: the shape test alone consumed essentially
226
- // every terse peer utterance — including decision grants and direct
227
- // answers to questions this seat asked — and a false consume is silent to
228
- // the sender and unrecoverable. Only commitment-shaped acks may be eaten.
229
- for (const content of [
230
- "Runway is 14 months.", // carries a fact (digit)
231
- "Yes.", // direct answer
232
- "No.", // direct answer
233
- "Approved.", // decision grant
234
- "Go ahead.", // decision grant
235
- "LGTM.", // review verdict
236
- "Ship it.", // decision grant
237
- "Done.", // completion report
238
- "Merged.", // completion report
239
- "Deployed, all green.", // completion report
240
- "Spec is at https://example.com/doc", // carries a link
241
- ]) {
242
- const v = shouldAcknowledge({ willSpawnSession: true, item: { ...PEER_ACK, content }, source: "inbox" });
243
- assert.equal(v.ack, true, `"${content}" is information-bearing and must pass`);
244
- }
245
- });
246
-
247
- test("REGRESSION: the daemon's OWN narrator lines are refused — built from the real compose* strings, so the list cannot drift", () => {
248
- const narrations = [
249
- // The two RETIRED progress lines. Nothing composes them any more, but an
250
- // older seat on the same fleet still emits them until its autoupdate
251
- // lands, and acking a peer's progress ping spawns a 45-minute session
252
- // behind it — the 25/27/30 Aug loop.
253
- ...RETIRED_NARRATOR_TEMPLATES,
254
- "Still on this — 7 minutes in. I'll come back as soon as I've got something.",
255
- "Still going — 22 minutes in. I'll follow up the moment it's done.",
256
- composeFailure({}, { failure: { human: "the working session ended unexpectedly" }, willRetry: true }),
257
- composeFailure({}, { failure: { human: "the work never came back with a result and has now outrun its time limit" }, willRetry: false }),
258
- assurance.composeInterrupted(),
259
- composeSilentSuccess({}, {}),
260
- composeSilentSuccess({}, { finalText: "Here is what I found before it died." }),
261
- ];
262
- for (const content of narrations) {
263
- const v = shouldAcknowledge({ willSpawnSession: true, item: { ...ITEM, content }, source: "inbox" });
264
- assert.equal(v.ack, false, `narrator line must be refused: "${content.slice(0, 50)}…"`);
265
- assert.equal(v.reason, "own-narrator-pattern");
266
- assert.equal(v.emitterClass, true);
267
- }
268
- });
269
-
270
- test("a human QUOTING a narrator line inside a longer message still passes (anchored full match)", () => {
271
- const content = `why did you send me this?\n\n${composeSilentSuccess({}, {})}`;
272
- const v = shouldAcknowledge({ willSpawnSession: true, item: { ...ITEM, content }, source: "inbox" });
273
- assert.equal(v.ack, true);
274
- });
275
-
276
- test("a genuine substantive ask is untouched by the gate", () => {
277
- const v = shouldAcknowledge({ willSpawnSession: true, item: ITEM, source: "inbox" });
278
- assert.equal(v.ack, true);
279
- assert.equal(v.reason, "session-dispatched");
280
- });
281
-
282
- test("every refusal is AUDITED — one JSONL row, self-echo-drop style", async () => {
283
- const { readFileSync, rmSync: rm } = await import("node:fs");
284
- const auditPath = join(ROOT, "logs", "daemon", "dispatch-gate-drops.jsonl");
285
- try { rm(auditPath, { force: true }); } catch { /* */ }
286
- shouldAcknowledge({ willSpawnSession: true, item: PEER_ACK, source: "inbox" });
287
- shouldAcknowledge({ willSpawnSession: true, item: { ...ITEM, content: composeSilentSuccess({}, {}) }, source: "inbox" });
288
- const rows = readFileSync(auditPath, "utf-8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
289
- assert.equal(rows.length, 2);
290
- assert.equal(rows[0].stage, "dispatch-gate");
291
- assert.equal(rows[0].reason, "peer-ack-shaped");
292
- assert.equal(rows[0].sender, "Piotr Novak");
293
- assert.equal(rows[1].reason, "own-narrator-pattern");
294
- });
295
- });
296
-
297
- // ---------------------------------------------------------------------------
298
- // 2. THE ACKNOWLEDGEMENT — one bespoke generated line, or SILENCE. Never a
299
- // canned rotation (CEO directive, 2026-08-30).
300
- // ---------------------------------------------------------------------------
301
-
302
- describe("generateAck — bespoke line or silence, never boilerplate", () => {
303
- test("the model's line is used verbatim once stripped to one clean line", async () => {
304
- const a = await generateAck(ITEM, CLASS_COMPLEX, {
305
- runImpl: async () => '```\n"Taking the poller fixes now — push coming."\n```',
306
- });
307
- assert.equal(a, "Taking the poller fixes now — push coming.");
308
- });
309
-
310
- test("REGRESSION: a failing generator yields NULL — the caller sends NOTHING, no canned fallback", async () => {
311
- // The retired behaviour rotated "On it." / "Looking now." / "On it —
312
- // digging in now." when convenient. The CEO's verdict: a reflex string is
313
- // worse than no ack. Null is the contract for "say nothing".
314
- const a = await generateAck(ITEM, CLASS_COMPLEX, {
315
- runImpl: async () => { throw new Error("claude CLI spawn error: ENOENT"); },
316
- });
317
- assert.equal(a, null);
318
- });
319
-
320
- test("a HANGING generator is cut off at the budget and yields NULL", async () => {
321
- const a = await generateAck(ITEM, CLASS_COMPLEX, {
322
- timeoutMs: 25,
323
- runImpl: () => new Promise(() => { /* never settles */ }),
324
- });
325
- assert.equal(a, null);
326
- });
327
-
328
- test("unusable output — empty, essay-length, refusals, assistant filler — yields NULL", () => {
329
- assert.equal(sanitiseAckText(""), null);
330
- assert.equal(sanitiseAckText(" \n\n "), null);
331
- assert.equal(sanitiseAckText("x".repeat(300)), null, "a paragraph is a reply, not an ack");
332
- assert.equal(sanitiseAckText("I cannot help with that request."), null);
333
- assert.equal(sanitiseAckText("I'm here to help! Let me know if you need anything else."), null);
334
- });
335
-
336
- test("multi-line output is reduced to the FIRST usable line", () => {
337
- assert.equal(
338
- sanitiseAckText("Taking the poller fixes now.\nsecond line that must not leak"),
339
- "Taking the poller fixes now.",
340
- );
341
- });
342
-
343
- test("REGRESSION: assistant-y openers are rejected — 'Sure — here it is:' is a chatbot, not a colleague", () => {
344
- for (const t of [
345
- "Sure — here it is:",
346
- "Sure, taking a look at the git fixes.",
347
- "Certainly! On the deck now.",
348
- "Absolutely — picking the poller fix up.",
349
- "Happy to — starting on the memo.",
350
- "Of course, I'll dig into the runway numbers.",
351
- ]) {
352
- assert.equal(sanitiseAckText(t), null, `"${t}" must be rejected`);
353
- }
354
- // …while a plain colleague line still passes untouched.
355
- assert.equal(sanitiseAckText("Taking the git fixes now — push coming."), "Taking the git fixes now — push coming.");
356
- });
357
-
358
- test("REGRESSION: the canned rotation is GONE from the module, not merely bypassed", async () => {
359
- const { readFileSync } = await import("node:fs");
360
- const src = readFileSync(new URL("./assurance.mjs", import.meta.url), "utf-8");
361
- // The three retired strings may survive in comments as history, but never
362
- // as a string-literal ARRAY a code path could pick from.
363
- assert.doesNotMatch(src, /\[\s*"On it\.",/, "the canned rotation array must not exist");
364
- });
365
- });
366
-
367
- describe("composeSilentSuccess — a finished answer is delivered, not silently clipped", () => {
368
- const LIMIT = SILENT_SUCCESS_EXCERPT_LIMIT;
369
-
370
- test("a normal multi-point answer (over the old 900 cap) comes through WHOLE", () => {
371
- // The exact failure mode the old cap produced: a substantive multi-point
372
- // answer, ~1200 chars — comfortably over the old 900 cap, well under the new
373
- // budget. The reader must get every word, with no ellipsis and no marker.
374
- const answer =
375
- "Queue placement. A discrete task I own lands cleanest in action-stack.yaml, " +
376
- "not follow-ups.yaml — the latter is external commitments with due dates, and " +
377
- "this is a capture I own, not a promise to a counterparty. ".repeat(16).trim();
378
- assert.ok(answer.length > 900 && answer.length < LIMIT, "fixture must straddle old cap / new budget");
379
- const out = composeSilentSuccess({}, { finalText: answer });
380
- assert.ok(out.includes(answer), "the full answer must be present verbatim");
381
- assert.ok(!out.includes("…"), "a whole answer carries no truncation ellipsis");
382
- });
383
-
384
- test("a genuinely huge output is bounded but truncated HONESTLY — count + resend offer, never a bare ellipsis", () => {
385
- const huge = "x".repeat(LIMIT + 2000);
386
- const out = composeSilentSuccess({}, { finalText: huge });
387
- assert.ok(out.length < huge.length, "oversized output is bounded, not dumped whole");
388
- assert.ok(out.includes(String(huge.length)), "must state the true full length so nothing is lost silently");
389
- assert.match(out, /resend|full/i, "must offer a path to the dropped remainder");
390
- });
391
-
392
- test("REGRESSION: a truncated recovery note is STILL recognised as the daemon's own narrator line (self-echo gate holds)", () => {
393
- // If truncation broke narrator-recognition, the daemon would re-ingest and
394
- // re-ack its own rescue message — the exact self-echo failure the gate exists
395
- // to prevent. The honesty marker must live inside the variable excerpt slot.
396
- const huge = "y".repeat(LIMIT + 2000);
397
- const truncated = composeSilentSuccess({}, { finalText: huge });
398
- assert.equal(isOwnNarratorText(truncated), true, "truncated silent-success must match a narrator pattern");
399
- });
400
- });
401
-
402
- // ---------------------------------------------------------------------------
403
- // 3. FAILURE CLASSIFICATION — retry the transient, stop on the permanent
404
- // ---------------------------------------------------------------------------
405
-
406
- describe("classifyFailure", () => {
407
- const cases = [
408
- [{ code: 143, error: "SIGTERM" }, true, "timeout"],
409
- [{ code: 137 }, true, "killed"],
410
- [{ code: 1, error: "rate limit exceeded (429)" }, true, "rate_limited"],
411
- [{ code: null, error: "claude CLI spawn error: ENOENT" }, false, "spawn_failed"],
412
- [{ code: 1, error: "essential-only: daily budget cap reached" }, false, "budget_refused"],
413
- [{ code: 1, error: "low free memory (need >=3686MB)" }, true, "resource_starved"],
414
- [{ code: 0 }, false, "silent_success"],
415
- ];
416
- for (const [input, transient, label] of cases) {
417
- test(`${label} → ${transient ? "retry" : "stop"}`, () => {
418
- const f = classifyFailure(input);
419
- assert.equal(f.label, label);
420
- assert.equal(f.transient, transient);
421
- assert.ok(f.human.length > 10, "every cause must have plain-English wording for the human");
422
- assert.doesNotMatch(f.human, /error occurred|something went wrong/i, "no content-free apologies");
423
- });
424
- }
425
- });
426
-
427
- // ---------------------------------------------------------------------------
428
- // 4. THE LEDGER — a debt survives the send failing
429
- // ---------------------------------------------------------------------------
430
-
431
- describe("openAndAcknowledge", () => {
432
- test("POLICY: opening a debt says NOTHING — the interim belongs to the sweep", async () => {
433
- // 3,069 opening acknowledgements in fourteen days were exactly this call
434
- // speaking at t=0. The record is still written first (a crash between here
435
- // and the session's first token must leave a debt the sweep finds); what is
436
- // gone is the reflex in front of it.
437
- const t = fakeTransport();
438
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, service: "cohort", tier: "work", deps: { ackSender: t.ackSender } });
439
- assert.equal(t.sent.length, 0, "not one byte at open time");
440
- assert.equal(r.acked, false);
441
- assert.equal(r.ackText, null);
442
- assert.equal(r.reason, "tier-work-deferred");
443
- const rec = readObligation(r.key);
444
- assert.equal(rec.state, "open", "the debt is open and owed");
445
- assert.equal(rec.tier, "work");
446
- assert.equal(rec.interimSaid, false, "and the one-interim latch is still unspent");
447
- });
448
-
449
- test("the answer tier opens the debt and forbids any interim, ever", async () => {
450
- const t = fakeTransport();
451
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "answer", ack: false, deps: { ackSender: t.ackSender } });
452
- assert.equal(t.sent.length, 0);
453
- const rec = readObligation(r.key);
454
- assert.equal(rec.interimForbidden, true, "durable on the record — the sweep runs in another tick, maybe another process");
455
- const after = await sweepObligations({
456
- now: Date.now() + ACK_AFTER_MS + 60_000,
457
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
458
- });
459
- assert.equal(after.acked, 0);
460
- assert.equal(t.sent.length, 0, "the answer tier is silence, at open and at every sweep after it");
461
- });
462
-
463
- test("REGRESSION: an un-acknowledged debt is still visible to the sweep", async () => {
464
- // The original production case: 20 of 32 acknowledgements died and the old
465
- // code did `logResponse(...); return {sent:false}` — the human was never
466
- // told, and nothing ever retried. Nothing is sent at open time any more, so
467
- // what this now pins is the half that still matters: the debt exists.
468
- const t = fakeTransport({ failTimes: 99, error: "claude CLI timed out after 60000ms" });
469
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work", deps: { ackSender: t.ackSender } });
470
- assert.equal(r.acked, false);
471
- const rec = readObligation(r.key);
472
- assert.equal(rec.acknowledged, false);
473
- assert.equal(rec.state, "open");
474
- assert.equal(openObligations().length, 1, "the debt must be visible to the sweep");
475
- });
476
-
477
- test("REGRESSION: a generation failure in the interim branch sends NOTHING and records why", async () => {
478
- // The ack generator's contract on failure is silence, never a placeholder.
479
- // The branch moved from open time to the sweep; the contract did not.
480
- const t = fakeTransport();
481
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
482
- await sweepObligations({
483
- now: Date.now() + ACK_AFTER_MS + 1000,
484
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => null },
485
- });
486
- assert.equal(t.sent.length, 0, "not one byte reached the human");
487
- const rec = readObligation(r.key);
488
- assert.equal(rec.acknowledged, false);
489
- assert.equal(rec.ackGenFailures, 1, "the failure is counted, so the retry is bounded");
490
- assert.equal(rec.state, "open", "the debt survives — the failure and stale notices still owe the human");
491
- });
492
-
493
- test("the stored summary keeps the classifier's casing — the board row and escalations read from it", async () => {
494
- // `rec.summary` feeds board-mirror's row title and the needs-attention
495
- // escalation record. A summary mangled at open time would resurface there.
496
- // It must NOT, however, appear in any message the human reads.
497
- const t = fakeTransport();
498
- const r = await openAndAcknowledge({
499
- item: ITEM,
500
- classResult: { ...CLASS_COMPLEX, summary: "CEO checking in on work progress" },
501
- deps: { ackSender: t.ackSender },
502
- });
503
- const rec = readObligation(r.key);
504
- assert.equal(rec.summary, "CEO checking in on work progress");
505
- // It must not leak into anything a human reads. Every deterministic notice
506
- // the module can still compose is checked, so a future composer that starts
507
- // interpolating `rec.summary` fails here.
508
- for (const text of [
509
- composeFailure(rec, { failure: { human: "the work ran past its time limit and got cut off" }, willRetry: true }),
510
- composeFailure(rec, { failure: { human: "the work never came back with a result" }, willRetry: false }),
511
- assurance.composeInterrupted(rec),
512
- composeSilentSuccess(rec, {}),
513
- ]) {
514
- assert.doesNotMatch(text, /CEO checking in on work progress/, "the summary is internal framing, never copy");
515
- }
516
- });
517
-
518
- test("speaks ONCE — a re-delivered item, and a RETRY that reopens the key, do not re-announce", async () => {
519
- // The retry half is R4: a failed obligation is CLOSED, the item is
520
- // re-delivered by the next poll, and a brand-new record opens under the
521
- // same key. Without inheritance that record starts clean and acks from
522
- // scratch — which is `On it.` arriving immediately after a failure notice,
523
- // as sampled live.
524
- const t = fakeTransport();
525
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
526
- await sweepObligations({
527
- now: Date.now() + ACK_AFTER_MS + 1000,
528
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
529
- });
530
- assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1);
531
- assert.equal(readObligation(r.key).interimSaid, true);
532
-
533
- assurance.closeObligation(r.key, { outcome: "failed" });
534
- const again = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
535
- assert.equal(readObligation(again.key).interimSaid, true, "the latch is INHERITED across the reopen");
536
- await sweepObligations({
537
- now: Date.now() + ACK_AFTER_MS + 10 * 60_000,
538
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
539
- });
540
- assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1, "one interim per ask, across every attempt at it");
541
- });
542
-
543
- test("opens NO board row of its own — there is exactly one writer per ask", async () => {
544
- // It used to mirror the obligation onto the board here, through
545
- // `board.create`, while agent-daemon tracked the SAME item forty lines
546
- // later through the shared ledger. One DM, two cards on one board, and the
547
- // one the "On now" banner rendered was the copy with no requester, no
548
- // comments and no provenance. The open belongs to the caller, which holds
549
- // the full inbox item; this function only owes the acknowledgement.
550
- const mirror = { calls: [], closeBoardItem: async (a) => { mirror.calls.push(a); return { mirrored: true }; } };
551
- assurance._setBoardMirror(mirror);
552
- try {
553
- const t = fakeTransport();
554
- await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
555
- await new Promise((r) => setTimeout(r, 5));
556
- assert.equal(mirror.calls.length, 0);
557
- assert.equal(typeof mirror.openBoardItem, "undefined", "there is no open hook to call");
558
- } finally {
559
- assurance._setBoardMirror(null);
560
- }
561
- });
562
-
563
- test("closing the debt retires the board row, carrying the obligation's own verdict", async () => {
564
- // `closeObligation` is the funnel EVERY terminal path goes through, and only
565
- // two of them reach the daemon's onClose. A row nothing retires sits in
566
- // `running`, which is the one column the "On now" banner reads.
567
- const mirror = { calls: [], closeBoardItem: async (a) => { mirror.calls.push(a); return { mirrored: true }; } };
568
- assurance._setBoardMirror(mirror);
569
- try {
570
- const t = fakeTransport();
571
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
572
- assurance.closeObligation(r.key, { outcome: "failed", note: "stale-no-outcome" });
573
- await new Promise((res) => setTimeout(res, 5));
574
- assert.equal(mirror.calls.length, 1);
575
- assert.equal(mirror.calls[0].outcome, "failed");
576
- assert.equal(mirror.calls[0].rec.key, r.key);
577
- assert.ok(mirror.calls[0].rec.item, "the close needs the item to identify the ask");
578
- } finally {
579
- assurance._setBoardMirror(null);
580
- }
581
- });
582
- });
583
-
584
- // ---------------------------------------------------------------------------
585
- // 5. SESSION SETTLEMENT — the four terminal states
586
- // ---------------------------------------------------------------------------
587
-
588
- describe("settleSession", () => {
589
- async function openOne(t) {
590
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
591
- return r.key;
592
- }
593
-
594
- test("exit 0 AND the session spoke → discharge quietly, add nothing", async () => {
595
- const t = fakeTransport();
596
- const key = await openOne(t);
597
- const before = t.sent.length;
598
- const r = await settleSession({ key, ok: true, code: 0, deps: { deliverImpl: t.impl, spokeSinceImpl: () => true } });
599
- assert.equal(r.verdict, "answered-by-session");
600
- assert.equal(t.sent.length, before, "the human already has their answer — saying more is spam");
601
- assert.equal(readObligation(key).state, "answered");
602
- });
603
-
604
- test("REGRESSION: exit 0 and the session said NOTHING → the daemon speaks", async () => {
605
- // Observed live: session s-1786600074079-52, exit 0, 241s, $2.36, its own
606
- // result text ending "Nothing was sent." Marked processed forever, emitted
607
- // as `sent`, never re-delivered. This is that case.
608
- const t = fakeTransport();
609
- const key = await openOne(t);
610
- const stdout = JSON.stringify({ type: "result", result: "I reviewed the three failing checks and pushed fixes for two. The third needs a decision from you. Nothing was sent." });
611
- const r = await settleSession({ key, ok: true, code: 0, stdout, deps: { deliverImpl: t.impl, spokeSinceImpl: () => false } });
612
- assert.equal(r.verdict, "silent-success");
613
- assert.equal(r.spoke, true);
614
- const msg = t.sent.at(-1).text;
615
- assert.match(msg, /didn't get a reply out to you/i);
616
- assert.match(msg, /pushed fixes for two/, "the work the session actually did must reach the human");
617
- assert.equal(readObligation(key).state, "answered");
618
- });
619
-
620
- test("an acknowledgement does NOT count as the session having answered", async () => {
621
- const t = fakeTransport();
622
- const key = await openOne(t);
623
- // A truthful receipt store that only ever saw the ack.
624
- const spokeSinceImpl = ({ excludeKinds }) => !(excludeKinds || []).includes("ack") ;
625
- const r = await settleSession({ key, ok: true, code: 0, stdout: "", deps: { deliverImpl: t.impl, spokeSinceImpl } });
626
- assert.equal(r.verdict, "silent-success", "'I'm on it' is not an answer");
627
- });
628
-
629
- test("transient failure with retries left → tell them, keep the debt, retry", async () => {
630
- const t = fakeTransport();
631
- const key = await openOne(t);
632
- const r = await settleSession({ key, ok: false, code: 143, error: "SIGTERM", deps: { deliverImpl: t.impl, spokeSinceImpl: () => false } });
633
- assert.equal(r.willRetry, true);
634
- assert.match(r.verdict, /^retrying:timeout$/);
635
- const msg = t.sent.at(-1).text;
636
- assert.match(msg, /ran past its time limit/i);
637
- assert.match(msg, /retrying now/i);
638
- assert.equal(readObligation(key).state, "open", "still owed — the retry has to be covered too");
639
- });
640
-
641
- test("permanent failure → tell them, stop, escalate durably", async () => {
642
- const t = fakeTransport();
643
- const key = await openOne(t);
644
- const r = await settleSession({ key, ok: false, code: 1, error: "claude CLI spawn error: ENOENT", deps: { deliverImpl: t.impl, spokeSinceImpl: () => false } });
645
- assert.equal(r.willRetry, false);
646
- assert.match(r.verdict, /^failed:spawn_failed$/);
647
- const msg = t.sent.at(-1).text;
648
- assert.match(msg, /couldn't finish this/i);
649
- assert.match(msg, /stopped retrying/i);
650
- assert.match(msg, /\?$/, "a dead end must end in a question, not a shrug");
651
- assert.equal(readObligation(key).state, "failed");
652
- });
653
-
654
- test("retries are BOUNDED — the second timeout does not promise a third attempt", async () => {
655
- const t = fakeTransport();
656
- const key = await openOne(t);
657
- // Simulate having already burned the allowed attempts.
658
- const rec = readObligation(key);
659
- rec.attempts = RETRY_MAX + 1;
660
- const { writeFileSync } = await import("node:fs");
661
- writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(key)}.json`), JSON.stringify(rec));
662
- const r = await settleSession({ key, ok: false, code: 143, error: "SIGTERM", deps: { deliverImpl: t.impl, spokeSinceImpl: () => false } });
663
- assert.equal(r.willRetry, false, "an unbounded silent retry loop is itself the defect");
664
- assert.match(t.sent.at(-1).text, /stopped retrying/i);
665
- });
666
- });
667
-
668
- // ---------------------------------------------------------------------------
669
- // 6. THE SWEEP — the safety net under everything above
670
- // ---------------------------------------------------------------------------
671
-
672
- describe("sweepObligations", () => {
673
- test("THE ONE INTERIM: nothing before ACK_AFTER_MS, then exactly one bespoke line", async () => {
674
- const t = fakeTransport();
675
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
676
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => "Back on the git fixes now." };
677
-
678
- const early = await sweepObligations({ now: Date.now() + ACK_AFTER_MS - 1000, deps });
679
- assert.equal(early.acked, 0);
680
- assert.equal(t.sent.length, 0, "below ninety seconds the typing indicator IS the acknowledgement");
681
-
682
- const stats = await sweepObligations({ now: Date.now() + ACK_AFTER_MS + 1000, deps });
683
- assert.equal(stats.acked, 1);
684
- assert.equal(t.sent[0].kind, "ack");
685
- assert.equal(t.sent[0].text, "Back on the git fixes now.");
686
- assert.doesNotMatch(t.sent[0].text, /^(on it|looking now)/i, "never the retired rotation");
687
- assert.equal(readObligation(r.key).interimSaid, true);
688
-
689
- // …and never a second one, however long the work runs.
690
- for (let m = 2; m < 40; m++) {
691
- const now = Date.now() + ACK_AFTER_MS + m * 60_000;
692
- if (now - Date.now() >= STALE_AFTER_MS) break;
693
- await sweepObligations({ now, deps });
694
- }
695
- assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1, "one interim per obligation, full stop");
696
- });
697
-
698
- test("ONE SEAT, eight concurrent obligations in ONE room, exactly ONE interim", async () => {
699
- // WHAT THIS IS AND IS NOT. It is the per-seat guarantee: one daemon holding
700
- // eight concurrent debts in one channel emits one holding line, not eight.
701
- //
702
- // It is NOT `capital-formation` reproduced, and the title used to say it
703
- // was. That room's 9,187 messages in 21 days came from EIGHT SEPARATE
704
- // SEATS, each with its own AGENT_DIR on its own machine and therefore its
705
- // own ledger — see room-budget.test.mjs, "the budget is per (SEAT, service,
706
- // channel)". This gate's ceiling in that room is 8 x (60/15) = 32/h against
707
- // a measured content-free rate of ~14/h, so it would not have been the
708
- // binding constraint there. What removed the measured traffic is the
709
- // deleted progress branch (961 -> 0), the sanitiser, and TIME+ONCE; this
710
- // gate is the backstop that bounds what survives all three.
711
- const t = fakeTransport();
712
- for (let i = 0; i < 8; i++) {
713
- await openAndAcknowledge({
714
- item: { ...ITEM, id: `msg-${i}`, raw_ref: `cohort:capform:${i}`, message_id: `${i}` },
715
- classResult: CLASS_COMPLEX,
716
- tier: "work",
717
- });
718
- }
719
- assert.equal(openObligations().length, 8, "every ask is still opened and tracked");
720
-
721
- const stats = await sweepObligations({
722
- now: Date.now() + ACK_AFTER_MS + 1000,
723
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
724
- });
725
- assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1, `eight obligations, one message — got ${t.sent.length}`);
726
- assert.equal(stats.acked, 1);
727
- assert.equal(stats.suppressed, 7, "…and the seven suppressions are COUNTED, so a quiet sweep can still prove it decided");
728
-
729
- // Over budget is not a drop: every debt is still open and still owed.
730
- assert.equal(openObligations().length, 8);
731
- });
732
-
733
- test("…and the suppressed debts stay silent afterwards rather than queueing up a burst", async () => {
734
- const t = fakeTransport();
735
- for (let i = 0; i < 8; i++) {
736
- await openAndAcknowledge({ item: { ...ITEM, id: `m-${i}`, raw_ref: `cohort:capform2:${i}` }, classResult: CLASS_COMPLEX, tier: "work" });
737
- }
738
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
739
- await sweepObligations({ now: Date.now() + ACK_AFTER_MS + 1000, deps });
740
- await sweepObligations({ now: Date.now() + 20 * 60_000, deps });
741
- await sweepObligations({ now: Date.now() + 40 * 60_000, deps });
742
- assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1, "the window reopening must not release a backlog");
743
- });
744
-
745
- test("a DIFFERENT room is never gagged by a busy one", async () => {
746
- const t = fakeTransport();
747
- for (let i = 0; i < 5; i++) {
748
- await openAndAcknowledge({ item: { ...ITEM, id: `b-${i}`, raw_ref: `cohort:busy:${i}` }, classResult: CLASS_COMPLEX, tier: "work" });
749
- }
750
- await openAndAcknowledge({
751
- item: { ...ITEM, id: "quiet-1", raw_ref: "cohort:quiet:1", channel_id: "C-QUIET" },
752
- classResult: CLASS_COMPLEX,
753
- tier: "work",
754
- });
755
- const stats = await sweepObligations({
756
- now: Date.now() + ACK_AFTER_MS + 1000,
757
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
758
- });
759
- assert.equal(stats.acked, 2, "one per room");
760
- assert.equal(new Set(t.sent.map((m) => m.channel)).size, 2);
761
- });
762
-
763
- test("REGRESSION: a failed generation sends NOTHING — bounded, then given up on", async () => {
764
- const t = fakeTransport();
765
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
766
-
767
- let genCalls = 0;
768
- const t0 = Date.now();
769
- for (let m = 1; m <= 8; m++) {
770
- await sweepObligations({ now: t0 + ACK_AFTER_MS + m * 60_000, deps: {
771
- deliverImpl: t.impl,
772
- spokeSinceImpl: () => false,
773
- generateAckImpl: async () => { genCalls++; return null; },
774
- } });
775
- }
776
- assert.equal(t.sent.filter((s) => s.kind === "ack").length, 0, "no canned line went out in place of the failed generation");
777
- assert.ok(genCalls <= ACK_GEN_MAX_FAILURES, `generation retries are bounded, not once-a-tick forever (saw ${genCalls})`);
778
- const rec = readObligation(r.key);
779
- assert.equal(rec.ackSuppressed, true, "the interim is given up on…");
780
- assert.equal(rec.state, "open", "…but the DEBT is not — the stale check still owes the human");
781
- });
782
-
783
- test("a failed generation does NOT spend the room's budget", async () => {
784
- // Ask, compose, THEN spend. A budget burned on a message that was never
785
- // composed gags the room for fifteen minutes in exchange for nothing.
786
- const t = fakeTransport();
787
- await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
788
- await openAndAcknowledge({ item: { ...ITEM, id: "x2", raw_ref: "cohort:same-room:2" }, classResult: CLASS_COMPLEX, tier: "work" });
789
- let calls = 0;
790
- const stats = await sweepObligations({
791
- now: Date.now() + ACK_AFTER_MS + 1000,
792
- deps: {
793
- deliverImpl: t.impl,
794
- spokeSinceImpl: () => false,
795
- // The first obligation's generation fails; the second's succeeds.
796
- generateAckImpl: async () => (++calls === 1 ? null : "Back on the git fixes now."),
797
- },
798
- });
799
- assert.equal(stats.acked, 1, "the room's one interim went to the ask that could actually compose one");
800
- assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1);
801
- });
802
-
803
- test("work that outruns every session timeout is declared dead, not left hanging", async () => {
804
- const t = fakeTransport();
805
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
806
- const now = Date.now() + STALE_AFTER_MS + 1000;
807
- const stats = await sweepObligations({ now, deps: { deliverImpl: t.impl, spokeSinceImpl: () => false } });
808
- assert.equal(stats.staled, 1);
809
- assert.match(t.sent.at(-1).text, /outrun its time limit/i);
810
- assert.equal(readObligation(r.key).state, "failed");
811
- });
812
-
813
- test("a debt the session has since answered is discharged silently", async () => {
814
- const t = fakeTransport();
815
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
816
- const before = t.sent.length;
817
- const now = Date.now() + STALE_AFTER_MS + 1000;
818
- const stats = await sweepObligations({ now, deps: { deliverImpl: t.impl, spokeSinceImpl: () => true } });
819
- assert.equal(stats.closed, 1);
820
- assert.equal(t.sent.length, before, "no message when the human already has their answer");
821
- assert.equal(readObligation(r.key).state, "answered");
822
- });
823
-
824
- test("a debt orphaned by a DEAD daemon is spoken to", async () => {
825
- const t = fakeTransport();
826
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
827
- // Rewrite the record as if a now-dead process had opened it.
828
- const rec = readObligation(r.key);
829
- rec.daemonPid = process.pid + 99999;
830
- const { writeFileSync } = await import("node:fs");
831
- writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
832
-
833
- const stats = await sweepObligations({
834
- now: Date.now() + INTERRUPT_GRACE_MS + 1000,
835
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
836
- });
837
- assert.equal(stats.interrupted, 1);
838
- assert.match(t.sent.at(-1).text, /was interrupted before it finished/i);
839
- assert.equal(t.sent.at(-1).kind, "notice", "an interrupted notice is an OUTCOME, not a progress ping — the kind it used to borrow no longer exists");
840
- });
841
-
842
- test("…and a SECOND interruption of the same ask says nothing more", async () => {
843
- // Said once per (channel, obligationKey). Two daemon restarts over one long
844
- // ask are one apology, not two — and a retry that reopens the same key
845
- // inherits the notice ledger, so it is not two either.
846
- const t = fakeTransport();
847
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
848
- const { writeFileSync } = await import("node:fs");
849
- const orphan = (extra = {}) => {
850
- const rec = { ...readObligation(r.key), daemonPid: process.pid + 99999, ...extra };
851
- writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
852
- };
853
- orphan();
854
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
855
- await sweepObligations({ now: Date.now() + INTERRUPT_GRACE_MS + 1000, deps });
856
- assert.equal(t.sent.filter((m) => /interrupted/i.test(m.text)).length, 1);
857
-
858
- orphan({ interruptedNotifiedAt: null });
859
- const second = await sweepObligations({ now: Date.now() + INTERRUPT_GRACE_MS + 120_000, deps });
860
- assert.equal(second.interrupted, 0);
861
- assert.equal(t.sent.filter((m) => /interrupted/i.test(m.text)).length, 1, "one apology per ask, per room");
862
- });
863
-
864
- test("REGRESSION: a LIVE daemon's obligation is never declared interrupted", async () => {
865
- // Two daemons on one AGENT_DIR is what an operator creates the moment they
866
- // run one by hand next to the launchd one. Each sees the other's healthy,
867
- // actively-running debts as foreign. With only a pid-mismatch test — which
868
- // is what shipped — both then tell those requesters "my session was
869
- // interrupted" while the work runs to completion behind the apology.
870
- const t = fakeTransport();
871
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
872
- const rec = readObligation(r.key);
873
- rec.daemonPid = process.pid + 99999;
874
- const { writeFileSync } = await import("node:fs");
875
- writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
876
-
877
- const before = t.sent.length;
878
- const stats = await sweepObligations({
879
- now: Date.now() + INTERRUPT_GRACE_MS + 1000,
880
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => true, generateAckImpl: async () => FAKE_ACK_LINE },
881
- });
882
- assert.equal(stats.interrupted, 0, "the owning daemon is alive — its work is not interrupted");
883
- assert.equal(t.sent.length, before, "and nobody is told a lie about it");
884
- });
885
-
886
- test("REGRESSION: a FRESH obligation is never declared interrupted, dead pid or not", async () => {
887
- // The age guard is the backstop for a recycled pid. One second after the ask
888
- // arrived, no reading of the evidence supports "your session was interrupted".
889
- const t = fakeTransport();
890
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
891
- const rec = readObligation(r.key);
892
- rec.daemonPid = process.pid + 99999;
893
- const { writeFileSync } = await import("node:fs");
894
- writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
895
-
896
- const stats = await sweepObligations({
897
- now: Date.now() + 1000,
898
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
899
- });
900
- assert.equal(stats.interrupted, 0);
901
- });
902
-
903
- test("REGRESSION: a debt OWNED by another LIVE daemon is not ack-retried by this one (two-daemon narration)", async () => {
904
- // Two live daemons on one AGENT_DIR both sweep the same ledger. The one
905
- // that did NOT open this debt must not compensate its acknowledgement —
906
- // the owner is alive and will; two daemons acking is one human hearing
907
- // double.
908
- const failing = fakeTransport({ failTimes: 99 });
909
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work", deps: { ackSender: failing.ackSender } });
910
- assert.equal(r.acked, false);
911
- const rec = readObligation(r.key);
912
- rec.daemonPid = process.pid + 99999;
913
- const { writeFileSync } = await import("node:fs");
914
- writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
915
-
916
- const working = fakeTransport();
917
- const stats = await sweepObligations({
918
- now: Date.now() + ACK_AFTER_MS + 1000,
919
- deps: {
920
- deliverImpl: working.impl,
921
- spokeSinceImpl: () => false,
922
- generateAckImpl: async () => "Back on this now.",
923
- isPidAliveImpl: () => true, // the owner is provably alive
924
- },
925
- });
926
- assert.equal(stats.acked, 0, "the live owner narrates its own debt");
927
- assert.equal(working.sent.length, 0, "not one byte from the non-owner");
928
- assert.equal(readObligation(r.key).acknowledged, false);
929
- assert.equal(readObligation(r.key).interimSaid, false, "and the non-owner does not spend the latch either");
930
- });
931
-
932
- test("…but once the owning daemon is DEAD, this daemon takes the interim over", async () => {
933
- const failing = fakeTransport({ failTimes: 99 });
934
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work", deps: { ackSender: failing.ackSender } });
935
- const rec = readObligation(r.key);
936
- rec.daemonPid = process.pid + 99999;
937
- const { writeFileSync } = await import("node:fs");
938
- writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
939
-
940
- const working = fakeTransport();
941
- const stats = await sweepObligations({
942
- now: Date.now() + ACK_AFTER_MS + 1000,
943
- deps: {
944
- deliverImpl: working.impl,
945
- spokeSinceImpl: () => false,
946
- generateAckImpl: async () => "Picking your ask up now.",
947
- isPidAliveImpl: () => false, // the owner is gone
948
- },
949
- });
950
- assert.equal(stats.acked, 1, "an orphaned debt is this daemon's to speak to");
951
- assert.equal(readObligation(r.key).acknowledged, true);
952
- });
953
-
954
- test("REGRESSION: a debt OWNED by another LIVE daemon gets no interim from this one", async () => {
955
- const t = fakeTransport();
956
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
957
- const rec = readObligation(r.key);
958
- rec.daemonPid = process.pid + 99999;
959
- const { writeFileSync } = await import("node:fs");
960
- writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
961
-
962
- const stats = await sweepObligations({
963
- now: Date.now() + ACK_AFTER_MS + 1000,
964
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => true, generateAckImpl: async () => FAKE_ACK_LINE },
965
- });
966
- assert.equal(stats.acked, 0, "the live owner speaks for its own debt");
967
- assert.equal(stats.interrupted, 0, "and a live owner is certainly not 'interrupted'");
968
- assert.equal(t.sent.length, 0);
969
- });
970
-
971
- test("…and a dead owner's debt DOES get this daemon's interim (after the interrupted notice is done)", async () => {
972
- const t = fakeTransport();
973
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
974
- const rec = readObligation(r.key);
975
- rec.daemonPid = process.pid + 99999;
976
- rec.interruptedNotifiedAt = Date.now(); // the orphan apology already went out
977
- const { writeFileSync } = await import("node:fs");
978
- writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
979
-
980
- const stats = await sweepObligations({
981
- now: Date.now() + ACK_AFTER_MS + 1000,
982
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false, generateAckImpl: async () => "Back on the git fixes now." },
983
- });
984
- assert.equal(stats.acked, 1);
985
- assert.equal(t.sent.at(-1).text, "Back on the git fixes now.");
986
- });
987
-
988
- test("each distinct message carries a distinct idempotency suffix", async () => {
989
- // hq de-dups server-side on the client message id, so two messages that key
990
- // on the same suffix are ONE message as far as hq is concerned and the
991
- // second is silently swallowed — "never silent" degrading into "spoke once,
992
- // then silent forever". Invisible to any test that stubs transport without
993
- // inspecting the options, hence this one.
994
- const seen = [];
995
- const impl = async (item, text, o = {}) => { seen.push(o.idempotencySuffix); return { sent: true, via: "fake", channel: "c" }; };
996
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
997
-
998
- const deps = { deliverImpl: impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
999
- await sweepObligations({ now: Date.now() + ACK_AFTER_MS + 1000, deps }); // the interim
1000
- await settleSession({ key: r.key, ok: false, code: 143, error: "SIGTERM", deps }); // the failure notice
1001
- await sweepObligations({ now: Date.now() + STALE_AFTER_MS + 1000, deps }); // the stale notice
1002
-
1003
- assert.ok(seen.length >= 2, `expected several distinct sends, saw ${seen.length}`);
1004
- assert.equal(new Set(seen).size, seen.length, `suffixes must be unique per message, got ${JSON.stringify(seen)}`);
1005
- });
1006
-
1007
- test("emits at most ONE message per obligation per tick", async () => {
1008
- const t = fakeTransport();
1009
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1010
- // Old enough to qualify for BOTH the interrupted notice and the interim.
1011
- const rec = readObligation(r.key);
1012
- rec.daemonPid = process.pid + 99999;
1013
- const { writeFileSync } = await import("node:fs");
1014
- writeFileSync(join(ROOT, "state", "obligations", `${assurance.sanitiseKey(r.key)}.json`), JSON.stringify(rec));
1015
- const now = Date.now() + INTERRUPT_GRACE_MS + 1000;
1016
- await sweepObligations({ now, deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, isPidAliveImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE } });
1017
- assert.equal(t.sent.length, 1, "two messages in one tick reads as a malfunction");
1018
- });
1019
- });
1020
-
1021
- // ---------------------------------------------------------------------------
1022
- // 7. RECEIPTS — the evidence the whole guarantee rests on
1023
- // ---------------------------------------------------------------------------
1024
-
1025
- describe("delivery receipts", () => {
1026
- test("a recorded send is visible to a later spokeSince query", () => {
1027
- const now = Date.now();
1028
- receipts.recordOutbound({ service: "cohort", channel: "C-RECEIPT-1", kind: "reply", chars: 12, agentRoot: ROOT, now });
1029
- assert.equal(receipts.spokeSince({ channel: "C-RECEIPT-1", sinceMs: now - 1000, agentRoot: ROOT }), true);
1030
- });
1031
-
1032
- test("acks and progress pings are excludable — they are not answers", () => {
1033
- const now = Date.now();
1034
- receipts.recordOutbound({ service: "cohort", channel: "C-RECEIPT-2", kind: "ack", agentRoot: ROOT, now });
1035
- receipts.recordOutbound({ service: "cohort", channel: "C-RECEIPT-2", kind: "progress", agentRoot: ROOT, now });
1036
- assert.equal(
1037
- receipts.spokeSince({ channel: "C-RECEIPT-2", sinceMs: now - 1000, excludeKinds: ["ack", "progress", "failure"], agentRoot: ROOT }),
1038
- false,
1039
- );
1040
- });
1041
-
1042
- test("REGRESSION: an 'interrupted' notice is not an answer either", async () => {
1043
- // The interrupted apology moved from kind "progress" to kind "notice" when
1044
- // progress pings were deleted. A kind that is not in the non-answer set
1045
- // reads to `spokeFor` as "the human was spoken to", which would discharge
1046
- // an unanswered debt the moment a daemon restarted — the exact class of bug
1047
- // the receipt ledger exists to prevent.
1048
- const { NON_ANSWER_KINDS } = await import("../../lib/comms/receipts.mjs");
1049
- assert.ok(NON_ANSWER_KINDS.includes("notice"), "every courtesy kind the daemon can emit belongs in the non-answer set");
1050
- const now = Date.now();
1051
- receipts.recordOutbound({ service: "cohort", channel: "C-NOTICE", kind: "notice", agentRoot: ROOT, now });
1052
- assert.equal(
1053
- receipts.spokeFor({ obligation: { key: "k", channel: "C-NOTICE", openedAt: now - 1000 }, now, siblingOpenCount: 1, agentRoot: ROOT }).heard,
1054
- false,
1055
- );
1056
- });
1057
-
1058
- test("every kind the daemon actually sends is classified as answer or courtesy — none left unclassified", async () => {
1059
- // A new kind that nobody adds to the set silently becomes "this discharged
1060
- // the debt" — the discharge-an-unanswered-debt bug the receipt ledger
1061
- // exists to prevent.
1062
- //
1063
- // DERIVED FROM THE EMITTERS, not hand-listed. The hand-written literal this
1064
- // replaces could not fail when a kind was added, and had ALREADY missed one:
1065
- // `plan` was being emitted by notePlan and was absent from the list. Same
1066
- // shape as narratorPatterns(), which derives its strings from the composers
1067
- // rather than restating them.
1068
- const { NON_ANSWER_KINDS } = await import("../../lib/comms/receipts.mjs");
1069
- const { readFileSync } = await import("node:fs");
1070
- const here = new URL(".", import.meta.url).pathname;
1071
- // Line-scoped, and `counters.bump` lines are skipped: a counter LABEL is
1072
- // not a delivery kind (responder.mjs tags its dedup/generation counters
1073
- // `kind: "holding"`, which never reaches a receipt). Erring toward a false
1074
- // alarm is deliberate — a spurious failure sends someone to look, a silent
1075
- // miss is the bug this test is for.
1076
- const emitted = new Set();
1077
- for (const f of ["assurance.mjs", "responder.mjs"]) {
1078
- for (const line of readFileSync(join(here, f), "utf-8").split("\n")) {
1079
- if (line.includes("counters.bump")) continue;
1080
- const m = /\bkind:\s*"([a-z-]+)"/.exec(line);
1081
- if (m) emitted.add(m[1]);
1082
- }
1083
- }
1084
- assert.ok(emitted.size >= 5, `the scan must find the emitters — found ${[...emitted].join(", ")}`);
1085
- // The ONE kind that is an answer. Everything else the daemon emits about an
1086
- // ask is a courtesy and must not discharge the debt.
1087
- const ANSWERS = new Set(["reply"]);
1088
- for (const kind of emitted) {
1089
- assert.equal(
1090
- NON_ANSWER_KINDS.includes(kind),
1091
- !ANSWERS.has(kind),
1092
- `"${kind}" is emitted by the daemon and is ${ANSWERS.has(kind) ? "an answer" : "a courtesy"} — lib/comms/receipts.mjs NON_ANSWER_KINDS must say so`,
1093
- );
1094
- }
1095
- // Pinned explicitly as well, so a scan that silently stops matching is also
1096
- // a failure rather than a green run over an empty set.
1097
- for (const kind of ["ack", "notice", "failure", "plan"]) {
1098
- assert.ok(emitted.has(kind), `"${kind}" must still be found by the scan`);
1099
- assert.ok(NON_ANSWER_KINDS.includes(kind), `"${kind}" is a courtesy`);
1100
- }
1101
- });
1102
-
1103
- test("an unreadable ledger reads as 'the human heard nothing', never as 'fine'", () => {
1104
- // The fail-open direction is deliberate: the cost of guessing wrong this way
1105
- // is one redundant message; guessing the other way is the bug being fixed.
1106
- assert.equal(receipts.spokeSince({ channel: "never-used-channel", sinceMs: Date.now(), agentRoot: ROOT }), false);
1107
- });
1108
- });
1109
-
1110
- // ---------------------------------------------------------------------------
1111
- // 7. ATTRIBUTION — a room is shared; a debt is not
1112
- // ---------------------------------------------------------------------------
1113
-
1114
- describe("attribution: which debt did that message actually discharge", () => {
1115
- const mk = (id, content) => ({
1116
- id, raw_ref: `cohort:C-SHARED:${id}`, message_id: id, service: "cohort",
1117
- channel: "C-SHARED", channel_id: "C-SHARED", sender: "Dana Whitfield", content,
1118
- });
1119
-
1120
- test("REGRESSION: one session's answer does NOT discharge a different ask in the same room", async () => {
1121
- // The owner sends two things a minute apart in one DM. Two debts, one room.
1122
- // Keyed on the room alone — which is what shipped — the first session's
1123
- // reply closed BOTH, and the second ask, which nobody answered, was recorded
1124
- // as "answered" with not one word sent about it. This is that scenario.
1125
- const t = fakeTransport();
1126
- const a = mk("ASK-A", "what's our runway?");
1127
- const b = mk("ASK-B", "also please fix the flaky poller test and push it");
1128
- const ra = await openAndAcknowledge({ item: a, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
1129
- const rb = await openAndAcknowledge({ item: b, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
1130
- assurance.noteSession(ra.key, "s-A");
1131
- assurance.noteSession(rb.key, "s-B");
1132
-
1133
- // Session A answers, stamping the receipt with the debt it was spawned for.
1134
- receipts.recordOutbound({
1135
- service: "cohort", channel: "C-SHARED", kind: "session",
1136
- obligationKey: ra.key, sessionId: "s-A", agentRoot: ROOT,
1137
- });
1138
-
1139
- const before = t.sent.length;
1140
- const settledA = await settleSession({ key: ra.key, ok: true, code: 0, deps: { deliverImpl: t.impl } });
1141
- assert.equal(settledA.verdict, "answered-by-session");
1142
- assert.equal(settledA.basis, "obligation-keyed");
1143
- assert.equal(t.sent.length, before, "A was answered — saying more would be spam");
1144
-
1145
- // B exited 0 having said nothing. It must NOT ride on A's receipt.
1146
- const settledB = await settleSession({
1147
- key: rb.key, ok: true, code: 0,
1148
- stdout: JSON.stringify({ type: "result", result: "Could not reproduce the flake." }),
1149
- deps: { deliverImpl: t.impl },
1150
- });
1151
- assert.equal(settledB.verdict, "silent-success", "B was never answered and must be rescued");
1152
- assert.match(t.sent.at(-1).text, /didn't get a reply out to you/i);
1153
- assert.match(t.sent.at(-1).text, /Could not reproduce the flake/, "and B's actual work reaches the human");
1154
- });
1155
-
1156
- test("an UNATTRIBUTED message in a room holding two open debts discharges neither", async () => {
1157
- const t = fakeTransport();
1158
- const a = mk("AMB-A", "first ask");
1159
- const b = mk("AMB-B", "second ask");
1160
- const ra = await openAndAcknowledge({ item: a, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
1161
- await openAndAcknowledge({ item: b, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
1162
- assurance.noteSession(ra.key, "s-amb");
1163
- receipts.recordOutbound({ service: "cohort", channel: "C-SHARED", kind: "session", agentRoot: ROOT });
1164
-
1165
- const r = await settleSession({ key: ra.key, ok: true, code: 0, stdout: "", deps: { deliverImpl: t.impl } });
1166
- assert.equal(r.verdict, "silent-success", "ambiguous evidence is not evidence");
1167
- });
1168
-
1169
- test("an unattributed message DOES discharge the only open debt in a quiet room", async () => {
1170
- // The tolerant rung. A send lane that loses the dispatcher's env still leaves
1171
- // a receipt; treating that as silence would make every such reply produce a
1172
- // redundant apology. Safe only because the room holds one debt and a session
1173
- // of ours has actually run.
1174
- const t = fakeTransport();
1175
- const solo = { ...ITEM, id: "SOLO-1", raw_ref: "cohort:C-QUIET:SOLO-1", channel: "C-QUIET", channel_id: "C-QUIET" };
1176
- const r = await openAndAcknowledge({ item: solo, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
1177
- assurance.noteSession(r.key, "s-solo");
1178
- receipts.recordOutbound({ service: "cohort", channel: "C-QUIET", kind: "session", agentRoot: ROOT });
1179
-
1180
- const before = t.sent.length;
1181
- const settled = await settleSession({ key: r.key, ok: true, code: 0, deps: { deliverImpl: t.impl } });
1182
- assert.equal(settled.verdict, "answered-by-session");
1183
- assert.equal(settled.basis, "sole-debt-in-room");
1184
- assert.equal(t.sent.length, before);
1185
- });
1186
-
1187
- test("REGRESSION: a message that arrives before any session of ours ran is not our answer", async () => {
1188
- // A debt whose acknowledgement failed, and something unrelated posts into the
1189
- // room. Closing on that leaves a human never acknowledged, never answered,
1190
- // and the debt recorded as discharged.
1191
- const t = fakeTransport();
1192
- const solo = { ...ITEM, id: "PRE-1", raw_ref: "cohort:C-PRE:PRE-1", channel: "C-PRE", channel_id: "C-PRE" };
1193
- const r = await openAndAcknowledge({ item: solo, classResult: CLASS_COMPLEX, tier: "work" });
1194
- assert.equal(readObligation(r.key).acknowledged, false, "nothing is said at open time any more");
1195
- receipts.recordOutbound({ service: "cohort", channel: "C-PRE", kind: "reply", agentRoot: ROOT });
1196
-
1197
- const stats = await sweepObligations({ now: Date.now() + ACK_AFTER_MS + 1000, deps: { deliverImpl: t.impl, generateAckImpl: async () => "Finally picking your ask up now." } });
1198
- assert.equal(stats.closed, 0, "no session of ours had run — that message was not our answer");
1199
- assert.equal(stats.acked, 1, "instead the interim is finally sent");
1200
- assert.equal(t.sent.at(-1).text, "Finally picking your ask up now.");
1201
- });
1202
- });
1203
-
1204
- // ---------------------------------------------------------------------------
1205
- // 8. UNREACHABLE CHANNELS — an impossible send must not become a loop
1206
- // ---------------------------------------------------------------------------
1207
-
1208
- describe("channels with no transport", () => {
1209
- const TELEGRAM = {
1210
- id: "TG-1", raw_ref: "telegram:12345:9", message_id: "TG-1",
1211
- service: "telegram", channel_id: "12345", sender: "Dana Whitfield",
1212
- content: "please redo the deck and send it over",
1213
- };
1214
-
1215
- test("no acknowledgement is promised on a service we cannot write to", () => {
1216
- const v = shouldAcknowledge({ willSpawnSession: true, item: TELEGRAM, source: "inbox" });
1217
- assert.equal(v.ack, false);
1218
- assert.equal(v.reason, "no-transport");
1219
- });
1220
-
1221
- test("REGRESSION: the debt is still opened, and the sweep escalates it ONCE instead of retrying forever", async () => {
1222
- // What shipped: the ack branch's `continue` sat above the stale branch, so an
1223
- // obligation whose send could never succeed was retried once per tick —
1224
- // 1,440 impossible sends a day, an immortal debt, and a human told nothing.
1225
- const t = fakeTransport();
1226
- const opened = await openAndAcknowledge({ item: TELEGRAM, classResult: CLASS_COMPLEX, ack: false });
1227
- assert.equal(opened.opened, true, "the debt exists even though we cannot speak to it");
1228
- assert.equal(readObligation(opened.key).deliverable, false);
1229
-
1230
- let attempts = 0;
1231
- const counting = async (...a) => { attempts++; return t.impl(...a); };
1232
- const t0 = Date.now();
1233
- for (let m = 1; m <= 240; m++) {
1234
- await sweepObligations({ now: t0 + m * 60_000, deps: { deliverImpl: counting } });
1235
- }
1236
- assert.equal(attempts, 0, "not one impossible send is attempted");
1237
- const rec = readObligation(opened.key);
1238
- assert.equal(rec.state, "undeliverable", "the debt is resolved, not immortal");
1239
- assert.equal(openObligations().length, 0);
1240
- });
1241
-
1242
- test("an acknowledgement that keeps failing is counted out and escalated", async () => {
1243
- const t = fakeTransport({ failTimes: 999 });
1244
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
1245
- const t0 = Date.now();
1246
- let ticks = 0;
1247
- for (let m = 1; m <= 60; m++) {
1248
- const s = await sweepObligations({ now: t0 + m * 60_000, deps: { deliverImpl: t.impl, generateAckImpl: async () => FAKE_ACK_LINE } });
1249
- ticks++;
1250
- if (s.unreachable) break;
1251
- }
1252
- assert.ok(ticks <= ACK_MAX_ATTEMPTS + 2, `gave up after ${ticks} ticks, not forever`);
1253
- assert.equal(readObligation(r.key).state, "undeliverable");
1254
- });
1255
- });
1256
-
1257
- // ---------------------------------------------------------------------------
1258
- // 9. THE RETRY CLOCK — never contradict a message already sent
1259
- // ---------------------------------------------------------------------------
1260
-
1261
- describe("a running retry is not declared dead", () => {
1262
- test("REGRESSION: 'I'm retrying it now' is not followed by 'I've stopped retrying' five minutes later", async () => {
1263
- // Attempt 1 is SIGTERMed at the dispatcher's 45-minute cap. STALE_AFTER_MS is
1264
- // 50 minutes and the clock used to run from the ORIGINAL arrival, so at
1265
- // minute 50 the sweep told the same human the exact opposite of what they had
1266
- // been told at minute 45 — while the retry was still running.
1267
- const t = fakeTransport();
1268
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
1269
- assurance.noteSession(r.key, "s-1");
1270
- const t0 = readObligation(r.key).openedAt;
1271
-
1272
- const settled = await settleSession({
1273
- key: r.key, ok: false, code: 143, error: "session_close exit 143",
1274
- now: t0 + 45 * 60_000, deps: { deliverImpl: t.impl, spokeSinceImpl: () => false },
1275
- });
1276
- assert.equal(settled.willRetry, true);
1277
- assert.match(t.sent.at(-1).text, /retrying now/i);
1278
-
1279
- const stats = await sweepObligations({ now: t0 + 51 * 60_000, deps: { deliverImpl: t.impl, spokeSinceImpl: () => false } });
1280
- assert.equal(stats.staled, 0, "the retry is minutes old, not fifty");
1281
- assert.equal(readObligation(r.key).state, "open");
1282
- assert.doesNotMatch(t.sent.at(-1).text, /stopped retrying/i);
1283
- });
1284
-
1285
- test("but a retry that itself goes nowhere DOES eventually stale out", async () => {
1286
- const t = fakeTransport();
1287
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, deps: { ackSender: t.ackSender } });
1288
- assurance.noteSession(r.key, "s-1");
1289
- const t0 = readObligation(r.key).openedAt;
1290
- await settleSession({
1291
- key: r.key, ok: false, code: 143, error: "session_close exit 143",
1292
- now: t0 + 45 * 60_000, deps: { deliverImpl: t.impl, spokeSinceImpl: () => false },
1293
- });
1294
- const stats = await sweepObligations({
1295
- now: t0 + 45 * 60_000 + STALE_AFTER_MS + 1000,
1296
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false },
1297
- });
1298
- assert.equal(stats.staled, 1, "the promise still has a floor under it");
1299
- assert.match(t.sent.at(-1).text, /outrun its time limit/i);
1300
- });
1301
- });
1302
-
1303
- // ---------------------------------------------------------------------------
1304
- // 10. ACKNOWLEDGEMENT DISCIPLINE — the 2026-09-12 policy, against the measured
1305
- // production strings that made it necessary.
1306
- // ---------------------------------------------------------------------------
1307
-
1308
- describe("sanitiseAckText enforces the prompt the model was already given", () => {
1309
- // The ack system prompt has said "Never a generic 'on it' or 'looking into
1310
- // it'" since it was written. Nothing checked. 3,069 generic acknowledgements
1311
- // shipped in fourteen days — 28.8% of every message this fleet sent — and 272
1312
- // of them were never followed by a substantive reply within the hour.
1313
- //
1314
- // These are the REAL strings, as observed in org_default_adaptic.
1315
- const OBSERVED_IN_PRODUCTION = [
1316
- "On it.",
1317
- "On it",
1318
- "On it — digging in now.",
1319
- "Looking now.",
1320
- "Looking into it.",
1321
- "Looking into this now.",
1322
- "Checking.",
1323
- "Checking now.",
1324
- "One moment.",
1325
- "Got it.",
1326
- "Got it — will do.",
1327
- "Will do.",
1328
- "Working on it.",
1329
- "Taking a look.",
1330
- "Digging in now.",
1331
- ];
1332
-
1333
- for (const line of OBSERVED_IN_PRODUCTION) {
1334
- test(`rejects the measured generic ack: ${JSON.stringify(line)}`, () => {
1335
- assert.equal(sanitiseAckText(line), null, "rejection ⇒ null ⇒ send nothing, which is the existing correct failure mode");
1336
- });
1337
- }
1338
-
1339
- test("rejects anything under the minimum length, whatever its phrasing", () => {
1340
- // The second, phrasing-independent net. A line too short to name the thing
1341
- // that was asked cannot be specific to it, which is the one thing the
1342
- // prompt requires.
1343
- assert.equal(ACK_MIN_CHARS, 20);
1344
- assert.equal(sanitiseAckText("Sorted, starting."), null);
1345
- assert.equal(sanitiseAckText("Yep."), null);
1346
- assert.equal(sanitiseAckText("Onto it now, boss."), null);
1347
- });
1348
-
1349
- test("…and still accepts a line that actually names the ask", () => {
1350
- // The discipline must not become a gag. A bespoke line is exactly what the
1351
- // prompt asks for, and it survives.
1352
- for (const good of [
1353
- "Give me a few minutes on the git fixes.",
1354
- "Pulling the July ledger against the forecast now.",
1355
- "Starting on the regulator gap analysis — the Thursday one.",
1356
- ]) {
1357
- assert.equal(sanitiseAckText(good), good, `"${good}" is specific and must pass`);
1358
- }
1359
- });
1360
-
1361
- test("REGRESSION: the ack prompt and the filter read the same pattern, so they cannot drift again", async () => {
1362
- // The whole defect was an instruction with no enforcement behind it. The
1363
- // filter now imports GENERIC_OPENER from the copy-rule module; this pins
1364
- // that every opener the prompt names is in fact refused.
1365
- const { GENERIC_OPENER } = await import("../../lib/assurance/plan-note.mjs");
1366
- for (const opener of ["on it", "looking now", "looking into", "checking", "one moment", "got it", "will do", "working on it", "taking a look", "digging in"]) {
1367
- assert.match(opener, GENERIC_OPENER, `the prompt forbids "${opener}" — the filter must too`);
1368
- }
1369
- });
1370
- });
1371
-
1372
- describe("failure notices are said ONCE per (channel, obligationKey)", () => {
1373
- test("REGRESSION: the same failure sentence does not reach one room three times in a minute", async () => {
1374
- // Sampled live at 06:32Z on 2026-09-12: the same failure sentence three
1375
- // times inside one minute, and `On it.` again immediately after. The cause
1376
- // was a retry loop that narrated every attempt — notice, retry, a FRESH
1377
- // obligation under the same key, notice again — with nothing suppressing
1378
- // the repeat.
1379
- const t = fakeTransport();
1380
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
1381
-
1382
- for (let attempt = 0; attempt < 3; attempt++) {
1383
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1384
- assurance.noteSession(r.key, `s-${attempt}`);
1385
- await settleSession({ key: r.key, ok: false, code: 143, error: "SIGTERM", deps });
1386
- // Whatever the outcome, the item is re-delivered and the key reopens.
1387
- assurance.closeObligation(r.key, { outcome: "failed" });
1388
- }
1389
-
1390
- const failures = t.sent.filter((m) => m.kind === "failure");
1391
- assert.equal(failures.length, 1, `the same sentence went out ${failures.length} times; the whole point is once`);
1392
- assert.match(failures[0].text, /hit a problem/i);
1393
- assert.equal(t.sent.filter((m) => m.kind === "ack").length, 0, "and no `On it.` behind it either");
1394
- });
1395
-
1396
- test("…but 'a retry is running' and 'I have stopped' remain two separate, sayable facts", async () => {
1397
- // Collapsing them would leave a human who was told a retry was in flight
1398
- // never learning it had been abandoned. What is suppressed is the REPEAT of
1399
- // one fact, not the second fact.
1400
- const t = fakeTransport();
1401
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false };
1402
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1403
- assurance.noteSession(r.key, "s-1");
1404
- const first = await settleSession({ key: r.key, ok: false, code: 143, error: "SIGTERM", deps });
1405
- assert.equal(first.willRetry, true);
1406
-
1407
- // The retry runs and fails terminally.
1408
- assurance.noteSession(r.key, "s-2");
1409
- const second = await settleSession({ key: r.key, ok: false, code: null, error: "claude CLI spawn error: ENOENT", deps });
1410
- assert.equal(second.willRetry, false);
1411
-
1412
- const texts = t.sent.filter((m) => m.kind === "failure").map((m) => m.text);
1413
- assert.equal(texts.length, 2);
1414
- assert.match(texts[0], /retrying now/i);
1415
- assert.match(texts[1], /stopped retrying/i);
1416
- });
1417
-
1418
- test("the notice ledger records WHICH notice reached WHICH room", async () => {
1419
- const t = fakeTransport();
1420
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1421
- await settleSession({ key: r.key, ok: false, code: 143, error: "SIGTERM", deps: { deliverImpl: t.impl, spokeSinceImpl: () => false } });
1422
- const rec = readObligation(r.key);
1423
- const ids = Object.keys(rec.notices || {});
1424
- assert.equal(ids.length, 1);
1425
- assert.ok(ids[0].startsWith(NOTICE.FAILURE_RETRYING), `expected a ${NOTICE.FAILURE_RETRYING} stamp, got ${ids[0]}`);
1426
- assert.ok(ids[0].includes(String(ITEM.channel_id).toLowerCase()), "the channel is part of the key — the same ask re-routed elsewhere genuinely has not been answered there");
1427
- });
1428
-
1429
- test("a stale notice is not repeated for a key that already heard one", async () => {
1430
- const t = fakeTransport();
1431
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
1432
- for (let attempt = 0; attempt < 3; attempt++) {
1433
- await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1434
- await sweepObligations({ now: Date.now() + STALE_AFTER_MS + 1000, deps });
1435
- }
1436
- assert.equal(t.sent.filter((m) => m.kind === "failure").length, 1);
1437
- });
1438
-
1439
- // ── THE SIBLING SHAPE, which the per-record ledger structurally cannot reach ─
1440
- //
1441
- // The dedupe above is keyed on the obligation RECORD, so it covers the RETRY
1442
- // loop: notice → retry → fresh record under the same key → notice again. It
1443
- // does NOT cover SIBLINGS. Twenty separate inbound asks in one room whose
1444
- // sessions all die the same way are twenty separate records, and the stale
1445
- // branch's sentence contains nothing about which ask it is — so the room gets
1446
- // the identical line twenty times and the reader cannot tell any of them
1447
- // apart. That is the commoner shape whenever a dispatcher or model path is
1448
- // down, and it is the one the sampled "same failure sentence three times in
1449
- // one minute" is most likely to have actually been.
1450
-
1451
- test("REGRESSION: twenty SIBLING debts staling together say the identical sentence ONCE", async () => {
1452
- const t = fakeTransport();
1453
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
1454
- for (let i = 0; i < 20; i++) {
1455
- await openAndAcknowledge({
1456
- item: { ...ITEM, id: `sib-${i}`, raw_ref: `cohort:sibling:${i}`, message_id: `${i}` },
1457
- classResult: CLASS_COMPLEX,
1458
- tier: "work",
1459
- });
1460
- }
1461
- assert.equal(openObligations().length, 20, "every ask is still opened and tracked");
1462
-
1463
- await sweepObligations({ now: Date.now() + STALE_AFTER_MS + 60_000, deps });
1464
-
1465
- const failures = t.sent.filter((m) => m.kind === "failure");
1466
- assert.equal(failures.length, 1, `twenty byte-identical failure sentences reached one room ${failures.length} times`);
1467
- // Nothing is LOST: every dead ask is still escalated for an operator, and
1468
- // every debt is still closed. Only the duplicate sentence is withheld.
1469
- assert.equal(openObligations().length, 0, "all twenty debts were still discharged");
1470
- const { readdirSync } = await import("node:fs");
1471
- assert.equal(readdirSync(join(ROOT, "state", "obligations", "needs-attention")).length, 20,
1472
- "…and an operator still sees all twenty in needs-attention");
1473
- });
1474
-
1475
- test("…but twenty siblings that fail DIFFERENTLY are all said, because each is a fact the reader lacks", async () => {
1476
- // The safety argument for the suppression above. Only an IDENTICAL sentence
1477
- // is withheld; suppressing information a person does not have re-creates
1478
- // the original bug — someone never told their work died.
1479
- const t = fakeTransport();
1480
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false };
1481
- const CAUSES = [
1482
- { code: null, error: "claude CLI spawn error: ENOENT" },
1483
- { code: 0, error: "" },
1484
- ];
1485
- let sent = 0;
1486
- for (let i = 0; i < CAUSES.length; i++) {
1487
- const r = await openAndAcknowledge({
1488
- item: { ...ITEM, id: `diff-${i}`, raw_ref: `cohort:different:${i}` },
1489
- classResult: CLASS_COMPLEX,
1490
- tier: "work",
1491
- });
1492
- assurance.noteSession(r.key, `s-${i}`);
1493
- const res = await settleSession({ key: r.key, ok: false, ...CAUSES[i], deps });
1494
- if (res.spoke) sent++;
1495
- }
1496
- assert.equal(sent, CAUSES.length, "distinct causes are distinct sentences and every one is said");
1497
- const texts = new Set(t.sent.filter((m) => m.kind === "failure").map((m) => m.text));
1498
- assert.equal(texts.size, CAUSES.length);
1499
- });
1500
-
1501
- test("a sibling in a DIFFERENT room still hears it — the coalescing is per room", async () => {
1502
- const t = fakeTransport();
1503
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE };
1504
- await openAndAcknowledge({ item: { ...ITEM, id: "r1-a", raw_ref: "cohort:r1:a" }, classResult: CLASS_COMPLEX, tier: "work" });
1505
- await openAndAcknowledge({ item: { ...ITEM, id: "r1-b", raw_ref: "cohort:r1:b" }, classResult: CLASS_COMPLEX, tier: "work" });
1506
- await openAndAcknowledge({
1507
- item: { ...ITEM, id: "r2-a", raw_ref: "cohort:r2:a", channel_id: "C-OTHER-ROOM" },
1508
- classResult: CLASS_COMPLEX, tier: "work",
1509
- });
1510
- await sweepObligations({ now: Date.now() + STALE_AFTER_MS + 60_000, deps });
1511
- const failures = t.sent.filter((m) => m.kind === "failure");
1512
- assert.equal(failures.length, 2, "one per room");
1513
- assert.equal(new Set(failures.map((m) => m.channel)).size, 2);
1514
- });
1515
-
1516
- test("a suppressed duplicate is still stamped on its own record, so it cannot leak out later", async () => {
1517
- // If the record's own ledger did not learn the fact had been told in this
1518
- // room, the next tick would re-say it the moment the room window rolled
1519
- // over — turning suppression into a delay.
1520
- const t = fakeTransport();
1521
- const deps = { deliverImpl: t.impl, spokeSinceImpl: () => false };
1522
- const a = await openAndAcknowledge({ item: { ...ITEM, id: "st-a", raw_ref: "cohort:stamp:a" }, classResult: CLASS_COMPLEX, tier: "work" });
1523
- const b = await openAndAcknowledge({ item: { ...ITEM, id: "st-b", raw_ref: "cohort:stamp:b" }, classResult: CLASS_COMPLEX, tier: "work" });
1524
- assurance.noteSession(a.key, "s-a");
1525
- assurance.noteSession(b.key, "s-b");
1526
- await settleSession({ key: a.key, ok: false, code: 143, error: "SIGTERM", deps });
1527
- await settleSession({ key: b.key, ok: false, code: 143, error: "SIGTERM", deps });
1528
- assert.equal(t.sent.filter((m) => m.kind === "failure").length, 1);
1529
- const recB = readObligation(b.key);
1530
- assert.ok(Object.keys(recB.notices || {}).some((k) => k.startsWith(NOTICE.FAILURE_RETRYING)),
1531
- "the suppressed record carries the stamp, so a later tick does not re-say it");
1532
- });
1533
- });
1534
-
1535
- describe("the sweep is not re-entrant", () => {
1536
- // agent-daemon.mjs fires sweepObligations() on a 60-second setInterval with no
1537
- // in-flight guard of its own, and ONE TICK IS NOT BOUNDED BY 60 SECONDS:
1538
- // branch (e) peeks the room budget with commit:false, then awaits a cold
1539
- // `claude --print` spawn, serialised per obligation. A daemon with a dozen-plus
1540
- // eligible obligations in distinct rooms spends more than a tick in generation
1541
- // alone, so tick N+1 begins while tick N is still composing — and BOTH peek the
1542
- // same room's budget and read "allowed", because the spend is only committed
1543
- // after a successful send.
1544
-
1545
- test("an overlapping sweep is refused rather than allowed to double-spend a room", async () => {
1546
- const t = fakeTransport();
1547
- await openAndAcknowledge({ item: { ...ITEM, id: "re-1", raw_ref: "cohort:reentry:1" }, classResult: CLASS_COMPLEX, tier: "work" });
1548
- await openAndAcknowledge({ item: { ...ITEM, id: "re-2", raw_ref: "cohort:reentry:2" }, classResult: CLASS_COMPLEX, tier: "work" });
1549
-
1550
- const now = Date.now() + ACK_AFTER_MS + 1000;
1551
- let overlapped = null;
1552
- // A generator that starts a SECOND sweep while the first is still awaiting
1553
- // it — exactly the shape a slow `claude --print` produces on the timer.
1554
- const deps = {
1555
- deliverImpl: t.impl,
1556
- spokeSinceImpl: () => false,
1557
- generateAckImpl: async () => {
1558
- if (overlapped === null) overlapped = await sweepObligations({ now, deps });
1559
- return FAKE_ACK_LINE;
1560
- },
1561
- };
1562
-
1563
- const first = await sweepObligations({ now, deps });
1564
- assert.equal(overlapped && overlapped.skipped, true, "the re-entrant call was refused");
1565
- assert.equal(overlapped.acked, 0, "…and it spoke to nobody");
1566
- assert.equal(first.acked, 1, "the outer sweep still did its one room's work");
1567
- assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1, "one room, one interim, even across overlapping ticks");
1568
- });
1569
-
1570
- test("the guard is released even when the sweep throws, so it cannot disable the daemon", async () => {
1571
- await openAndAcknowledge({ item: { ...ITEM, id: "thr-1", raw_ref: "cohort:throw:1" }, classResult: CLASS_COMPLEX, tier: "work" });
1572
- const boom = {
1573
- deliverImpl: async () => { throw new Error("transport exploded"); },
1574
- spokeSinceImpl: () => false,
1575
- generateAckImpl: async () => FAKE_ACK_LINE,
1576
- };
1577
- await assert.rejects(() => sweepObligations({ now: Date.now() + ACK_AFTER_MS + 1000, deps: boom }));
1578
- // The very next tick must run. Before the `finally`, this returned skipped.
1579
- const after = await sweepObligations({
1580
- now: Date.now() + ACK_AFTER_MS + 2000,
1581
- deps: { deliverImpl: async () => ({ sent: true, via: "fake" }), spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
1582
- });
1583
- assert.notEqual(after.skipped, true, "a throw must not leave the sweep disabled for the life of the daemon");
1584
- });
1585
- });
1586
-
1587
- describe("the room-budget ledger is not an obligation", () => {
1588
- // It lives at state/obligations/room-budget.json — the path the design names
1589
- // — inside a directory whose reader JSON-parses every *.json as a debt record.
1590
- // Parsed as one it was a phantom with no `key` and no `state`: it inflated
1591
- // every count derived from listObligations(), and pruneObligations() called
1592
- // unlinkSync(pathFor(undefined)) -> state/obligations/unknown.json on every
1593
- // tick, swallowed by a catch. Harmless only because nothing can key to
1594
- // "unknown" today.
1595
- test("a sweep that spends a room's budget does not add an obligation to the ledger", async () => {
1596
- const t = fakeTransport();
1597
- await openAndAcknowledge({ item: { ...ITEM, id: "led-1", raw_ref: "cohort:ledger:1" }, classResult: CLASS_COMPLEX, tier: "work" });
1598
- await sweepObligations({
1599
- now: Date.now() + ACK_AFTER_MS + 1000,
1600
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
1601
- });
1602
- const { existsSync } = await import("node:fs");
1603
- assert.equal(existsSync(join(ROOT, "state", "obligations", "room-budget.json")), true, "the budget really was written there");
1604
- const all = assurance.listObligations();
1605
- assert.equal(all.length, 1, `one ask, one record — got ${all.length}`);
1606
- assert.equal(all.every((r) => typeof r.key === "string" && r.key), true, "every record the reader returns is addressable");
1607
- });
1608
-
1609
- test("pruning never unlinks state/obligations/unknown.json", async () => {
1610
- const { writeFileSync, existsSync } = await import("node:fs");
1611
- const decoy = join(ROOT, "state", "obligations", "unknown.json");
1612
- writeFileSync(decoy, JSON.stringify({ not: "an obligation" }));
1613
- assurance.pruneObligations({ now: Date.now() + 10 * 24 * 3600_000 });
1614
- assert.equal(existsSync(decoy), true, "a keyless record must never be resolved to a path and deleted");
1615
- try { (await import("node:fs")).unlinkSync(decoy); } catch { /* cleanup only */ }
1616
- });
1617
- });
1618
-
1619
- describe("notePlan — the plan tier says the plan, or says nothing", () => {
1620
- const FIRST_TURN = {
1621
- type: "assistant",
1622
- message: { content: [{ type: "text", text: "1. Pull the July ledger rows and the signed forecast.\n2. Reconcile them line by line and mark every variance over 5%.\n3. Write up the three biggest gaps with the numbers behind each." }] },
1623
- };
1624
-
1625
- test("posts the plan the SESSION stated, once", async () => {
1626
- const t = fakeTransport();
1627
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "plan" });
1628
- const res = await notePlan({ key: r.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } });
1629
- assert.equal(res.spoke, true);
1630
- assert.equal(t.sent.length, 1);
1631
- assert.match(t.sent[0].text, /Pull the July ledger rows/);
1632
- assert.match(t.sent[0].text, /three biggest gaps/);
1633
- assert.equal(readObligation(r.key).interimSaid, true);
1634
-
1635
- const again = await notePlan({ key: r.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } });
1636
- assert.equal(again.spoke, false);
1637
- assert.equal(again.reason, "interim-already-said");
1638
- assert.equal(t.sent.length, 1, "one message, and it is the plan");
1639
- });
1640
-
1641
- test("NEVER FABRICATES: no stated plan ⇒ nothing is posted", async () => {
1642
- const t = fakeTransport();
1643
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "plan" });
1644
- for (const turn of [null, undefined, "", "Sure, happy to look at that.", { type: "result" }, "{truncated"]) {
1645
- const res = await notePlan({ key: r.key, firstTurn: turn, now: Date.now(), deps: { deliverImpl: t.impl } });
1646
- assert.equal(res.spoke, false, `${JSON.stringify(turn)} must post nothing`);
1647
- assert.equal(res.reason, "no-plan-stated");
1648
- }
1649
- assert.equal(t.sent.length, 0);
1650
- assert.equal(readObligation(r.key).interimSaid, false, "and the latch is left unspent, so the sweep's one interim is still available");
1651
- });
1652
-
1653
- test("the ROOM budget outranks the plan tier too", async () => {
1654
- const t = fakeTransport();
1655
- const a = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "plan" });
1656
- const b = await openAndAcknowledge({ item: { ...ITEM, id: "p2", raw_ref: "cohort:same-room:p2" }, classResult: CLASS_COMPLEX, tier: "plan" });
1657
- assert.equal((await notePlan({ key: a.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } })).spoke, true);
1658
- const second = await notePlan({ key: b.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } });
1659
- assert.equal(second.spoke, false);
1660
- assert.equal(second.reason, "room-budget-spent");
1661
- assert.equal(t.sent.length, 1);
1662
- });
1663
-
1664
- test("a plan-tier debt is NEVER silent, even though no caller supplies a first turn yet", async () => {
1665
- // `notePlan` is exported and tested but not called by the daemon:
1666
- // dispatcher.mjs spawns with `--output-format json` (not `stream-json`), so
1667
- // the session's stdout arrives as one object at exit — after the answer has
1668
- // gone out. Streaming is a dispatcher change and belongs with WP-2.
1669
- //
1670
- // What must hold in the meantime is that the plan tier degrades to the WORK
1671
- // path rather than to silence. The sweep gates on `tier !== "answer"`, so it
1672
- // does; this pins it, because a tier that mutes a 15-45 minute session is
1673
- // the one failure this whole package exists to end.
1674
- const t = fakeTransport();
1675
- const r = await openAndAcknowledge({ item: { ...ITEM, id: "plan-fb", raw_ref: "cohort:planfb:1" }, classResult: CLASS_COMPLEX, tier: "plan" });
1676
- const stats = await sweepObligations({
1677
- now: Date.now() + ACK_AFTER_MS + 1000,
1678
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
1679
- });
1680
- assert.equal(stats.acked, 1, "the plan tier falls back to the one interim, not to nothing");
1681
- assert.equal(t.sent.filter((m) => m.kind === "ack").length, 1);
1682
- assert.equal(readObligation(r.key).interimSaid, true, "…and it is still only one");
1683
- });
1684
-
1685
- test("a work-tier or answer-tier debt is never given a plan", async () => {
1686
- const t = fakeTransport();
1687
- for (const tier of ["work", "answer"]) {
1688
- const r = await openAndAcknowledge({ item: { ...ITEM, id: `t-${tier}`, raw_ref: `cohort:tier:${tier}` }, classResult: CLASS_COMPLEX, tier });
1689
- const res = await notePlan({ key: r.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } });
1690
- assert.equal(res.spoke, false);
1691
- assert.equal(res.reason, `tier-${tier}`);
1692
- }
1693
- assert.equal(t.sent.length, 0);
1694
- });
1695
-
1696
- test("a plan posted, then the session answers — the debt discharges with no extra word", async () => {
1697
- const t = fakeTransport();
1698
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "plan" });
1699
- await notePlan({ key: r.key, firstTurn: FIRST_TURN, now: Date.now(), deps: { deliverImpl: t.impl } });
1700
- const before = t.sent.length;
1701
- const settled = await settleSession({ key: r.key, ok: true, code: 0, deps: { deliverImpl: t.impl, spokeForImpl: () => ({ heard: true, basis: "receipt" }) } });
1702
- assert.equal(settled.verdict, "answered-by-session");
1703
- assert.equal(t.sent.length, before, "the daemon adds nothing on top of the session's own answer");
1704
- });
1705
- });
1706
-
1707
- describe("COPY: no promise without a mechanism", async () => {
1708
- // §5.1.5. An interim may not contain a future-tense commitment unless a
1709
- // DURABLE obligation exists and the sweep can close it. The two deleted
1710
- // progress lines broke this twice over: they promised ("I'll come back as
1711
- // soon as I've got something") from a timer that had no idea whether anything
1712
- // would ever come back, and 272 acknowledgements proved the promise empty.
1713
- // IMPORTED, never re-typed. The sibling test one describe-block up exists to
1714
- // stop the ack prompt and its filter drifting apart; a copy test that
1715
- // re-declares the pattern it is checking has the identical defect. The local
1716
- // copy this replaces had already drifted: it was missing the `revert`
1717
- // alternative and the whole `i will (come back|...)` branch, so
1718
- // "I will report back once the reconciliation is done" was dropped by the
1719
- // implementation and would have PASSED the test's own check.
1720
- const { FORWARD_PROMISE } = await import("../../lib/assurance/plan-note.mjs");
1721
-
1722
- test("the deleted progress lines are gone from the module entirely", () => {
1723
- assert.equal(typeof assurance.composeProgress, "undefined", "composeProgress may not be reintroduced");
1724
- assert.equal(typeof assurance.PROGRESS_AFTER_MS, "undefined");
1725
- assert.equal(typeof assurance.PROGRESS_EVERY_MS, "undefined");
1726
- assert.equal(typeof assurance.PROGRESS_MAX, "undefined");
1727
- });
1728
-
1729
- test("…and the retired strings are still RECOGNISED, so a peer running older code cannot restart the loop", () => {
1730
- for (const line of RETIRED_NARRATOR_TEMPLATES) {
1731
- assert.equal(isOwnNarratorText(line), true, `"${line}" must still be recognised as machinery talking`);
1732
- }
1733
- });
1734
-
1735
- test("every interim the system can emit is promise-free", async () => {
1736
- // The plan note is the only composed interim left; the other is the
1737
- // model-generated ack, which the sanitiser governs. Both are checked.
1738
- const { renderPlanNote } = await import("../../lib/assurance/plan-note.mjs");
1739
- const note = renderPlanNote("1. Pull the ledger rows for July.\n2. Reconcile them against the signed forecast.\n3. I'll come back as soon as I've got something.");
1740
- assert.ok(note);
1741
- assert.doesNotMatch(note, FORWARD_PROMISE, "a plan bullet carries no durable backing and may not promise a return");
1742
-
1743
- // A generated ack that promises is still admissible ONLY because a durable
1744
- // obligation is what carries it — pinned here so the coupling is explicit.
1745
- const promising = "Starting on the July reconciliation now; I'll come back with the variances.";
1746
- assert.equal(sanitiseAckText(promising), promising);
1747
- const t = fakeTransport();
1748
- const r = await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "work" });
1749
- await sweepObligations({
1750
- now: Date.now() + ACK_AFTER_MS + 1000,
1751
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => promising },
1752
- });
1753
- assert.equal(t.sent.length, 1);
1754
- const rec = readObligation(r.key);
1755
- assert.equal(rec.state, "open", "the promise is backed: the debt is durable…");
1756
- assert.equal(openObligations().some((o) => o.key === r.key), true, "…and the sweep can still find it and close it");
1757
- });
1758
-
1759
- test("an obligation the sweep CANNOT close never gets to promise anything", async () => {
1760
- // No transport ⇒ no interim, at open time or ever. A promise into a channel
1761
- // that does not exist is the purest form of the broken-promise failure.
1762
- const t = fakeTransport();
1763
- const TELEGRAM_ITEM = { ...ITEM, id: "TG-9", raw_ref: "telegram:9", service: "telegram", channel: null, channel_id: null };
1764
- const r = await openAndAcknowledge({ item: TELEGRAM_ITEM, classResult: CLASS_COMPLEX, tier: "work", ack: false });
1765
- assert.equal(readObligation(r.key).interimForbidden, true);
1766
- await sweepObligations({
1767
- now: Date.now() + ACK_AFTER_MS + 1000,
1768
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => "I'll come back as soon as I've got something." },
1769
- });
1770
- assert.equal(t.sent.length, 0);
1771
- });
1772
- });
1773
-
1774
- describe("the answer tier is silent even when a caller gets the flag wrong", () => {
1775
- test("a record whose TIER is answer never produces an interim, ack:true or not", async () => {
1776
- // Defence in depth. `shouldAcknowledge` returns ack:false for the answer
1777
- // tier and the daemon turns that into `interimForbidden`, but the tier is
1778
- // the durable fact on the record and is what "never speak about this one"
1779
- // actually means. A caller that passes the tier and forgets the flag must
1780
- // not be able to re-open the flood.
1781
- const t = fakeTransport();
1782
- await openAndAcknowledge({ item: ITEM, classResult: CLASS_COMPLEX, tier: "answer", ack: true });
1783
- assert.equal(readObligation(ITEM.raw_ref).interimForbidden, undefined, "the flag was NOT set — this is the caller bug being defended against");
1784
- const stats = await sweepObligations({
1785
- now: Date.now() + ACK_AFTER_MS + 60_000,
1786
- deps: { deliverImpl: t.impl, spokeSinceImpl: () => false, generateAckImpl: async () => FAKE_ACK_LINE },
1787
- });
1788
- assert.equal(stats.acked, 0);
1789
- assert.equal(t.sent.length, 0, "the tier alone is enough to keep it quiet");
1790
- });
1791
- });