@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/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
+ }