@cohortapp/agent-sdk 2.16.0 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +29 -2
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -1,267 +1,58 @@
1
1
  #!/bin/bash
2
- # Pre-send audit hook — gates outbound communications
3
- # Called by Claude Code PreToolUse hook before Slack/Gmail/WhatsApp sends.
4
- # Reads the PreToolUse payload from stdin, screens the outbound CONTENT
5
- # against the banned-phrase list (policies/communication-style.md), runs a
6
- # recipient allowlist sanity check, and enforces hourly + daily rate caps.
2
+ # Pre-send audit hook — gates outbound communications.
3
+ # Called by Claude Code PreToolUse hook before Slack/Gmail/WhatsApp/Cohort sends.
7
4
  #
8
5
  # Usage: pre-send-audit.sh <channel_type>
9
6
  # channel_type: slack | slack-draft | gmail | whatsapp | cohort
10
7
  #
11
8
  # `cohort` is the PreToolUse channel `maestro global-setup` wires into
12
9
  # ~/.claude/settings.json for EVERY mcp__cohort outbound tool (messaging_send,
13
- # messaging_send_voice_note, email_send, email_draft_send, org_rpc) — the
14
- # persona gate below (section a2) is what the front-door session relies on to
15
- # never expose its own machinery to a human (design spec 2026-09-08 §3.6). The
16
- # same patterns run in-process in lib/comms/send-gate.mjs (screenPersona) for
17
- # the lanes no hook fronts: `maestro inbox reply`, the daemon's delivery, the
18
- # adapters. Keep the two lists in sync.
10
+ # messaging_send_voice_note, email_send, email_draft_send, org_rpc).
19
11
  #
20
- # Stdin: Claude Code PreToolUse JSON payload:
21
- # { "tool_name": "...", "tool_input": { "text"|"message"|"body"|"bodyText": "...",
22
- # "subject": "...", "params": { …an org_rpc body… },
23
- # "to"|"recipient"|"channel": "..." }, ... }
12
+ # THIN WRAPPER. Every rule this script used to carry in bash + jq — the banned
13
+ # AI-tell phrases (policies/communication-style.md), the filler openers, the
14
+ # persona gate (PERSONA_PATTERNS and internal session names: <first>-main,
15
+ # state/session/peers.json, MAESTRO_SESSION_NAMES), the recipient allowlist
16
+ # (MAESTRO_SEND_ALLOWLIST) and the hourly/daily caps (MAESTRO_SEND_PER_HOUR /
17
+ # MAESTRO_SEND_PER_DAY, counters under MAESTRO_RUNTIME_DIR) — now runs in
18
+ # lib/comms/send-gate.mjs (`--pre-send-hook`), the module every in-process send
19
+ # path already shares. One implementation means the Claude Code hook and a
20
+ # native engine gate cannot drift. scripts/hooks/pre-send-audit.parity.test.mjs
21
+ # proves the verdicts, reasons and counters match the previous shell script
22
+ # (kept as scripts/hooks/fixtures/pre-send-audit.legacy.sh) over the hook's
23
+ # test corpus.
24
24
  #
25
- # Exit codes:
26
- # 0 = allowed (stdout message shown to Claude)
27
- # 2 = blocked / any parse|IO error (stderr reason shown as rejection).
28
- # Claude Code blocks the tool call on any non-zero exit, so EVERY
29
- # failure path below intentionally fails CLOSED with exit 2.
25
+ # Stdin: the Claude Code PreToolUse JSON payload, passed through untouched.
30
26
  #
31
- # Design note: this script does NOT use `set -e`. A non-zero from a grep
32
- # (no match) or an arithmetic expansion must not silently abort the script
33
- # into an implicit success/fail-open state. Instead every gate is checked
34
- # explicitly and the script ends with `exit 0` only after all checks pass.
27
+ # Exit codes:
28
+ # 0 = allowed (stdout shown to Claude)
29
+ # 2 = blocked (stderr reason shown as the rejection)
30
+ # Claude Code does NOT block on exit 1, so every failure here — no node, no
31
+ # gate module, a crash inside the gate — is mapped to exit 2 (fail closed).
35
32
 
36
33
  set -uo pipefail
37
34
 
38
- # --- fail-closed helper -----------------------------------------------------
39
35
  deny() {
40
- # $1 = human-readable reason
41
36
  echo "BLOCKED: ${1:-pre-send audit failed}" >&2
42
37
  exit 2
43
38
  }
44
39
 
45
- CHANNEL_TYPE="${1:-}"
46
- [ -n "$CHANNEL_TYPE" ] || deny "Channel type required (slack|slack-draft|gmail|whatsapp|cohort)"
47
-
48
- # jq is mandatory — without it we cannot inspect content, so refuse to send.
49
- command -v jq >/dev/null 2>&1 || deny "jq not available — cannot screen outbound content (fail closed)"
50
-
51
- # --- read stdin payload -----------------------------------------------------
52
- TOOL_INPUT=$(cat)
53
- [ -n "$TOOL_INPUT" ] || deny "Empty hook payload — nothing to audit (fail closed)"
54
-
55
- # Validate that the payload is well-formed JSON before doing anything else.
56
- if ! printf '%s' "$TOOL_INPUT" | jq -e . >/dev/null 2>&1; then
57
- deny "Malformed hook payload (not valid JSON) — fail closed"
58
- fi
59
-
60
- # Extract the outbound text. Claude Code wraps the tool args under
61
- # tool_input; for robustness we also look at the top level. We concatenate
62
- # every plausible content field so a banned phrase anywhere is caught.
63
- # `params` is where org_rpc carries an outbound body; `bodyText`/`subject` are
64
- # the mail-desk (email_draft_send / email_send) content fields.
65
- CONTENT=$(printf '%s' "$TOOL_INPUT" | jq -r '
66
- [ .tool_input // {} , ( .tool_input.params? | if type=="object" then . else {} end ) , . ]
67
- | map(
68
- ( .text? // empty ),
69
- ( .message? // empty ),
70
- ( .body? // empty ),
71
- ( .bodyText? // empty ),
72
- ( .subject? // empty ),
73
- ( .content? // empty ),
74
- ( .blocks? | if type=="array" or type=="object" then tojson else empty end )
75
- )
76
- | map(if type=="string" then . else tojson end)
77
- | flatten
78
- | map(select(. != null and . != ""))
79
- | join("\n")
80
- ' 2>/dev/null) || deny "Failed to parse outbound content (fail closed)"
81
-
82
- # Extract the recipient for the allowlist sanity check (optional field).
83
- RECIPIENT=$(printf '%s' "$TOOL_INPUT" | jq -r '
84
- ( .tool_input // {} ) as $ti
85
- | ( $ti.to? // $ti.recipient? // $ti.channel? // $ti.email? // $ti.thread_ts? // .to? // .recipient? // empty )
86
- | if . == null then "" else (. | tostring) end
87
- ' 2>/dev/null) || deny "Failed to parse recipient (fail closed)"
88
-
89
- # --- (a) banned-phrase screen ----------------------------------------------
90
- # Source of truth: policies/communication-style.md (kept in sync manually).
91
- # Matching is case-insensitive and substring-based. Opener phrases
92
- # ("Certainly!" / "Absolutely!") are common in legitimate prose mid-sentence,
93
- # so we anchor those to the start of the content / a line.
94
- BANNED_SUBSTRINGS=(
95
- "I'm here to help"
96
- "I am here to help"
97
- "Let me know if you need anything else"
98
- "I hope this helps"
99
- "Feel free to ask if you have any questions"
100
- "As an AI"
101
- "As a language model"
102
- "I should clarify that I am"
103
- "Great question!"
104
- "Here's a comprehensive overview of"
105
- "Here is a comprehensive overview of"
106
- "I'd be happy to"
107
- "I would be happy to"
108
- )
40
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" || deny "cannot locate the hook directory (fail closed)"
41
+ GATE="$HERE/../../lib/comms/send-gate.mjs"
42
+ [ -f "$GATE" ] || deny "send gate module missing at $GATE (fail closed)"
109
43
 
110
- # Lowercased copy for case-insensitive substring matching.
111
- CONTENT_LC=$(printf '%s' "$CONTENT" | tr '[:upper:]' '[:lower:]')
112
-
113
- for phrase in "${BANNED_SUBSTRINGS[@]}"; do
114
- phrase_lc=$(printf '%s' "$phrase" | tr '[:upper:]' '[:lower:]')
115
- case "$CONTENT_LC" in
116
- *"$phrase_lc"*)
117
- deny "Banned AI-tell phrase in outbound message: \"$phrase\" (see policies/communication-style.md)"
118
- ;;
119
- esac
120
- done
121
-
122
- # Opener-only phrases: block when they begin the message or any line.
123
- # (Mid-sentence "absolutely" is fine; "Absolutely!" as a line opener is not.)
124
- if printf '%s' "$CONTENT" | grep -Eiq '(^|[[:cntrl:]])[[:space:]]*(Certainly!|Absolutely!|Great question!)'; then
125
- deny "Banned filler opener (Certainly!/Absolutely!/Great question!) — see policies/communication-style.md"
126
- fi
127
-
128
- # --- (a2) persona-leak screen ----------------------------------------------
129
- # The agent is one persona to the humans it talks to; parallel sessions,
130
- # sub-sessions, subagents and workflows are internal workings. Outbound text
131
- # may not name them, and may not name Claude Code / a language model. The
132
- # denial carries the offending phrase so the session rewrites in its own voice
133
- # ("my team", "a colleague", "one of my analysts").
134
- #
135
- # Patterns are case-insensitive extended regexes. "subagent"/"sub-agent" is
136
- # blocked only when it is followed by said|reported|session — the bare noun in
137
- # an internal-process sentence is tolerated, the attribution of speech to a
138
- # machine part is not.
139
- PERSONA_PATTERNS=(
140
- 'claude[[:space:]]+code'
141
- 'claude[[:space:]]+sessions?'
142
- 'sub-sessions?'
143
- 'sub-?agents?[[:space:]]+(said|reported|sessions?)'
144
- 'workflow[[:space:]]+agents?'
145
- 'as[[:space:]]+an[[:space:]]+ai\b'
146
- 'language[[:space:]]+model'
147
- )
148
- for pat in "${PERSONA_PATTERNS[@]}"; do
149
- hit=$(printf '%s' "$CONTENT" | grep -Eio -- "$pat" 2>/dev/null | head -n 1) || true
150
- if [ -n "$hit" ]; then
151
- deny "Persona leak in outbound message: \"$hit\" — rewrite in your own voice (my team / a colleague), never name the machinery"
152
- fi
153
- done
154
-
155
- # Session names are internal identifiers and must never reach a human — but
156
- # the rule is BOUNDED, not guessed: `<first>-main` is always internal; any
157
- # other name is internal only when it is a registered peer
158
- # ($AGENT_ROOT/state/session/peers.json, written by `maestro session spawn`,
159
- # an array or {peers:[…]} of {name,…}) or listed in MAESTRO_SESSION_NAMES
160
- # (comma-separated). A colleague called Marc-Antoine, the slug james-kirkland
161
- # in a URL or the mailbox ethan-miller@ are ordinary text.
162
- #
163
- # The first name resolves, in order, from MAESTRO_AGENT_FIRST,
164
- # $AGENT_ROOT/config/agent.json (firstName, else the first word of fullName)
165
- # and the machine pointer ~/.claude/maestro-agent.json (agentName). When none
166
- # resolves the `<first>-main` check is skipped — the phrase gate above still
167
- # applies — because a wrong guess would block legitimate text.
168
- FIRST="${MAESTRO_AGENT_FIRST:-}"
169
- if [ -z "$FIRST" ]; then
170
- for cand in "${AGENT_ROOT:-}/config/agent.json" "${AGENT_DIR:-}/config/agent.json" "$HOME/.claude/maestro-agent.json"; do
171
- [ -f "$cand" ] || continue
172
- FIRST=$(jq -r '(.firstName // .agentName // .fullName // "") | tostring | split(" ")[0] | ascii_downcase' "$cand" 2>/dev/null) || FIRST=""
173
- [ -n "$FIRST" ] && [ "$FIRST" != "null" ] && break
174
- FIRST=""
44
+ NODE="$(command -v node 2>/dev/null || true)"
45
+ if [ -z "$NODE" ]; then
46
+ # launchd and some hook runners carry a bare PATH.
47
+ for cand in /opt/homebrew/bin/node /usr/local/bin/node "${HOME:-}/.local/bin/node" /usr/bin/node; do
48
+ if [ -x "$cand" ]; then NODE="$cand"; break; fi
175
49
  done
176
50
  fi
177
- FIRST=$(printf '%s' "$FIRST" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9-')
178
-
179
- SESSION_NAMES=()
180
- [ -n "$FIRST" ] && SESSION_NAMES+=("${FIRST}-main")
181
- for peers in "${AGENT_ROOT:-}/state/session/peers.json" "${AGENT_DIR:-}/state/session/peers.json"; do
182
- [ -f "$peers" ] || continue
183
- # A corrupt registry is an empty one: this must never fail the whole send closed.
184
- while IFS= read -r pn; do
185
- [ -n "$pn" ] && SESSION_NAMES+=("$pn")
186
- done < <(jq -r '(if type=="array" then . elif type=="object" then (.peers // []) else [] end) | .[] | select(type=="object") | (.name // empty) | tostring | ascii_downcase' "$peers" 2>/dev/null || true)
187
- break
188
- done
189
- if [ -n "${MAESTRO_SESSION_NAMES:-}" ]; then
190
- IFS=',' read -r -a extra_names <<< "$MAESTRO_SESSION_NAMES"
191
- # bash 3.2 (macOS) + set -u: an empty array expands as "unbound" — use the ${a[@]+"${a[@]}"} idiom.
192
- for en in ${extra_names[@]+"${extra_names[@]}"}; do
193
- en=$(printf '%s' "$en" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
194
- [ -n "$en" ] && SESSION_NAMES+=("$en")
195
- done
196
- fi
197
- for sname in ${SESSION_NAMES[@]+"${SESSION_NAMES[@]}"}; do
198
- sname_re=$(printf '%s' "$sname" | sed 's/[.[\*^$()+?{|]/\\&/g')
199
- # Word-bounded on [A-Za-z0-9_-]: "alex-main" hits, "alex-mainland" / "alex-main-street" do not.
200
- name_hit=$(printf '%s' "$CONTENT" | grep -Eio -- "(^|[^[:alnum:]_-])${sname_re}([^[:alnum:]_-]|$)" 2>/dev/null | head -n 1 | sed -E 's/^[^[:alnum:]_-]//; s/[^[:alnum:]_-]$//') || true
201
- if [ -n "$name_hit" ]; then
202
- deny "Persona leak in outbound message: \"$name_hit\" is an internal session name — say \"my team\" or \"a colleague\" instead"
203
- fi
204
- done
205
-
206
- # --- (b) recipient allowlist sanity check ----------------------------------
207
- # Only enforced when a recipient is present AND an allowlist file exists.
208
- # Allowlist file: one entry (channel id, email, handle) per line, '#' comments.
209
- ALLOWLIST_FILE="${MAESTRO_SEND_ALLOWLIST:-}"
210
- if [ -n "$RECIPIENT" ] && [ -n "$ALLOWLIST_FILE" ] && [ -f "$ALLOWLIST_FILE" ]; then
211
- if ! grep -Eq -- "^[[:space:]]*$(printf '%s' "$RECIPIENT" | sed 's/[.[\*^$()+?{|]/\\&/g')[[:space:]]*(#.*)?$" "$ALLOWLIST_FILE" 2>/dev/null; then
212
- deny "Recipient \"$RECIPIENT\" not on send allowlist ($ALLOWLIST_FILE)"
213
- fi
214
- fi
215
-
216
- # --- (c) rate caps (hourly + daily) ----------------------------------------
217
- RUNTIME_DIR="${MAESTRO_RUNTIME_DIR:-$HOME/.maestro/runtime}/audit"
218
- if ! mkdir -p "$RUNTIME_DIR" 2>/dev/null; then
219
- deny "Cannot create audit runtime dir $RUNTIME_DIR (fail closed)"
220
- fi
221
-
222
- COUNTER_FILE="$RUNTIME_DIR/send-counter.tsv"
223
- TODAY=$(date +%Y-%m-%d)
224
- HOUR=$(date +%H)
225
- TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
226
- SEND_LOG="$RUNTIME_DIR/$TODAY-sends.jsonl"
227
-
228
- PER_HOUR_CAP="${MAESTRO_SEND_PER_HOUR:-3000}"
229
- PER_DAY_CAP="${MAESTRO_SEND_PER_DAY:-20000}"
230
-
231
- # Counter format (TSV): date<TAB>hour<TAB>hour_count<TAB>day_count
232
- # Reset whenever the date rolls over; reset the hour bucket when hour rolls.
233
- PREV_DATE=""; PREV_HOUR=""; HOUR_COUNT=0; DAY_COUNT=0
234
- if [ -f "$COUNTER_FILE" ]; then
235
- IFS=$'\t' read -r PREV_DATE PREV_HOUR HOUR_COUNT DAY_COUNT < "$COUNTER_FILE" 2>/dev/null || true
236
- fi
237
- # Sanitise to integers (fail closed to 0 rather than aborting).
238
- case "$HOUR_COUNT" in (''|*[!0-9]*) HOUR_COUNT=0 ;; esac
239
- case "$DAY_COUNT" in (''|*[!0-9]*) DAY_COUNT=0 ;; esac
240
-
241
- if [ "$PREV_DATE" != "$TODAY" ]; then
242
- DAY_COUNT=0
243
- HOUR_COUNT=0
244
- elif [ "$PREV_HOUR" != "$HOUR" ]; then
245
- HOUR_COUNT=0
246
- fi
247
-
248
- if [ "$DAY_COUNT" -ge "$PER_DAY_CAP" ]; then
249
- deny "Daily send limit ($PER_DAY_CAP) reached. Sends resume tomorrow."
250
- fi
251
- if [ "$HOUR_COUNT" -ge "$PER_HOUR_CAP" ]; then
252
- deny "Hourly send limit ($PER_HOUR_CAP) reached. Sends resume next hour."
253
- fi
254
-
255
- # --- commit: increment counters + log allowed send -------------------------
256
- HOUR_COUNT=$((HOUR_COUNT + 1))
257
- DAY_COUNT=$((DAY_COUNT + 1))
258
- if ! printf '%s\t%s\t%s\t%s\n' "$TODAY" "$HOUR" "$HOUR_COUNT" "$DAY_COUNT" > "$COUNTER_FILE" 2>/dev/null; then
259
- deny "Cannot write send counter (fail closed)"
260
- fi
261
-
262
- RECIPIENT_JSON=$(printf '%s' "$RECIPIENT" | jq -Rs . 2>/dev/null || printf '""')
263
- printf '{"timestamp":"%s","channel":"%s","recipient":%s,"status":"allowed"}\n' \
264
- "$TIMESTAMP" "$CHANNEL_TYPE" "$RECIPIENT_JSON" >> "$SEND_LOG" 2>/dev/null || true
51
+ [ -n "$NODE" ] || deny "node not available — cannot screen outbound content (fail closed)"
265
52
 
266
- echo "Audit: $CHANNEL_TYPE send screened and logged at $TIMESTAMP (hour: $HOUR_COUNT/$PER_HOUR_CAP, day: $DAY_COUNT/$PER_DAY_CAP)"
267
- exit 0
53
+ "$NODE" "$GATE" --pre-send-hook "${1:-}"
54
+ rc=$?
55
+ case "$rc" in
56
+ 0|2) exit "$rc" ;;
57
+ *) echo "BLOCKED: pre-send gate exited $rc (fail closed)" >&2; exit 2 ;;
58
+ esac
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pre-write-yaml-validate.mjs — PreToolUse gate on Write|Edit for agent state YAML.
4
+ *
5
+ * Why: the queue files under state/ are the agent's memory of what it owes whom. Three
6
+ * failure shapes have each cost a seat hours: (1) a Write that replaced a 119-row queue with
7
+ * the 6 rows the writer had in view (a clobber); (2) an Edit whose old_string matched twice
8
+ * and landed on the wrong row; (3) a plain scalar carrying an unquoted " #" that parsed
9
+ * cleanly and silently lost the rest of the value — the only trace is a provenance field
10
+ * ending on a dangling connector ("... @mention in"). All three pass a syntax check after the
11
+ * fact; only a gate that sees the WOULD-BE content before the write can refuse them.
12
+ *
13
+ * Contract (Claude Code PreToolUse): reads the hook payload on stdin —
14
+ * { tool_name, tool_input: { file_path, content | old_string, new_string, replace_all } }
15
+ * Acts only on paths under state/queues, state/dashboards or state/indexes ending in .yaml
16
+ * (.yml). Computes the would-be file in memory (Write: `content`; Edit: the on-disk file with
17
+ * old_string replaced by new_string) and REFUSES — exit 2, a permissionDecision:"deny" JSON on
18
+ * stdout and the reason on stderr — when:
19
+ * - an Edit's old_string is absent from the file, or matches more than once without
20
+ * replace_all (the edit would land somewhere the writer did not look at);
21
+ * - the result does not parse as YAML (js-yaml, which also rejects duplicate keys);
22
+ * - a provenance-keyed value NEW in this write ends on a dangling connector. The delta is
23
+ * judged, not the whole file, so a pre-existing truncated row never blocks an unrelated
24
+ * write — the PostToolUse backstop (validate-state-yaml.sh) still reports it;
25
+ * - the top-level `items:` row count would shrink versus the file on disk (or a top-level
26
+ * sequence would get shorter), unless MAESTRO_ALLOW_ROW_SHRINK=1 is set for a deliberate
27
+ * archive/prune. Other list keys (dashboard sections, index tables) are regenerated and
28
+ * legitimately shrink, so they are not guarded.
29
+ * Everything else exits 0. Tooling gaps (no stdin, unparseable payload, js-yaml missing)
30
+ * fail OPEN with a note on stderr: a gate that strands every write on a missing dependency
31
+ * costs more than the clobber it prevents. Paths are resolved against $CLAUDE_PROJECT_DIR,
32
+ * else the working directory.
33
+ *
34
+ * The evaluation is a pure function (`evaluate`) so it is unit-tested without a harness;
35
+ * the CLI wrapper at the bottom only does stdin/stdout/exit.
36
+ *
37
+ * @module scripts/hooks/pre-write-yaml-validate
38
+ */
39
+
40
+ import { readFileSync, existsSync } from "node:fs";
41
+ import { resolve, isAbsolute } from "node:path";
42
+ import { createRequire } from "node:module";
43
+ import { fileURLToPath, pathToFileURL } from "node:url";
44
+
45
+ /** Path segment test: any .yaml under one of the three state-bearing directories. */
46
+ export const SCOPE_RE = /(^|\/)state\/(queues|dashboards|indexes)\/[^/]+\.ya?ml$/;
47
+
48
+ /** Keys whose values name where a row came from or who owns it — short, so the connector test is close to exact. */
49
+ export const PROVENANCE_KEYS = Object.freeze([
50
+ "origin", "source", "source_ref", "sourceRef", "raised_by", "counterparty", "channel", "via",
51
+ "provenance", "escalated_to", "reported_by", "requested_by", "decided_by", "decision_owner",
52
+ "owner", "reviewer", "notify",
53
+ ]);
54
+ const PROV_SET = new Set(PROVENANCE_KEYS);
55
+
56
+ /** A value ending on one of these is the signature of a comment-eaten scalar. */
57
+ export const CONNECTOR_RE = /\s(in|to|at|on|from|with|the|and|of|for|by)$/i;
58
+
59
+ /** Is this file_path one the gate cares about? */
60
+ export function inScope(filePath) {
61
+ if (typeof filePath !== "string" || !filePath) return false;
62
+ return SCOPE_RE.test(filePath.replace(/\\/g, "/"));
63
+ }
64
+
65
+ /**
66
+ * Load js-yaml from wherever the agent has it: next to this script (the agent repo's own
67
+ * node_modules), the project dir, or the working directory. Returns null when none resolves.
68
+ */
69
+ export function loadYaml(bases = []) {
70
+ const candidates = [
71
+ fileURLToPath(new URL("./", import.meta.url)),
72
+ ...bases.filter(Boolean),
73
+ process.cwd(),
74
+ ];
75
+ for (const base of candidates) {
76
+ try {
77
+ const req = createRequire(pathToFileURL(base.endsWith("/") ? base : `${base}/`));
78
+ return req("js-yaml");
79
+ } catch { /* try the next base */ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /** Walk a parsed document and collect every provenance-keyed string ending on a connector. */
85
+ export function provenanceHits(doc) {
86
+ const hits = [];
87
+ // A nested map (history entry, sub-object) reports the nearest enclosing row id.
88
+ const walk = (node, inherited) => {
89
+ if (Array.isArray(node)) { for (const it of node) walk(it, inherited); return; }
90
+ if (!node || typeof node !== "object") return;
91
+ const rid = typeof node.id === "string" ? node.id : inherited;
92
+ for (const [k, v] of Object.entries(node)) {
93
+ if (typeof v === "string") {
94
+ if (PROV_SET.has(k) && v.length < 300 && CONNECTOR_RE.test(v.trimEnd())) hits.push({ id: rid, key: k, value: v });
95
+ } else if (v && typeof v === "object") {
96
+ walk(v, rid);
97
+ }
98
+ }
99
+ };
100
+ walk(doc, "?");
101
+ return hits;
102
+ }
103
+
104
+ /** Row count the shrink guard protects: a top-level `items:` list, or a top-level sequence. */
105
+ export function guardedRowCount(doc) {
106
+ if (Array.isArray(doc)) return doc.length;
107
+ if (doc && typeof doc === "object" && Array.isArray(doc.items)) return doc.items.length;
108
+ return null;
109
+ }
110
+
111
+ /** Count non-overlapping occurrences of `needle` in `hay`. */
112
+ function countOccurrences(hay, needle) {
113
+ if (!needle) return 0;
114
+ let n = 0;
115
+ let i = 0;
116
+ for (;;) {
117
+ const at = hay.indexOf(needle, i);
118
+ if (at < 0) return n;
119
+ n += 1;
120
+ i = at + needle.length;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Compute the would-be content for the tool call. Returns { content } or { refuse }.
126
+ * @param {string} toolName
127
+ * @param {object} ti tool_input
128
+ * @param {string|null} current on-disk content (null when the file does not exist)
129
+ */
130
+ export function wouldBeContent(toolName, ti, current) {
131
+ if (toolName === "Write") {
132
+ return { content: typeof ti.content === "string" ? ti.content : "" };
133
+ }
134
+ if (toolName === "Edit") {
135
+ if (current === null) return { refuse: "Edit on a file that does not exist" };
136
+ const oldStr = ti.old_string;
137
+ const newStr = typeof ti.new_string === "string" ? ti.new_string : "";
138
+ if (typeof oldStr !== "string" || oldStr === "") return { refuse: "Edit with an empty old_string" };
139
+ const n = countOccurrences(current, oldStr);
140
+ if (n === 0) return { refuse: "old_string is not present in the file on disk — the edit would not land where you looked" };
141
+ if (n > 1 && !ti.replace_all) return { refuse: `old_string matches ${n} places — anchor it to one row or pass replace_all` };
142
+ return { content: ti.replace_all ? current.split(oldStr).join(newStr) : current.replace(oldStr, () => newStr) };
143
+ }
144
+ if (toolName === "MultiEdit" && Array.isArray(ti.edits)) {
145
+ let content = current === null ? "" : current;
146
+ for (const e of ti.edits) {
147
+ const r = wouldBeContent("Edit", e || {}, content);
148
+ if (r.refuse) return r;
149
+ content = r.content;
150
+ }
151
+ return { content };
152
+ }
153
+ return { content: null };
154
+ }
155
+
156
+ /**
157
+ * Pure evaluation of one hook payload.
158
+ *
159
+ * @param {object} payload the hook JSON ({tool_name, tool_input})
160
+ * @param {object} [o]
161
+ * @param {string} [o.projectDir] base for relative file_path (default: cwd)
162
+ * @param {object} [o.env] environment (default: process.env)
163
+ * @param {(p:string)=>string|null} [o.readFile] test seam; returns null when absent
164
+ * @param {object} [o.yaml] js-yaml module (test seam)
165
+ * @returns {{ decision:"allow"|"deny"|"skip", reason?:string }}
166
+ */
167
+ export function evaluate(payload, o = {}) {
168
+ const env = o.env || process.env;
169
+ const projectDir = o.projectDir || env.CLAUDE_PROJECT_DIR || process.cwd();
170
+ if (!payload || typeof payload !== "object") return { decision: "skip", reason: "no payload" };
171
+ const toolName = String(payload.tool_name || "");
172
+ const ti = payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {};
173
+ if (!["Write", "Edit", "MultiEdit"].includes(toolName)) return { decision: "skip" };
174
+ const fp = typeof ti.file_path === "string" ? ti.file_path : "";
175
+ if (!inScope(fp)) return { decision: "skip" };
176
+ const abs = isAbsolute(fp) ? fp : resolve(projectDir, fp);
177
+
178
+ const readFile = o.readFile || ((p) => (existsSync(p) ? readFileSync(p, "utf-8") : null));
179
+ let current = null;
180
+ try { current = readFile(abs); } catch { current = null; }
181
+
182
+ const wb = wouldBeContent(toolName, ti, current);
183
+ if (wb.refuse) return { decision: "deny", reason: `${toolName} ${fp}: ${wb.refuse}` };
184
+ if (wb.content === null) return { decision: "skip" };
185
+
186
+ const yaml = o.yaml || loadYaml([projectDir]);
187
+ if (!yaml) return { decision: "skip", reason: "js-yaml not resolvable — gate skipped (fail-open)" };
188
+
189
+ let next;
190
+ try {
191
+ next = yaml.load(wb.content);
192
+ } catch (err) {
193
+ const msg = (err && err.message ? err.message : String(err)).split("\n")[0].slice(0, 300);
194
+ return { decision: "deny", reason: `${toolName} ${fp}: the result would not parse as YAML — ${msg}. Quote scalars carrying ':' or ' #', and never append a second copy of a key.` };
195
+ }
196
+
197
+ let prev;
198
+ let prevParsed = false;
199
+ if (current !== null) {
200
+ try { prev = yaml.load(current); prevParsed = true; } catch { prevParsed = false; }
201
+ }
202
+
203
+ // Provenance truncation — judged on the DELTA so pre-existing rows never block a new write.
204
+ const before = new Set(prevParsed ? provenanceHits(prev).map((h) => `${h.id}\u0000${h.key}\u0000${h.value}`) : []);
205
+ const fresh = provenanceHits(next).filter((h) => !before.has(`${h.id}\u0000${h.key}\u0000${h.value}`));
206
+ if (fresh.length) {
207
+ const shown = fresh.slice(0, 5).map((h) => `${h.id} ${h.key}: ...${h.value.slice(-60)}`).join("; ");
208
+ return {
209
+ decision: "deny",
210
+ reason: `${toolName} ${fp}: provenance field ends on a dangling connector (an unquoted ' #' silently truncates a plain scalar) — ${shown}. Quote the scalar and write the whole value.`,
211
+ };
212
+ }
213
+
214
+ // Row-count shrink guard.
215
+ if (prevParsed && env.MAESTRO_ALLOW_ROW_SHRINK !== "1") {
216
+ const was = guardedRowCount(prev);
217
+ const now = guardedRowCount(next);
218
+ if (was !== null && now !== null && now < was) {
219
+ return {
220
+ decision: "deny",
221
+ reason: `${toolName} ${fp}: top-level items would shrink from ${was} to ${now} rows — a write that drops rows is almost always a clobber of rows you did not have in view. Re-read the file and edit the one row, or set MAESTRO_ALLOW_ROW_SHRINK=1 for a deliberate archive.`,
222
+ };
223
+ }
224
+ }
225
+
226
+ return { decision: "allow" };
227
+ }
228
+
229
+ /** Claude Code hook output for a refusal (exit 2 + this JSON on stdout). */
230
+ export function denyOutput(reason) {
231
+ return {
232
+ decision: "block",
233
+ reason,
234
+ hookSpecificOutput: {
235
+ hookEventName: "PreToolUse",
236
+ permissionDecision: "deny",
237
+ permissionDecisionReason: reason,
238
+ },
239
+ };
240
+ }
241
+
242
+ async function readStdin() {
243
+ const chunks = [];
244
+ for await (const c of process.stdin) chunks.push(c);
245
+ return Buffer.concat(chunks).toString("utf-8");
246
+ }
247
+
248
+ async function main() {
249
+ let payload;
250
+ try {
251
+ const raw = await readStdin();
252
+ if (!raw.trim()) process.exit(0);
253
+ payload = JSON.parse(raw);
254
+ } catch (err) {
255
+ process.stderr.write(`pre-write-yaml-validate: unreadable hook payload (${err && err.message}) — gate skipped\n`);
256
+ process.exit(0);
257
+ }
258
+ let verdict;
259
+ try {
260
+ verdict = evaluate(payload);
261
+ } catch (err) {
262
+ process.stderr.write(`pre-write-yaml-validate: internal error (${err && err.message}) — gate skipped\n`);
263
+ process.exit(0);
264
+ }
265
+ if (verdict.decision === "deny") {
266
+ process.stdout.write(`${JSON.stringify(denyOutput(verdict.reason))}\n`);
267
+ process.stderr.write(`BLOCKED by pre-write-yaml-validate: ${verdict.reason}\n`);
268
+ process.exit(2);
269
+ }
270
+ if (verdict.decision === "skip" && verdict.reason) process.stderr.write(`pre-write-yaml-validate: ${verdict.reason}\n`);
271
+ process.exit(0);
272
+ }
273
+
274
+ const invokedDirectly = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
275
+ if (invokedDirectly) main();