@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
@@ -0,0 +1,269 @@
1
+ /**
2
+ * lib/engine/commands/index.mjs — slash commands in a prompt (W4-E2, conformance
3
+ * rows 14 and 34).
4
+ *
5
+ * A prompt (the -p prompt, or a stream-json user message) whose text starts with
6
+ * `/<name>` followed by whitespace or the end is a slash invocation:
7
+ *
8
+ * /review src/loop.mjs name "review", arguments "src/loop.mjs"
9
+ * /maestro:main-session a namespaced name
10
+ * /Users/me/notes.txt NOT an invocation (the name is not followed by
11
+ * whitespace or the end), passed on as written
12
+ *
13
+ * The name resolves, in order, to:
14
+ * 1. a project command <cwd>/.claude/commands/**\/*.md
15
+ * 2. a user command ~/.claude/commands/**\/*.md
16
+ * 3. a skill skills/index.mjs `findSkill` (exact name, plugin namespace
17
+ * alias, the flat installed form `<ns>-<name>`, a unique short name)
18
+ * 4. a plugin command <plugin root>/commands/*.md, named `<plugin>:<name>`
19
+ * A command file in a subdirectory is named by its file name; `<dir>:<name>`
20
+ * (subdirectories joined by ":") is accepted as well. The first name wins within
21
+ * a source; shadowed files are reported.
22
+ *
23
+ * A command file is Markdown with optional frontmatter: `description`,
24
+ * `argument-hint`, `allowed-tools` (recorded, not enforced, as for skills) and
25
+ * `model` (the turn runs on that model's Cohort tier). Its body becomes the
26
+ * turn's instructions: `$ARGUMENTS` is replaced by the argument text and `$1` …
27
+ * `$9` by the arguments split on whitespace (quotes group); a body with neither
28
+ * gets the arguments appended. A skill's body is injected with its base
29
+ * directory, exactly as the Skill tool loads it.
30
+ *
31
+ * An unknown name is not an error: the prompt is passed on as written, with a
32
+ * note saying no command or skill had that name.
33
+ *
34
+ * Everything here is pure except `discoverCommands` and `expandSlashCommand`,
35
+ * which read through an injected fs.
36
+ *
37
+ * Authorship: the injected instruction text was written for Cohort Engine by the
38
+ * Cohort team, 2026-09-14.
39
+ *
40
+ * @module lib/engine/commands/index
41
+ */
42
+
43
+ import path from "node:path";
44
+ import { expandPluginRoots, findSkill, parseSkillFile, pluginIdentity } from "../skills/index.mjs";
45
+
46
+ const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*(?::[A-Za-z0-9][A-Za-z0-9_.-]*)*$/;
47
+ const SLASH_RE = /^\/([A-Za-z0-9][A-Za-z0-9_.-]*(?::[A-Za-z0-9][A-Za-z0-9_.-]*)*)(?:[ \t\r\n]+([\s\S]*))?$/;
48
+ const MAX_COMMAND_DEPTH = 3;
49
+
50
+ /**
51
+ * @typedef {Object} Command
52
+ * @property {string} name
53
+ * @property {string[]} aliases
54
+ * @property {string} description
55
+ * @property {string|null} argumentHint
56
+ * @property {string[]} allowedTools
57
+ * @property {string|null} model
58
+ * @property {string} file
59
+ * @property {'project'|'user'|'plugin'} source
60
+ */
61
+
62
+ /** @param {unknown} v */
63
+ function toolList(v) {
64
+ if (Array.isArray(v)) return v.map(String).map((s) => s.trim()).filter(Boolean);
65
+ if (typeof v === "string") return v.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
66
+ return [];
67
+ }
68
+
69
+ /**
70
+ * Is this text a slash invocation? Pure.
71
+ * @param {string} text
72
+ * @returns {{name:string, args:string}|null}
73
+ */
74
+ export function parseSlashCommand(text) {
75
+ const m = SLASH_RE.exec(String(text ?? "").replace(/^\s+/, "").replace(/\s+$/, ""));
76
+ return m ? { name: m[1], args: (m[2] ?? "").trim() } : null;
77
+ }
78
+
79
+ /**
80
+ * Split arguments on whitespace; single or double quotes group. Pure.
81
+ * @param {string} args
82
+ */
83
+ export function splitArguments(args) {
84
+ const out = [];
85
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
86
+ for (let m = re.exec(args); m; m = re.exec(args)) out.push(m[1] ?? m[2] ?? m[3]);
87
+ return out;
88
+ }
89
+
90
+ /**
91
+ * A command body with its arguments substituted. Pure.
92
+ * @param {string} body @param {string} args
93
+ */
94
+ export function substituteArguments(body, args) {
95
+ const positional = splitArguments(args);
96
+ const hasArguments = body.includes("$ARGUMENTS");
97
+ const hasPositional = /\$[1-9](?!\d)/.test(body);
98
+ let out = hasArguments ? body.split("$ARGUMENTS").join(args) : body;
99
+ if (hasPositional) out = out.replace(/\$([1-9])(?!\d)/g, (_, n) => positional[Number(n) - 1] ?? "");
100
+ if (!hasArguments && !hasPositional && args) out = `${out}\n\nArguments: ${args}`;
101
+ return out;
102
+ }
103
+
104
+ /**
105
+ * @param {{file:string, text:string, name:string, aliases:string[], source:Command['source']}} p
106
+ * @returns {Command}
107
+ */
108
+ export function commandFromFile({ file, text, name, aliases, source }) {
109
+ const { frontmatter: fm, body } = parseSkillFile(text);
110
+ let description = typeof fm.description === "string" ? fm.description.trim() : "";
111
+ if (description === "") description = (body.split(/\r?\n/).map((l) => l.replace(/^#+\s*/, "").trim()).find((l) => l !== "") ?? "").slice(0, 200);
112
+ const hint = fm["argument-hint"] ?? fm.argument_hint;
113
+ const model = typeof fm.model === "string" && fm.model.trim() !== "" ? fm.model.trim() : null;
114
+ return { name, aliases, description, argumentHint: typeof hint === "string" ? hint : null, allowedTools: toolList(fm["allowed-tools"] ?? fm.allowed_tools), model, file, source };
115
+ }
116
+
117
+ /**
118
+ * @typedef {Object} CommandFs
119
+ * @property {(p:string)=>string} readFile
120
+ * @property {(p:string)=>string[]} readdir
121
+ * @property {(p:string)=>boolean} isFile
122
+ * @property {(p:string)=>boolean} isDir
123
+ */
124
+
125
+ /**
126
+ * @param {object} p
127
+ * @param {string|null} p.userDir ~/.claude (null: no user commands)
128
+ * @param {string|null} p.cwd null: no project commands (--bare)
129
+ * @param {Array<{dir:string}>} [p.pluginDirs]
130
+ * @param {CommandFs} p.fs
131
+ * @returns {{commands: Command[], errors: string[]}}
132
+ */
133
+ export function discoverCommands({ userDir, cwd, pluginDirs = [], fs }) {
134
+ /** @type {Command[]} */
135
+ const commands = [];
136
+ const errors = [];
137
+ const names = (/** @type string */ dir) => {
138
+ try {
139
+ return fs.readdir(dir).sort();
140
+ } catch {
141
+ return [];
142
+ }
143
+ };
144
+ /** @param {string} dir @param {Command['source']} source @param {string|null} namespace @param {string[]} nsAliases */
145
+ const scan = (dir, source, namespace, nsAliases) => {
146
+ /** @type {Map<string, Command>} */
147
+ const seen = new Map();
148
+ /** @param {string} d @param {string[]} parents */
149
+ const walk = (d, parents) => {
150
+ if (!fs.isDir(d)) return;
151
+ for (const entry of names(d)) {
152
+ const full = path.join(d, entry);
153
+ if (entry.startsWith(".")) continue;
154
+ if (fs.isDir(full)) {
155
+ if (namespace === null && parents.length + 1 < MAX_COMMAND_DEPTH) walk(full, [...parents, entry]);
156
+ continue;
157
+ }
158
+ if (!entry.endsWith(".md") || !fs.isFile(full)) continue;
159
+ const base = entry.slice(0, -3);
160
+ if (!NAME_RE.test(base)) continue;
161
+ const name = namespace ? `${namespace}:${base}` : base;
162
+ const aliases = namespace ? nsAliases.map((a) => `${a}:${base}`) : parents.length ? [[...parents, base].join(":")] : [];
163
+ if (seen.has(name)) {
164
+ errors.push(`command "${name}" at ${full} is shadowed by ${seen.get(name)?.file}`);
165
+ continue;
166
+ }
167
+ let text;
168
+ try {
169
+ text = fs.readFile(full);
170
+ } catch (e) {
171
+ errors.push(`command ${full}: cannot read (${e instanceof Error ? e.message : e})`);
172
+ continue;
173
+ }
174
+ const c = commandFromFile({ file: full, text, name, aliases: aliases.filter((a) => NAME_RE.test(a)), source });
175
+ seen.set(name, c);
176
+ commands.push(c);
177
+ }
178
+ };
179
+ walk(dir, []);
180
+ };
181
+ if (cwd) scan(path.join(cwd, ".claude", "commands"), "project", null, []);
182
+ if (userDir) scan(path.join(userDir, "commands"), "user", null, []);
183
+ for (const root of expandPluginRoots(pluginDirs, fs)) {
184
+ if (!fs.isDir(root)) continue; // skills discovery reports a missing plugin root
185
+ const identity = pluginIdentity(root, fs);
186
+ scan(path.join(root, "commands"), "plugin", identity.name, identity.aliases);
187
+ }
188
+ return { commands, errors };
189
+ }
190
+
191
+ /**
192
+ * Find a command by name among one source's commands. Pure.
193
+ * @param {Command[]} commands @param {string} name
194
+ */
195
+ function findCommand(commands, name) {
196
+ const exact = commands.find((c) => c.name === name) ?? commands.find((c) => c.aliases.includes(name));
197
+ if (exact) return exact;
198
+ const colon = name.indexOf(":");
199
+ if (colon > 0 && name.indexOf(":", colon + 1) === -1) return commands.find((c) => c.name === `${name.slice(0, colon)}-${name.slice(colon + 1)}`) ?? null;
200
+ return null;
201
+ }
202
+
203
+ /**
204
+ * What a prompt's slash invocation names. Pure.
205
+ * @param {{text:string, commands:Command[], skills:import('../skills/index.mjs').Skill[]}} p
206
+ * @returns {{kind:'none'} | {kind:'command', name:string, args:string, command:Command} | {kind:'skill', name:string, args:string, skill:import('../skills/index.mjs').Skill} | {kind:'unknown', name:string, args:string, reason:string|null}}
207
+ */
208
+ export function resolveSlashCommand({ text, commands, skills }) {
209
+ const parsed = parseSlashCommand(text);
210
+ if (!parsed) return { kind: "none" };
211
+ const { name, args } = parsed;
212
+ for (const source of /** @type {const} */ (["project", "user"])) {
213
+ const c = findCommand(commands.filter((x) => x.source === source), name);
214
+ if (c) return { kind: "command", name, args, command: c };
215
+ }
216
+ const s = findSkill(skills, name);
217
+ if (s.ok) return { kind: "skill", name, args, skill: s.skill };
218
+ const p = findCommand(commands.filter((x) => x.source === "plugin"), name);
219
+ if (p) return { kind: "command", name, args, command: p };
220
+ // A short name that several plugin skills share is ambiguous, not unknown.
221
+ return { kind: "unknown", name, args, reason: /matches several skills/.test(s.error) ? s.error : null };
222
+ }
223
+
224
+ /**
225
+ * The note an unknown slash command's prompt carries. Pure.
226
+ * @param {string} name @param {string|null} [reason]
227
+ */
228
+ export function unknownCommandNote(name, reason = null) {
229
+ return `[Note: "/${name}" is not a command or skill available in this session${reason ? ` (${reason})` : ""}; the message above is passed on as written.]`;
230
+ }
231
+
232
+ /**
233
+ * The text a resolved invocation puts in the turn, read through `fs`.
234
+ * @param {Exclude<ReturnType<typeof resolveSlashCommand>, {kind:'none'}>} resolved
235
+ * @param {{readFile:(p:string)=>string}} fs
236
+ * @param {string} original the prompt as typed
237
+ * @returns {{ok:true, text:string, model:string|null, allowedTools:string[], invoked:{kind:string, name:string, file:string|null}|null} | {ok:false, error:string}}
238
+ */
239
+ export function expandSlashCommand(resolved, fs, original) {
240
+ if (resolved.kind === "unknown") {
241
+ return { ok: true, text: `${original}\n\n${unknownCommandNote(resolved.name, resolved.reason)}`, model: null, allowedTools: [], invoked: null };
242
+ }
243
+ const file = resolved.kind === "command" ? resolved.command.file : resolved.skill.file;
244
+ let raw;
245
+ try {
246
+ raw = fs.readFile(file);
247
+ } catch (e) {
248
+ return { ok: false, error: `/${resolved.name}: ${file} could not be read (${e instanceof Error ? e.message : e})` };
249
+ }
250
+ const body = substituteArguments(parseSkillFile(raw).body.trim(), resolved.args);
251
+ if (resolved.kind === "command") {
252
+ const c = resolved.command;
253
+ return {
254
+ ok: true,
255
+ text: `The user ran the /${c.name} command${resolved.args ? ` with the arguments: ${resolved.args}` : ""}. Carry out its instructions, which follow.\n\n${body}`,
256
+ model: c.model,
257
+ allowedTools: c.allowedTools,
258
+ invoked: { kind: "command", name: c.name, file: c.file },
259
+ };
260
+ }
261
+ const s = resolved.skill;
262
+ return {
263
+ ok: true,
264
+ text: `The user invoked the "${s.name}" skill${resolved.args ? ` with the arguments: ${resolved.args}` : ""}. Its base directory is ${s.baseDir}; resolve relative paths in these instructions against it. Follow its instructions below.\n\n${body}`,
265
+ model: null,
266
+ allowedTools: s.allowedTools,
267
+ invoked: { kind: "skill", name: s.name, file: s.file },
268
+ };
269
+ }
@@ -0,0 +1,219 @@
1
+ /**
2
+ * lib/engine/context/budget.mjs — how big a request is, and when to compact (pure).
3
+ *
4
+ * Context windows are DATA: `CONTEXT_WINDOWS` maps each Cohort tier to the
5
+ * window the engine plans against. A settings file (`cohort.contextWindows`)
6
+ * or `COHORT_ENGINE_CONTEXT_WINDOW` overrides it, because the slot map behind a
7
+ * tier is decided by the parity eval and can change without an engine release.
8
+ *
9
+ * Estimation is a characters-per-token heuristic, CALIBRATED by the provider:
10
+ * after every model call the loop reports the input tokens the gateway counted
11
+ * for a request of a known size (`calibrate`). The projection for the next
12
+ * request is then the provider's own count plus the growth since, converted at
13
+ * the observed ratio (`projectTokens`). Before the first call the default
14
+ * ratio (4 characters per token) is used. Images are counted at a flat
15
+ * estimate each; their bytes say little about their token cost.
16
+ *
17
+ * Compaction fires when the projected input reaches `threshold` of the window
18
+ * (default 85%). Everything here is a function of its inputs: no clock, no
19
+ * environment read — `resolveContextConfig` takes the environment as a
20
+ * parameter.
21
+ *
22
+ * @module lib/engine/context/budget
23
+ */
24
+
25
+ /** The window, in tokens, the engine plans against per tier. */
26
+ export const CONTEXT_WINDOWS = Object.freeze({
27
+ "cohort-agentic": 1_000_000,
28
+ "cohort-reason": 1_000_000,
29
+ "cohort-fast": 128_000,
30
+ "cohort-extract": 128_000,
31
+ "cohort-vision": 128_000,
32
+ });
33
+ /** A tier the table does not know gets the smallest window any tier has. */
34
+ export const DEFAULT_CONTEXT_WINDOW = 128_000;
35
+ export const DEFAULT_COMPACT_THRESHOLD = 0.85;
36
+ export const DEFAULT_CHARS_PER_TOKEN = 4;
37
+ /** Tool results kept verbatim by stage-1 compaction. */
38
+ export const DEFAULT_KEEP_TOOL_RESULTS = 6;
39
+ /** Messages kept verbatim after a stage-2 summary (rounded back to an assistant turn). */
40
+ export const DEFAULT_KEEP_RECENT_MESSAGES = 6;
41
+ /** Image-carrying tool results whose images stay in the history (images.mjs). */
42
+ export const DEFAULT_KEEP_IMAGE_RESULTS = 3;
43
+ /** Tokens one image is assumed to cost. */
44
+ export const IMAGE_TOKEN_ESTIMATE = 1_600;
45
+ const MIN_RATIO = 1;
46
+ const MAX_RATIO = 10;
47
+
48
+ /**
49
+ * @typedef {Object} ContextConfig
50
+ * @property {number} contextWindow
51
+ * @property {number} threshold 0 < threshold ≤ 1
52
+ * @property {boolean} autoCompact
53
+ * @property {number} keepToolResults
54
+ * @property {number} keepRecentMessages
55
+ * @property {number} [keepImageResults] image-carrying tool results whose images stay in the history
56
+ */
57
+
58
+ /**
59
+ * @param {string} tier
60
+ * @param {Record<string, unknown>} [overrides] settings `cohort.contextWindows`
61
+ */
62
+ export function contextWindowFor(tier, overrides = {}) {
63
+ const o = overrides?.[tier];
64
+ if (typeof o === "number" && Number.isInteger(o) && o > 0) return o;
65
+ return /** @type {Record<string,number>} */ (CONTEXT_WINDOWS)[tier] ?? DEFAULT_CONTEXT_WINDOW;
66
+ }
67
+
68
+ /**
69
+ * Parse a threshold: a fraction (0.85) or a percentage (85).
70
+ * @param {unknown} v @returns {number|null}
71
+ */
72
+ export function parseThreshold(v) {
73
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v.trim().replace(/%$/, "")) : NaN;
74
+ if (!Number.isFinite(n) || n <= 0) return null;
75
+ const f = n > 1 ? n / 100 : n;
76
+ return f > 0 && f <= 1 ? f : null;
77
+ }
78
+
79
+ /**
80
+ * The run's context settings. Precedence: environment > settings > defaults.
81
+ *
82
+ * settings cohort.contextWindows {tier: tokens}
83
+ * cohort.compaction {threshold, auto, keepToolResults, keepRecentMessages, keepImageResults}
84
+ * env COHORT_ENGINE_CONTEXT_WINDOW, COHORT_ENGINE_COMPACT_THRESHOLD,
85
+ * COHORT_ENGINE_AUTO_COMPACT (0/false/off disables automatic compaction)
86
+ *
87
+ * @param {{tier:string, settings?:any, env?:Record<string,string|undefined>}} p
88
+ * @returns {{config: ContextConfig, warnings: string[]}}
89
+ */
90
+ export function resolveContextConfig({ tier, settings = {}, env = {} }) {
91
+ const warnings = [];
92
+ const s = settings && typeof settings === "object" ? settings : {};
93
+ const comp = s.compaction && typeof s.compaction === "object" ? s.compaction : {};
94
+ let contextWindow = contextWindowFor(tier, s.contextWindows);
95
+ if (env.COHORT_ENGINE_CONTEXT_WINDOW !== undefined && env.COHORT_ENGINE_CONTEXT_WINDOW !== "") {
96
+ const n = Number(env.COHORT_ENGINE_CONTEXT_WINDOW);
97
+ if (Number.isInteger(n) && n > 0) contextWindow = n;
98
+ else warnings.push(`COHORT_ENGINE_CONTEXT_WINDOW "${env.COHORT_ENGINE_CONTEXT_WINDOW}" is not a positive integer (ignored)`);
99
+ }
100
+ let threshold = DEFAULT_COMPACT_THRESHOLD;
101
+ if (comp.threshold !== undefined) {
102
+ const t = parseThreshold(comp.threshold);
103
+ if (t === null) warnings.push(`settings cohort.compaction.threshold ${JSON.stringify(comp.threshold)} is not a fraction or percentage (ignored)`);
104
+ else threshold = t;
105
+ }
106
+ if (env.COHORT_ENGINE_COMPACT_THRESHOLD !== undefined && env.COHORT_ENGINE_COMPACT_THRESHOLD !== "") {
107
+ const t = parseThreshold(env.COHORT_ENGINE_COMPACT_THRESHOLD);
108
+ if (t === null) warnings.push(`COHORT_ENGINE_COMPACT_THRESHOLD "${env.COHORT_ENGINE_COMPACT_THRESHOLD}" is not a fraction or percentage (ignored)`);
109
+ else threshold = t;
110
+ }
111
+ let autoCompact = comp.auto !== false;
112
+ const envAuto = String(env.COHORT_ENGINE_AUTO_COMPACT ?? "").trim().toLowerCase();
113
+ if (["0", "false", "off", "no"].includes(envAuto)) autoCompact = false;
114
+ else if (["1", "true", "on", "yes"].includes(envAuto)) autoCompact = true;
115
+ const posInt = (/** @type unknown */ v, /** @type number */ d) => (typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : d);
116
+ return {
117
+ config: {
118
+ contextWindow,
119
+ threshold,
120
+ autoCompact,
121
+ keepToolResults: posInt(comp.keepToolResults, DEFAULT_KEEP_TOOL_RESULTS),
122
+ keepRecentMessages: Math.max(2, posInt(comp.keepRecentMessages, DEFAULT_KEEP_RECENT_MESSAGES)),
123
+ keepImageResults: posInt(comp.keepImageResults, DEFAULT_KEEP_IMAGE_RESULTS),
124
+ },
125
+ warnings,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * The size of one request, in characters and images.
131
+ * @param {{system?:string, messages?:any[], tools?:any[]}} req
132
+ * @returns {{chars:number, images:number}}
133
+ */
134
+ export function measureRequest({ system = "", messages = [], tools = [] }) {
135
+ let chars = String(system ?? "").length;
136
+ let images = 0;
137
+ if (tools.length > 0) chars += JSON.stringify(tools).length;
138
+ for (const m of messages) {
139
+ if (!m || !Array.isArray(m.content)) continue;
140
+ for (const b of m.content) {
141
+ if (!b || typeof b !== "object") continue;
142
+ switch (b.type) {
143
+ case "text":
144
+ chars += String(b.text ?? "").length;
145
+ break;
146
+ case "tool_use":
147
+ chars += String(b.name ?? "").length + (b.rawInput ? String(b.rawInput).length : JSON.stringify(b.input ?? {}).length);
148
+ break;
149
+ case "tool_result":
150
+ chars += String(b.content ?? "").length;
151
+ if (Array.isArray(b.images)) images += b.images.length;
152
+ break;
153
+ case "thinking":
154
+ chars += String(b.thinking ?? "").length;
155
+ break;
156
+ default:
157
+ break;
158
+ }
159
+ }
160
+ }
161
+ return { chars, images };
162
+ }
163
+
164
+ /**
165
+ * @typedef {{charsPerToken:number, anchorChars:number|null, anchorImages:number, anchorTokens:number|null}} Calibration
166
+ */
167
+
168
+ /** @returns {Calibration} */
169
+ export function initialCalibration(charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
170
+ return { charsPerToken, anchorChars: null, anchorImages: 0, anchorTokens: null };
171
+ }
172
+
173
+ /**
174
+ * Input tokens a normalised usage record says the provider counted.
175
+ * @param {import('../wire/usage.mjs').NormalisedUsage|null|undefined} u
176
+ */
177
+ export function reportedInputTokens(u) {
178
+ if (!u) return 0;
179
+ return (u.inputUncached || 0) + (u.cacheRead || 0) + (u.cacheWrite5m || 0) + (u.cacheWrite1h || 0);
180
+ }
181
+
182
+ /**
183
+ * Fold one provider measurement into the calibration. A request with no
184
+ * reported input (usage missing or estimated) leaves it unchanged.
185
+ * @param {Calibration} cal
186
+ * @param {{chars:number, images:number, inputTokens:number}} observed
187
+ * @returns {Calibration}
188
+ */
189
+ export function calibrate(cal, { chars, images, inputTokens }) {
190
+ if (!(inputTokens > 0) || !(chars > 0)) return cal;
191
+ const textTokens = inputTokens - images * IMAGE_TOKEN_ESTIMATE;
192
+ const ratio = textTokens > 0 ? Math.min(MAX_RATIO, Math.max(MIN_RATIO, chars / textTokens)) : cal.charsPerToken;
193
+ return { charsPerToken: ratio, anchorChars: chars, anchorImages: images, anchorTokens: inputTokens };
194
+ }
195
+
196
+ /**
197
+ * Projected input tokens for a request of this size.
198
+ * @param {Calibration} cal @param {{chars:number, images:number}} size
199
+ */
200
+ export function projectTokens(cal, { chars, images }) {
201
+ if (cal.anchorTokens !== null && cal.anchorChars !== null && chars >= cal.anchorChars) {
202
+ return cal.anchorTokens + Math.ceil((chars - cal.anchorChars) / cal.charsPerToken) + Math.max(0, images - cal.anchorImages) * IMAGE_TOKEN_ESTIMATE;
203
+ }
204
+ return Math.ceil(chars / cal.charsPerToken) + images * IMAGE_TOKEN_ESTIMATE;
205
+ }
206
+
207
+ /** Characters that fit in `tokens` at the calibrated ratio. @param {Calibration} cal @param {number} tokens */
208
+ export function charsForTokens(cal, tokens) {
209
+ return Math.floor(tokens * cal.charsPerToken);
210
+ }
211
+
212
+ /**
213
+ * @param {{projected:number, contextWindow:number, threshold:number}} p
214
+ * @returns {{over:boolean, limit:number}}
215
+ */
216
+ export function compactionLimit({ projected, contextWindow, threshold }) {
217
+ const limit = Math.floor(contextWindow * threshold);
218
+ return { over: projected >= limit, limit };
219
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * lib/engine/context/cache.mjs — prompt-cache discipline (pure).
3
+ *
4
+ * A supplier caches a request's PREFIX. Every cached token is read at a
5
+ * fraction of the input price, so what the engine controls is that the prefix
6
+ * of turn N+1 is byte-identical to the whole of turn N, and that two runs of
7
+ * the same seat in the same project start from the same bytes:
8
+ *
9
+ * · the system prompt is built once per run; per-run values (date,
10
+ * working directory) sit at its end, and nothing is added to it mid-run —
11
+ * hook context, reminders, lazily found instruction files and compaction
12
+ * summaries are appended as later MESSAGES;
13
+ * · tool definitions are ordered deterministically: built-ins in registry
14
+ * order, then engine tools, then MCP tools sorted by name, and every
15
+ * definition is serialised with its keys sorted (`stableToolDefinitions`),
16
+ * so a server that lists its tools or schema keys in a different order on
17
+ * the next run does not change the bytes;
18
+ * · the tool list only ever GROWS at its tail (deferred tools loaded by
19
+ * ToolSearch are appended in load order), so a load rewrites the cache from
20
+ * the tool list on once, after which the new prefix is stable again.
21
+ *
22
+ * Anthropic breakpoints (`planAnthropicBreakpoints`, at most four): the tail
23
+ * of the tool list, the last system block, the last user message of the
24
+ * PREVIOUS turn (it read or wrote the cache last time, so this turn reads it
25
+ * even when many new blocks follow it), and the last block of the final
26
+ * message (which writes the prefix the next turn will read). Placement is a
27
+ * function of the history's shape only. After compaction the history is
28
+ * shorter, and the same function places the two message breakpoints on the new
29
+ * history — the tool and system breakpoints, and their bytes, are unchanged.
30
+ *
31
+ * OpenAI wire: the supplier caches automatically by prefix. `promptCacheKey`
32
+ * derives the routing key (`prompt_cache_key`) sent when the gateway
33
+ * advertises support: one key per project and tier, so sessions of one seat
34
+ * on one project share cache routing.
35
+ *
36
+ * @module lib/engine/context/cache
37
+ */
38
+
39
+ import { createHash } from "node:crypto";
40
+
41
+ export const MAX_CACHE_BREAKPOINTS = 4;
42
+
43
+ /**
44
+ * @param {{toolCount:number, hasSystem:boolean, messages:Array<{role:string, content:any[]}>}} shape
45
+ * @returns {{tool:number|null, system:boolean, messages:number[]}} message indices, ascending
46
+ */
47
+ export function planAnthropicBreakpoints({ toolCount, hasSystem, messages }) {
48
+ const tool = toolCount > 0 ? toolCount - 1 : null;
49
+ const picks = new Set();
50
+ const last = messages.length - 1;
51
+ if (last >= 0) {
52
+ picks.add(last);
53
+ // The previous turn's final message: the last user message that precedes
54
+ // the last assistant message.
55
+ let a = last;
56
+ while (a >= 0 && messages[a].role !== "assistant") a--;
57
+ for (let i = a - 1; i >= 0; i--) {
58
+ if (messages[i].role === "user") {
59
+ picks.add(i);
60
+ break;
61
+ }
62
+ }
63
+ }
64
+ const budget = MAX_CACHE_BREAKPOINTS - (tool !== null ? 1 : 0) - (hasSystem ? 1 : 0);
65
+ const chosen = [...picks].sort((x, y) => y - x).slice(0, Math.max(0, budget)).sort((x, y) => x - y);
66
+ return { tool, system: hasSystem, messages: chosen };
67
+ }
68
+
69
+ /**
70
+ * The index of the block in a message that may carry `cache_control`: the last
71
+ * block that is not a thinking block (thinking blocks cannot be marked).
72
+ * @param {{content:any[]}} message @returns {number}
73
+ */
74
+ export function markableBlockIndex(message) {
75
+ for (let i = message.content.length - 1; i >= 0; i--) if (message.content[i]?.type !== "thinking") return i;
76
+ return -1;
77
+ }
78
+
79
+ /**
80
+ * A deep copy with object keys sorted, so equal values serialise to equal bytes.
81
+ * @param {unknown} v @returns {any}
82
+ */
83
+ export function canonicalJson(v) {
84
+ if (Array.isArray(v)) return v.map(canonicalJson);
85
+ if (!v || typeof v !== "object") return v;
86
+ /** @type {Record<string, unknown>} */
87
+ const out = {};
88
+ for (const k of Object.keys(v).sort()) out[k] = canonicalJson(/** @type any */ (v)[k]);
89
+ return out;
90
+ }
91
+
92
+ /**
93
+ * Deterministic order: non-MCP tools as given, then MCP tools sorted by name.
94
+ * @template {{name:string, mcp?:unknown}} T
95
+ * @param {T[]} tools @returns {T[]}
96
+ */
97
+ export function stableToolOrder(tools) {
98
+ const plain = tools.filter((t) => !t.mcp);
99
+ const mcp = tools.filter((t) => t.mcp).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
100
+ return [...plain, ...mcp];
101
+ }
102
+
103
+ /**
104
+ * Wire-facing definitions with canonical schemas.
105
+ * @param {Array<{name:string, description:string, input_schema:object}>} tools
106
+ */
107
+ export function stableToolDefinitions(tools) {
108
+ return tools.map((t) => ({ name: t.name, description: t.description, input_schema: canonicalJson(t.input_schema) }));
109
+ }
110
+
111
+ /**
112
+ * The OpenAI-wire prompt-cache routing key for a project and tier.
113
+ * @param {{cwd:string, model:string}} p
114
+ */
115
+ export function promptCacheKey({ cwd, model }) {
116
+ return `cohort-${createHash("sha256").update(`${cwd}\u0000${model}`).digest("hex").slice(0, 32)}`;
117
+ }
118
+
119
+ /**
120
+ * Count `cache_control` markers anywhere in a request body (a test and audit aid).
121
+ * @param {unknown} body
122
+ */
123
+ export function countCacheBreakpoints(body) {
124
+ return (JSON.stringify(body).match(/"cache_control"/g) || []).length;
125
+ }