@cohortapp/agent-sdk 2.16.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 (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  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/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  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/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  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 +29 -2
  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 +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -0,0 +1,327 @@
1
+ /**
2
+ * lib/context/budget.mjs — the declared token budget for an inbound prompt.
3
+ *
4
+ * Every bound in this repo up to now has been a row count or a `.slice(n)` char
5
+ * clamp (design §5.4, R8). Both are proxies: a row count cannot tell a one-word
6
+ * turn from a pasted stack trace, and a char clamp spends the allowance on
7
+ * whichever section happens to be assembled first. So the prompt that reaches
8
+ * the model is whatever the assembly order happened to produce, and the parts
9
+ * that fall off do so SILENTLY — the model reads a thread with a hole in it and
10
+ * cannot tell the hole from a conversation that never happened.
11
+ *
12
+ * This module states the budget instead of discovering it:
13
+ *
14
+ * 1. a budget PER TIER (§5.1) — an Answer gets 6k input tokens, Work 12k,
15
+ * Plan 20k. Context rot is measured: accuracy falls with input length well
16
+ * before the window limit, so a bigger prompt is not a better one.
17
+ * 2. sections filled in PRIORITY order ({@link SECTION_ORDER}) — the trigger
18
+ * is the thing being answered and is never dropped whole; the backlog is
19
+ * the first thing to go.
20
+ * 3. every drop VISIBLE — `… 14 earlier turns not shown`, in the prompt, where
21
+ * the model reads it. A section that did not fit at all still leaves a line
22
+ * saying so.
23
+ *
24
+ * PURE. No clock, no env, no fs, no network: tokens are estimated from
25
+ * character length at {@link CHARS_PER_TOKEN}, the same ratio
26
+ * `context-compiler.mjs` already uses, so the two agree. A caller that has a
27
+ * real tokeniser passes `estimate` and this module uses it instead.
28
+ *
29
+ * Char clamps stay where they are as a second belt — this is the first one.
30
+ *
31
+ * @module lib/context/budget
32
+ */
33
+
34
+ "use strict";
35
+
36
+ /** Characters per token. The estimate `scripts/daemon/context-compiler.mjs` uses. */
37
+ export const CHARS_PER_TOKEN = 4;
38
+
39
+ /**
40
+ * Input-token budget per reply tier (design §5.4). A tier is the ack/effort
41
+ * tier from `lib/assurance/tier.mjs` — answer | work | plan.
42
+ */
43
+ export const TIER_BUDGETS = Object.freeze({
44
+ answer: 6000,
45
+ work: 12000,
46
+ plan: 20000,
47
+ });
48
+
49
+ /**
50
+ * FILL priority. Earlier wins the budget; later is dropped first.
51
+ *
52
+ * trigger the message being answered — without it there is nothing to answer
53
+ * thread the conversation it sits in
54
+ * entity the anchored artifact: the task card, the doc body, the decision
55
+ * digest the distilled record of this thread
56
+ * memory recalled facts from elsewhere
57
+ * backlog what else is open — useful, never load-bearing
58
+ */
59
+ export const FILL_PRIORITY = Object.freeze([
60
+ "trigger",
61
+ "thread",
62
+ "entity",
63
+ "digest",
64
+ "memory",
65
+ "backlog",
66
+ ]);
67
+
68
+ /**
69
+ * RENDER order — a DIFFERENT question, and conflating the two costs the cache.
70
+ *
71
+ * Fill priority asks "who wins the budget"; the trigger wins it, because a
72
+ * prompt without the message being answered is not a smaller prompt but a
73
+ * broken one. Render order asks "what byte comes first", and design §4 is
74
+ * explicit about the consequence: "any byte change before a breakpoint
75
+ * invalidates everything after it". Rendering the trigger first — the ONE
76
+ * section that is different on every single inbound — gives the block a
77
+ * zero-length stable prefix, so WP-4's `cache_control` breakpoint would sit
78
+ * after volatile content and every read would be a silent miss. Prompt caching
79
+ * is the highest-ROI latency lever in the research pass; this is what it needs.
80
+ *
81
+ * So: stable first, volatile last, trigger last of all. `thread` sits second to
82
+ * last because it is append-only and rendered oldest-first, which makes its own
83
+ * prefix stable between turns even as it grows.
84
+ */
85
+ export const RENDER_ORDER = Object.freeze([
86
+ "entity",
87
+ "digest",
88
+ "backlog",
89
+ "memory",
90
+ "thread",
91
+ "trigger",
92
+ ]);
93
+
94
+ /**
95
+ * @deprecated Read {@link FILL_PRIORITY} or {@link RENDER_ORDER} by name — the
96
+ * whole point of the split is that "the section order" is two different orders.
97
+ * Kept as an alias so an existing importer does not silently get the wrong one.
98
+ */
99
+ export const SECTION_ORDER = FILL_PRIORITY;
100
+
101
+ /** Sections that must render SOMETHING rather than be dropped whole. */
102
+ const NEVER_DROPPED_WHOLE = new Set(["trigger"]);
103
+
104
+ /** A blank-safe string. */
105
+ function s(v) {
106
+ return v == null ? "" : String(v);
107
+ }
108
+
109
+ /** The declared budget for a tier; an unknown tier takes the middle one. */
110
+ export function budgetFor(tier) {
111
+ const key = s(tier).toLowerCase();
112
+ return Object.prototype.hasOwnProperty.call(TIER_BUDGETS, key) ? TIER_BUDGETS[key] : TIER_BUDGETS.work;
113
+ }
114
+
115
+ /** Estimated tokens for a string, rounded up so a partial token still costs one. */
116
+ export function estimateTokens(text, charsPerToken = CHARS_PER_TOKEN) {
117
+ const t = s(text);
118
+ if (!t) return 0;
119
+ const per = Number.isFinite(charsPerToken) && charsPerToken > 0 ? charsPerToken : CHARS_PER_TOKEN;
120
+ return Math.ceil(t.length / per);
121
+ }
122
+
123
+ /** Where a section sorts in a declared order; unknown names sort last, stably. */
124
+ function rankIn(order, name) {
125
+ const i = order.indexOf(name);
126
+ return i === -1 ? order.length : i;
127
+ }
128
+
129
+ /** `1 earlier turn` / `14 earlier turns` — the count is the point, so it leads. */
130
+ function turnNote(n) {
131
+ return `… ${n} earlier turn${n === 1 ? "" : "s"} not shown`;
132
+ }
133
+
134
+ /** `… 1,204 characters not shown`. */
135
+ function charNote(n) {
136
+ return `… ${n.toLocaleString("en-US")} character${n === 1 ? "" : "s"} not shown`;
137
+ }
138
+
139
+ /**
140
+ * @typedef {Object} SectionInput
141
+ * @property {string} name one of {@link SECTION_ORDER}, or any name (sorts last)
142
+ * @property {string} [title] heading rendered above the body
143
+ * @property {string} [text] a whole-text section
144
+ * @property {string[]} [turns] a turn-structured section, OLDEST FIRST
145
+ */
146
+
147
+ /**
148
+ * @typedef {Object} FittedSection
149
+ * @property {string} name
150
+ * @property {boolean} included false == dropped whole (still rendered as a note)
151
+ * @property {string} text what was kept, drop note included
152
+ * @property {number} keptTurns
153
+ * @property {number} droppedTurns
154
+ * @property {number} droppedChars
155
+ * @property {number} tokens
156
+ */
157
+
158
+ /**
159
+ * Fit the sections into the tier's budget.
160
+ *
161
+ * Turn-structured sections spend the budget from the NEWEST turn backwards —
162
+ * the turns nearest the message being answered are the ones that explain it
163
+ * (the same rule `lib/org/inbound/hydrate.mjs#renderThread` follows, and for the
164
+ * same reason: clipping the joined string keeps the oldest turns and drops the
165
+ * newest, which is the exact inverse of what context is for).
166
+ *
167
+ * NEVER THROWS. A malformed section is skipped; a missing budget takes the
168
+ * tier's. The prompt path must not be able to fail on its own budgeting.
169
+ *
170
+ * @param {Object} o
171
+ * @param {SectionInput[]} o.sections
172
+ * @param {string} [o.tier] answer | work | plan
173
+ * @param {number} [o.budgetTokens] override the tier's budget
174
+ * @param {number} [o.charsPerToken]
175
+ * @param {(text:string)=>number} [o.estimate] a real tokeniser, when the caller has one
176
+ * @returns {{tier:string, budgetTokens:number, usedTokens:number,
177
+ * sections:FittedSection[], drops:Array<object>, text:string}}
178
+ */
179
+ export function fitSections(o = {}) {
180
+ const tier = s(o.tier).toLowerCase() || "work";
181
+ const budgetTokens = Number.isFinite(o.budgetTokens) && o.budgetTokens > 0
182
+ ? Math.floor(o.budgetTokens)
183
+ : budgetFor(tier);
184
+ const charsPerToken = Number.isFinite(o.charsPerToken) && o.charsPerToken > 0 ? o.charsPerToken : CHARS_PER_TOKEN;
185
+ const tokensOf = typeof o.estimate === "function" ? o.estimate : (t) => estimateTokens(t, charsPerToken);
186
+
187
+ const input = (Array.isArray(o.sections) ? o.sections : [])
188
+ .map((sec, i) => ({ sec, i }))
189
+ .filter(({ sec }) => sec && typeof sec === "object" && s(sec.name))
190
+ .sort((a, b) => rankIn(FILL_PRIORITY, s(a.sec.name)) - rankIn(FILL_PRIORITY, s(b.sec.name)) || a.i - b.i);
191
+
192
+ const out = [];
193
+ const drops = [];
194
+ let used = 0;
195
+
196
+ for (const { sec } of input) {
197
+ const name = s(sec.name);
198
+ const title = s(sec.title);
199
+ const headCost = title ? tokensOf(`${title}\n`) : 0;
200
+ const room = budgetTokens - used - headCost;
201
+
202
+ const fitted = Array.isArray(sec.turns)
203
+ ? fitTurns(sec.turns, room, tokensOf)
204
+ : fitText(s(sec.text), room, tokensOf, charsPerToken);
205
+
206
+ // A section with no content at all is simply absent — there is nothing to
207
+ // report the loss of, and a note about an empty section is noise.
208
+ if (fitted.empty) continue;
209
+
210
+ if (!fitted.body && !NEVER_DROPPED_WHOLE.has(name)) {
211
+ // Dropped whole. It still shows: an absence the reader cannot see is the
212
+ // failure this module exists to prevent.
213
+ const note = `… ${name} not shown`;
214
+ const cost = tokensOf(note);
215
+ if (used + cost <= budgetTokens) {
216
+ out.push({ name, included: false, text: note, keptTurns: 0, droppedTurns: fitted.droppedTurns, droppedChars: fitted.droppedChars, tokens: cost });
217
+ used += cost;
218
+ } else {
219
+ out.push({ name, included: false, text: "", keptTurns: 0, droppedTurns: fitted.droppedTurns, droppedChars: fitted.droppedChars, tokens: 0 });
220
+ }
221
+ drops.push({ section: name, turns: fitted.droppedTurns, chars: fitted.droppedChars, note });
222
+ continue;
223
+ }
224
+
225
+ // The trigger is the thing being answered: it always renders something,
226
+ // even when the budget cannot pay for it. A prompt with no trigger is not a
227
+ // smaller prompt, it is a broken one.
228
+ let body = fitted.body;
229
+ if (!body && NEVER_DROPPED_WHOLE.has(name)) {
230
+ const minChars = Math.max(1, Math.floor(Math.max(room, 1) * charsPerToken));
231
+ const whole = Array.isArray(sec.turns) ? sec.turns.filter(Boolean).map(s).join("\n") : s(sec.text);
232
+ body = whole.slice(0, minChars);
233
+ const lost = whole.length - body.length;
234
+ if (lost > 0) {
235
+ body += `\n${charNote(lost)}`;
236
+ drops.push({ section: name, turns: 0, chars: lost, note: charNote(lost) });
237
+ }
238
+ } else if (fitted.droppedTurns > 0) {
239
+ drops.push({ section: name, turns: fitted.droppedTurns, chars: 0, note: turnNote(fitted.droppedTurns) });
240
+ } else if (fitted.droppedChars > 0) {
241
+ drops.push({ section: name, turns: 0, chars: fitted.droppedChars, note: charNote(fitted.droppedChars) });
242
+ }
243
+
244
+ const text = title ? `${title}\n${body}` : body;
245
+ const cost = tokensOf(text);
246
+ out.push({
247
+ name,
248
+ included: true,
249
+ text,
250
+ keptTurns: fitted.keptTurns,
251
+ droppedTurns: fitted.droppedTurns,
252
+ droppedChars: fitted.droppedChars,
253
+ tokens: cost,
254
+ });
255
+ used += cost;
256
+ }
257
+
258
+ // Filled in priority order, RENDERED in cache order. The two are separate
259
+ // arrays for the reason RENDER_ORDER states, and this is the only place the
260
+ // difference is applied.
261
+ const rendered = out
262
+ .map((sec, i) => ({ sec, i }))
263
+ .sort((a, b) => rankIn(RENDER_ORDER, a.sec.name) - rankIn(RENDER_ORDER, b.sec.name) || a.i - b.i)
264
+ .map(({ sec }) => sec);
265
+
266
+ return {
267
+ tier,
268
+ budgetTokens,
269
+ usedTokens: used,
270
+ sections: rendered,
271
+ drops,
272
+ text: rendered.map((sec) => sec.text).filter(Boolean).join("\n\n"),
273
+ };
274
+ }
275
+
276
+ /** Keep the newest turns that fit; report the rest as a visible note. */
277
+ function fitTurns(turns, room, tokensOf) {
278
+ const rows = (Array.isArray(turns) ? turns : []).filter((t) => s(t).trim()).map((t) => s(t));
279
+ if (rows.length === 0) return { empty: true, body: "", keptTurns: 0, droppedTurns: 0, droppedChars: 0 };
280
+ if (room <= 0) return { empty: false, body: "", keptTurns: 0, droppedTurns: rows.length, droppedChars: 0 };
281
+
282
+ const kept = [];
283
+ let used = 0;
284
+ for (let i = rows.length - 1; i >= 0; i -= 1) {
285
+ const cost = tokensOf(rows[i]) + (kept.length ? 1 : 0);
286
+ // The note itself has to fit, or the drop would be the silent kind.
287
+ const reserve = i > 0 ? tokensOf(turnNote(i)) + 1 : 0;
288
+ if (used + cost + reserve > room) break;
289
+ kept.unshift(rows[i]);
290
+ used += cost;
291
+ }
292
+ const dropped = rows.length - kept.length;
293
+ if (kept.length === 0) return { empty: false, body: "", keptTurns: 0, droppedTurns: dropped, droppedChars: 0 };
294
+ const body = dropped > 0 ? [turnNote(dropped), ...kept].join("\n") : kept.join("\n");
295
+ return { empty: false, body, keptTurns: kept.length, droppedTurns: dropped, droppedChars: 0 };
296
+ }
297
+
298
+ /** Keep the head that fits; report the tail as a visible note. */
299
+ function fitText(text, room, tokensOf, charsPerToken) {
300
+ const t = s(text).trim();
301
+ if (!t) return { empty: true, body: "", keptTurns: 0, droppedTurns: 0, droppedChars: 0 };
302
+ if (room <= 0) return { empty: false, body: "", keptTurns: 0, droppedTurns: 0, droppedChars: t.length };
303
+ if (tokensOf(t) <= room) return { empty: false, body: t, keptTurns: 1, droppedTurns: 0, droppedChars: 0 };
304
+
305
+ // Reserve room for the note, then take the largest head that still fits.
306
+ let chars = Math.max(0, Math.floor(room * charsPerToken));
307
+ let head = t.slice(0, chars);
308
+ let note = charNote(t.length - head.length);
309
+ while (head.length > 0 && tokensOf(`${head}\n${note}`) > room) {
310
+ chars = Math.floor(chars * 0.8);
311
+ head = t.slice(0, chars);
312
+ note = charNote(t.length - head.length);
313
+ }
314
+ if (!head) return { empty: false, body: "", keptTurns: 0, droppedTurns: 0, droppedChars: t.length };
315
+ return { empty: false, body: `${head}\n${note}`, keptTurns: 1, droppedTurns: 0, droppedChars: t.length - head.length };
316
+ }
317
+
318
+ export default {
319
+ TIER_BUDGETS,
320
+ FILL_PRIORITY,
321
+ RENDER_ORDER,
322
+ SECTION_ORDER,
323
+ CHARS_PER_TOKEN,
324
+ budgetFor,
325
+ estimateTokens,
326
+ fitSections,
327
+ };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * lib/context/history-scope.mjs — which interaction logs may be read for THIS
3
+ * reply.
4
+ *
5
+ * THE DEFECT THIS EXISTS TO CLOSE
6
+ *
7
+ * `scripts/daemon/responder.mjs#logInteraction` files every exchange under
8
+ * `memory/interactions/<service>/…`, and it files each one TWICE: once under the
9
+ * room (`<channelId>`) and once under the person (`dm-<sender-slug>`). The two
10
+ * readers — `prompt-builder.mjs` and `context-compiler.mjs` — then merged every
11
+ * candidate directory into one timestamp-sorted block headed "Recent
12
+ * conversation history with this sender".
13
+ *
14
+ * While that read was pinned to `…/slack/…` the merge was inert for every other
15
+ * service. Reading the item's own service (design §3 R12) makes it live, and
16
+ * Cohort is the one service where a person's DMs and the public rooms they post
17
+ * in share a namespace. So: Dana DMs the agent something private, later
18
+ * @-mentions the agent in a public channel, and the public room's prompt
19
+ * carries the DM sentence verbatim. That is design §5.5's resource-scope leak —
20
+ * "per-person facts may inform a reply but are NEVER quoted into a shared room"
21
+ * — arriving through the back door of a directory list.
22
+ *
23
+ * THE RULE, stated once so both readers and the writer cannot drift:
24
+ *
25
+ * A per-person directory is readable only when the reply itself is going to
26
+ * that person privately. In a shared room only the room's own log is read.
27
+ * Unknown is not private: an item that does not positively say it is a DM is
28
+ * treated as a room.
29
+ *
30
+ * PURE. Everything arrives on the argument; nothing is read from disk, the
31
+ * clock or the environment.
32
+ *
33
+ * @module lib/context/history-scope
34
+ */
35
+
36
+ "use strict";
37
+
38
+ /** A blank-safe string. */
39
+ function s(v) {
40
+ return v == null ? "" : String(v);
41
+ }
42
+
43
+ /**
44
+ * Is this inbound item a private 1:1 (or small group DM) conversation?
45
+ *
46
+ * The signals, in the order they are trusted:
47
+ * 1. `is_dm` — set from Cohort's `channel_kind` (`lib/org/inbound/project.mjs`)
48
+ * and from Slack's `channel_type` (`scripts/poller/utils.mjs`). A literal
49
+ * `false` is a positive assertion of "room" and ends the question.
50
+ * 2. `channel_type` — Slack's own envelope field: `im` / `mpim`.
51
+ * 3. A Slack channel id beginning `D` — the convention the raw_ref parser in
52
+ * `prompt-builder.mjs` already relies on.
53
+ * 4. A `dm/<name>` channel label, which is what the Telegram and Slack
54
+ * adapters write for a private chat.
55
+ *
56
+ * Anything else is a room. That direction is deliberate and is the opposite of
57
+ * this module's neighbours: everywhere else in hydration an unknown degrades
58
+ * towards MORE context, because the cost is a thinner prompt. Here the cost of
59
+ * guessing wrong is one person's private sentence in a public channel, so
60
+ * unknown buys nothing.
61
+ *
62
+ * @param {object} item inbound item (`{service, is_dm, channel_type, channel_id, channel}`)
63
+ * @returns {boolean}
64
+ */
65
+ export function isPrivateConversation(item) {
66
+ if (!item || typeof item !== "object") return false;
67
+ if (item.is_dm === true) return true;
68
+ if (item.is_dm === false) return false;
69
+
70
+ const type = s(item.channel_type).toLowerCase();
71
+ if (type === "im" || type === "mpim") return true;
72
+ if (type === "channel" || type === "group") return false;
73
+
74
+ // Slack's own id convention. `service` defaults to slack here for the same
75
+ // reason both readers write `String(item.service || "slack")` — an item that
76
+ // predates the per-service split carries no service field, and a `D…` id on
77
+ // such an item is a Slack DM.
78
+ const service = s(item.service) || "slack";
79
+ const channelId = s(item.channel_id || item.channelId);
80
+ if (service === "slack" && /^D[A-Z0-9]+$/.test(channelId)) return true;
81
+
82
+ if (/^dm\//i.test(s(item.channel))) return true;
83
+
84
+ return false;
85
+ }
86
+
87
+ /**
88
+ * The interaction-log directory NAMES this reply may read, relative to
89
+ * `memory/interactions/<service>/`. Callers join them onto the service root.
90
+ *
91
+ * Ordered room-first so a merged read still prefers the room's own record.
92
+ * Slack keeps its legacy bare `<sender-slug>` directory alongside the `dm-`
93
+ * one — historical logs live there and must not go dark — and it is subject to
94
+ * exactly the same gate.
95
+ *
96
+ * @param {object} o
97
+ * @param {string|null} [o.channelId] the room/DM channel id, when known
98
+ * @param {string|null} [o.senderSlug] the sender, slugified
99
+ * @param {boolean} [o.isPrivate] is the reply going to that person privately
100
+ * @param {string} [o.service]
101
+ * @returns {string[]} directory names, possibly empty
102
+ */
103
+ export function historyDirNames({ channelId, senderSlug, isPrivate, service } = {}) {
104
+ const dirs = [];
105
+ const cid = s(channelId);
106
+ const slug = s(senderSlug);
107
+ if (cid) dirs.push(cid);
108
+ if (isPrivate === true && slug) {
109
+ dirs.push(`dm-${slug}`);
110
+ if ((s(service) || "slack") === "slack") dirs.push(slug);
111
+ }
112
+ return dirs;
113
+ }
114
+
115
+ /**
116
+ * The directories `logInteraction` may WRITE this exchange to.
117
+ *
118
+ * The read gate above is the one that stops the leak, but leaving the writer
119
+ * unconditional keeps every per-person directory permanently contaminated with
120
+ * public-room content — so a future reader, or an operator grepping the tree,
121
+ * meets the same defect again. One rule, both ends.
122
+ *
123
+ * @param {object} o
124
+ * @param {string|null} [o.channelId]
125
+ * @param {string|null} [o.senderSlug]
126
+ * @param {boolean} [o.isPrivate]
127
+ * @returns {string[]}
128
+ */
129
+ export function interactionWriteDirNames({ channelId, senderSlug, isPrivate } = {}) {
130
+ const dirs = [];
131
+ const cid = s(channelId);
132
+ const slug = s(senderSlug);
133
+ if (cid) dirs.push(cid);
134
+ if (isPrivate === true && slug) dirs.push(`dm-${slug}`);
135
+ return dirs;
136
+ }
137
+
138
+ export default { isPrivateConversation, historyDirNames, interactionWriteDirNames };
@@ -91,6 +91,16 @@ export const DEFAULT_THRESHOLDS = Object.freeze({
91
91
  warnSec: 120, // 2 min (heartbeat refreshes ~15s)
92
92
  critSec: 300, // 5 min
93
93
  }),
94
+ cadenceOutputMissing: Object.freeze({
95
+ // A scheduled workflow finished without leaving its output artefacts behind.
96
+ // This is the ONLY rule here not derived from the cadence bus, and that is
97
+ // the point: the evening wrap missed 5 of 9 days in Aug/Sep 2026 and the bus
98
+ // logged one of those misses as stage="processed". A rule reading the bus's
99
+ // own decision is green during that outage. See
100
+ // lib/diagnostics/cadence-output-freshness.mjs.
101
+ warnCount: 1, // one missing artefact is already a real miss
102
+ critCount: 2, // both artefacts gone = the workflow did not run at all
103
+ }),
94
104
  });
95
105
 
96
106
  const SEVERITY_RANK = { critical: 0, warning: 1 };
@@ -243,6 +253,9 @@ function round2(n) {
243
253
  * optionally with `perSessionUSD: number[]`
244
254
  * @param {object} [args.presence] `{ heartbeatAgeSec?, daemonHealthAgeSec?,
245
255
  * daemonDown? }`
256
+ * @param {object} [args.outputs] cadence output verdict from
257
+ * lib/diagnostics/cadence-output-freshness.mjs
258
+ * `{ ok, days, checked, missing[] }`
246
259
  * @param {object} [args.thresholds] merged thresholds (default DEFAULT_THRESHOLDS)
247
260
  * @param {number} [args.now] clock in epoch ms (reserved; rules are
248
261
  * age-based via injected `*AgeSec`)
@@ -254,6 +267,7 @@ export function evaluateAlerts(args = {}) {
254
267
  const snapshot = args.snapshot && typeof args.snapshot === "object" ? args.snapshot : {};
255
268
  const ledger = args.ledger && typeof args.ledger === "object" ? args.ledger : {};
256
269
  const presence = args.presence && typeof args.presence === "object" ? args.presence : {};
270
+ const outputs = args.outputs && typeof args.outputs === "object" ? args.outputs : {};
257
271
  const t = mergeThresholds(DEFAULT_THRESHOLDS, args.thresholds || {});
258
272
 
259
273
  push(alerts, refusalSpikeRule(snapshot, t.refusalSpike));
@@ -261,6 +275,7 @@ export function evaluateAlerts(args = {}) {
261
275
  push(alerts, costPerSessionRule(ledger, t.costPerSessionP99));
262
276
  push(alerts, absentAgentRule(presence, t.absentAgent));
263
277
  push(alerts, daemonDownRule(presence, t.daemonDown));
278
+ push(alerts, cadenceOutputMissingRule(outputs, t.cadenceOutputMissing));
264
279
 
265
280
  // Deterministic order: critical first, then by id, so identical inputs always
266
281
  // produce identical output (a de-dupe upstream can key on id).
@@ -365,6 +380,40 @@ function daemonDownRule(presence, cfg = {}) {
365
380
  };
366
381
  }
367
382
 
383
+ /**
384
+ * A scheduled workflow completed without producing its output artefacts.
385
+ *
386
+ * Unlike every other rule here, this one is NOT derived from the lane it judges.
387
+ * It reads files on disk that the workflow's own steps write, so it stays true
388
+ * when the cadence bus is wrong about itself — which is the failure this exists
389
+ * to catch (a timed-out evening wrap recorded as stage="processed", 29 Aug 2026).
390
+ *
391
+ * `checked === 0` means nothing was in scope (e.g. every candidate day predates
392
+ * its cadence). That is silence, not health, so it does not fire.
393
+ */
394
+ function cadenceOutputMissingRule(outputs, cfg = {}) {
395
+ const missing = Array.isArray(outputs.missing) ? outputs.missing : null;
396
+ if (!missing || !num(outputs.checked)) return null; // nothing in scope
397
+ const count = missing.length;
398
+ const sev = severityFor(count, num(cfg.warnCount, Infinity), num(cfg.critCount, Infinity));
399
+ if (!sev) return null;
400
+
401
+ // Name the days and the workflow — an alert saying only "2 artefacts missing"
402
+ // sends the reader back to the bus log, which is exactly where the answer is not.
403
+ const days = [...new Set(missing.map((m) => m.day))].sort();
404
+ const labels = [...new Set(missing.map((m) => m.label || m.cadence))].sort();
405
+ const detail = missing
406
+ .slice(0, 4)
407
+ .map((m) => `${m.path} (${m.reason})`)
408
+ .join("; ");
409
+ const more = count > 4 ? ` +${count - 4} more` : "";
410
+ return {
411
+ id: "cadence_output_missing",
412
+ severity: sev,
413
+ detail: `${labels.join(", ")} produced no usable output for ${days.join(", ")}: ${detail}${more}. Checked on disk, independently of the cadence bus — the bus may still report this run as processed.`,
414
+ };
415
+ }
416
+
368
417
  // ---------------------------------------------------------------------------
369
418
  // emitAlerts
370
419
  // ---------------------------------------------------------------------------