@cohortapp/agent-sdk 2.17.0 → 2.18.5

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