@cohortapp/agent-sdk 2.17.0 → 2.18.4

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