@cohortapp/agent-sdk 2.16.0 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +29 -2
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -1,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
- });