@cohortapp/agent-sdk 2.17.0 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (526) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture.mjs +8 -6
  23. package/lib/collective/global-config.mjs +63 -1
  24. package/lib/collective/presence.mjs +142 -5
  25. package/lib/comms/send-gate.mjs +559 -1
  26. package/lib/diagnostics/alerts.mjs +49 -0
  27. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  28. package/lib/engine/agents/definitions.mjs +343 -0
  29. package/lib/engine/agents/persist.mjs +275 -0
  30. package/lib/engine/agents/runtime.mjs +748 -0
  31. package/lib/engine/agents/usage.mjs +95 -0
  32. package/lib/engine/auth-status.mjs +139 -0
  33. package/lib/engine/budget.mjs +194 -0
  34. package/lib/engine/cli.mjs +1204 -0
  35. package/lib/engine/commands/index.mjs +269 -0
  36. package/lib/engine/context/budget.mjs +219 -0
  37. package/lib/engine/context/cache.mjs +125 -0
  38. package/lib/engine/context/child-env.mjs +215 -0
  39. package/lib/engine/context/compaction.mjs +342 -0
  40. package/lib/engine/context/images.mjs +90 -0
  41. package/lib/engine/context/instructions.mjs +327 -0
  42. package/lib/engine/context/lazy-instructions.mjs +169 -0
  43. package/lib/engine/context/manager.mjs +182 -0
  44. package/lib/engine/context/real-path.mjs +91 -0
  45. package/lib/engine/context/secret-values.mjs +163 -0
  46. package/lib/engine/context/settings.mjs +274 -0
  47. package/lib/engine/context/stream-input.mjs +159 -0
  48. package/lib/engine/guard.mjs +152 -0
  49. package/lib/engine/hooks.mjs +713 -0
  50. package/lib/engine/loop.mjs +560 -0
  51. package/lib/engine/mcp/client.mjs +254 -0
  52. package/lib/engine/mcp/config.mjs +301 -0
  53. package/lib/engine/mcp/http.mjs +201 -0
  54. package/lib/engine/mcp/index.mjs +146 -0
  55. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  56. package/lib/engine/mcp/naming.mjs +66 -0
  57. package/lib/engine/mcp/resources.mjs +89 -0
  58. package/lib/engine/mcp/results.mjs +133 -0
  59. package/lib/engine/mcp/stdio.mjs +137 -0
  60. package/lib/engine/mcp/supervisor.mjs +116 -0
  61. package/lib/engine/messages.mjs +104 -0
  62. package/lib/engine/output/json.mjs +164 -0
  63. package/lib/engine/output/stream-json.mjs +266 -0
  64. package/lib/engine/permissions.mjs +845 -0
  65. package/lib/engine/process-identity.mjs +164 -0
  66. package/lib/engine/process-tree.mjs +551 -0
  67. package/lib/engine/prompt.mjs +60 -0
  68. package/lib/engine/session/store.mjs +299 -0
  69. package/lib/engine/session-runtime/args.mjs +97 -0
  70. package/lib/engine/session-runtime/host.mjs +143 -0
  71. package/lib/engine/session-runtime/inbox.mjs +122 -0
  72. package/lib/engine/session-runtime/notifications.mjs +129 -0
  73. package/lib/engine/session-runtime/registry.mjs +328 -0
  74. package/lib/engine/session-runtime/runner.mjs +344 -0
  75. package/lib/engine/session-runtime/socket.mjs +212 -0
  76. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  77. package/lib/engine/skills/index.mjs +321 -0
  78. package/lib/engine/tools/bash-background.mjs +533 -0
  79. package/lib/engine/tools/bash.mjs +216 -0
  80. package/lib/engine/tools/edit.mjs +97 -0
  81. package/lib/engine/tools/glob.mjs +81 -0
  82. package/lib/engine/tools/grep.mjs +224 -0
  83. package/lib/engine/tools/index.mjs +84 -0
  84. package/lib/engine/tools/list-agents.mjs +32 -0
  85. package/lib/engine/tools/ls.mjs +127 -0
  86. package/lib/engine/tools/monitor.mjs +82 -0
  87. package/lib/engine/tools/notebook-edit.mjs +218 -0
  88. package/lib/engine/tools/read.mjs +103 -0
  89. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  90. package/lib/engine/tools/schema.mjs +144 -0
  91. package/lib/engine/tools/send-message.mjs +77 -0
  92. package/lib/engine/tools/session.mjs +70 -0
  93. package/lib/engine/tools/todo.mjs +144 -0
  94. package/lib/engine/tools/toolsearch.mjs +217 -0
  95. package/lib/engine/tools/walk.mjs +193 -0
  96. package/lib/engine/tools/web-switch.mjs +31 -0
  97. package/lib/engine/tools/webfetch-html.mjs +387 -0
  98. package/lib/engine/tools/webfetch-net.mjs +340 -0
  99. package/lib/engine/tools/webfetch.mjs +198 -0
  100. package/lib/engine/tools/websearch.mjs +91 -0
  101. package/lib/engine/tools/workflow.mjs +95 -0
  102. package/lib/engine/tools/write.mjs +76 -0
  103. package/lib/engine/tui/line-editor.mjs +137 -0
  104. package/lib/engine/tui/render.mjs +86 -0
  105. package/lib/engine/tui/tui.mjs +274 -0
  106. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  107. package/lib/engine/wire/effort.mjs +36 -0
  108. package/lib/engine/wire/errors.mjs +496 -0
  109. package/lib/engine/wire/http.mjs +441 -0
  110. package/lib/engine/wire/index.mjs +76 -0
  111. package/lib/engine/wire/openai-chat.mjs +332 -0
  112. package/lib/engine/wire/prompt-cache.mjs +79 -0
  113. package/lib/engine/wire/search.mjs +140 -0
  114. package/lib/engine/wire/sse.mjs +114 -0
  115. package/lib/engine/wire/stall.mjs +349 -0
  116. package/lib/engine/wire/token-provider.mjs +175 -0
  117. package/lib/engine/wire/usage.mjs +192 -0
  118. package/lib/engine/workflow/host.mjs +524 -0
  119. package/lib/engine/workflow/journal.mjs +188 -0
  120. package/lib/engine/workflow/json-schema.mjs +171 -0
  121. package/lib/engine/workflow/meta.mjs +329 -0
  122. package/lib/engine/workflow/notifications.mjs +52 -0
  123. package/lib/engine/workflow/runtime.mjs +447 -0
  124. package/lib/engine/workflow/sandbox.mjs +534 -0
  125. package/lib/engine/workflow/worker.mjs +141 -0
  126. package/lib/engine/workflow/worktree.mjs +74 -0
  127. package/lib/execution/disposition.mjs +1 -1
  128. package/lib/execution/intake.mjs +10 -0
  129. package/lib/execution/surface-policy.mjs +15 -0
  130. package/lib/learning/curator.mjs +8 -6
  131. package/lib/learning/reflect.mjs +8 -6
  132. package/lib/model-router/catalog/cohort.yaml +137 -0
  133. package/lib/model-router/catalog.mjs +118 -1
  134. package/lib/model-router/failover.mjs +67 -16
  135. package/lib/model-router/llm-task.mjs +39 -3
  136. package/lib/model-router/resolve.mjs +89 -3
  137. package/lib/model-router/spawn.mjs +46 -47
  138. package/lib/model-router/taxonomy.mjs +126 -4
  139. package/lib/org/cost-sync.mjs +141 -11
  140. package/lib/org/inbound/broadcast.mjs +289 -0
  141. package/lib/org/inbound/collective.mjs +375 -0
  142. package/lib/org/inbound/directedness.mjs +96 -8
  143. package/lib/org/inbound/facts.mjs +78 -2
  144. package/lib/org/inbound/project.mjs +22 -0
  145. package/lib/org/inbound/surfaces.mjs +14 -0
  146. package/lib/org/llm-token.mjs +879 -0
  147. package/lib/org/mesh.mjs +61 -0
  148. package/lib/org/messaging.mjs +3 -1
  149. package/lib/org/protocol.checksum +1 -1
  150. package/lib/org/protocol.mjs +15 -0
  151. package/lib/org/quota.mjs +520 -0
  152. package/lib/org/tool-surface.mjs +104 -16
  153. package/lib/org/ui-parity.mjs +16 -1
  154. package/lib/org/work-ledger.mjs +37 -6
  155. package/lib/rate-guard.mjs +114 -1
  156. package/lib/resource-governor.mjs +41 -6
  157. package/lib/runtime/adapter.mjs +823 -0
  158. package/lib/runtime/child-env.mjs +191 -0
  159. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  160. package/lib/runtime/seat-engine.mjs +162 -0
  161. package/lib/session/ask-ledger.mjs +271 -0
  162. package/lib/session/current-work.mjs +676 -0
  163. package/lib/session/feed-core.mjs +40 -3
  164. package/lib/session/launch-args.mjs +56 -4
  165. package/lib/session/status-summary.mjs +26 -9
  166. package/lib/session/upgrade-notice.mjs +42 -0
  167. package/lib/setup/claude-probe.mjs +117 -13
  168. package/lib/setup/enrich.mjs +13 -10
  169. package/lib/setup/sections/model.mjs +39 -13
  170. package/lib/telemetry/collect.mjs +208 -9
  171. package/lib/upgrade/ignored-drift.mjs +105 -0
  172. package/lib/voice/post-call-brief.mjs +30 -17
  173. package/package.json +13 -3
  174. package/plugins/maestro-skills/skills/board-work.md +5 -0
  175. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  176. package/plugins/maestro-skills/skills/main-session.md +18 -7
  177. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  178. package/scripts/ci/run-tests.mjs +47 -19
  179. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  180. package/scripts/collective/hook-runner.mjs +29 -2
  181. package/scripts/continuous-monitor.sh +13 -0
  182. package/scripts/cost/track-claude-usage.mjs +15 -0
  183. package/scripts/daemon/agent-daemon.mjs +408 -20
  184. package/scripts/daemon/assurance.mjs +48 -12
  185. package/scripts/daemon/cadence-consumer.mjs +218 -68
  186. package/scripts/daemon/cadence-handlers.mjs +73 -4
  187. package/scripts/daemon/classifier.mjs +75 -26
  188. package/scripts/daemon/context-compiler.mjs +51 -37
  189. package/scripts/daemon/deliver.mjs +30 -1
  190. package/scripts/daemon/dispatcher.mjs +595 -149
  191. package/scripts/daemon/health.mjs +14 -1
  192. package/scripts/daemon/maestro-daemon.mjs +11 -0
  193. package/scripts/daemon/prompt-builder.mjs +24 -0
  194. package/scripts/daemon/responder.mjs +246 -79
  195. package/scripts/daemon/sdk-version.mjs +98 -16
  196. package/scripts/eval/probe-gateway.mjs +635 -0
  197. package/scripts/eval/replay/extract.mjs +270 -0
  198. package/scripts/eval/replay/grade.mjs +260 -0
  199. package/scripts/eval/replay/lib/config.mjs +50 -0
  200. package/scripts/eval/replay/lib/effects.mjs +65 -0
  201. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  202. package/scripts/eval/replay/lib/judge.mjs +72 -0
  203. package/scripts/eval/replay/lib/redact.mjs +136 -0
  204. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  205. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  206. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  207. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  208. package/scripts/eval/replay/report.mjs +185 -0
  209. package/scripts/eval/replay/run.mjs +404 -0
  210. package/scripts/fleet/rollout.mjs +1094 -0
  211. package/scripts/hooks/pre-send-audit.sh +36 -245
  212. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  213. package/scripts/hooks/validate-state-yaml.sh +190 -0
  214. package/scripts/huddle/huddle-llm.mjs +361 -0
  215. package/scripts/huddle/huddle-server.mjs +46 -121
  216. package/scripts/local-triggers/autoupdate.sh +448 -78
  217. package/scripts/local-triggers/run-trigger.sh +13 -0
  218. package/scripts/maintenance/pin-integrity.mjs +364 -0
  219. package/scripts/poll-slack-events.sh +41 -9
  220. package/scripts/poller/slack-socket-mode.mjs +28 -3
  221. package/scripts/session/supervisor.mjs +80 -13
  222. package/scripts/spawn-session.sh +13 -0
  223. package/bin/maestro.test.mjs +0 -1574
  224. package/lib/action-executor.test.mjs +0 -871
  225. package/lib/archetype.test.mjs +0 -132
  226. package/lib/assurance/plan-note.test.mjs +0 -234
  227. package/lib/assurance/room-budget.test.mjs +0 -486
  228. package/lib/assurance/tier.test.mjs +0 -174
  229. package/lib/autonomy.test.mjs +0 -66
  230. package/lib/backlog.test.mjs +0 -302
  231. package/lib/backup/policy.test.mjs +0 -305
  232. package/lib/budget-escalate.test.mjs +0 -232
  233. package/lib/budget-guard.envelope.test.mjs +0 -476
  234. package/lib/budget-guard.test.mjs +0 -427
  235. package/lib/cadence-bus-requeue.test.mjs +0 -83
  236. package/lib/cadence-bus-schedule.test.mjs +0 -194
  237. package/lib/cadence-bus.test.mjs +0 -720
  238. package/lib/cadences.test.mjs +0 -230
  239. package/lib/capability/inventory.test.mjs +0 -232
  240. package/lib/capability.test.mjs +0 -78
  241. package/lib/channels/base-adapter.test.mjs +0 -590
  242. package/lib/channels/channels.test.mjs +0 -371
  243. package/lib/channels/contract.test.mjs +0 -162
  244. package/lib/channels/inbox-item.test.mjs +0 -368
  245. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  246. package/lib/channels/pairing.test.mjs +0 -270
  247. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  248. package/lib/channels/slack-adapter.test.mjs +0 -212
  249. package/lib/channels/telegram-adapter.test.mjs +0 -306
  250. package/lib/channels/voice/adapter.test.mjs +0 -278
  251. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  252. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  253. package/lib/charter.test.mjs +0 -89
  254. package/lib/claude-bin.test.mjs +0 -131
  255. package/lib/cli/board.test.mjs +0 -227
  256. package/lib/cli/design.test.mjs +0 -270
  257. package/lib/cli/doctor-checks.test.mjs +0 -336
  258. package/lib/cli/global-setup-extras.test.mjs +0 -462
  259. package/lib/cli/inbox.test.mjs +0 -230
  260. package/lib/cli/session-ack.test.mjs +0 -63
  261. package/lib/cli/session.test.mjs +0 -613
  262. package/lib/collective/capture.test.mjs +0 -121
  263. package/lib/collective/cards.test.mjs +0 -114
  264. package/lib/collective/config.test.mjs +0 -123
  265. package/lib/collective/global-config.test.mjs +0 -220
  266. package/lib/collective/global-skills.test.mjs +0 -126
  267. package/lib/collective/presence.test.mjs +0 -95
  268. package/lib/collective/recall.test.mjs +0 -116
  269. package/lib/collective/vendor-skills.test.mjs +0 -306
  270. package/lib/comms/send-gate.test.mjs +0 -770
  271. package/lib/comms.test.mjs +0 -41
  272. package/lib/context/budget.test.mjs +0 -252
  273. package/lib/context/history-scope.test.mjs +0 -79
  274. package/lib/cost/ledger-row.test.mjs +0 -183
  275. package/lib/design/design-md.test.mjs +0 -318
  276. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  277. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  278. package/lib/design/fixtures/foundation.json +0 -133
  279. package/lib/design/refresh-gate.test.mjs +0 -144
  280. package/lib/design/write.test.mjs +0 -241
  281. package/lib/diagnostics/alerts.test.mjs +0 -318
  282. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  283. package/lib/diagnostics/counters.test.mjs +0 -206
  284. package/lib/diagnostics/events.test.mjs +0 -290
  285. package/lib/diagnostics/otel.test.mjs +0 -196
  286. package/lib/diagnostics/trace.test.mjs +0 -251
  287. package/lib/env-compat.test.mjs +0 -104
  288. package/lib/execution/disposition.test.mjs +0 -553
  289. package/lib/execution/drive.test.mjs +0 -270
  290. package/lib/execution/effects.test.mjs +0 -344
  291. package/lib/execution/intake.test.mjs +0 -389
  292. package/lib/execution/journal.test.mjs +0 -261
  293. package/lib/execution/match.test.mjs +0 -235
  294. package/lib/execution/pipeline.test.mjs +0 -392
  295. package/lib/execution/route.test.mjs +0 -186
  296. package/lib/execution/surface-policy.test.mjs +0 -162
  297. package/lib/fs-atomic.test.mjs +0 -72
  298. package/lib/fs-ownership.test.mjs +0 -158
  299. package/lib/goals/admission.test.mjs +0 -164
  300. package/lib/goals/classify.test.mjs +0 -167
  301. package/lib/goals/collaborate.test.mjs +0 -336
  302. package/lib/goals/gaps.test.mjs +0 -284
  303. package/lib/goals/loop.test.mjs +0 -845
  304. package/lib/hooks/bus.test.mjs +0 -387
  305. package/lib/identity/persona.test.mjs +0 -142
  306. package/lib/kpi-sensors.test.mjs +0 -278
  307. package/lib/kpi.test.mjs +0 -244
  308. package/lib/learning/config.test.mjs +0 -75
  309. package/lib/learning/counters.test.mjs +0 -69
  310. package/lib/learning/curator-consolidate.test.mjs +0 -238
  311. package/lib/learning/curator.test.mjs +0 -106
  312. package/lib/learning/reflect.test.mjs +0 -0
  313. package/lib/learning/session-index.test.mjs +0 -125
  314. package/lib/learning/skill-writer.test.mjs +0 -210
  315. package/lib/mandate/audit.test.mjs +0 -195
  316. package/lib/mandate/contract.test.mjs +0 -185
  317. package/lib/mandate/derive.test.mjs +0 -274
  318. package/lib/mandate/model.test.mjs +0 -164
  319. package/lib/mandate/refresh.test.mjs +0 -389
  320. package/lib/mcp/server.test.mjs +0 -426
  321. package/lib/model-router/auth-profiles.test.mjs +0 -580
  322. package/lib/model-router/catalog.test.mjs +0 -385
  323. package/lib/model-router/economics.test.mjs +0 -438
  324. package/lib/model-router/failover.test.mjs +0 -439
  325. package/lib/model-router/health.test.mjs +0 -338
  326. package/lib/model-router/integration-coverage.test.mjs +0 -831
  327. package/lib/model-router/integration.test.mjs +0 -564
  328. package/lib/model-router/ledger.test.mjs +0 -415
  329. package/lib/model-router/llm-task.test.mjs +0 -392
  330. package/lib/model-router/org-credentials.test.mjs +0 -265
  331. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  332. package/lib/model-router/reconcile.test.mjs +0 -316
  333. package/lib/model-router/repair.test.mjs +0 -180
  334. package/lib/model-router/spawn.test.mjs +0 -446
  335. package/lib/model-router/taxonomy.test.mjs +0 -410
  336. package/lib/model-router.test.mjs +0 -1207
  337. package/lib/org/activity.test.mjs +0 -134
  338. package/lib/org/approvals.test.mjs +0 -216
  339. package/lib/org/awareness.test.mjs +0 -159
  340. package/lib/org/board-mine-cache.test.mjs +0 -53
  341. package/lib/org/board.test.mjs +0 -187
  342. package/lib/org/bootstrap-context.test.mjs +0 -153
  343. package/lib/org/client.test.mjs +0 -1206
  344. package/lib/org/cohort-client.test.mjs +0 -126
  345. package/lib/org/cost-sync.test.mjs +0 -153
  346. package/lib/org/doctor.test.mjs +0 -346
  347. package/lib/org/engagement-ledger.test.mjs +0 -112
  348. package/lib/org/engagement.test.mjs +0 -739
  349. package/lib/org/handoff.test.mjs +0 -269
  350. package/lib/org/inbound/directedness.test.mjs +0 -668
  351. package/lib/org/inbound/facts.test.mjs +0 -471
  352. package/lib/org/inbound/hydrate.test.mjs +0 -908
  353. package/lib/org/inbound/index.test.mjs +0 -429
  354. package/lib/org/inbound/project.test.mjs +0 -287
  355. package/lib/org/integration-tools.test.mjs +0 -160
  356. package/lib/org/keys.test.mjs +0 -92
  357. package/lib/org/knowledge.test.mjs +0 -326
  358. package/lib/org/leases.test.mjs +0 -235
  359. package/lib/org/mesh-directives.test.mjs +0 -110
  360. package/lib/org/mesh-integration.test.mjs +0 -127
  361. package/lib/org/mesh.test.mjs +0 -400
  362. package/lib/org/messaging.test.mjs +0 -471
  363. package/lib/org/param-contract.test.mjs +0 -477
  364. package/lib/org/policy.test.mjs +0 -237
  365. package/lib/org/protocol.checksum.test.mjs +0 -90
  366. package/lib/org/protocol.test.mjs +0 -323
  367. package/lib/org/push.test.mjs +0 -792
  368. package/lib/org/registry.test.mjs +0 -100
  369. package/lib/org/resource-tools.test.mjs +0 -361
  370. package/lib/org/tool-access.test.mjs +0 -144
  371. package/lib/org/tool-surface-integration.test.mjs +0 -120
  372. package/lib/org/tool-surface.test.mjs +0 -1268
  373. package/lib/org/typing.test.mjs +0 -291
  374. package/lib/org/ui-parity.test.mjs +0 -560
  375. package/lib/org/verify.test.mjs +0 -194
  376. package/lib/org/work-ledger.test.mjs +0 -273
  377. package/lib/plan/adoption-e2e.test.mjs +0 -366
  378. package/lib/plan/budget-enforcement.test.mjs +0 -400
  379. package/lib/plan/compile.test.mjs +0 -382
  380. package/lib/plan/emit.test.mjs +0 -269
  381. package/lib/plan/explain.test.mjs +0 -188
  382. package/lib/prompts/parallelism.test.mjs +0 -177
  383. package/lib/rag/rag.test.mjs +0 -505
  384. package/lib/rate-guard.test.mjs +0 -272
  385. package/lib/reactive-gate.test.mjs +0 -57
  386. package/lib/render.test.mjs +0 -68
  387. package/lib/resource-governor.test.mjs +0 -488
  388. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  389. package/lib/scheduling/jitter.test.mjs +0 -140
  390. package/lib/secrets/broker.test.mjs +0 -280
  391. package/lib/secrets/providers.test.mjs +0 -274
  392. package/lib/security/audit-engine.test.mjs +0 -424
  393. package/lib/security/coerce-args.test.mjs +0 -281
  394. package/lib/security/dangerous-tools.test.mjs +0 -68
  395. package/lib/security/external-content.test.mjs +0 -84
  396. package/lib/security/redact.test.mjs +0 -441
  397. package/lib/security/secret-equal.test.mjs +0 -55
  398. package/lib/session/config.test.mjs +0 -92
  399. package/lib/session/feed-core.test.mjs +0 -198
  400. package/lib/session/first-run.test.mjs +0 -121
  401. package/lib/session/frontdoor.test.mjs +0 -205
  402. package/lib/session/handoffs.test.mjs +0 -183
  403. package/lib/session/identity.test.mjs +0 -180
  404. package/lib/session/inbox-claims.test.mjs +0 -286
  405. package/lib/session/launch-args.test.mjs +0 -157
  406. package/lib/session/liveness.test.mjs +0 -100
  407. package/lib/session/status-summary.test.mjs +0 -118
  408. package/lib/session-permissions.test.mjs +0 -120
  409. package/lib/setup/claude-probe.test.mjs +0 -187
  410. package/lib/setup/completeness.test.mjs +0 -110
  411. package/lib/setup/context-pack.test.mjs +0 -89
  412. package/lib/setup/enrich.test.mjs +0 -115
  413. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  414. package/lib/setup/integration.test.mjs +0 -162
  415. package/lib/setup/io.test.mjs +0 -77
  416. package/lib/setup/runner.test.mjs +0 -132
  417. package/lib/setup/sections/identity.test.mjs +0 -234
  418. package/lib/setup/sections/inventory.test.mjs +0 -198
  419. package/lib/setup/sections/learning.test.mjs +0 -81
  420. package/lib/setup/sections/mandate.test.mjs +0 -388
  421. package/lib/setup/sections/messaging.test.mjs +0 -127
  422. package/lib/setup/sections/model.test.mjs +0 -240
  423. package/lib/setup/sections/org.test.mjs +0 -346
  424. package/lib/setup/sections/orgmail.test.mjs +0 -118
  425. package/lib/setup/sections/recovery.test.mjs +0 -98
  426. package/lib/setup/sections/subagents.test.mjs +0 -429
  427. package/lib/setup/sections/verify.test.mjs +0 -175
  428. package/lib/setup/sot.test.mjs +0 -81
  429. package/lib/setup/state.test.mjs +0 -115
  430. package/lib/singleton.test.mjs +0 -151
  431. package/lib/subagents/cli.test.mjs +0 -389
  432. package/lib/subagents/client.test.mjs +0 -309
  433. package/lib/subagents/gap.test.mjs +0 -234
  434. package/lib/subagents/lock.test.mjs +0 -248
  435. package/lib/subagents/manifest.test.mjs +0 -175
  436. package/lib/subagents/refs.test.mjs +0 -204
  437. package/lib/subagents/resolve.test.mjs +0 -422
  438. package/lib/subagents/schema.test.mjs +0 -328
  439. package/lib/telemetry/alerts.test.mjs +0 -109
  440. package/lib/telemetry/collect.test.mjs +0 -1274
  441. package/lib/tool-definitions-integration.test.mjs +0 -83
  442. package/lib/tool-definitions.test.mjs +0 -437
  443. package/lib/upgrade/global-refresh.test.mjs +0 -65
  444. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  445. package/lib/upgrade/post-steps.test.mjs +0 -200
  446. package/lib/upgrade/verify.test.mjs +0 -164
  447. package/lib/util/fetch-timeout.test.mjs +0 -202
  448. package/lib/util/reconnect.test.mjs +0 -369
  449. package/lib/util/unhandled.test.mjs +0 -216
  450. package/lib/voice/outbound.test.mjs +0 -69
  451. package/lib/voice/session-rotation.test.mjs +0 -114
  452. package/lib/voice/stt.test.mjs +0 -226
  453. package/lib/voice/voice.test.mjs +0 -990
  454. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  455. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  456. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  457. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  458. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  459. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  460. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  461. package/scripts/ci/check.test.mjs +0 -194
  462. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  463. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  464. package/scripts/collective/hook-runner.test.mjs +0 -173
  465. package/scripts/cost/fleet-digest.test.mjs +0 -207
  466. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  467. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  468. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  469. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  470. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  471. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  472. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  473. package/scripts/daemon/assurance.test.mjs +0 -1791
  474. package/scripts/daemon/board-mirror.test.mjs +0 -165
  475. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  476. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  477. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  478. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  479. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  480. package/scripts/daemon/classifier.test.mjs +0 -266
  481. package/scripts/daemon/classify-kind.test.mjs +0 -40
  482. package/scripts/daemon/context-compiler.test.mjs +0 -406
  483. package/scripts/daemon/deliver.test.mjs +0 -564
  484. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  485. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  486. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  487. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  488. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  489. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  490. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  491. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  492. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  493. package/scripts/daemon/integration.test.mjs +0 -149
  494. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  495. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  496. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  497. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  498. package/scripts/daemon/responder-cost.test.mjs +0 -68
  499. package/scripts/daemon/responder-history.test.mjs +0 -221
  500. package/scripts/daemon/sdk-version.test.mjs +0 -31
  501. package/scripts/daemon/session-lock.test.mjs +0 -252
  502. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  503. package/scripts/daemon/typing-registry.test.mjs +0 -102
  504. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  505. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  506. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  507. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  508. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  509. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  510. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  511. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  512. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  513. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  514. package/scripts/poller-launchd/install.test.mjs +0 -243
  515. package/scripts/restore-from-backup.test.mjs +0 -181
  516. package/scripts/session/feed.test.mjs +0 -196
  517. package/scripts/session/supervisor-sh.test.mjs +0 -218
  518. package/scripts/session/supervisor.test.mjs +0 -482
  519. package/scripts/setup/configure-macos.test.mjs +0 -306
  520. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  521. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  522. package/scripts/setup/generate-capability.test.mjs +0 -134
  523. package/scripts/setup/init-agent.test.mjs +0 -370
  524. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  525. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  526. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
package/lib/org/mesh.mjs CHANGED
@@ -29,6 +29,12 @@
29
29
  * - "session.restart" → touch state/session/restart-requested; the main
30
30
  * session's feed sees it and the session restarts itself
31
31
  * at an idle moment (design §3.10). The daemon stays up.
32
+ * Every beat outcome is also RECORDED locally in state/org/last-beat.json
33
+ * {at, ok, code?} — the only on-disk evidence that the org has actually
34
+ * ACKNOWLEDGED this daemon (a log line says the daemon tried; `ok:true`
35
+ * says the server answered 2xx). scripts/local-triggers/autoupdate.sh gates
36
+ * on it: a daemon can be alive, log "org-mesh connected" and still be
37
+ * 401'd on every beat — or crash-looping — and nobody in the org sees it.
32
38
  * 3. stop() — clears the interval so the daemon can shut the mesh down cleanly.
33
39
  *
34
40
  * FAIL-OPEN by design (mirrors lib/org/client): if the org is disabled
@@ -193,6 +199,55 @@ function escalateCriticalAlerts(agentRoot, alerts, now) {
193
199
  }
194
200
  }
195
201
 
202
+ /** Where the last beat outcome is recorded, relative to the agent root. */
203
+ export const LAST_BEAT_FILE = join("state", "org", "last-beat.json");
204
+
205
+ /**
206
+ * Record the outcome of ONE presence beat in state/org/last-beat.json:
207
+ * { at: ISO, ok: true, sdkVersion? } — the org answered 2xx
208
+ * { at: ISO, ok: false, code, message?, sdkVersion? }
209
+ * — rejected (error frame) or the
210
+ * transport failed (code "TRANSPORT")
211
+ * Overwritten on every beat, so `at` is always the LATEST attempt and `ok`
212
+ * says whether the org accepted it. This is what "the org can see this seat"
213
+ * means on disk: hq derives online/offline from the server-side presence.beat
214
+ * ledger (stale after 5 min), and this file is the daemon's own copy of that
215
+ * fact. Readers (autoupdate.sh's health gate, doctor) compare `at` against the
216
+ * daemon's start time and require `ok === true`.
217
+ *
218
+ * `sdkVersion` is the version THIS beat told the org (status.machine.sdkVersion,
219
+ * which hq persists to AgentStatus.machine — the fleet view reads it). The
220
+ * ledger's presence.beat row keeps only a trimmed status, so this field is the
221
+ * only local proof that an accepted beat carried the installed version and
222
+ * not the one the previous daemon process was started on. Absent when the
223
+ * beat went out without a status (collector failure) — a reader treats absence
224
+ * as "no version proof", never as a mismatch.
225
+ *
226
+ * Atomic write, fail-open: a disk error is logged and never breaks the beat.
227
+ */
228
+ function recordBeat(agentRoot, frame, now, transportError, status) {
229
+ const at = new Date((typeof now === "function" ? now() : Date.now())).toISOString();
230
+ const rec = { at, ok: !!(frame && frame.ok === true) };
231
+ const v = status && status.machine && typeof status.machine.sdkVersion === "string" ? status.machine.sdkVersion.trim() : "";
232
+ if (v) rec.sdkVersion = v;
233
+ if (!rec.ok) {
234
+ if (transportError) {
235
+ rec.code = "TRANSPORT";
236
+ rec.message = String((transportError && transportError.message) || transportError).slice(0, 200);
237
+ } else {
238
+ rec.code = (frame && frame.error && frame.error.code) || "UNKNOWN";
239
+ const msg = frame && frame.error && frame.error.message;
240
+ if (msg) rec.message = String(msg).slice(0, 200);
241
+ }
242
+ }
243
+ try {
244
+ writeJsonAtomic(join(agentRoot, LAST_BEAT_FILE), rec);
245
+ } catch (err) {
246
+ logWarn(`failed to record beat outcome in ${LAST_BEAT_FILE} (${err && err.message})`);
247
+ }
248
+ return rec;
249
+ }
250
+
196
251
  /**
197
252
  * Collect the SHARED STATUS SNAPSHOT for this beat, fully fail-open: any collector
198
253
  * error yields `undefined` so the beat goes out WITHOUT a status (never broken).
@@ -384,9 +439,14 @@ async function beatOnce(ctx) {
384
439
  );
385
440
  } catch (err) {
386
441
  logWarn(`presence beat failed (${err && err.message}) — org may be down, continuing standalone`);
442
+ recordBeat(ctx.agentRoot, null, ctx.now, err || new Error("presence beat failed"), status);
387
443
  return [];
388
444
  }
389
445
 
446
+ // Record the outcome BEFORE acting on directives: a `halt` below stops the
447
+ // daemon, and the record must already say what the org last answered.
448
+ recordBeat(ctx.agentRoot, frame, ctx.now, undefined, status);
449
+
390
450
  // A REJECTED beat is not a thrown beat. `presenceBeat` RESOLVES with a res
391
451
  // FRAME — {ok:false,error:{code,message}} — for 401/403/404/500 alike, so the
392
452
  // catch above only ever sees transport failures. Without this, a beat the
@@ -545,6 +605,7 @@ export async function connectOrgMesh(o = {}) {
545
605
 
546
606
  export const _internals = {
547
607
  beatOnce,
608
+ recordBeat,
548
609
  extractDirectives,
549
610
  handleDirective,
550
611
  tripKillSwitch,
@@ -193,7 +193,9 @@ async function emitAfterSend(ctx, deps = {}) {
193
193
  * 1. resolve org enrollment (disabled ⇒ fail-open error frame, no send).
194
194
  * 2. screenOutbound({channel, recipient:channel, text:body}) — a BLOCK returns
195
195
  * a frame `{ok:false, error:{code:"FORBIDDEN_SCOPE"|"BAD_REQUEST", message}}`
196
- * and the message is NEVER sent (no client.call).
196
+ * and the message is NEVER sent (no client.call). This also covers thread
197
+ * replies (params.threadId) — screenOutbound runs screenScaffold first, so
198
+ * coverage here is real, just not visible at this call site.
197
199
  * 3. messaging.send RPC (Bearer + x-org-protocol + x-idempotency-key from the
198
200
  * client-supplied message id, so a retried send dedupes server-side).
199
201
  * 4. on ok: emit afterSend + attribute the ledger row.
@@ -1 +1 @@
1
- 8bce96163171fc5b9f23ea03889cd9220113af09e14f87d704c635f33f9aa76e
1
+ 24cc454def1119b37d24b9b00232b98d5cd52352be08ca98b53843c610979af5
@@ -236,6 +236,10 @@ export const METHODS = Object.freeze({
236
236
  "registry.register": { family: "registry", scope: "registry.write", sideEffecting: true, idempotent: true },
237
237
  "registry.heartbeat": { family: "registry", scope: "registry.write", sideEffecting: true },
238
238
  "presence.beat": { family: "presence", scope: "presence.write", sideEffecting: true },
239
+ // Read-only roll-out visibility: which SDK version every seat reports and
240
+ // whether its daemon is healthy. `org.read` (the fleet's shape is
241
+ // org-structural), floored to `fleet.view` in the handler.
242
+ "presence.fleetVersions": { family: "presence", scope: "org.read", sideEffecting: false },
239
243
  // --- board (kanban / backlog) ---
240
244
  "board.create": { family: "board", scope: "board.write", sideEffecting: true, idempotent: true },
241
245
  "board.claim": { family: "board", scope: "board.work", sideEffecting: true },
@@ -349,6 +353,17 @@ export const METHODS = Object.freeze({
349
353
  // side-effecting exactly-one-append contract forbids — so it self-audits its
350
354
  // own chain row like messaging.synthesizeVoiceNote and books.ask do. ---
351
355
  "messaging.electResponder": { family: "messaging", scope: "messaging.read", sideEffecting: false },
356
+ // --- messaging.silences (2026-09): WHY a message got no reply. Reads the
357
+ // "responder"/"responder.silence" ledger rows hq's ambient responder writes
358
+ // when it routes a human's message and stays quiet (deferred to a human,
359
+ // or nobody elected) — for ONE message ({messageId}) or a channel over a
360
+ // window ({channelId, windowDays?}). One of the two is required; the room
361
+ // is ACL-checked through the same channel policy as messaging.history, and
362
+ // there is no org-wide arm on this lane (that is the ADMIN panel's, under
363
+ // audit.orgLog). Each row carries the operator reason vocabulary
364
+ // (human_mentioned | addressed_to_human | nobody_elected | no_responders)
365
+ // and its text. Pure read, zero appends. ---
366
+ "messaging.silences": { family: "messaging", scope: "messaging.read", sideEffecting: false },
352
367
  // --- calling (SP3): call lifecycle in the human app. NOT reserved-admin. ---
353
368
  "calling.start": { family: "calling", scope: "calling.write", sideEffecting: true },
354
369
  "calling.join": { family: "calling", scope: "calling.write", sideEffecting: true },
@@ -0,0 +1,520 @@
1
+ /**
2
+ * lib/org/quota.mjs — the seat's view of its cohort-llm quota.
3
+ *
4
+ * Cohort quota UX comes from Cohort's own channel, not from
5
+ * `anthropic-ratelimit-unified-*` (design C6): the `GET /cohort/v1/quota`
6
+ * endpoint, the `x-cohort-quota-*` headers on every success, the terminal
7
+ * `event: cohort` SSE frame, and the `cohort` refusal object carried by every
8
+ * 402/429/503/529 body (wire contract §4.8).
9
+ *
10
+ * Two halves:
11
+ *
12
+ * PURE parsers for each of those four surfaces, and the decisions over
13
+ * them — `refusalDisposition` (what a spawn site does with a refusal)
14
+ * and `quotaBand` (warn at 75 %, at-limit at 100 %). No clock, no I/O:
15
+ * `now` is a parameter.
16
+ * EDGE `createQuotaClient` — the GET with a short cache (≤ 30 s), single
17
+ * flight, one retry after a 401 with a fresh token, and a snapshot on
18
+ * disk (state/cohort-llm/quota.json) so a sibling process on the seat
19
+ * (budget-guard in the cadence consumer) reads the same state without
20
+ * its own network call.
21
+ *
22
+ * Money stays a decimal string of integer micro-USD end to end; nothing here
23
+ * turns it into a float except the display helpers budget-guard calls.
24
+ *
25
+ * @module lib/org/quota
26
+ */
27
+
28
+ "use strict";
29
+
30
+ import { existsSync, readFileSync, mkdirSync } from "node:fs";
31
+ import { join, dirname, resolve } from "node:path";
32
+
33
+ import { writeJsonAtomic } from "../fs-atomic.mjs";
34
+
35
+ /** The cache may never be older than this (the pre-spawn gate reads it). */
36
+ export const QUOTA_MAX_TTL_MS = 30_000;
37
+ export const QUOTA_DEFAULT_TTL_MS = 15_000;
38
+ /** Utilization at which a window warns (contract: status "warning" at ≥ 0.75). */
39
+ export const WARN_UTILIZATION = 0.75;
40
+ export const LIMIT_UTILIZATION = 1;
41
+ /** A limit refusal without a parseable resetsAt holds this long, flagged estimated. */
42
+ export const DEFAULT_LIMIT_HOLD_MS = 30 * 60_000;
43
+ export const CONCURRENCY_RETRY_MS = 2_000;
44
+ export const OVERLOADED_RETRY_MS = 30_000;
45
+ /** How long a failed quota read defers a spawn before the gate asks again. */
46
+ export const QUOTA_UNAVAILABLE_RETRY_MS = 30_000;
47
+ export const QUOTA_TIMEOUT_MS = 8_000;
48
+
49
+ /** Every refusal code in the wire contract. */
50
+ export const REFUSAL_CODES = Object.freeze([
51
+ "cohort_limit_exceeded",
52
+ "cohort_credits_exhausted",
53
+ "cohort_spend_limit_reached",
54
+ "cohort_concurrency_exceeded",
55
+ "cohort_upstream_overloaded",
56
+ "cohort_unpriced_model",
57
+ "cohort_unavailable",
58
+ "cohort_seat_suspended",
59
+ "cohort_seat_required",
60
+ "cohort_invalid_key",
61
+ ]);
62
+ const WINDOWS = ["5h", "7d", "30d"];
63
+
64
+ const str = (v) => (typeof v === "string" && v !== "" ? v : null);
65
+ const numOrNull = (v) => {
66
+ if (v === null || v === undefined || v === "") return null;
67
+ const n = Number(v);
68
+ return Number.isFinite(n) ? n : null;
69
+ };
70
+ /** Integer micro-USD as a decimal string (digits, optional sign), or null. */
71
+ const micros = (v) => {
72
+ if (typeof v === "number") return Number.isSafeInteger(v) ? String(v) : null;
73
+ if (typeof v !== "string") return null;
74
+ return /^-?\d+$/.test(v.trim()) ? v.trim() : null;
75
+ };
76
+
77
+ function header(headers, name) {
78
+ if (!headers) return null;
79
+ if (typeof headers.get === "function") return headers.get(name);
80
+ const key = Object.keys(headers).find((k) => k.toLowerCase() === name);
81
+ if (key === undefined) return null;
82
+ const v = headers[key];
83
+ return Array.isArray(v) ? v.join(", ") : v == null ? null : String(v);
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Pure: parsers
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * Normalise a refusal object (`<refusal>` in §4.8). Accepts `resets_at` as an
92
+ * alias of `resetsAt`. null unless it carries a cohort_* code.
93
+ * @param {any} raw
94
+ * @returns {{code:string, window:string|null, scope:string|null, resetsAt:string|null, retryable:boolean, requestId:string|null}|null}
95
+ */
96
+ export function normaliseRefusal(raw) {
97
+ if (!raw || typeof raw !== "object") return null;
98
+ const code = str(raw.code);
99
+ if (!code || !code.startsWith("cohort_")) return null;
100
+ return {
101
+ code,
102
+ window: str(raw.window),
103
+ scope: str(raw.scope),
104
+ resetsAt: str(raw.resetsAt) || str(raw.resets_at),
105
+ retryable: raw.retryable === true,
106
+ requestId: str(raw.requestId) || str(raw.request_id),
107
+ };
108
+ }
109
+
110
+ /**
111
+ * The refusal carried by an error body on either wire. The top-level `cohort`
112
+ * object wins; an OpenAI-wire `error.code` (or Anthropic-wire `error`) naming a
113
+ * cohort code is the fallback.
114
+ * @param {string|object|null} body
115
+ * @returns {ReturnType<typeof normaliseRefusal>}
116
+ */
117
+ export function parseRefusalBody(body) {
118
+ let o = body;
119
+ if (typeof body === "string") {
120
+ try { o = JSON.parse(body); } catch { return null; }
121
+ }
122
+ if (!o || typeof o !== "object") return null;
123
+ const top = normaliseRefusal(o.cohort);
124
+ if (top) return top;
125
+ const e = o.error && typeof o.error === "object" ? o.error : null;
126
+ return e ? normaliseRefusal({ ...e, code: e.code }) : null;
127
+ }
128
+
129
+ /**
130
+ * The `x-cohort-*` headers on a gateway response.
131
+ * @param {Headers|object} headers
132
+ */
133
+ export function parseQuotaHeaders(headers) {
134
+ const windows = {};
135
+ for (const w of WINDOWS) {
136
+ const u = numOrNull(header(headers, `x-cohort-quota-${w}-utilization`));
137
+ const reset = str(header(headers, `x-cohort-quota-${w}-reset`));
138
+ if (u !== null || reset !== null) windows[w] = { utilization: u, resetsAt: reset };
139
+ }
140
+ return {
141
+ requestId: str(header(headers, "x-cohort-request-id")),
142
+ modelTier: str(header(headers, "x-cohort-model-tier")),
143
+ costMicros: micros(header(headers, "x-cohort-cost-micros")),
144
+ creditsRemainingMicros: micros(header(headers, "x-cohort-credits-remaining-micros")),
145
+ funding: str(header(headers, "x-cohort-funding")),
146
+ limitKind: str(header(headers, "x-cohort-limit-kind")),
147
+ shouldRetry: (() => {
148
+ const v = header(headers, "x-should-retry");
149
+ return v === "false" ? false : v === "true" ? true : null;
150
+ })(),
151
+ windows,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * The terminal `event: cohort` frame. Accepts a raw SSE block (or a whole
157
+ * stream — the LAST cohort event wins), the frame's `data` JSON text, or the
158
+ * parsed object. An unparseable frame is null: it is advisory and must never
159
+ * fail a completion that streamed correctly.
160
+ * @param {string|object} input
161
+ */
162
+ export function parseCohortSseFrame(input) {
163
+ let obj = null;
164
+ if (input && typeof input === "object") {
165
+ obj = input;
166
+ } else if (typeof input === "string") {
167
+ const text = input.trim();
168
+ if (text.startsWith("{")) {
169
+ try { obj = JSON.parse(text); } catch { obj = null; }
170
+ } else {
171
+ let data = null;
172
+ for (const block of text.split(/\r?\n\r?\n/)) {
173
+ const lines = block.split(/\r?\n/);
174
+ const ev = lines.find((l) => /^event:\s*/.test(l));
175
+ if (!ev || ev.replace(/^event:\s*/, "").trim() !== "cohort") continue;
176
+ data = lines.filter((l) => /^data:/.test(l)).map((l) => l.replace(/^data:\s?/, "")).join("\n");
177
+ }
178
+ if (data !== null) {
179
+ try { obj = JSON.parse(data); } catch { obj = null; }
180
+ }
181
+ }
182
+ }
183
+ if (!obj || typeof obj !== "object") return null;
184
+ const q = obj.quota && typeof obj.quota === "object" ? obj.quota : {};
185
+ const windows = {};
186
+ for (const w of WINDOWS) {
187
+ const win = q[w];
188
+ if (!win || typeof win !== "object") continue;
189
+ windows[w] = { utilization: numOrNull(win.utilization), resetsAt: str(win.resetsAt) || str(win.resets_at) };
190
+ }
191
+ return {
192
+ requestId: str(obj.request_id) || str(obj.requestId),
193
+ modelTier: str(obj.model_tier) || str(obj.modelTier),
194
+ costMicros: micros(obj.cost_micros ?? obj.costMicros),
195
+ funding: str(obj.funding),
196
+ windows,
197
+ usage: obj.usage && typeof obj.usage === "object" ? { ...obj.usage } : null,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Validate and normalise a `GET /cohort/v1/quota` 200 body.
203
+ * @param {any} body
204
+ * @returns {{ok:true, quota:object}|{ok:false, error:{code:string, message:string}}}
205
+ */
206
+ export function parseQuotaResponse(body) {
207
+ if (!body || typeof body !== "object" || !Array.isArray(body.windows) || !body.canAdmit || typeof body.canAdmit !== "object") {
208
+ return { ok: false, error: { code: "cohort_quota_malformed", message: "the quota response is missing windows[] or canAdmit" } };
209
+ }
210
+ const windows = body.windows
211
+ .filter((w) => w && typeof w === "object" && WINDOWS.includes(w.kind))
212
+ .map((w) => ({
213
+ kind: w.kind,
214
+ limitMicros: micros(w.limitMicros),
215
+ consumedMicros: micros(w.consumedMicros),
216
+ utilization: numOrNull(w.utilization),
217
+ resetsAt: str(w.resetsAt),
218
+ status: str(w.status),
219
+ }));
220
+ const canAdmit = body.canAdmit.ok === true
221
+ ? { ok: true, funding: str(body.canAdmit.funding) }
222
+ : { ok: false, cohort: normaliseRefusal(body.canAdmit.cohort) || { code: "cohort_unavailable", window: null, scope: null, resetsAt: null, retryable: false, requestId: null } };
223
+ return {
224
+ ok: true,
225
+ quota: {
226
+ principal: body.principal && typeof body.principal === "object" ? { ...body.principal } : null,
227
+ windows,
228
+ extraUsageEnabled: body.extraUsageEnabled === true,
229
+ credits: body.credits && typeof body.credits === "object"
230
+ ? { remainingMicros: micros(body.credits.remainingMicros), overageEnabled: body.credits.overageEnabled === true }
231
+ : null,
232
+ spendLimits: (Array.isArray(body.spendLimits) ? body.spendLimits : [])
233
+ .filter((s) => s && typeof s === "object")
234
+ .map((s) => ({ scope: str(s.scope), period: str(s.period), limitMicros: micros(s.limitMicros), consumedMicros: micros(s.consumedMicros), resetsAt: str(s.resetsAt) })),
235
+ canAdmit,
236
+ asOf: str(body.asOf),
237
+ },
238
+ };
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // Pure: decisions
243
+ // ---------------------------------------------------------------------------
244
+
245
+ /**
246
+ * What a spawn site does with a refusal.
247
+ *
248
+ * cohort_limit_exceeded, cohort_spend_limit_reached → defer_until resetsAt
249
+ * cohort_credits_exhausted → hold_for_credits
250
+ * cohort_concurrency_exceeded, cohort_upstream_overloaded → retry afterMs
251
+ * cohort_unavailable (a gateway-side provider outage) → retry afterMs
252
+ * anything else → fail
253
+ *
254
+ * @param {object|null} refusal a normalised refusal
255
+ * @param {{now?:number, retryAfterMs?:number|null, defaultHoldMs?:number, unavailableRetryMs?:number}} [o]
256
+ * unavailableRetryMs overrides QUOTA_UNAVAILABLE_RETRY_MS (the 30 s
257
+ * cohort_unavailable hold; its value is pending owner confirmation, CF-143)
258
+ * @returns {{kind:"defer_until", until:number, code:string, estimated?:true} |
259
+ * {kind:"hold_for_credits", code:string} |
260
+ * {kind:"retry", afterMs:number, code:string} |
261
+ * {kind:"fail", code:string|null}}
262
+ */
263
+ export function refusalDisposition(refusal, o = {}) {
264
+ const r = refusal && typeof refusal === "object" ? refusal : null;
265
+ const code = r ? r.code : null;
266
+ const now = Number.isFinite(o.now) ? o.now : 0;
267
+ const retryAfter = Number.isFinite(o.retryAfterMs) && o.retryAfterMs >= 0 ? o.retryAfterMs : null;
268
+ switch (code) {
269
+ case "cohort_limit_exceeded":
270
+ case "cohort_spend_limit_reached": {
271
+ const until = r.resetsAt ? Date.parse(r.resetsAt) : NaN;
272
+ if (Number.isFinite(until)) return { kind: "defer_until", until, code };
273
+ return { kind: "defer_until", until: now + (Number.isFinite(o.defaultHoldMs) ? o.defaultHoldMs : DEFAULT_LIMIT_HOLD_MS), code, estimated: true };
274
+ }
275
+ case "cohort_credits_exhausted":
276
+ return { kind: "hold_for_credits", code };
277
+ case "cohort_concurrency_exceeded":
278
+ return { kind: "retry", afterMs: retryAfter ?? CONCURRENCY_RETRY_MS, code };
279
+ case "cohort_upstream_overloaded":
280
+ return { kind: "retry", afterMs: retryAfter ?? OVERLOADED_RETRY_MS, code };
281
+ case "cohort_unavailable":
282
+ // Cohort's own provider key is refused or unfunded upstream (503,
283
+ // x-should-retry: true): a gateway outage, not this seat's credential or
284
+ // quota. Back off and look again; never a fail-hold, never a re-mint.
285
+ return { kind: "retry", afterMs: retryAfter ?? (Number.isFinite(o.unavailableRetryMs) && o.unavailableRetryMs >= 0 ? o.unavailableRetryMs : QUOTA_UNAVAILABLE_RETRY_MS), code };
286
+ default:
287
+ return { kind: "fail", code };
288
+ }
289
+ }
290
+
291
+ /**
292
+ * The seat's band from a quota snapshot: the worst window (and spend limit)
293
+ * utilization, "approaching" at ≥ 75 %, "at" at ≥ 100 % or when canAdmit is
294
+ * refused. An absent snapshot is "unknown".
295
+ * @param {object|null} quota a parseQuotaResponse().quota
296
+ * @returns {{band:"ok"|"approaching"|"at"|"unknown", utilization:number|null,
297
+ * window:string|null, resetsAt:string|null, canAdmit:boolean|null,
298
+ * limitMicros:string|null, consumedMicros:string|null}}
299
+ */
300
+ export function quotaBand(quota) {
301
+ if (!quota || typeof quota !== "object") {
302
+ return { band: "unknown", utilization: null, window: null, resetsAt: null, canAdmit: null, limitMicros: null, consumedMicros: null };
303
+ }
304
+ let worst = null;
305
+ const consider = (label, utilization, resetsAt, limitMicros, consumedMicros) => {
306
+ if (utilization === null || !Number.isFinite(utilization)) return;
307
+ if (!worst || utilization > worst.utilization) worst = { window: label, utilization, resetsAt, limitMicros, consumedMicros };
308
+ };
309
+ for (const w of quota.windows || []) consider(w.kind, w.utilization, w.resetsAt, w.limitMicros, w.consumedMicros);
310
+ for (const s of quota.spendLimits || []) {
311
+ const lim = Number(s.limitMicros);
312
+ const used = Number(s.consumedMicros);
313
+ if (Number.isFinite(lim) && lim > 0 && Number.isFinite(used)) consider(`spend:${s.scope}:${s.period}`, used / lim, s.resetsAt, s.limitMicros, s.consumedMicros);
314
+ }
315
+ const admits = quota.canAdmit ? quota.canAdmit.ok === true : null;
316
+ const u = worst ? worst.utilization : 0;
317
+ const band = admits === false || u >= LIMIT_UTILIZATION ? "at" : u >= WARN_UTILIZATION ? "approaching" : "ok";
318
+ return {
319
+ band,
320
+ utilization: worst ? worst.utilization : null,
321
+ window: worst ? worst.window : null,
322
+ resetsAt: worst ? worst.resetsAt : (quota.canAdmit && quota.canAdmit.cohort && quota.canAdmit.cohort.resetsAt) || null,
323
+ canAdmit: admits,
324
+ limitMicros: worst ? worst.limitMicros : null,
325
+ consumedMicros: worst ? worst.consumedMicros : null,
326
+ };
327
+ }
328
+
329
+ /**
330
+ * The admission answer for a quota read (or a failed read). Pure.
331
+ * @param {{ok:boolean, quota?:object, error?:object, refusal?:object, retryAfterMs?:number}} read
332
+ * @param {number} now
333
+ */
334
+ export function admissionFromRead(read, now) {
335
+ if (read && read.ok && read.quota) {
336
+ const ca = read.quota.canAdmit;
337
+ if (ca && ca.ok) return { ok: true, funding: ca.funding || null, quota: read.quota };
338
+ const refusal = (ca && ca.cohort) || null;
339
+ return { ok: false, refusal, disposition: refusalDisposition(refusal, { now }), quota: read.quota };
340
+ }
341
+ const error = (read && read.error) || { code: "cohort_quota_unavailable", message: "no quota read" };
342
+ if (read && read.refusal) {
343
+ const d = refusalDisposition(read.refusal, { now, retryAfterMs: read.retryAfterMs });
344
+ if (d.kind !== "fail") return { ok: false, refusal: read.refusal, disposition: d, error };
345
+ }
346
+ // A seat whose key is refused cannot be fixed by waiting 30 s.
347
+ const hard = error.code === "cohort_invalid_key" || error.code === "cohort_seat_required" || error.code === "cohort_seat_key_missing" || error.code === "cohort_seat_suspended";
348
+ return {
349
+ ok: false,
350
+ error,
351
+ refusal: read && read.refusal ? read.refusal : null,
352
+ disposition: hard ? { kind: "fail", code: error.code } : { kind: "retry", afterMs: QUOTA_UNAVAILABLE_RETRY_MS, code: error.code },
353
+ };
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Edge: the client
358
+ // ---------------------------------------------------------------------------
359
+
360
+ /** Where the seat's last quota snapshot lives. */
361
+ export function quotaStatePath(agentRoot) {
362
+ return join(resolve(agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd()), "state", "cohort-llm", "quota.json");
363
+ }
364
+
365
+ /**
366
+ * Read the persisted snapshot if it is no older than `maxAgeMs`. Never throws.
367
+ * @param {string} path
368
+ * @param {number} now
369
+ * @param {number} maxAgeMs
370
+ * @returns {object|null} the normalised quota, with `fetchedAt`
371
+ */
372
+ export function readQuotaSnapshot(path, now, maxAgeMs) {
373
+ try {
374
+ if (!existsSync(path)) return null;
375
+ const rec = JSON.parse(readFileSync(path, "utf-8"));
376
+ const at = rec && Number(rec.fetchedAtMs);
377
+ if (!rec || !rec.quota || !Number.isFinite(at)) return null;
378
+ if (Number.isFinite(maxAgeMs) && now - at > maxAgeMs) return null;
379
+ return { ...rec.quota, fetchedAtMs: at };
380
+ } catch {
381
+ return null;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * @param {object} o
387
+ * @param {string} o.baseUrl
388
+ * @param {(opts?:{force?:boolean}) => Promise<{ok:boolean, token?:string, error?:object}>} o.getToken
389
+ * @param {() => void} [o.invalidateToken]
390
+ * @param {typeof fetch} [o.fetchImpl]
391
+ * @param {() => number} [o.now]
392
+ * @param {number} [o.ttlMs] clamped to ≤ 30 s
393
+ * @param {string|null} [o.statePath]
394
+ * @param {(level:string, msg:string) => void} [o.log]
395
+ */
396
+ export function createQuotaClient(o = {}) {
397
+ const baseUrl = String(o.baseUrl || "").replace(/\/+$/, "");
398
+ const now = typeof o.now === "function" ? o.now : Date.now;
399
+ const ttlMs = Math.min(QUOTA_MAX_TTL_MS, Math.max(0, Number.isFinite(o.ttlMs) ? o.ttlMs : QUOTA_DEFAULT_TTL_MS));
400
+ const fetchImpl = typeof o.fetchImpl === "function" ? o.fetchImpl : globalThis.fetch;
401
+ const statePath = o.statePath || null;
402
+ const log = (level, msg) => { if (typeof o.log === "function") { try { o.log(level, msg); } catch { /* */ } } };
403
+ let cache = null; // {at, read}
404
+ let inflight = null;
405
+
406
+ async function fetchOnce(token) {
407
+ let res;
408
+ try {
409
+ res = await fetchImpl(`${baseUrl}/cohort/v1/quota`, {
410
+ method: "GET",
411
+ headers: { authorization: `Bearer ${token}`, accept: "application/json" },
412
+ signal: typeof AbortSignal !== "undefined" && AbortSignal.timeout ? AbortSignal.timeout(QUOTA_TIMEOUT_MS) : undefined,
413
+ });
414
+ } catch (err) {
415
+ return { ok: false, status: null, error: { code: "cohort_unavailable", message: `quota read did not complete: ${err && err.message ? err.message : err}` } };
416
+ }
417
+ let body = null;
418
+ try { body = await res.json(); } catch { body = null; }
419
+ if (!res.ok) {
420
+ const refusal = parseRefusalBody(body);
421
+ const ra = numOrNull(header(res.headers, "retry-after"));
422
+ return {
423
+ ok: false,
424
+ status: res.status,
425
+ refusal,
426
+ retryAfterMs: ra === null ? null : ra * 1000,
427
+ error: { code: (refusal && refusal.code) || (res.status === 401 ? "cohort_invalid_key" : `http_${res.status}`), message: `quota read refused (HTTP ${res.status})` },
428
+ };
429
+ }
430
+ const parsed = parseQuotaResponse(body);
431
+ return parsed.ok ? { ok: true, status: res.status, quota: parsed.quota } : { ok: false, status: res.status, error: parsed.error };
432
+ }
433
+
434
+ async function load() {
435
+ if (!baseUrl || typeof fetchImpl !== "function" || typeof o.getToken !== "function") {
436
+ return { ok: false, error: { code: "cohort_quota_unconfigured", message: "the quota client has no base URL, fetch, or token source" } };
437
+ }
438
+ let t = await o.getToken();
439
+ if (!t || !t.ok) return { ok: false, error: (t && t.error) || { code: "cohort_unavailable", message: "no seat token" } };
440
+ let read = await fetchOnce(t.token);
441
+ if (!read.ok && read.status === 401) {
442
+ // The token was revoked or rotated under us: mint once more, then give up.
443
+ // Naming the refused token lets the manager serve a replacement another
444
+ // process already minted, and caps re-mints per window (CF-136). A 503 or
445
+ // a 429 on this read never reaches here, so it never mints.
446
+ const refused = t.token;
447
+ if (typeof o.invalidateToken === "function") o.invalidateToken(refused);
448
+ t = await o.getToken({ force: true, rejected: refused });
449
+ if (!t || !t.ok) return { ok: false, error: (t && t.error) || { code: "cohort_invalid_key", message: "the seat token was refused" } };
450
+ read = await fetchOnce(t.token);
451
+ }
452
+ return read;
453
+ }
454
+
455
+ /**
456
+ * The quota, from the cache when it is fresh.
457
+ * @param {{force?:boolean}} [opts]
458
+ */
459
+ async function getQuota(opts = {}) {
460
+ const t = now();
461
+ if (!opts.force && cache && t - cache.at <= ttlMs) return { ...cache.read, cached: true };
462
+ if (!inflight) {
463
+ inflight = load()
464
+ .then((read) => {
465
+ const at = now();
466
+ cache = { at, read };
467
+ if (read.ok && statePath) {
468
+ try {
469
+ mkdirSync(dirname(statePath), { recursive: true });
470
+ writeJsonAtomic(statePath, { fetchedAtMs: at, quota: read.quota });
471
+ } catch (err) {
472
+ log("warn", `could not persist the quota snapshot: ${err && err.message ? err.message : err}`);
473
+ }
474
+ }
475
+ if (!read.ok) log("warn", `cohort-llm quota read failed: ${read.error && read.error.code}`);
476
+ return read;
477
+ })
478
+ .finally(() => { inflight = null; });
479
+ }
480
+ return inflight;
481
+ }
482
+
483
+ /** The cached read when fresh, else null. Synchronous. */
484
+ function peek() {
485
+ if (cache && now() - cache.at <= ttlMs) return cache.read;
486
+ return null;
487
+ }
488
+
489
+ /** May a cohort-routed session start now? */
490
+ async function canAdmit(opts = {}) {
491
+ const read = await getQuota(opts);
492
+ return admissionFromRead(read, now());
493
+ }
494
+
495
+ /** Synchronous admission from the cache; `{cold:true}` when there is none. */
496
+ function canAdmitCached() {
497
+ const read = peek();
498
+ if (!read) return { cold: true };
499
+ return admissionFromRead(read, now());
500
+ }
501
+
502
+ function invalidate() { cache = null; }
503
+
504
+ return { getQuota, peek, canAdmit, canAdmitCached, invalidate, ttlMs, baseUrl, statePath };
505
+ }
506
+
507
+ export default {
508
+ REFUSAL_CODES,
509
+ normaliseRefusal,
510
+ parseRefusalBody,
511
+ parseQuotaHeaders,
512
+ parseCohortSseFrame,
513
+ parseQuotaResponse,
514
+ refusalDisposition,
515
+ quotaBand,
516
+ admissionFromRead,
517
+ quotaStatePath,
518
+ readQuotaSnapshot,
519
+ createQuotaClient,
520
+ };