@cohortapp/agent-sdk 2.17.0 → 2.18.5

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