@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,166 +0,0 @@
1
- /**
2
- * dispatcher-resume.test.mjs — audit F11: `defaultSpawnResume` built a BARE
3
- * env (no router, no scrub) and a bare argv (no --output-format json, no
4
- * knobs). A session originally routed to a third-party backend therefore
5
- * resumed against api.anthropic.com with a foreign model id, and every
6
- * resumed run recorded $0 because nothing parsed its usage.
7
- *
8
- * The resume now goes through the SAME resolveSpawnTarget / buildChildEnv
9
- * path as a fresh spawnSession. The pure builder is tested directly; the
10
- * real defaultSpawnResume is exercised through setSpawnForTests so the argv
11
- * and env handed to `claude` are asserted byte-for-byte.
12
- */
13
-
14
- import { test } from "node:test";
15
- import assert from "node:assert/strict";
16
- import { promises as fsp } from "node:fs";
17
- import { writeFileSync, mkdirSync } from "node:fs";
18
- import { tmpdir } from "node:os";
19
- import { join } from "node:path";
20
-
21
- async function freshDispatcher() {
22
- const dir = join(tmpdir(), `dispatcher-resume-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
23
- mkdirSync(join(dir, "state/sessions"), { recursive: true });
24
- mkdirSync(join(dir, "logs/daemon"), { recursive: true });
25
- process.env.AGENT_DIR = dir;
26
- const url = new URL("./dispatcher.mjs", import.meta.url);
27
- const mod = await import(`${url.href}?resume=${Math.random()}`);
28
- return { mod, dir };
29
- }
30
- async function cleanup(dir) { try { await fsp.rm(dir, { recursive: true, force: true }); } catch { /* */ } }
31
- function plantMarker(dir, sessionId, marker) {
32
- const d = join(dir, "state/sessions/resume-pending");
33
- mkdirSync(d, { recursive: true });
34
- writeFileSync(join(d, `${sessionId}.json`), JSON.stringify({
35
- sessionId, claudeSessionId: `cs-${sessionId}`, source: "backlog", itemId: "BL-1", sourceFile: "q.yaml",
36
- prompt: "finish the memo", modelFlag: "claude-opus-4-6", model: "opus", priority: "normal",
37
- startedAt: Date.now(), recoveryAttempts: 0, ...marker,
38
- }, null, 2));
39
- }
40
- function writeV2Config(dir) {
41
- mkdirSync(join(dir, "config"), { recursive: true });
42
- writeFileSync(join(dir, "config/model-routing.json"), JSON.stringify({
43
- schema_version: 2,
44
- aliases: { frontier: "anthropic/claude-opus-4-8", default: "anthropic/claude-sonnet-4-6", fast: "anthropic/claude-haiku-4-5" },
45
- defaults: { needs_tool_use_for_sessions: true, data_class: "sensitive", cache_ttl: "1h" },
46
- backends: { anthropic: { allowed_data_classes: ["public", "internal", "sensitive"] } },
47
- routing_policy: [{ default: true, chain: ["default", "fast"] }],
48
- fallback_to_anthropic: true,
49
- }, null, 2));
50
- }
51
- function fakeProc() {
52
- const handlers = {};
53
- return { stdout: { on: () => {} }, stderr: { on: () => {} }, on: (ev, cb) => { handlers[ev] = cb; }, kill: () => {}, killed: false, _handlers: handlers };
54
- }
55
-
56
- const ANTHROPIC_TARGET = { modelFlag: "claude-sonnet-4-6", envForSpawn: {}, decisionId: "d-1", backend: "anthropic", transport: "anthropic-cli", maxTurns: 40, effort: null, agentsJson: { "Haiku-Explore": { model: "haiku" } } };
57
- const RETARGET = { modelFlag: "kimi-k2", envForSpawn: { ANTHROPIC_BASE_URL: "https://moon.example", ANTHROPIC_AUTH_TOKEN: "tok-xyz", ANTHROPIC_API_KEY: "" }, decisionId: "d-2", backend: "moonshot", transport: "anthropic-compatible", maxTurns: 40, effort: "high", agentsJson: null };
58
-
59
- test("buildResumeSpawn (Anthropic): --print --output-format json + knobs + --session-id + marker model + prompt last; legacy key strip; session id in env", async () => {
60
- const { mod, dir } = await freshDispatcher();
61
- try {
62
- const marker = { sessionId: "s-1", claudeSessionId: "CS-1", prompt: "finish the memo", modelFlag: "claude-opus-4-6", model: "opus" };
63
- const { args, env, modelFlag } = mod.buildResumeSpawn({
64
- marker, target: ANTHROPIC_TARGET,
65
- baseEnv: { PATH: "/usr/bin", ANTHROPIC_API_KEY: "sk-live", ANTHROPIC_AUTH_TOKEN: "at", FOO_API_KEY: "foo", CLAUDE_CODE_OAUTH_TOKEN: "oauth-1" },
66
- permissionArgs: ["--dangerously-skip-permissions"], daemonArgs: ["--strict-mcp-config"], augmentedPath: () => "/aug",
67
- });
68
- assert.equal(args[0], "--print");
69
- assert.equal(args[args.indexOf("--output-format") + 1], "json", "usage is measurable on a resumed run");
70
- assert.ok(args.includes("--dangerously-skip-permissions"));
71
- assert.ok(args.includes("--strict-mcp-config"));
72
- assert.equal(args[args.indexOf("--max-turns") + 1], "40", "the router's knob args ride the resume");
73
- assert.ok(/Haiku-Explore/.test(args[args.indexOf("--agents") + 1]));
74
- assert.equal(args[args.indexOf("--session-id") + 1], "CS-1");
75
- assert.equal(args[args.indexOf("--model") + 1], "claude-opus-4-6", "an Anthropic session resumes on the exact flag it used");
76
- assert.equal(modelFlag, "claude-opus-4-6");
77
- assert.equal(args[args.length - 1], "finish the memo");
78
- assert.ok(!args.includes("--resume"));
79
- // Env: the non-retarget path keeps the historical explicit-empty-key posture (keychain/OAuth fallthrough).
80
- assert.equal(env.ANTHROPIC_API_KEY, "");
81
- assert.equal(env.ANTHROPIC_AUTH_TOKEN, "");
82
- assert.equal(env.PATH, "/aug");
83
- assert.equal(env.MAESTRO_SESSION_ID, "s-1", "delivery receipts attribute to the resumed session");
84
- assert.equal(env.CLAUDE_CODE_OAUTH_TOKEN, "oauth-1", "the subscription token reaches the resumed session");
85
- } finally { await cleanup(dir); }
86
- });
87
-
88
- test("buildResumeSpawn (retarget): env goes through buildChildEnv — base URL + token injected, foreign credentials scrubbed", async () => {
89
- const { mod, dir } = await freshDispatcher();
90
- try {
91
- const marker = { sessionId: "s-2", claudeSessionId: "CS-2", prompt: "p", modelFlag: "kimi-k2", model: "sonnet" };
92
- const { args, env } = mod.buildResumeSpawn({
93
- marker, target: RETARGET,
94
- baseEnv: { PATH: "/usr/bin", ANTHROPIC_API_KEY: "sk-live", FOO_API_KEY: "foo", HOME: "/h" },
95
- permissionArgs: [], daemonArgs: [], augmentedPath: () => "/aug",
96
- });
97
- assert.equal(env.ANTHROPIC_BASE_URL, "https://moon.example", "the resumed session is retargeted like the original");
98
- assert.equal(env.ANTHROPIC_AUTH_TOKEN, "tok-xyz");
99
- assert.equal(env.ANTHROPIC_API_KEY, "", "empty-string discipline: no keychain fallthrough on a retarget");
100
- assert.equal(env.FOO_API_KEY, undefined, "§7.3 scrub: no foreign credential leaks");
101
- assert.equal(env.HOME, "/h");
102
- assert.equal(env.PATH, "/aug");
103
- assert.equal(env.MAESTRO_SESSION_ID, "s-2");
104
- assert.equal(args[args.indexOf("--model") + 1], "kimi-k2");
105
- assert.equal(args[args.indexOf("--effort") + 1], "high");
106
- } finally { await cleanup(dir); }
107
- });
108
-
109
- test("resumeModelFlag: a foreign model id never resumes against Anthropic (F11) — the router's current flag wins", async () => {
110
- const { mod, dir } = await freshDispatcher();
111
- try {
112
- // Original ran on a third party; the router now resolves Anthropic (provider down / config changed).
113
- assert.equal(mod.resumeModelFlag({ modelFlag: "moonshot/kimi-k2" }, ANTHROPIC_TARGET), "claude-sonnet-4-6");
114
- assert.equal(mod.resumeModelFlag({ modelFlag: "kimi-k2" }, ANTHROPIC_TARGET), "claude-sonnet-4-6");
115
- // Original ran on Anthropic; still Anthropic → the exact flag it used (H1 pin).
116
- assert.equal(mod.resumeModelFlag({ modelFlag: "claude-opus-4-6" }, ANTHROPIC_TARGET), "claude-opus-4-6");
117
- assert.equal(mod.resumeModelFlag({ modelFlag: "opus" }, ANTHROPIC_TARGET), "opus");
118
- // A retarget: the router's flag for that backend.
119
- assert.equal(mod.resumeModelFlag({ modelFlag: "claude-opus-4-6" }, RETARGET), "kimi-k2");
120
- // No router at all (target null) → marker flag, then coarse class, then sonnet.
121
- assert.equal(mod.resumeModelFlag({ modelFlag: "claude-opus-4-6" }, null), "claude-opus-4-6");
122
- assert.equal(mod.resumeModelFlag({ model: "opus" }, null), "opus");
123
- assert.equal(mod.resumeModelFlag({}, null), "sonnet");
124
- } finally { await cleanup(dir); }
125
- });
126
-
127
- test("defaultSpawnResume (real path, v2 config): argv carries --output-format json + router knobs; env carries the session id and the key strip", async () => {
128
- const { mod, dir } = await freshDispatcher();
129
- try {
130
- writeV2Config(dir);
131
- plantMarker(dir, "s-real", { claudeSessionId: "CS-FIXED", prompt: "finish drafting the board memo", modelFlag: "claude-opus-4-6" });
132
- const calls = [];
133
- const restore = mod.setSpawnForTests((bin, args, opts) => { calls.push({ bin, args, opts }); return fakeProc(); });
134
- try {
135
- const stats = mod.reconcileResumePending();
136
- assert.equal(stats.resumed, 1);
137
- } finally { restore(); }
138
- assert.equal(calls.length, 1);
139
- const { args, opts } = calls[0];
140
- assert.equal(args[0], "--print");
141
- assert.equal(args[args.indexOf("--output-format") + 1], "json");
142
- assert.equal(args[args.indexOf("--max-turns") + 1], "40", "v2 knobs are no longer dropped on resume");
143
- assert.ok(args.includes("--agents"));
144
- assert.equal(args[args.indexOf("--session-id") + 1], "CS-FIXED");
145
- assert.equal(args[args.indexOf("--model") + 1], "claude-opus-4-6");
146
- assert.equal(args[args.length - 1], "finish drafting the board memo");
147
- assert.ok(!args.includes("--resume"));
148
- assert.equal(opts.env.MAESTRO_SESSION_ID, "s-real");
149
- assert.equal(opts.env.ANTHROPIC_API_KEY, "");
150
- assert.equal(opts.cwd, dir);
151
- } finally { await cleanup(dir); }
152
- });
153
-
154
- test("defaultSpawnResume (no routing config): still a --print --output-format json spawn on the marker's flag", async () => {
155
- const { mod, dir } = await freshDispatcher();
156
- try {
157
- plantMarker(dir, "s-plain", { claudeSessionId: "CS-P", prompt: "p", modelFlag: "claude-sonnet-4-6" });
158
- const calls = [];
159
- const restore = mod.setSpawnForTests((bin, args, opts) => { calls.push({ args, opts }); return fakeProc(); });
160
- try { mod.reconcileResumePending(); } finally { restore(); }
161
- const { args } = calls[0];
162
- assert.equal(args[args.indexOf("--output-format") + 1], "json");
163
- assert.equal(args[args.indexOf("--model") + 1], "claude-sonnet-4-6");
164
- assert.ok(!args.includes("--max-turns"), "no router → no knobs (unchanged)");
165
- } finally { await cleanup(dir); }
166
- });
@@ -1,365 +0,0 @@
1
- /**
2
- * dispatcher-session-continuity.test.mjs — session continuity and effort
3
- * routing on the dispatch path (design §5.6, R10 + R13).
4
- *
5
- * Two defects are pinned here:
6
- *
7
- * 1. `randomUUID()` per dispatch. Every full session in a live thread started
8
- * COLD — the agent re-read the room and answered a follow-up as if it were
9
- * an opening. The router the responder already consults now keys the
10
- * conversation, and a follow-up inside the TTL continues it.
11
- * 2. `taskClass = "session.responder"`, hardcoded. `effort` was therefore
12
- * always null and `maxTurns` always 40, so complexity moved the model and
13
- * the timeout and nothing else.
14
- *
15
- * The dispatcher is stateful and binds AGENT_DIR at import, so every test takes
16
- * a fresh module against its own tmpdir.
17
- *
18
- * Run: node --test scripts/daemon/dispatcher-session-continuity.test.mjs
19
- */
20
-
21
- import { test } from "node:test";
22
- import assert from "node:assert/strict";
23
- import { replyTier } from "../../lib/assurance/tier.mjs";
24
- import { promises as fsp, mkdirSync, writeFileSync } from "fs";
25
- import { tmpdir } from "os";
26
- import { join } from "path";
27
-
28
- /**
29
- * The governor/breaker are injected ADMIT for every test here, because none of
30
- * these tests is about admission — and the real governor reads live machine
31
- * load, so under a full-suite run it can QUEUE a dispatch and the session id
32
- * under test is never spawned at all. (It did: these passed alone and failed in
33
- * `npm test`.) `dispatcher-governance.test.mjs` owns the admission decisions.
34
- */
35
- const ADMIT_ALL = {
36
- governor: { admit: () => ({ decision: "ADMIT", reason: "forced-ADMIT", snapshot: {} }), defaultDeps: () => ({}) },
37
- rateGuard: { checkRateLimit: () => ({ allowed: true, retryAt: null }), classifyStderr: () => false, recordRateLimit: () => {}, recordSuccess: () => {} },
38
- budgetGuard: { dailyStatus: () => ({ essentialOnly: false }) },
39
- };
40
-
41
- /**
42
- * The in-flight lease lives in `lib/session-router.mjs` at MODULE scope, which
43
- * is correct — one daemon process, one set of child processes — but it means a
44
- * dispatch whose fake proc never closes leaves its key claimed for the rest of
45
- * the file. Reset it per test, the way the real process would by exiting.
46
- *
47
- * Imported INSIDE `freshDispatcher`, after AGENT_DIR is pointed at the tmpdir:
48
- * session-router pulls in `session-lock.mjs`, which binds its claim directory
49
- * from AGENT_DIR at ITS first import. Importing it at module scope bound that
50
- * to the real repo and left `.claim` files in the working tree.
51
- */
52
- async function freshDispatcher() {
53
- const dir = join(tmpdir(), `dispatcher-cont-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
54
- await fsp.mkdir(join(dir, "state/sessions"), { recursive: true });
55
- await fsp.mkdir(join(dir, "state/daemon"), { recursive: true });
56
- await fsp.mkdir(join(dir, "logs/daemon"), { recursive: true });
57
- process.env.AGENT_DIR = dir;
58
- const { _resetInFlightForTests } = await import("./lib/session-router.mjs");
59
- _resetInFlightForTests();
60
- const url = new URL("./dispatcher.mjs", import.meta.url);
61
- const mod = await import(`${url.href}?test=${Math.random()}`);
62
- mod.setGovernanceForTests(ADMIT_ALL);
63
- return { mod, dir };
64
- }
65
-
66
- async function cleanup(dir) {
67
- try { await fsp.rm(dir, { recursive: true, force: true }); } catch { /* */ }
68
- }
69
-
70
- function writeV2Config(dir) {
71
- mkdirSync(join(dir, "config"), { recursive: true });
72
- writeFileSync(join(dir, "config/model-routing.json"), JSON.stringify({
73
- schema_version: 2,
74
- aliases: { frontier: "anthropic/claude-opus-4-8", default: "anthropic/claude-sonnet-4-6", fast: "anthropic/claude-haiku-4-5" },
75
- defaults: { needs_tool_use_for_sessions: true, data_class: "sensitive", cache_ttl: "1h" },
76
- backends: { anthropic: { allowed_data_classes: ["public", "internal", "sensitive"] } },
77
- routing_policy: [{ default: true, chain: ["default", "fast"] }],
78
- fallback_to_anthropic: true,
79
- }, null, 2));
80
- }
81
-
82
- function fakeProc() {
83
- const handlers = {};
84
- return { stdout: { on: () => {} }, stderr: { on: () => {} }, on: (ev, cb) => { handlers[ev] = cb; }, kill: () => {}, killed: false, _handlers: handlers };
85
- }
86
-
87
- const COHORT_ITEM = {
88
- id: "i-1",
89
- service: "cohort",
90
- channel: "engineering", // the display label
91
- channel_id: "chan-eng", // the real room
92
- sender: "Dana",
93
- content: "pick up where we left off",
94
- };
95
- const WORK = { priority: "normal", action: "respond", model: "sonnet", summary: "follow-up in the engineering room", answerable: false };
96
-
97
- /** Dispatch once and return the argv + the captured child process. */
98
- function dispatchOnce(mod, item, classResult) {
99
- const calls = [];
100
- const procs = [];
101
- const restore = mod.setSpawnForTests((bin, args) => { calls.push(args); const p = fakeProc(); procs.push(p); return p; });
102
- try {
103
- mod.dispatch("do the thing", item, classResult, "inbox");
104
- } finally { restore(); }
105
- assert.equal(calls.length, 1, "the dispatch did not spawn — admission, not session routing");
106
- return { args: calls[0], proc: procs[0] };
107
- }
108
-
109
- const sessionIdOf = (args) => args[args.indexOf("--session-id") + 1];
110
-
111
- // ---------------------------------------------------------------------------
112
- // the tier → task class
113
- // ---------------------------------------------------------------------------
114
-
115
- test("replyTier: answerable alone is NOT the answer tier — the rung is required", async () => {
116
- const { mod, dir } = await freshDispatcher();
117
- try {
118
- // The five rules of lib/assurance/tier.mjs#replyTier, in its order.
119
- assert.equal(replyTier({ answerable: true, rung: 0 }), "answer");
120
- assert.equal(replyTier({ answerable: true, rung: 1 }), "answer");
121
- assert.equal(replyTier({ willSpawnSession: false }), "answer");
122
-
123
- // THE BUG THIS PINS. `resolveSpawnTarget` runs only for items isQuickReply()
124
- // REFUSED, and execution-ladder pins that a rung-3 answerable ask is exactly
125
- // such an item. Reading `answerable` alone gave it 6 turns at low effort
126
- // where the real module gives 60 at high.
127
- assert.equal(replyTier({ answerable: true, rung: 3 }), "plan");
128
- assert.equal(replyTier({ answerable: true, action: "respond", priority: "normal" }), "work",
129
- "an unrouted answerable ask is work, never answer — unknown buys nothing");
130
-
131
- assert.equal(replyTier({ rung: 4 }), "plan");
132
- assert.equal(replyTier({ action: "research", priority: "critical" }), "plan");
133
- assert.equal(replyTier({ action: "draft", priority: "high" }), "plan");
134
- // …but only at the priorities the design names.
135
- assert.equal(replyTier({ action: "research", priority: "normal" }), "work");
136
- assert.equal(replyTier({ action: "respond", priority: "critical" }), "work");
137
- // A stale or malformed rung is not a rung.
138
- assert.equal(replyTier({ answerable: true, rung: "1" }), "work");
139
- assert.equal(replyTier({ rung: 9 }), "work");
140
- // An unrecognised classification is WORK — the historical behaviour.
141
- assert.equal(replyTier({}), "work");
142
- assert.equal(replyTier(), "work");
143
- } finally { await cleanup(dir); }
144
- });
145
-
146
- /**
147
- * There used to be a test here asserting that dispatcher.mjs's own copy of the
148
- * tier table agreed with `lib/assurance/tier.mjs`. It earned its place: at the
149
- * merge it FAILED, because the copy predated the review fix that added
150
- * `willSpawnSession !== true` to rule (2) — so an item the quick path fell
151
- * through on would have been tiered `answer` (total silence) while a long
152
- * session ran. The copy is gone and the dispatcher imports the one definition,
153
- * which makes the agreement test tautological.
154
- *
155
- * What it was really protecting is that the dispatcher's task-class mapping is
156
- * driven by that module and degrades safely, so THAT is what is pinned now.
157
- */
158
- test("a tier module that throws does not stop a dispatch — it degrades to work", async () => {
159
- const { mod, dir } = await freshDispatcher();
160
- try {
161
- // `work` is the tier the flood was made of and the one that speaks at most
162
- // once: the right thing to land on when the decision is unavailable.
163
- assert.equal(mod.taskClassFor({ rung: 4 }), "session.plan");
164
- assert.equal(mod.taskClassFor({ answerable: true, rung: 0 }), "session.answer");
165
- assert.equal(mod.taskClassFor({}), "session.responder");
166
- assert.equal(mod.taskClassFor(), "session.responder");
167
- } finally { await cleanup(dir); }
168
- });
169
-
170
- test("rungForItem reads the ladder's decision off the item, not a field the classifier never emits", async () => {
171
- const { mod, dir } = await freshDispatcher();
172
- try {
173
- // agent-daemon.mjs stamps `item.execution = routed`. classifier.mjs emits no
174
- // `rung` at all, so reading classResult.rung finds undefined forever —
175
- // which would make session.answer's knobs dead code.
176
- assert.equal(mod.rungForItem({ execution: { rung: 1 } }), 1);
177
- assert.equal(mod.rungForItem({ execution: { rung: 3, mechanism: "dispatcher-spawn" } }), 3);
178
- assert.equal(mod.rungForItem({ rung: 0 }), 0);
179
- assert.equal(mod.rungForItem({}), null);
180
- assert.equal(mod.rungForItem(null), null);
181
- assert.equal(mod.rungForItem({ execution: { rung: "2" } }), null);
182
- } finally { await cleanup(dir); }
183
- });
184
-
185
- test("taskClassFor maps each tier onto the class that carries its knobs", async () => {
186
- const { mod, dir } = await freshDispatcher();
187
- try {
188
- assert.equal(mod.taskClassFor({ answerable: true }, { execution: { rung: 0 } }), "session.answer");
189
- assert.equal(mod.taskClassFor({ action: "respond", priority: "normal" }), "session.responder");
190
- assert.equal(mod.taskClassFor({ action: "research", priority: "critical" }), "session.plan");
191
- assert.equal(mod.taskClassFor({ answerable: true }, { execution: { rung: 3 } }), "session.plan",
192
- "the ladder said this needs a session; 6 turns at low effort is the wrong answer, fast");
193
- assert.equal(mod.taskClassFor(null), "session.responder", "no classification is not an excuse to throw");
194
- } finally { await cleanup(dir); }
195
- });
196
-
197
- test("effort finally varies with the ask — a plan spawns with high effort, an answer with low", async () => {
198
- const { mod, dir } = await freshDispatcher();
199
- try {
200
- writeV2Config(dir);
201
- const plan = dispatchOnce(mod, { ...COHORT_ITEM, id: "i-plan" },
202
- { priority: "critical", action: "research", model: "opus", summary: "work out the pricing", answerable: false });
203
- assert.equal(plan.args[plan.args.indexOf("--effort") + 1], "high");
204
-
205
- // The ladder placed this at rung 0 — a single protocol call — so the answer
206
- // tier is a fact about the routed work, not an opinion about the question.
207
- const answer = dispatchOnce(mod,
208
- { ...COHORT_ITEM, id: "i-ans", channel_id: "chan-other", execution: { rung: 0, mechanism: "tool-surface" } },
209
- { priority: "normal", action: "respond", model: "sonnet", summary: "which model are you", answerable: true });
210
- assert.equal(answer.args[answer.args.indexOf("--effort") + 1], "low");
211
- assert.equal(answer.args[answer.args.indexOf("--max-turns") + 1], "6", "an answerable question does not need 40 turns");
212
- } finally { await cleanup(dir); }
213
- });
214
-
215
- test("an answerable ask the LADDER routed to a session does not get the 6-turn knobs", async () => {
216
- const { mod, dir } = await freshDispatcher();
217
- try {
218
- writeV2Config(dir);
219
- const routed = dispatchOnce(mod,
220
- { ...COHORT_ITEM, id: "i-rung3", channel_id: "chan-rung3", execution: { rung: 3, mechanism: "dispatcher-spawn" } },
221
- { priority: "normal", action: "respond", model: "sonnet", summary: "reconcile the two models", answerable: true });
222
- assert.notEqual(routed.args[routed.args.indexOf("--max-turns") + 1], "6",
223
- "a session that runs out of turns without replying leaves its obligation open — which is the retry loop");
224
- assert.equal(routed.args[routed.args.indexOf("--effort") + 1], "high");
225
- } finally { await cleanup(dir); }
226
- });
227
-
228
- // ---------------------------------------------------------------------------
229
- // session continuity
230
- // ---------------------------------------------------------------------------
231
-
232
- test("a follow-up in the same Cohort room CONTINUES the session instead of starting cold", async () => {
233
- const { mod, dir } = await freshDispatcher();
234
- try {
235
- const first = dispatchOnce(mod, COHORT_ITEM, WORK);
236
- const firstId = sessionIdOf(first.args);
237
- assert.ok(firstId, "a session id is always passed");
238
-
239
- // The session finishes cleanly — the key goes live in the registry.
240
- first.proc._handlers.close(0);
241
-
242
- const second = dispatchOnce(mod, { ...COHORT_ITEM, id: "i-2", content: "and the second half?" }, WORK);
243
- assert.equal(sessionIdOf(second.args), firstId, "the follow-up must continue, not re-open");
244
- } finally { await cleanup(dir); }
245
- });
246
-
247
- test("a DIFFERENT room gets a different session", async () => {
248
- const { mod, dir } = await freshDispatcher();
249
- try {
250
- const a = dispatchOnce(mod, COHORT_ITEM, WORK);
251
- a.proc._handlers.close(0);
252
- const b = dispatchOnce(mod, { ...COHORT_ITEM, id: "i-3", channel_id: "chan-sales" }, WORK);
253
- assert.notEqual(sessionIdOf(b.args), sessionIdOf(a.args));
254
- } finally { await cleanup(dir); }
255
- });
256
-
257
- test("a session that exited NON-ZERO is never resumed into", async () => {
258
- const { mod, dir } = await freshDispatcher();
259
- try {
260
- const first = dispatchOnce(mod, COHORT_ITEM, WORK);
261
- first.proc._handlers.close(1);
262
- const second = dispatchOnce(mod, { ...COHORT_ITEM, id: "i-4" }, WORK);
263
- assert.notEqual(sessionIdOf(second.args), sessionIdOf(first.args));
264
- } finally { await cleanup(dir); }
265
- });
266
-
267
- test("BACKLOG work is never keyed — it is not a conversation", async () => {
268
- const { mod, dir } = await freshDispatcher();
269
- try {
270
- const calls = [];
271
- const procs = [];
272
- const restore = mod.setSpawnForTests((bin, args) => { calls.push(args); const p = fakeProc(); procs.push(p); return p; });
273
- try {
274
- const backlogItem = { id: "b-1", title: "Write the runbook", service: "cohort", channel_id: "chan-eng" };
275
- mod.dispatch("p", backlogItem, { ...WORK, summary: "runbook" }, "backlog");
276
- procs[0]._handlers.close(0);
277
- mod.dispatch("p", { ...backlogItem, id: "b-2" }, { ...WORK, summary: "runbook again" }, "backlog");
278
- } finally { restore(); }
279
- assert.equal(calls.length, 2);
280
- assert.notEqual(sessionIdOf(calls[1]), sessionIdOf(calls[0]), "two backlog items must not share a session");
281
- } finally { await cleanup(dir); }
282
- });
283
-
284
- test("an item the router cannot key still dispatches, with a fresh id", async () => {
285
- const { mod, dir } = await freshDispatcher();
286
- try {
287
- // A Cohort surface with only a DISPLAY label — no room id to key on.
288
- const labelOnly = { id: "i-5", service: "cohort", channel: "doc/Pricing memo", sender: "Dana", content: "?" };
289
- const a = dispatchOnce(mod, labelOnly, WORK);
290
- a.proc._handlers.close(0);
291
- const b = dispatchOnce(mod, { ...labelOnly, id: "i-6" }, WORK);
292
- assert.ok(sessionIdOf(a.args));
293
- assert.notEqual(sessionIdOf(b.args), sessionIdOf(a.args));
294
- } finally { await cleanup(dir); }
295
- });
296
-
297
- test("crash recovery still gets a resume marker carrying the session id in play", async () => {
298
- const { mod, dir } = await freshDispatcher();
299
- try {
300
- const first = dispatchOnce(mod, COHORT_ITEM, WORK);
301
- const markers = await fsp.readdir(join(dir, "state", "sessions", "resume-pending"));
302
- assert.equal(markers.length, 1, "the in-flight marker is unchanged by session routing");
303
- const marker = JSON.parse(await fsp.readFile(join(dir, "state", "sessions", "resume-pending", markers[0]), "utf-8"));
304
- assert.equal(marker.claudeSessionId, sessionIdOf(first.args));
305
- } finally { await cleanup(dir); }
306
- });
307
-
308
- // ---------------------------------------------------------------------------
309
- // ONE CLI PROCESS PER SESSION ID
310
- //
311
- // A registry row only goes live when a session CLOSES cleanly, so the registry
312
- // alone cannot say "something is already running on this key". It did not have
313
- // to before — every dispatch minted its own UUID. Now they share a key, and the
314
- // room that produced 9,187 messages in 21 days is a TOP-LEVEL channel, where
315
- // agent-daemon's thread lock (which needs a thread id, or a DM) does not apply.
316
- // Without an in-flight lease a burst of three puts three
317
- // `claude --print --session-id <same id>` processes on ONE transcript.
318
- // ---------------------------------------------------------------------------
319
-
320
- test("a burst in one room never puts two processes on ONE session id", async () => {
321
- const { mod, dir } = await freshDispatcher();
322
- try {
323
- // Turn one completes cleanly: the key is live in the registry.
324
- const first = dispatchOnce(mod, COHORT_ITEM, WORK);
325
- first.proc._handlers.close(0);
326
- const liveId = sessionIdOf(first.args);
327
-
328
- // Three more land while nothing has finished.
329
- const burst = [
330
- dispatchOnce(mod, { ...COHORT_ITEM, id: "burst-1", content: "and the margin?" }, WORK),
331
- dispatchOnce(mod, { ...COHORT_ITEM, id: "burst-2", content: "and the runway?" }, WORK),
332
- dispatchOnce(mod, { ...COHORT_ITEM, id: "burst-3", content: "and the headcount?" }, WORK),
333
- ];
334
- const ids = burst.map((b) => sessionIdOf(b.args));
335
-
336
- assert.equal(ids[0], liveId, "the first of the burst legitimately continues the finished session");
337
- assert.equal(new Set(ids).size, 3, "every concurrent turn must have its OWN session id");
338
- assert.equal(ids.filter((id) => id === liveId).length, 1, "exactly one process may hold the live id");
339
-
340
- // …and once the room goes quiet, continuity comes back.
341
- burst.forEach((b) => b.proc._handlers.close(0));
342
- const later = dispatchOnce(mod, { ...COHORT_ITEM, id: "burst-4", content: "thanks — one more" }, WORK);
343
- assert.equal(typeof sessionIdOf(later.args), "string");
344
- } finally { await cleanup(dir); }
345
- });
346
-
347
- test("a spawn that ERRORS releases the room's key rather than stranding it", async () => {
348
- const { mod, dir } = await freshDispatcher();
349
- try {
350
- const first = dispatchOnce(mod, COHORT_ITEM, WORK);
351
- first.proc._handlers.close(0);
352
- const liveId = sessionIdOf(first.args);
353
-
354
- const failed = dispatchOnce(mod, { ...COHORT_ITEM, id: "e-1" }, WORK);
355
- assert.equal(sessionIdOf(failed.args), liveId);
356
- failed.proc._handlers.error(Object.assign(new Error("spawn ENOENT"), { code: "ENOENT" }));
357
-
358
- // The key is free again: the next turn is routed, not permanently cold.
359
- const next = dispatchOnce(mod, { ...COHORT_ITEM, id: "e-2" }, WORK);
360
- assert.equal(typeof sessionIdOf(next.args), "string");
361
- next.proc._handlers.close(0);
362
- const after = dispatchOnce(mod, { ...COHORT_ITEM, id: "e-3" }, WORK);
363
- assert.equal(sessionIdOf(after.args), sessionIdOf(next.args), "continuity works again after the failure");
364
- } finally { await cleanup(dir); }
365
- });