@cohortapp/agent-sdk 2.17.0 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (526) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture.mjs +8 -6
  23. package/lib/collective/global-config.mjs +63 -1
  24. package/lib/collective/presence.mjs +142 -5
  25. package/lib/comms/send-gate.mjs +559 -1
  26. package/lib/diagnostics/alerts.mjs +49 -0
  27. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  28. package/lib/engine/agents/definitions.mjs +343 -0
  29. package/lib/engine/agents/persist.mjs +275 -0
  30. package/lib/engine/agents/runtime.mjs +748 -0
  31. package/lib/engine/agents/usage.mjs +95 -0
  32. package/lib/engine/auth-status.mjs +139 -0
  33. package/lib/engine/budget.mjs +194 -0
  34. package/lib/engine/cli.mjs +1204 -0
  35. package/lib/engine/commands/index.mjs +269 -0
  36. package/lib/engine/context/budget.mjs +219 -0
  37. package/lib/engine/context/cache.mjs +125 -0
  38. package/lib/engine/context/child-env.mjs +215 -0
  39. package/lib/engine/context/compaction.mjs +342 -0
  40. package/lib/engine/context/images.mjs +90 -0
  41. package/lib/engine/context/instructions.mjs +327 -0
  42. package/lib/engine/context/lazy-instructions.mjs +169 -0
  43. package/lib/engine/context/manager.mjs +182 -0
  44. package/lib/engine/context/real-path.mjs +91 -0
  45. package/lib/engine/context/secret-values.mjs +163 -0
  46. package/lib/engine/context/settings.mjs +274 -0
  47. package/lib/engine/context/stream-input.mjs +159 -0
  48. package/lib/engine/guard.mjs +152 -0
  49. package/lib/engine/hooks.mjs +713 -0
  50. package/lib/engine/loop.mjs +560 -0
  51. package/lib/engine/mcp/client.mjs +254 -0
  52. package/lib/engine/mcp/config.mjs +301 -0
  53. package/lib/engine/mcp/http.mjs +201 -0
  54. package/lib/engine/mcp/index.mjs +146 -0
  55. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  56. package/lib/engine/mcp/naming.mjs +66 -0
  57. package/lib/engine/mcp/resources.mjs +89 -0
  58. package/lib/engine/mcp/results.mjs +133 -0
  59. package/lib/engine/mcp/stdio.mjs +137 -0
  60. package/lib/engine/mcp/supervisor.mjs +116 -0
  61. package/lib/engine/messages.mjs +104 -0
  62. package/lib/engine/output/json.mjs +164 -0
  63. package/lib/engine/output/stream-json.mjs +266 -0
  64. package/lib/engine/permissions.mjs +845 -0
  65. package/lib/engine/process-identity.mjs +164 -0
  66. package/lib/engine/process-tree.mjs +551 -0
  67. package/lib/engine/prompt.mjs +60 -0
  68. package/lib/engine/session/store.mjs +299 -0
  69. package/lib/engine/session-runtime/args.mjs +97 -0
  70. package/lib/engine/session-runtime/host.mjs +143 -0
  71. package/lib/engine/session-runtime/inbox.mjs +122 -0
  72. package/lib/engine/session-runtime/notifications.mjs +129 -0
  73. package/lib/engine/session-runtime/registry.mjs +328 -0
  74. package/lib/engine/session-runtime/runner.mjs +344 -0
  75. package/lib/engine/session-runtime/socket.mjs +212 -0
  76. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  77. package/lib/engine/skills/index.mjs +321 -0
  78. package/lib/engine/tools/bash-background.mjs +533 -0
  79. package/lib/engine/tools/bash.mjs +216 -0
  80. package/lib/engine/tools/edit.mjs +97 -0
  81. package/lib/engine/tools/glob.mjs +81 -0
  82. package/lib/engine/tools/grep.mjs +224 -0
  83. package/lib/engine/tools/index.mjs +84 -0
  84. package/lib/engine/tools/list-agents.mjs +32 -0
  85. package/lib/engine/tools/ls.mjs +127 -0
  86. package/lib/engine/tools/monitor.mjs +82 -0
  87. package/lib/engine/tools/notebook-edit.mjs +218 -0
  88. package/lib/engine/tools/read.mjs +103 -0
  89. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  90. package/lib/engine/tools/schema.mjs +144 -0
  91. package/lib/engine/tools/send-message.mjs +77 -0
  92. package/lib/engine/tools/session.mjs +70 -0
  93. package/lib/engine/tools/todo.mjs +144 -0
  94. package/lib/engine/tools/toolsearch.mjs +217 -0
  95. package/lib/engine/tools/walk.mjs +193 -0
  96. package/lib/engine/tools/web-switch.mjs +31 -0
  97. package/lib/engine/tools/webfetch-html.mjs +387 -0
  98. package/lib/engine/tools/webfetch-net.mjs +340 -0
  99. package/lib/engine/tools/webfetch.mjs +198 -0
  100. package/lib/engine/tools/websearch.mjs +91 -0
  101. package/lib/engine/tools/workflow.mjs +95 -0
  102. package/lib/engine/tools/write.mjs +76 -0
  103. package/lib/engine/tui/line-editor.mjs +137 -0
  104. package/lib/engine/tui/render.mjs +86 -0
  105. package/lib/engine/tui/tui.mjs +274 -0
  106. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  107. package/lib/engine/wire/effort.mjs +36 -0
  108. package/lib/engine/wire/errors.mjs +496 -0
  109. package/lib/engine/wire/http.mjs +441 -0
  110. package/lib/engine/wire/index.mjs +76 -0
  111. package/lib/engine/wire/openai-chat.mjs +332 -0
  112. package/lib/engine/wire/prompt-cache.mjs +79 -0
  113. package/lib/engine/wire/search.mjs +140 -0
  114. package/lib/engine/wire/sse.mjs +114 -0
  115. package/lib/engine/wire/stall.mjs +349 -0
  116. package/lib/engine/wire/token-provider.mjs +175 -0
  117. package/lib/engine/wire/usage.mjs +192 -0
  118. package/lib/engine/workflow/host.mjs +524 -0
  119. package/lib/engine/workflow/journal.mjs +188 -0
  120. package/lib/engine/workflow/json-schema.mjs +171 -0
  121. package/lib/engine/workflow/meta.mjs +329 -0
  122. package/lib/engine/workflow/notifications.mjs +52 -0
  123. package/lib/engine/workflow/runtime.mjs +447 -0
  124. package/lib/engine/workflow/sandbox.mjs +534 -0
  125. package/lib/engine/workflow/worker.mjs +141 -0
  126. package/lib/engine/workflow/worktree.mjs +74 -0
  127. package/lib/execution/disposition.mjs +1 -1
  128. package/lib/execution/intake.mjs +10 -0
  129. package/lib/execution/surface-policy.mjs +15 -0
  130. package/lib/learning/curator.mjs +8 -6
  131. package/lib/learning/reflect.mjs +8 -6
  132. package/lib/model-router/catalog/cohort.yaml +137 -0
  133. package/lib/model-router/catalog.mjs +118 -1
  134. package/lib/model-router/failover.mjs +67 -16
  135. package/lib/model-router/llm-task.mjs +39 -3
  136. package/lib/model-router/resolve.mjs +89 -3
  137. package/lib/model-router/spawn.mjs +46 -47
  138. package/lib/model-router/taxonomy.mjs +126 -4
  139. package/lib/org/cost-sync.mjs +141 -11
  140. package/lib/org/inbound/broadcast.mjs +289 -0
  141. package/lib/org/inbound/collective.mjs +375 -0
  142. package/lib/org/inbound/directedness.mjs +96 -8
  143. package/lib/org/inbound/facts.mjs +78 -2
  144. package/lib/org/inbound/project.mjs +22 -0
  145. package/lib/org/inbound/surfaces.mjs +14 -0
  146. package/lib/org/llm-token.mjs +879 -0
  147. package/lib/org/mesh.mjs +61 -0
  148. package/lib/org/messaging.mjs +3 -1
  149. package/lib/org/protocol.checksum +1 -1
  150. package/lib/org/protocol.mjs +15 -0
  151. package/lib/org/quota.mjs +520 -0
  152. package/lib/org/tool-surface.mjs +104 -16
  153. package/lib/org/ui-parity.mjs +16 -1
  154. package/lib/org/work-ledger.mjs +37 -6
  155. package/lib/rate-guard.mjs +114 -1
  156. package/lib/resource-governor.mjs +41 -6
  157. package/lib/runtime/adapter.mjs +823 -0
  158. package/lib/runtime/child-env.mjs +191 -0
  159. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  160. package/lib/runtime/seat-engine.mjs +162 -0
  161. package/lib/session/ask-ledger.mjs +271 -0
  162. package/lib/session/current-work.mjs +676 -0
  163. package/lib/session/feed-core.mjs +40 -3
  164. package/lib/session/launch-args.mjs +56 -4
  165. package/lib/session/status-summary.mjs +26 -9
  166. package/lib/session/upgrade-notice.mjs +42 -0
  167. package/lib/setup/claude-probe.mjs +117 -13
  168. package/lib/setup/enrich.mjs +13 -10
  169. package/lib/setup/sections/model.mjs +39 -13
  170. package/lib/telemetry/collect.mjs +208 -9
  171. package/lib/upgrade/ignored-drift.mjs +105 -0
  172. package/lib/voice/post-call-brief.mjs +30 -17
  173. package/package.json +13 -3
  174. package/plugins/maestro-skills/skills/board-work.md +5 -0
  175. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  176. package/plugins/maestro-skills/skills/main-session.md +18 -7
  177. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  178. package/scripts/ci/run-tests.mjs +47 -19
  179. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  180. package/scripts/collective/hook-runner.mjs +29 -2
  181. package/scripts/continuous-monitor.sh +13 -0
  182. package/scripts/cost/track-claude-usage.mjs +15 -0
  183. package/scripts/daemon/agent-daemon.mjs +408 -20
  184. package/scripts/daemon/assurance.mjs +48 -12
  185. package/scripts/daemon/cadence-consumer.mjs +218 -68
  186. package/scripts/daemon/cadence-handlers.mjs +73 -4
  187. package/scripts/daemon/classifier.mjs +75 -26
  188. package/scripts/daemon/context-compiler.mjs +51 -37
  189. package/scripts/daemon/deliver.mjs +30 -1
  190. package/scripts/daemon/dispatcher.mjs +595 -149
  191. package/scripts/daemon/health.mjs +14 -1
  192. package/scripts/daemon/maestro-daemon.mjs +11 -0
  193. package/scripts/daemon/prompt-builder.mjs +24 -0
  194. package/scripts/daemon/responder.mjs +246 -79
  195. package/scripts/daemon/sdk-version.mjs +98 -16
  196. package/scripts/eval/probe-gateway.mjs +635 -0
  197. package/scripts/eval/replay/extract.mjs +270 -0
  198. package/scripts/eval/replay/grade.mjs +260 -0
  199. package/scripts/eval/replay/lib/config.mjs +50 -0
  200. package/scripts/eval/replay/lib/effects.mjs +65 -0
  201. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  202. package/scripts/eval/replay/lib/judge.mjs +72 -0
  203. package/scripts/eval/replay/lib/redact.mjs +136 -0
  204. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  205. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  206. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  207. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  208. package/scripts/eval/replay/report.mjs +185 -0
  209. package/scripts/eval/replay/run.mjs +404 -0
  210. package/scripts/fleet/rollout.mjs +1094 -0
  211. package/scripts/hooks/pre-send-audit.sh +36 -245
  212. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  213. package/scripts/hooks/validate-state-yaml.sh +190 -0
  214. package/scripts/huddle/huddle-llm.mjs +361 -0
  215. package/scripts/huddle/huddle-server.mjs +46 -121
  216. package/scripts/local-triggers/autoupdate.sh +448 -78
  217. package/scripts/local-triggers/run-trigger.sh +13 -0
  218. package/scripts/maintenance/pin-integrity.mjs +364 -0
  219. package/scripts/poll-slack-events.sh +41 -9
  220. package/scripts/poller/slack-socket-mode.mjs +28 -3
  221. package/scripts/session/supervisor.mjs +80 -13
  222. package/scripts/spawn-session.sh +13 -0
  223. package/bin/maestro.test.mjs +0 -1574
  224. package/lib/action-executor.test.mjs +0 -871
  225. package/lib/archetype.test.mjs +0 -132
  226. package/lib/assurance/plan-note.test.mjs +0 -234
  227. package/lib/assurance/room-budget.test.mjs +0 -486
  228. package/lib/assurance/tier.test.mjs +0 -174
  229. package/lib/autonomy.test.mjs +0 -66
  230. package/lib/backlog.test.mjs +0 -302
  231. package/lib/backup/policy.test.mjs +0 -305
  232. package/lib/budget-escalate.test.mjs +0 -232
  233. package/lib/budget-guard.envelope.test.mjs +0 -476
  234. package/lib/budget-guard.test.mjs +0 -427
  235. package/lib/cadence-bus-requeue.test.mjs +0 -83
  236. package/lib/cadence-bus-schedule.test.mjs +0 -194
  237. package/lib/cadence-bus.test.mjs +0 -720
  238. package/lib/cadences.test.mjs +0 -230
  239. package/lib/capability/inventory.test.mjs +0 -232
  240. package/lib/capability.test.mjs +0 -78
  241. package/lib/channels/base-adapter.test.mjs +0 -590
  242. package/lib/channels/channels.test.mjs +0 -371
  243. package/lib/channels/contract.test.mjs +0 -162
  244. package/lib/channels/inbox-item.test.mjs +0 -368
  245. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  246. package/lib/channels/pairing.test.mjs +0 -270
  247. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  248. package/lib/channels/slack-adapter.test.mjs +0 -212
  249. package/lib/channels/telegram-adapter.test.mjs +0 -306
  250. package/lib/channels/voice/adapter.test.mjs +0 -278
  251. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  252. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  253. package/lib/charter.test.mjs +0 -89
  254. package/lib/claude-bin.test.mjs +0 -131
  255. package/lib/cli/board.test.mjs +0 -227
  256. package/lib/cli/design.test.mjs +0 -270
  257. package/lib/cli/doctor-checks.test.mjs +0 -336
  258. package/lib/cli/global-setup-extras.test.mjs +0 -462
  259. package/lib/cli/inbox.test.mjs +0 -230
  260. package/lib/cli/session-ack.test.mjs +0 -63
  261. package/lib/cli/session.test.mjs +0 -613
  262. package/lib/collective/capture.test.mjs +0 -121
  263. package/lib/collective/cards.test.mjs +0 -114
  264. package/lib/collective/config.test.mjs +0 -123
  265. package/lib/collective/global-config.test.mjs +0 -220
  266. package/lib/collective/global-skills.test.mjs +0 -126
  267. package/lib/collective/presence.test.mjs +0 -95
  268. package/lib/collective/recall.test.mjs +0 -116
  269. package/lib/collective/vendor-skills.test.mjs +0 -306
  270. package/lib/comms/send-gate.test.mjs +0 -770
  271. package/lib/comms.test.mjs +0 -41
  272. package/lib/context/budget.test.mjs +0 -252
  273. package/lib/context/history-scope.test.mjs +0 -79
  274. package/lib/cost/ledger-row.test.mjs +0 -183
  275. package/lib/design/design-md.test.mjs +0 -318
  276. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  277. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  278. package/lib/design/fixtures/foundation.json +0 -133
  279. package/lib/design/refresh-gate.test.mjs +0 -144
  280. package/lib/design/write.test.mjs +0 -241
  281. package/lib/diagnostics/alerts.test.mjs +0 -318
  282. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  283. package/lib/diagnostics/counters.test.mjs +0 -206
  284. package/lib/diagnostics/events.test.mjs +0 -290
  285. package/lib/diagnostics/otel.test.mjs +0 -196
  286. package/lib/diagnostics/trace.test.mjs +0 -251
  287. package/lib/env-compat.test.mjs +0 -104
  288. package/lib/execution/disposition.test.mjs +0 -553
  289. package/lib/execution/drive.test.mjs +0 -270
  290. package/lib/execution/effects.test.mjs +0 -344
  291. package/lib/execution/intake.test.mjs +0 -389
  292. package/lib/execution/journal.test.mjs +0 -261
  293. package/lib/execution/match.test.mjs +0 -235
  294. package/lib/execution/pipeline.test.mjs +0 -392
  295. package/lib/execution/route.test.mjs +0 -186
  296. package/lib/execution/surface-policy.test.mjs +0 -162
  297. package/lib/fs-atomic.test.mjs +0 -72
  298. package/lib/fs-ownership.test.mjs +0 -158
  299. package/lib/goals/admission.test.mjs +0 -164
  300. package/lib/goals/classify.test.mjs +0 -167
  301. package/lib/goals/collaborate.test.mjs +0 -336
  302. package/lib/goals/gaps.test.mjs +0 -284
  303. package/lib/goals/loop.test.mjs +0 -845
  304. package/lib/hooks/bus.test.mjs +0 -387
  305. package/lib/identity/persona.test.mjs +0 -142
  306. package/lib/kpi-sensors.test.mjs +0 -278
  307. package/lib/kpi.test.mjs +0 -244
  308. package/lib/learning/config.test.mjs +0 -75
  309. package/lib/learning/counters.test.mjs +0 -69
  310. package/lib/learning/curator-consolidate.test.mjs +0 -238
  311. package/lib/learning/curator.test.mjs +0 -106
  312. package/lib/learning/reflect.test.mjs +0 -0
  313. package/lib/learning/session-index.test.mjs +0 -125
  314. package/lib/learning/skill-writer.test.mjs +0 -210
  315. package/lib/mandate/audit.test.mjs +0 -195
  316. package/lib/mandate/contract.test.mjs +0 -185
  317. package/lib/mandate/derive.test.mjs +0 -274
  318. package/lib/mandate/model.test.mjs +0 -164
  319. package/lib/mandate/refresh.test.mjs +0 -389
  320. package/lib/mcp/server.test.mjs +0 -426
  321. package/lib/model-router/auth-profiles.test.mjs +0 -580
  322. package/lib/model-router/catalog.test.mjs +0 -385
  323. package/lib/model-router/economics.test.mjs +0 -438
  324. package/lib/model-router/failover.test.mjs +0 -439
  325. package/lib/model-router/health.test.mjs +0 -338
  326. package/lib/model-router/integration-coverage.test.mjs +0 -831
  327. package/lib/model-router/integration.test.mjs +0 -564
  328. package/lib/model-router/ledger.test.mjs +0 -415
  329. package/lib/model-router/llm-task.test.mjs +0 -392
  330. package/lib/model-router/org-credentials.test.mjs +0 -265
  331. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  332. package/lib/model-router/reconcile.test.mjs +0 -316
  333. package/lib/model-router/repair.test.mjs +0 -180
  334. package/lib/model-router/spawn.test.mjs +0 -446
  335. package/lib/model-router/taxonomy.test.mjs +0 -410
  336. package/lib/model-router.test.mjs +0 -1207
  337. package/lib/org/activity.test.mjs +0 -134
  338. package/lib/org/approvals.test.mjs +0 -216
  339. package/lib/org/awareness.test.mjs +0 -159
  340. package/lib/org/board-mine-cache.test.mjs +0 -53
  341. package/lib/org/board.test.mjs +0 -187
  342. package/lib/org/bootstrap-context.test.mjs +0 -153
  343. package/lib/org/client.test.mjs +0 -1206
  344. package/lib/org/cohort-client.test.mjs +0 -126
  345. package/lib/org/cost-sync.test.mjs +0 -153
  346. package/lib/org/doctor.test.mjs +0 -346
  347. package/lib/org/engagement-ledger.test.mjs +0 -112
  348. package/lib/org/engagement.test.mjs +0 -739
  349. package/lib/org/handoff.test.mjs +0 -269
  350. package/lib/org/inbound/directedness.test.mjs +0 -668
  351. package/lib/org/inbound/facts.test.mjs +0 -471
  352. package/lib/org/inbound/hydrate.test.mjs +0 -908
  353. package/lib/org/inbound/index.test.mjs +0 -429
  354. package/lib/org/inbound/project.test.mjs +0 -287
  355. package/lib/org/integration-tools.test.mjs +0 -160
  356. package/lib/org/keys.test.mjs +0 -92
  357. package/lib/org/knowledge.test.mjs +0 -326
  358. package/lib/org/leases.test.mjs +0 -235
  359. package/lib/org/mesh-directives.test.mjs +0 -110
  360. package/lib/org/mesh-integration.test.mjs +0 -127
  361. package/lib/org/mesh.test.mjs +0 -400
  362. package/lib/org/messaging.test.mjs +0 -471
  363. package/lib/org/param-contract.test.mjs +0 -477
  364. package/lib/org/policy.test.mjs +0 -237
  365. package/lib/org/protocol.checksum.test.mjs +0 -90
  366. package/lib/org/protocol.test.mjs +0 -323
  367. package/lib/org/push.test.mjs +0 -792
  368. package/lib/org/registry.test.mjs +0 -100
  369. package/lib/org/resource-tools.test.mjs +0 -361
  370. package/lib/org/tool-access.test.mjs +0 -144
  371. package/lib/org/tool-surface-integration.test.mjs +0 -120
  372. package/lib/org/tool-surface.test.mjs +0 -1268
  373. package/lib/org/typing.test.mjs +0 -291
  374. package/lib/org/ui-parity.test.mjs +0 -560
  375. package/lib/org/verify.test.mjs +0 -194
  376. package/lib/org/work-ledger.test.mjs +0 -273
  377. package/lib/plan/adoption-e2e.test.mjs +0 -366
  378. package/lib/plan/budget-enforcement.test.mjs +0 -400
  379. package/lib/plan/compile.test.mjs +0 -382
  380. package/lib/plan/emit.test.mjs +0 -269
  381. package/lib/plan/explain.test.mjs +0 -188
  382. package/lib/prompts/parallelism.test.mjs +0 -177
  383. package/lib/rag/rag.test.mjs +0 -505
  384. package/lib/rate-guard.test.mjs +0 -272
  385. package/lib/reactive-gate.test.mjs +0 -57
  386. package/lib/render.test.mjs +0 -68
  387. package/lib/resource-governor.test.mjs +0 -488
  388. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  389. package/lib/scheduling/jitter.test.mjs +0 -140
  390. package/lib/secrets/broker.test.mjs +0 -280
  391. package/lib/secrets/providers.test.mjs +0 -274
  392. package/lib/security/audit-engine.test.mjs +0 -424
  393. package/lib/security/coerce-args.test.mjs +0 -281
  394. package/lib/security/dangerous-tools.test.mjs +0 -68
  395. package/lib/security/external-content.test.mjs +0 -84
  396. package/lib/security/redact.test.mjs +0 -441
  397. package/lib/security/secret-equal.test.mjs +0 -55
  398. package/lib/session/config.test.mjs +0 -92
  399. package/lib/session/feed-core.test.mjs +0 -198
  400. package/lib/session/first-run.test.mjs +0 -121
  401. package/lib/session/frontdoor.test.mjs +0 -205
  402. package/lib/session/handoffs.test.mjs +0 -183
  403. package/lib/session/identity.test.mjs +0 -180
  404. package/lib/session/inbox-claims.test.mjs +0 -286
  405. package/lib/session/launch-args.test.mjs +0 -157
  406. package/lib/session/liveness.test.mjs +0 -100
  407. package/lib/session/status-summary.test.mjs +0 -118
  408. package/lib/session-permissions.test.mjs +0 -120
  409. package/lib/setup/claude-probe.test.mjs +0 -187
  410. package/lib/setup/completeness.test.mjs +0 -110
  411. package/lib/setup/context-pack.test.mjs +0 -89
  412. package/lib/setup/enrich.test.mjs +0 -115
  413. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  414. package/lib/setup/integration.test.mjs +0 -162
  415. package/lib/setup/io.test.mjs +0 -77
  416. package/lib/setup/runner.test.mjs +0 -132
  417. package/lib/setup/sections/identity.test.mjs +0 -234
  418. package/lib/setup/sections/inventory.test.mjs +0 -198
  419. package/lib/setup/sections/learning.test.mjs +0 -81
  420. package/lib/setup/sections/mandate.test.mjs +0 -388
  421. package/lib/setup/sections/messaging.test.mjs +0 -127
  422. package/lib/setup/sections/model.test.mjs +0 -240
  423. package/lib/setup/sections/org.test.mjs +0 -346
  424. package/lib/setup/sections/orgmail.test.mjs +0 -118
  425. package/lib/setup/sections/recovery.test.mjs +0 -98
  426. package/lib/setup/sections/subagents.test.mjs +0 -429
  427. package/lib/setup/sections/verify.test.mjs +0 -175
  428. package/lib/setup/sot.test.mjs +0 -81
  429. package/lib/setup/state.test.mjs +0 -115
  430. package/lib/singleton.test.mjs +0 -151
  431. package/lib/subagents/cli.test.mjs +0 -389
  432. package/lib/subagents/client.test.mjs +0 -309
  433. package/lib/subagents/gap.test.mjs +0 -234
  434. package/lib/subagents/lock.test.mjs +0 -248
  435. package/lib/subagents/manifest.test.mjs +0 -175
  436. package/lib/subagents/refs.test.mjs +0 -204
  437. package/lib/subagents/resolve.test.mjs +0 -422
  438. package/lib/subagents/schema.test.mjs +0 -328
  439. package/lib/telemetry/alerts.test.mjs +0 -109
  440. package/lib/telemetry/collect.test.mjs +0 -1274
  441. package/lib/tool-definitions-integration.test.mjs +0 -83
  442. package/lib/tool-definitions.test.mjs +0 -437
  443. package/lib/upgrade/global-refresh.test.mjs +0 -65
  444. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  445. package/lib/upgrade/post-steps.test.mjs +0 -200
  446. package/lib/upgrade/verify.test.mjs +0 -164
  447. package/lib/util/fetch-timeout.test.mjs +0 -202
  448. package/lib/util/reconnect.test.mjs +0 -369
  449. package/lib/util/unhandled.test.mjs +0 -216
  450. package/lib/voice/outbound.test.mjs +0 -69
  451. package/lib/voice/session-rotation.test.mjs +0 -114
  452. package/lib/voice/stt.test.mjs +0 -226
  453. package/lib/voice/voice.test.mjs +0 -990
  454. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  455. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  456. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  457. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  458. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  459. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  460. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  461. package/scripts/ci/check.test.mjs +0 -194
  462. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  463. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  464. package/scripts/collective/hook-runner.test.mjs +0 -173
  465. package/scripts/cost/fleet-digest.test.mjs +0 -207
  466. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  467. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  468. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  469. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  470. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  471. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  472. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  473. package/scripts/daemon/assurance.test.mjs +0 -1791
  474. package/scripts/daemon/board-mirror.test.mjs +0 -165
  475. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  476. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  477. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  478. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  479. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  480. package/scripts/daemon/classifier.test.mjs +0 -266
  481. package/scripts/daemon/classify-kind.test.mjs +0 -40
  482. package/scripts/daemon/context-compiler.test.mjs +0 -406
  483. package/scripts/daemon/deliver.test.mjs +0 -564
  484. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  485. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  486. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  487. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  488. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  489. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  490. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  491. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  492. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  493. package/scripts/daemon/integration.test.mjs +0 -149
  494. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  495. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  496. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  497. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  498. package/scripts/daemon/responder-cost.test.mjs +0 -68
  499. package/scripts/daemon/responder-history.test.mjs +0 -221
  500. package/scripts/daemon/sdk-version.test.mjs +0 -31
  501. package/scripts/daemon/session-lock.test.mjs +0 -252
  502. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  503. package/scripts/daemon/typing-registry.test.mjs +0 -102
  504. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  505. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  506. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  507. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  508. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  509. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  510. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  511. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  512. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  513. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  514. package/scripts/poller-launchd/install.test.mjs +0 -243
  515. package/scripts/restore-from-backup.test.mjs +0 -181
  516. package/scripts/session/feed.test.mjs +0 -196
  517. package/scripts/session/supervisor-sh.test.mjs +0 -218
  518. package/scripts/session/supervisor.test.mjs +0 -482
  519. package/scripts/setup/configure-macos.test.mjs +0 -306
  520. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  521. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  522. package/scripts/setup/generate-capability.test.mjs +0 -134
  523. package/scripts/setup/init-agent.test.mjs +0 -370
  524. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  525. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  526. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -1,486 +0,0 @@
1
- /**
2
- * room-budget.test.mjs — one room, one interim, whatever else is going on.
3
- *
4
- * The production failure this pins: the interim budget was per OBLIGATION and
5
- * never per ROOM. Eight agents with N concurrent obligations each in
6
- * `capital-formation` emitted up to N acks + 2N progress pings + N failure
7
- * notices into one channel — 9,187 messages in 21 days, ~45 % of them exact
8
- * duplicates, two independent timers narrating the same overlapping work at the
9
- * same timestamp. Every one of those decisions was individually correct.
10
- *
11
- * Run: node --test lib/assurance/room-budget.test.mjs
12
- */
13
-
14
- import { test, describe, beforeEach } from "node:test";
15
- import assert from "node:assert/strict";
16
- import { mkdtempSync, rmSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
17
- import { tmpdir } from "node:os";
18
- import { join } from "node:path";
19
-
20
- import {
21
- DEFAULT_ROOM_INTERIM_WINDOW_MS,
22
- DEFAULT_ROOM_NOTICE_WINDOW_MS,
23
- roomInterimWindowMs,
24
- roomNoticeWindowMs,
25
- roomKey,
26
- noticeKey,
27
- textDigest,
28
- interimAllowed,
29
- noticeAllowed,
30
- recordInterim,
31
- recordNotice,
32
- pruneLedger,
33
- claimRoomInterim,
34
- claimRoomNotice,
35
- ledgerPath,
36
- LEDGER_BASENAME,
37
- readLedger,
38
- _emptyLedger,
39
- } from "./room-budget.mjs";
40
-
41
- const ROOM_INTERIM_WINDOW_MS = DEFAULT_ROOM_INTERIM_WINDOW_MS;
42
-
43
- const T0 = 1_757_000_000_000; // a fixed instant; nothing here reads a clock
44
-
45
- // ---------------------------------------------------------------------------
46
- // The pure half
47
- // ---------------------------------------------------------------------------
48
-
49
- describe("roomKey — a room is (service, channel) and nothing else", () => {
50
- test("service and channel together name the room", () => {
51
- assert.equal(roomKey({ service: "cohort", channel: "C-CAPFORM" }), "cohort|c-capform");
52
- });
53
-
54
- test("case and surrounding space never make two rooms out of one", () => {
55
- assert.equal(roomKey({ service: "Cohort", channel: " C-CapForm " }), roomKey({ service: "cohort", channel: "c-capform" }));
56
- });
57
-
58
- test("a missing channel is still a room, not a crash", () => {
59
- assert.equal(typeof roomKey({ service: "cohort", channel: null }), "string");
60
- assert.equal(typeof roomKey({}), "string");
61
- assert.equal(typeof roomKey(), "string");
62
- });
63
-
64
- test("two different channels on one service are two rooms", () => {
65
- assert.notEqual(roomKey({ service: "cohort", channel: "A" }), roomKey({ service: "cohort", channel: "B" }));
66
- });
67
-
68
- test("the same channel name on two services is two rooms", () => {
69
- assert.notEqual(roomKey({ service: "cohort", channel: "general" }), roomKey({ service: "slack", channel: "general" }));
70
- });
71
- });
72
-
73
- describe("interimAllowed — the pure decision", () => {
74
- test("an empty ledger allows the first interim", () => {
75
- const v = interimAllowed({ ledger: _emptyLedger(), service: "cohort", channel: "C1", now: T0 });
76
- assert.equal(v.allowed, true);
77
- assert.equal(v.reason, "room-budget-free");
78
- });
79
-
80
- test("a second interim inside the window is refused, and says which room and when", () => {
81
- const led = recordInterim({ ledger: _emptyLedger(), service: "cohort", channel: "C1", now: T0 });
82
- const v = interimAllowed({ ledger: led, service: "cohort", channel: "C1", now: T0 + 60_000 });
83
- assert.equal(v.allowed, false);
84
- assert.equal(v.reason, "room-budget-spent");
85
- assert.equal(v.lastAt, T0);
86
- assert.equal(v.room, roomKey({ service: "cohort", channel: "C1" }));
87
- });
88
-
89
- test("the window reopens exactly at the boundary, not a millisecond early", () => {
90
- const led = recordInterim({ ledger: _emptyLedger(), service: "cohort", channel: "C1", now: T0 });
91
- assert.equal(interimAllowed({ ledger: led, service: "cohort", channel: "C1", now: T0 + ROOM_INTERIM_WINDOW_MS - 1 }).allowed, false);
92
- assert.equal(interimAllowed({ ledger: led, service: "cohort", channel: "C1", now: T0 + ROOM_INTERIM_WINDOW_MS }).allowed, true);
93
- });
94
-
95
- test("a spent room never silences a DIFFERENT room", () => {
96
- const led = recordInterim({ ledger: _emptyLedger(), service: "cohort", channel: "C1", now: T0 });
97
- assert.equal(interimAllowed({ ledger: led, service: "cohort", channel: "C2", now: T0 + 1 }).allowed, true);
98
- assert.equal(interimAllowed({ ledger: led, service: "slack", channel: "C1", now: T0 + 1 }).allowed, true);
99
- });
100
-
101
- test("a clock that goes backwards does not hand out a free interim", () => {
102
- // NTP steps, a restored snapshot, a test harness. `now` before the recorded
103
- // stamp must read as "inside the window", never as "the window elapsed".
104
- const led = recordInterim({ ledger: _emptyLedger(), service: "cohort", channel: "C1", now: T0 });
105
- assert.equal(interimAllowed({ ledger: led, service: "cohort", channel: "C1", now: T0 - 60 * 60_000 }).allowed, false);
106
- });
107
-
108
- test("a corrupt or absent entry reads as 'never spoken here', never as a permanent gag", () => {
109
- for (const bad of [{ rooms: null }, { rooms: { "cohort|c1": "yesterday" } }, {}, null, undefined, "nonsense"]) {
110
- assert.equal(interimAllowed({ ledger: bad, service: "cohort", channel: "C1", now: T0 }).allowed, true, `${JSON.stringify(bad)} must fail open`);
111
- }
112
- });
113
-
114
- test("the decision is pure: it never mutates the ledger it is given", () => {
115
- const led = recordInterim({ ledger: _emptyLedger(), service: "cohort", channel: "C1", now: T0 });
116
- const before = JSON.stringify(led);
117
- interimAllowed({ ledger: led, service: "cohort", channel: "C1", now: T0 + 1 });
118
- assert.equal(JSON.stringify(led), before);
119
- });
120
-
121
- test("the window is env-tunable and defaults to fifteen minutes", () => {
122
- assert.equal(DEFAULT_ROOM_INTERIM_WINDOW_MS, 15 * 60_000);
123
- const led = recordInterim({ ledger: _emptyLedger(), service: "cohort", channel: "C1", now: T0 });
124
- assert.equal(interimAllowed({ ledger: led, service: "cohort", channel: "C1", now: T0 + 2000, windowMs: 1000 }).allowed, true);
125
- });
126
- });
127
-
128
- describe("recordInterim / pruneLedger", () => {
129
- test("recording returns a NEW ledger and leaves the old one untouched", () => {
130
- const led = _emptyLedger();
131
- const next = recordInterim({ ledger: led, service: "cohort", channel: "C1", now: T0 });
132
- assert.notEqual(led, next);
133
- assert.deepEqual(led.rooms, {});
134
- assert.equal(next.rooms[roomKey({ service: "cohort", channel: "C1" })], T0);
135
- });
136
-
137
- test("the ledger does not grow without bound — entries older than two windows are dropped", () => {
138
- // The store is a durable file on a machine that runs for months. An entry
139
- // that can never refuse anything again is landfill.
140
- let led = _emptyLedger();
141
- for (let i = 0; i < 50; i++) led = recordInterim({ ledger: led, service: "cohort", channel: `OLD-${i}`, now: T0 });
142
- led = recordInterim({ ledger: led, service: "cohort", channel: "FRESH", now: T0 + 3 * ROOM_INTERIM_WINDOW_MS });
143
- const pruned = pruneLedger({ ledger: led, now: T0 + 3 * ROOM_INTERIM_WINDOW_MS });
144
- assert.equal(Object.keys(pruned.rooms).length, 1);
145
- assert.ok(pruned.rooms[roomKey({ service: "cohort", channel: "FRESH" })]);
146
- });
147
-
148
- test("pruning never drops an entry that can still refuse an interim", () => {
149
- const led = recordInterim({ ledger: _emptyLedger(), service: "cohort", channel: "C1", now: T0 });
150
- const pruned = pruneLedger({ ledger: led, now: T0 + ROOM_INTERIM_WINDOW_MS });
151
- assert.equal(interimAllowed({ ledger: pruned, service: "cohort", channel: "C1", now: T0 + ROOM_INTERIM_WINDOW_MS - 1 }).allowed, false);
152
- });
153
- });
154
-
155
- // ---------------------------------------------------------------------------
156
- // The durable edge
157
- // ---------------------------------------------------------------------------
158
-
159
- describe("claimRoomInterim — the durable edge, with an injected clock and fs", () => {
160
- let ROOT;
161
- beforeEach(() => {
162
- if (ROOT) { try { rmSync(ROOT, { recursive: true, force: true }); } catch { /* */ } }
163
- ROOT = mkdtempSync(join(tmpdir(), "room-budget-"));
164
- });
165
-
166
- test("THE FLOOD FIX: N concurrent obligations in one room yield exactly ONE interim", () => {
167
- // 8 agents' worth of concurrent asks, arriving together, as measured.
168
- const said = [];
169
- for (let i = 0; i < 40; i++) {
170
- const v = claimRoomInterim({ service: "cohort", channel: "capital-formation", now: T0 + i * 1000, agentRoot: ROOT });
171
- if (v.allowed) said.push(i);
172
- }
173
- assert.deepEqual(said, [0], `expected one interim across 40 obligations, got ${said.length}`);
174
- });
175
-
176
- test("…and the next window allows exactly one more, not a backlog burst", () => {
177
- for (let i = 0; i < 40; i++) claimRoomInterim({ service: "cohort", channel: "capital-formation", now: T0 + i * 1000, agentRoot: ROOT });
178
- let allowed = 0;
179
- for (let i = 0; i < 40; i++) {
180
- if (claimRoomInterim({ service: "cohort", channel: "capital-formation", now: T0 + ROOM_INTERIM_WINDOW_MS + i * 1000, agentRoot: ROOT }).allowed) allowed++;
181
- }
182
- assert.equal(allowed, 1);
183
- });
184
-
185
- test("a refused claim writes nothing and does not move the window", () => {
186
- claimRoomInterim({ service: "cohort", channel: "C1", now: T0, agentRoot: ROOT });
187
- claimRoomInterim({ service: "cohort", channel: "C1", now: T0 + 10 * 60_000, agentRoot: ROOT });
188
- // If the refused claim had stamped the ledger, the window would run from
189
- // T0+10min and the room would stay gagged past T0+15min.
190
- assert.equal(claimRoomInterim({ service: "cohort", channel: "C1", now: T0 + ROOM_INTERIM_WINDOW_MS, agentRoot: ROOT }).allowed, true);
191
- });
192
-
193
- test("busy rooms never gag quiet ones", () => {
194
- for (let i = 0; i < 20; i++) claimRoomInterim({ service: "cohort", channel: "busy", now: T0 + i, agentRoot: ROOT });
195
- assert.equal(claimRoomInterim({ service: "cohort", channel: "quiet", now: T0 + 100, agentRoot: ROOT }).allowed, true);
196
- });
197
-
198
- test("the store is durable — a fresh process sees the spend", () => {
199
- claimRoomInterim({ service: "cohort", channel: "C1", now: T0, agentRoot: ROOT });
200
- const onDisk = JSON.parse(readFileSync(ledgerPath(ROOT), "utf-8"));
201
- assert.equal(onDisk.rooms[roomKey({ service: "cohort", channel: "C1" })], T0);
202
- assert.equal(readLedger({ agentRoot: ROOT }).rooms[roomKey({ service: "cohort", channel: "C1" })], T0);
203
- });
204
-
205
- test("it lives beside the obligation ledger it governs", () => {
206
- assert.equal(ledgerPath(ROOT), join(ROOT, "state", "obligations", "room-budget.json"));
207
- });
208
-
209
- test("an unreadable store fails OPEN — a broken file may not silence a human", () => {
210
- mkdirSync(join(ROOT, "state", "obligations"), { recursive: true });
211
- writeFileSync(ledgerPath(ROOT), "{ this is not json");
212
- const v = claimRoomInterim({ service: "cohort", channel: "C1", now: T0, agentRoot: ROOT });
213
- assert.equal(v.allowed, true);
214
- assert.equal(v.degraded, true, "…but it SAYS it is degraded, so the condition is measurable");
215
- });
216
-
217
- test("an unwritable store still lets the message out, and says so", () => {
218
- const v = claimRoomInterim({
219
- service: "cohort", channel: "C1", now: T0, agentRoot: ROOT,
220
- deps: { writeImpl: () => { throw new Error("EROFS"); } },
221
- });
222
- assert.equal(v.allowed, true);
223
- assert.equal(v.degraded, true);
224
- });
225
-
226
- test("the clock is injected, never read from the wall", () => {
227
- // Passing `now` is the contract; omitting it is a caller bug, not a licence
228
- // to read Date.now() from inside a decision module.
229
- const v = claimRoomInterim({ service: "cohort", channel: "C1", agentRoot: ROOT });
230
- assert.equal(v.allowed, false);
231
- assert.equal(v.reason, "no-clock");
232
- });
233
-
234
- test("fs is injectable end to end — no real filesystem needed", () => {
235
- let stored = null;
236
- const deps = {
237
- readImpl: () => stored,
238
- writeImpl: (_p, obj) => { stored = obj; },
239
- };
240
- assert.equal(claimRoomInterim({ service: "cohort", channel: "C1", now: T0, agentRoot: ROOT, deps }).allowed, true);
241
- assert.equal(claimRoomInterim({ service: "cohort", channel: "C1", now: T0 + 1, agentRoot: ROOT, deps }).allowed, false);
242
- assert.equal(stored.rooms[roomKey({ service: "cohort", channel: "C1" })], T0);
243
- });
244
- });
245
-
246
- describe("claimRoomInterim commit:false — ask without spending", () => {
247
- let ROOT;
248
- beforeEach(() => {
249
- if (ROOT) { try { rmSync(ROOT, { recursive: true, force: true }); } catch { /* */ } }
250
- ROOT = mkdtempSync(join(tmpdir(), "room-budget-peek-"));
251
- });
252
-
253
- test("a peek does not spend the budget", () => {
254
- // The interim's text comes from a model call that can fail. Spending the
255
- // room's fifteen minutes BEFORE the message exists gags the room in
256
- // exchange for nothing — so the caller asks first, composes, then claims.
257
- const peek = claimRoomInterim({ service: "cohort", channel: "C1", now: T0, agentRoot: ROOT, commit: false });
258
- assert.equal(peek.allowed, true);
259
- assert.equal(peek.committed, false);
260
- assert.equal(claimRoomInterim({ service: "cohort", channel: "C1", now: T0 + 1, agentRoot: ROOT }).allowed, true, "the peek left the budget unspent");
261
- });
262
-
263
- test("a peek still reports a spent room", () => {
264
- claimRoomInterim({ service: "cohort", channel: "C1", now: T0, agentRoot: ROOT });
265
- const peek = claimRoomInterim({ service: "cohort", channel: "C1", now: T0 + 1000, agentRoot: ROOT, commit: false });
266
- assert.equal(peek.allowed, false);
267
- assert.equal(peek.reason, "room-budget-spent");
268
- });
269
-
270
- test("a committing claim says so", () => {
271
- assert.equal(claimRoomInterim({ service: "cohort", channel: "C1", now: T0, agentRoot: ROOT }).committed, true);
272
- });
273
- });
274
-
275
- // ---------------------------------------------------------------------------
276
- // The HONEST SCOPE of the guarantee
277
- // ---------------------------------------------------------------------------
278
-
279
- describe("the budget is per (SEAT, service, channel) — not per (service, channel)", () => {
280
- let A, B;
281
- beforeEach(() => {
282
- A = mkdtempSync(join(tmpdir(), "room-seat-a-"));
283
- B = mkdtempSync(join(tmpdir(), "room-seat-b-"));
284
- });
285
-
286
- test("two seats in ONE room each get their own interim, because they share no store", () => {
287
- // This is the arithmetic the module header now states rather than glosses.
288
- // `agentRoot` is AGENT_DIR — one agent's repo on one Mac mini. There is no
289
- // shared filesystem between seats, so the guarantee this module can make is
290
- // per seat. The measured room had EIGHT seats in it: the ceiling this gate
291
- // alone imposes there is 8 x (60/15) = 32 interims an hour, against a
292
- // measured content-free rate of ~14/h. It is the backstop, not the thing
293
- // that removed the measured flood.
294
- //
295
- // Pinned as a TEST rather than left in prose because the comment that said
296
- // otherwise survived a review: an operator who reads "one room, one message"
297
- // will size the next change wrong.
298
- assert.equal(claimRoomInterim({ service: "cohort", channel: "capital-formation", now: T0, agentRoot: A }).allowed, true);
299
- assert.equal(claimRoomInterim({ service: "cohort", channel: "capital-formation", now: T0 + 1, agentRoot: B }).allowed, true,
300
- "a second SEAT is not gagged by the first — there is no store between them");
301
- // …and within either seat the guarantee still holds absolutely.
302
- assert.equal(claimRoomInterim({ service: "cohort", channel: "capital-formation", now: T0 + 2, agentRoot: A }).allowed, false);
303
- assert.equal(claimRoomInterim({ service: "cohort", channel: "capital-formation", now: T0 + 3, agentRoot: B }).allowed, false);
304
- });
305
-
306
- test("the eight-seat ceiling is exactly eight per window, and no more", () => {
307
- const seats = [];
308
- for (let i = 0; i < 8; i++) seats.push(mkdtempSync(join(tmpdir(), `room-seat-${i}-`)));
309
- let spoke = 0;
310
- // Each seat tries ten times inside one window; each may speak once.
311
- for (const seat of seats) {
312
- for (let i = 0; i < 10; i++) {
313
- if (claimRoomInterim({ service: "cohort", channel: "capital-formation", now: T0 + i, agentRoot: seat }).allowed) spoke++;
314
- }
315
- }
316
- assert.equal(spoke, 8, "eight seats, eight interims per window — the honest ceiling");
317
- for (const seat of seats) rmSync(seat, { recursive: true, force: true });
318
- });
319
- });
320
-
321
- describe("the window default is resolved at the EDGE, never captured at import", () => {
322
- // The gate report for this package claimed the pure modules read no
323
- // clock/env/fs. A `const X = process.env.Y` at module scope falsified that
324
- // and, more concretely, meant an operator who exported the variable after the
325
- // first import silently got the stale default.
326
- test("roomInterimWindowMs reads the env it is given, every time it is called", () => {
327
- assert.equal(roomInterimWindowMs({}), DEFAULT_ROOM_INTERIM_WINDOW_MS);
328
- assert.equal(roomInterimWindowMs({ ASSURANCE_ROOM_INTERIM_WINDOW_MS: "60000" }), 60_000);
329
- assert.equal(roomNoticeWindowMs({ ASSURANCE_ROOM_NOTICE_WINDOW_MS: "1000" }), 1000);
330
- // Garbage falls back rather than producing NaN, which would compare false
331
- // against every elapsed time and make the budget decorative.
332
- assert.equal(roomInterimWindowMs({ ASSURANCE_ROOM_INTERIM_WINDOW_MS: "soon" }), DEFAULT_ROOM_INTERIM_WINDOW_MS);
333
- assert.equal(roomNoticeWindowMs(undefined) > 0, true);
334
- });
335
-
336
- test("an env set AFTER this module was first imported still takes effect", () => {
337
- const ROOT = mkdtempSync(join(tmpdir(), "room-env-"));
338
- const prev = process.env.ASSURANCE_ROOM_INTERIM_WINDOW_MS;
339
- try {
340
- process.env.ASSURANCE_ROOM_INTERIM_WINDOW_MS = "1000";
341
- assert.equal(claimRoomInterim({ service: "cohort", channel: "C1", now: T0, agentRoot: ROOT }).allowed, true);
342
- assert.equal(claimRoomInterim({ service: "cohort", channel: "C1", now: T0 + 999, agentRoot: ROOT }).allowed, false);
343
- assert.equal(claimRoomInterim({ service: "cohort", channel: "C1", now: T0 + 1000, agentRoot: ROOT }).allowed, true,
344
- "the one-second window was honoured, so the value was not folded at import time");
345
- } finally {
346
- if (prev === undefined) delete process.env.ASSURANCE_ROOM_INTERIM_WINDOW_MS;
347
- else process.env.ASSURANCE_ROOM_INTERIM_WINDOW_MS = prev;
348
- rmSync(ROOT, { recursive: true, force: true });
349
- }
350
- });
351
-
352
- test("the pure decision never reads the environment — it defaults to the CONSTANT", () => {
353
- const prev = process.env.ASSURANCE_ROOM_INTERIM_WINDOW_MS;
354
- try {
355
- process.env.ASSURANCE_ROOM_INTERIM_WINDOW_MS = "1000";
356
- const led = recordInterim({ ledger: _emptyLedger(), service: "cohort", channel: "C1", now: T0 });
357
- assert.equal(
358
- interimAllowed({ ledger: led, service: "cohort", channel: "C1", now: T0 + 2000 }).allowed,
359
- false,
360
- "the pure function used the 15-minute constant, not the environment",
361
- );
362
- } finally {
363
- if (prev === undefined) delete process.env.ASSURANCE_ROOM_INTERIM_WINDOW_MS;
364
- else process.env.ASSURANCE_ROOM_INTERIM_WINDOW_MS = prev;
365
- }
366
- });
367
- });
368
-
369
- // ---------------------------------------------------------------------------
370
- // Identical outcome notices
371
- // ---------------------------------------------------------------------------
372
-
373
- describe("textDigest / noticeKey", () => {
374
- test("the same sentence digests the same however it is spaced", () => {
375
- assert.equal(textDigest("Couldn't finish this — it timed out."), textDigest(" Couldn't finish this — it timed out. "));
376
- });
377
-
378
- test("a different sentence digests differently", () => {
379
- assert.notEqual(textDigest("Hit a problem — it timed out. Retrying now."), textDigest("Couldn't finish this — it timed out."));
380
- });
381
-
382
- test("the key separates room, notice id and sentence", () => {
383
- const base = { service: "cohort", channel: "C1", notice: "failure:final", text: "same" };
384
- assert.notEqual(noticeKey(base), noticeKey({ ...base, channel: "C2" }));
385
- assert.notEqual(noticeKey(base), noticeKey({ ...base, notice: "failure:retrying" }));
386
- assert.notEqual(noticeKey(base), noticeKey({ ...base, text: "different" }));
387
- assert.equal(noticeKey(base), noticeKey({ ...base }));
388
- });
389
- });
390
-
391
- describe("claimRoomNotice — a byte-identical outcome notice is not said twice in one room", () => {
392
- let ROOT;
393
- const STALE = "Couldn't finish this — the work never came back with a result and has now outrun its time limit. I've stopped retrying and flagged it so it isn't lost.";
394
- beforeEach(() => { ROOT = mkdtempSync(join(tmpdir(), "room-notice-")); });
395
-
396
- test("THE SIBLING CASE: twenty debts, one sentence, one message", () => {
397
- // The per-obligation notice ledger cannot reach this: twenty separate asks
398
- // are twenty separate records, and the stale sentence carries nothing about
399
- // which ask it is, so the reader gets the identical line twenty times and
400
- // cannot tell any of them apart.
401
- let said = 0;
402
- for (let i = 0; i < 20; i++) {
403
- if (claimRoomNotice({ service: "cohort", channel: "C-CAPFORM", notice: "failure:final", text: STALE, now: T0 + i * 1000, agentRoot: ROOT }).allowed) said++;
404
- }
405
- assert.equal(said, 1, `twenty identical failure sentences must reach one room once — got ${said}`);
406
- });
407
-
408
- test("a DIFFERENT sentence is never suppressed, at any volume", () => {
409
- // This is the safety argument. Suppressing information a reader does not
410
- // have re-creates the original bug: a person never told their work died.
411
- let said = 0;
412
- for (let i = 0; i < 20; i++) {
413
- const text = `Couldn't finish this — cause number ${i}. I've stopped retrying.`;
414
- if (claimRoomNotice({ service: "cohort", channel: "C-CAPFORM", notice: "failure:final", text, now: T0 + i * 1000, agentRoot: ROOT }).allowed) said++;
415
- }
416
- assert.equal(said, 20, "every distinct fact is still said");
417
- });
418
-
419
- test("'a retry is running' and 'I have stopped' stay separately sayable", () => {
420
- const retrying = "Hit a problem — it timed out. Retrying now; if it fails again I'll come straight back.";
421
- assert.equal(claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:retrying", text: retrying, now: T0, agentRoot: ROOT }).allowed, true);
422
- assert.equal(claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:final", text: STALE, now: T0 + 1, agentRoot: ROOT }).allowed, true);
423
- });
424
-
425
- test("a different ROOM is never gagged by a busy one", () => {
426
- claimRoomNotice({ service: "cohort", channel: "busy", notice: "failure:final", text: STALE, now: T0, agentRoot: ROOT });
427
- assert.equal(claimRoomNotice({ service: "cohort", channel: "quiet", notice: "failure:final", text: STALE, now: T0 + 1, agentRoot: ROOT }).allowed, true);
428
- });
429
-
430
- test("the window reopens, because the same fact a day later is news again", () => {
431
- claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:final", text: STALE, now: T0, agentRoot: ROOT });
432
- assert.equal(claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:final", text: STALE, now: T0 + DEFAULT_ROOM_NOTICE_WINDOW_MS - 1, agentRoot: ROOT }).allowed, false);
433
- assert.equal(claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:final", text: STALE, now: T0 + DEFAULT_ROOM_NOTICE_WINDOW_MS, agentRoot: ROOT }).allowed, true);
434
- });
435
-
436
- test("commit:false asks without spending", () => {
437
- const peek = claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:final", text: STALE, now: T0, agentRoot: ROOT, commit: false });
438
- assert.equal(peek.allowed, true);
439
- assert.equal(peek.committed, false);
440
- assert.equal(claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:final", text: STALE, now: T0 + 1, agentRoot: ROOT }).allowed, true,
441
- "the peek left it unspent — a send that never happened must not silence the next one");
442
- });
443
-
444
- test("FAIL-OPEN, and the OPPOSITE way to the interim: no clock ⇒ say it", () => {
445
- // An interim is a courtesy and is safe to drop. "Your work died" is not.
446
- const v = claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:final", text: STALE, agentRoot: ROOT });
447
- assert.equal(v.allowed, true);
448
- assert.equal(v.degraded, true);
449
- });
450
-
451
- test("an unreadable ledger lets the notice through, flagged", () => {
452
- mkdirSync(join(ROOT, "state", "obligations"), { recursive: true });
453
- writeFileSync(ledgerPath(ROOT), "{ not json");
454
- const v = claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:final", text: STALE, now: T0, agentRoot: ROOT });
455
- assert.equal(v.allowed, true);
456
- assert.equal(v.degraded, true);
457
- });
458
-
459
- test("interims and notices share one file without evicting each other", () => {
460
- claimRoomInterim({ service: "cohort", channel: "C1", now: T0, agentRoot: ROOT });
461
- claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:final", text: STALE, now: T0 + 1, agentRoot: ROOT });
462
- const led = JSON.parse(readFileSync(ledgerPath(ROOT), "utf-8"));
463
- assert.equal(Object.keys(led.rooms).length, 1, "the interim spend survived the notice write");
464
- assert.equal(Object.keys(led.notices).length, 1);
465
- assert.equal(claimRoomInterim({ service: "cohort", channel: "C1", now: T0 + 2, agentRoot: ROOT }).allowed, false);
466
- assert.equal(claimRoomNotice({ service: "cohort", channel: "C1", notice: "failure:final", text: STALE, now: T0 + 3, agentRoot: ROOT }).allowed, false);
467
- });
468
-
469
- test("the notice map is pruned too, so the file cannot grow without bound", () => {
470
- let led = _emptyLedger();
471
- for (let i = 0; i < 50; i++) {
472
- led = recordNotice({ ledger: led, service: "cohort", channel: "C1", notice: "failure:final", text: `old ${i}`, now: T0 });
473
- }
474
- const pruned = pruneLedger({ ledger: led, now: T0 + 3 * DEFAULT_ROOM_NOTICE_WINDOW_MS });
475
- assert.equal(Object.keys(pruned.notices).length, 0);
476
- });
477
- });
478
-
479
- describe("the ledger basename is exported, because its reader has to skip it", () => {
480
- test("LEDGER_BASENAME is the file ledgerPath produces", () => {
481
- // `assurance.listObligations` parses every *.json in this directory as a
482
- // debt record. It skips this one BY NAME, so the name must be one string.
483
- assert.equal(ledgerPath("/tmp/x").endsWith(`/${LEDGER_BASENAME}`), true);
484
- assert.equal(LEDGER_BASENAME, "room-budget.json");
485
- });
486
- });
@@ -1,174 +0,0 @@
1
- /**
2
- * tier.test.mjs — the reply tier, across the whole classifier matrix.
3
- *
4
- * The tier is the ONE decision that says whether a human hears anything before
5
- * the answer. Getting it wrong in the loud direction is the 3,069 generic acks
6
- * and 961 clock-derived nags measured in production on 2026-09-12; getting it
7
- * wrong in the quiet direction is the silence the obligation ledger was built
8
- * to end. So every cell of the matrix is pinned here rather than sampled.
9
- *
10
- * Run: node --test lib/assurance/tier.test.mjs
11
- */
12
-
13
- import { test, describe } from "node:test";
14
- import assert from "node:assert/strict";
15
-
16
- import {
17
- replyTier,
18
- TIERS,
19
- ANSWER_MAX_RUNG,
20
- PLAN_MIN_RUNG,
21
- PLAN_ACTIONS,
22
- PLAN_PRIORITIES,
23
- } from "./tier.mjs";
24
-
25
- /** Every value `classifier.mjs` can emit — the real enums, not a sample. */
26
- const ACTIONS = ["respond", "draft", "research", "queue", "archive", "ignore"];
27
- const PRIORITIES = ["critical", "high", "normal", "ignore"];
28
- /** Every rung in `lib/execution/route.RUNGS`, plus "not routed yet". */
29
- const RUNGS = [0, 1, 2, 3, 4, 5, null];
30
-
31
- describe("replyTier — the three tiers and nothing else", () => {
32
- test("every cell of the matrix returns one of exactly three tiers", () => {
33
- for (const action of ACTIONS) {
34
- for (const priority of PRIORITIES) {
35
- for (const rung of RUNGS) {
36
- for (const answerable of [true, false, null, undefined]) {
37
- for (const willSpawnSession of [true, false]) {
38
- const t = replyTier({ answerable, action, priority, rung, willSpawnSession });
39
- assert.ok(TIERS.includes(t), `{${action}/${priority}/rung:${rung}/answerable:${answerable}/spawn:${willSpawnSession}} → ${t}`);
40
- }
41
- }
42
- }
43
- }
44
- }
45
- });
46
-
47
- test("no session spawning ⇒ answer tier, whatever the classifier said", () => {
48
- // The reply arrives in this turn. An interim in front of an answer the
49
- // human is about to read is the definition of content-free traffic.
50
- for (const action of ACTIONS) {
51
- for (const priority of PRIORITIES) {
52
- for (const rung of RUNGS) {
53
- assert.equal(
54
- replyTier({ answerable: false, action, priority, rung, willSpawnSession: false }),
55
- "answer",
56
- `${action}/${priority}/rung:${rung} with no session must be "answer"`,
57
- );
58
- }
59
- }
60
- }
61
- });
62
-
63
- test("answerable at rung 0 or 1 is the answer tier — the reply IS the acknowledgement", () => {
64
- // `willSpawnSession` UNKNOWN: the caller is asking hypothetically (which is
65
- // how WP-2's effort router will use it), so the classifier verdict decides.
66
- for (const rung of [0, ANSWER_MAX_RUNG]) {
67
- assert.equal(replyTier({ answerable: true, action: "respond", priority: "high", rung }), "answer");
68
- assert.equal(replyTier({ answerable: true, action: "respond", priority: "high", rung, willSpawnSession: false }), "answer");
69
- }
70
- });
71
-
72
- test("REGRESSION: an ASSERTED session disqualifies the answer tier, however answerable the item looked", () => {
73
- // THE QUICK-PATH FALL-THROUGH. agent-daemon.mjs tries a quick reply first;
74
- // when that reply fails transiently or is blocked by validation it falls
75
- // THROUGH to a full session dispatch and asks for a tier with
76
- // willSpawnSession:true while classResult.answerable is still true. The
77
- // answer tier's whole premise — "the reply arrives in this turn" — is
78
- // exactly what the fall-through has falsified.
79
- //
80
- // Latent rather than live only because `rung` is null today (R13). The
81
- // moment WP-2 routes it, that item would have landed in `answer`,
82
- // shouldAcknowledge would have returned ack:false, the sweep would have
83
- // read the durable tier as never-speak, and a 15-45 minute session would
84
- // have run with the human hearing NOTHING at all. Pinned before WP-2 lands
85
- // on top of it.
86
- for (const rung of [0, ANSWER_MAX_RUNG]) {
87
- assert.equal(
88
- replyTier({ answerable: true, action: "respond", priority: "high", rung, willSpawnSession: true }),
89
- "work",
90
- `rung ${rung}: a spawning session speaks once at most — it is never silent`,
91
- );
92
- }
93
- // …and the plan tier still outranks it, because rung 3+ is a project.
94
- assert.equal(replyTier({ answerable: true, action: "respond", priority: "high", rung: 4, willSpawnSession: true }), "plan");
95
- });
96
-
97
- test("a routed rung of 3 or above is the plan tier, whatever the action", () => {
98
- for (const rung of RUNGS.filter((r) => r != null && r >= PLAN_MIN_RUNG)) {
99
- for (const action of ACTIONS) {
100
- assert.equal(
101
- replyTier({ answerable: false, action, priority: "normal", rung, willSpawnSession: true }),
102
- "plan",
103
- `rung ${rung} / ${action} must be "plan"`,
104
- );
105
- }
106
- }
107
- });
108
-
109
- test("research or draft at critical/high priority is the plan tier even with no routed rung", () => {
110
- for (const action of PLAN_ACTIONS) {
111
- for (const priority of PLAN_PRIORITIES) {
112
- assert.equal(replyTier({ answerable: false, action, priority, rung: null, willSpawnSession: true }), "plan");
113
- }
114
- }
115
- });
116
-
117
- test("…and the same actions at normal priority are only work", () => {
118
- for (const action of PLAN_ACTIONS) {
119
- for (const priority of ["normal", "ignore"]) {
120
- assert.equal(replyTier({ answerable: false, action, priority, rung: null, willSpawnSession: true }), "work");
121
- }
122
- }
123
- });
124
-
125
- test("the ordinary directed ask — respond/high, unrouted — is work: silence, then at most one line", () => {
126
- // This is the shape of the overwhelming majority of the 10,667 measured
127
- // agent messages. It must NOT be plan tier, or the flood returns wearing a
128
- // better costume.
129
- assert.equal(replyTier({ answerable: false, action: "respond", priority: "high", rung: null, willSpawnSession: true }), "work");
130
- assert.equal(replyTier({ answerable: false, action: "queue", priority: "critical", rung: null, willSpawnSession: true }), "work");
131
- });
132
-
133
- test("an ABSENT rung is never read as the cheapest rung", () => {
134
- // `lib/backlog` already learned this: absence is not rung 0. A missing rung
135
- // may not buy the answer tier's silence-on-the-strength-of-a-quick-path,
136
- // and may not buy the plan tier's licence to speak either.
137
- assert.equal(replyTier({ answerable: true, action: "respond", priority: "high", rung: null, willSpawnSession: true }), "work");
138
- assert.equal(replyTier({ answerable: true, action: "respond", priority: "high", rung: undefined, willSpawnSession: true }), "work");
139
- });
140
-
141
- test("a non-numeric or out-of-range rung is treated as unrouted, never as a licence to speak", () => {
142
- for (const rung of ["3", "team", NaN, Infinity, -1, 6, {}, []]) {
143
- assert.equal(
144
- replyTier({ answerable: false, action: "respond", priority: "normal", rung, willSpawnSession: true }),
145
- "work",
146
- `rung ${JSON.stringify(rung)} must not be trusted`,
147
- );
148
- }
149
- });
150
-
151
- test("answerable only counts when it is strictly true", () => {
152
- // classifier.mjs coerces a missing/odd field to false for the same reason:
153
- // never guess "answerable".
154
- for (const answerable of ["true", 1, {}, null, undefined]) {
155
- assert.equal(replyTier({ answerable, action: "respond", priority: "high", rung: 0, willSpawnSession: true }), "work");
156
- }
157
- });
158
-
159
- test("garbage in gives work out — the tier never throws and never invents silence", () => {
160
- // Fail-open direction: the tier that can still speak, never the tier that
161
- // cannot. A classifier failure must not silently mute the agent.
162
- assert.equal(replyTier(), "work");
163
- assert.equal(replyTier(null), "work");
164
- assert.equal(replyTier({}), "work");
165
- assert.equal(replyTier({ action: 12, priority: [], rung: "x", willSpawnSession: "yes" }), "work");
166
- });
167
-
168
- test("the tier is a pure function — same input, same answer, no ambient reads", () => {
169
- const args = { answerable: false, action: "research", priority: "critical", rung: null, willSpawnSession: true };
170
- const first = replyTier(args);
171
- for (let i = 0; i < 50; i++) assert.equal(replyTier(args), first);
172
- assert.deepEqual(args, { answerable: false, action: "research", priority: "critical", rung: null, willSpawnSession: true }, "the argument is not mutated");
173
- });
174
- });