@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,2535 @@
1
+ /**
2
+ * lib/org/tool-surface.mjs — the curated Cohort org tool table (single SoT).
3
+ *
4
+ * ONE table consumed by BOTH exposure planes so they cannot drift:
5
+ * - the MCP server (`lib/mcp/server.mjs`, bin `cohort-mcp`) — tools/list +
6
+ * tools/call for interactive Claude sessions;
7
+ * - the native function-call path (`lib/tool-definitions.js` category 7 +
8
+ * `lib/action-executor.js` executor routing).
9
+ *
10
+ * 28 curated tools + 5 email tools + 5 artifact tools + 69 agent-desk tools
11
+ * (mail app / files / calendar / crm / books / meetings.recapFile / directory /
12
+ * design / call working sessions — the additive families register only when
13
+ * the vendored protocol carries them — see emailFamilyAvailable /
14
+ * artifactFamilyAvailable / deskFamilyAvailable) — NOT a 406-method
15
+ * one-to-one mirror. The long tail
16
+ * stays reachable through
17
+ * the `org_rpc` / `org_read` escape hatches, which validate against the frozen
18
+ * protocol table before any network I/O and are deliberately `access:"admin"`.
19
+ *
20
+ * Governance parity (spec §1.5):
21
+ * - Tools that emit human-visible outbound content carry `outbound: true`
22
+ * (exactly messaging_send + email_send + email_draft_send +
23
+ * org_call_share_step — the verbs whose params CARRY free text a human
24
+ * will read: the share-step highlight notes / typed text render on every
25
+ * call participant's stage tile). `executeOrgTool`
26
+ * screens them through lib/comms/send-gate.screenOutbound BEFORE dispatch
27
+ * in BOTH planes. Desk verbs that trigger server-TEMPLATED sends
28
+ * (books.invoiceSend / statementRun, calendar invite emails) are NOT
29
+ * outbound-flagged: their params carry no free text to screen, and hq's
30
+ * server-side governance (holds, chase ladders, honest emailQueued/
31
+ * emailSkipped payloads) owns those sends.
32
+ * - `OUTBOUND_METHODS` is DERIVED from the table (never hand-listed), and an
33
+ * `org_rpc` invocation whose method is in that set is screened the same way
34
+ * — the escape hatch is not an unscreened outbound lane.
35
+ * - Audit rows are written by the CALLERS (native: executeAction's
36
+ * appendActionAudit; MCP: the tools/call dispatcher) — one row shape, both
37
+ * planes, no double-logging here.
38
+ *
39
+ * Fail-open like the whole org client: executeOrgTool NEVER throws — transport
40
+ * faults, unknown tools, and missing credentials all come back as the standard
41
+ * `{ok:false, error:{code,message}}` frame.
42
+ *
43
+ * Node builtins only. ESM.
44
+ *
45
+ * @module lib/org/tool-surface
46
+ */
47
+
48
+ "use strict";
49
+
50
+ import { randomUUID } from "node:crypto";
51
+ import { existsSync, readFileSync } from "node:fs";
52
+ import { join } from "node:path";
53
+ import {
54
+ PROTOCOL_VERSION,
55
+ FAMILIES,
56
+ METHODS,
57
+ READS,
58
+ methodDef,
59
+ okFrame,
60
+ errFrame,
61
+ } from "./protocol.mjs";
62
+ import {
63
+ loadOrgConfig,
64
+ configFromAgent,
65
+ call,
66
+ read,
67
+ approvalWait,
68
+ fetchSelfProfile,
69
+ } from "./client.mjs";
70
+
71
+ /** Default org server origin when neither env nor config names one. */
72
+ export const DEFAULT_COHORT_BASE = "https://os.cohortapp.com";
73
+
74
+ /** Long-poll budget for approval_wait (server clamps to ≤55s; hq precedent). */
75
+ const APPROVAL_WAIT_BUDGET_MS = 55_000;
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Config resolution (spec §1.4 — env-first, config-fallback)
79
+ // ---------------------------------------------------------------------------
80
+
81
+ /**
82
+ * Resolve the org client config for a tool invocation. Reuses the EXISTING
83
+ * `configFromAgent` chain (token: config → COHORT_API_TOKEN → COHORT_TOKEN →
84
+ * COHORT_API_KEY; org: config → COHORT_ORG_ID) and adds the base resolution
85
+ * the MCP boot documents: COHORT_BASE → COHORT_API_URL → config base →
86
+ * https://os.cohortapp.com. Fail-open: never throws; missing fields are "".
87
+ *
88
+ * @param {object} [o] - { agentRoot?, orgConfig?, env? }
89
+ * @returns {{base:string, orgId:string, token:string, agentRoot:string}}
90
+ */
91
+ export function resolveOrgToolConfig(o = {}) {
92
+ const env = o.env || process.env;
93
+ const agentRoot =
94
+ o.agentRoot || env.COHORT_AGENT_ROOT || env.AGENT_ROOT || process.cwd();
95
+ const cfg = o.orgConfig || loadOrgConfig(agentRoot);
96
+ const fromCfg = configFromAgent(cfg);
97
+ const base =
98
+ (env.COHORT_BASE && String(env.COHORT_BASE).replace(/\/+$/, "")) ||
99
+ (env.COHORT_API_URL && String(env.COHORT_API_URL).replace(/\/+$/, "")) ||
100
+ fromCfg.base ||
101
+ DEFAULT_COHORT_BASE;
102
+ return { base, orgId: fromCfg.orgId, token: fromCfg.token, agentRoot };
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Email family gate
107
+ // ---------------------------------------------------------------------------
108
+
109
+ /**
110
+ * Is the additive `email` protocol family present in the VENDORED protocol?
111
+ * The five `email_*` tools register only when true — the moment the family
112
+ * lands via sync-protocol the tools appear with no further SDK edit.
113
+ * @returns {boolean}
114
+ */
115
+ export function emailFamilyAvailable() {
116
+ return methodDef("email.send") != null;
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Artifact family gate
121
+ // ---------------------------------------------------------------------------
122
+
123
+ /**
124
+ * Is the additive `artifact` protocol family (generative-UI artifact cards;
125
+ * GU program 2026-07) present in the VENDORED protocol? The five `artifact_*`
126
+ * tools register only when true — same additive-family pattern as email: the
127
+ * moment the family lands via sync-protocol the tools appear with no further
128
+ * SDK edit, and older vendored protocols degrade cleanly.
129
+ * @returns {boolean}
130
+ */
131
+ export function artifactFamilyAvailable() {
132
+ return methodDef("artifact.act") != null;
133
+ }
134
+
135
+ /**
136
+ * Per-desk probe methods: a desk's tools register only when the VENDORED
137
+ * protocol carries that desk's family (2026-07 agent desks). "mail" probes
138
+ * email.draftSend — the Inbox-app surface is additive on the older email
139
+ * family, so a 2.0-vendored protocol (email.send only) keeps the five legacy
140
+ * email_* tools but not the mail-desk ones.
141
+ * @type {Readonly<Record<string,string>>}
142
+ */
143
+ const DESK_PROBES = Object.freeze({
144
+ mail: "email.draftSend",
145
+ files: "files.list",
146
+ calendar: "calendar.list",
147
+ crm: "crm.listDeals",
148
+ books: "books.accounts",
149
+ meetings: "meetings.recapFile",
150
+ // 2026-07 Directory & Design. "design" probes branding.getFoundation rather
151
+ // than a branding.* method that already existed at 2.2.0 — the Brand Studio
152
+ // write surface has shipped since then, so probing an OLD branding method
153
+ // would light the design desk up on a protocol that lacks the read half.
154
+ directory: "directory.search",
155
+ design: "branding.getFoundation",
156
+ // 2026-08 call working sessions. "calls" probes calling.appShareStart, not a
157
+ // calling.* method that already existed at 2.2.0 — the base calling family
158
+ // (lifecycle/react/transcript) has been vendored since SP3, so probing an
159
+ // OLD calling method would light these tools up on a protocol that lacks
160
+ // the app-share surface.
161
+ calls: "calling.appShareStart",
162
+ });
163
+
164
+ /**
165
+ * Is a desk family present in the vendored protocol? Unknown desk → false
166
+ * (default-deny, same posture as the method table).
167
+ * @param {string} desk
168
+ * @returns {boolean}
169
+ */
170
+ export function deskFamilyAvailable(desk) {
171
+ const probe = DESK_PROBES[String(desk || "")];
172
+ return probe ? methodDef(probe) != null : false;
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // The table
177
+ // ---------------------------------------------------------------------------
178
+
179
+ const S = (properties, required = []) => ({ type: "object", properties, required });
180
+ const str = (description) => ({ type: "string", description });
181
+ const num = (description) => ({ type: "number", description });
182
+ const bool = (description) => ({ type: "boolean", description });
183
+ const arr = (items, description) => ({ type: "array", items, description });
184
+
185
+ /**
186
+ * The curated org tool table. Each entry:
187
+ * { name, title, description, input_schema, access: "read"|"write"|"admin",
188
+ * binding: {kind:"rpc",method} | {kind:"read",path} | {kind:"local"},
189
+ * outbound?: true, email?: true }
190
+ *
191
+ * `input_schema` is Claude `tool_use`-shaped (type/properties/required), which
192
+ * the MCP server republishes verbatim as `inputSchema` — one schema, two planes.
193
+ * @type {Array<object>}
194
+ */
195
+ export const ORG_TOOLS = Object.freeze([
196
+ // ── org spine ────────────────────────────────────────────────────────────
197
+ {
198
+ name: "org_whoami",
199
+ title: "Who am I",
200
+ description:
201
+ "This agent's own identity on the Cohort org: member profile (slug, displayName, role) " +
202
+ "resolved server-side via the org-data whoami lane, plus the client config summary " +
203
+ "{base, orgId, protocolVersion, tokenPresent}. Never returns the token. In a multi-agent " +
204
+ "org set COHORT_AGENT_ID (member id/slug) or COHORT_AGENT_EMAIL so resolution is exact; " +
205
+ "unresolvable identity degrades to the config summary with member: null (never an error).",
206
+ input_schema: S({}),
207
+ access: "read",
208
+ binding: { kind: "local" },
209
+ },
210
+ {
211
+ name: "org_directory",
212
+ title: "Org directory",
213
+ description: "The org roster: members + teams from the /v1 directory read.",
214
+ input_schema: S({}),
215
+ access: "read",
216
+ binding: { kind: "read", path: "directory" },
217
+ },
218
+ {
219
+ name: "org_snapshot",
220
+ title: "Org snapshot",
221
+ description: "The signed org state head (GET /v1/snapshot).",
222
+ input_schema: S({}),
223
+ access: "read",
224
+ binding: { kind: "read", path: "snapshot" },
225
+ },
226
+ {
227
+ name: "org_events_tail",
228
+ title: "Org event chain tail",
229
+ description:
230
+ "Read the org event chain (GET /v1/events). Messaging/email payloads are REDACTED " +
231
+ "on-chain — fetch bodies via messaging_history / email_message, never from here.",
232
+ input_schema: S({
233
+ cursor: num("Sequence to read from (exclusive). Default 0."),
234
+ limit: num("Max events to return (server caps at 500)."),
235
+ }),
236
+ access: "read",
237
+ binding: { kind: "read", path: "events" },
238
+ },
239
+ // ── messaging ────────────────────────────────────────────────────────────
240
+ {
241
+ name: "messaging_channels",
242
+ title: "List org channels",
243
+ description: "Org messaging channels visible to this agent, with membership.",
244
+ input_schema: S({}),
245
+ access: "read",
246
+ binding: { kind: "rpc", method: "messaging.channels" },
247
+ },
248
+ {
249
+ name: "messaging_history",
250
+ title: "Channel history",
251
+ description: "Message bodies for one org channel (messaging.history).",
252
+ input_schema: S(
253
+ {
254
+ channelId: str("Channel id."),
255
+ limit: num("Max messages (server default applies)."),
256
+ before: str("Paginate: only messages before this cursor/id."),
257
+ },
258
+ ["channelId"],
259
+ ),
260
+ access: "read",
261
+ binding: { kind: "rpc", method: "messaging.history" },
262
+ },
263
+ {
264
+ name: "messaging_send",
265
+ title: "Send org message",
266
+ description:
267
+ "Post a message into an org channel (messaging.send). Send-gate screened locally; " +
268
+ "the server enforces channel ACLs + pairing. A clientMsgId is auto-minted for dedup.",
269
+ input_schema: S(
270
+ {
271
+ channelId: str("Channel id (use messaging_open_dm for a member DM)."),
272
+ body: str("Message body."),
273
+ threadRootId: str("Reply under this thread root (optional)."),
274
+ },
275
+ ["channelId", "body"],
276
+ ),
277
+ access: "write",
278
+ outbound: true,
279
+ binding: { kind: "rpc", method: "messaging.send" },
280
+ },
281
+ {
282
+ name: "messaging_open_dm",
283
+ title: "Open a DM",
284
+ description:
285
+ "Resolve or create the DM channel with a member (channel.resolveOrCreateDm). " +
286
+ "Returns the channelId to feed messaging_send.",
287
+ input_schema: S({ memberId: str("The member to DM.") }, ["memberId"]),
288
+ access: "write",
289
+ binding: { kind: "rpc", method: "channel.resolveOrCreateDm" },
290
+ },
291
+ // ── board / tasks ────────────────────────────────────────────────────────
292
+ {
293
+ name: "board_ready",
294
+ title: "Claimable work",
295
+ description: "The board ready feed — work this agent can claim (GET /v1/board.ready).",
296
+ input_schema: S({}),
297
+ access: "read",
298
+ binding: { kind: "read", path: "board.ready" },
299
+ },
300
+ {
301
+ name: "board_claim",
302
+ title: "Claim work item",
303
+ description: "Claim a ready board item (board.claim). The server resolves races atomically.",
304
+ input_schema: S({ itemId: str("Board item id.") }, ["itemId"]),
305
+ access: "write",
306
+ binding: { kind: "rpc", method: "board.claim" },
307
+ },
308
+ {
309
+ name: "board_complete",
310
+ title: "Complete work item",
311
+ description: "Mark a claimed board item complete (board.complete).",
312
+ input_schema: S({ itemId: str("Board item id."), note: str("Completion note (optional).") }, ["itemId"]),
313
+ access: "write",
314
+ binding: { kind: "rpc", method: "board.complete" },
315
+ },
316
+ {
317
+ name: "task_create",
318
+ title: "Create task",
319
+ description: "Create a task on the org board (board.createTask).",
320
+ input_schema: S(
321
+ {
322
+ title: str("Task title."),
323
+ description: str("Task body/description (optional)."),
324
+ assignee: str("Member to assign (optional)."),
325
+ status: str("Initial status (optional)."),
326
+ priority: str("Priority (optional)."),
327
+ dueAt: str("Due date ISO 8601 (optional)."),
328
+ },
329
+ ["title"],
330
+ ),
331
+ access: "write",
332
+ binding: { kind: "rpc", method: "board.createTask" },
333
+ },
334
+ {
335
+ name: "task_update",
336
+ title: "Update task",
337
+ description:
338
+ "Update a task's status/fields, or move it into a workstream lane (board.updateTask). Pass workstreamName to assign a task that's sitting in no lane — reuses an existing lane by name or creates it.",
339
+ input_schema: S(
340
+ {
341
+ taskId: str("Task id."),
342
+ status: str("New status (optional)."),
343
+ title: str("New title (optional)."),
344
+ description: str("New description (optional)."),
345
+ assignee: str("New assignee (optional)."),
346
+ priority: str("New priority (optional)."),
347
+ dueAt: str("New due date ISO 8601 (optional)."),
348
+ workstreamName: str(
349
+ "Move the task into this workstream lane — reuses an existing lane by name or creates it (optional).",
350
+ ),
351
+ },
352
+ ["taskId"],
353
+ ),
354
+ access: "write",
355
+ binding: { kind: "rpc", method: "board.updateTask" },
356
+ },
357
+ {
358
+ name: "task_comment",
359
+ title: "Comment on task",
360
+ description: "Add a comment to a task (board.addTaskComment).",
361
+ input_schema: S({ taskId: str("Task id."), body: str("Comment body.") }, ["taskId", "body"]),
362
+ access: "write",
363
+ binding: { kind: "rpc", method: "board.addTaskComment" },
364
+ },
365
+ // ── decisions / approvals ────────────────────────────────────────────────
366
+ {
367
+ name: "decision_list",
368
+ title: "List decisions",
369
+ description: "The org decision registry (GET /v1/decision.list).",
370
+ input_schema: S({ status: str("Filter by status (optional).") }),
371
+ access: "read",
372
+ binding: { kind: "read", path: "decision.list" },
373
+ },
374
+ {
375
+ name: "decision_propose",
376
+ title: "Propose decision",
377
+ description: "Propose a decision into the org registry (decision.propose).",
378
+ input_schema: S(
379
+ {
380
+ title: str("Decision title."),
381
+ rationale: str("Why (optional)."),
382
+ scope: str("Scope (optional)."),
383
+ },
384
+ ["title"],
385
+ ),
386
+ access: "write",
387
+ binding: { kind: "rpc", method: "decision.propose" },
388
+ },
389
+ {
390
+ name: "decision_sign",
391
+ title: "Sign decision",
392
+ description: "Sign a proposed decision (decision.sign).",
393
+ input_schema: S({ decisionId: str("Decision id.") }, ["decisionId"]),
394
+ access: "write",
395
+ binding: { kind: "rpc", method: "decision.sign" },
396
+ },
397
+ {
398
+ name: "decision_comment",
399
+ title: "Comment on decision",
400
+ description: "Comment on a decision (decision.comment).",
401
+ input_schema: S({ decisionId: str("Decision id."), body: str("Comment body.") }, ["decisionId", "body"]),
402
+ access: "write",
403
+ binding: { kind: "rpc", method: "decision.comment" },
404
+ },
405
+ {
406
+ name: "approval_request",
407
+ title: "Request approval",
408
+ description:
409
+ "File an approval request (approval.request) for a gated action — e.g. the " +
410
+ "external_comms first-contact flow email_send returns. Requester ≠ approver is enforced server-side.",
411
+ input_schema: S(
412
+ {
413
+ actionClass: str("Action class, e.g. \"external_comms\"."),
414
+ payload: { type: "object", description: "The intent payload the approval binds (hash-bound server-side)." },
415
+ subject: str("Human-readable subject (optional)."),
416
+ payload_hash: str("Precomputed sha256 payload hash (optional — pass through from a structured approval_required result)."),
417
+ },
418
+ ["actionClass"],
419
+ ),
420
+ access: "write",
421
+ binding: { kind: "rpc", method: "approval.request" },
422
+ },
423
+ {
424
+ name: "approval_wait",
425
+ title: "Wait for approval",
426
+ description:
427
+ "Bounded long-poll (≤55s) for an approval outcome (GET /v1/approval.wait). " +
428
+ "Returns the current state either way; poll again while status is pending.",
429
+ input_schema: S({ approvalId: str("Approval id."), timeoutMs: num("Wait budget ms (clamped ≤55000).") }, ["approvalId"]),
430
+ access: "read",
431
+ binding: { kind: "local" }, // dispatched via approvalWait (long-poll budget)
432
+ },
433
+ // ── knowledge / memory ───────────────────────────────────────────────────
434
+ {
435
+ name: "knowledge_search",
436
+ title: "Search org knowledge",
437
+ description:
438
+ "Search the ACL'd shared org knowledge plane (knowledge.search). Every retrieval is " +
439
+ "audit-logged server-side; the server decides what this agent may see.",
440
+ input_schema: S({ query: str("Free-text query."), limit: num("Max hits (optional).") }, ["query"]),
441
+ access: "read",
442
+ binding: { kind: "rpc", method: "knowledge.search" },
443
+ },
444
+ {
445
+ name: "knowledge_append",
446
+ title: "Append org knowledge",
447
+ description: "Append a knowledge episode to the shared plane (knowledge.append).",
448
+ input_schema: S(
449
+ {
450
+ text: str("The fact/episode text."),
451
+ kind: str("Kind (optional)."),
452
+ group: str("Group hint (server enforces ACL; optional)."),
453
+ },
454
+ ["text"],
455
+ ),
456
+ access: "write",
457
+ binding: { kind: "rpc", method: "knowledge.append" },
458
+ },
459
+ {
460
+ name: "memory_author",
461
+ title: "Author org memory",
462
+ description: "Write to the org memory plane (memory.author).",
463
+ input_schema: S(
464
+ {
465
+ content: str("Memory content."),
466
+ kind: str("Memory kind (optional)."),
467
+ tags: arr({ type: "string" }, "Tags (optional)."),
468
+ },
469
+ ["content"],
470
+ ),
471
+ access: "write",
472
+ binding: { kind: "rpc", method: "memory.author" },
473
+ },
474
+ // ── members / escalation ─────────────────────────────────────────────────
475
+ {
476
+ name: "member_profile",
477
+ title: "Member profile",
478
+ description: "One member's full profile (member.get).",
479
+ input_schema: S({ memberId: str("Member id or slug.") }, ["memberId"]),
480
+ access: "read",
481
+ binding: { kind: "rpc", method: "member.get" },
482
+ },
483
+ {
484
+ name: "escalation_raise",
485
+ title: "Raise escalation",
486
+ description: "Raise an escalation to the org (escalation.create).",
487
+ input_schema: S(
488
+ {
489
+ severity: str("Severity, e.g. low|medium|high|critical."),
490
+ subject: str("Escalation subject."),
491
+ body: str("Details."),
492
+ },
493
+ ["severity", "subject", "body"],
494
+ ),
495
+ access: "write",
496
+ binding: { kind: "rpc", method: "escalation.create" },
497
+ },
498
+ // ── discoverability + escape hatches ─────────────────────────────────────
499
+ {
500
+ name: "org_describe",
501
+ title: "Describe the org protocol",
502
+ description:
503
+ "Discoverability (works offline): list the protocol FAMILIES, methods " +
504
+ "({name, family, scope, sideEffecting, idempotent}) and GET reads from the vendored " +
505
+ "protocol table. Optional {family} filter. Use this to find the long tail before " +
506
+ "reaching for org_rpc.",
507
+ input_schema: S({ family: str("Filter to one family (optional).") }),
508
+ access: "read",
509
+ binding: { kind: "local" },
510
+ },
511
+ {
512
+ name: "org_rpc",
513
+ title: "Raw org RPC (escape hatch)",
514
+ description:
515
+ "Invoke ANY protocol method by name (the ~170 methods without a curated tool). The " +
516
+ "method is validated against the frozen protocol table before any network I/O; hq's " +
517
+ "server-side scope gate still applies (this tool adds no privilege). Outbound-content " +
518
+ "methods (messaging.send, email.send) are send-gate screened before dispatch. Prefer " +
519
+ "the curated tools when one exists.",
520
+ input_schema: S(
521
+ {
522
+ method: str("Full dotted method name, e.g. \"contacts.upsert\"."),
523
+ params: { type: "object", description: "The params object (request body)." },
524
+ idempotencyKey: str("Stable idempotency key for side-effecting retries (optional)."),
525
+ },
526
+ ["method"],
527
+ ),
528
+ access: "admin",
529
+ binding: { kind: "local" }, // dispatched through call() after validation
530
+ },
531
+ {
532
+ name: "org_read",
533
+ title: "Raw org GET read (escape hatch)",
534
+ description:
535
+ "Invoke one of the protocol GET reads by path (snapshot, directory, events, hierarchy, " +
536
+ "board.ready, board.context, ops, approval.wait, cost.rollup, decision.list, policy, " +
537
+ "contacts.list, meetings.list), with optional query params. Validated against READS " +
538
+ "before any network I/O.",
539
+ input_schema: S(
540
+ {
541
+ path: str("Read path, e.g. \"events\"."),
542
+ query: { type: "object", description: "Query params, e.g. {cursor: 0, limit: 100}." },
543
+ },
544
+ ["path"],
545
+ ),
546
+ access: "admin",
547
+ binding: { kind: "local" },
548
+ },
549
+ // ── email (workspace mailbox; registered only when the family is vendored) ─
550
+ {
551
+ name: "email_send",
552
+ title: "Send workspace email",
553
+ description:
554
+ "Send email from this agent's workspace mailbox (email.send; outbox-queued server-side). " +
555
+ "Thread replies may omit to/subject — the server derives them from the thread. May return " +
556
+ "a NORMAL result {status:\"approval_required\", approval:{actionClass:\"external_comms\", " +
557
+ "payloadHash, intent}} for first-contact recipients: call approval_request with that exact " +
558
+ "payload, approval_wait, then re-send with approvalId and the SAME idempotencyId. An " +
559
+ "idempotencyId is auto-minted when absent.",
560
+ input_schema: S(
561
+ {
562
+ to: arr({ type: "string" }, "Recipient addresses (≤25). Optional for thread replies."),
563
+ cc: arr({ type: "string" }, "Cc addresses (optional)."),
564
+ bcc: arr({ type: "string" }, "Bcc addresses (optional)."),
565
+ subject: str("Subject (optional for thread replies)."),
566
+ text: str("Plain-text body."),
567
+ html: str("HTML body (optional)."),
568
+ threadId: str("Reply into this thread (optional)."),
569
+ replyToMessageId: str("Reply to this specific message (optional)."),
570
+ approvalId: str("Consumed external_comms approval for first-contact sends (optional)."),
571
+ idempotencyId: str("Business dedup id 8..64 chars (auto-minted when absent)."),
572
+ },
573
+ ["text"],
574
+ ),
575
+ access: "write",
576
+ outbound: true,
577
+ email: true,
578
+ binding: { kind: "rpc", method: "email.send" },
579
+ },
580
+ {
581
+ name: "email_inbox",
582
+ title: "Workspace inbox",
583
+ description:
584
+ "Keyset-paged own-mailbox listing (email.inbox) — summaries + snippet; " +
585
+ "bodies via email_message.",
586
+ input_schema: S({
587
+ unreadOnly: bool("Only unread messages."),
588
+ cursor: str("Keyset cursor from a prior page."),
589
+ limit: num("Page size 1..100 (default 50)."),
590
+ }),
591
+ access: "read",
592
+ email: true,
593
+ binding: { kind: "rpc", method: "email.inbox" },
594
+ },
595
+ {
596
+ name: "email_message",
597
+ title: "Read email",
598
+ description: "Full body/headers/attachment-metadata for one own-mailbox message (email.message).",
599
+ input_schema: S({ id: str("EmailMessage id.") }, ["id"]),
600
+ access: "read",
601
+ email: true,
602
+ binding: { kind: "rpc", method: "email.message" },
603
+ },
604
+ {
605
+ name: "email_thread",
606
+ title: "Read email thread",
607
+ description: "A whole email thread with bodies, ascending (email.thread).",
608
+ input_schema: S({ threadId: str("Thread id."), limit: num("Max messages (default 50).") }, ["threadId"]),
609
+ access: "read",
610
+ email: true,
611
+ binding: { kind: "rpc", method: "email.thread" },
612
+ },
613
+ {
614
+ name: "email_mark_read",
615
+ title: "Mark email read",
616
+ description:
617
+ "Advance the server-side read cursor (email.markRead) for one message or a whole thread " +
618
+ "(exactly one of messageId | threadId).",
619
+ input_schema: S({ messageId: str("Message id."), threadId: str("Thread id.") }),
620
+ access: "write",
621
+ email: true,
622
+ binding: { kind: "rpc", method: "email.markRead" },
623
+ },
624
+ // ── artifact (generative-UI cards; registered only when the family is vendored) ─
625
+ {
626
+ name: "artifact_create",
627
+ title: "Post artifact card",
628
+ description:
629
+ "Post a generative-UI artifact into an org channel (artifact.create) — an interactive " +
630
+ "card message whose every rendered button is equally invocable via artifact_act. Returns " +
631
+ "{artifactId, messageId}. The envelope is genui/v2: {genui:\"v2\", artifact:{class, version, " +
632
+ "state: draft|streaming|complete|acted|expired|error, source:{kind:\"native\"|\"plugin\", ...}, " +
633
+ "provenance?:{sources[], counts?, costCents?, generatedAt}, ttlAt?, deepLink?}, " +
634
+ "actions:[≤12 descriptors], root:{type:\"card\", tone, title?, blocks:[...]}}; simple static " +
635
+ "cards may use genui/v1 {genui:\"v1\", root:card}. Discover the native classes, their " +
636
+ "envelope versions and action descriptors via artifact_catalog FIRST. For slow data post " +
637
+ "state:\"streaming\" with skeleton blocks, then hydrate via artifact.patch (org_rpc) to " +
638
+ "state:\"complete\". A clientMsgId is auto-minted for dedup.",
639
+ input_schema: S(
640
+ {
641
+ channelId: str("Channel id to post the artifact message into."),
642
+ envelope: {
643
+ type: "object",
644
+ description: "The genui/v2 envelope object ({genui, artifact, actions, root}); genui/v1 {genui:\"v1\", root} also accepted.",
645
+ },
646
+ threadRootId: str("Post under this thread root (optional)."),
647
+ clientMsgId: str("Business dedup id (auto-minted when absent)."),
648
+ },
649
+ ["channelId", "envelope"],
650
+ ),
651
+ access: "write",
652
+ artifact: true,
653
+ binding: { kind: "rpc", method: "artifact.create" },
654
+ },
655
+ {
656
+ name: "artifact_act",
657
+ title: "Act on artifact",
658
+ description:
659
+ "Invoke an action on an artifact (artifact.act) — the SAME action a human clicking the " +
660
+ "rendered button invokes, with the same params schema (see artifact_catalog). Address by " +
661
+ "artifactId or messageId. The result carries the action outcome; {held:true, approvalId} " +
662
+ "means the action is approval-gated — approval_wait that id, then re-invoke with the SAME " +
663
+ "idempotencyKey to consume the grant; {replay:true} marks an idempotent replay (the action " +
664
+ "already ran under this key). An idempotencyKey is auto-minted when absent — pass your own " +
665
+ "stable key whenever you may retry.",
666
+ input_schema: S(
667
+ {
668
+ artifactId: str("Artifact id (pass this or messageId)."),
669
+ messageId: str("The artifact's message id (alternative address)."),
670
+ action: str("Action name from the artifact's action descriptors / the catalog."),
671
+ params: { type: "object", description: "Action params, per the catalog's JSON Schema for this action (optional)." },
672
+ idempotencyKey: str("Stable dedup key (auto-minted when absent; reuse it to retry or to re-invoke after approval)."),
673
+ },
674
+ ["action"],
675
+ ),
676
+ access: "write",
677
+ artifact: true,
678
+ binding: { kind: "rpc", method: "artifact.act" },
679
+ },
680
+ {
681
+ name: "artifact_get",
682
+ title: "Read artifact",
683
+ description:
684
+ "One artifact row (artifact.get): envelope, state, class, messageId, channelId. Use it to " +
685
+ "re-read current state and any receipt after acting/patching, or before acting on an " +
686
+ "artifact another agent posted.",
687
+ input_schema: S({ artifactId: str("Artifact id.") }, ["artifactId"]),
688
+ access: "read",
689
+ artifact: true,
690
+ binding: { kind: "rpc", method: "artifact.get" },
691
+ },
692
+ {
693
+ name: "artifact_list",
694
+ title: "List artifacts",
695
+ description:
696
+ "List artifact rows (artifact.list), filterable by channelId, class, and lifecycle state " +
697
+ "(draft|streaming|complete|acted|expired|error); newest first, server-capped limit.",
698
+ input_schema: S({
699
+ channelId: str("Only artifacts in this channel (optional)."),
700
+ class: str("Only this artifact class, e.g. \"governance.approval\" (optional)."),
701
+ state: str("Only this lifecycle state (optional)."),
702
+ limit: num("Max rows (server caps; optional)."),
703
+ }),
704
+ access: "read",
705
+ artifact: true,
706
+ binding: { kind: "rpc", method: "artifact.list" },
707
+ },
708
+ {
709
+ name: "artifact_catalog",
710
+ title: "Artifact class catalog",
711
+ description:
712
+ "THE discovery surface for generative UI (artifact.catalog): the org's native artifact " +
713
+ "classes — {version, classes:[{class, version, title, describe, actions:[{name, label, " +
714
+ "kind, description?, params? (JSON Schema), confirm?, gate?, audience, roles?}]}]}. Call " +
715
+ "this BEFORE composing a native-class artifact_create or an artifact_act so the class " +
716
+ "version, action names, and params match the server's registry; `describe` says when to " +
717
+ "use each class.",
718
+ input_schema: S({}),
719
+ access: "read",
720
+ artifact: true,
721
+ binding: { kind: "rpc", method: "artifact.catalog" },
722
+ },
723
+ // ── mail desk (the hq Inbox app; registered when the family is vendored) ──
724
+ {
725
+ name: "email_mailboxes",
726
+ title: "List mailboxes",
727
+ description:
728
+ "The mailboxes/viewpoints this seat is entitled to (email.mailboxes), with unread and " +
729
+ "held-approval counts. Start here — every other mail-desk tool takes a mailboxId from this list.",
730
+ input_schema: S({}),
731
+ access: "read",
732
+ desk: "mail",
733
+ binding: { kind: "rpc", method: "email.mailboxes" },
734
+ },
735
+ {
736
+ name: "email_threads",
737
+ title: "List mail threads",
738
+ description:
739
+ "Thread rows for one mailbox (email.threads). folder is inbox|archive|starred|assigned|" +
740
+ "tag:<name>|<folderId>. Summaries only — bodies via email_thread_messages.",
741
+ input_schema: S(
742
+ {
743
+ mailboxId: str("Mailbox id from email_mailboxes."),
744
+ folder: str("inbox | archive | starred | assigned | tag:<name> | a folder id (default inbox)."),
745
+ unreadOnly: bool("Only unread threads."),
746
+ limit: num("Max rows 1..200 (default 50)."),
747
+ },
748
+ ["mailboxId"],
749
+ ),
750
+ access: "read",
751
+ desk: "mail",
752
+ binding: { kind: "rpc", method: "email.threads" },
753
+ },
754
+ {
755
+ name: "email_thread_messages",
756
+ title: "Read mail thread",
757
+ description:
758
+ "All messages of one Inbox-app thread with bodies (email.messages; bcc visible on " +
759
+ "OUTBOUND only). NB distinct from email_thread (the agent's own workspace mailbox).",
760
+ input_schema: S({ threadId: str("Thread id from email_threads.") }, ["threadId"]),
761
+ access: "read",
762
+ desk: "mail",
763
+ binding: { kind: "rpc", method: "email.messages" },
764
+ },
765
+ {
766
+ name: "email_triage",
767
+ title: "Triage mail thread",
768
+ description:
769
+ "One triage facet per call (email.triage): set priority (P0|P1|P2), category, tags, " +
770
+ "starred, read, OR assigneeMemberId — the same one-verb-per-call rule the Inbox UI applies.",
771
+ input_schema: S(
772
+ {
773
+ threadId: str("Thread id."),
774
+ priority: str("P0 | P1 | P2 (or null to clear)."),
775
+ category: str("Category label (optional)."),
776
+ tags: arr({ type: "string" }, "Replace the tag set (≤12; optional)."),
777
+ starred: bool("Star/unstar."),
778
+ read: bool("Mark read/unread."),
779
+ assigneeMemberId: str("Assign to this member (null to unassign)."),
780
+ },
781
+ ["threadId"],
782
+ ),
783
+ access: "write",
784
+ desk: "mail",
785
+ binding: { kind: "rpc", method: "email.triage" },
786
+ },
787
+ {
788
+ name: "email_move",
789
+ title: "Move mail thread",
790
+ description:
791
+ "Move a thread (email.move): target inbox|archive|delete|restore|snooze|folder " +
792
+ "(folderId required for folder; snoozeUntil ISO for snooze).",
793
+ input_schema: S(
794
+ {
795
+ threadId: str("Thread id."),
796
+ target: str("inbox | archive | delete | restore | snooze | folder."),
797
+ folderId: str("Destination folder id (target folder)."),
798
+ snoozeUntil: str("ISO datetime (target snooze)."),
799
+ },
800
+ ["threadId", "target"],
801
+ ),
802
+ access: "write",
803
+ desk: "mail",
804
+ binding: { kind: "rpc", method: "email.move" },
805
+ },
806
+ {
807
+ name: "email_draft_save",
808
+ title: "Save mail draft",
809
+ description:
810
+ "Create or update a draft in any entitled mailbox (email.draftSave). Attachments may " +
811
+ "carry workspaceFileId for live drive links. Nothing sends — pair with email_draft_send.",
812
+ input_schema: S(
813
+ {
814
+ draftId: str("Existing draft to update (omit to create)."),
815
+ mailboxId: str("Mailbox id."),
816
+ threadId: str("Reply thread (optional)."),
817
+ mode: str("compose | reply | replyAll | forward."),
818
+ to: arr({ type: "string" }, "To addresses (≤25)."),
819
+ cc: arr({ type: "string" }, "Cc addresses."),
820
+ bcc: arr({ type: "string" }, "Bcc addresses."),
821
+ subject: str("Subject."),
822
+ bodyText: str("Plain-text body."),
823
+ bodyHtml: str("HTML body (optional)."),
824
+ attachments: {
825
+ type: "array",
826
+ items: { type: "object", properties: { name: { type: "string" }, workspaceFileId: { type: "string" }, contentType: { type: "string" }, size: { type: "number" } }, required: ["name"] },
827
+ description: "Attachments (≤8); workspaceFileId links a drive file.",
828
+ },
829
+ },
830
+ ["mailboxId", "mode"],
831
+ ),
832
+ access: "write",
833
+ desk: "mail",
834
+ binding: { kind: "rpc", method: "email.draftSave" },
835
+ },
836
+ {
837
+ name: "email_draft_send",
838
+ title: "Send mail (governed)",
839
+ description:
840
+ "Compose/reply/forward from ANY entitled mailbox through the governance path " +
841
+ "(email.draftSend). Unlike email_send (the agent's own mailbox + first-contact approvals), " +
842
+ "this rides the Inbox domain layer: when mailbox rules or a per-thread hold trip, the " +
843
+ "result is {status:\"held\", draftId, policyCite} and the draft lands in the guardian's " +
844
+ "approvals queue — NEVER a silent send. Send-gate screened locally before dispatch.",
845
+ input_schema: S(
846
+ {
847
+ mailboxId: str("Mailbox to send from."),
848
+ threadId: str("Reply thread (optional for compose)."),
849
+ mode: str("compose | reply | replyAll | forward (default reply)."),
850
+ to: arr({ type: "string" }, "Recipient addresses (≤25)."),
851
+ cc: arr({ type: "string" }, "Cc addresses."),
852
+ bcc: arr({ type: "string" }, "Bcc addresses."),
853
+ subject: str("Subject (optional for replies)."),
854
+ bodyText: str("Plain-text body."),
855
+ bodyHtml: str("HTML body (optional)."),
856
+ attachments: {
857
+ type: "array",
858
+ items: { type: "object", properties: { name: { type: "string" }, workspaceFileId: { type: "string" } }, required: ["name"] },
859
+ description: "Attachments (≤8).",
860
+ },
861
+ draftId: str("Send this saved draft (optional)."),
862
+ },
863
+ ["mailboxId", "to", "bodyText"],
864
+ ),
865
+ access: "write",
866
+ outbound: true,
867
+ desk: "mail",
868
+ binding: { kind: "rpc", method: "email.draftSend" },
869
+ },
870
+ {
871
+ name: "email_report_spam",
872
+ title: "Report spam / not spam",
873
+ description:
874
+ "Report a thread as spam — or clear it with spam:false (email.reportSpam). Stamps the " +
875
+ "disposition AND maintains the mailbox's Blocked-senders rule, so future mail from the " +
876
+ "sender files as spam on arrival. Returns the blocked address for honest relay.",
877
+ input_schema: S(
878
+ {
879
+ threadId: str("Thread id."),
880
+ spam: bool("true = report spam (default); false = not spam (unblocks the sender)."),
881
+ },
882
+ ["threadId"],
883
+ ),
884
+ access: "write",
885
+ desk: "mail",
886
+ binding: { kind: "rpc", method: "email.reportSpam" },
887
+ },
888
+ {
889
+ name: "email_react",
890
+ title: "React to a mail message",
891
+ description:
892
+ "Toggle YOUR reaction pill on one message (email.react): one of 👍 ✅ 🎉 ❤️ 😂 👀. A new " +
893
+ "emoji replaces yours; the same emoji removes it. A reachable colleague sender is " +
894
+ "notified through the notification lane — the result's `notified` says so honestly.",
895
+ input_schema: S(
896
+ {
897
+ messageId: str("Message id (from email_thread_messages)."),
898
+ emoji: str("One of: 👍 ✅ 🎉 ❤️ 😂 👀."),
899
+ },
900
+ ["messageId", "emoji"],
901
+ ),
902
+ access: "write",
903
+ desk: "mail",
904
+ binding: { kind: "rpc", method: "email.react" },
905
+ },
906
+ {
907
+ name: "email_move_mailbox",
908
+ title: "Move thread to another desk",
909
+ description:
910
+ "Reassign a thread to ANOTHER mailbox you hold (email.moveMailbox) — the mailbox sheet's " +
911
+ "'Move to…'. ACL is enforced on BOTH desks in-domain; folder placement and rule filing " +
912
+ "reset so the destination desk's filing rules take over.",
913
+ input_schema: S(
914
+ {
915
+ threadId: str("Thread id."),
916
+ mailboxId: str("DESTINATION mailbox id (from email_mailboxes)."),
917
+ },
918
+ ["threadId", "mailboxId"],
919
+ ),
920
+ access: "write",
921
+ desk: "mail",
922
+ binding: { kind: "rpc", method: "email.moveMailbox" },
923
+ },
924
+ {
925
+ name: "email_summarise",
926
+ title: "Summarise mail thread (stored)",
927
+ description:
928
+ "Generate AND STORE the thread's grounded brief (email.summarise) — the same aiSummary " +
929
+ "card the mobile Inbox shows, written through the governed lane: gateway-metered " +
930
+ "(costCents + auditId returned; relay them) with one chain event. Re-running refreshes " +
931
+ "the brief; prior reply suggestions are preserved.",
932
+ input_schema: S({ threadId: str("Thread id.") }, ["threadId"]),
933
+ access: "write",
934
+ desk: "mail",
935
+ binding: { kind: "rpc", method: "email.summarise" },
936
+ },
937
+ {
938
+ name: "email_ask",
939
+ title: "Ask the mail desk",
940
+ description:
941
+ "Grounded ask over your entitled mailboxes (email.ask): desks, recent thread rows " +
942
+ "(triage-level — never bodies) and held approvals form the context. Result carries " +
943
+ "content + grounding + costCents + auditId — cite them. mailboxId narrows to one desk.",
944
+ input_schema: S(
945
+ {
946
+ question: str("The question (≤500 chars)."),
947
+ mailboxId: str("Ground in this one desk only (optional)."),
948
+ },
949
+ ["question"],
950
+ ),
951
+ access: "read",
952
+ desk: "mail",
953
+ binding: { kind: "rpc", method: "email.ask" },
954
+ },
955
+ // ── files desk (workspace drive; registered when the family is vendored) ──
956
+ {
957
+ name: "files_list",
958
+ title: "List drive files",
959
+ description:
960
+ "The workspace drive listing (files.list) with the human surface's filters: folder " +
961
+ "scope, kind, starred, agent-owned, free-text q; deleted:true shows the recovery bin.",
962
+ input_schema: S({
963
+ parentId: str("Folder scope (null/omit = root)."),
964
+ kind: str("FOLDER | DOC | SHEET | DECK | PDF | IMAGE | FILE."),
965
+ starred: bool("Only starred."),
966
+ byAgents: bool("Only agent-owned files."),
967
+ q: str("Name contains (case-insensitive)."),
968
+ deleted: bool("Show soft-deleted (30-day bin)."),
969
+ limit: num("Max rows 1..200 (default 100)."),
970
+ }),
971
+ access: "read",
972
+ desk: "files",
973
+ binding: { kind: "rpc", method: "files.list" },
974
+ },
975
+ {
976
+ name: "files_get",
977
+ title: "Get drive file",
978
+ description:
979
+ "One file with the acting seat's effective permission and its shares (files.get). " +
980
+ "Shares/ACLs (VIEW/COMMENT/EDIT) govern agents exactly as humans.",
981
+ input_schema: S({ fileId: str("File id.") }, ["fileId"]),
982
+ access: "read",
983
+ desk: "files",
984
+ binding: { kind: "rpc", method: "files.get" },
985
+ },
986
+ {
987
+ name: "files_doc_read",
988
+ title: "Read doc",
989
+ description:
990
+ "A DOC's HEAD revision as HTML (files.docRead). Sheets/decks: org_rpc files.sheetRead / " +
991
+ "files.deckRead.",
992
+ input_schema: S({ fileId: str("DOC file id.") }, ["fileId"]),
993
+ access: "read",
994
+ desk: "files",
995
+ binding: { kind: "rpc", method: "files.docRead" },
996
+ },
997
+ {
998
+ name: "files_create",
999
+ title: "Create file/folder",
1000
+ description: "Create a DOC, SHEET, DECK, or FOLDER in the workspace drive (files.create).",
1001
+ input_schema: S(
1002
+ {
1003
+ kind: str("DOC | SHEET | DECK | FOLDER."),
1004
+ name: str("Name (≤200 chars)."),
1005
+ parentId: str("Parent folder (omit for root)."),
1006
+ },
1007
+ ["kind", "name"],
1008
+ ),
1009
+ access: "write",
1010
+ desk: "files",
1011
+ binding: { kind: "rpc", method: "files.create" },
1012
+ },
1013
+ {
1014
+ name: "files_doc_write",
1015
+ title: "Write doc revision",
1016
+ description:
1017
+ "Write a DOC revision (files.docWriteRevision; EDIT permission required — bumps HEAD + " +
1018
+ "DocRevision). Set aiDrafted + costCents when the content came from a model call.",
1019
+ input_schema: S(
1020
+ {
1021
+ fileId: str("DOC file id."),
1022
+ html: str("The full new HTML body (≤400KB)."),
1023
+ summary: str("Revision summary (optional)."),
1024
+ aiDrafted: bool("Content was AI-drafted."),
1025
+ costCents: num("Model cost in cents (optional)."),
1026
+ },
1027
+ ["fileId", "html"],
1028
+ ),
1029
+ access: "write",
1030
+ desk: "files",
1031
+ binding: { kind: "rpc", method: "files.docWriteRevision" },
1032
+ },
1033
+ {
1034
+ name: "files_share_grant",
1035
+ title: "Share drive file",
1036
+ description:
1037
+ "Grant VIEW/COMMENT/EDIT on a file or folder (files.shareGrant; folder grants " +
1038
+ "materialize inherited rows). Subjects: members, channels, or external emails.",
1039
+ input_schema: S(
1040
+ {
1041
+ fileId: str("File or folder id."),
1042
+ permission: str("VIEW | COMMENT | EDIT."),
1043
+ subjects: {
1044
+ type: "array",
1045
+ items: {
1046
+ type: "object",
1047
+ properties: {
1048
+ type: { type: "string", description: "member | channel | email" },
1049
+ memberId: { type: "string" },
1050
+ channelId: { type: "string" },
1051
+ email: { type: "string" },
1052
+ },
1053
+ required: ["type"],
1054
+ },
1055
+ description: "Who to share with.",
1056
+ },
1057
+ message: str("Share message (optional)."),
1058
+ },
1059
+ ["fileId", "permission", "subjects"],
1060
+ ),
1061
+ access: "write",
1062
+ desk: "files",
1063
+ binding: { kind: "rpc", method: "files.shareGrant" },
1064
+ },
1065
+ {
1066
+ name: "files_ask",
1067
+ title: "Ask the drive",
1068
+ description:
1069
+ "Grounded ask over the workspace drive (files.ask): file names, kinds, owners, share/" +
1070
+ "expiry facts — never file CONTENT (read a file via its own lane). Result carries " +
1071
+ "content + grounding + costCents + auditId — cite them.",
1072
+ input_schema: S(
1073
+ {
1074
+ question: str("The question (≤500 chars)."),
1075
+ limit: num("How many recent files to ground in, 4..30 (default 24)."),
1076
+ },
1077
+ ["question"],
1078
+ ),
1079
+ access: "read",
1080
+ desk: "files",
1081
+ binding: { kind: "rpc", method: "files.ask" },
1082
+ },
1083
+ // ── calendar desk (registered when the family is vendored) ──
1084
+ {
1085
+ name: "calendar_list",
1086
+ title: "List calendar events",
1087
+ description:
1088
+ "Events in a window with attendee rows (calendar.list). Defaults to the acting seat's " +
1089
+ "calendar; calendarMemberId targets a colleague's, subject to visibility.",
1090
+ input_schema: S({
1091
+ fromIso: str("Window start ISO (optional)."),
1092
+ toIso: str("Window end ISO (optional)."),
1093
+ calendarMemberId: str("Whose calendar (optional)."),
1094
+ limit: num("Max rows 1..500 (default 200)."),
1095
+ }),
1096
+ access: "read",
1097
+ desk: "calendar",
1098
+ binding: { kind: "rpc", method: "calendar.list" },
1099
+ },
1100
+ {
1101
+ name: "calendar_get",
1102
+ title: "Get calendar event",
1103
+ description: "One event with attendee/RSVP rows and linked records (calendar.get).",
1104
+ input_schema: S({ eventId: str("Event id.") }, ["eventId"]),
1105
+ access: "read",
1106
+ desk: "calendar",
1107
+ binding: { kind: "rpc", method: "calendar.get" },
1108
+ },
1109
+ {
1110
+ name: "calendar_find_a_time",
1111
+ title: "Find a time",
1112
+ description:
1113
+ "Deterministic free-slot scan across attendees (calendar.findATime): 15-min lattice, " +
1114
+ "Mon–Fri 08–18, ≤20 attendees. Proposes times — changes nothing.",
1115
+ input_schema: S(
1116
+ {
1117
+ attendeeMemberIds: arr({ type: "string" }, "Member ids to intersect (1..20)."),
1118
+ durationMinutes: num("Meeting length 15..480."),
1119
+ fromDay: str("First day to scan (ISO date)."),
1120
+ scanDays: num("Days to scan 1..21 (default 7)."),
1121
+ },
1122
+ ["attendeeMemberIds", "durationMinutes", "fromDay"],
1123
+ ),
1124
+ access: "read",
1125
+ desk: "calendar",
1126
+ binding: { kind: "rpc", method: "calendar.findATime" },
1127
+ },
1128
+ {
1129
+ name: "calendar_create",
1130
+ title: "Create calendar event",
1131
+ description:
1132
+ "Create a typed event (calendar.create): MEETING/REVIEW/FOCUS/AGENT_BLOCK/ALL_HANDS/HOLD. " +
1133
+ "Queues REAL invite emails to attendees (invitesQueued/invitesSkipped honest in the payload). " +
1134
+ "companyCalendar:true targets the shared calendar; AGENT_BLOCK carries estCostCents.",
1135
+ input_schema: S(
1136
+ {
1137
+ title: str("Event title."),
1138
+ kind: str("MEETING | REVIEW | FOCUS | AGENT_BLOCK | ALL_HANDS | HOLD (default MEETING)."),
1139
+ startsAt: str("Start ISO datetime."),
1140
+ endsAt: str("End ISO datetime."),
1141
+ allDay: bool("All-day event."),
1142
+ location: str("Location (optional)."),
1143
+ description: str("Description/agenda (optional)."),
1144
+ companyCalendar: bool("Put on the shared company calendar."),
1145
+ calendarMemberId: str("Whose calendar (optional; ignored with companyCalendar)."),
1146
+ agendaFileId: str("Link a drive file as the agenda (optional)."),
1147
+ estCostCents: num("Estimated cost for AGENT_BLOCK (optional)."),
1148
+ attendees: {
1149
+ type: "array",
1150
+ items: { type: "object", properties: { memberId: { type: "string" }, email: { type: "string" }, name: { type: "string" }, role: { type: "string" } } },
1151
+ description: "Attendees by memberId or external email (≤50).",
1152
+ },
1153
+ },
1154
+ ["title", "startsAt", "endsAt"],
1155
+ ),
1156
+ access: "write",
1157
+ desk: "calendar",
1158
+ binding: { kind: "rpc", method: "calendar.create" },
1159
+ },
1160
+ {
1161
+ name: "calendar_rsvp",
1162
+ title: "RSVP event",
1163
+ description:
1164
+ "RSVP the acting seat's own invite (calendar.rsvp): ACCEPTED | DECLINED | TENTATIVE | " +
1165
+ "COMMITTED. Reschedule/update/cancel ride org_rpc (calendar.reschedule / update / delete).",
1166
+ input_schema: S(
1167
+ { eventId: str("Event id."), rsvp: str("ACCEPTED | DECLINED | TENTATIVE | COMMITTED.") },
1168
+ ["eventId", "rsvp"],
1169
+ ),
1170
+ access: "write",
1171
+ desk: "calendar",
1172
+ binding: { kind: "rpc", method: "calendar.rsvp" },
1173
+ },
1174
+ {
1175
+ name: "calendar_ask",
1176
+ title: "Ask the calendar",
1177
+ description:
1178
+ "Grounded ask over the calendar window this seat may see (calendar.ask) — company + own " +
1179
+ "events; PERSONAL_RESTRICTED events of other seats are ABSENT by law. Defaults to " +
1180
+ "yesterday..+14d; fromIso/toIso override. Result carries content + grounding + " +
1181
+ "costCents + auditId — cite them.",
1182
+ input_schema: S(
1183
+ {
1184
+ question: str("The question (≤500 chars)."),
1185
+ fromIso: str("Window start ISO (optional)."),
1186
+ toIso: str("Window end ISO (optional)."),
1187
+ },
1188
+ ["question"],
1189
+ ),
1190
+ access: "read",
1191
+ desk: "calendar",
1192
+ binding: { kind: "rpc", method: "calendar.ask" },
1193
+ },
1194
+ // ── crm desk (registered when the family is vendored) ──
1195
+ {
1196
+ name: "crm_list_deals",
1197
+ title: "List deals",
1198
+ description: "Pipeline listing (crm.listDeals) filterable by stage/account/owner/openOnly.",
1199
+ input_schema: S({
1200
+ stage: str("Deal stage filter (optional)."),
1201
+ accountId: str("Account filter (optional)."),
1202
+ ownerId: str("Owner filter (optional)."),
1203
+ openOnly: bool("Only open deals."),
1204
+ limit: num("Max rows 1..100 (default 50)."),
1205
+ }),
1206
+ access: "read",
1207
+ desk: "crm",
1208
+ binding: { kind: "rpc", method: "crm.listDeals" },
1209
+ },
1210
+ {
1211
+ name: "crm_get_deal",
1212
+ title: "Get deal",
1213
+ description:
1214
+ "One deal with account, contacts, and hold detail (crm.getDeal) — check heldDetail " +
1215
+ "before acting; a held deal refuses next-best-action.",
1216
+ input_schema: S({ dealId: str("Deal id.") }, ["dealId"]),
1217
+ access: "read",
1218
+ desk: "crm",
1219
+ binding: { kind: "rpc", method: "crm.getDeal" },
1220
+ },
1221
+ {
1222
+ name: "crm_list_escalations",
1223
+ title: "List CRM escalations",
1224
+ description:
1225
+ "Held deals pending human resolution (crm.listEscalations). Signing/returning an " +
1226
+ "escalation is HUMAN-ONLY — agents surface them, humans resolve them.",
1227
+ input_schema: S({ limit: num("Max rows 1..50 (default 20).") }),
1228
+ access: "read",
1229
+ desk: "crm",
1230
+ binding: { kind: "rpc", method: "crm.listEscalations" },
1231
+ },
1232
+ {
1233
+ name: "crm_create_deal",
1234
+ title: "Create deal",
1235
+ description: "Create a deal (crm.createDeal); account resolved/minted by company name.",
1236
+ input_schema: S(
1237
+ {
1238
+ company: str("Company/account name."),
1239
+ name: str("Deal name (optional; defaults from company)."),
1240
+ valueCents: num("Deal value in cents (optional)."),
1241
+ ownerId: str("Owner member id (optional)."),
1242
+ source: str("Source label (optional)."),
1243
+ },
1244
+ ["company"],
1245
+ ),
1246
+ access: "write",
1247
+ desk: "crm",
1248
+ binding: { kind: "rpc", method: "crm.createDeal" },
1249
+ },
1250
+ {
1251
+ name: "crm_move_stage",
1252
+ title: "Move deal stage",
1253
+ description:
1254
+ "Move a deal through the pipeline (crm.moveStage). Moving to CLOSED without an outcome " +
1255
+ "is refused — close via org_rpc crm.closeDeal (WON auto-drafts a Books invoice).",
1256
+ input_schema: S(
1257
+ {
1258
+ dealId: str("Deal id."),
1259
+ stage: str("Target stage enum value."),
1260
+ sortOrder: num("Position inside the column (optional)."),
1261
+ },
1262
+ ["dealId", "stage"],
1263
+ ),
1264
+ access: "write",
1265
+ desk: "crm",
1266
+ binding: { kind: "rpc", method: "crm.moveStage" },
1267
+ },
1268
+ {
1269
+ name: "crm_log_interaction",
1270
+ title: "Log CRM interaction",
1271
+ description: "Log a note/call/email/meeting against a deal or contact (crm.logInteraction).",
1272
+ input_schema: S(
1273
+ {
1274
+ kind: str("note | call | email | meeting."),
1275
+ text: str("What happened (≤4000 chars)."),
1276
+ dealId: str("Deal (optional)."),
1277
+ contactId: str("Contact (optional)."),
1278
+ },
1279
+ ["kind", "text"],
1280
+ ),
1281
+ access: "write",
1282
+ desk: "crm",
1283
+ binding: { kind: "rpc", method: "crm.logInteraction" },
1284
+ },
1285
+ {
1286
+ name: "crm_next_best_action",
1287
+ title: "Run next best action",
1288
+ description:
1289
+ "Run the playbook's next-best-action for a deal (crm.runNextBestAction). Refused while " +
1290
+ "the deal is held — resolve the hold first (crm_list_escalations).",
1291
+ input_schema: S({ dealId: str("Deal id.") }, ["dealId"]),
1292
+ access: "write",
1293
+ desk: "crm",
1294
+ binding: { kind: "rpc", method: "crm.runNextBestAction" },
1295
+ },
1296
+ {
1297
+ name: "crm_ask",
1298
+ title: "Ask the pipeline",
1299
+ description:
1300
+ "Grounded ask over the deals this seat may see (crm.ask): value, stage, owner, held-for-" +
1301
+ "signature state, next steps. Result carries content + grounding + costCents + auditId — " +
1302
+ "cite them.",
1303
+ input_schema: S(
1304
+ {
1305
+ question: str("The question (≤500 chars)."),
1306
+ limit: num("How many deals to ground in, 4..40 (default 30)."),
1307
+ },
1308
+ ["question"],
1309
+ ),
1310
+ access: "read",
1311
+ desk: "crm",
1312
+ binding: { kind: "rpc", method: "crm.ask" },
1313
+ },
1314
+ // ── books desk (registered when the family is vendored) ──
1315
+ {
1316
+ name: "books_reports",
1317
+ title: "Books reports",
1318
+ description:
1319
+ "Ledger-current reporting (books.reports): pl | balance | cashflow | agedAr | agedAp. " +
1320
+ "Every figure carries the journal-line ids it was computed from.",
1321
+ input_schema: S(
1322
+ {
1323
+ report: str("pl | balance | cashflow | agedAr | agedAp."),
1324
+ from: str("Window start ISO (optional; defaults to month start)."),
1325
+ to: str("Window end ISO (optional; defaults to now)."),
1326
+ },
1327
+ ["report"],
1328
+ ),
1329
+ access: "read",
1330
+ desk: "books",
1331
+ binding: { kind: "rpc", method: "books.reports" },
1332
+ },
1333
+ {
1334
+ name: "books_invoices",
1335
+ title: "List AR invoices",
1336
+ description: "AR invoices with chase state (books.invoices); filter by status.",
1337
+ input_schema: S({
1338
+ status: str("DRAFT | SENT | PART_PAID | PAID | VOID | WRITTEN_OFF (optional)."),
1339
+ limit: num("Max rows 1..100 (default 50)."),
1340
+ }),
1341
+ access: "read",
1342
+ desk: "books",
1343
+ binding: { kind: "rpc", method: "books.invoices" },
1344
+ },
1345
+ {
1346
+ name: "books_bills",
1347
+ title: "List AP bills",
1348
+ description: "AP bills with hold reasons + payment-run linkage (books.bills).",
1349
+ input_schema: S({
1350
+ status: str("CAPTURED | AWAITING_APPROVAL | APPROVED | SCHEDULED | PAID | VOID (optional)."),
1351
+ limit: num("Max rows 1..100 (default 50)."),
1352
+ }),
1353
+ access: "read",
1354
+ desk: "books",
1355
+ binding: { kind: "rpc", method: "books.bills" },
1356
+ },
1357
+ {
1358
+ name: "books_tax_obligations",
1359
+ title: "List tax obligations",
1360
+ description:
1361
+ "Tax obligations + lodgement state (books.taxObligations). Signing/lodging is the " +
1362
+ "HUMAN-SIGNATURE LAW (PB-B4) — agents prepare, humans sign.",
1363
+ input_schema: S({
1364
+ status: str("UPCOMING | ASSEMBLING | READY | HELD_FOR_SIGNATURE | SIGNED | LODGED | ACCEPTED | REJECTED | AMENDED (optional)."),
1365
+ }),
1366
+ access: "read",
1367
+ desk: "books",
1368
+ binding: { kind: "rpc", method: "books.taxObligations" },
1369
+ },
1370
+ {
1371
+ name: "books_ask",
1372
+ title: "Ask the books",
1373
+ description:
1374
+ "Grounded finance Q&A over the ledger via the shared AI gateway (books.ask). Returns " +
1375
+ "{content, grounding[], costCents, auditId} — always show grounding + the mono cost.",
1376
+ input_schema: S({ question: str("The question (≤500 chars).") }, ["question"]),
1377
+ access: "read",
1378
+ desk: "books",
1379
+ binding: { kind: "rpc", method: "books.ask" },
1380
+ },
1381
+ {
1382
+ name: "books_invoice_draft",
1383
+ title: "Draft AR invoice",
1384
+ description:
1385
+ "Draft an AR invoice (books.invoiceDraft) — payee by id or minted by name; dealId " +
1386
+ "prefills from a closed-won deal. Nothing posts or emails until books_invoice_send.",
1387
+ input_schema: S(
1388
+ {
1389
+ payeeId: str("Existing payee id (or use payeeName)."),
1390
+ payeeName: str("Payee name to resolve-or-mint."),
1391
+ description: str("Line description (≤240 chars)."),
1392
+ totalCents: num("Total in cents (positive)."),
1393
+ dueDays: num("Days until due 0..90 (default 14)."),
1394
+ dealId: str("CRM deal to link (optional)."),
1395
+ },
1396
+ ["description", "totalCents"],
1397
+ ),
1398
+ access: "write",
1399
+ desk: "books",
1400
+ binding: { kind: "rpc", method: "books.invoiceDraft" },
1401
+ },
1402
+ {
1403
+ name: "books_invoice_send",
1404
+ title: "Send AR invoice",
1405
+ description:
1406
+ "Send a drafted invoice (books.invoiceSend): posts the AR journal, mints a SANDBOX " +
1407
+ "payment link, arms the PB-B2 chase ladder, and queues a REAL customer email from the " +
1408
+ "billing/accounts mailbox — the payload's emailQueued/emailSkipped is honest.",
1409
+ input_schema: S({ id: str("Invoice id.") }, ["id"]),
1410
+ access: "write",
1411
+ desk: "books",
1412
+ binding: { kind: "rpc", method: "books.invoiceSend" },
1413
+ },
1414
+ {
1415
+ name: "books_bill_approve",
1416
+ title: "Approve AP bill",
1417
+ description:
1418
+ "Release a held bill onto the payment run (books.billApprove). PB-B1 floor/new-payee " +
1419
+ "holds require a verified human signature — without it the hold frame comes back " +
1420
+ "(SIGNATURE_REQUIRED), never a silent release.",
1421
+ input_schema: S(
1422
+ {
1423
+ id: str("Bill id."),
1424
+ humanSignatureMemberId: str("Human signer member id (required by PB-B1 holds)."),
1425
+ },
1426
+ ["id"],
1427
+ ),
1428
+ access: "write",
1429
+ desk: "books",
1430
+ binding: { kind: "rpc", method: "books.billApprove" },
1431
+ },
1432
+ // ── meetings desk addition ──
1433
+ {
1434
+ name: "meetings_recap_file",
1435
+ title: "File meeting recap",
1436
+ description:
1437
+ "File a held meeting's recap + transcript into the workspace drive (meetings.recapFile): " +
1438
+ "two DOCs + MeetingRecord links back to the meeting. The notetaker's landing verb.",
1439
+ input_schema: S(
1440
+ {
1441
+ meetingId: str("Meeting id."),
1442
+ callId: str("Source call id (optional)."),
1443
+ occurredAt: str("ISO datetime (optional)."),
1444
+ recap: {
1445
+ type: "object",
1446
+ description: "{ bullets: string[] (1..20), decisions?: string[], actions?: string[] }",
1447
+ properties: {
1448
+ bullets: { type: "array", items: { type: "string" } },
1449
+ decisions: { type: "array", items: { type: "string" } },
1450
+ actions: { type: "array", items: { type: "string" } },
1451
+ },
1452
+ required: ["bullets"],
1453
+ },
1454
+ transcript: {
1455
+ type: "array",
1456
+ items: { type: "object", properties: { at: { type: "string" }, name: { type: "string" }, speakerMemberId: { type: "string" }, text: { type: "string" } }, required: ["at", "name", "text"] },
1457
+ description: "Transcript lines (optional).",
1458
+ },
1459
+ },
1460
+ ["meetingId", "recap"],
1461
+ ),
1462
+ access: "write",
1463
+ desk: "meetings",
1464
+ binding: { kind: "rpc", method: "meetings.recapFile" },
1465
+ },
1466
+ // ── directory desk (registered when the family is vendored) ──
1467
+ //
1468
+ // The Directory holds EXTERNAL people and organisations only — colleagues live
1469
+ // in the workforce, and the domain refuses to archive one from here (D-1).
1470
+ // Two governance facts shape this block:
1471
+ // - Awareness (D-4): a NAMED record is ABSENT, not redacted, from every read
1472
+ // below for a seat without a grant. An empty result is the correct answer,
1473
+ // never evidence that a record does not exist — do not infer from absence.
1474
+ // - The propose-only permission matrix (D-7): agents read, enrich and
1475
+ // PROPOSE. Merge execution, awareness changes and archives are human-gated
1476
+ // server-side; an agent call to them files a review item rather than
1477
+ // acting, so they stay in the `org_rpc` long tail deliberately.
1478
+ {
1479
+ name: "directory_search",
1480
+ title: "Search the directory",
1481
+ description:
1482
+ "One-box search across external people and organisations (directory.search) — name, " +
1483
+ "organisation, email, phone, handle. Awareness-scoped: records you hold no grant for " +
1484
+ "are absent from the results, not marked restricted.",
1485
+ input_schema: S(
1486
+ {
1487
+ q: str("Query text (≤200 chars)."),
1488
+ scope: str("external | internal | all (default external)."),
1489
+ limit: num("Max rows 1..50."),
1490
+ },
1491
+ ["q"],
1492
+ ),
1493
+ access: "read",
1494
+ desk: "directory",
1495
+ binding: { kind: "rpc", method: "directory.search" },
1496
+ },
1497
+ {
1498
+ name: "directory_list_people",
1499
+ title: "List people",
1500
+ description:
1501
+ "The People view (directory.listPeople): filter by relationship, organisation, awareness, " +
1502
+ "list membership or free text; sort by touch or name. Cursor-paginated. " +
1503
+ "Awareness-scoped — restricted records are absent.",
1504
+ input_schema: S({
1505
+ scope: str("external | internal | all (default external)."),
1506
+ relationship: str("all | client | vendor | prospect | partner | personal."),
1507
+ accountId: str("Only people at this organisation."),
1508
+ awareness: str("WORKSPACE | NAMED."),
1509
+ listId: str("Only members of this contact list."),
1510
+ q: str("Free-text filter (≤200 chars)."),
1511
+ sort: str("touch | name."),
1512
+ includeArchived: bool("Include archived records (default false)."),
1513
+ limit: num("Page size 1..200."),
1514
+ cursor: str("Opaque cursor from a previous page."),
1515
+ }),
1516
+ access: "read",
1517
+ desk: "directory",
1518
+ binding: { kind: "rpc", method: "directory.listPeople" },
1519
+ },
1520
+ {
1521
+ name: "directory_list_orgs",
1522
+ title: "List organisations",
1523
+ description:
1524
+ "The Organisations view (directory.listOrgs): external companies with their relationship " +
1525
+ "set, filterable by relationship and free text. Cursor-paginated.",
1526
+ input_schema: S({
1527
+ relationship: str("all | LEAD | PROSPECT | CUSTOMER | PARTNER | VENDOR | OTHER."),
1528
+ q: str("Free-text filter (≤200 chars)."),
1529
+ includeArchived: bool("Include archived organisations (default false)."),
1530
+ limit: num("Page size 1..200."),
1531
+ cursor: str("Opaque cursor from a previous page."),
1532
+ }),
1533
+ access: "read",
1534
+ desk: "directory",
1535
+ binding: { kind: "rpc", method: "directory.listOrgs" },
1536
+ },
1537
+ {
1538
+ name: "directory_get_person",
1539
+ title: "Get person record",
1540
+ description:
1541
+ "One person record (directory.getPerson): channels with their provenance and verification " +
1542
+ "state, links across mail/calendar/books/CRM, steward, awareness and grantees. " +
1543
+ "NOT_FOUND is also what a restricted record looks like — treat it as absence.",
1544
+ input_schema: S({ contactId: str("Contact id.") }, ["contactId"]),
1545
+ access: "read",
1546
+ desk: "directory",
1547
+ binding: { kind: "rpc", method: "directory.getPerson" },
1548
+ },
1549
+ {
1550
+ name: "directory_get_org",
1551
+ title: "Get organisation record",
1552
+ description:
1553
+ "One organisation record (directory.getOrg): address, contact channels, relationships, " +
1554
+ "billing entity, steward, and the people attached to it.",
1555
+ input_schema: S({ accountId: str("Account id.") }, ["accountId"]),
1556
+ access: "read",
1557
+ desk: "directory",
1558
+ binding: { kind: "rpc", method: "directory.getOrg" },
1559
+ },
1560
+ {
1561
+ name: "directory_ask",
1562
+ title: "Ask the directory",
1563
+ description:
1564
+ "Grounded Q&A over the directory via the shared AI gateway (directory.ask). Returns " +
1565
+ "{content, grounding[], costCents, auditId} — always show the grounding rows and the " +
1566
+ "cost. Grounding is awareness-scoped, so restricted records never reach the model.",
1567
+ input_schema: S(
1568
+ {
1569
+ question: str("The question (≤500 chars)."),
1570
+ limit: num("People to ground in, 1..24 (orgs get half as many again)."),
1571
+ },
1572
+ ["question"],
1573
+ ),
1574
+ access: "read",
1575
+ desk: "directory",
1576
+ binding: { kind: "rpc", method: "directory.ask" },
1577
+ },
1578
+ {
1579
+ name: "directory_list_captures",
1580
+ title: "List capture queue",
1581
+ description:
1582
+ "The Review view's capture queue (directory.listCaptures): records proposed from mail " +
1583
+ "signatures, calendar rosters, call transcripts and bank feeds, each with its provenance " +
1584
+ "line. A capture writes nothing until a human approves it.",
1585
+ input_schema: S({
1586
+ status: str("PENDING | APPROVED | EDITED_APPROVED | DISMISSED."),
1587
+ limit: num("Max rows 1..200."),
1588
+ }),
1589
+ access: "read",
1590
+ desk: "directory",
1591
+ binding: { kind: "rpc", method: "directory.listCaptures" },
1592
+ },
1593
+ {
1594
+ name: "directory_list_merges",
1595
+ title: "List duplicate candidates",
1596
+ description:
1597
+ "Proposed duplicate pairs awaiting a human (directory.listMerges), with the evidence that " +
1598
+ "flagged them. Executing or reverting a merge is human-gated — surface these, never " +
1599
+ "resolve them yourself.",
1600
+ input_schema: S({
1601
+ status: str("PENDING | MERGED | DISMISSED | REVERTED."),
1602
+ limit: num("Max rows 1..200."),
1603
+ }),
1604
+ access: "read",
1605
+ desk: "directory",
1606
+ binding: { kind: "rpc", method: "directory.listMerges" },
1607
+ },
1608
+ {
1609
+ name: "directory_create_person",
1610
+ title: "Create person",
1611
+ description:
1612
+ "Create an EXTERNAL person record (directory.createPerson) with an optional first channel " +
1613
+ "and provenance. Colleagues are not directory records — add a workforce member instead. " +
1614
+ "Prefer directory_propose_capture when the person came from parsed traffic.",
1615
+ input_schema: S(
1616
+ {
1617
+ displayName: str("Full name."),
1618
+ firstName: str("Given name (optional)."),
1619
+ lastName: str("Family name (optional)."),
1620
+ title: str("Role title (optional)."),
1621
+ accountId: str("Existing organisation id (optional)."),
1622
+ organisationName: str("Organisation name to resolve-or-mint (optional)."),
1623
+ relationship: str("LEAD | PROSPECT | CUSTOMER | PARTNER | VENDOR | OTHER."),
1624
+ partyKind: str("BUSINESS | PERSONAL | FAMILY."),
1625
+ email: str("Primary email (optional)."),
1626
+ phone: str("Primary phone (optional)."),
1627
+ location: str("Location (optional)."),
1628
+ timezone: str("IANA timezone (optional)."),
1629
+ notes: str("Notes (≤4000 chars)."),
1630
+ stewardMemberId: str("Owning member id (optional)."),
1631
+ awareness: str("WORKSPACE | NAMED (default WORKSPACE)."),
1632
+ granteeMemberIds: arr(str("Member id."), "Grantees when awareness is NAMED."),
1633
+ source: str("MAIL | CALENDAR | CALL | BOOKS | CRM | MANUAL | IMPORT."),
1634
+ sourceRef: str("Where it came from (≤400 chars)."),
1635
+ },
1636
+ ["displayName"],
1637
+ ),
1638
+ access: "write",
1639
+ desk: "directory",
1640
+ binding: { kind: "rpc", method: "directory.createPerson" },
1641
+ },
1642
+ {
1643
+ name: "directory_update_person",
1644
+ title: "Update person",
1645
+ description:
1646
+ "Edit a person record (directory.updatePerson). Every field is optional — send only what " +
1647
+ "changed. `verifiedFrom` records HOW a channel written by this edit was learned, which is " +
1648
+ "what re-verification writes.",
1649
+ input_schema: S(
1650
+ {
1651
+ contactId: str("Contact id."),
1652
+ displayName: str("Full name."),
1653
+ title: str("Role title."),
1654
+ accountId: str("Organisation id."),
1655
+ organisationName: str("Organisation name to resolve-or-mint."),
1656
+ relationship: str("LEAD | PROSPECT | CUSTOMER | PARTNER | VENDOR | OTHER."),
1657
+ email: str("Primary email."),
1658
+ phone: str("Primary phone."),
1659
+ location: str("Location."),
1660
+ timezone: str("IANA timezone."),
1661
+ notes: str("Notes (≤4000 chars)."),
1662
+ stewardMemberId: str("Owning member id."),
1663
+ verifiedFrom: str("SIGNATURE | CALL | MANUAL | IMPORT | BANK_FEED."),
1664
+ source: str("MAIL | CALENDAR | CALL | BOOKS | CRM | MANUAL | IMPORT."),
1665
+ sourceRef: str("Where it came from (≤400 chars)."),
1666
+ },
1667
+ ["contactId"],
1668
+ ),
1669
+ access: "write",
1670
+ desk: "directory",
1671
+ binding: { kind: "rpc", method: "directory.updatePerson" },
1672
+ },
1673
+ {
1674
+ name: "directory_add_channel",
1675
+ title: "Add a channel",
1676
+ description:
1677
+ "Attach an email/phone/handle/URL to a person or organisation (directory.addChannel) with " +
1678
+ "the provenance that proves it. THE enrichment verb: re-adding a channel already on the " +
1679
+ "record with a fresh `verifiedFrom` is how a stale channel is re-verified.",
1680
+ input_schema: S(
1681
+ {
1682
+ subjectType: str("PERSON | ORGANISATION."),
1683
+ subjectId: str("Contact id (PERSON) or account id (ORGANISATION)."),
1684
+ type: str("EMAIL | PHONE | HANDLE | URL."),
1685
+ value: str("The address/number/handle/URL."),
1686
+ isPrimary: bool("Make this the primary channel of its type."),
1687
+ verifiedFrom: str("SIGNATURE | CALL | MANUAL | IMPORT | BANK_FEED."),
1688
+ label: str("Display label, e.g. 'work' (≤80 chars)."),
1689
+ source: str("MAIL | CALENDAR | CALL | BOOKS | CRM | MANUAL | IMPORT."),
1690
+ sourceRef: str("Where it came from (≤400 chars)."),
1691
+ confidence: num("0..1 confidence in the observation."),
1692
+ },
1693
+ ["subjectType", "subjectId", "type", "value"],
1694
+ ),
1695
+ access: "write",
1696
+ desk: "directory",
1697
+ binding: { kind: "rpc", method: "directory.addChannel" },
1698
+ },
1699
+ {
1700
+ name: "directory_propose_capture",
1701
+ title: "Propose a capture",
1702
+ description:
1703
+ "Propose a person/organisation for the Review queue (directory.proposeCapture) from a " +
1704
+ "signature block, meeting roster, transcript or bank feed. Writes NO record — a human " +
1705
+ "approves it. `sourceFingerprint` dedupes, so re-proposing the same observation is safe.",
1706
+ input_schema: S(
1707
+ {
1708
+ kind: str("PERSON | ORG | PERSON_AND_ORG."),
1709
+ payload: {
1710
+ type: "object",
1711
+ description:
1712
+ "{ person?: {displayName?, email?, phone?, title?, …}, org?: {name?, domain?, …} } — what was observed.",
1713
+ properties: {
1714
+ person: { type: "object", description: "Observed person fields." },
1715
+ org: { type: "object", description: "Observed organisation fields." },
1716
+ },
1717
+ },
1718
+ source: str("MAIL | CALENDAR | CALL | BOOKS | CRM | MANUAL | IMPORT."),
1719
+ sourceRef: str("The message/event/feed-line id (≤200 chars)."),
1720
+ sourceFingerprint: str("Stable hash of the observation — the dedup key."),
1721
+ confidence: num("0..1 confidence in the observation."),
1722
+ },
1723
+ ["kind", "payload", "source", "sourceFingerprint"],
1724
+ ),
1725
+ access: "write",
1726
+ desk: "directory",
1727
+ binding: { kind: "rpc", method: "directory.proposeCapture" },
1728
+ },
1729
+ {
1730
+ name: "directory_propose_merge",
1731
+ title: "Propose a merge",
1732
+ description:
1733
+ "Flag two records as the same party (directory.proposeMerge) with the evidence that says " +
1734
+ "so. Files a duplicate candidate for a human — it does NOT merge. Refs are " +
1735
+ "`contact:<id>` / `account:<id>`.",
1736
+ input_schema: S(
1737
+ {
1738
+ aRef: str("Party ref, e.g. contact:abc123."),
1739
+ bRef: str("The other party ref."),
1740
+ evidence: {
1741
+ type: "object",
1742
+ description: "Why they look alike — printed verbatim in the queue, e.g. {reason:'Same signature block'}.",
1743
+ },
1744
+ },
1745
+ ["aRef", "bRef"],
1746
+ ),
1747
+ access: "write",
1748
+ desk: "directory",
1749
+ binding: { kind: "rpc", method: "directory.proposeMerge" },
1750
+ },
1751
+ {
1752
+ name: "directory_request_awareness",
1753
+ title: "Request awareness",
1754
+ description:
1755
+ "Ask for awareness of a restricted record (directory.requestAwareness). The reason is " +
1756
+ "printed verbatim to the person who decides. Granting is human-only — you file the " +
1757
+ "request, a human answers it.",
1758
+ input_schema: S(
1759
+ {
1760
+ contactId: str("Contact id."),
1761
+ reason: str("Why you need it (≤500 chars) — shown verbatim to the decider."),
1762
+ },
1763
+ ["contactId"],
1764
+ ),
1765
+ access: "write",
1766
+ desk: "directory",
1767
+ binding: { kind: "rpc", method: "directory.requestAwareness" },
1768
+ },
1769
+ // ── design desk (Brand Studio; registered when the family is vendored) ──
1770
+ //
1771
+ // The wire family stays `branding.*`; the product section is called Design
1772
+ // (D-5). Foundation mutations are ALWAYS human-gated (D-7): `design.write` —
1773
+ // the scope an ordinary agent seat holds — reaches design_propose_change,
1774
+ // which stages a reviewable diff and writes nothing. branding.saveFoundation /
1775
+ // restoreFoundation are `admin` scope and stay in the org_rpc long tail.
1776
+ {
1777
+ name: "design_foundation",
1778
+ title: "Get brand foundation",
1779
+ description:
1780
+ "The workspace's brand foundation (branding.getFoundation): typography, colour, scale, " +
1781
+ "archetype and principles, plus the version history head. Pass `versionId` to read a " +
1782
+ "historical snapshot before proposing a restore.",
1783
+ input_schema: S({
1784
+ versionId: str("Read this saved snapshot instead of the head (optional)."),
1785
+ historyLimit: num("History rows 0..50; 0 suppresses the history block."),
1786
+ }),
1787
+ access: "read",
1788
+ desk: "design",
1789
+ binding: { kind: "rpc", method: "branding.getFoundation" },
1790
+ },
1791
+ {
1792
+ name: "design_voice",
1793
+ title: "Get brand voice",
1794
+ description:
1795
+ "The voice statement, trait pills, SAY/AVOID lexicon and in-practice examples " +
1796
+ "(branding.getVoice). Read this BEFORE drafting anything the workspace publishes.",
1797
+ input_schema: S({}),
1798
+ access: "read",
1799
+ desk: "design",
1800
+ binding: { kind: "rpc", method: "branding.getVoice" },
1801
+ },
1802
+ {
1803
+ name: "design_list_templates",
1804
+ title: "List brand templates",
1805
+ description:
1806
+ "The template gallery (branding.listTemplates) with each template's status chip — " +
1807
+ "READY / GENERATING / STALE / DRAFT. STALE means the foundation moved under it.",
1808
+ input_schema: S({
1809
+ visibleOnly: bool("Default true; false is the founder-facing inventory."),
1810
+ category: str("digital | print | email | social | og."),
1811
+ }),
1812
+ access: "read",
1813
+ desk: "design",
1814
+ binding: { kind: "rpc", method: "branding.listTemplates" },
1815
+ },
1816
+ {
1817
+ name: "design_render_template",
1818
+ title: "Render a template",
1819
+ description:
1820
+ "Render a template to HTML, or PNG for GENERATED templates (branding.renderTemplate). " +
1821
+ "`budgetCents` caps this run's spend and is clamped to the hard per-run ceiling — always " +
1822
+ "report the returned cost.",
1823
+ input_schema: S(
1824
+ {
1825
+ templateId: str("Registry spec id or BrandTemplate.slug."),
1826
+ theme: str("light | dark."),
1827
+ size: str("mobile | tablet | desktop."),
1828
+ format: str("html (any template) | png (GENERATED only)."),
1829
+ content: { type: "object", description: "Per-template copy merged over the stored content." },
1830
+ budgetCents: num("Spend cap for this run, US cents 0..1000."),
1831
+ },
1832
+ ["templateId"],
1833
+ ),
1834
+ access: "read",
1835
+ desk: "design",
1836
+ binding: { kind: "rpc", method: "branding.renderTemplate" },
1837
+ },
1838
+ {
1839
+ name: "design_rewrite_in_voice",
1840
+ title: "Rewrite in brand voice",
1841
+ description:
1842
+ "Rewrite a sentence in the workspace's brand voice (branding.rewriteInVoice). Returns the " +
1843
+ "rewrite plus its cost. This produces copy — it never sends anything; the send-gate still " +
1844
+ "screens whatever you do with the result.",
1845
+ input_schema: S(
1846
+ {
1847
+ text: str("The sentence to rewrite (≤4000 chars)."),
1848
+ subject: str("Framing for the audit event's subject column (≤160 chars)."),
1849
+ },
1850
+ ["text"],
1851
+ ),
1852
+ access: "read",
1853
+ desk: "design",
1854
+ binding: { kind: "rpc", method: "branding.rewriteInVoice" },
1855
+ },
1856
+ {
1857
+ name: "design_export_kit",
1858
+ title: "Export the brand kit",
1859
+ description:
1860
+ "Export the brand kit (branding.exportKit): tokens in px/rem/em, voice, and the mark bytes " +
1861
+ "unless `includeMarks:false`. `versionId` exports a historical version instead of the head.",
1862
+ input_schema: S({
1863
+ unit: str("px | rem | em."),
1864
+ includeMarks: bool("Default true; false drops the mark bytes."),
1865
+ versionId: str("Export this version instead of the head."),
1866
+ budgetCents: num("Spend cap for this run, US cents 0..1000."),
1867
+ }),
1868
+ access: "read",
1869
+ desk: "design",
1870
+ binding: { kind: "rpc", method: "branding.exportKit" },
1871
+ },
1872
+ {
1873
+ name: "design_generate_image",
1874
+ title: "Generate brand imagery",
1875
+ description:
1876
+ "Regenerate canonical brand image slots (branding.generateImage). Omitting `slots` " +
1877
+ "regenerates every STALE slot — never a silent full re-run. `budgetCents: 0` is the QUOTE " +
1878
+ "call: it generates nothing, spends nothing, and returns every slot's price and staleness. " +
1879
+ "Quote before you spend.",
1880
+ input_schema: S({
1881
+ slots: arr(str("Slot name, e.g. hero.primary."), "1..8 canonical slots; omit for every STALE slot."),
1882
+ force: bool("Regenerate a slot that is already current."),
1883
+ budgetCents: num("Spend cap, US cents 0..1000. 0 = quote only."),
1884
+ }),
1885
+ access: "write",
1886
+ desk: "design",
1887
+ binding: { kind: "rpc", method: "branding.generateImage" },
1888
+ },
1889
+ {
1890
+ name: "design_propose_change",
1891
+ title: "Propose a foundation change",
1892
+ description:
1893
+ "Stage a reviewable foundation diff (branding.proposeChange). Writes NOTHING to the " +
1894
+ "foundation — a human applies it. Blocks are REPLACED WHOLESALE, so send the COMPLETE " +
1895
+ "block (the whole `fonts` object, the whole `colors` array); a partial block is a silent " +
1896
+ "half-write. Pass `expectedVersionId` from design_foundation so a proposal computed " +
1897
+ "against a stale head is refused.",
1898
+ input_schema: S(
1899
+ {
1900
+ patch: {
1901
+ type: "object",
1902
+ description: "Top-level DesignSystem blocks to change, each COMPLETE.",
1903
+ },
1904
+ note: str("One-line note — becomes the version label when applied."),
1905
+ rationale: str("Why, for the reviewer (≤2000 chars)."),
1906
+ expectedVersionId: str("Head version id you computed against; omit to skip the check."),
1907
+ },
1908
+ ["patch"],
1909
+ ),
1910
+ access: "write",
1911
+ desk: "design",
1912
+ binding: { kind: "rpc", method: "branding.proposeChange" },
1913
+ },
1914
+ // ── calls desk (call working sessions; registered when the family is vendored) ──
1915
+ //
1916
+ // An AI participant "presents" a Cohort surface (doc/sheet/channel/board/
1917
+ // charter) on the call stage — an app share every client renders locally,
1918
+ // driven by step frames over the room data lane. Three laws shape this block:
1919
+ // - Chain redaction: share.* events carry ids/counts only. Step text IS
1920
+ // shown to the humans on the call, which is why org_call_share_step is
1921
+ // the block's one `outbound: true` verb — the send-gate screens its
1922
+ // notes/typed text before dispatch, per the header's definition.
1923
+ // - Frames are never persisted server-side: org_call_visual_context reads
1924
+ // narration TEXT distilled from screen/camera stills, nothing more.
1925
+ // - Real doc edits ride the files desk (files_doc_write) — a share step
1926
+ // only choreographs what participants see; pair the write with a
1927
+ // refresh step so the room's tiles refetch.
1928
+ {
1929
+ name: "org_call_share_start",
1930
+ title: "Present a surface on a call",
1931
+ description:
1932
+ "Start presenting a Cohort surface (doc/sheet/channel/board/charter) on a live call's " +
1933
+ "stage (calling.appShareStart) — every participant's client renders it like a screen " +
1934
+ "share. One active session per call: your own previous session is superseded; CONFLICT " +
1935
+ "while another member presents. Returns {sessionId} — keep it for share_step/share_end. " +
1936
+ "You must be a participant in the call; narrate verbally what you're showing.",
1937
+ input_schema: S(
1938
+ {
1939
+ callId: str("Live call id."),
1940
+ surface: {
1941
+ type: "object",
1942
+ description: "What to present: {kind, ref, title?}.",
1943
+ properties: {
1944
+ kind: { type: "string", description: "doc | sheet | channel | board | charter." },
1945
+ ref: { type: "string", description: "fileId (doc/sheet) | channelId | 'org' (board) | charter slug." },
1946
+ title: { type: "string", description: "Display title for the stage tile (optional)." },
1947
+ },
1948
+ required: ["kind", "ref"],
1949
+ },
1950
+ },
1951
+ ["callId", "surface"],
1952
+ ),
1953
+ access: "write",
1954
+ desk: "calls",
1955
+ binding: { kind: "rpc", method: "calling.appShareStart" },
1956
+ },
1957
+ {
1958
+ name: "org_call_share_step",
1959
+ title: "Step through the share",
1960
+ description:
1961
+ "Drive your active share with a step batch (calling.appShareAct), ≤20 steps: " +
1962
+ "{k:'cursor',x,y} (normalized 0..1) · {k:'scroll',anchor} · {k:'highlight',anchor,note?} " +
1963
+ "· {k:'type',anchor,text} (visual typing preview — NOT a real edit; write real content " +
1964
+ "via files_doc_write, then send a refresh step) · {k:'refresh'} (tiles refetch the " +
1965
+ "surface). Anchors: 'block:3' | 'msg:<id>' | 'cell:B4' | 'task:<id>' | 'section:<idx>' " +
1966
+ "| 'top' | 'bottom'. Send-gate screened locally — the note/typed text is read by every " +
1967
+ "human on the call.",
1968
+ input_schema: S(
1969
+ {
1970
+ callId: str("Live call id."),
1971
+ sessionId: str("Active session id from org_call_share_start."),
1972
+ steps: {
1973
+ type: "array",
1974
+ items: {
1975
+ type: "object",
1976
+ properties: {
1977
+ k: { type: "string", description: "cursor | scroll | highlight | type | refresh." },
1978
+ x: { type: "number", description: "Cursor x 0..1 (k:cursor)." },
1979
+ y: { type: "number", description: "Cursor y 0..1 (k:cursor)." },
1980
+ anchor: { type: "string", description: "Target anchor (k:scroll/highlight/type)." },
1981
+ note: { type: "string", description: "Highlight note ≤200 chars (k:highlight; optional)." },
1982
+ text: { type: "string", description: "Typing-preview text ≤2000 chars (k:type)." },
1983
+ },
1984
+ required: ["k"],
1985
+ },
1986
+ description: "The step batch (1..20), applied in order.",
1987
+ },
1988
+ },
1989
+ ["callId", "sessionId", "steps"],
1990
+ ),
1991
+ access: "write",
1992
+ outbound: true,
1993
+ desk: "calls",
1994
+ binding: { kind: "rpc", method: "calling.appShareAct" },
1995
+ },
1996
+ {
1997
+ name: "org_call_share_end",
1998
+ title: "End the share",
1999
+ description:
2000
+ "End the active share session (calling.appShareEnd) and clear the stage tile. The " +
2001
+ "sharer or the call host may end it.",
2002
+ input_schema: S(
2003
+ {
2004
+ callId: str("Live call id."),
2005
+ sessionId: str("Session id to end."),
2006
+ reason: str("ended | superseded (optional; default ended)."),
2007
+ },
2008
+ ["callId", "sessionId"],
2009
+ ),
2010
+ access: "write",
2011
+ desk: "calls",
2012
+ binding: { kind: "rpc", method: "calling.appShareEnd" },
2013
+ },
2014
+ {
2015
+ name: "org_call_visual_context",
2016
+ title: "Read what's on screen",
2017
+ description:
2018
+ "The call's narrated visual stream (calling.getVisualContext): the last N visual events " +
2019
+ "{kind: screen|camera|appshare, sourceMemberId, summary, at}, chronological. Narration " +
2020
+ "text distilled from sampled frames — frames themselves are never persisted. Use it to " +
2021
+ "ground \"this chart\"/\"that number\" talk in what participants actually see.",
2022
+ input_schema: S(
2023
+ {
2024
+ callId: str("Call id."),
2025
+ limit: num("Max events 1..50 (default 20)."),
2026
+ },
2027
+ ["callId"],
2028
+ ),
2029
+ access: "read",
2030
+ desk: "calls",
2031
+ binding: { kind: "rpc", method: "calling.getVisualContext" },
2032
+ },
2033
+ {
2034
+ name: "org_call_events",
2035
+ title: "Read call events",
2036
+ description:
2037
+ "Recent non-speech call events (calling.getCallEvents): reactions, raised/lowered hands, " +
2038
+ "joins, and share.* lifecycle, mapped {kind, actorId, at, payload} (payloads already " +
2039
+ "redacted). Read hands/reactions here to bring quiet participants in.",
2040
+ input_schema: S(
2041
+ {
2042
+ callId: str("Call id."),
2043
+ limit: num("Max events 1..100 (default 30)."),
2044
+ },
2045
+ ["callId"],
2046
+ ),
2047
+ access: "read",
2048
+ desk: "calls",
2049
+ binding: { kind: "rpc", method: "calling.getCallEvents" },
2050
+ },
2051
+ ]);
2052
+
2053
+ // ---------------------------------------------------------------------------
2054
+ // Integration tools (SP5) — the workspace's GRANTED plugin tools, merged onto
2055
+ // BOTH planes from the cross-process disk cache the daemon refreshes.
2056
+ // ---------------------------------------------------------------------------
2057
+
2058
+ /** Resolve agentRoot env-first — mirrors integration-tools.resolveAgentRoot. */
2059
+ function integrationAgentRoot(agentRoot) {
2060
+ return (
2061
+ agentRoot ||
2062
+ process.env.COHORT_AGENT_ROOT ||
2063
+ process.env.AGENT_ROOT ||
2064
+ process.env.AGENT_DIR ||
2065
+ process.cwd()
2066
+ );
2067
+ }
2068
+
2069
+ /**
2070
+ * Read the versioned integration tools the daemon persisted to
2071
+ * `state/org/toolset.json` (written by lib/org/integration-tools; SP5). Returns
2072
+ * the surface-shaped tool array, or [] on any error — the SAME defensive pattern
2073
+ * as state/org/repair-needed.json. This is the cross-process seam: a per-session
2074
+ * cohort-mcp subprocess + the native snapshot both read the current set here, so
2075
+ * a newly-granted tool is usable without a daemon restart. Only well-formed
2076
+ * integration descriptors survive (defence against a partial write).
2077
+ * @param {string} [agentRoot]
2078
+ * @returns {object[]}
2079
+ */
2080
+ export function loadIntegrationToolsFromDisk(agentRoot) {
2081
+ try {
2082
+ const p = join(integrationAgentRoot(agentRoot), "state", "org", "toolset.json");
2083
+ if (!existsSync(p)) return [];
2084
+ const doc = JSON.parse(readFileSync(p, "utf8"));
2085
+ const tools = doc && Array.isArray(doc.tools) ? doc.tools : [];
2086
+ return tools.filter(
2087
+ (t) =>
2088
+ t &&
2089
+ typeof t.name === "string" &&
2090
+ t.integration &&
2091
+ t.binding &&
2092
+ t.binding.kind === "rpc",
2093
+ );
2094
+ } catch {
2095
+ return [];
2096
+ }
2097
+ }
2098
+
2099
+ /** Look up a disk integration tool descriptor by name (undefined when unknown). */
2100
+ export function integrationToolDef(name, agentRoot) {
2101
+ const n = String(name || "");
2102
+ return loadIntegrationToolsFromDisk(agentRoot).find((t) => t.name === n);
2103
+ }
2104
+
2105
+ /**
2106
+ * The ACTIVE tool table: every always-on tool, plus the email/artifact/desk
2107
+ * tools when the vendored protocol carries those families, plus the workspace's
2108
+ * GRANTED integration tools from the shared disk cache (SP5; additive — never
2109
+ * reorders/drops a curated tool). `o.agentRoot` locates the disk cache
2110
+ * (env-first default).
2111
+ * @param {object} [o] - { emailAvailable?: boolean, artifactAvailable?: boolean,
2112
+ * desksAvailable?: boolean, agentRoot?: string }
2113
+ * @returns {Array<object>}
2114
+ */
2115
+ export function getOrgTools(o = {}) {
2116
+ const emailOk = o.emailAvailable ?? emailFamilyAvailable();
2117
+ const artifactOk = o.artifactAvailable ?? artifactFamilyAvailable();
2118
+ const deskOk = (desk) => o.desksAvailable ?? deskFamilyAvailable(desk);
2119
+ const base = ORG_TOOLS.filter(
2120
+ (t) => (!t.email || emailOk) && (!t.artifact || artifactOk) && (!t.desk || deskOk(t.desk)),
2121
+ );
2122
+ return [...base, ...loadIntegrationToolsFromDisk(o.agentRoot)];
2123
+ }
2124
+
2125
+ /** Tool-name → definition lookup over the FULL table (email included). */
2126
+ const TOOLS_BY_NAME = new Map(ORG_TOOLS.map((t) => [t.name, t]));
2127
+
2128
+ /**
2129
+ * Is this name one of the org tools — a curated tool (email included) OR a
2130
+ * currently-granted integration tool from the disk cache? Both planes gate
2131
+ * routing on this (native action-executor + MCP tools/call), so recognising a
2132
+ * disk integration tool here is what makes it executable without touching either
2133
+ * plane. Disk lookup uses the env-first agent root.
2134
+ */
2135
+ export function isOrgTool(name) {
2136
+ return TOOLS_BY_NAME.has(String(name || "")) || integrationToolDef(name) != null;
2137
+ }
2138
+
2139
+ /** Look up a CURATED tool definition by name (undefined when unknown). */
2140
+ export function orgToolDef(name) {
2141
+ return TOOLS_BY_NAME.get(String(name || ""));
2142
+ }
2143
+
2144
+ /**
2145
+ * The protocol methods bound by `outbound: true` tools — DERIVED from the
2146
+ * table, never hand-listed, so it cannot drift (spec §1.5(a)). Today:
2147
+ * messaging.send + email.send + email.draftSend (the mail-desk governed send)
2148
+ * + calling.appShareAct (share-step notes/typed text render on every call
2149
+ * participant's stage tile).
2150
+ * `org_rpc` invocations of these methods are send-gate screened exactly like
2151
+ * the first-class tools.
2152
+ * @type {Set<string>}
2153
+ */
2154
+ export const OUTBOUND_METHODS = Object.freeze(
2155
+ new Set(
2156
+ ORG_TOOLS.filter((t) => t.outbound && t.binding && t.binding.kind === "rpc").map(
2157
+ (t) => t.binding.method,
2158
+ ),
2159
+ ),
2160
+ );
2161
+
2162
+ // ---------------------------------------------------------------------------
2163
+ // Send-gate screen (spec §1.5(a)) — same perimeter BaseAdapter.send applies
2164
+ // ---------------------------------------------------------------------------
2165
+
2166
+ /**
2167
+ * Screen outbound content through lib/comms/send-gate.screenOutbound. Failure
2168
+ * mode mirrors the adapter's: a THROWING gate fails open with a counted
2169
+ * `channel.send_gate.fail_open` diagnostic (screenOutbound itself owns the
2170
+ * fail-closed-for-external policy). Returns {allow, reason, text?}.
2171
+ */
2172
+ async function screenOrgOutbound({ channel, recipient, text, agentRoot, screenImpl, bumpImpl }) {
2173
+ let screen = screenImpl;
2174
+ if (typeof screen !== "function") {
2175
+ try {
2176
+ ({ screenOutbound: screen } = await import("../comms/send-gate.mjs"));
2177
+ } catch {
2178
+ screen = null;
2179
+ }
2180
+ }
2181
+ if (typeof screen !== "function") return { allow: true, reason: "" };
2182
+ try {
2183
+ const verdict = await screen({
2184
+ channel,
2185
+ recipient: String(recipient ?? ""),
2186
+ text: typeof text === "string" ? text : "",
2187
+ agentRoot,
2188
+ });
2189
+ if (verdict && verdict.allow === false) {
2190
+ return { allow: false, reason: verdict.reason || "blocked" };
2191
+ }
2192
+ return { allow: true, reason: "", text: verdict?.redactedText };
2193
+ } catch (err) {
2194
+ try {
2195
+ let bump = bumpImpl;
2196
+ if (typeof bump !== "function") ({ bump } = await import("../diagnostics/counters.mjs"));
2197
+ bump(
2198
+ "channel.send_gate.fail_open",
2199
+ { channel, stage: "org-tool", error: err && err.message },
2200
+ { agentRoot },
2201
+ );
2202
+ } catch {
2203
+ /* a counter must never crash the path it observes */
2204
+ }
2205
+ return { allow: true, reason: "" };
2206
+ }
2207
+ }
2208
+
2209
+ /** Outbound screen inputs (recipient + text) for a screened tool/method. */
2210
+ function outboundScreenShape(toolName, method, input) {
2211
+ if (method === "messaging.send") {
2212
+ return { recipient: input?.channelId ?? "", text: input?.body ?? "" };
2213
+ }
2214
+ if (method === "email.send") {
2215
+ const to = Array.isArray(input?.to) ? input.to.join(", ") : String(input?.to ?? "");
2216
+ const subject = input?.subject ? `Subject: ${input.subject}\n\n` : "";
2217
+ return { recipient: to || String(input?.threadId ?? ""), text: `${subject}${input?.text ?? ""}` };
2218
+ }
2219
+ if (method === "email.draftSend") {
2220
+ // The mail-desk governed send: the CONTENT rides bodyText (not text), and
2221
+ // the recipient set is to+cc+bcc; a bare reply screens under the thread id.
2222
+ const rcpt = ["to", "cc", "bcc"]
2223
+ .flatMap((k) => (Array.isArray(input?.[k]) ? input[k] : []))
2224
+ .join(", ");
2225
+ const subject = input?.subject ? `Subject: ${input.subject}\n\n` : "";
2226
+ return {
2227
+ recipient: rcpt || String(input?.threadId ?? ""),
2228
+ text: `${subject}${input?.bodyText ?? ""}`,
2229
+ };
2230
+ }
2231
+ if (method === "calling.appShareAct") {
2232
+ // The share-step batch: the human-read free text rides highlight `note`s
2233
+ // and `type` texts; screen them concatenated under the call id.
2234
+ const steps = Array.isArray(input?.steps) ? input.steps : [];
2235
+ const text = steps
2236
+ .map((s) => (s && typeof s === "object" ? [s.note, s.text].filter((v) => typeof v === "string" && v).join("\n") : ""))
2237
+ .filter(Boolean)
2238
+ .join("\n");
2239
+ return { recipient: String(input?.callId ?? ""), text };
2240
+ }
2241
+ // org_rpc passthrough of an outbound method: screen the whole params blob.
2242
+ return { recipient: String(input?.channelId ?? input?.to ?? ""), text: JSON.stringify(input ?? {}) };
2243
+ }
2244
+
2245
+ // ---------------------------------------------------------------------------
2246
+ // Local bindings
2247
+ // ---------------------------------------------------------------------------
2248
+
2249
+ /** Read config/agent.json fail-open (identity hints for org_whoami). */
2250
+ function loadAgentJson(agentRoot) {
2251
+ try {
2252
+ const p = join(agentRoot, "config", "agent.json");
2253
+ if (!existsSync(p)) return {};
2254
+ const doc = JSON.parse(readFileSync(p, "utf8"));
2255
+ return doc && typeof doc === "object" ? doc : {};
2256
+ } catch {
2257
+ return {};
2258
+ }
2259
+ }
2260
+
2261
+ /** org_describe — offline protocol discoverability. */
2262
+ function describeProtocol(input = {}) {
2263
+ const family = input && input.family ? String(input.family) : "";
2264
+ const methods = Object.entries(METHODS)
2265
+ .filter(([, d]) => !family || d.family === family)
2266
+ .map(([name, d]) => ({
2267
+ name,
2268
+ family: d.family,
2269
+ scope: d.scope,
2270
+ sideEffecting: !!d.sideEffecting,
2271
+ idempotent: !!d.idempotent,
2272
+ }));
2273
+ const reads = Object.entries(READS)
2274
+ .filter(([p]) => !family || p === family || p.startsWith(`${family}.`))
2275
+ .map(([path, scope]) => ({ path, scope }));
2276
+ return okFrame({
2277
+ protocolVersion: PROTOCOL_VERSION,
2278
+ families: family ? FAMILIES.filter((f) => f === family) : [...FAMILIES],
2279
+ methodCount: methods.length,
2280
+ methods,
2281
+ reads,
2282
+ });
2283
+ }
2284
+
2285
+ /** org_whoami — config summary + best-effort server-side member resolution. */
2286
+ async function whoami(cfg, { fetchImpl, env = process.env } = {}) {
2287
+ const summary = {
2288
+ base: cfg.base,
2289
+ orgId: cfg.orgId,
2290
+ protocolVersion: PROTOCOL_VERSION,
2291
+ tokenPresent: !!cfg.token,
2292
+ };
2293
+ if (!cfg.token || !cfg.orgId) {
2294
+ return okFrame({
2295
+ member: null,
2296
+ ...summary,
2297
+ note: "not fully enrolled — set COHORT_API_TOKEN / COHORT_ORG_ID or run `cohort setup --only org`",
2298
+ });
2299
+ }
2300
+ // Identity hints, client-side: COHORT_AGENT_ID (member id/slug → direct
2301
+ // member GET) → COHORT_AGENT_EMAIL → config/agent.json email. Hint-less
2302
+ // whoami still resolves in a single-agent org (server-side sole-AI_AGENT
2303
+ // fallback).
2304
+ const agentId = env.COHORT_AGENT_ID ? String(env.COHORT_AGENT_ID) : "";
2305
+ const emailHint =
2306
+ (env.COHORT_AGENT_EMAIL && String(env.COHORT_AGENT_EMAIL)) ||
2307
+ (loadAgentJson(cfg.agentRoot).email ? String(loadAgentJson(cfg.agentRoot).email) : "");
2308
+ const res = await fetchSelfProfile({
2309
+ base: cfg.base,
2310
+ apiKey: cfg.token,
2311
+ orgId: cfg.orgId,
2312
+ agentId: agentId || undefined,
2313
+ emailHint: emailHint || undefined,
2314
+ fetchImpl,
2315
+ });
2316
+ if (res && res.ok) return okFrame({ member: res.result, ...summary });
2317
+ // Unresolvable (multi-agent org, no hints) → degrade, never an error frame.
2318
+ return okFrame({
2319
+ member: null,
2320
+ ...summary,
2321
+ note: "member identity unresolved — set COHORT_AGENT_ID to this agent's member id/slug",
2322
+ });
2323
+ }
2324
+
2325
+ // ---------------------------------------------------------------------------
2326
+ // The shared executor (both planes)
2327
+ // ---------------------------------------------------------------------------
2328
+
2329
+ /** Build a query-string suffix for a read path from a plain object. */
2330
+ function withQuery(path, query) {
2331
+ if (!query || typeof query !== "object") return path;
2332
+ const entries = Object.entries(query).filter(([, v]) => v != null && v !== "");
2333
+ if (!entries.length) return path;
2334
+ const qs = entries
2335
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
2336
+ .join("&");
2337
+ return `${path}?${qs}`;
2338
+ }
2339
+
2340
+ /** Normalise a client.read() outcome into a res frame. */
2341
+ function readToFrame(r) {
2342
+ if (r && r.ok) return okFrame(r.payload);
2343
+ return { ok: false, error: (r && r.error) || errFrame("INTERNAL", "read failed").error };
2344
+ }
2345
+
2346
+ /**
2347
+ * Execute one curated org tool. The ONE executor both planes share: resolves
2348
+ * config (§1.4), screens outbound content (§1.5a), dispatches via the org
2349
+ * client, and returns the standard `{ok,result}|{ok:false,error}` frame.
2350
+ * NEVER throws (matches the client's fail-open style).
2351
+ *
2352
+ * @param {string} name tool name from ORG_TOOLS
2353
+ * @param {object} input tool input (already schema-shaped by the caller's plane)
2354
+ * @param {object} [o] { agentRoot?, orgConfig?, fetchImpl?, env?,
2355
+ * screenImpl?, bumpImpl? } (injectables for tests)
2356
+ * @returns {Promise<object>} res frame
2357
+ */
2358
+ export async function executeOrgTool(name, input = {}, o = {}) {
2359
+ try {
2360
+ const cfg = resolveOrgToolConfig(o);
2361
+ // A curated tool first; else a granted integration tool from the disk cache
2362
+ // (SP5), located under the resolved agent root.
2363
+ const def = orgToolDef(name) || integrationToolDef(name, cfg.agentRoot);
2364
+ if (!def) return errFrame("NOT_FOUND", `unknown org tool ${name}`);
2365
+ if (def.email && !(o.emailAvailable ?? emailFamilyAvailable())) {
2366
+ return errFrame(
2367
+ "NOT_FOUND",
2368
+ `${name}: the email protocol family is not in this SDK's vendored protocol — upgrade @cohortapp/agent-sdk`,
2369
+ );
2370
+ }
2371
+ if (def.artifact && !(o.artifactAvailable ?? artifactFamilyAvailable())) {
2372
+ return errFrame(
2373
+ "NOT_FOUND",
2374
+ `${name}: the artifact protocol family is not in this SDK's vendored protocol — upgrade @cohortapp/agent-sdk`,
2375
+ );
2376
+ }
2377
+ if (def.desk && !(o.desksAvailable ?? deskFamilyAvailable(def.desk))) {
2378
+ return errFrame(
2379
+ "NOT_FOUND",
2380
+ `${name}: the ${def.desk} desk family is not in this SDK's vendored protocol — upgrade @cohortapp/agent-sdk`,
2381
+ );
2382
+ }
2383
+
2384
+ // Offline-capable local tools first.
2385
+ if (name === "org_describe") return describeProtocol(input);
2386
+
2387
+ // Every network tool needs a token; per §1.4 the server stays up and each
2388
+ // call returns a clear error frame instead.
2389
+ if (!cfg.token && name !== "org_whoami") {
2390
+ return errFrame(
2391
+ "UNAUTHORIZED",
2392
+ "no org credential — set COHORT_API_TOKEN or run `cohort setup --only org`",
2393
+ );
2394
+ }
2395
+
2396
+ const callOpts = { base: cfg.base, token: cfg.token, orgId: cfg.orgId, fetchImpl: o.fetchImpl };
2397
+
2398
+ // Send-gate screen for outbound-content tools (BOTH planes, before dispatch).
2399
+ if (def.outbound) {
2400
+ const method = def.binding && def.binding.method;
2401
+ const shape = outboundScreenShape(name, method, input);
2402
+ const gate = await screenOrgOutbound({
2403
+ channel: "cohort-org",
2404
+ recipient: shape.recipient,
2405
+ text: shape.text,
2406
+ agentRoot: cfg.agentRoot,
2407
+ screenImpl: o.screenImpl,
2408
+ bumpImpl: o.bumpImpl,
2409
+ });
2410
+ if (!gate.allow) return errFrame("FORBIDDEN_SCOPE", `send blocked by outbound gate: ${gate.reason}`);
2411
+ }
2412
+
2413
+ switch (name) {
2414
+ case "org_whoami":
2415
+ return whoami(cfg, { fetchImpl: o.fetchImpl, env: o.env || process.env });
2416
+
2417
+ case "org_events_tail": {
2418
+ const q = {};
2419
+ if (input && input.cursor != null) q.cursor = input.cursor;
2420
+ q.limit = Math.min(Number(input?.limit) > 0 ? Number(input.limit) : 500, 500);
2421
+ return readToFrame(await read(withQuery("events", q), callOpts));
2422
+ }
2423
+
2424
+ case "decision_list": {
2425
+ const q = input && input.status ? { status: input.status } : null;
2426
+ return readToFrame(await read(withQuery("decision.list", q), callOpts));
2427
+ }
2428
+
2429
+ case "approval_wait": {
2430
+ const id = input && (input.approvalId || input.id);
2431
+ if (!id) return errFrame("BAD_REQUEST", "approval_wait: approvalId required");
2432
+ const timeoutMs = Number(input?.timeoutMs) > 0 ? Number(input.timeoutMs) : APPROVAL_WAIT_BUDGET_MS;
2433
+ return readToFrame(await approvalWait({ id, timeoutMs }, callOpts));
2434
+ }
2435
+
2436
+ case "org_rpc": {
2437
+ const method = input && input.method ? String(input.method) : "";
2438
+ if (!method || !methodDef(method)) {
2439
+ return errFrame("NOT_FOUND", `org_rpc: unknown method ${method || "(none)"} — see org_describe`);
2440
+ }
2441
+ const params = input && typeof input.params === "object" && input.params ? input.params : {};
2442
+ // Escape-hatch parity: outbound-content methods are screened here too.
2443
+ if (OUTBOUND_METHODS.has(method)) {
2444
+ const shape = outboundScreenShape(name, method, params);
2445
+ const gate = await screenOrgOutbound({
2446
+ channel: "cohort-org",
2447
+ recipient: shape.recipient,
2448
+ text: shape.text,
2449
+ agentRoot: cfg.agentRoot,
2450
+ screenImpl: o.screenImpl,
2451
+ bumpImpl: o.bumpImpl,
2452
+ });
2453
+ if (!gate.allow) return errFrame("FORBIDDEN_SCOPE", `send blocked by outbound gate: ${gate.reason}`);
2454
+ }
2455
+ const opts = { ...callOpts };
2456
+ if (input.idempotencyKey) opts.idempotencyKey = String(input.idempotencyKey);
2457
+ return call(method, params, opts);
2458
+ }
2459
+
2460
+ case "org_read": {
2461
+ const rawPath = input && input.path ? String(input.path) : "";
2462
+ const basePath = rawPath.split("?")[0];
2463
+ if (!basePath || !Object.prototype.hasOwnProperty.call(READS, basePath)) {
2464
+ return errFrame("NOT_FOUND", `org_read: unknown read path ${basePath || "(none)"} — see org_describe`);
2465
+ }
2466
+ return readToFrame(await read(withQuery(basePath, input.query), callOpts));
2467
+ }
2468
+
2469
+ default: {
2470
+ const binding = def.binding || {};
2471
+ if (binding.kind === "read") {
2472
+ return readToFrame(await read(binding.path, callOpts));
2473
+ }
2474
+ if (binding.kind === "rpc") {
2475
+ // SP5: a granted integration tool proxies to integration.invokeTool
2476
+ // with the descriptor's {slug, tool} + the caller's input as `args`,
2477
+ // then unwraps the handler's {ok, output} into the standard frame — a
2478
+ // tool-level failure (incl. a background self-heal) surfaces as an
2479
+ // error frame so both planes flag it as an error, not a bare result.
2480
+ if (def.integration) {
2481
+ const res = await call(
2482
+ "integration.invokeTool",
2483
+ { slug: def.integration.slug, tool: def.integration.tool, args: input || {} },
2484
+ { ...callOpts },
2485
+ );
2486
+ if (!res.ok) return res;
2487
+ const r = res.result;
2488
+ if (r && r.ok === false) {
2489
+ const suffix = r.healing ? " (auto-repair in progress — retry shortly)" : "";
2490
+ return errFrame("INTERNAL", `${r.error || "integration tool failed"}${suffix}`);
2491
+ }
2492
+ return okFrame(r && r.output !== undefined ? r.output : (r ?? null));
2493
+ }
2494
+ const params = { ...(input || {}) };
2495
+ // Business-dedup ids auto-minted where hq expects them. (artifact.*
2496
+ // is sideEffecting:false at the dispatcher — the hq artifact service
2497
+ // self-manages its writes — so these params ARE the dedup lane.)
2498
+ if (name === "messaging_send" && !params.clientMsgId) params.clientMsgId = randomUUID();
2499
+ if (name === "email_send" && !params.idempotencyId) params.idempotencyId = randomUUID();
2500
+ if (name === "artifact_create" && !params.clientMsgId) params.clientMsgId = randomUUID();
2501
+ if (name === "artifact_act" && !params.idempotencyKey) params.idempotencyKey = randomUUID();
2502
+ const opts = { ...callOpts };
2503
+ const mdef = methodDef(binding.method);
2504
+ if (mdef && mdef.sideEffecting && !opts.idempotencyKey) {
2505
+ // A fresh dispatcher key per invocation: idempotent replays are
2506
+ // governed by the BUSINESS id (clientMsgId/idempotencyId), and a
2507
+ // fresh header key avoids the cached-refusal replay trap on
2508
+ // approval re-sends (companion §8.2).
2509
+ opts.idempotencyKey = randomUUID();
2510
+ }
2511
+ return call(binding.method, params, opts);
2512
+ }
2513
+ return errFrame("INTERNAL", `org tool ${name} has no dispatchable binding`);
2514
+ }
2515
+ }
2516
+ } catch (err) {
2517
+ return errFrame("INTERNAL", `org tool ${name} failed: ${err && err.message ? err.message : String(err)}`);
2518
+ }
2519
+ }
2520
+
2521
+ export default {
2522
+ ORG_TOOLS,
2523
+ getOrgTools,
2524
+ isOrgTool,
2525
+ orgToolDef,
2526
+ loadIntegrationToolsFromDisk,
2527
+ integrationToolDef,
2528
+ OUTBOUND_METHODS,
2529
+ emailFamilyAvailable,
2530
+ artifactFamilyAvailable,
2531
+ deskFamilyAvailable,
2532
+ resolveOrgToolConfig,
2533
+ executeOrgTool,
2534
+ DEFAULT_COHORT_BASE,
2535
+ };