@cohortapp/agent-sdk 2.17.0 → 2.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (531) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture-slots.mjs +234 -0
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/config.mjs +2 -0
  25. package/lib/collective/global-config.mjs +63 -1
  26. package/lib/collective/loop-guard.mjs +155 -0
  27. package/lib/collective/presence.mjs +142 -5
  28. package/lib/comms/send-gate.mjs +559 -1
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/failover.mjs +67 -16
  138. package/lib/model-router/llm-task.mjs +39 -3
  139. package/lib/model-router/resolve.mjs +89 -3
  140. package/lib/model-router/spawn.mjs +46 -47
  141. package/lib/model-router/taxonomy.mjs +126 -4
  142. package/lib/org/cost-sync.mjs +141 -11
  143. package/lib/org/inbound/broadcast.mjs +289 -0
  144. package/lib/org/inbound/collective.mjs +375 -0
  145. package/lib/org/inbound/directedness.mjs +96 -8
  146. package/lib/org/inbound/facts.mjs +78 -2
  147. package/lib/org/inbound/project.mjs +22 -0
  148. package/lib/org/inbound/surfaces.mjs +14 -0
  149. package/lib/org/llm-token.mjs +879 -0
  150. package/lib/org/mesh.mjs +61 -0
  151. package/lib/org/messaging.mjs +3 -1
  152. package/lib/org/protocol.checksum +1 -1
  153. package/lib/org/protocol.mjs +15 -0
  154. package/lib/org/quota.mjs +520 -0
  155. package/lib/org/tool-surface.mjs +104 -16
  156. package/lib/org/ui-parity.mjs +16 -1
  157. package/lib/org/work-ledger.mjs +37 -6
  158. package/lib/rate-guard.mjs +114 -1
  159. package/lib/resource-governor.mjs +41 -6
  160. package/lib/runtime/adapter.mjs +833 -0
  161. package/lib/runtime/child-env.mjs +191 -0
  162. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  163. package/lib/runtime/seat-engine.mjs +162 -0
  164. package/lib/session/ask-ledger.mjs +271 -0
  165. package/lib/session/current-work.mjs +676 -0
  166. package/lib/session/feed-core.mjs +40 -3
  167. package/lib/session/launch-args.mjs +56 -4
  168. package/lib/session/status-summary.mjs +26 -9
  169. package/lib/session/upgrade-notice.mjs +42 -0
  170. package/lib/setup/claude-probe.mjs +117 -13
  171. package/lib/setup/enrich.mjs +13 -10
  172. package/lib/setup/sections/model.mjs +39 -13
  173. package/lib/telemetry/collect.mjs +229 -11
  174. package/lib/upgrade/ignored-drift.mjs +105 -0
  175. package/lib/voice/post-call-brief.mjs +30 -17
  176. package/package.json +13 -3
  177. package/plugins/maestro-skills/skills/board-work.md +5 -0
  178. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  179. package/plugins/maestro-skills/skills/main-session.md +18 -7
  180. package/scaffold/config/collective.yaml +7 -0
  181. package/scripts/ci/check-durable-write-seam.mjs +3 -1
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +142 -19
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +51 -37
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +595 -149
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/maestro-daemon.mjs +11 -0
  198. package/scripts/daemon/prompt-builder.mjs +24 -0
  199. package/scripts/daemon/responder.mjs +246 -79
  200. package/scripts/daemon/sdk-version.mjs +98 -16
  201. package/scripts/eval/probe-gateway.mjs +635 -0
  202. package/scripts/eval/replay/extract.mjs +270 -0
  203. package/scripts/eval/replay/grade.mjs +260 -0
  204. package/scripts/eval/replay/lib/config.mjs +50 -0
  205. package/scripts/eval/replay/lib/effects.mjs +65 -0
  206. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  207. package/scripts/eval/replay/lib/judge.mjs +72 -0
  208. package/scripts/eval/replay/lib/redact.mjs +136 -0
  209. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  210. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  211. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  212. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  213. package/scripts/eval/replay/report.mjs +185 -0
  214. package/scripts/eval/replay/run.mjs +404 -0
  215. package/scripts/fleet/rollout.mjs +1151 -0
  216. package/scripts/hooks/pre-send-audit.sh +36 -245
  217. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  218. package/scripts/hooks/validate-state-yaml.sh +190 -0
  219. package/scripts/huddle/huddle-llm.mjs +361 -0
  220. package/scripts/huddle/huddle-server.mjs +46 -121
  221. package/scripts/local-triggers/autoupdate.sh +465 -81
  222. package/scripts/local-triggers/run-trigger.sh +13 -0
  223. package/scripts/maintenance/pin-integrity.mjs +364 -0
  224. package/scripts/poll-slack-events.sh +41 -9
  225. package/scripts/poller/slack-socket-mode.mjs +28 -3
  226. package/scripts/session/supervisor.mjs +80 -13
  227. package/scripts/spawn-session.sh +13 -0
  228. package/bin/maestro.test.mjs +0 -1574
  229. package/lib/action-executor.test.mjs +0 -871
  230. package/lib/archetype.test.mjs +0 -132
  231. package/lib/assurance/plan-note.test.mjs +0 -234
  232. package/lib/assurance/room-budget.test.mjs +0 -486
  233. package/lib/assurance/tier.test.mjs +0 -174
  234. package/lib/autonomy.test.mjs +0 -66
  235. package/lib/backlog.test.mjs +0 -302
  236. package/lib/backup/policy.test.mjs +0 -305
  237. package/lib/budget-escalate.test.mjs +0 -232
  238. package/lib/budget-guard.envelope.test.mjs +0 -476
  239. package/lib/budget-guard.test.mjs +0 -427
  240. package/lib/cadence-bus-requeue.test.mjs +0 -83
  241. package/lib/cadence-bus-schedule.test.mjs +0 -194
  242. package/lib/cadence-bus.test.mjs +0 -720
  243. package/lib/cadences.test.mjs +0 -230
  244. package/lib/capability/inventory.test.mjs +0 -232
  245. package/lib/capability.test.mjs +0 -78
  246. package/lib/channels/base-adapter.test.mjs +0 -590
  247. package/lib/channels/channels.test.mjs +0 -371
  248. package/lib/channels/contract.test.mjs +0 -162
  249. package/lib/channels/inbox-item.test.mjs +0 -368
  250. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  251. package/lib/channels/pairing.test.mjs +0 -270
  252. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  253. package/lib/channels/slack-adapter.test.mjs +0 -212
  254. package/lib/channels/telegram-adapter.test.mjs +0 -306
  255. package/lib/channels/voice/adapter.test.mjs +0 -278
  256. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  257. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  258. package/lib/charter.test.mjs +0 -89
  259. package/lib/claude-bin.test.mjs +0 -131
  260. package/lib/cli/board.test.mjs +0 -227
  261. package/lib/cli/design.test.mjs +0 -270
  262. package/lib/cli/doctor-checks.test.mjs +0 -336
  263. package/lib/cli/global-setup-extras.test.mjs +0 -462
  264. package/lib/cli/inbox.test.mjs +0 -230
  265. package/lib/cli/session-ack.test.mjs +0 -63
  266. package/lib/cli/session.test.mjs +0 -613
  267. package/lib/collective/capture.test.mjs +0 -121
  268. package/lib/collective/cards.test.mjs +0 -114
  269. package/lib/collective/config.test.mjs +0 -123
  270. package/lib/collective/global-config.test.mjs +0 -220
  271. package/lib/collective/global-skills.test.mjs +0 -126
  272. package/lib/collective/presence.test.mjs +0 -95
  273. package/lib/collective/recall.test.mjs +0 -116
  274. package/lib/collective/vendor-skills.test.mjs +0 -306
  275. package/lib/comms/send-gate.test.mjs +0 -770
  276. package/lib/comms.test.mjs +0 -41
  277. package/lib/context/budget.test.mjs +0 -252
  278. package/lib/context/history-scope.test.mjs +0 -79
  279. package/lib/cost/ledger-row.test.mjs +0 -183
  280. package/lib/design/design-md.test.mjs +0 -318
  281. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  282. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  283. package/lib/design/fixtures/foundation.json +0 -133
  284. package/lib/design/refresh-gate.test.mjs +0 -144
  285. package/lib/design/write.test.mjs +0 -241
  286. package/lib/diagnostics/alerts.test.mjs +0 -318
  287. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  288. package/lib/diagnostics/counters.test.mjs +0 -206
  289. package/lib/diagnostics/events.test.mjs +0 -290
  290. package/lib/diagnostics/otel.test.mjs +0 -196
  291. package/lib/diagnostics/trace.test.mjs +0 -251
  292. package/lib/env-compat.test.mjs +0 -104
  293. package/lib/execution/disposition.test.mjs +0 -553
  294. package/lib/execution/drive.test.mjs +0 -270
  295. package/lib/execution/effects.test.mjs +0 -344
  296. package/lib/execution/intake.test.mjs +0 -389
  297. package/lib/execution/journal.test.mjs +0 -261
  298. package/lib/execution/match.test.mjs +0 -235
  299. package/lib/execution/pipeline.test.mjs +0 -392
  300. package/lib/execution/route.test.mjs +0 -186
  301. package/lib/execution/surface-policy.test.mjs +0 -162
  302. package/lib/fs-atomic.test.mjs +0 -72
  303. package/lib/fs-ownership.test.mjs +0 -158
  304. package/lib/goals/admission.test.mjs +0 -164
  305. package/lib/goals/classify.test.mjs +0 -167
  306. package/lib/goals/collaborate.test.mjs +0 -336
  307. package/lib/goals/gaps.test.mjs +0 -284
  308. package/lib/goals/loop.test.mjs +0 -845
  309. package/lib/hooks/bus.test.mjs +0 -387
  310. package/lib/identity/persona.test.mjs +0 -142
  311. package/lib/kpi-sensors.test.mjs +0 -278
  312. package/lib/kpi.test.mjs +0 -244
  313. package/lib/learning/config.test.mjs +0 -75
  314. package/lib/learning/counters.test.mjs +0 -69
  315. package/lib/learning/curator-consolidate.test.mjs +0 -238
  316. package/lib/learning/curator.test.mjs +0 -106
  317. package/lib/learning/reflect.test.mjs +0 -0
  318. package/lib/learning/session-index.test.mjs +0 -125
  319. package/lib/learning/skill-writer.test.mjs +0 -210
  320. package/lib/mandate/audit.test.mjs +0 -195
  321. package/lib/mandate/contract.test.mjs +0 -185
  322. package/lib/mandate/derive.test.mjs +0 -274
  323. package/lib/mandate/model.test.mjs +0 -164
  324. package/lib/mandate/refresh.test.mjs +0 -389
  325. package/lib/mcp/server.test.mjs +0 -426
  326. package/lib/model-router/auth-profiles.test.mjs +0 -580
  327. package/lib/model-router/catalog.test.mjs +0 -385
  328. package/lib/model-router/economics.test.mjs +0 -438
  329. package/lib/model-router/failover.test.mjs +0 -439
  330. package/lib/model-router/health.test.mjs +0 -338
  331. package/lib/model-router/integration-coverage.test.mjs +0 -831
  332. package/lib/model-router/integration.test.mjs +0 -564
  333. package/lib/model-router/ledger.test.mjs +0 -415
  334. package/lib/model-router/llm-task.test.mjs +0 -392
  335. package/lib/model-router/org-credentials.test.mjs +0 -265
  336. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  337. package/lib/model-router/reconcile.test.mjs +0 -316
  338. package/lib/model-router/repair.test.mjs +0 -180
  339. package/lib/model-router/spawn.test.mjs +0 -446
  340. package/lib/model-router/taxonomy.test.mjs +0 -410
  341. package/lib/model-router.test.mjs +0 -1207
  342. package/lib/org/activity.test.mjs +0 -134
  343. package/lib/org/approvals.test.mjs +0 -216
  344. package/lib/org/awareness.test.mjs +0 -159
  345. package/lib/org/board-mine-cache.test.mjs +0 -53
  346. package/lib/org/board.test.mjs +0 -187
  347. package/lib/org/bootstrap-context.test.mjs +0 -153
  348. package/lib/org/client.test.mjs +0 -1206
  349. package/lib/org/cohort-client.test.mjs +0 -126
  350. package/lib/org/cost-sync.test.mjs +0 -153
  351. package/lib/org/doctor.test.mjs +0 -346
  352. package/lib/org/engagement-ledger.test.mjs +0 -112
  353. package/lib/org/engagement.test.mjs +0 -739
  354. package/lib/org/handoff.test.mjs +0 -269
  355. package/lib/org/inbound/directedness.test.mjs +0 -668
  356. package/lib/org/inbound/facts.test.mjs +0 -471
  357. package/lib/org/inbound/hydrate.test.mjs +0 -908
  358. package/lib/org/inbound/index.test.mjs +0 -429
  359. package/lib/org/inbound/project.test.mjs +0 -287
  360. package/lib/org/integration-tools.test.mjs +0 -160
  361. package/lib/org/keys.test.mjs +0 -92
  362. package/lib/org/knowledge.test.mjs +0 -326
  363. package/lib/org/leases.test.mjs +0 -235
  364. package/lib/org/mesh-directives.test.mjs +0 -110
  365. package/lib/org/mesh-integration.test.mjs +0 -127
  366. package/lib/org/mesh.test.mjs +0 -400
  367. package/lib/org/messaging.test.mjs +0 -471
  368. package/lib/org/param-contract.test.mjs +0 -477
  369. package/lib/org/policy.test.mjs +0 -237
  370. package/lib/org/protocol.checksum.test.mjs +0 -90
  371. package/lib/org/protocol.test.mjs +0 -323
  372. package/lib/org/push.test.mjs +0 -792
  373. package/lib/org/registry.test.mjs +0 -100
  374. package/lib/org/resource-tools.test.mjs +0 -361
  375. package/lib/org/tool-access.test.mjs +0 -144
  376. package/lib/org/tool-surface-integration.test.mjs +0 -120
  377. package/lib/org/tool-surface.test.mjs +0 -1268
  378. package/lib/org/typing.test.mjs +0 -291
  379. package/lib/org/ui-parity.test.mjs +0 -560
  380. package/lib/org/verify.test.mjs +0 -194
  381. package/lib/org/work-ledger.test.mjs +0 -273
  382. package/lib/plan/adoption-e2e.test.mjs +0 -366
  383. package/lib/plan/budget-enforcement.test.mjs +0 -400
  384. package/lib/plan/compile.test.mjs +0 -382
  385. package/lib/plan/emit.test.mjs +0 -269
  386. package/lib/plan/explain.test.mjs +0 -188
  387. package/lib/prompts/parallelism.test.mjs +0 -177
  388. package/lib/rag/rag.test.mjs +0 -505
  389. package/lib/rate-guard.test.mjs +0 -272
  390. package/lib/reactive-gate.test.mjs +0 -57
  391. package/lib/render.test.mjs +0 -68
  392. package/lib/resource-governor.test.mjs +0 -488
  393. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  394. package/lib/scheduling/jitter.test.mjs +0 -140
  395. package/lib/secrets/broker.test.mjs +0 -280
  396. package/lib/secrets/providers.test.mjs +0 -274
  397. package/lib/security/audit-engine.test.mjs +0 -424
  398. package/lib/security/coerce-args.test.mjs +0 -281
  399. package/lib/security/dangerous-tools.test.mjs +0 -68
  400. package/lib/security/external-content.test.mjs +0 -84
  401. package/lib/security/redact.test.mjs +0 -441
  402. package/lib/security/secret-equal.test.mjs +0 -55
  403. package/lib/session/config.test.mjs +0 -92
  404. package/lib/session/feed-core.test.mjs +0 -198
  405. package/lib/session/first-run.test.mjs +0 -121
  406. package/lib/session/frontdoor.test.mjs +0 -205
  407. package/lib/session/handoffs.test.mjs +0 -183
  408. package/lib/session/identity.test.mjs +0 -180
  409. package/lib/session/inbox-claims.test.mjs +0 -286
  410. package/lib/session/launch-args.test.mjs +0 -157
  411. package/lib/session/liveness.test.mjs +0 -100
  412. package/lib/session/status-summary.test.mjs +0 -118
  413. package/lib/session-permissions.test.mjs +0 -120
  414. package/lib/setup/claude-probe.test.mjs +0 -187
  415. package/lib/setup/completeness.test.mjs +0 -110
  416. package/lib/setup/context-pack.test.mjs +0 -89
  417. package/lib/setup/enrich.test.mjs +0 -115
  418. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  419. package/lib/setup/integration.test.mjs +0 -162
  420. package/lib/setup/io.test.mjs +0 -77
  421. package/lib/setup/runner.test.mjs +0 -132
  422. package/lib/setup/sections/identity.test.mjs +0 -234
  423. package/lib/setup/sections/inventory.test.mjs +0 -198
  424. package/lib/setup/sections/learning.test.mjs +0 -81
  425. package/lib/setup/sections/mandate.test.mjs +0 -388
  426. package/lib/setup/sections/messaging.test.mjs +0 -127
  427. package/lib/setup/sections/model.test.mjs +0 -240
  428. package/lib/setup/sections/org.test.mjs +0 -346
  429. package/lib/setup/sections/orgmail.test.mjs +0 -118
  430. package/lib/setup/sections/recovery.test.mjs +0 -98
  431. package/lib/setup/sections/subagents.test.mjs +0 -429
  432. package/lib/setup/sections/verify.test.mjs +0 -175
  433. package/lib/setup/sot.test.mjs +0 -81
  434. package/lib/setup/state.test.mjs +0 -115
  435. package/lib/singleton.test.mjs +0 -151
  436. package/lib/subagents/cli.test.mjs +0 -389
  437. package/lib/subagents/client.test.mjs +0 -309
  438. package/lib/subagents/gap.test.mjs +0 -234
  439. package/lib/subagents/lock.test.mjs +0 -248
  440. package/lib/subagents/manifest.test.mjs +0 -175
  441. package/lib/subagents/refs.test.mjs +0 -204
  442. package/lib/subagents/resolve.test.mjs +0 -422
  443. package/lib/subagents/schema.test.mjs +0 -328
  444. package/lib/telemetry/alerts.test.mjs +0 -109
  445. package/lib/telemetry/collect.test.mjs +0 -1274
  446. package/lib/tool-definitions-integration.test.mjs +0 -83
  447. package/lib/tool-definitions.test.mjs +0 -437
  448. package/lib/upgrade/global-refresh.test.mjs +0 -65
  449. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  450. package/lib/upgrade/post-steps.test.mjs +0 -200
  451. package/lib/upgrade/verify.test.mjs +0 -164
  452. package/lib/util/fetch-timeout.test.mjs +0 -202
  453. package/lib/util/reconnect.test.mjs +0 -369
  454. package/lib/util/unhandled.test.mjs +0 -216
  455. package/lib/voice/outbound.test.mjs +0 -69
  456. package/lib/voice/session-rotation.test.mjs +0 -114
  457. package/lib/voice/stt.test.mjs +0 -226
  458. package/lib/voice/voice.test.mjs +0 -990
  459. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  460. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  461. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  462. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  463. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  464. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  465. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  466. package/scripts/ci/check.test.mjs +0 -194
  467. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  468. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  469. package/scripts/collective/hook-runner.test.mjs +0 -173
  470. package/scripts/cost/fleet-digest.test.mjs +0 -207
  471. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  472. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  473. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  474. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  475. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  476. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  477. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  478. package/scripts/daemon/assurance.test.mjs +0 -1791
  479. package/scripts/daemon/board-mirror.test.mjs +0 -165
  480. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  481. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  482. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  483. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  484. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  485. package/scripts/daemon/classifier.test.mjs +0 -266
  486. package/scripts/daemon/classify-kind.test.mjs +0 -40
  487. package/scripts/daemon/context-compiler.test.mjs +0 -406
  488. package/scripts/daemon/deliver.test.mjs +0 -564
  489. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  490. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  491. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  492. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  493. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  494. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  495. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  496. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  497. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  498. package/scripts/daemon/integration.test.mjs +0 -149
  499. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  500. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  501. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  502. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  503. package/scripts/daemon/responder-cost.test.mjs +0 -68
  504. package/scripts/daemon/responder-history.test.mjs +0 -221
  505. package/scripts/daemon/sdk-version.test.mjs +0 -31
  506. package/scripts/daemon/session-lock.test.mjs +0 -252
  507. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  508. package/scripts/daemon/typing-registry.test.mjs +0 -102
  509. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  510. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  511. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  512. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  513. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  514. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  515. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  516. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  517. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  518. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  519. package/scripts/poller-launchd/install.test.mjs +0 -243
  520. package/scripts/restore-from-backup.test.mjs +0 -181
  521. package/scripts/session/feed.test.mjs +0 -196
  522. package/scripts/session/supervisor-sh.test.mjs +0 -218
  523. package/scripts/session/supervisor.test.mjs +0 -482
  524. package/scripts/setup/configure-macos.test.mjs +0 -306
  525. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  526. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  527. package/scripts/setup/generate-capability.test.mjs +0 -134
  528. package/scripts/setup/init-agent.test.mjs +0 -370
  529. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  530. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  531. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -0,0 +1,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
+ }