@cohortapp/agent-sdk 2.3.0

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 (731) hide show
  1. package/.claude/commands/init-agent.md +104 -0
  2. package/.claude/commands/init-maestro.md +1187 -0
  3. package/.claude/settings.json +161 -0
  4. package/.env.example +216 -0
  5. package/README.md +632 -0
  6. package/agents/browser-operator/agent.md +52 -0
  7. package/agents/calendar-ops/agent.md +50 -0
  8. package/agents/communications/agent.md +96 -0
  9. package/agents/decision-log/agent.md +65 -0
  10. package/agents/desktop-operator/agent.md +59 -0
  11. package/agents/gmail-operator/agent.md +62 -0
  12. package/agents/inbound-dispatcher/agent.md +66 -0
  13. package/agents/inbox-processor/agent.md +39 -0
  14. package/agents/pmo-execution/agent.md +60 -0
  15. package/agents/session-spawner/agent.md +64 -0
  16. package/agents/slack-operator/agent.md +60 -0
  17. package/agents/whatsapp-operator/agent.md +60 -0
  18. package/agents/workflow-automation/agent.md +61 -0
  19. package/archetypes/altitudes/c-suite.yaml +58 -0
  20. package/archetypes/altitudes/founder.yaml +68 -0
  21. package/archetypes/altitudes/senior-manager.yaml +63 -0
  22. package/archetypes/altitudes/svp.yaml +60 -0
  23. package/archetypes/altitudes/vp.yaml +50 -0
  24. package/archetypes/archetype.schema.json +77 -0
  25. package/archetypes/base.yaml +47 -0
  26. package/archetypes/capabilities/commercial-leader.yaml +159 -0
  27. package/archetypes/capabilities/compliance-officer.yaml +159 -0
  28. package/archetypes/capabilities/executive-operator.yaml +169 -0
  29. package/archetypes/capabilities/finance-leader.yaml +162 -0
  30. package/archetypes/capabilities/operations-leader.yaml +154 -0
  31. package/archetypes/capabilities/product-leader.yaml +148 -0
  32. package/archetypes/capabilities/technical-leader.yaml +146 -0
  33. package/archetypes/functions/commercial-leader.yaml +62 -0
  34. package/archetypes/functions/compliance-officer.yaml +64 -0
  35. package/archetypes/functions/executive-operator.yaml +70 -0
  36. package/archetypes/functions/finance-leader.yaml +70 -0
  37. package/archetypes/functions/operations-leader.yaml +62 -0
  38. package/archetypes/functions/product-leader.yaml +61 -0
  39. package/archetypes/functions/technical-leader.yaml +57 -0
  40. package/bin/cohort-mcp.mjs +81 -0
  41. package/bin/maestro.mjs +3516 -0
  42. package/bin/maestro.test.mjs +1015 -0
  43. package/desktop-control/README.md +56 -0
  44. package/desktop-control/app-profiles/gmail.yaml +120 -0
  45. package/desktop-control/app-profiles/slack.yaml +315 -0
  46. package/desktop-control/app-profiles/whatsapp.yaml +107 -0
  47. package/docs/architecture/agent-topology.md +2239 -0
  48. package/docs/architecture/archetype-agent-factory.md +110 -0
  49. package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
  50. package/docs/architecture/continuous-monitoring.md +221 -0
  51. package/docs/architecture/mcp-capability-map.md +585 -0
  52. package/docs/architecture/system-architecture.md +1272 -0
  53. package/docs/company-context/README.md +40 -0
  54. package/docs/guides/agent-persona-setup.md +600 -0
  55. package/docs/guides/agents-observe-setup.md +64 -0
  56. package/docs/guides/billing-console-keys.md +88 -0
  57. package/docs/guides/ccxray-diagnostics.md +65 -0
  58. package/docs/guides/channel-bus.md +127 -0
  59. package/docs/guides/claude-mem-setup.md +79 -0
  60. package/docs/guides/claude-pace-setup.md +56 -0
  61. package/docs/guides/claudraband-sessions.md +98 -0
  62. package/docs/guides/clawteam-swarm.md +116 -0
  63. package/docs/guides/code-review-graph-setup.md +86 -0
  64. package/docs/guides/email-setup.md +431 -0
  65. package/docs/guides/mac-mini.md +119 -0
  66. package/docs/guides/media-generation-setup.md +349 -0
  67. package/docs/guides/model-routing.md +162 -0
  68. package/docs/guides/observability-otel.md +265 -0
  69. package/docs/guides/org-onboarding.md +132 -0
  70. package/docs/guides/outbound-governance-setup.md +437 -0
  71. package/docs/guides/pdf-generation-setup.md +315 -0
  72. package/docs/guides/poller-daemon-setup.md +563 -0
  73. package/docs/guides/rag-context-setup.md +459 -0
  74. package/docs/guides/self-optimization-pattern.md +82 -0
  75. package/docs/guides/setup-wizard.md +178 -0
  76. package/docs/guides/slack-setup.md +350 -0
  77. package/docs/guides/telegram-setup.md +227 -0
  78. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  79. package/docs/guides/verification.md +128 -0
  80. package/docs/guides/voice-mode.md +188 -0
  81. package/docs/guides/voice-sms-setup.md +698 -0
  82. package/docs/guides/webhook-relay-setup.md +349 -0
  83. package/docs/guides/whatsapp-setup.md +288 -0
  84. package/docs/prompts/board-pack-cover-template.md +36 -0
  85. package/docs/prompts/decision-recommendation-template.md +88 -0
  86. package/docs/prompts/followup-message-template.md +141 -0
  87. package/docs/prompts/investor-letter-template.md +52 -0
  88. package/docs/prompts/morning-brief-template.md +82 -0
  89. package/docs/prompts/presentation-template.md +58 -0
  90. package/docs/prompts/weekly-strategic-memo-template.md +104 -0
  91. package/docs/research/hallucinated-tool-output-investigation.md +151 -0
  92. package/docs/runbooks/backup-restore.md +205 -0
  93. package/docs/runbooks/cohort-cutover.md +129 -0
  94. package/docs/runbooks/fleet-operations.md +200 -0
  95. package/docs/runbooks/incident-response.md +226 -0
  96. package/docs/runbooks/mac-mini-bootstrap.md +431 -0
  97. package/docs/runbooks/perpetual-operations.md +509 -0
  98. package/docs/runbooks/recovery-and-failover.md +260 -0
  99. package/framework-features.json +267 -0
  100. package/ingest/README.md +87 -0
  101. package/lib/action-executor.js +689 -0
  102. package/lib/action-executor.test.mjs +871 -0
  103. package/lib/agent-root.mjs +37 -0
  104. package/lib/archetype.mjs +236 -0
  105. package/lib/archetype.test.mjs +132 -0
  106. package/lib/autonomy.mjs +114 -0
  107. package/lib/autonomy.test.mjs +66 -0
  108. package/lib/backlog.mjs +358 -0
  109. package/lib/backlog.test.mjs +266 -0
  110. package/lib/budget-guard.mjs +279 -0
  111. package/lib/budget-guard.test.mjs +291 -0
  112. package/lib/cadence-bus-schedule.test.mjs +194 -0
  113. package/lib/cadence-bus.mjs +1120 -0
  114. package/lib/cadence-bus.test.mjs +720 -0
  115. package/lib/cadences.mjs +205 -0
  116. package/lib/cadences.test.mjs +125 -0
  117. package/lib/capability.mjs +154 -0
  118. package/lib/capability.test.mjs +78 -0
  119. package/lib/channels/base-adapter.mjs +719 -0
  120. package/lib/channels/base-adapter.test.mjs +590 -0
  121. package/lib/channels/channel.mjs +128 -0
  122. package/lib/channels/channels.test.mjs +371 -0
  123. package/lib/channels/contract.mjs +215 -0
  124. package/lib/channels/contract.test.mjs +137 -0
  125. package/lib/channels/conversation-resolver.mjs +95 -0
  126. package/lib/channels/gmail/adapter.mjs +87 -0
  127. package/lib/channels/inbox-item.mjs +255 -0
  128. package/lib/channels/inbox-item.test.mjs +335 -0
  129. package/lib/channels/index.mjs +94 -0
  130. package/lib/channels/orgmail/adapter.mjs +353 -0
  131. package/lib/channels/orgmail/adapter.test.mjs +311 -0
  132. package/lib/channels/pairing.mjs +363 -0
  133. package/lib/channels/pairing.test.mjs +270 -0
  134. package/lib/channels/registry.mjs +164 -0
  135. package/lib/channels/slack/adapter.mjs +317 -0
  136. package/lib/channels/slack-adapter.test.mjs +212 -0
  137. package/lib/channels/sms/adapter.mjs +43 -0
  138. package/lib/channels/telegram/adapter.mjs +432 -0
  139. package/lib/channels/telegram-adapter.test.mjs +306 -0
  140. package/lib/channels/voice/adapter.mjs +301 -0
  141. package/lib/channels/voice/adapter.test.mjs +278 -0
  142. package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
  143. package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
  144. package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
  145. package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
  146. package/lib/charter.mjs +256 -0
  147. package/lib/charter.test.mjs +89 -0
  148. package/lib/claude-bin.mjs +134 -0
  149. package/lib/claude-bin.test.mjs +75 -0
  150. package/lib/collective/capture.mjs +185 -0
  151. package/lib/collective/capture.test.mjs +121 -0
  152. package/lib/collective/cards.mjs +201 -0
  153. package/lib/collective/cards.test.mjs +114 -0
  154. package/lib/collective/config.mjs +186 -0
  155. package/lib/collective/config.test.mjs +123 -0
  156. package/lib/collective/global-config.mjs +113 -0
  157. package/lib/collective/global-config.test.mjs +75 -0
  158. package/lib/collective/presence.mjs +201 -0
  159. package/lib/collective/presence.test.mjs +95 -0
  160. package/lib/collective/recall.mjs +215 -0
  161. package/lib/collective/recall.test.mjs +116 -0
  162. package/lib/comms/send-gate.mjs +554 -0
  163. package/lib/comms/send-gate.test.mjs +577 -0
  164. package/lib/comms.mjs +67 -0
  165. package/lib/comms.test.mjs +41 -0
  166. package/lib/diagnostics/alerts.mjs +424 -0
  167. package/lib/diagnostics/alerts.test.mjs +318 -0
  168. package/lib/diagnostics/backup-freshness.mjs +188 -0
  169. package/lib/diagnostics/backup-freshness.test.mjs +185 -0
  170. package/lib/diagnostics/counters.mjs +269 -0
  171. package/lib/diagnostics/counters.test.mjs +206 -0
  172. package/lib/diagnostics/events.mjs +188 -0
  173. package/lib/diagnostics/events.test.mjs +290 -0
  174. package/lib/diagnostics/otel.mjs +237 -0
  175. package/lib/diagnostics/otel.test.mjs +196 -0
  176. package/lib/diagnostics/trace.mjs +216 -0
  177. package/lib/diagnostics/trace.test.mjs +251 -0
  178. package/lib/env-compat.mjs +74 -0
  179. package/lib/env-compat.test.mjs +104 -0
  180. package/lib/feature-init.mjs +331 -0
  181. package/lib/fs-atomic.mjs +112 -0
  182. package/lib/fs-atomic.test.mjs +72 -0
  183. package/lib/fs-ownership.mjs +111 -0
  184. package/lib/fs-ownership.test.mjs +158 -0
  185. package/lib/hooks/bus.mjs +347 -0
  186. package/lib/hooks/bus.test.mjs +387 -0
  187. package/lib/index.js +16 -0
  188. package/lib/learning/config.mjs +106 -0
  189. package/lib/learning/config.test.mjs +75 -0
  190. package/lib/learning/counters.mjs +156 -0
  191. package/lib/learning/counters.test.mjs +69 -0
  192. package/lib/learning/curator-consolidate.test.mjs +238 -0
  193. package/lib/learning/curator.mjs +453 -0
  194. package/lib/learning/curator.test.mjs +106 -0
  195. package/lib/learning/log.mjs +40 -0
  196. package/lib/learning/reflect.mjs +534 -0
  197. package/lib/learning/reflect.test.mjs +0 -0
  198. package/lib/learning/session-index.mjs +352 -0
  199. package/lib/learning/session-index.test.mjs +125 -0
  200. package/lib/learning/skill-writer.mjs +474 -0
  201. package/lib/learning/skill-writer.test.mjs +210 -0
  202. package/lib/mcp/server.mjs +328 -0
  203. package/lib/mcp/server.test.mjs +400 -0
  204. package/lib/model-router/auth-profiles.mjs +758 -0
  205. package/lib/model-router/auth-profiles.test.mjs +580 -0
  206. package/lib/model-router/catalog/anthropic.yaml +153 -0
  207. package/lib/model-router/catalog/deepseek.yaml +86 -0
  208. package/lib/model-router/catalog/moonshot.yaml +81 -0
  209. package/lib/model-router/catalog/qwen.yaml +114 -0
  210. package/lib/model-router/catalog.mjs +925 -0
  211. package/lib/model-router/catalog.test.mjs +385 -0
  212. package/lib/model-router/economics.mjs +564 -0
  213. package/lib/model-router/economics.test.mjs +344 -0
  214. package/lib/model-router/failover.mjs +298 -0
  215. package/lib/model-router/failover.test.mjs +439 -0
  216. package/lib/model-router/health.mjs +453 -0
  217. package/lib/model-router/health.test.mjs +338 -0
  218. package/lib/model-router/integration-coverage.test.mjs +829 -0
  219. package/lib/model-router/integration.test.mjs +564 -0
  220. package/lib/model-router/ledger.mjs +402 -0
  221. package/lib/model-router/ledger.test.mjs +382 -0
  222. package/lib/model-router/llm-task.mjs +515 -0
  223. package/lib/model-router/llm-task.test.mjs +392 -0
  224. package/lib/model-router/org-credentials.mjs +260 -0
  225. package/lib/model-router/org-credentials.test.mjs +265 -0
  226. package/lib/model-router/pricing-refresh.mjs +463 -0
  227. package/lib/model-router/pricing-refresh.test.mjs +286 -0
  228. package/lib/model-router/reconcile.mjs +429 -0
  229. package/lib/model-router/reconcile.test.mjs +316 -0
  230. package/lib/model-router/repair.mjs +471 -0
  231. package/lib/model-router/repair.test.mjs +180 -0
  232. package/lib/model-router/resolve.mjs +1206 -0
  233. package/lib/model-router/spawn.mjs +497 -0
  234. package/lib/model-router/spawn.test.mjs +425 -0
  235. package/lib/model-router/taxonomy.mjs +893 -0
  236. package/lib/model-router/taxonomy.test.mjs +410 -0
  237. package/lib/model-router.mjs +677 -0
  238. package/lib/model-router.test.mjs +907 -0
  239. package/lib/org/activity.mjs +211 -0
  240. package/lib/org/activity.test.mjs +134 -0
  241. package/lib/org/approvals.mjs +448 -0
  242. package/lib/org/approvals.test.mjs +216 -0
  243. package/lib/org/awareness.mjs +222 -0
  244. package/lib/org/awareness.test.mjs +159 -0
  245. package/lib/org/board.mjs +229 -0
  246. package/lib/org/board.test.mjs +177 -0
  247. package/lib/org/bootstrap-context.mjs +169 -0
  248. package/lib/org/bootstrap-context.test.mjs +153 -0
  249. package/lib/org/client.mjs +1628 -0
  250. package/lib/org/client.test.mjs +1107 -0
  251. package/lib/org/cohort-client.mjs +67 -0
  252. package/lib/org/cohort-client.test.mjs +126 -0
  253. package/lib/org/cost-sync.mjs +227 -0
  254. package/lib/org/cost-sync.test.mjs +153 -0
  255. package/lib/org/doctor.mjs +212 -0
  256. package/lib/org/doctor.test.mjs +212 -0
  257. package/lib/org/handoff.mjs +293 -0
  258. package/lib/org/handoff.test.mjs +269 -0
  259. package/lib/org/integration-tools.mjs +182 -0
  260. package/lib/org/integration-tools.test.mjs +160 -0
  261. package/lib/org/keys.mjs +131 -0
  262. package/lib/org/keys.test.mjs +92 -0
  263. package/lib/org/knowledge.mjs +463 -0
  264. package/lib/org/knowledge.test.mjs +319 -0
  265. package/lib/org/leases.mjs +335 -0
  266. package/lib/org/leases.test.mjs +235 -0
  267. package/lib/org/mesh-integration.test.mjs +127 -0
  268. package/lib/org/mesh.mjs +459 -0
  269. package/lib/org/mesh.test.mjs +345 -0
  270. package/lib/org/messaging.mjs +503 -0
  271. package/lib/org/messaging.test.mjs +238 -0
  272. package/lib/org/policy.mjs +345 -0
  273. package/lib/org/policy.test.mjs +237 -0
  274. package/lib/org/protocol.checksum +1 -0
  275. package/lib/org/protocol.checksum.test.mjs +90 -0
  276. package/lib/org/protocol.mjs +967 -0
  277. package/lib/org/protocol.test.mjs +264 -0
  278. package/lib/org/registry.mjs +194 -0
  279. package/lib/org/registry.test.mjs +100 -0
  280. package/lib/org/tool-surface-integration.test.mjs +120 -0
  281. package/lib/org/tool-surface.mjs +2535 -0
  282. package/lib/org/tool-surface.test.mjs +589 -0
  283. package/lib/org/ui-parity.mjs +3236 -0
  284. package/lib/org/ui-parity.test.mjs +348 -0
  285. package/lib/org/verify.mjs +176 -0
  286. package/lib/org/verify.test.mjs +194 -0
  287. package/lib/rag/embed.mjs +188 -0
  288. package/lib/rag/indexer.mjs +425 -0
  289. package/lib/rag/rag.test.mjs +505 -0
  290. package/lib/rag/search.mjs +475 -0
  291. package/lib/rate-guard.mjs +246 -0
  292. package/lib/rate-guard.test.mjs +201 -0
  293. package/lib/render.mjs +112 -0
  294. package/lib/render.test.mjs +68 -0
  295. package/lib/resource-governor.mjs +297 -0
  296. package/lib/resource-governor.test.mjs +262 -0
  297. package/lib/scheduling/dynamic-jobs.mjs +675 -0
  298. package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
  299. package/lib/scheduling/jitter.mjs +0 -0
  300. package/lib/scheduling/jitter.test.mjs +140 -0
  301. package/lib/secrets/broker.mjs +315 -0
  302. package/lib/secrets/broker.test.mjs +280 -0
  303. package/lib/secrets/providers.mjs +461 -0
  304. package/lib/secrets/providers.test.mjs +274 -0
  305. package/lib/security/audit-engine.mjs +684 -0
  306. package/lib/security/audit-engine.test.mjs +389 -0
  307. package/lib/security/coerce-args.mjs +552 -0
  308. package/lib/security/coerce-args.test.mjs +281 -0
  309. package/lib/security/dangerous-tools.mjs +97 -0
  310. package/lib/security/dangerous-tools.test.mjs +68 -0
  311. package/lib/security/external-content.mjs +145 -0
  312. package/lib/security/external-content.test.mjs +67 -0
  313. package/lib/security/redact.mjs +592 -0
  314. package/lib/security/redact.test.mjs +441 -0
  315. package/lib/security/secret-equal.mjs +73 -0
  316. package/lib/security/secret-equal.test.mjs +55 -0
  317. package/lib/session-permissions.mjs +101 -0
  318. package/lib/session-permissions.test.mjs +100 -0
  319. package/lib/setup/claude-probe.mjs +74 -0
  320. package/lib/setup/completeness.mjs +175 -0
  321. package/lib/setup/completeness.test.mjs +110 -0
  322. package/lib/setup/context-pack.mjs +173 -0
  323. package/lib/setup/context-pack.test.mjs +89 -0
  324. package/lib/setup/enrich.mjs +277 -0
  325. package/lib/setup/enrich.test.mjs +115 -0
  326. package/lib/setup/enroll-from-cohort.mjs +441 -0
  327. package/lib/setup/enroll-from-cohort.test.mjs +233 -0
  328. package/lib/setup/integration.test.mjs +162 -0
  329. package/lib/setup/io.mjs +360 -0
  330. package/lib/setup/io.test.mjs +77 -0
  331. package/lib/setup/run-generator.mjs +81 -0
  332. package/lib/setup/runner.mjs +244 -0
  333. package/lib/setup/runner.test.mjs +132 -0
  334. package/lib/setup/sections/comms.mjs +173 -0
  335. package/lib/setup/sections/company.mjs +120 -0
  336. package/lib/setup/sections/enrich.mjs +138 -0
  337. package/lib/setup/sections/identity.mjs +182 -0
  338. package/lib/setup/sections/identity.test.mjs +140 -0
  339. package/lib/setup/sections/learning.mjs +153 -0
  340. package/lib/setup/sections/learning.test.mjs +81 -0
  341. package/lib/setup/sections/messaging.mjs +219 -0
  342. package/lib/setup/sections/messaging.test.mjs +127 -0
  343. package/lib/setup/sections/model.mjs +102 -0
  344. package/lib/setup/sections/operating-model.mjs +78 -0
  345. package/lib/setup/sections/org.mjs +475 -0
  346. package/lib/setup/sections/org.test.mjs +313 -0
  347. package/lib/setup/sections/orgmail.mjs +173 -0
  348. package/lib/setup/sections/orgmail.test.mjs +118 -0
  349. package/lib/setup/sections/recovery.mjs +159 -0
  350. package/lib/setup/sections/recovery.test.mjs +98 -0
  351. package/lib/setup/sections/tools.mjs +132 -0
  352. package/lib/setup/sections/verify.mjs +97 -0
  353. package/lib/setup/sot.mjs +205 -0
  354. package/lib/setup/sot.test.mjs +81 -0
  355. package/lib/setup/state.mjs +151 -0
  356. package/lib/setup/state.test.mjs +92 -0
  357. package/lib/singleton.js +229 -0
  358. package/lib/singleton.test.mjs +135 -0
  359. package/lib/telemetry/alerts.mjs +216 -0
  360. package/lib/telemetry/alerts.test.mjs +109 -0
  361. package/lib/telemetry/collect.mjs +512 -0
  362. package/lib/telemetry/collect.test.mjs +202 -0
  363. package/lib/tool-definitions-integration.test.mjs +83 -0
  364. package/lib/tool-definitions.js +738 -0
  365. package/lib/tool-definitions.test.mjs +437 -0
  366. package/lib/util/fetch-timeout.mjs +136 -0
  367. package/lib/util/fetch-timeout.test.mjs +202 -0
  368. package/lib/util/reconnect.mjs +343 -0
  369. package/lib/util/reconnect.test.mjs +369 -0
  370. package/lib/util/unhandled.mjs +205 -0
  371. package/lib/util/unhandled.test.mjs +216 -0
  372. package/lib/voice/context-loader.mjs +466 -0
  373. package/lib/voice/index.mjs +100 -0
  374. package/lib/voice/openai-realtime.mjs +510 -0
  375. package/lib/voice/outbound.mjs +542 -0
  376. package/lib/voice/outbound.test.mjs +69 -0
  377. package/lib/voice/post-call-brief.mjs +428 -0
  378. package/lib/voice/provider.mjs +52 -0
  379. package/lib/voice/session-rotation.mjs +257 -0
  380. package/lib/voice/stt.mjs +161 -0
  381. package/lib/voice/stt.test.mjs +226 -0
  382. package/lib/voice/tool-bridge.mjs +370 -0
  383. package/lib/voice/tts.mjs +104 -0
  384. package/lib/voice/twilio-sip-bridge.mjs +288 -0
  385. package/lib/voice/voice.test.mjs +990 -0
  386. package/mcp/README.md +80 -0
  387. package/package.json +151 -0
  388. package/plugins/maestro-skills/plugin.json +139 -0
  389. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  390. package/plugins/maestro-skills/skills/board-deck.md +68 -0
  391. package/plugins/maestro-skills/skills/books-close.md +77 -0
  392. package/plugins/maestro-skills/skills/brand-steward.md +121 -0
  393. package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
  394. package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
  395. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  396. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  397. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  398. package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
  399. package/plugins/maestro-skills/skills/decision-brief.md +89 -0
  400. package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
  401. package/plugins/maestro-skills/skills/draft-comms.md +84 -0
  402. package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
  403. package/plugins/maestro-skills/skills/files-find.md +65 -0
  404. package/plugins/maestro-skills/skills/generative-ui.md +228 -0
  405. package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
  406. package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
  407. package/plugins/maestro-skills/skills/mail-triage.md +86 -0
  408. package/plugins/maestro-skills/skills/morning-brief.md +54 -0
  409. package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
  410. package/plugins/maestro-skills/skills/org-board.md +133 -0
  411. package/plugins/maestro-skills/skills/org-credential.md +68 -0
  412. package/plugins/maestro-skills/skills/org-recall.md +81 -0
  413. package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
  414. package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
  415. package/plugins/maestro-skills/skills/router-why.md +78 -0
  416. package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
  417. package/plugins/maestro-skills/skills/session-search.md +71 -0
  418. package/plugins/maestro-skills/skills/set-reminder.md +93 -0
  419. package/plugins/maestro-skills/skills/slack-followup.md +64 -0
  420. package/plugins/maestro-skills/skills/team-activity.md +86 -0
  421. package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
  422. package/policies/action-classification.yaml +114 -0
  423. package/policies/ai-disclosure.yaml +294 -0
  424. package/policies/communication-style.md +139 -0
  425. package/policies/information-barriers.yaml +118 -0
  426. package/policies/prompt-injection-defence.yaml +138 -0
  427. package/public/assets/icon-dark.png +0 -0
  428. package/public/assets/icon-dark.svg +9 -0
  429. package/public/assets/icon-light.svg +9 -0
  430. package/public/assets/logo-dark.svg +15 -0
  431. package/public/assets/logo-light.svg +15 -0
  432. package/scaffold/.mcp.json +7 -0
  433. package/scaffold/CLAUDE.md +368 -0
  434. package/scaffold/config/agent.json +55 -0
  435. package/scaffold/config/agent.ts +76 -0
  436. package/scaffold/config/agent.ts.example +89 -0
  437. package/scaffold/config/alerts.yaml +23 -0
  438. package/scaffold/config/allowlist.yaml.example +25 -0
  439. package/scaffold/config/caller-id-map.yaml +46 -0
  440. package/scaffold/config/collective.yaml +49 -0
  441. package/scaffold/config/company.json +20 -0
  442. package/scaffold/config/known-agents.json +6 -0
  443. package/scaffold/config/learning.yaml +55 -0
  444. package/scaffold/config/model-routing.yaml.example +104 -0
  445. package/scaffold/config/org.yaml +25 -0
  446. package/scaffold/config/orgmail.yaml.example +19 -0
  447. package/scaffold/config/recovery.yaml +72 -0
  448. package/scaffold/config/secrets.yaml +27 -0
  449. package/scaffold/config/slack.yaml.example +35 -0
  450. package/scaffold/config/telegram.yaml.example +38 -0
  451. package/scaffold/config/voice.yaml.example +89 -0
  452. package/scaffold/config/whatsapp.yaml.example +39 -0
  453. package/schedules/README.md +49 -0
  454. package/schedules/triggers/backlog-executor.md +102 -0
  455. package/schedules/triggers/brand-steward.md +72 -0
  456. package/schedules/triggers/daily-evening-wrap.md +159 -0
  457. package/schedules/triggers/daily-midday-sweep.md +58 -0
  458. package/schedules/triggers/daily-morning-brief.md +55 -0
  459. package/schedules/triggers/directory-hygiene.md +81 -0
  460. package/schedules/triggers/dynamic-jobs.md +40 -0
  461. package/schedules/triggers/inbox-processor.md +115 -0
  462. package/schedules/triggers/meeting-action-capture.md +60 -0
  463. package/schedules/triggers/meeting-prep.md +69 -0
  464. package/schedules/triggers/messaging-inbound.md +50 -0
  465. package/schedules/triggers/org-pulse.md +24 -0
  466. package/schedules/triggers/quarterly-self-assessment.md +54 -0
  467. package/schedules/triggers/weekly-engineering-health.md +37 -0
  468. package/schedules/triggers/weekly-execution.md +65 -0
  469. package/schedules/triggers/weekly-hiring.md +53 -0
  470. package/schedules/triggers/weekly-priorities.md +38 -0
  471. package/schedules/triggers/weekly-strategic-memo.md +124 -0
  472. package/scripts/archive-email.sh +55 -0
  473. package/scripts/cadence/cadence-status.mjs +36 -0
  474. package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
  475. package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
  476. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  477. package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
  478. package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
  479. package/scripts/ci/check-docs-accuracy.mjs +493 -0
  480. package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
  481. package/scripts/ci/check-exports-exist.mjs +140 -0
  482. package/scripts/ci/check-files-exist.mjs +107 -0
  483. package/scripts/ci/check-no-build-artifacts.mjs +111 -0
  484. package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
  485. package/scripts/ci/check-no-confidential.mjs +198 -0
  486. package/scripts/ci/check-no-conflict-markers.mjs +169 -0
  487. package/scripts/ci/check-no-residual-identity.mjs +163 -0
  488. package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
  489. package/scripts/ci/check-tarball-fidelity.mjs +205 -0
  490. package/scripts/ci/check-unresolved-tokens.mjs +83 -0
  491. package/scripts/ci/check.mjs +109 -0
  492. package/scripts/ci/check.test.mjs +194 -0
  493. package/scripts/ci/run-coverage.mjs +82 -0
  494. package/scripts/ci/run-tests.mjs +71 -0
  495. package/scripts/cloud-relay/README.md +59 -0
  496. package/scripts/cloud-relay/index.mjs +233 -0
  497. package/scripts/cloud-relay/package.json +15 -0
  498. package/scripts/cloud-relay/railway.json +13 -0
  499. package/scripts/cloud-relay/voice/README.md +94 -0
  500. package/scripts/cloud-relay/voice/package-lock.json +39 -0
  501. package/scripts/cloud-relay/voice/package.json +16 -0
  502. package/scripts/cloud-relay/voice/railway.json +13 -0
  503. package/scripts/cloud-relay/voice/server.mjs +532 -0
  504. package/scripts/collective/hook-runner.mjs +211 -0
  505. package/scripts/collective/hook-runner.test.mjs +90 -0
  506. package/scripts/collective/org-pulse.mjs +72 -0
  507. package/scripts/collective/org-sync.mjs +61 -0
  508. package/scripts/collective/recall.mjs +45 -0
  509. package/scripts/collective/who.mjs +30 -0
  510. package/scripts/comms-monitor.sh +288 -0
  511. package/scripts/configure-whatsapp-sandbox.sh +201 -0
  512. package/scripts/continuous-monitor.sh +91 -0
  513. package/scripts/cost/fleet-digest.mjs +407 -0
  514. package/scripts/cost/fleet-digest.test.mjs +207 -0
  515. package/scripts/cost/track-claude-usage.mjs +169 -0
  516. package/scripts/daemon/agent-daemon.mjs +989 -0
  517. package/scripts/daemon/agent-daemon.test.mjs +525 -0
  518. package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
  519. package/scripts/daemon/cadence-consumer.mjs +1080 -0
  520. package/scripts/daemon/cadence-consumer.test.mjs +770 -0
  521. package/scripts/daemon/cadence-handlers.mjs +1121 -0
  522. package/scripts/daemon/cadence-handlers.test.mjs +617 -0
  523. package/scripts/daemon/classifier.mjs +704 -0
  524. package/scripts/daemon/classifier.test.mjs +238 -0
  525. package/scripts/daemon/classify-kind.mjs +54 -0
  526. package/scripts/daemon/classify-kind.test.mjs +40 -0
  527. package/scripts/daemon/context-compiler.mjs +605 -0
  528. package/scripts/daemon/context-compiler.test.mjs +300 -0
  529. package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
  530. package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
  531. package/scripts/daemon/dispatcher.mjs +1516 -0
  532. package/scripts/daemon/health.mjs +72 -0
  533. package/scripts/daemon/inbox-deferral.mjs +210 -0
  534. package/scripts/daemon/inbox-deferral.test.mjs +242 -0
  535. package/scripts/daemon/integration.test.mjs +149 -0
  536. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  537. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  538. package/scripts/daemon/launchd-wrapper.sh +91 -0
  539. package/scripts/daemon/lib/session-router.mjs +274 -0
  540. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  541. package/scripts/daemon/maestro-daemon.mjs +275 -0
  542. package/scripts/daemon/prompt-builder.mjs +685 -0
  543. package/scripts/daemon/prompt-builder.test.mjs +213 -0
  544. package/scripts/daemon/responder.mjs +854 -0
  545. package/scripts/daemon/session-lock.mjs +721 -0
  546. package/scripts/daemon/session-lock.test.mjs +252 -0
  547. package/scripts/daemon/session-outcomes.mjs +640 -0
  548. package/scripts/daemon/session-outcomes.test.mjs +533 -0
  549. package/scripts/daemon/typing-registry.mjs +90 -0
  550. package/scripts/daemon/typing-registry.test.mjs +77 -0
  551. package/scripts/daemon/voice-webhook-server.mjs +804 -0
  552. package/scripts/decisions/capture-decision.mjs +116 -0
  553. package/scripts/disclosure_assessment.py +873 -0
  554. package/scripts/disclosure_boundaries.py +562 -0
  555. package/scripts/email-signature-principal.html +52 -0
  556. package/scripts/email-signature.html +60 -0
  557. package/scripts/email_quote_thread.py +167 -0
  558. package/scripts/email_thread_dedup.py +362 -0
  559. package/scripts/emergency-stop.sh +81 -0
  560. package/scripts/healthcheck.sh +116 -0
  561. package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
  562. package/scripts/hooks/block-mcp-slack-send.sh +7 -0
  563. package/scripts/hooks/post-action-log.sh +126 -0
  564. package/scripts/hooks/pre-send-audit.sh +174 -0
  565. package/scripts/hooks/pre-send-audit.test.mjs +215 -0
  566. package/scripts/hooks/session-end-log.sh +27 -0
  567. package/scripts/hooks/session-start-banner.sh +115 -0
  568. package/scripts/huddle/audio-bridge.mjs +664 -0
  569. package/scripts/huddle/boot-slack-cdp.sh +102 -0
  570. package/scripts/huddle/huddle-controller.mjs +942 -0
  571. package/scripts/huddle/huddle-server.mjs +1229 -0
  572. package/scripts/huddle/launch-slack.sh +232 -0
  573. package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
  574. package/scripts/huddle/package-lock.json +62 -0
  575. package/scripts/huddle/package.json +22 -0
  576. package/scripts/huddle/setup-audio.sh +239 -0
  577. package/scripts/huddle/start-call.mjs +318 -0
  578. package/scripts/huddle/test-pipeline.mjs +263 -0
  579. package/scripts/learning/consolidate-skills.mjs +72 -0
  580. package/scripts/learning/session-search.mjs +125 -0
  581. package/scripts/llm_email_dedup.py +442 -0
  582. package/scripts/local-triggers/generate-plists.sh +432 -0
  583. package/scripts/local-triggers/generate-plists.test.mjs +413 -0
  584. package/scripts/local-triggers/install-all.sh +49 -0
  585. package/scripts/local-triggers/plists/.gitkeep +0 -0
  586. package/scripts/local-triggers/run-trigger.sh +63 -0
  587. package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
  588. package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
  589. package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
  590. package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
  591. package/scripts/maintenance/backup-to-cloud.sh +124 -0
  592. package/scripts/maintenance/health-check.sh +377 -0
  593. package/scripts/media-generation/README.md +105 -0
  594. package/scripts/media-generation/gemini-image-client.mjs +173 -0
  595. package/scripts/media-generation/generate-assets.mjs +289 -0
  596. package/scripts/media-generation/veo-video-client.mjs +219 -0
  597. package/scripts/org/send-orgmail.mjs +227 -0
  598. package/scripts/outbound-dedup-cleanup.sh +43 -0
  599. package/scripts/outbound-dedup.sh +477 -0
  600. package/scripts/outbound_dedup.py +115 -0
  601. package/scripts/parse-voice-transcript.mjs +481 -0
  602. package/scripts/pdf-generation/README.md +63 -0
  603. package/scripts/pdf-generation/build-document.mjs +247 -0
  604. package/scripts/pdf-generation/templates/board-pack.latex +136 -0
  605. package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
  606. package/scripts/pdf-generation/templates/memo.latex +114 -0
  607. package/scripts/poll-slack-events.sh +35 -0
  608. package/scripts/poller/calendar-poller.mjs +12 -0
  609. package/scripts/poller/gmail-poller.mjs +192 -0
  610. package/scripts/poller/imap-client.mjs +289 -0
  611. package/scripts/poller/inbox-scan-poller.mjs +156 -0
  612. package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
  613. package/scripts/poller/index.mjs +73 -0
  614. package/scripts/poller/intra-session-check.mjs +285 -0
  615. package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
  616. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
  617. package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
  618. package/scripts/poller/secondary-gmail-poller.mjs +132 -0
  619. package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
  620. package/scripts/poller/slack-poller.mjs +854 -0
  621. package/scripts/poller/slack-socket-mode.mjs +917 -0
  622. package/scripts/poller/slack-socket-mode.test.mjs +753 -0
  623. package/scripts/poller/trigger.mjs +75 -0
  624. package/scripts/poller/utils.mjs +371 -0
  625. package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
  626. package/scripts/poller/voice-poller.mjs +236 -0
  627. package/scripts/poller-launchd/install.sh +66 -0
  628. package/scripts/poller-launchd/poller.plist.template +40 -0
  629. package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
  630. package/scripts/post-interaction-indexer.py +1598 -0
  631. package/scripts/pre-draft-context.py +994 -0
  632. package/scripts/pre_draft_lookup.py +258 -0
  633. package/scripts/rag/build-index.mjs +47 -0
  634. package/scripts/rag/ingest.mjs +111 -0
  635. package/scripts/rag/search.mjs +119 -0
  636. package/scripts/rag-indexer.py +629 -0
  637. package/scripts/restore-from-backup.sh +248 -0
  638. package/scripts/restore-from-backup.test.mjs +178 -0
  639. package/scripts/resume-operations.sh +80 -0
  640. package/scripts/search-secondary-inbox.py +181 -0
  641. package/scripts/secondary-inbox-poller.py +437 -0
  642. package/scripts/self-optimization/compute-metrics.py +398 -0
  643. package/scripts/send-email-as-principal.py +369 -0
  644. package/scripts/send-email-threaded.py +392 -0
  645. package/scripts/send-email-with-attachment.py +377 -0
  646. package/scripts/send-email.sh +131 -0
  647. package/scripts/send-sms.sh +175 -0
  648. package/scripts/send-whatsapp.sh +292 -0
  649. package/scripts/session-start.sh +106 -0
  650. package/scripts/setup/boot-claude-session.sh +94 -0
  651. package/scripts/setup/configure-macos.sh +674 -0
  652. package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
  653. package/scripts/setup/configure-voice-tunnel.mjs +182 -0
  654. package/scripts/setup/generate-agent-env.mjs +92 -0
  655. package/scripts/setup/generate-agent-package-json.mjs +222 -0
  656. package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
  657. package/scripts/setup/generate-autonomy.mjs +60 -0
  658. package/scripts/setup/generate-backlog.mjs +101 -0
  659. package/scripts/setup/generate-cadences.mjs +92 -0
  660. package/scripts/setup/generate-capability.mjs +162 -0
  661. package/scripts/setup/generate-charter.mjs +76 -0
  662. package/scripts/setup/generate-comms.mjs +59 -0
  663. package/scripts/setup/generate-company.mjs +92 -0
  664. package/scripts/setup/init-agent.sh +547 -0
  665. package/scripts/setup/init-agent.test.mjs +151 -0
  666. package/scripts/setup/init-archetype.mjs +74 -0
  667. package/scripts/setup/init-backup.mjs +54 -0
  668. package/scripts/setup/init-cadence-bus.mjs +60 -0
  669. package/scripts/setup/init-channel-bus.mjs +46 -0
  670. package/scripts/setup/init-cost-tracking.mjs +45 -0
  671. package/scripts/setup/init-decision-capture.mjs +66 -0
  672. package/scripts/setup/init-known-agents.mjs +57 -0
  673. package/scripts/setup/init-learning.mjs +70 -0
  674. package/scripts/setup/init-memory-executive.mjs +45 -0
  675. package/scripts/setup/init-model-router.mjs +124 -0
  676. package/scripts/setup/init-rag.mjs +174 -0
  677. package/scripts/setup/init-session-router.mjs +38 -0
  678. package/scripts/setup/init-slack-socket-mode.mjs +260 -0
  679. package/scripts/setup/init-telegram.mjs +165 -0
  680. package/scripts/setup/init-voice-realtime.mjs +204 -0
  681. package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
  682. package/scripts/setup/install-dev-tools.sh +150 -0
  683. package/scripts/setup/lib/install-plist.mjs +95 -0
  684. package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
  685. package/scripts/setup/render-environment-yaml.mjs +133 -0
  686. package/scripts/slack-events-ctl.sh +177 -0
  687. package/scripts/slack-events-server.mjs +1045 -0
  688. package/scripts/slack-react.mjs +89 -0
  689. package/scripts/slack-responded.sh +232 -0
  690. package/scripts/slack-send.sh +287 -0
  691. package/scripts/slack-typing.mjs +196 -0
  692. package/scripts/slack-upload-v2.py +95 -0
  693. package/scripts/sms-handler.mjs +450 -0
  694. package/scripts/spawn-session.sh +120 -0
  695. package/scripts/sync-protocol.mjs +217 -0
  696. package/scripts/system-verify.sh +184 -0
  697. package/scripts/test-email-thread-dedup.py +239 -0
  698. package/scripts/test-information-barriers.py +484 -0
  699. package/scripts/test-llm-email-dedup.py +251 -0
  700. package/scripts/test-pre-draft-integration.py +203 -0
  701. package/scripts/test-rag-phase2.sh +442 -0
  702. package/scripts/test-rag-search.sh +251 -0
  703. package/scripts/test-voice-parser.mjs +316 -0
  704. package/scripts/user-context-search.py +659 -0
  705. package/scripts/validate_outbound.py +1504 -0
  706. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
  707. package/scripts/watchdog/force-reboot.sh +157 -0
  708. package/scripts/watchdog/memory-watchdog.sh +473 -0
  709. package/scripts/whatsapp-handler.mjs +538 -0
  710. package/teams/desktop-operations.yaml +34 -0
  711. package/teams/executive-office.yaml +27 -0
  712. package/teams/legal-and-regulatory.yaml +24 -0
  713. package/teams/platform-and-engineering.yaml +23 -0
  714. package/teams/strategy-and-growth.yaml +29 -0
  715. package/workflows/continuous/backlog-executor.yaml +141 -0
  716. package/workflows/continuous/inbound-monitor.yaml +168 -0
  717. package/workflows/daily/applicant-triage.yaml +197 -0
  718. package/workflows/daily/comms-triage.yaml +80 -0
  719. package/workflows/daily/evening-wrap.yaml +105 -0
  720. package/workflows/daily/morning-brief.yaml +164 -0
  721. package/workflows/daily/slack-followup-sweep.yaml +87 -0
  722. package/workflows/event-driven/README.md +50 -0
  723. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  724. package/workflows/event-driven/pr-review.yaml +107 -0
  725. package/workflows/monthly/board-readiness.yaml +76 -0
  726. package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
  727. package/workflows/session-protocol.md +171 -0
  728. package/workflows/weekly/engineering-health.yaml +154 -0
  729. package/workflows/weekly/hiring-review.yaml +169 -0
  730. package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
  731. package/workflows/weekly/strategic-memo.yaml +79 -0
@@ -0,0 +1,448 @@
1
+ /**
2
+ * lib/org/approvals.mjs — agent-mediated approval delivery + the action-class gate.
3
+ *
4
+ * The agent half of the phase-3 approval flow (SPEC-org-server §4.6 / §5.4). The
5
+ * Cohort server holds NO Slack/WhatsApp credentials; an `approval.request`
6
+ * merely records the pending row and emits an `approval.pending` event. THIS
7
+ * module is the side that (a) decides whether an action even needs approval (the
8
+ * action-class gate), (b) sha256-binds the exact payload the human will approve,
9
+ * (c) posts `approval.request`, and (d) — agent-mediated — delivers the
10
+ * approve/deny ask to a human over the configured channel (default Slack). The
11
+ * human's reply routes back through the channel adapter → `approval.resolve` (the
12
+ * approver side; not this module), and the requester wakes via the
13
+ * `approval.wait` long-poll for a <60s end-to-end round-trip.
14
+ *
15
+ * G16 (zero-LLM-in-the-coordination-loop): NOTHING here makes an LLM call. The
16
+ * classification is a pure table lookup over policies/action-classification.yaml;
17
+ * delivery is a deterministic channel send; the wait is a bounded HTTP long-poll.
18
+ * If this module ever needs a model to decide whether to gate, the invariant is
19
+ * broken — keep it deterministic.
20
+ *
21
+ * G5 (requester != approver): enforced at the storage + method layer server-side
22
+ * (the approvals-table CHECK constraint and approval.resolve actor check). This
23
+ * module never resolves an approval it requested; it only requests + waits.
24
+ *
25
+ * Pure gate/hash/classify logic (unit-tested) + a thin, injectable, fail-open
26
+ * channel transport (default: scripts/slack-send.sh) and the long-poll. Node
27
+ * builtins only beyond the optional js-yaml policy parse (loaded via createRequire,
28
+ * same as lib/voice/tool-bridge.mjs — absent it the gate fails CLOSED).
29
+ *
30
+ * @module lib/org/approvals
31
+ */
32
+
33
+ "use strict";
34
+
35
+ import { spawnSync } from "node:child_process";
36
+ import { createHash } from "node:crypto";
37
+ import { existsSync, readFileSync } from "node:fs";
38
+ import { join, resolve } from "node:path";
39
+ import { createRequire } from "node:module";
40
+
41
+ import { isEnabled, configFromAgent, approvalRequest, approvalWait } from "./client.mjs";
42
+
43
+ const require = createRequire(import.meta.url);
44
+
45
+ /**
46
+ * The gated action classes (SPEC §4.6 + the SHARED CONTRACT). An action that maps
47
+ * to ANY of these MUST be approved before it executes. Keyed off the action
48
+ * classification — `irreversible` / `external` / `financial`.
49
+ * @type {readonly string[]}
50
+ */
51
+ export const GATED_CLASSES = Object.freeze(["irreversible", "external", "financial"]);
52
+
53
+ /**
54
+ * Map an action-classification level (the `risk`/`level`/`approval` triple from
55
+ * policies/action-classification.yaml) onto the gated action classes. Pure.
56
+ *
57
+ * The default/template policy speaks in `action_levels` (observe / draft /
58
+ * execute_autonomous / escalate_to_principal) and per-action `risk` + `approval`.
59
+ * The mapping below is conservative — anything that escalates to the principal,
60
+ * is approval-gated, or is explicitly tagged high/critical risk is treated as a
61
+ * gated class. We also honour an explicit `classes:[…]` / `action_class` field
62
+ * when the archetype factory emits one (the per-action map it generates).
63
+ *
64
+ * @param {object} def normalised classification entry for the action
65
+ * @returns {string[]} the subset of GATED_CLASSES this action falls under
66
+ */
67
+ export function classesForAction(def) {
68
+ if (!def || typeof def !== "object") return [];
69
+ const out = new Set();
70
+ // 1. Explicit class tags win (archetype-generated `actions` map may carry these).
71
+ const explicit = []
72
+ .concat(def.classes || [])
73
+ .concat(def.action_class ? [def.action_class] : [])
74
+ .concat(def.action_classes || []);
75
+ for (const c of explicit) {
76
+ const k = String(c || "").toLowerCase();
77
+ if (GATED_CLASSES.includes(k)) out.add(k);
78
+ }
79
+ if (out.size) return [...out];
80
+
81
+ // 2. Derive from risk + approval + level when no explicit tag is present.
82
+ const risk = String(def.risk || def.level || "").toLowerCase();
83
+ const approval = String(def.approval || def.gate || "").toLowerCase();
84
+ const level = String(def.action_level || def.level || "").toLowerCase();
85
+ const highRisk = risk.includes("high") || risk.includes("critical");
86
+ const gated =
87
+ highRisk ||
88
+ approval.includes("escalat") ||
89
+ approval.includes("principal") ||
90
+ approval.includes("approval") ||
91
+ level.includes("escalate");
92
+ if (!gated) return [];
93
+
94
+ // Heuristic class assignment from the action's notes/keywords (deterministic).
95
+ const hay = `${def.notes || ""} ${def.description || ""} ${def.name || ""}`.toLowerCase();
96
+ if (/spend|payment|invoice|wire|financ|budget|purchase|pricing|deal terms/.test(hay)) out.add("financial");
97
+ if (/external|public|press|partner|customer|regulat|submission|filing/.test(hay)) out.add("external");
98
+ if (/delete|irreversible|destroy|prod|merge|binding|commit/.test(hay)) out.add("irreversible");
99
+ // A gated action with no keyword signal still needs approval — default to the
100
+ // most generic gated class so the gate never silently passes it.
101
+ if (!out.size) out.add("irreversible");
102
+ return [...out];
103
+ }
104
+
105
+ /**
106
+ * Load + normalise the action-classification policy (YAML or JSON), mirroring
107
+ * lib/voice/tool-bridge.loadActionClassification: a map of
108
+ * `{ actionName: { risk, approval, level, classes?, notes } }`. Absent js-yaml
109
+ * (and no JSON), the gate fails CLOSED (returns {} → unknown actions gate by the
110
+ * caller's `defaultGate`). Never throws.
111
+ *
112
+ * @param {string} agentRoot
113
+ * @returns {Record<string, object>}
114
+ */
115
+ export function loadActionClassification(agentRoot) {
116
+ const root = resolve(agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd());
117
+ const candidates = [
118
+ join(root, "policies/action-classification.json"),
119
+ join(root, "policies/action-classification.yaml"),
120
+ ];
121
+ for (const path of candidates) {
122
+ if (!existsSync(path)) continue;
123
+ let raw;
124
+ try { raw = readFileSync(path, "utf-8"); } catch { continue; }
125
+ try {
126
+ const parsed = path.endsWith(".json") ? JSON.parse(raw) : require("js-yaml").load(raw);
127
+ return normaliseClassification(parsed);
128
+ } catch {
129
+ // Misconfigured/absent parser → empty map; the gate fails closed.
130
+ return {};
131
+ }
132
+ }
133
+ return {};
134
+ }
135
+
136
+ function normaliseClassification(raw) {
137
+ if (!raw || typeof raw !== "object") return {};
138
+ const out = {};
139
+ const actions = raw.actions || raw.action_levels || raw;
140
+ for (const [name, def] of Object.entries(actions)) {
141
+ if (!def || typeof def !== "object") continue;
142
+ out[name] = {
143
+ name,
144
+ risk: def.risk || def.level || "low",
145
+ approval: def.approval || def.gate || "none",
146
+ action_level: def.action_level || def.level || "",
147
+ classes: def.classes || def.action_classes || (def.action_class ? [def.action_class] : []),
148
+ notes: def.notes || def.description || "",
149
+ };
150
+ }
151
+ return out;
152
+ }
153
+
154
+ /**
155
+ * Classify an action by name against the loaded policy → its gated classes. When
156
+ * the caller already knows the classification (e.g. a tool that self-declares
157
+ * `irreversible`), pass `classification` directly and skip the lookup. Pure.
158
+ *
159
+ * @param {object} o - { action, classification?, policy?, agentRoot? }
160
+ * @returns {{ gated:boolean, classes:string[], source:string }}
161
+ */
162
+ export function classifyAction(o = {}) {
163
+ // Caller-supplied classification short-circuits the policy lookup.
164
+ if (o.classification) {
165
+ const list = []
166
+ .concat(o.classification)
167
+ .map((c) => String(c || "").toLowerCase())
168
+ .filter((c) => GATED_CLASSES.includes(c));
169
+ return { gated: list.length > 0, classes: list, source: "explicit" };
170
+ }
171
+ const policy = o.policy || loadActionClassification(o.agentRoot);
172
+ const def = policy[o.action];
173
+ if (!def) {
174
+ // Unknown action: fail closed — treat as gated unless caller opts out.
175
+ return { gated: true, classes: ["irreversible"], source: "unknown-fail-closed" };
176
+ }
177
+ const classes = classesForAction(def);
178
+ return { gated: classes.length > 0, classes, source: "policy" };
179
+ }
180
+
181
+ /**
182
+ * sha256-bind a payload: the canonical "the human approved exactly this" hash.
183
+ * Stable across re-serialization (keys sorted) so the executor can recompute it
184
+ * and the server can reject a resolve whose executing payload hash differs. Pure.
185
+ *
186
+ * @param {unknown} payload
187
+ * @returns {string} lowercase hex sha-256 over the canonical JSON
188
+ */
189
+ export function hashPayload(payload) {
190
+ return createHash("sha256").update(canonicalJson(payload)).digest("hex");
191
+ }
192
+
193
+ /** Deterministic JSON: object keys sorted recursively. */
194
+ function canonicalJson(value) {
195
+ return JSON.stringify(sortDeep(value));
196
+ }
197
+ function sortDeep(v) {
198
+ if (Array.isArray(v)) return v.map(sortDeep);
199
+ if (v && typeof v === "object") {
200
+ const out = {};
201
+ for (const k of Object.keys(v).sort()) out[k] = sortDeep(v[k]);
202
+ return out;
203
+ }
204
+ return v;
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Agent-mediated delivery (default transport: scripts/slack-send.sh)
209
+ // ---------------------------------------------------------------------------
210
+
211
+ /**
212
+ * Build the human-facing approval ask (a plain-text message the delivery agent
213
+ * posts to the approver's channel). Deterministic; no LLM. The approver replies
214
+ * over the same channel → the channel adapter routes the reply to
215
+ * `approval.resolve`. Pure.
216
+ *
217
+ * @param {object} o - { id, action, classes, subject?, requester? }
218
+ * @returns {string}
219
+ */
220
+ export function buildApprovalAsk(o = {}) {
221
+ const classes = Array.isArray(o.classes) && o.classes.length ? o.classes.join(", ") : "gated";
222
+ const who = o.requester ? ` (requested by ${o.requester})` : "";
223
+ const subject = o.subject ? `\n${String(o.subject).slice(0, 800)}` : "";
224
+ return (
225
+ `Approval needed${who}: ${o.action || "an action"} [${classes}]` +
226
+ subject +
227
+ `\nReply "approve ${o.id}" or "deny ${o.id}".`
228
+ );
229
+ }
230
+
231
+ /** Default delivery transport: scripts/slack-send.sh to the approver. Fail-open. */
232
+ export function defaultDeliver({ agentRoot, approver, text }) {
233
+ try {
234
+ const dest = approver && (approver.slackId || approver.channel || approver.id);
235
+ if (!dest) return { ok: false, channel: null, reason: "no-approver-coordinates" };
236
+ const res = spawnSync(join(agentRoot, "scripts", "slack-send.sh"), [String(dest), String(text)], {
237
+ cwd: agentRoot,
238
+ env: { ...process.env, AGENT_DIR: agentRoot },
239
+ encoding: "utf8",
240
+ timeout: 30_000,
241
+ });
242
+ return { ok: res.status === 0, channel: `slack:${dest}`, reason: res.status === 0 ? "delivered" : `exit-${res.status}` };
243
+ } catch (e) {
244
+ return { ok: false, channel: null, reason: e.message };
245
+ }
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // requestApproval — classify, bind, request, deliver
250
+ // ---------------------------------------------------------------------------
251
+
252
+ /**
253
+ * Request approval for a classified action and deliver the ask to a human.
254
+ *
255
+ * Steps (all deterministic; G16):
256
+ * 1. Classify the action (irreversible/external/financial) via the policy (or
257
+ * the caller-supplied `classification`). If NOT gated, returns immediately
258
+ * with `{ gated:false }` — no server call, no delivery.
259
+ * 2. sha256-bind the EXACT payload (`payload_hash`) so the human approves this
260
+ * and only this; the executor re-checks the hash before acting.
261
+ * 3. POST approval.request (idempotent on the derived id) — the server records
262
+ * the pending row + emits approval.pending.
263
+ * 4. Agent-mediated delivery: post the approve/deny ask to the approver's
264
+ * channel (default Slack). Delivery failure is recorded, not fatal — the
265
+ * pending row + /ops dashboard remain the fallback surface.
266
+ *
267
+ * Always resolves (fail-open).
268
+ *
269
+ * @param {object} o - {
270
+ * cfg, agentRoot, action, payload, classification?, policy?,
271
+ * subject?, kind?, itemId?, requester?, approver?, channel?,
272
+ * expiresInMs?, id?, deliverFn?, fetchImpl?, nowMs?
273
+ * }
274
+ * @returns {Promise<{
275
+ * gated:boolean, classes:string[], id:string|null, payloadHash:string,
276
+ * requested:boolean, frame:object|null, delivery:object|null
277
+ * }>}
278
+ */
279
+ export async function requestApproval(o = {}) {
280
+ const { gated, classes } = classifyAction({
281
+ action: o.action,
282
+ classification: o.classification,
283
+ policy: o.policy,
284
+ agentRoot: o.agentRoot,
285
+ });
286
+ const payloadHash = hashPayload(o.payload ?? null);
287
+
288
+ if (!gated) {
289
+ return { gated: false, classes: [], id: null, payloadHash, requested: false, frame: null, delivery: null };
290
+ }
291
+
292
+ // Stable approval id: caller-supplied else derived from action+payload so a
293
+ // retried request (same action, same payload) reuses the same approval row.
294
+ const id = o.id || `ap-${payloadHash.slice(0, 16)}`;
295
+ const kind = o.kind || classToKind(classes);
296
+ const subject = o.subject || `${o.action || "action"} (${classes.join(", ")})`;
297
+
298
+ if (!isEnabled(o.cfg)) {
299
+ // Org binding disabled: surface the gate decision + hash but make no call.
300
+ return { gated: true, classes, id, payloadHash, requested: false, frame: null, delivery: null };
301
+ }
302
+
303
+ const c = configFromAgent(o.cfg);
304
+ const params = {
305
+ id,
306
+ kind,
307
+ subject,
308
+ payload_hash: payloadHash,
309
+ requester: o.requester || c.orgId || undefined,
310
+ };
311
+ if (o.itemId) params.itemId = String(o.itemId);
312
+ if (Number.isFinite(o.expiresInMs)) params.expiresInMs = o.expiresInMs;
313
+
314
+ const frame = await approvalRequest(params, {
315
+ base: c.base,
316
+ token: c.token,
317
+ idempotencyKey: `approval.request:${id}`,
318
+ fetchImpl: o.fetchImpl,
319
+ });
320
+ const requested = !!(frame && frame.ok);
321
+
322
+ // Agent-mediated delivery (only when the request landed).
323
+ let delivery = null;
324
+ if (requested && o.approver) {
325
+ const text = buildApprovalAsk({ id, action: o.action, classes, subject: o.subject, requester: o.requester });
326
+ const deliver = o.deliverFn || defaultDeliver;
327
+ try {
328
+ delivery = await deliver({ agentRoot: o.agentRoot, approver: o.approver, channel: o.channel, text, id });
329
+ } catch (e) {
330
+ delivery = { ok: false, reason: e.message };
331
+ }
332
+ }
333
+
334
+ return { gated: true, classes, id, payloadHash, requested, frame, delivery };
335
+ }
336
+
337
+ /** Pick the approvals.kind enum value (§4.6) from the derived classes. */
338
+ function classToKind(classes) {
339
+ if (classes.includes("financial")) return "spend";
340
+ if (classes.includes("external")) return "external_comms";
341
+ if (classes.includes("irreversible")) return "exec";
342
+ return "exec";
343
+ }
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // awaitDecision — bounded long-poll for the outcome
347
+ // ---------------------------------------------------------------------------
348
+
349
+ /**
350
+ * Wait for an approval to be decided via the bounded long-poll (approval.wait),
351
+ * with optional re-polling until an overall deadline so the caller can wait
352
+ * longer than one 55s window without holding the connection open. Fail-open.
353
+ *
354
+ * @param {string} id approval id
355
+ * @param {number} timeoutMs overall deadline (each poll bounded to <=55s)
356
+ * @param {object} o - { cfg, fetchImpl?, nowMs?, pollMs? }
357
+ * @returns {Promise<{ decided:boolean, status:string, payload:object|null }>}
358
+ */
359
+ export async function awaitDecision(id, timeoutMs = 55000, o = {}) {
360
+ if (!id) return { decided: false, status: "unknown", payload: null };
361
+ if (!isEnabled(o.cfg)) return { decided: false, status: "unknown", payload: null };
362
+ const c = configFromAgent(o.cfg);
363
+ const now = typeof o.nowMs === "function" ? o.nowMs : Date.now;
364
+ const deadline = now() + (Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 55000);
365
+
366
+ let last = null;
367
+ // Each iteration is one bounded long-poll; loop until decided or the deadline.
368
+ // The first poll is given the full remaining budget (clamped to 55s by the
369
+ // client), so a single call covers the common <60s case in one round-trip.
370
+ do {
371
+ const remaining = deadline - now();
372
+ const pollMs = Math.max(1, Math.min(55000, remaining));
373
+ const r = await approvalWait({ id, timeoutMs: pollMs }, { base: c.base, token: c.token, fetchImpl: o.fetchImpl });
374
+ last = r && r.ok ? r.payload : last;
375
+ const status = decisionStatus(last);
376
+ if (isDecided(status)) return { decided: true, status, payload: last };
377
+ } while (now() < deadline);
378
+
379
+ return { decided: false, status: decisionStatus(last) || "pending", payload: last };
380
+ }
381
+
382
+ function decisionStatus(payload) {
383
+ if (!payload || typeof payload !== "object") return "";
384
+ return String(payload.status || payload.state || "").toLowerCase();
385
+ }
386
+
387
+ function isDecided(status) {
388
+ return status === "approved" || status === "denied" || status === "expired";
389
+ }
390
+
391
+ // ---------------------------------------------------------------------------
392
+ // gateActionClass — the blocking helper the action-executor calls
393
+ // ---------------------------------------------------------------------------
394
+
395
+ /**
396
+ * Block an action-class until it is approved. The helper the action-executor
397
+ * wraps a gated tool with: classify → (if gated) request + deliver + wait →
398
+ * return `{ approved, reason }`. Targets <60s end-to-end via the long-poll.
399
+ *
400
+ * Non-gated actions return `{ approved:true, reason:"not-gated" }` immediately
401
+ * (no server round-trip). A disabled org binding returns
402
+ * `{ approved:false, reason:"org-disabled", ... }` so the executor can decide its
403
+ * own fallback (the gate never silently passes a gated action when it cannot
404
+ * obtain an approval). The returned `payloadHash` lets the executor verify it
405
+ * runs exactly the approved payload.
406
+ *
407
+ * @param {object} o - all of requestApproval's params + { timeoutMs?, pollMs? }
408
+ * @returns {Promise<{
409
+ * approved:boolean, reason:string, id:string|null, classes:string[],
410
+ * payloadHash:string, status?:string
411
+ * }>}
412
+ */
413
+ export async function gateActionClass(o = {}) {
414
+ const req = await requestApproval(o);
415
+ if (!req.gated) {
416
+ return { approved: true, reason: "not-gated", id: null, classes: [], payloadHash: req.payloadHash };
417
+ }
418
+ if (!req.requested) {
419
+ const reason = isEnabled(o.cfg) ? "request-failed" : "org-disabled";
420
+ return { approved: false, reason, id: req.id, classes: req.classes, payloadHash: req.payloadHash };
421
+ }
422
+ const dec = await awaitDecision(req.id, Number.isFinite(o.timeoutMs) ? o.timeoutMs : 55000, {
423
+ cfg: o.cfg,
424
+ fetchImpl: o.fetchImpl,
425
+ nowMs: o.nowMs,
426
+ pollMs: o.pollMs,
427
+ });
428
+ if (dec.status === "approved") {
429
+ return { approved: true, reason: "approved", id: req.id, classes: req.classes, payloadHash: req.payloadHash, status: dec.status };
430
+ }
431
+ const reason = dec.decided ? dec.status : "timeout";
432
+ return { approved: false, reason, id: req.id, classes: req.classes, payloadHash: req.payloadHash, status: dec.status };
433
+ }
434
+
435
+ export const _internals = { canonicalJson, classToKind, decisionStatus, isDecided, normaliseClassification };
436
+
437
+ export default {
438
+ GATED_CLASSES,
439
+ classesForAction,
440
+ loadActionClassification,
441
+ classifyAction,
442
+ hashPayload,
443
+ buildApprovalAsk,
444
+ defaultDeliver,
445
+ requestApproval,
446
+ awaitDecision,
447
+ gateActionClass,
448
+ };
@@ -0,0 +1,216 @@
1
+ /**
2
+ * approvals.test.mjs — action-class gate, sha256 payload binding, agent-mediated
3
+ * delivery, and the bounded long-poll awaitDecision / gateActionClass blocking
4
+ * helper. Injected fetchImpl + deliverFn; no network, no real channels.
5
+ * Run: node --test lib/org/approvals.test.mjs
6
+ */
7
+ "use strict";
8
+
9
+ import { test } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { createHash } from "node:crypto";
12
+
13
+ import {
14
+ GATED_CLASSES,
15
+ classesForAction,
16
+ classifyAction,
17
+ hashPayload,
18
+ buildApprovalAsk,
19
+ requestApproval,
20
+ awaitDecision,
21
+ gateActionClass,
22
+ } from "./approvals.mjs";
23
+
24
+ const BASE = "https://org.cohortapp.com";
25
+ const CFG = { org: { cohort: { enabled: true, base: BASE, token: "tok", orgId: "alex-ai" } } };
26
+ const OFF = { org: { cohort: { enabled: false } } };
27
+
28
+ delete process.env.COHORT_API_TOKEN;
29
+ delete process.env.COHORT_TOKEN;
30
+
31
+ /** Fake fetch capturing POST bodies and returning canned res frames / read payloads. */
32
+ function fakeFetch(responder) {
33
+ const calls = [];
34
+ const impl = async (url, init) => {
35
+ calls.push({ url, init, headers: (init && init.headers) || {}, body: init && init.body ? JSON.parse(init.body) : null });
36
+ const r = responder ? responder(url, init, calls.length - 1) : null;
37
+ const status = r && typeof r.status === "number" ? r.status : 200;
38
+ const ok = r && typeof r.ok === "boolean" ? r.ok : status >= 200 && status < 300;
39
+ const body = r ? r.body : { ok: true, result: null };
40
+ return { ok, status, headers: { get: () => undefined }, json: async () => body };
41
+ };
42
+ impl.calls = calls;
43
+ return impl;
44
+ }
45
+
46
+ // --- classification --------------------------------------------------------
47
+
48
+ test("classesForAction: explicit class tags win; gated-by-risk derivation; non-gated empty", () => {
49
+ assert.deepEqual(classesForAction({ classes: ["financial"] }), ["financial"]);
50
+ assert.deepEqual(classesForAction({ action_class: "external" }), ["external"]);
51
+ // high-risk + escalation, no explicit tag, keyword "delete prod" → irreversible
52
+ const d = classesForAction({ risk: "high", approval: "principal_escalation", notes: "delete production data" });
53
+ assert.ok(d.includes("irreversible"));
54
+ // low-risk autonomous action is not gated
55
+ assert.deepEqual(classesForAction({ risk: "low", approval: "autonomous" }), []);
56
+ });
57
+
58
+ test("classifyAction: explicit classification short-circuits; unknown fails closed", () => {
59
+ assert.deepEqual(classifyAction({ action: "x", classification: "financial" }), { gated: true, classes: ["financial"], source: "explicit" });
60
+ const unknown = classifyAction({ action: "never-seen", policy: {} });
61
+ assert.equal(unknown.gated, true);
62
+ assert.equal(unknown.source, "unknown-fail-closed");
63
+ const seen = classifyAction({ action: "send_slack", policy: { send_slack: { risk: "low", approval: "autonomous" } } });
64
+ assert.equal(seen.gated, false);
65
+ });
66
+
67
+ test("GATED_CLASSES are the three action classes", () => {
68
+ assert.deepEqual([...GATED_CLASSES].sort(), ["external", "financial", "irreversible"]);
69
+ });
70
+
71
+ // --- payload binding -------------------------------------------------------
72
+
73
+ test("hashPayload: stable sha256 over canonical JSON (key order independent)", () => {
74
+ const a = hashPayload({ amount: 100, to: "bob" });
75
+ const b = hashPayload({ to: "bob", amount: 100 });
76
+ assert.equal(a, b);
77
+ assert.equal(a, createHash("sha256").update(JSON.stringify({ amount: 100, to: "bob" })).digest("hex"));
78
+ });
79
+
80
+ // --- requestApproval: binds the hash + posts approval.request ---------------
81
+
82
+ test("requestApproval: binds payload hash and POSTs approval.request with it", async () => {
83
+ const payload = { action: "wire", amount: 5000, to: "vendor" };
84
+ const expected = hashPayload(payload);
85
+ const f = fakeFetch((url) => {
86
+ if (url.endsWith("/v1/approval.request")) return { body: { ok: true, result: { id: "ap-x", status: "pending" } } };
87
+ return { body: { ok: true, result: null } };
88
+ });
89
+ const r = await requestApproval({
90
+ cfg: CFG,
91
+ action: "wire_funds",
92
+ classification: "financial",
93
+ payload,
94
+ approver: { slackId: "U123" },
95
+ deliverFn: async () => ({ ok: true, channel: "slack:U123", reason: "delivered" }),
96
+ fetchImpl: f,
97
+ });
98
+ assert.equal(r.gated, true);
99
+ assert.deepEqual(r.classes, ["financial"]);
100
+ assert.equal(r.requested, true);
101
+ assert.equal(r.payloadHash, expected);
102
+ // the request body carried the exact payload_hash ("approved exactly this")
103
+ const reqCall = f.calls.find((c) => c.url.endsWith("/v1/approval.request"));
104
+ assert.equal(reqCall.body.payload_hash, expected);
105
+ assert.equal(reqCall.body.kind, "spend");
106
+ // agent-mediated delivery fired
107
+ assert.equal(r.delivery.ok, true);
108
+ });
109
+
110
+ test("requestApproval: NON-gated action makes no server call and no delivery", async () => {
111
+ const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
112
+ const r = await requestApproval({
113
+ cfg: CFG,
114
+ action: "send_slack",
115
+ classification: [], // explicitly not a gated class
116
+ payload: { msg: "hi" },
117
+ fetchImpl: f,
118
+ });
119
+ assert.equal(r.gated, false);
120
+ assert.equal(r.requested, false);
121
+ assert.equal(f.calls.length, 0);
122
+ });
123
+
124
+ test("requestApproval: org disabled → gated decision + hash, but no call", async () => {
125
+ const r = await requestApproval({ cfg: OFF, action: "x", classification: "irreversible", payload: { a: 1 } });
126
+ assert.equal(r.gated, true);
127
+ assert.equal(r.requested, false);
128
+ assert.ok(r.payloadHash);
129
+ });
130
+
131
+ test("buildApprovalAsk: human-readable approve/deny affordance referencing the id", () => {
132
+ const ask = buildApprovalAsk({ id: "ap-1", action: "wire_funds", classes: ["financial"], requester: "alex" });
133
+ assert.match(ask, /Approval needed/);
134
+ assert.match(ask, /approve ap-1/);
135
+ assert.match(ask, /deny ap-1/);
136
+ assert.match(ask, /financial/);
137
+ });
138
+
139
+ // --- awaitDecision: resolves on approval -----------------------------------
140
+
141
+ test("awaitDecision: resolves decided=true when approval.wait returns approved", async () => {
142
+ const f = fakeFetch(() => ({ body: { id: "ap-1", status: "approved", resolved_by_agent: "human-1" } }));
143
+ const dec = await awaitDecision("ap-1", 10000, { cfg: CFG, fetchImpl: f });
144
+ assert.equal(dec.decided, true);
145
+ assert.equal(dec.status, "approved");
146
+ assert.match(f.calls[0].url, /\/v1\/approval\.wait\?id=ap-1/);
147
+ });
148
+
149
+ test("awaitDecision: denied is decided; pending past deadline is not decided", async () => {
150
+ const denied = fakeFetch(() => ({ body: { id: "ap-1", status: "denied" } }));
151
+ const d1 = await awaitDecision("ap-1", 5000, { cfg: CFG, fetchImpl: denied });
152
+ assert.equal(d1.decided, true);
153
+ assert.equal(d1.status, "denied");
154
+
155
+ // pending forever: a fixed clock makes now() === deadline after the first poll,
156
+ // so the do/while runs exactly once and then exits not-decided.
157
+ let t = 1000;
158
+ const clock = () => t;
159
+ const pending = fakeFetch(() => { t += 100000; return { body: { id: "ap-1", status: "pending" } }; });
160
+ const d2 = await awaitDecision("ap-1", 5000, { cfg: CFG, fetchImpl: pending, nowMs: clock });
161
+ assert.equal(d2.decided, false);
162
+ assert.equal(d2.status, "pending");
163
+ });
164
+
165
+ // --- gateActionClass: blocks then unblocks ---------------------------------
166
+
167
+ test("gateActionClass: non-gated → approved immediately, no server call", async () => {
168
+ const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
169
+ const g = await gateActionClass({ cfg: CFG, action: "send_slack", classification: [], payload: {}, fetchImpl: f });
170
+ assert.equal(g.approved, true);
171
+ assert.equal(g.reason, "not-gated");
172
+ assert.equal(f.calls.length, 0);
173
+ });
174
+
175
+ test("gateActionClass: gated action BLOCKS then UNBLOCKS on human approval", async () => {
176
+ const payload = { action: "merge", repo: "core", pr: 42 };
177
+ const expectHash = hashPayload(payload);
178
+ // request → pending; wait → approved (the human approved exactly this payload).
179
+ const f = fakeFetch((url) => {
180
+ if (url.includes("/v1/approval.request")) return { body: { ok: true, result: { id: "ap-m", status: "pending" } } };
181
+ if (url.includes("/v1/approval.wait")) return { body: { id: "ap-m", status: "approved" } };
182
+ return { body: { ok: true, result: null } };
183
+ });
184
+ const g = await gateActionClass({
185
+ cfg: CFG,
186
+ action: "merge_pr",
187
+ classification: "irreversible",
188
+ payload,
189
+ approver: { slackId: "U1" },
190
+ deliverFn: async () => ({ ok: true }),
191
+ timeoutMs: 10000,
192
+ fetchImpl: f,
193
+ });
194
+ assert.equal(g.approved, true);
195
+ assert.equal(g.reason, "approved");
196
+ assert.deepEqual(g.classes, ["irreversible"]);
197
+ // the executor can verify it runs EXACTLY the approved payload
198
+ assert.equal(g.payloadHash, expectHash);
199
+ });
200
+
201
+ test("gateActionClass: denied → not approved with reason 'denied'", async () => {
202
+ const f = fakeFetch((url) => {
203
+ if (url.includes("/v1/approval.request")) return { body: { ok: true, result: { id: "ap-d", status: "pending" } } };
204
+ if (url.includes("/v1/approval.wait")) return { body: { id: "ap-d", status: "denied" } };
205
+ return { body: { ok: true, result: null } };
206
+ });
207
+ const g = await gateActionClass({ cfg: CFG, action: "x", classification: "external", payload: { a: 1 }, timeoutMs: 5000, fetchImpl: f });
208
+ assert.equal(g.approved, false);
209
+ assert.equal(g.reason, "denied");
210
+ });
211
+
212
+ test("gateActionClass: org disabled → not approved, reason org-disabled (never silently passes)", async () => {
213
+ const g = await gateActionClass({ cfg: OFF, action: "x", classification: "financial", payload: { a: 1 } });
214
+ assert.equal(g.approved, false);
215
+ assert.equal(g.reason, "org-disabled");
216
+ });