@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,234 @@
1
+ /**
2
+ * lib/collective/capture-slots.mjs — bound the capture workers a machine runs.
3
+ *
4
+ * Every Stop/PreCompact on the machine spawns a detached capture worker, and
5
+ * each worker runs one or two `claude --print` passes (hundreds of MB of RSS
6
+ * apiece). With no ceiling, a burst of session ends — or any loop that feeds
7
+ * the hook — becomes an unbounded fan-out of model processes. Two locks keep it
8
+ * bounded:
9
+ *
10
+ * - SLOT locks — `slot-<i>.lock` for i < max. A worker must hold one to run
11
+ * its passes, so at most `max` capture workers do model work machine-wide.
12
+ * - SESSION locks — `session-<id>.lock`. One session is captured by at most
13
+ * one worker at a time (a PreCompact and the Stop that follows it must not
14
+ * distil the same transcript twice in parallel).
15
+ *
16
+ * Both are O_EXCL files (`{flag:"wx"}` — the lock idiom registered in
17
+ * scripts/ci/check-durable-write-seam.mjs) under ONE machine-wide directory, so
18
+ * two agent installs on a machine share the ceiling. A lock records its holder's
19
+ * pid; a lock whose pid is dead (or which is older than `maxAgeMs`, covering
20
+ * pid reuse) is reclaimed. Reclaim re-reads the lock and deletes it only if it
21
+ * still holds the exact bytes judged stale, so a lock freshly re-taken by
22
+ * another reclaimer is not deleted; the residual window between that read and
23
+ * the unlink can at worst admit one extra worker, never lose the ceiling.
24
+ *
25
+ * Expected failure is a returned `{ok:false, error}`; nothing here throws.
26
+ *
27
+ * @module lib/collective/capture-slots
28
+ */
29
+
30
+ "use strict";
31
+
32
+ import { mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
33
+ import { homedir } from "node:os";
34
+ import { dirname, join } from "node:path";
35
+ import { randomBytes } from "node:crypto";
36
+
37
+ /** Default machine-wide ceiling on capture workers doing model work. */
38
+ export const DEFAULT_MAX_CONCURRENT = 2;
39
+ /** Default time a worker waits for a slot before recording a skipped capture. */
40
+ export const DEFAULT_QUEUE_WAIT_MS = 30_000;
41
+ /** Poll interval while queued for a slot. */
42
+ export const DEFAULT_POLL_MS = 1_000;
43
+ /**
44
+ * A lock older than this is stale even if its pid is alive (the pid was
45
+ * reused). Well above a worker's worst case: distil (60s) + reflect (90s) +
46
+ * index + queue wait.
47
+ */
48
+ export const DEFAULT_LOCK_MAX_AGE_MS = 15 * 60_000;
49
+ /** A lock with unreadable content younger than this is a holder mid-write, not a corpse. */
50
+ const UNREADABLE_GRACE_MS = 10_000;
51
+
52
+ /**
53
+ * The machine-wide lock directory. `MAESTRO_CAPTURE_LOCK_DIR` overrides it
54
+ * (tests, and a machine that wants the locks elsewhere).
55
+ * @param {Record<string, string|undefined>} [env]
56
+ * @returns {string}
57
+ */
58
+ export function captureLockDir(env = process.env) {
59
+ const override = env && env.MAESTRO_CAPTURE_LOCK_DIR;
60
+ return override && String(override).trim() ? String(override) : join(homedir(), ".claude", "maestro-capture-locks");
61
+ }
62
+
63
+ /**
64
+ * The configured ceiling, clamped to a positive integer. Absent/invalid → default.
65
+ * @param {object} [cfg] collective config
66
+ * @returns {number}
67
+ */
68
+ export function maxConcurrentFrom(cfg) {
69
+ const n = Number(cfg && cfg.captureMaxConcurrent);
70
+ return Number.isFinite(n) && n >= 1 ? Math.floor(n) : DEFAULT_MAX_CONCURRENT;
71
+ }
72
+
73
+ /**
74
+ * The configured queue wait in ms, clamped to >= 0. Absent/invalid → default.
75
+ * @param {object} [cfg] collective config
76
+ * @returns {number}
77
+ */
78
+ export function queueWaitFrom(cfg) {
79
+ const n = Number(cfg && cfg.captureQueueWaitMs);
80
+ return Number.isFinite(n) && n >= 0 ? n : DEFAULT_QUEUE_WAIT_MS;
81
+ }
82
+
83
+ /** Is `pid` a live process? EPERM means alive (someone else's process). */
84
+ export function pidAlive(pid) {
85
+ if (!Number.isInteger(pid) || pid <= 0) return false;
86
+ try {
87
+ process.kill(pid, 0);
88
+ return true;
89
+ } catch (err) {
90
+ return !!err && err.code === "EPERM";
91
+ }
92
+ }
93
+
94
+ function sanitize(id) {
95
+ const s = String(id || "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 128);
96
+ return s || "unknown";
97
+ }
98
+
99
+ /** Path of the session lock for `sessionId`. */
100
+ export function sessionLockPath(dir, sessionId) {
101
+ return join(dir, `session-${sanitize(sessionId)}.lock`);
102
+ }
103
+
104
+ /** Path of slot `i`. */
105
+ export function slotLockPath(dir, i) {
106
+ return join(dir, `slot-${i}.lock`);
107
+ }
108
+
109
+ /**
110
+ * Is the lock whose raw bytes are `raw` (mtime `mtimeMs`) stale at `nowMs`?
111
+ * @returns {boolean}
112
+ */
113
+ function isStale(raw, mtimeMs, nowMs, deps) {
114
+ let holder = null;
115
+ try { holder = JSON.parse(raw); } catch { holder = null; }
116
+ if (!holder || !Number.isInteger(holder.pid)) {
117
+ // Empty/corrupt: a holder between open(wx) and write, or a crashed one.
118
+ return nowMs - mtimeMs > UNREADABLE_GRACE_MS;
119
+ }
120
+ const acquired = Date.parse(holder.acquiredAt || "");
121
+ const ageMs = Number.isFinite(acquired) ? nowMs - acquired : nowMs - mtimeMs;
122
+ if (ageMs > deps.maxAgeMs) return true;
123
+ return !deps.isAlive(holder.pid);
124
+ }
125
+
126
+ /**
127
+ * Try once to take the O_EXCL lock at `lockPath`, reclaiming a stale one.
128
+ *
129
+ * @param {string} lockPath
130
+ * @param {{pid?:number, sessionId?:string}} [owner]
131
+ * @param {{now?:()=>number, isAlive?:(pid:number)=>boolean, maxAgeMs?:number}} [deps]
132
+ * @returns {{ok:true, lockPath:string, release:()=>boolean, reclaimed:boolean} |
133
+ * {ok:false, error:{code:"held"|"io", message:string, holder?:object}}}
134
+ */
135
+ export function tryAcquireLock(lockPath, owner = {}, deps = {}) {
136
+ const d = {
137
+ now: typeof deps.now === "function" ? deps.now : Date.now,
138
+ isAlive: typeof deps.isAlive === "function" ? deps.isAlive : pidAlive,
139
+ maxAgeMs: Number.isFinite(deps.maxAgeMs) ? deps.maxAgeMs : DEFAULT_LOCK_MAX_AGE_MS,
140
+ };
141
+ const pid = Number.isInteger(owner.pid) ? owner.pid : process.pid;
142
+ const token = randomBytes(8).toString("hex");
143
+ const dir = dirname(lockPath);
144
+ try { mkdirSync(dir, { recursive: true }); } catch (err) {
145
+ return { ok: false, error: { code: "io", message: `lock dir: ${err && err.message}` } };
146
+ }
147
+ let reclaimed = false;
148
+ // Two attempts: the first may find a stale lock and reclaim it; the second
149
+ // takes the freed name (or loses it to a concurrent reclaimer, which is "held").
150
+ for (let attempt = 0; attempt < 2; attempt++) {
151
+ try {
152
+ writeFileSync(lockPath, JSON.stringify({ pid, sessionId: owner.sessionId || "", token, acquiredAt: new Date(d.now()).toISOString() }), { flag: "wx" });
153
+ const release = () => {
154
+ try {
155
+ const cur = JSON.parse(readFileSync(lockPath, "utf8"));
156
+ if (!cur || cur.token !== token) return false; // reclaimed from us after we went stale — not ours to delete
157
+ unlinkSync(lockPath);
158
+ return true;
159
+ } catch {
160
+ return false; // already gone (reclaimed or cleaned) — nothing to release
161
+ }
162
+ };
163
+ return { ok: true, lockPath, release, reclaimed };
164
+ } catch (err) {
165
+ if (!err || err.code !== "EEXIST") {
166
+ return { ok: false, error: { code: "io", message: `lock ${lockPath}: ${err && err.message}` } };
167
+ }
168
+ }
169
+ let raw = "";
170
+ let mtimeMs = 0;
171
+ try {
172
+ raw = readFileSync(lockPath, "utf8");
173
+ mtimeMs = statSync(lockPath).mtimeMs;
174
+ } catch {
175
+ continue; // the holder released between our create and read — retry the create
176
+ }
177
+ if (!isStale(raw, mtimeMs, d.now(), d)) {
178
+ let holder = null;
179
+ try { holder = JSON.parse(raw); } catch { holder = null; }
180
+ return { ok: false, error: { code: "held", message: `lock ${lockPath} is held`, holder: holder || undefined } };
181
+ }
182
+ try {
183
+ // Delete only the exact stale bytes we judged; a lock re-taken since is left alone.
184
+ if (readFileSync(lockPath, "utf8") === raw) { unlinkSync(lockPath); reclaimed = true; }
185
+ } catch { /* someone else reclaimed it first — the retry below races for the name fairly */ }
186
+ }
187
+ return { ok: false, error: { code: "held", message: `lock ${lockPath} is held (lost the reclaim race)` } };
188
+ }
189
+
190
+ /**
191
+ * Take any free slot among `max`, once.
192
+ * @param {string} dir
193
+ * @param {number} max
194
+ * @param {object} [owner]
195
+ * @param {object} [deps] as {@link tryAcquireLock}
196
+ * @returns {{ok:true, slot:number, release:()=>boolean, reclaimed:boolean} |
197
+ * {ok:false, error:{code:"no-slot"|"io", message:string}}}
198
+ */
199
+ export function tryAcquireSlot(dir, max, owner = {}, deps = {}) {
200
+ let ioError = null;
201
+ for (let i = 0; i < max; i++) {
202
+ const r = tryAcquireLock(slotLockPath(dir, i), owner, deps);
203
+ if (r.ok) return { ok: true, slot: i, release: r.release, reclaimed: r.reclaimed };
204
+ if (r.error.code === "io") ioError = r.error;
205
+ }
206
+ if (ioError) return { ok: false, error: ioError };
207
+ return { ok: false, error: { code: "no-slot", message: `all ${max} capture slots are held` } };
208
+ }
209
+
210
+ /**
211
+ * Wait up to `waitMs` for a slot, polling. Resolves `{ok:false, error:{code:"no-slot"}}`
212
+ * when the wait runs out — the caller records the skipped capture.
213
+ * @param {string} dir
214
+ * @param {number} max
215
+ * @param {object} [owner]
216
+ * @param {{waitMs?:number, pollMs?:number, sleep?:(ms:number)=>Promise<void>, now?:()=>number}} [opts]
217
+ * plus the {@link tryAcquireLock} deps
218
+ * @returns {Promise<{ok:true, slot:number, release:()=>boolean, waitedMs:number} |
219
+ * {ok:false, error:{code:string, message:string}, waitedMs:number}>}
220
+ */
221
+ export async function acquireSlotQueued(dir, max, owner = {}, opts = {}) {
222
+ const now = typeof opts.now === "function" ? opts.now : Date.now;
223
+ const sleep = typeof opts.sleep === "function" ? opts.sleep : (ms) => new Promise((r) => setTimeout(r, ms));
224
+ const waitMs = Number.isFinite(opts.waitMs) ? Math.max(0, opts.waitMs) : DEFAULT_QUEUE_WAIT_MS;
225
+ const pollMs = Number.isFinite(opts.pollMs) && opts.pollMs > 0 ? opts.pollMs : DEFAULT_POLL_MS;
226
+ const start = now();
227
+ for (;;) {
228
+ const r = tryAcquireSlot(dir, max, owner, opts);
229
+ if (r.ok) return { ...r, waitedMs: now() - start };
230
+ if (r.error.code !== "no-slot") return { ...r, waitedMs: now() - start };
231
+ if (now() - start >= waitMs) return { ...r, waitedMs: now() - start };
232
+ await sleep(pollMs);
233
+ }
234
+ }
@@ -22,7 +22,7 @@
22
22
  import { spawnSync } from "node:child_process";
23
23
  import { normalizeCard, appendCards, repoLabel } from "./cards.mjs";
24
24
  import { isRepoDenied } from "./config.mjs";
25
- import { resolveClaudeBin, daemonClaudeArgs, augmentedPath } from "../claude-bin.mjs";
25
+ import { buildSpawn } from "../runtime/adapter.mjs";
26
26
 
27
27
  const DISTILL_TIMEOUT_MS = 60_000;
28
28
  const MAX_TRANSCRIPT_CHARS = 16_000;
@@ -118,13 +118,15 @@ export function extractCardsFromModelOutput(text) {
118
118
  /** Default distiller: spawn a cheap `claude --print` pass. Fail-open → "". */
119
119
  export function defaultDistiller(prompt, opts = {}) {
120
120
  try {
121
- const model = opts.model || "haiku";
122
- const args = ["--print", "--model", model, ...daemonClaudeArgs()];
123
- const res = spawnSync(resolveClaudeBin(), args, {
124
- input: prompt,
121
+ // argv + env from the runtime adapter (lane "capture"): --print --model <m>
122
+ // <daemon MCP args>, prompt on stdin, PATH augmented.
123
+ const spec = buildSpawn({ lane: "capture", model: opts.model || "haiku", prompt, mcp: {}, env: process.env });
124
+ if (!spec.ok) return "";
125
+ const res = spawnSync(spec.bin, spec.argv, {
126
+ input: spec.stdin,
125
127
  encoding: "utf8",
126
128
  timeout: DISTILL_TIMEOUT_MS,
127
- env: { ...process.env, PATH: augmentedPath() },
129
+ env: spec.env,
128
130
  maxBuffer: 8 * 1024 * 1024,
129
131
  });
130
132
  if (res.status === 0 && res.stdout) return res.stdout;
@@ -27,6 +27,8 @@ export const DEFAULTS = Object.freeze({
27
27
  captureOn: ["Stop", "PreCompact"],
28
28
  minTurnsToCapture: 2,
29
29
  maxCardsPerSession: 12,
30
+ captureMaxConcurrent: 2,
31
+ captureQueueWaitMs: 30000,
30
32
  repoDenylist: [],
31
33
  digestBudgetTokens: 1500,
32
34
  recencyHalfLifeDays: 30,
@@ -200,14 +200,25 @@ export function buildIdentityClaudeMd(agentJson, o = {}) {
200
200
  * Default command is the bare `cohort-mcp` (the global npm install); a caller
201
201
  * that knows it is not on PATH passes `{command, args}` (e.g. the current node
202
202
  * + this package's bin/cohort-mcp.mjs) so the server still starts. Pure.
203
+ *
204
+ * `inheritEnv` (CF-22, W5-B): the Cohort Engine gives no MCP server a
205
+ * credential its entry does not name, so the entry names the org credential
206
+ * cohort-mcp resolves from the environment (lib/org/client.mjs, both brands —
207
+ * bin/cohort-mcp.mjs bridges NEOLITH_* at boot). An enrolled seat's cohort-mcp
208
+ * reads its token from config/org.yaml first; the list keeps an env-only seat
209
+ * working. The claude binary tolerates the key; `claude mcp add-json` drops it,
210
+ * so global-setup's verify step falls back to the JSON merge, which keeps it.
203
211
  * @param {string} agentRoot @param {{command?:string, args?:string[]}} [o]
204
212
  */
205
213
  export function cohortMcpEntry(agentRoot, o = {}) {
206
214
  const command = o.command ? String(o.command) : "cohort-mcp";
207
215
  const args = Array.isArray(o.args) ? o.args.map(String) : [];
208
- return { command, args, env: { COHORT_AGENT_ROOT: String(agentRoot || "") } };
216
+ return { command, args, env: { COHORT_AGENT_ROOT: String(agentRoot || "") }, inheritEnv: [...COHORT_MCP_INHERIT_ENV] };
209
217
  }
210
218
 
219
+ /** The org credential names cohort-mcp's entry inherits (= lib/engine/context/child-env.mjs ORG_CREDENTIAL_ENV). */
220
+ export const COHORT_MCP_INHERIT_ENV = Object.freeze(["COHORT_API_TOKEN", "COHORT_TOKEN", "COHORT_API_KEY", "NEOLITH_API_TOKEN", "NEOLITH_TOKEN", "NEOLITH_API_KEY"]);
221
+
211
222
  function clone(v) { return JSON.parse(JSON.stringify(v)); }
212
223
  function isObj(v) { return v && typeof v === "object" && !Array.isArray(v); }
213
224
 
@@ -305,6 +316,57 @@ export function mergeSessionSettings(settings, o = {}) {
305
316
  return { settings: out, changes };
306
317
  }
307
318
 
319
+ // ---------------------------------------------------------------------------
320
+ // State-YAML gates (2026-09): the two hooks that keep an agent's state/queues,
321
+ // state/dashboards and state/indexes YAML parseable and un-clobbered. They live
322
+ // in the SEAT's .claude/settings.json (commands are seat-relative and only make
323
+ // sense inside an agent repo), never in ~/.claude. A hand edit to that file does
324
+ // not survive a fresh checkout (the scaffold copy is what `create` ships) or an
325
+ // upgrade (settings.json is not on the upgrade path), so the scaffold carries
326
+ // them for new seats and `global-setup` — which `upgrade` runs as a post-step —
327
+ // merges them onto existing ones with this helper. Pure; I/O is in bin/maestro.
328
+ // ---------------------------------------------------------------------------
329
+
330
+ /** The two hook groups, in the exact shape the scaffold .claude/settings.json carries. */
331
+ export const STATE_YAML_GATE_HOOKS = Object.freeze([
332
+ Object.freeze({
333
+ event: "PreToolUse",
334
+ matcher: "Write|Edit",
335
+ marker: "scripts/hooks/pre-write-yaml-validate.mjs",
336
+ command: "node ./scripts/hooks/pre-write-yaml-validate.mjs",
337
+ }),
338
+ Object.freeze({
339
+ event: "PostToolUse",
340
+ matcher: "Bash|Write|Edit",
341
+ marker: "scripts/hooks/validate-state-yaml.sh",
342
+ command: "./scripts/hooks/validate-state-yaml.sh",
343
+ }),
344
+ ]);
345
+
346
+ /**
347
+ * Merge the state-YAML gate hook groups into a seat's settings.json object, once.
348
+ * Presence is judged by the script marker anywhere under the event (any matcher),
349
+ * so an operator who already wired the script by hand is left alone. Existing
350
+ * groups, permissions and every other key are preserved. Idempotent. Pure.
351
+ *
352
+ * @param {object} settings parsed .claude/settings.json ({} / null when absent)
353
+ * @returns {{ settings: object, added: string[] }} added = "PreToolUse:pre-write-yaml-validate" ...
354
+ */
355
+ export function mergeStateYamlGateHooks(settings) {
356
+ const out = isObj(settings) ? clone(settings) : {};
357
+ const added = [];
358
+ if (!isObj(out.hooks)) out.hooks = {};
359
+ for (const g of STATE_YAML_GATE_HOOKS) {
360
+ if (!Array.isArray(out.hooks[g.event])) out.hooks[g.event] = [];
361
+ const present = out.hooks[g.event].some((grp) =>
362
+ grp && Array.isArray(grp.hooks) && grp.hooks.some((h) => h && typeof h.command === "string" && h.command.includes(g.marker)));
363
+ if (present) continue;
364
+ out.hooks[g.event].push({ matcher: g.matcher, hooks: [{ type: "command", command: g.command }] });
365
+ added.push(`${g.event}:${basename(g.marker).replace(/\.(mjs|sh)$/, "")}`);
366
+ }
367
+ return { settings: out, added };
368
+ }
369
+
308
370
  /** Build the machine pointer object. */
309
371
  export function buildPointer(repoRoot, agentName, nowMs) {
310
372
  return {
@@ -0,0 +1,155 @@
1
+ /**
2
+ * lib/collective/loop-guard.mjs — keep the collective layer from capturing itself.
3
+ *
4
+ * The global hooks (`maestro global-setup`) fire on EVERY Claude Code session on
5
+ * the machine, and the capture worker's distiller, the reflect reviewer and the
6
+ * skill curator are themselves `claude --print` sessions. Without an exemption
7
+ * each of those children's Stop hook captures ITS OWN transcript — whose content
8
+ * is the capture prompt — which spawns another worker, whose children stop and
9
+ * capture again. That is a fork-bomb: nothing in the chain terminates it.
10
+ *
11
+ * Two independent signals mark a session as the collective layer's own child:
12
+ *
13
+ * 1. ENV — every collective spawn carries {@link COLLECTIVE_CHILD_ENV}=1
14
+ * (set by the runtime adapter for the capture/reflect/curator lanes, and by
15
+ * the capture worker for its whole process tree). The hook classifies such
16
+ * a session as kind `collective` and does nothing for it.
17
+ * 2. CONTENT — if the env did not propagate (an older runtime, a wrapper that
18
+ * scrubs the environment), the transcript's FIRST user message is one of
19
+ * the collective prompts. Those prompts are this module's own text, so the
20
+ * opener is a reliable fingerprint; a human session does not open with it.
21
+ *
22
+ * Pure except {@link readTranscriptHead}, which reads a bounded prefix of a file.
23
+ *
24
+ * @module lib/collective/loop-guard
25
+ */
26
+
27
+ "use strict";
28
+
29
+ import { openSync, readSync, closeSync } from "node:fs";
30
+
31
+ /** The env marker every `claude` process spawned by the collective layer carries. */
32
+ export const COLLECTIVE_CHILD_ENV = "MAESTRO_COLLECTIVE_CHILD";
33
+
34
+ /**
35
+ * The opening line of each collective prompt. A drift test pins each one to its
36
+ * prompt builder (capture.buildDistillPrompt, reflect.buildReviewPrompt,
37
+ * curator.buildConsolidationPrompt), so a reworded prompt cannot silently disarm
38
+ * the content fallback.
39
+ */
40
+ export const COLLECTIVE_PROMPT_SENTINELS = Object.freeze([
41
+ "You distil a finished work session into durable MEMORY CARDS",
42
+ "You are the self-improvement reviewer for an autonomous AI agent",
43
+ "You are the skill CURATOR for an autonomous AI agent",
44
+ ]);
45
+
46
+ /**
47
+ * How much of a transcript the content fallback reads. The prompt is the first
48
+ * user input; SessionStart attachments (skill listings, digests) can precede it
49
+ * by tens of KB, so the window is generous — and one bounded read per stop.
50
+ */
51
+ export const TRANSCRIPT_HEAD_BYTES = 512 * 1024;
52
+
53
+ /** How far into the first user message a sentinel may start (tolerates a harness preamble). */
54
+ const SENTINEL_WINDOW_CHARS = 2000;
55
+
56
+ /**
57
+ * Does this environment mark a collective child?
58
+ * @param {Record<string, string|undefined>} [env]
59
+ * @returns {boolean}
60
+ */
61
+ export function isCollectiveChildEnv(env = process.env) {
62
+ return !!env && String(env[COLLECTIVE_CHILD_ENV] || "").trim() === "1";
63
+ }
64
+
65
+ /**
66
+ * The session kind the hooks act on. `collective` wins over `daemon`: a
67
+ * collective child spawned from a daemon session inherits MAESTRO_DAEMON=1 and
68
+ * must still be exempt from capture.
69
+ * @param {{source?:string}} [input] the hook's stdin payload
70
+ * @param {Record<string, string|undefined>} [env]
71
+ * @returns {"collective"|"daemon"|"interactive"}
72
+ */
73
+ export function classifyKind(input = {}, env = process.env) {
74
+ if (isCollectiveChildEnv(env)) return "collective";
75
+ const src = String((input && input.source) || "").toLowerCase();
76
+ if (src.includes("daemon") || (env && env.MAESTRO_DAEMON === "1")) return "daemon";
77
+ return "interactive";
78
+ }
79
+
80
+ function messageText(obj) {
81
+ const c = obj.content ?? (obj.message && obj.message.content);
82
+ if (typeof c === "string") return c;
83
+ if (Array.isArray(c)) {
84
+ return c.map((p) => (typeof p === "string" ? p : p && typeof p.text === "string" ? p.text : "")).filter(Boolean).join(" ");
85
+ }
86
+ return "";
87
+ }
88
+
89
+ /**
90
+ * The text of the first user message in a JSONL transcript prefix, or "".
91
+ * A truncated last line is simply skipped.
92
+ * @param {string} head
93
+ * @returns {string}
94
+ */
95
+ export function firstUserMessage(head) {
96
+ if (!head) return "";
97
+ for (const raw of String(head).split("\n")) {
98
+ const line = raw.trim();
99
+ if (!line.startsWith("{")) continue;
100
+ let obj;
101
+ try { obj = JSON.parse(line); } catch { continue; /* a truncated/partial line is not a message */ }
102
+ // A `--print` prompt is recorded twice: first as the queued input
103
+ // (`queue-operation`/`enqueue`, ahead of any attachment lines), then as the
104
+ // first `user` message. Either one is the session's first user input.
105
+ const queued = obj.type === "queue-operation" && obj.operation === "enqueue";
106
+ const role = (obj.message && obj.message.role) || obj.role || obj.type || "";
107
+ if (role !== "user" && !queued) continue;
108
+ const text = messageText(obj);
109
+ if (text) return text;
110
+ }
111
+ return "";
112
+ }
113
+
114
+ /**
115
+ * Is this transcript a collective child's (its first user turn is a collective prompt)?
116
+ * @param {string} head a prefix of the transcript JSONL
117
+ * @returns {boolean}
118
+ */
119
+ export function isCollectivePromptTranscript(head) {
120
+ const first = firstUserMessage(head).slice(0, SENTINEL_WINDOW_CHARS);
121
+ if (!first) return false;
122
+ return COLLECTIVE_PROMPT_SENTINELS.some((s) => first.includes(s));
123
+ }
124
+
125
+ /**
126
+ * Read at most `bytes` from the start of a file. "" when absent/unreadable.
127
+ * @param {string} path
128
+ * @param {number} [bytes]
129
+ * @returns {string}
130
+ */
131
+ export function readTranscriptHead(path, bytes = TRANSCRIPT_HEAD_BYTES) {
132
+ if (!path) return "";
133
+ let fd = null;
134
+ try {
135
+ fd = openSync(path, "r");
136
+ const buf = Buffer.alloc(bytes);
137
+ const n = readSync(fd, buf, 0, bytes, 0);
138
+ return buf.subarray(0, n).toString("utf8");
139
+ } catch {
140
+ return ""; // no transcript → the content fallback has nothing to say; the env signal still governs
141
+ } finally {
142
+ if (fd !== null) { try { closeSync(fd); } catch { /* the read already completed or failed */ } }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Why a stop/capture event must NOT spawn a capture worker, or null when it may.
148
+ * @param {{kind:string, transcriptHead?:string}} o
149
+ * @returns {null|"collective-child-env"|"collective-child-transcript"}
150
+ */
151
+ export function captureExemption({ kind, transcriptHead = "" } = {}) {
152
+ if (kind === "collective") return "collective-child-env";
153
+ if (isCollectivePromptTranscript(transcriptHead)) return "collective-child-transcript";
154
+ return null;
155
+ }