@cohortapp/agent-sdk 2.17.0 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (526) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture.mjs +8 -6
  23. package/lib/collective/global-config.mjs +63 -1
  24. package/lib/collective/presence.mjs +142 -5
  25. package/lib/comms/send-gate.mjs +559 -1
  26. package/lib/diagnostics/alerts.mjs +49 -0
  27. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  28. package/lib/engine/agents/definitions.mjs +343 -0
  29. package/lib/engine/agents/persist.mjs +275 -0
  30. package/lib/engine/agents/runtime.mjs +748 -0
  31. package/lib/engine/agents/usage.mjs +95 -0
  32. package/lib/engine/auth-status.mjs +139 -0
  33. package/lib/engine/budget.mjs +194 -0
  34. package/lib/engine/cli.mjs +1204 -0
  35. package/lib/engine/commands/index.mjs +269 -0
  36. package/lib/engine/context/budget.mjs +219 -0
  37. package/lib/engine/context/cache.mjs +125 -0
  38. package/lib/engine/context/child-env.mjs +215 -0
  39. package/lib/engine/context/compaction.mjs +342 -0
  40. package/lib/engine/context/images.mjs +90 -0
  41. package/lib/engine/context/instructions.mjs +327 -0
  42. package/lib/engine/context/lazy-instructions.mjs +169 -0
  43. package/lib/engine/context/manager.mjs +182 -0
  44. package/lib/engine/context/real-path.mjs +91 -0
  45. package/lib/engine/context/secret-values.mjs +163 -0
  46. package/lib/engine/context/settings.mjs +274 -0
  47. package/lib/engine/context/stream-input.mjs +159 -0
  48. package/lib/engine/guard.mjs +152 -0
  49. package/lib/engine/hooks.mjs +713 -0
  50. package/lib/engine/loop.mjs +560 -0
  51. package/lib/engine/mcp/client.mjs +254 -0
  52. package/lib/engine/mcp/config.mjs +301 -0
  53. package/lib/engine/mcp/http.mjs +201 -0
  54. package/lib/engine/mcp/index.mjs +146 -0
  55. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  56. package/lib/engine/mcp/naming.mjs +66 -0
  57. package/lib/engine/mcp/resources.mjs +89 -0
  58. package/lib/engine/mcp/results.mjs +133 -0
  59. package/lib/engine/mcp/stdio.mjs +137 -0
  60. package/lib/engine/mcp/supervisor.mjs +116 -0
  61. package/lib/engine/messages.mjs +104 -0
  62. package/lib/engine/output/json.mjs +164 -0
  63. package/lib/engine/output/stream-json.mjs +266 -0
  64. package/lib/engine/permissions.mjs +845 -0
  65. package/lib/engine/process-identity.mjs +164 -0
  66. package/lib/engine/process-tree.mjs +551 -0
  67. package/lib/engine/prompt.mjs +60 -0
  68. package/lib/engine/session/store.mjs +299 -0
  69. package/lib/engine/session-runtime/args.mjs +97 -0
  70. package/lib/engine/session-runtime/host.mjs +143 -0
  71. package/lib/engine/session-runtime/inbox.mjs +122 -0
  72. package/lib/engine/session-runtime/notifications.mjs +129 -0
  73. package/lib/engine/session-runtime/registry.mjs +328 -0
  74. package/lib/engine/session-runtime/runner.mjs +344 -0
  75. package/lib/engine/session-runtime/socket.mjs +212 -0
  76. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  77. package/lib/engine/skills/index.mjs +321 -0
  78. package/lib/engine/tools/bash-background.mjs +533 -0
  79. package/lib/engine/tools/bash.mjs +216 -0
  80. package/lib/engine/tools/edit.mjs +97 -0
  81. package/lib/engine/tools/glob.mjs +81 -0
  82. package/lib/engine/tools/grep.mjs +224 -0
  83. package/lib/engine/tools/index.mjs +84 -0
  84. package/lib/engine/tools/list-agents.mjs +32 -0
  85. package/lib/engine/tools/ls.mjs +127 -0
  86. package/lib/engine/tools/monitor.mjs +82 -0
  87. package/lib/engine/tools/notebook-edit.mjs +218 -0
  88. package/lib/engine/tools/read.mjs +103 -0
  89. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  90. package/lib/engine/tools/schema.mjs +144 -0
  91. package/lib/engine/tools/send-message.mjs +77 -0
  92. package/lib/engine/tools/session.mjs +70 -0
  93. package/lib/engine/tools/todo.mjs +144 -0
  94. package/lib/engine/tools/toolsearch.mjs +217 -0
  95. package/lib/engine/tools/walk.mjs +193 -0
  96. package/lib/engine/tools/web-switch.mjs +31 -0
  97. package/lib/engine/tools/webfetch-html.mjs +387 -0
  98. package/lib/engine/tools/webfetch-net.mjs +340 -0
  99. package/lib/engine/tools/webfetch.mjs +198 -0
  100. package/lib/engine/tools/websearch.mjs +91 -0
  101. package/lib/engine/tools/workflow.mjs +95 -0
  102. package/lib/engine/tools/write.mjs +76 -0
  103. package/lib/engine/tui/line-editor.mjs +137 -0
  104. package/lib/engine/tui/render.mjs +86 -0
  105. package/lib/engine/tui/tui.mjs +274 -0
  106. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  107. package/lib/engine/wire/effort.mjs +36 -0
  108. package/lib/engine/wire/errors.mjs +496 -0
  109. package/lib/engine/wire/http.mjs +441 -0
  110. package/lib/engine/wire/index.mjs +76 -0
  111. package/lib/engine/wire/openai-chat.mjs +332 -0
  112. package/lib/engine/wire/prompt-cache.mjs +79 -0
  113. package/lib/engine/wire/search.mjs +140 -0
  114. package/lib/engine/wire/sse.mjs +114 -0
  115. package/lib/engine/wire/stall.mjs +349 -0
  116. package/lib/engine/wire/token-provider.mjs +175 -0
  117. package/lib/engine/wire/usage.mjs +192 -0
  118. package/lib/engine/workflow/host.mjs +524 -0
  119. package/lib/engine/workflow/journal.mjs +188 -0
  120. package/lib/engine/workflow/json-schema.mjs +171 -0
  121. package/lib/engine/workflow/meta.mjs +329 -0
  122. package/lib/engine/workflow/notifications.mjs +52 -0
  123. package/lib/engine/workflow/runtime.mjs +447 -0
  124. package/lib/engine/workflow/sandbox.mjs +534 -0
  125. package/lib/engine/workflow/worker.mjs +141 -0
  126. package/lib/engine/workflow/worktree.mjs +74 -0
  127. package/lib/execution/disposition.mjs +1 -1
  128. package/lib/execution/intake.mjs +10 -0
  129. package/lib/execution/surface-policy.mjs +15 -0
  130. package/lib/learning/curator.mjs +8 -6
  131. package/lib/learning/reflect.mjs +8 -6
  132. package/lib/model-router/catalog/cohort.yaml +137 -0
  133. package/lib/model-router/catalog.mjs +118 -1
  134. package/lib/model-router/failover.mjs +67 -16
  135. package/lib/model-router/llm-task.mjs +39 -3
  136. package/lib/model-router/resolve.mjs +89 -3
  137. package/lib/model-router/spawn.mjs +46 -47
  138. package/lib/model-router/taxonomy.mjs +126 -4
  139. package/lib/org/cost-sync.mjs +141 -11
  140. package/lib/org/inbound/broadcast.mjs +289 -0
  141. package/lib/org/inbound/collective.mjs +375 -0
  142. package/lib/org/inbound/directedness.mjs +96 -8
  143. package/lib/org/inbound/facts.mjs +78 -2
  144. package/lib/org/inbound/project.mjs +22 -0
  145. package/lib/org/inbound/surfaces.mjs +14 -0
  146. package/lib/org/llm-token.mjs +879 -0
  147. package/lib/org/mesh.mjs +61 -0
  148. package/lib/org/messaging.mjs +3 -1
  149. package/lib/org/protocol.checksum +1 -1
  150. package/lib/org/protocol.mjs +15 -0
  151. package/lib/org/quota.mjs +520 -0
  152. package/lib/org/tool-surface.mjs +104 -16
  153. package/lib/org/ui-parity.mjs +16 -1
  154. package/lib/org/work-ledger.mjs +37 -6
  155. package/lib/rate-guard.mjs +114 -1
  156. package/lib/resource-governor.mjs +41 -6
  157. package/lib/runtime/adapter.mjs +823 -0
  158. package/lib/runtime/child-env.mjs +191 -0
  159. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  160. package/lib/runtime/seat-engine.mjs +162 -0
  161. package/lib/session/ask-ledger.mjs +271 -0
  162. package/lib/session/current-work.mjs +676 -0
  163. package/lib/session/feed-core.mjs +40 -3
  164. package/lib/session/launch-args.mjs +56 -4
  165. package/lib/session/status-summary.mjs +26 -9
  166. package/lib/session/upgrade-notice.mjs +42 -0
  167. package/lib/setup/claude-probe.mjs +117 -13
  168. package/lib/setup/enrich.mjs +13 -10
  169. package/lib/setup/sections/model.mjs +39 -13
  170. package/lib/telemetry/collect.mjs +208 -9
  171. package/lib/upgrade/ignored-drift.mjs +105 -0
  172. package/lib/voice/post-call-brief.mjs +30 -17
  173. package/package.json +13 -3
  174. package/plugins/maestro-skills/skills/board-work.md +5 -0
  175. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  176. package/plugins/maestro-skills/skills/main-session.md +18 -7
  177. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  178. package/scripts/ci/run-tests.mjs +47 -19
  179. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  180. package/scripts/collective/hook-runner.mjs +29 -2
  181. package/scripts/continuous-monitor.sh +13 -0
  182. package/scripts/cost/track-claude-usage.mjs +15 -0
  183. package/scripts/daemon/agent-daemon.mjs +408 -20
  184. package/scripts/daemon/assurance.mjs +48 -12
  185. package/scripts/daemon/cadence-consumer.mjs +218 -68
  186. package/scripts/daemon/cadence-handlers.mjs +73 -4
  187. package/scripts/daemon/classifier.mjs +75 -26
  188. package/scripts/daemon/context-compiler.mjs +51 -37
  189. package/scripts/daemon/deliver.mjs +30 -1
  190. package/scripts/daemon/dispatcher.mjs +595 -149
  191. package/scripts/daemon/health.mjs +14 -1
  192. package/scripts/daemon/maestro-daemon.mjs +11 -0
  193. package/scripts/daemon/prompt-builder.mjs +24 -0
  194. package/scripts/daemon/responder.mjs +246 -79
  195. package/scripts/daemon/sdk-version.mjs +98 -16
  196. package/scripts/eval/probe-gateway.mjs +635 -0
  197. package/scripts/eval/replay/extract.mjs +270 -0
  198. package/scripts/eval/replay/grade.mjs +260 -0
  199. package/scripts/eval/replay/lib/config.mjs +50 -0
  200. package/scripts/eval/replay/lib/effects.mjs +65 -0
  201. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  202. package/scripts/eval/replay/lib/judge.mjs +72 -0
  203. package/scripts/eval/replay/lib/redact.mjs +136 -0
  204. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  205. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  206. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  207. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  208. package/scripts/eval/replay/report.mjs +185 -0
  209. package/scripts/eval/replay/run.mjs +404 -0
  210. package/scripts/fleet/rollout.mjs +1094 -0
  211. package/scripts/hooks/pre-send-audit.sh +36 -245
  212. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  213. package/scripts/hooks/validate-state-yaml.sh +190 -0
  214. package/scripts/huddle/huddle-llm.mjs +361 -0
  215. package/scripts/huddle/huddle-server.mjs +46 -121
  216. package/scripts/local-triggers/autoupdate.sh +448 -78
  217. package/scripts/local-triggers/run-trigger.sh +13 -0
  218. package/scripts/maintenance/pin-integrity.mjs +364 -0
  219. package/scripts/poll-slack-events.sh +41 -9
  220. package/scripts/poller/slack-socket-mode.mjs +28 -3
  221. package/scripts/session/supervisor.mjs +80 -13
  222. package/scripts/spawn-session.sh +13 -0
  223. package/bin/maestro.test.mjs +0 -1574
  224. package/lib/action-executor.test.mjs +0 -871
  225. package/lib/archetype.test.mjs +0 -132
  226. package/lib/assurance/plan-note.test.mjs +0 -234
  227. package/lib/assurance/room-budget.test.mjs +0 -486
  228. package/lib/assurance/tier.test.mjs +0 -174
  229. package/lib/autonomy.test.mjs +0 -66
  230. package/lib/backlog.test.mjs +0 -302
  231. package/lib/backup/policy.test.mjs +0 -305
  232. package/lib/budget-escalate.test.mjs +0 -232
  233. package/lib/budget-guard.envelope.test.mjs +0 -476
  234. package/lib/budget-guard.test.mjs +0 -427
  235. package/lib/cadence-bus-requeue.test.mjs +0 -83
  236. package/lib/cadence-bus-schedule.test.mjs +0 -194
  237. package/lib/cadence-bus.test.mjs +0 -720
  238. package/lib/cadences.test.mjs +0 -230
  239. package/lib/capability/inventory.test.mjs +0 -232
  240. package/lib/capability.test.mjs +0 -78
  241. package/lib/channels/base-adapter.test.mjs +0 -590
  242. package/lib/channels/channels.test.mjs +0 -371
  243. package/lib/channels/contract.test.mjs +0 -162
  244. package/lib/channels/inbox-item.test.mjs +0 -368
  245. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  246. package/lib/channels/pairing.test.mjs +0 -270
  247. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  248. package/lib/channels/slack-adapter.test.mjs +0 -212
  249. package/lib/channels/telegram-adapter.test.mjs +0 -306
  250. package/lib/channels/voice/adapter.test.mjs +0 -278
  251. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  252. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  253. package/lib/charter.test.mjs +0 -89
  254. package/lib/claude-bin.test.mjs +0 -131
  255. package/lib/cli/board.test.mjs +0 -227
  256. package/lib/cli/design.test.mjs +0 -270
  257. package/lib/cli/doctor-checks.test.mjs +0 -336
  258. package/lib/cli/global-setup-extras.test.mjs +0 -462
  259. package/lib/cli/inbox.test.mjs +0 -230
  260. package/lib/cli/session-ack.test.mjs +0 -63
  261. package/lib/cli/session.test.mjs +0 -613
  262. package/lib/collective/capture.test.mjs +0 -121
  263. package/lib/collective/cards.test.mjs +0 -114
  264. package/lib/collective/config.test.mjs +0 -123
  265. package/lib/collective/global-config.test.mjs +0 -220
  266. package/lib/collective/global-skills.test.mjs +0 -126
  267. package/lib/collective/presence.test.mjs +0 -95
  268. package/lib/collective/recall.test.mjs +0 -116
  269. package/lib/collective/vendor-skills.test.mjs +0 -306
  270. package/lib/comms/send-gate.test.mjs +0 -770
  271. package/lib/comms.test.mjs +0 -41
  272. package/lib/context/budget.test.mjs +0 -252
  273. package/lib/context/history-scope.test.mjs +0 -79
  274. package/lib/cost/ledger-row.test.mjs +0 -183
  275. package/lib/design/design-md.test.mjs +0 -318
  276. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  277. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  278. package/lib/design/fixtures/foundation.json +0 -133
  279. package/lib/design/refresh-gate.test.mjs +0 -144
  280. package/lib/design/write.test.mjs +0 -241
  281. package/lib/diagnostics/alerts.test.mjs +0 -318
  282. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  283. package/lib/diagnostics/counters.test.mjs +0 -206
  284. package/lib/diagnostics/events.test.mjs +0 -290
  285. package/lib/diagnostics/otel.test.mjs +0 -196
  286. package/lib/diagnostics/trace.test.mjs +0 -251
  287. package/lib/env-compat.test.mjs +0 -104
  288. package/lib/execution/disposition.test.mjs +0 -553
  289. package/lib/execution/drive.test.mjs +0 -270
  290. package/lib/execution/effects.test.mjs +0 -344
  291. package/lib/execution/intake.test.mjs +0 -389
  292. package/lib/execution/journal.test.mjs +0 -261
  293. package/lib/execution/match.test.mjs +0 -235
  294. package/lib/execution/pipeline.test.mjs +0 -392
  295. package/lib/execution/route.test.mjs +0 -186
  296. package/lib/execution/surface-policy.test.mjs +0 -162
  297. package/lib/fs-atomic.test.mjs +0 -72
  298. package/lib/fs-ownership.test.mjs +0 -158
  299. package/lib/goals/admission.test.mjs +0 -164
  300. package/lib/goals/classify.test.mjs +0 -167
  301. package/lib/goals/collaborate.test.mjs +0 -336
  302. package/lib/goals/gaps.test.mjs +0 -284
  303. package/lib/goals/loop.test.mjs +0 -845
  304. package/lib/hooks/bus.test.mjs +0 -387
  305. package/lib/identity/persona.test.mjs +0 -142
  306. package/lib/kpi-sensors.test.mjs +0 -278
  307. package/lib/kpi.test.mjs +0 -244
  308. package/lib/learning/config.test.mjs +0 -75
  309. package/lib/learning/counters.test.mjs +0 -69
  310. package/lib/learning/curator-consolidate.test.mjs +0 -238
  311. package/lib/learning/curator.test.mjs +0 -106
  312. package/lib/learning/reflect.test.mjs +0 -0
  313. package/lib/learning/session-index.test.mjs +0 -125
  314. package/lib/learning/skill-writer.test.mjs +0 -210
  315. package/lib/mandate/audit.test.mjs +0 -195
  316. package/lib/mandate/contract.test.mjs +0 -185
  317. package/lib/mandate/derive.test.mjs +0 -274
  318. package/lib/mandate/model.test.mjs +0 -164
  319. package/lib/mandate/refresh.test.mjs +0 -389
  320. package/lib/mcp/server.test.mjs +0 -426
  321. package/lib/model-router/auth-profiles.test.mjs +0 -580
  322. package/lib/model-router/catalog.test.mjs +0 -385
  323. package/lib/model-router/economics.test.mjs +0 -438
  324. package/lib/model-router/failover.test.mjs +0 -439
  325. package/lib/model-router/health.test.mjs +0 -338
  326. package/lib/model-router/integration-coverage.test.mjs +0 -831
  327. package/lib/model-router/integration.test.mjs +0 -564
  328. package/lib/model-router/ledger.test.mjs +0 -415
  329. package/lib/model-router/llm-task.test.mjs +0 -392
  330. package/lib/model-router/org-credentials.test.mjs +0 -265
  331. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  332. package/lib/model-router/reconcile.test.mjs +0 -316
  333. package/lib/model-router/repair.test.mjs +0 -180
  334. package/lib/model-router/spawn.test.mjs +0 -446
  335. package/lib/model-router/taxonomy.test.mjs +0 -410
  336. package/lib/model-router.test.mjs +0 -1207
  337. package/lib/org/activity.test.mjs +0 -134
  338. package/lib/org/approvals.test.mjs +0 -216
  339. package/lib/org/awareness.test.mjs +0 -159
  340. package/lib/org/board-mine-cache.test.mjs +0 -53
  341. package/lib/org/board.test.mjs +0 -187
  342. package/lib/org/bootstrap-context.test.mjs +0 -153
  343. package/lib/org/client.test.mjs +0 -1206
  344. package/lib/org/cohort-client.test.mjs +0 -126
  345. package/lib/org/cost-sync.test.mjs +0 -153
  346. package/lib/org/doctor.test.mjs +0 -346
  347. package/lib/org/engagement-ledger.test.mjs +0 -112
  348. package/lib/org/engagement.test.mjs +0 -739
  349. package/lib/org/handoff.test.mjs +0 -269
  350. package/lib/org/inbound/directedness.test.mjs +0 -668
  351. package/lib/org/inbound/facts.test.mjs +0 -471
  352. package/lib/org/inbound/hydrate.test.mjs +0 -908
  353. package/lib/org/inbound/index.test.mjs +0 -429
  354. package/lib/org/inbound/project.test.mjs +0 -287
  355. package/lib/org/integration-tools.test.mjs +0 -160
  356. package/lib/org/keys.test.mjs +0 -92
  357. package/lib/org/knowledge.test.mjs +0 -326
  358. package/lib/org/leases.test.mjs +0 -235
  359. package/lib/org/mesh-directives.test.mjs +0 -110
  360. package/lib/org/mesh-integration.test.mjs +0 -127
  361. package/lib/org/mesh.test.mjs +0 -400
  362. package/lib/org/messaging.test.mjs +0 -471
  363. package/lib/org/param-contract.test.mjs +0 -477
  364. package/lib/org/policy.test.mjs +0 -237
  365. package/lib/org/protocol.checksum.test.mjs +0 -90
  366. package/lib/org/protocol.test.mjs +0 -323
  367. package/lib/org/push.test.mjs +0 -792
  368. package/lib/org/registry.test.mjs +0 -100
  369. package/lib/org/resource-tools.test.mjs +0 -361
  370. package/lib/org/tool-access.test.mjs +0 -144
  371. package/lib/org/tool-surface-integration.test.mjs +0 -120
  372. package/lib/org/tool-surface.test.mjs +0 -1268
  373. package/lib/org/typing.test.mjs +0 -291
  374. package/lib/org/ui-parity.test.mjs +0 -560
  375. package/lib/org/verify.test.mjs +0 -194
  376. package/lib/org/work-ledger.test.mjs +0 -273
  377. package/lib/plan/adoption-e2e.test.mjs +0 -366
  378. package/lib/plan/budget-enforcement.test.mjs +0 -400
  379. package/lib/plan/compile.test.mjs +0 -382
  380. package/lib/plan/emit.test.mjs +0 -269
  381. package/lib/plan/explain.test.mjs +0 -188
  382. package/lib/prompts/parallelism.test.mjs +0 -177
  383. package/lib/rag/rag.test.mjs +0 -505
  384. package/lib/rate-guard.test.mjs +0 -272
  385. package/lib/reactive-gate.test.mjs +0 -57
  386. package/lib/render.test.mjs +0 -68
  387. package/lib/resource-governor.test.mjs +0 -488
  388. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  389. package/lib/scheduling/jitter.test.mjs +0 -140
  390. package/lib/secrets/broker.test.mjs +0 -280
  391. package/lib/secrets/providers.test.mjs +0 -274
  392. package/lib/security/audit-engine.test.mjs +0 -424
  393. package/lib/security/coerce-args.test.mjs +0 -281
  394. package/lib/security/dangerous-tools.test.mjs +0 -68
  395. package/lib/security/external-content.test.mjs +0 -84
  396. package/lib/security/redact.test.mjs +0 -441
  397. package/lib/security/secret-equal.test.mjs +0 -55
  398. package/lib/session/config.test.mjs +0 -92
  399. package/lib/session/feed-core.test.mjs +0 -198
  400. package/lib/session/first-run.test.mjs +0 -121
  401. package/lib/session/frontdoor.test.mjs +0 -205
  402. package/lib/session/handoffs.test.mjs +0 -183
  403. package/lib/session/identity.test.mjs +0 -180
  404. package/lib/session/inbox-claims.test.mjs +0 -286
  405. package/lib/session/launch-args.test.mjs +0 -157
  406. package/lib/session/liveness.test.mjs +0 -100
  407. package/lib/session/status-summary.test.mjs +0 -118
  408. package/lib/session-permissions.test.mjs +0 -120
  409. package/lib/setup/claude-probe.test.mjs +0 -187
  410. package/lib/setup/completeness.test.mjs +0 -110
  411. package/lib/setup/context-pack.test.mjs +0 -89
  412. package/lib/setup/enrich.test.mjs +0 -115
  413. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  414. package/lib/setup/integration.test.mjs +0 -162
  415. package/lib/setup/io.test.mjs +0 -77
  416. package/lib/setup/runner.test.mjs +0 -132
  417. package/lib/setup/sections/identity.test.mjs +0 -234
  418. package/lib/setup/sections/inventory.test.mjs +0 -198
  419. package/lib/setup/sections/learning.test.mjs +0 -81
  420. package/lib/setup/sections/mandate.test.mjs +0 -388
  421. package/lib/setup/sections/messaging.test.mjs +0 -127
  422. package/lib/setup/sections/model.test.mjs +0 -240
  423. package/lib/setup/sections/org.test.mjs +0 -346
  424. package/lib/setup/sections/orgmail.test.mjs +0 -118
  425. package/lib/setup/sections/recovery.test.mjs +0 -98
  426. package/lib/setup/sections/subagents.test.mjs +0 -429
  427. package/lib/setup/sections/verify.test.mjs +0 -175
  428. package/lib/setup/sot.test.mjs +0 -81
  429. package/lib/setup/state.test.mjs +0 -115
  430. package/lib/singleton.test.mjs +0 -151
  431. package/lib/subagents/cli.test.mjs +0 -389
  432. package/lib/subagents/client.test.mjs +0 -309
  433. package/lib/subagents/gap.test.mjs +0 -234
  434. package/lib/subagents/lock.test.mjs +0 -248
  435. package/lib/subagents/manifest.test.mjs +0 -175
  436. package/lib/subagents/refs.test.mjs +0 -204
  437. package/lib/subagents/resolve.test.mjs +0 -422
  438. package/lib/subagents/schema.test.mjs +0 -328
  439. package/lib/telemetry/alerts.test.mjs +0 -109
  440. package/lib/telemetry/collect.test.mjs +0 -1274
  441. package/lib/tool-definitions-integration.test.mjs +0 -83
  442. package/lib/tool-definitions.test.mjs +0 -437
  443. package/lib/upgrade/global-refresh.test.mjs +0 -65
  444. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  445. package/lib/upgrade/post-steps.test.mjs +0 -200
  446. package/lib/upgrade/verify.test.mjs +0 -164
  447. package/lib/util/fetch-timeout.test.mjs +0 -202
  448. package/lib/util/reconnect.test.mjs +0 -369
  449. package/lib/util/unhandled.test.mjs +0 -216
  450. package/lib/voice/outbound.test.mjs +0 -69
  451. package/lib/voice/session-rotation.test.mjs +0 -114
  452. package/lib/voice/stt.test.mjs +0 -226
  453. package/lib/voice/voice.test.mjs +0 -990
  454. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  455. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  456. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  457. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  458. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  459. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  460. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  461. package/scripts/ci/check.test.mjs +0 -194
  462. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  463. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  464. package/scripts/collective/hook-runner.test.mjs +0 -173
  465. package/scripts/cost/fleet-digest.test.mjs +0 -207
  466. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  467. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  468. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  469. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  470. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  471. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  472. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  473. package/scripts/daemon/assurance.test.mjs +0 -1791
  474. package/scripts/daemon/board-mirror.test.mjs +0 -165
  475. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  476. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  477. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  478. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  479. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  480. package/scripts/daemon/classifier.test.mjs +0 -266
  481. package/scripts/daemon/classify-kind.test.mjs +0 -40
  482. package/scripts/daemon/context-compiler.test.mjs +0 -406
  483. package/scripts/daemon/deliver.test.mjs +0 -564
  484. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  485. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  486. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  487. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  488. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  489. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  490. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  491. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  492. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  493. package/scripts/daemon/integration.test.mjs +0 -149
  494. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  495. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  496. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  497. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  498. package/scripts/daemon/responder-cost.test.mjs +0 -68
  499. package/scripts/daemon/responder-history.test.mjs +0 -221
  500. package/scripts/daemon/sdk-version.test.mjs +0 -31
  501. package/scripts/daemon/session-lock.test.mjs +0 -252
  502. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  503. package/scripts/daemon/typing-registry.test.mjs +0 -102
  504. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  505. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  506. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  507. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  508. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  509. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  510. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  511. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  512. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  513. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  514. package/scripts/poller-launchd/install.test.mjs +0 -243
  515. package/scripts/restore-from-backup.test.mjs +0 -181
  516. package/scripts/session/feed.test.mjs +0 -196
  517. package/scripts/session/supervisor-sh.test.mjs +0 -218
  518. package/scripts/session/supervisor.test.mjs +0 -482
  519. package/scripts/setup/configure-macos.test.mjs +0 -306
  520. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  521. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  522. package/scripts/setup/generate-capability.test.mjs +0 -134
  523. package/scripts/setup/init-agent.test.mjs +0 -370
  524. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  525. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  526. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -0,0 +1,327 @@
1
+ /**
2
+ * lib/engine/context/instructions.mjs — the instruction files a run reads.
3
+ *
4
+ * Every file found is included, in this order, each under a header naming it:
5
+ *
6
+ * 1. managed the organisation-managed CLAUDE.md
7
+ * (/Library/Application Support/ClaudeCode/CLAUDE.md on macOS,
8
+ * /etc/claude-code/CLAUDE.md on Linux)
9
+ * 2. user ~/.claude/CLAUDE.md ($CLAUDE_CONFIG_DIR/CLAUDE.md)
10
+ * 3. then for each directory from the filesystem root down to the working
11
+ * directory, in this order at each level:
12
+ * CLAUDE.md, .claude/CLAUDE.md, CLAUDE.local.md, COHORT.md, AGENTS.md
13
+ * rules every `*.md` under ~/.claude/rules (after the user file) and
14
+ * <cwd>/.claude/rules (after the last level), recursively, in
15
+ * path order. A rule whose frontmatter has `paths:` (globs,
16
+ * relative to cwd) is not loaded here: it is returned in
17
+ * `conditionalRules` and injected when a matching file is
18
+ * touched (lazy-instructions.mjs), as are CLAUDE.md files in
19
+ * directories below cwd.
20
+ * 4. memory the project's auto-memory index,
21
+ * ~/.claude/projects/<cwd with every non-alphanumeric as "->/memory/MEMORY.md,
22
+ * capped at its first 200 lines and 25 KB
23
+ *
24
+ * Nothing shadows anything: a global AGENTS.md does not hide ~/.claude/CLAUDE.md
25
+ * (where maestro writes an agent's identity block), and COHORT.md sits beside
26
+ * CLAUDE.md rather than replacing it. The same file reached twice (a home
27
+ * directory that is also an ancestor of cwd) is included once.
28
+ *
29
+ * Imports: `@path` in a file's text (outside code spans and fenced blocks)
30
+ * includes that file after the line that names it. Relative paths resolve
31
+ * against the importing file's directory; `~/` against the home directory.
32
+ * Imports nest to a depth of 5; a file already on the import chain, or already
33
+ * included, is not included again. A token that does not name an existing file
34
+ * is left as text (it is probably an @-mention).
35
+ *
36
+ * The text is deterministic for a given set of files, so it can sit in the
37
+ * cached prefix of the system prompt.
38
+ *
39
+ * All reads go through the injected `readFile` / `isFile`.
40
+ *
41
+ * @module lib/engine/context/instructions
42
+ */
43
+
44
+ import path from "node:path";
45
+ import { parseSkillFile } from "../skills/index.mjs";
46
+
47
+ export const RULES_DIR = path.join(".claude", "rules");
48
+ const MAX_RULE_DEPTH = 8;
49
+
50
+ export const FILES_PER_LEVEL = Object.freeze(["CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md", "COHORT.md", "AGENTS.md"]);
51
+ export const MAX_IMPORT_DEPTH = 5;
52
+ export const MEMORY_MAX_LINES = 200;
53
+ export const MEMORY_MAX_BYTES = 25_000;
54
+
55
+ /** @param {string} platform */
56
+ export function defaultManagedInstructionsPath(platform) {
57
+ if (platform === "darwin") return "/Library/Application Support/ClaudeCode/CLAUDE.md";
58
+ if (platform === "linux") return "/etc/claude-code/CLAUDE.md";
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Directories from the filesystem root down to `cwd`, inclusive.
64
+ * @param {string} cwd
65
+ */
66
+ export function ancestorDirs(cwd) {
67
+ const abs = path.resolve(cwd);
68
+ const dirs = [];
69
+ let cur = abs;
70
+ for (;;) {
71
+ dirs.unshift(cur);
72
+ const parent = path.dirname(cur);
73
+ if (parent === cur) break;
74
+ cur = parent;
75
+ }
76
+ return dirs;
77
+ }
78
+
79
+ /** @param {string} name */
80
+ function scopeOf(name) {
81
+ return name === "CLAUDE.local.md" ? "local" : "project";
82
+ }
83
+
84
+ /**
85
+ * @param {{cwd:string, userDir:string|null, managedPath?:string|null}} p
86
+ * @returns {Array<{path:string, scope:'managed'|'user'|'project'|'local'}>}
87
+ */
88
+ export function instructionCandidates({ cwd, userDir, managedPath = null }) {
89
+ const out = [];
90
+ if (managedPath) out.push({ path: managedPath, scope: /** @type const */ ("managed") });
91
+ if (userDir) out.push({ path: path.join(userDir, "CLAUDE.md"), scope: /** @type const */ ("user") });
92
+ for (const dir of ancestorDirs(cwd)) {
93
+ for (const name of FILES_PER_LEVEL) out.push({ path: path.join(dir, name), scope: scopeOf(name) });
94
+ }
95
+ return /** @type any */ (out);
96
+ }
97
+
98
+ /**
99
+ * The @path tokens of a text, outside fenced blocks and inline code.
100
+ * @param {string} text
101
+ * @returns {Array<{line:number, target:string}>}
102
+ */
103
+ export function findImports(text) {
104
+ const found = [];
105
+ let fenced = false;
106
+ const lines = String(text).split("\n");
107
+ lines.forEach((line, i) => {
108
+ if (/^\s*(```|~~~)/.test(line)) {
109
+ fenced = !fenced;
110
+ return;
111
+ }
112
+ if (fenced) return;
113
+ const visible = line.replace(/`[^`]*`/g, (m) => " ".repeat(m.length));
114
+ const re = /(^|\s)@((?:~\/|\.{1,2}\/|\/)?[^\s`'"()<>]+)/g;
115
+ let m;
116
+ while ((m = re.exec(visible)) !== null) {
117
+ const target = m[2].replace(/[.,;:!?]+$/, "");
118
+ if (target !== "") found.push({ line: i, target });
119
+ }
120
+ });
121
+ return found;
122
+ }
123
+
124
+ /**
125
+ * @param {string} target @param {{baseDir:string, homedir:string|null}} o
126
+ */
127
+ export function resolveImportPath(target, { baseDir, homedir }) {
128
+ if (target.startsWith("~/")) return homedir ? path.join(homedir, target.slice(2)) : null;
129
+ return path.resolve(baseDir, target);
130
+ }
131
+
132
+ /**
133
+ * @param {string} text
134
+ * @param {object} o
135
+ * @param {string} o.file the file this text came from
136
+ * @param {string|null} o.homedir
137
+ * @param {(p:string)=>string} o.readFile
138
+ * @param {(p:string)=>boolean} o.isFile
139
+ * @param {Set<string>} o.included every file already in the output (mutated)
140
+ * @param {string[]} [o.chain] the import chain above this file
141
+ * @returns {string}
142
+ */
143
+ export function expandImports(text, o) {
144
+ const chain = [...(o.chain ?? []), o.file];
145
+ const depth = chain.length - 1;
146
+ const imports = findImports(text);
147
+ if (imports.length === 0 || depth >= MAX_IMPORT_DEPTH) return text;
148
+ const byLine = new Map();
149
+ for (const imp of imports) {
150
+ const abs = resolveImportPath(imp.target, { baseDir: path.dirname(o.file), homedir: o.homedir });
151
+ if (!abs || chain.includes(abs) || o.included.has(abs) || !o.isFile(abs)) continue;
152
+ let body;
153
+ try {
154
+ body = o.readFile(abs);
155
+ } catch {
156
+ continue; // unreadable: leave the token as written
157
+ }
158
+ o.included.add(abs);
159
+ const expanded = expandImports(body, { ...o, file: abs, chain });
160
+ const list = byLine.get(imp.line) ?? [];
161
+ list.push(`[imported from ${abs}]\n${expanded.trimEnd()}\n[end of ${abs}]`);
162
+ byLine.set(imp.line, list);
163
+ }
164
+ if (byLine.size === 0) return text;
165
+ return String(text)
166
+ .split("\n")
167
+ .flatMap((line, i) => (byLine.has(i) ? [line, ...byLine.get(i)] : [line]))
168
+ .join("\n");
169
+ }
170
+
171
+ /**
172
+ * @param {{cwd:string, userDir:string|null}} p
173
+ */
174
+ export function memoryIndexPath({ cwd, userDir }) {
175
+ if (!userDir) return null;
176
+ return path.join(userDir, "projects", path.resolve(cwd).replace(/[^A-Za-z0-9]/g, "-"), "memory", "MEMORY.md");
177
+ }
178
+
179
+ /** First 200 lines, then at most 25 KB (cut at a line boundary where possible). @param {string} text */
180
+ export function capMemory(text) {
181
+ const lines = String(text).split("\n");
182
+ let out = lines.slice(0, MEMORY_MAX_LINES).join("\n");
183
+ const truncatedLines = lines.length > MEMORY_MAX_LINES;
184
+ let truncatedBytes = false;
185
+ if (Buffer.byteLength(out, "utf8") > MEMORY_MAX_BYTES) {
186
+ truncatedBytes = true;
187
+ let buf = Buffer.from(out, "utf8").subarray(0, MEMORY_MAX_BYTES).toString("utf8");
188
+ buf = buf.replace(/�$/, "");
189
+ const nl = buf.lastIndexOf("\n");
190
+ out = nl > 0 ? buf.slice(0, nl) : buf;
191
+ }
192
+ return { text: out, truncated: truncatedLines || truncatedBytes };
193
+ }
194
+
195
+ /**
196
+ * Every `.md` file under a rules directory, recursively, sorted by path.
197
+ * @param {string} dir @param {{readdir?:(p:string)=>string[], isDir?:(p:string)=>boolean, isFile:(p:string)=>boolean}} fs
198
+ */
199
+ export function listRuleFiles(dir, fs) {
200
+ if (!fs.readdir || !fs.isDir || !fs.isDir(dir)) return [];
201
+ const out = [];
202
+ const walk = (/** @type string */ d, /** @type number */ depth) => {
203
+ let names;
204
+ try {
205
+ names = /** @type {(p:string)=>string[]} */ (fs.readdir)(d).sort();
206
+ } catch {
207
+ return;
208
+ }
209
+ for (const n of names) {
210
+ const full = path.join(d, n);
211
+ if (/** @type {(p:string)=>boolean} */ (fs.isDir)(full)) {
212
+ if (depth < MAX_RULE_DEPTH) walk(full, depth + 1);
213
+ } else if (n.endsWith(".md") && fs.isFile(full)) out.push(full);
214
+ }
215
+ };
216
+ walk(dir, 0);
217
+ return out;
218
+ }
219
+
220
+ /**
221
+ * A rule file's `paths` frontmatter (string or list, comma separated) and body.
222
+ * @param {string} text @returns {{patterns:string[], body:string}}
223
+ */
224
+ export function parseRule(text) {
225
+ const { frontmatter, body } = parseSkillFile(text);
226
+ const raw = frontmatter.paths;
227
+ const list = Array.isArray(raw) ? raw.map(String) : typeof raw === "string" ? raw.split(",") : [];
228
+ return { patterns: list.map((s) => s.trim().replace(/^["']|["']$/g, "").trim()).filter(Boolean), body };
229
+ }
230
+
231
+ /**
232
+ * @param {object} p
233
+ * @param {string} p.cwd
234
+ * @param {string|null} p.homedir
235
+ * @param {string|null} p.userDir
236
+ * @param {string|null} [p.managedPath]
237
+ * @param {(p:string)=>string} p.readFile
238
+ * @param {(p:string)=>boolean} p.isFile
239
+ * @param {(p:string)=>string} [p.realpath]
240
+ * @param {(p:string)=>string[]} [p.readdir] with isDir: enables .claude/rules
241
+ * @param {(p:string)=>boolean} [p.isDir]
242
+ */
243
+ export function loadInstructions({ cwd, homedir, userDir, managedPath = null, readFile, isFile, realpath = (p) => p, readdir, isDir }) {
244
+ const included = new Set();
245
+ const seenReal = new Set();
246
+ /** @type {Array<{path:string, scope:string, content:string}>} */
247
+ const files = [];
248
+ const errors = [];
249
+ /** @type {Array<{path:string, patterns:string[], content:string}>} */
250
+ const conditionalRules = [];
251
+ const addRules = (/** @type string */ dir, /** @type string */ scope) => {
252
+ for (const file of listRuleFiles(dir, { readdir, isDir, isFile })) {
253
+ let real;
254
+ try {
255
+ real = realpath(file);
256
+ } catch {
257
+ real = file;
258
+ }
259
+ // A rule already pulled in by an @import (or reached twice) is included once.
260
+ if (seenReal.has(real) || included.has(file)) continue;
261
+ seenReal.add(real);
262
+ let text;
263
+ try {
264
+ text = readFile(file);
265
+ } catch (e) {
266
+ errors.push(`${file}: ${e instanceof Error ? e.message : e}`);
267
+ continue;
268
+ }
269
+ included.add(file);
270
+ const { patterns, body } = parseRule(text);
271
+ const content = expandImports(body, { file, homedir, readFile, isFile, included });
272
+ if (patterns.length > 0) conditionalRules.push({ path: file, patterns, content });
273
+ else files.push({ path: file, scope, content });
274
+ }
275
+ };
276
+ let userRulesDone = false;
277
+ for (const c of instructionCandidates({ cwd, userDir, managedPath })) {
278
+ if (!userRulesDone && c.scope !== "managed" && c.scope !== "user") {
279
+ userRulesDone = true;
280
+ if (userDir) addRules(path.join(userDir, "rules"), "user rule");
281
+ }
282
+ if (!isFile(c.path)) continue;
283
+ let real;
284
+ try {
285
+ real = realpath(c.path);
286
+ } catch {
287
+ real = c.path;
288
+ }
289
+ if (seenReal.has(real) || included.has(c.path)) continue;
290
+ seenReal.add(real);
291
+ let text;
292
+ try {
293
+ text = readFile(c.path);
294
+ } catch (e) {
295
+ errors.push(`${c.path}: ${e instanceof Error ? e.message : e}`);
296
+ continue;
297
+ }
298
+ included.add(c.path);
299
+ files.push({ path: c.path, scope: c.scope, content: expandImports(text, { file: c.path, homedir, readFile, isFile, included }) });
300
+ }
301
+ addRules(path.join(cwd, RULES_DIR), "project rule");
302
+ let memory = null;
303
+ const mem = memoryIndexPath({ cwd, userDir });
304
+ if (mem && isFile(mem)) {
305
+ try {
306
+ const capped = capMemory(readFile(mem));
307
+ memory = { path: mem, content: capped.text, truncated: capped.truncated };
308
+ } catch (e) {
309
+ errors.push(`${mem}: ${e instanceof Error ? e.message : e}`);
310
+ }
311
+ }
312
+ return { files, memory, errors, conditionalRules };
313
+ }
314
+
315
+ /**
316
+ * The system-prompt block (pure). Null when nothing was found.
317
+ * @param {{files:Array<{path:string, scope:string, content:string}>, memory:{path:string, content:string, truncated?:boolean}|null}} loaded
318
+ */
319
+ export function formatInstructions({ files, memory }) {
320
+ if (files.length === 0 && !memory) return null;
321
+ const parts = ["Instructions for this work follow, from the most general file to the most specific. Where they conflict, the more specific file wins."];
322
+ for (const f of files) parts.push(`## ${f.path} (${f.scope})\n\n${f.content.trim()}`);
323
+ if (memory) {
324
+ parts.push(`## ${memory.path} (memory index${memory.truncated ? ", truncated" : ""})\n\n${memory.content.trim()}`);
325
+ }
326
+ return parts.join("\n\n");
327
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * lib/engine/context/lazy-instructions.mjs — instructions found as the run moves (CF-21).
3
+ *
4
+ * At startup the engine reads the instruction files from the filesystem root
5
+ * down to the working directory (instructions.mjs). A repository often keeps
6
+ * more specific files further down — `packages/api/CLAUDE.md` — and
7
+ * `.claude/rules/*.md` files can say which paths they apply to (`paths:`
8
+ * frontmatter). Loading all of those up front would spend context on parts
9
+ * of the tree the run never touches, so they are loaded LAZILY:
10
+ *
11
+ * · when Read, Edit, Write, MultiEdit or NotebookEdit touches a file below
12
+ * the working directory, every instruction file (CLAUDE.md,
13
+ * .claude/CLAUDE.md, CLAUDE.local.md, COHORT.md, AGENTS.md) in each
14
+ * directory from just below cwd down to that file's directory is read,
15
+ * with its @imports;
16
+ * · a path-scoped rule is read when a touched file matches one of its globs
17
+ * (relative to the working directory);
18
+ * · each file is injected ONCE per run, as text appended to the message that
19
+ * carries the tool results — never into the system prompt, whose bytes are
20
+ * the cached prefix.
21
+ *
22
+ * The loader remembers what it injected, so compaction can carry it forward.
23
+ *
24
+ * @module lib/engine/context/lazy-instructions
25
+ */
26
+
27
+ import path from "node:path";
28
+ import { FILES_PER_LEVEL, expandImports } from "./instructions.mjs";
29
+
30
+ export const TOUCH_TOOLS = Object.freeze(new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit"]));
31
+
32
+ /**
33
+ * The absolute file a tool call touches, or null.
34
+ * @param {string} toolName @param {any} input @param {string} cwd
35
+ */
36
+ export function touchedPath(toolName, input, cwd) {
37
+ if (!TOUCH_TOOLS.has(toolName) || !input || typeof input !== "object") return null;
38
+ const p = input.file_path ?? input.notebook_path;
39
+ if (typeof p !== "string" || p === "") return null;
40
+ return path.resolve(cwd, p);
41
+ }
42
+
43
+ /**
44
+ * Instruction files for a touched file: directories strictly below `cwd` down
45
+ * to the file's directory, outermost first. Empty for a file outside cwd.
46
+ * @param {{cwd:string, file:string}} p
47
+ */
48
+ export function nestedInstructionCandidates({ cwd, file }) {
49
+ const root = path.resolve(cwd);
50
+ const dir = path.dirname(path.resolve(file));
51
+ const rel = path.relative(root, dir);
52
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return [];
53
+ const out = [];
54
+ let cur = root;
55
+ for (const part of rel.split(path.sep)) {
56
+ cur = path.join(cur, part);
57
+ for (const name of FILES_PER_LEVEL) out.push(path.join(cur, name));
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /**
63
+ * A glob as a regular expression over a forward-slash relative path:
64
+ * `**` any depth, `*` within a segment, `?` one character, `{a,b}` either.
65
+ * A pattern with no slash matches the file name at any depth.
66
+ * @param {string} glob
67
+ */
68
+ export function globToRegExp(glob) {
69
+ let g = String(glob).trim().replace(/^\.\//, "");
70
+ if (!g.includes("/")) g = `**/${g}`;
71
+ let re = "";
72
+ for (let i = 0; i < g.length; i++) {
73
+ const ch = g[i];
74
+ if (ch === "*") {
75
+ if (g[i + 1] === "*") {
76
+ const slash = g[i + 2] === "/";
77
+ re += slash ? "(?:.*/)?" : ".*";
78
+ i += slash ? 2 : 1;
79
+ } else re += "[^/]*";
80
+ } else if (ch === "?") re += "[^/]";
81
+ else if (ch === "{") {
82
+ const end = g.indexOf("}", i);
83
+ if (end === -1) re += "\\{";
84
+ else {
85
+ re += `(?:${g.slice(i + 1, end).split(",").map((s) => s.replace(/[.+^$()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join("|")})`;
86
+ i = end;
87
+ }
88
+ } else re += ch.replace(/[.+^$()|[\]\\]/g, "\\$&");
89
+ }
90
+ return new RegExp(`^${re}$`);
91
+ }
92
+
93
+ /**
94
+ * @typedef {{path:string, patterns:string[], content:string}} ConditionalRule
95
+ */
96
+
97
+ /**
98
+ * @param {object} p
99
+ * @param {string} p.cwd
100
+ * @param {string|null} p.homedir
101
+ * @param {(p:string)=>string} p.readFile
102
+ * @param {(p:string)=>boolean} p.isFile
103
+ * @param {(p:string)=>string} [p.realpath]
104
+ * @param {string[]} [p.loaded] files already in the system prompt
105
+ * @param {ConditionalRule[]} [p.conditionalRules]
106
+ */
107
+ export function createLazyInstructions({ cwd, homedir, readFile, isFile, realpath = (x) => x, loaded = [], conditionalRules = [] }) {
108
+ const real = (/** @type string */ p) => {
109
+ try {
110
+ return realpath(p);
111
+ } catch {
112
+ return p;
113
+ }
114
+ };
115
+ const seen = new Set(loaded.map(real));
116
+ const included = new Set(loaded);
117
+ /** @type {Array<{path:string, content:string, reason:string}>} */
118
+ const injected = [];
119
+ const rules = conditionalRules.map((r) => ({ ...r, res: r.patterns.map(globToRegExp) }));
120
+
121
+ return {
122
+ /**
123
+ * Instruction files to inject because this file was touched (each once).
124
+ * @param {string} file absolute
125
+ * @returns {Array<{path:string, content:string, reason:string}>}
126
+ */
127
+ touch(file) {
128
+ const out = [];
129
+ for (const candidate of nestedInstructionCandidates({ cwd, file })) {
130
+ if (!isFile(candidate)) continue;
131
+ const r = real(candidate);
132
+ if (seen.has(r)) continue;
133
+ seen.add(r);
134
+ let text;
135
+ try {
136
+ text = readFile(candidate);
137
+ } catch {
138
+ continue;
139
+ }
140
+ included.add(candidate);
141
+ const content = expandImports(text, { file: candidate, homedir, readFile, isFile, included });
142
+ out.push({ path: candidate, content, reason: `it applies to files under ${path.dirname(candidate)}` });
143
+ }
144
+ const rel = path.relative(path.resolve(cwd), path.resolve(file)).split(path.sep).join("/");
145
+ if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
146
+ for (const rule of rules) {
147
+ if (seen.has(real(rule.path)) || !rule.res.some((re) => re.test(rel))) continue;
148
+ seen.add(real(rule.path));
149
+ out.push({ path: rule.path, content: rule.content, reason: `its paths (${rule.patterns.join(", ")}) match ${rel}` });
150
+ }
151
+ }
152
+ injected.push(...out);
153
+ return out;
154
+ },
155
+ /** Everything injected so far, in order. */
156
+ injected: () => injected.map(({ path: p, content }) => ({ path: p, content })),
157
+ };
158
+ }
159
+
160
+ /**
161
+ * The text block appended after tool results (pure).
162
+ * @param {Array<{path:string, content:string, reason:string}>} entries
163
+ */
164
+ export function formatLazyInstructions(entries) {
165
+ if (entries.length === 0) return null;
166
+ return entries
167
+ .map((e) => `Instructions from ${e.path} now apply (${e.reason}). Follow them alongside the instructions you already have; where they conflict, these more specific ones win.\n\n${e.content.trim()}`)
168
+ .join("\n\n");
169
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * lib/engine/context/manager.mjs — what the loop asks at every turn boundary.
3
+ *
4
+ * The loop (loop.mjs `p.context`) calls three hooks:
5
+ *
6
+ * beforeTurn before each model request:
7
+ * 1. servers that announced a tool-list change are re-listed
8
+ * and the tool set rebuilt (never mid-turn);
9
+ * 2. queued input is drained — a stream-json
10
+ * `{type:"control", subtype:"compact"}` requests manual
11
+ * compaction; queued user messages are appended;
12
+ * 3. images leave all but the most recent image-carrying
13
+ * tool results (images.mjs);
14
+ * the request is measured and projected (budget.mjs);
15
+ * at the threshold — or on a manual request — compaction
16
+ * runs (compaction.mjs) and the history is replaced;
17
+ * 4. the definitions to send are computed: the fixed tools and
18
+ * loaded deferred tools, canonical and in stable order.
19
+ * observe after each model request: the provider's input-token count
20
+ * calibrates the estimator.
21
+ * afterTools after each tool batch: a deferred tool called by name is
22
+ * loaded; files touched by Read/Edit/Write bring in the
23
+ * instruction files that apply to them (lazy-instructions.mjs),
24
+ * returned as a text block appended to the tool results.
25
+ *
26
+ * Automatic compaction does not repeat on the next turn if it could not get
27
+ * under the limit: it waits until the projection has grown by a tenth of the
28
+ * window since, so a history that cannot shrink further is not re-summarised
29
+ * every turn.
30
+ *
31
+ * @module lib/engine/context/manager
32
+ */
33
+
34
+ import { measureRequest, initialCalibration, calibrate, projectTokens, compactionLimit, charsForTokens, reportedInputTokens } from "./budget.mjs";
35
+ import { stableToolDefinitions } from "./cache.mjs";
36
+ import { runCompaction } from "./compaction.mjs";
37
+ import { touchedPath, formatLazyInstructions } from "./lazy-instructions.mjs";
38
+ import { pruneOldImages, DEFAULT_KEEP_IMAGE_RESULTS } from "./images.mjs";
39
+
40
+ /**
41
+ * @param {object} p
42
+ * @param {import('./budget.mjs').ContextConfig} p.config
43
+ * @param {string} p.cwd
44
+ * @param {(turn:{system:string, messages:any[], tools:any[], signal?:AbortSignal})=>Promise<any>} p.summarise same wire, same tier
45
+ * @param {any[]} p.tools every callable tool, in stable order
46
+ * @param {ReturnType<typeof import('../tools/toolsearch.mjs').createDeferredTools>|null} [p.deferred]
47
+ * @param {{preCompact:(o:{trigger:string, customInstructions?:string})=>Promise<{blockReason?:string|null}>}|null} [p.hooks]
48
+ * @param {ReturnType<typeof import('./lazy-instructions.mjs').createLazyInstructions>|null} [p.lazy]
49
+ * @param {{refreshChanged:()=>Promise<{changed:boolean, tools:any[], errors:string[]}>}|null} [p.mcp]
50
+ * @param {(mcpTools:any[]) => any[]} [p.composeTools] the full tool list for a new MCP tool list
51
+ * @param {{drain:()=>Array<{kind:'user', message:any}|{kind:'control', subtype:string, customInstructions?:string}>}|null} [p.input]
52
+ * @param {(e:{trigger:string, stage:number, message:any}) => void} [p.onCompact] a stage-2 summary was made
53
+ * @param {(line:string) => void} [p.log]
54
+ */
55
+ export function createContextManager(p) {
56
+ const log = p.log ?? (() => {});
57
+ let cal = initialCalibration();
58
+ let tools = p.tools;
59
+ /** @type {{chars:number, images:number}|null} */
60
+ let lastSize = null;
61
+ /** @type {number|null} projection right after the last automatic compaction */
62
+ let lastAfter = null;
63
+ /** @type {Array<{trigger:string, stage:number, cleared:number, before:number, after:number, blocked?:string, error?:string}>} */
64
+ const compactions = [];
65
+
66
+ const active = () => (p.deferred ? p.deferred.active() : tools);
67
+
68
+ return {
69
+ async beforeTurn({ system, messages, signal, recordModelCall }) {
70
+ /** @type {{messages?:any[], append:any[], tools:any[], definitions:any[]}} */
71
+ const out = { append: [], tools, definitions: [] };
72
+
73
+ if (p.mcp) {
74
+ const r = await p.mcp.refreshChanged();
75
+ for (const e of r.errors) log(`MCP: ${e}`);
76
+ if (r.changed) {
77
+ tools = p.composeTools ? p.composeTools(r.tools) : tools;
78
+ p.deferred?.replace(tools);
79
+ out.tools = tools;
80
+ log(`MCP tool list changed; ${tools.length} tools now available`);
81
+ }
82
+ }
83
+
84
+ /** @type {{customInstructions?:string}|null} */
85
+ let manual = null;
86
+ for (const item of p.input?.drain() ?? []) {
87
+ if (item.kind === "control" && item.subtype === "compact") manual = item;
88
+ else if (item.kind === "user") out.append.push(item.message);
89
+ }
90
+
91
+ // Images on older tool results leave the history (each rewrite happens once, as an image ages out).
92
+ const pruned = pruneOldImages(messages, p.config.keepImageResults ?? DEFAULT_KEEP_IMAGE_RESULTS);
93
+ if (pruned.removed > 0) {
94
+ messages = pruned.messages;
95
+ out.messages = pruned.messages;
96
+ }
97
+
98
+ const definitions = stableToolDefinitions(active());
99
+ out.definitions = definitions;
100
+ const measure = (/** @type any[] */ history) => measureRequest({ system, messages: [...history, ...out.append], tools: definitions });
101
+ let size = measure(messages);
102
+ const projected = projectTokens(cal, size);
103
+ const { over, limit } = compactionLimit({ projected, contextWindow: p.config.contextWindow, threshold: p.config.threshold });
104
+ const minGrowth = lastAfter !== null && lastAfter >= limit ? Math.ceil(p.config.contextWindow * 0.1) : 0;
105
+ const auto = p.config.autoCompact && over && (lastAfter === null || projected - lastAfter >= minGrowth);
106
+
107
+ if (manual || auto) {
108
+ const trigger = manual ? "manual" : "auto";
109
+ const r = await runCompaction({
110
+ messages,
111
+ trigger,
112
+ config: p.config,
113
+ callModel: p.summarise,
114
+ hooks: p.hooks ?? null,
115
+ stillOver: (m) => compactionLimit({ projected: projectTokens(cal, measure(m)), contextWindow: p.config.contextWindow, threshold: p.config.threshold }).over,
116
+ instructions: p.lazy?.injected() ?? [],
117
+ charsPerToken: cal.charsPerToken,
118
+ maxTranscriptChars: charsForTokens(cal, Math.floor(p.config.contextWindow * 0.5)),
119
+ customInstructions: manual?.customInstructions,
120
+ record: recordModelCall,
121
+ signal,
122
+ });
123
+ if (r.stage > 0) out.messages = r.messages;
124
+ size = measure(r.messages);
125
+ const after = projectTokens(cal, size);
126
+ if (trigger === "auto") lastAfter = after;
127
+ const entry = { trigger, stage: r.stage, cleared: r.cleared, before: projected, after };
128
+ if (r.blocked) Object.assign(entry, { blocked: r.blocked });
129
+ if (r.error) Object.assign(entry, { error: r.error });
130
+ compactions.push(entry);
131
+ log(
132
+ `context: ${trigger} compaction, stage ${r.stage} (${r.cleared} tool result${r.cleared === 1 ? "" : "s"} cleared; ~${projected} → ~${after} tokens of ${p.config.contextWindow})` +
133
+ (r.blocked ? `; a PreCompact hook limited it to clearing tool results: ${r.blocked}` : "") +
134
+ (r.error ? `; ${r.error}` : ""),
135
+ );
136
+ if (r.stage === 2) p.onCompact?.({ trigger, stage: 2, message: r.messages[0] });
137
+ }
138
+ lastSize = size;
139
+ return out;
140
+ },
141
+
142
+ observe({ res }) {
143
+ if (!lastSize || !res?.ok) return;
144
+ const inputTokens = reportedInputTokens(res.value?.usage);
145
+ if (res.value?.usage?.usageSource === "estimated") return;
146
+ cal = calibrate(cal, { ...lastSize, inputTokens });
147
+ },
148
+
149
+ async afterTools({ calls, results }) {
150
+ if (p.deferred) {
151
+ const direct = calls.filter((c) => p.deferred?.isDeferred(c.name) && !p.deferred.isLoaded(c.name)).map((c) => c.name);
152
+ if (direct.length > 0) p.deferred.load(direct);
153
+ }
154
+ if (!p.lazy) return null;
155
+ const entries = [];
156
+ calls.forEach((c, i) => {
157
+ if (results[i]?.is_error) return;
158
+ const file = touchedPath(c.name, c.input, p.cwd);
159
+ if (file) entries.push(...(/** @type any */ (p.lazy).touch(file)));
160
+ });
161
+ const text = formatLazyInstructions(entries);
162
+ return text ? [{ type: "text", text }] : null;
163
+ },
164
+
165
+ /** For the result object. */
166
+ stats() {
167
+ return {
168
+ contextWindow: p.config.contextWindow,
169
+ threshold: p.config.threshold,
170
+ charsPerToken: Math.round(cal.charsPerToken * 100) / 100,
171
+ compactions: [...compactions],
172
+ deferredTools: p.deferred ? p.deferred.deferred().length : 0,
173
+ loadedTools: p.deferred ? p.deferred.loadedNames() : [],
174
+ instructionsLoaded: p.lazy ? p.lazy.injected().map((f) => f.path) : [],
175
+ };
176
+ },
177
+ /** The tools currently callable. */
178
+ get tools() {
179
+ return tools;
180
+ },
181
+ };
182
+ }