@cohortapp/agent-sdk 2.16.0 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +29 -2
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -1,1268 +0,0 @@
1
- /**
2
- * tool-surface.test.mjs — the curated org tool table + shared executor.
3
- *
4
- * Hermetic: every network call rides an injected fetchImpl; the send-gate is an
5
- * injected stub; env is snapshotted/cleared so a developer's COHORT_* vars
6
- * can't leak in. Run: node --test lib/org/tool-surface.test.mjs
7
- */
8
-
9
- "use strict";
10
-
11
- import { test, before, after } from "node:test";
12
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
13
- import { tmpdir } from "node:os";
14
- import { join } from "node:path";
15
- import assert from "node:assert/strict";
16
-
17
- import {
18
- ORG_TOOLS,
19
- getOrgTools,
20
- isOrgTool,
21
- orgToolDef,
22
- OUTBOUND_METHODS,
23
- emailFamilyAvailable,
24
- artifactFamilyAvailable,
25
- deskFamilyAvailable,
26
- resolveOrgToolConfig,
27
- executeOrgTool,
28
- DEFAULT_COHORT_BASE,
29
- } from "./tool-surface.mjs";
30
- import { methodDef, PROTOCOL_VERSION, READS } from "./protocol.mjs";
31
- import { ADMIN_TOOLS, expectedAccessFor, normalizeToolAccess } from "./tool-access.mjs";
32
-
33
- // ── env hygiene ────────────────────────────────────────────────────────────
34
- const ENV_KEYS = [
35
- "COHORT_API_TOKEN", "COHORT_TOKEN", "COHORT_API_KEY", "COHORT_ORG_ID",
36
- "COHORT_BASE", "COHORT_API_URL", "COHORT_AGENT_ROOT", "AGENT_ROOT",
37
- "COHORT_AGENT_ID", "COHORT_AGENT_EMAIL",
38
- ];
39
- const saved = {};
40
- before(() => {
41
- for (const k of ENV_KEYS) { saved[k] = process.env[k]; delete process.env[k]; }
42
- });
43
- after(() => {
44
- for (const k of ENV_KEYS) {
45
- if (saved[k] === undefined) delete process.env[k];
46
- else process.env[k] = saved[k];
47
- }
48
- });
49
-
50
- // ── helpers ────────────────────────────────────────────────────────────────
51
- const CFG = { org: { cohort: { enabled: true, base: "https://org.example", orgId: "acme", token: "nlk_secret" } } };
52
-
53
- /** fetch stub: records calls, returns a res-frame body. */
54
- function fakeFetch(body = { ok: true, result: { fine: true } }, { status = 200, ok = true } = {}) {
55
- const calls = [];
56
- const fn = async (url, init) => {
57
- calls.push({ url: String(url), init: init || {} });
58
- return { ok, status, json: async () => body, headers: { get: () => undefined } };
59
- };
60
- fn.calls = calls;
61
- return fn;
62
- }
63
-
64
- // ── table shape ────────────────────────────────────────────────────────────
65
-
66
- test("table: curated 54 + 5 email + 5 artifact + 77 desk tools, snake_case names, valid access + schemas", () => {
67
- // 2026-08 mobile-parity delta: +5 mail (report_spam/react/move_mailbox/
68
- // summarise/ask) and +3 asks (files/calendar/crm) → desk 56 → 64; the
69
- // call-working-sessions delta adds the 5 calls-desk tools → 69.
70
- // The wire-contract fix adds task_assign: hq's editTaskSchema (board.updateTask)
71
- // has NO assignee field, so reassignment needs board.assignTask — curated 28 → 29.
72
- // The conversation-parity delta adds the per-message actions the human
73
- // long-press sheet ships (react/bookmark/delete/mark_unread_from) and the two
74
- // huddle verbs the share tools presuppose (start/join) — curated 29 → 35.
75
- // They are ALWAYS-ON, not desk-gated: the base messaging/channel/calling
76
- // families have been vendored since SP3.
77
- // The macro-parity delta adds org_pulse (GET ops?pulse=1 — the founder's
78
- // attention surface, previously human-only), org_huddle_end (which closes the
79
- // loop org_huddle_start opened) and memory_recall — hq built memory.recall
80
- // explicitly because "an SDK-driven seat has been reasoning off a strictly
81
- // smaller memory than the chat lane", then shipped no tool for it, so the
82
- // reverse-parity fix never reached the plane it was built for.
83
- // Curated 35 → 38.
84
- // The ENGAGEMENT delta adds the four verbs an SDK seat needs to bring somebody
85
- // in and had no binding for, despite all four sitting in the frozen protocol
86
- // table since it was written: messaging_open_group (channel.createConversation),
87
- // messaging_create_channel (channel.create), messaging_add_to_channel
88
- // (channel.addMember), and engage_colleagues — the judged path that decides
89
- // WHETHER anyone needs involving before it opens anything at all.
90
- // Curated 38 → 42, +1 for work_track → 43.
91
- // The VOICE delta adds messaging_send_voice_note — hq could synthesise a
92
- // colleague's voice note only as a reflex (answering a human's spoken note in
93
- // kind), so an agent ASKED in text for one had no verb and truthfully said it
94
- // could not. A `local` binding: it records (messaging.synthesizeVoiceNote)
95
- // then posts (plain messaging.send with the file attached). Curated 43 → 44.
96
- // The MANDATE + REGISTRY + PREFERENCE delta (+10 → 54). Three families that
97
- // were protocol-declared and curated NOWHERE, so a session could reach them
98
- // only through `org_rpc` — access:"admin", i.e. not at all below the CEO tier:
99
- // mandate (5) — hq's own chat colleague has carried the whole belt
100
- // since the spine landed, under a docblock naming SDK
101
- // parity as its reason; the SDK plane had none of it, so
102
- // the daemon knew the seat's objectives and the session it
103
- // spawned did not. adopt/retire are deliberately absent —
104
- // `mandate.write` is not in DEFAULT_AGENT_SCOPES.
105
- // subagent (3) — READS only. hq keeps the registry, maestro generates
106
- // sub-agents locally, and the two halves never met. The
107
- // writes stay with lib/subagents/client.mjs, which owns
108
- // the offline outbox and the lock file a second writer
109
- // would silently bypass.
110
- // preference (2) — hq built this family FOR this plane and said so in the
111
- // handler; no tool was ever added, so the same workspace
112
- // remembered a standing preference when hq answered and
113
- // forgot it when the seat's own daemon did.
114
- // The RESOURCE delta (+8 desk → 77, table 133 → 141). A venture's artifacts
115
- // cross from the portal and are filed as real WorkspaceFile rows, and the
116
- // OrgResource catalogue that indexes them was reachable from NO agent code
117
- // path in EITHER plane — the venture's own colleagues could not list, add,
118
- // correct, reorder or remove one of their own deliverables. Unlike every
119
- // block above it, this one is GENERATED from the vendored protocol
120
- // declaration (lib/org/resource-tools.mjs) rather than transcribed from hq's
121
- // desk, so it cannot acquire the 239-vs-69 drift the hand-copied desks
122
- // already carry; resource-tools.test.mjs is the parity that holds it there.
123
- // The FRONT-DOOR delta (+3 curated → 57, table 141 → 144; design spec
124
- // 2026-09-08 §3.5/§3.7): board_mine (the agent's own items across every
125
- // board — board.ready is unassigned-only, so "my tasks" was unreadable from
126
- // any plane), board_track (the sanctioned inbound→board seam with the
127
- // accepted/done ends of the ladder and title/why passthrough) and
128
- // session_status (the main session's liveness + peers from local state,
129
- // offline-capable).
130
- assert.equal(ORG_TOOLS.length, 144, "57 curated + 5 email + 5 artifact + 77 desk");
131
- assert.equal(ORG_TOOLS.filter((t) => t.desk).length, 77, "exactly seventy-seven desk tools");
132
- assert.equal(ORG_TOOLS.filter((t) => t.email).length, 5, "exactly five email tools");
133
- assert.equal(ORG_TOOLS.filter((t) => t.artifact).length, 5, "exactly five artifact tools");
134
- for (const t of ORG_TOOLS) {
135
- assert.match(t.name, /^[a-z0-9_]+$/, `${t.name} snake_case`);
136
- assert.ok(["read", "write", "admin"].includes(t.access), `${t.name} access`);
137
- assert.ok(t.description && t.description.length > 10, `${t.name} described`);
138
- assert.equal(t.input_schema.type, "object", `${t.name} schema object`);
139
- assert.ok(t.binding && ["rpc", "read", "local"].includes(t.binding.kind), `${t.name} binding`);
140
- if (t.binding.kind === "rpc") {
141
- assert.ok(methodDef(t.binding.method), `${t.name} binds a real protocol method (${t.binding.method})`);
142
- }
143
- // The rpc side was already pinned to the frozen table; the READ side was
144
- // not, so a curated tool could name a read path the server does not serve
145
- // and only fail at runtime, as a bare 404 the model cannot interpret.
146
- if (t.binding.kind === "read") {
147
- assert.ok(
148
- Object.prototype.hasOwnProperty.call(READS, t.binding.path),
149
- `${t.name} binds a real protocol read (${t.binding.path})`,
150
- );
151
- if (t.binding.query !== undefined) {
152
- assert.equal(typeof t.binding.query, "object", `${t.name} binding.query is an object`);
153
- for (const [k, v] of Object.entries(t.binding.query)) {
154
- assert.equal(typeof v, "string", `${t.name} binding.query.${k} is a string`);
155
- }
156
- }
157
- }
158
- }
159
- });
160
-
161
- test("OUTBOUND_METHODS is DERIVED from outbound:true tools (messaging/email/mail-desk/share-step sends)", () => {
162
- assert.deepEqual([...OUTBOUND_METHODS].sort(), ["calling.appShareAct", "email.draftSend", "email.send", "messaging.send"]);
163
- // messaging_send_voice_note is outbound-flagged but adds NOTHING to
164
- // OUTBOUND_METHODS: it is a `local` binding, and the derivation reads rpc
165
- // bindings only. Its POST leg is messaging.send, which is already in the set,
166
- // so the escape hatch stays covered too — the flag here buys the screen on
167
- // the SPOKEN words, before a billable character is synthesised.
168
- const outboundTools = ORG_TOOLS.filter((t) => t.outbound).map((t) => t.name).sort();
169
- assert.deepEqual(outboundTools, [
170
- "email_draft_send",
171
- "email_send",
172
- "messaging_send",
173
- "messaging_send_voice_note",
174
- "org_call_share_step",
175
- ]);
176
- });
177
-
178
- test("email gating: family present in the vendored protocol → tools active; override excludes", () => {
179
- // The email family landed in the vendored protocol (sync-protocol phase 1).
180
- assert.equal(emailFamilyAvailable(), true, "vendored protocol carries email.send");
181
- assert.equal(getOrgTools().length, 144);
182
- const without = getOrgTools({ emailAvailable: false });
183
- assert.equal(without.length, 139);
184
- assert.ok(!without.some((t) => t.email), "email tools excluded when family absent");
185
- });
186
-
187
- test("artifact gating: family present → tools active; override excludes (email precedent)", () => {
188
- assert.equal(artifactFamilyAvailable(), true, "vendored protocol carries artifact.act");
189
- const without = getOrgTools({ artifactAvailable: false });
190
- assert.equal(without.length, 139);
191
- assert.ok(!without.some((t) => t.artifact), "artifact tools excluded when family absent");
192
- const neither = getOrgTools({ emailAvailable: false, artifactAvailable: false, desksAvailable: false });
193
- assert.equal(neither.length, 57, "all additive families off → the 57 always-on tools");
194
- });
195
-
196
- test("isOrgTool / orgToolDef cover the full table; unknown names rejected", () => {
197
- for (const t of ORG_TOOLS) assert.equal(isOrgTool(t.name), true);
198
- assert.equal(isOrgTool("slack_send"), false);
199
- assert.equal(isOrgTool(""), false);
200
- assert.equal(orgToolDef("org_rpc").access, "admin");
201
- });
202
-
203
- // ── config resolution (§1.4) ───────────────────────────────────────────────
204
-
205
- test("resolveOrgToolConfig: config base/token honoured; default base when nothing names one", () => {
206
- const cfg = resolveOrgToolConfig({ orgConfig: CFG, agentRoot: "/tmp/none" });
207
- assert.equal(cfg.base, "https://org.example");
208
- assert.equal(cfg.token, "nlk_secret");
209
- assert.equal(cfg.orgId, "acme");
210
- const bare = resolveOrgToolConfig({ orgConfig: {}, agentRoot: "/tmp/none" });
211
- assert.equal(bare.base, DEFAULT_COHORT_BASE);
212
- assert.equal(bare.token, "");
213
- });
214
-
215
- test("resolveOrgToolConfig: COHORT_BASE env wins over config base (env-first)", () => {
216
- process.env.COHORT_BASE = "https://env.example/";
217
- try {
218
- const cfg = resolveOrgToolConfig({ orgConfig: CFG, agentRoot: "/tmp/none" });
219
- assert.equal(cfg.base, "https://env.example", "env base wins, trailing slash stripped");
220
- } finally {
221
- delete process.env.COHORT_BASE;
222
- }
223
- });
224
-
225
- // ── executor: basics ───────────────────────────────────────────────────────
226
-
227
- test("executeOrgTool: unknown tool → NOT_FOUND frame, never a throw", async () => {
228
- const frame = await executeOrgTool("nope_tool", {}, { orgConfig: CFG, agentRoot: "/tmp/none" });
229
- assert.equal(frame.ok, false);
230
- assert.equal(frame.error.code, "NOT_FOUND");
231
- });
232
-
233
- test("executeOrgTool: missing token → clear UNAUTHORIZED frame for network tools", async () => {
234
- const fetchImpl = fakeFetch();
235
- const frame = await executeOrgTool("org_directory", {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
236
- assert.equal(frame.ok, false);
237
- assert.equal(frame.error.code, "UNAUTHORIZED");
238
- assert.match(frame.error.message, /COHORT_API_TOKEN/);
239
- assert.equal(fetchImpl.calls.length, 0, "no network attempted");
240
- });
241
-
242
- test("org_describe: offline, no network, family filter works", async () => {
243
- const fetchImpl = fakeFetch();
244
- const frame = await executeOrgTool("org_describe", {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
245
- assert.equal(frame.ok, true);
246
- assert.equal(frame.result.protocolVersion, PROTOCOL_VERSION);
247
- assert.ok(frame.result.methods.length >= 200, "full method table listed");
248
- assert.ok(frame.result.reads.length >= 13, "reads listed");
249
- const fam = await executeOrgTool("org_describe", { family: "board" }, { orgConfig: {}, agentRoot: "/tmp/none" });
250
- assert.ok(fam.result.methods.every((m) => m.family === "board"));
251
- assert.deepEqual(fam.result.families, ["board"]);
252
- assert.equal(fetchImpl.calls.length, 0);
253
- });
254
-
255
- // ── executor: rpc + read bindings ──────────────────────────────────────────
256
-
257
- test("rpc binding: messaging_history POSTs /api/v1/messaging.history with Bearer + org pin", async () => {
258
- const fetchImpl = fakeFetch({ ok: true, result: { messages: [] } });
259
- const frame = await executeOrgTool("messaging_history", { channelId: "C1", limit: 5 }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
260
- assert.equal(frame.ok, true);
261
- assert.equal(fetchImpl.calls.length, 1);
262
- const { url, init } = fetchImpl.calls[0];
263
- assert.equal(url, "https://org.example/api/v1/messaging.history");
264
- assert.equal(init.method, "POST");
265
- assert.equal(init.headers.authorization, "Bearer nlk_secret");
266
- assert.equal(init.headers["x-org-id"], "acme");
267
- assert.deepEqual(JSON.parse(init.body), { channelId: "C1", limit: 5 });
268
- });
269
-
270
- test("read binding: board_ready GETs /api/v1/board.ready and normalises the bare payload", async () => {
271
- const fetchImpl = fakeFetch({ items: [{ id: "w1" }] });
272
- const frame = await executeOrgTool("board_ready", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
273
- assert.equal(frame.ok, true);
274
- assert.deepEqual(frame.result, { items: [{ id: "w1" }] });
275
- assert.equal(fetchImpl.calls[0].init.method, "GET");
276
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/board\.ready$/);
277
- });
278
-
279
- // ── macro pulse (the founder's attention surface, previously human-only) ────
280
-
281
- test("org_pulse: the binding PINS ?pulse=1 — the agent never has to know the param", async () => {
282
- const fetchImpl = fakeFetch({
283
- chainHead: { seq: 42, rowHash: "abc" },
284
- members: 9,
285
- openAlerts: 2,
286
- readyTasks: 5,
287
- pulse: {
288
- stats: { agentsRunning: 3, blocked: 1, decisionsToday: 2, needs: 2 },
289
- needs: [{ id: "esc_1", severity: "high", what: "Blocked on pricing" }],
290
- activity: [{ id: "ev_0", sentence: "Assigned a card in Growth" }],
291
- },
292
- });
293
- const frame = await executeOrgTool("org_pulse", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
294
- assert.equal(frame.ok, true);
295
- assert.equal(fetchImpl.calls[0].init.method, "GET");
296
- // Without a pinned query this GETs the bare probe and the pulse is null — the
297
- // whole capability turns on this one param riding the binding.
298
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/ops\?pulse=1$/);
299
- assert.equal(frame.result.pulse.stats.blocked, 1);
300
- assert.equal(frame.result.pulse.needs[0].severity, "high");
301
- });
302
-
303
- test("org_pulse is reachable at the READ tier — not another admin-only escape hatch", () => {
304
- const pulse = orgToolDef("org_pulse");
305
- assert.equal(pulse.access, "read", "an ordinary agent must be able to ask what needs attention");
306
- assert.equal(pulse.desk, undefined, "always-on: `ops` has been in READS since the first protocol");
307
- // The failure mode the description must pre-empt: reporting "nothing needs
308
- // attention" when the derivation actually failed.
309
- assert.match(pulse.description, /pulse: null/);
310
- assert.match(pulse.description, /do not report zero/);
311
- });
312
-
313
- test("huddle lifecycle is CLOSED: a room an agent can open is a room it can enter and shut", async () => {
314
- const names = ORG_TOOLS.filter((t) => t.name.startsWith("org_huddle_")).map((t) => t.name).sort();
315
- assert.deepEqual(names, ["org_huddle_end", "org_huddle_join", "org_huddle_start"]);
316
- for (const n of names) {
317
- const def = orgToolDef(n);
318
- assert.equal(def.desk, undefined, `${n} is always-on (the base calling family predates the desks)`);
319
- assert.equal(def.access, "write");
320
- assert.ok(def.binding.method.startsWith("calling."), `${n} rides the calling family`);
321
- }
322
- const fetchImpl = fakeFetch({ ok: true, result: { callId: "call_1", endedAt: "2026-08-12T10:00:00.000Z" } });
323
- const frame = await executeOrgTool("org_huddle_end", { callId: "call_1" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
324
- assert.equal(frame.ok, true);
325
- assert.equal(fetchImpl.calls[0].url, "https://org.example/api/v1/calling.end");
326
- assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body), { callId: "call_1" });
327
- // CONFLICT means "already closed", not "you failed" — the description must
328
- // say so, or a model retries a call it successfully ended.
329
- assert.match(orgToolDef("org_huddle_end").description, /CONFLICT/);
330
- });
331
-
332
- test("org_directory advertises the derived presence lane (a field nobody is told about is unreachable)", () => {
333
- const dir = orgToolDef("org_directory");
334
- for (const needle of ["presence", "lane", "detail", "indisposed"]) {
335
- assert.match(dir.description, new RegExp(needle), `org_directory names ${needle}`);
336
- }
337
- // The two honest-reading rules: a human seat's null is not "offline", and the
338
- // raw client-stamped `status.ts` is not the dot.
339
- assert.match(dir.description, /presence: null/);
340
- assert.match(dir.description, /client-stamped/);
341
- });
342
-
343
- test("memory_recall: the SDK seat gets the same memory the chat lane reasons off", async () => {
344
- const recall = orgToolDef("memory_recall");
345
- assert.equal(recall.access, "read", "recall is read-only — it must not need a write tier");
346
- assert.equal(recall.binding.method, "memory.recall");
347
- // knowledge.search is a NARROWED PROJECTION of the same index; if the model is
348
- // not told that, it keeps reaching for the smaller one out of habit.
349
- assert.match(orgToolDef("knowledge_search").description, /memory_recall/);
350
- // The two contracts a model gets wrong without being told: the entity pin is
351
- // a PAIR, and an empty index is a fact, not a failure to retry.
352
- assert.match(recall.description, /TOGETHER/);
353
- assert.match(recall.description, /found:false/);
354
-
355
- const fetchImpl = fakeFetch({ ok: true, result: { found: true, items: [{ id: "m1" }] } });
356
- const frame = await executeOrgTool(
357
- "memory_recall",
358
- { query: "what did we decide about pricing", scope: "self", depth: "specific", k: 5 },
359
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
360
- );
361
- assert.equal(frame.ok, true);
362
- assert.equal(fetchImpl.calls[0].url, "https://org.example/api/v1/memory.recall");
363
- assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body), {
364
- query: "what did we decide about pricing", scope: "self", depth: "specific", k: 5,
365
- });
366
- // A read carries no dispatcher idempotency key.
367
- assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined);
368
- });
369
-
370
- test("email_triage exposes every facet the server accepts (mark-unread-from, muted, done)", () => {
371
- const triage = orgToolDef("email_triage");
372
- const props = Object.keys(triage.input_schema.properties).sort();
373
- assert.deepEqual(props, [
374
- "assigneeMemberId", "category", "done", "muted", "priority",
375
- "read", "starred", "tags", "threadId", "unreadFromMessageId",
376
- ], "the schema matches email.triage's real param set — an undocumented param is an unreachable one");
377
- // The exclusivity rules are the server's; a model that does not know them
378
- // burns a turn on a BAD_REQUEST it cannot diagnose.
379
- assert.match(triage.description, /mutually exclusive/);
380
- assert.match(triage.description, /one-verb-per-call/);
381
- });
382
-
383
- test("org_events_tail: cursor + clamped limit ride the query string", async () => {
384
- const fetchImpl = fakeFetch({ events: [] });
385
- await executeOrgTool("org_events_tail", { cursor: 7, limit: 9999 }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
386
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/events\?cursor=7&limit=500$/, "limit clamped to 500");
387
- });
388
-
389
- test("approval_wait: requires approvalId; dispatches the bounded long-poll", async () => {
390
- const missing = await executeOrgTool("approval_wait", {}, { orgConfig: CFG, agentRoot: "/tmp/none" });
391
- assert.equal(missing.error.code, "BAD_REQUEST");
392
- const fetchImpl = fakeFetch({ id: "a1", status: "approved" });
393
- const frame = await executeOrgTool("approval_wait", { approvalId: "a1" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
394
- assert.equal(frame.ok, true);
395
- assert.match(fetchImpl.calls[0].url, /approval\.wait\?id=a1&timeoutMs=55000/);
396
- });
397
-
398
- // ── executor: send-gate parity (§1.5a) ─────────────────────────────────────
399
-
400
- test("messaging_send: screened through the send-gate, idempotencyId auto-minted", async () => {
401
- const screened = [];
402
- const fetchImpl = fakeFetch({ ok: true, result: { messageId: "m1" } });
403
- const frame = await executeOrgTool(
404
- "messaging_send",
405
- { channelId: "C9", body: "hello org" },
406
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
407
- );
408
- assert.equal(frame.ok, true);
409
- assert.equal(screened.length, 1, "screenOutbound ran");
410
- assert.equal(screened[0].recipient, "C9");
411
- assert.equal(screened[0].text, "hello org");
412
- const body = JSON.parse(fetchImpl.calls[0].init.body);
413
- // hq's sendMessageSchemaV1 REQUIRES `idempotencyId`. Minting `clientMsgId`
414
- // (the column name, not the wire name) 400'd every message this plane sent.
415
- assert.ok(body.idempotencyId && body.idempotencyId.length >= 8, "idempotencyId auto-minted");
416
- assert.equal(body.clientMsgId, undefined, "the column name is NOT the wire name");
417
- assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "dispatcher idempotency key present");
418
- });
419
-
420
- test("messaging_send: a vetoing gate blocks BEFORE any network", async () => {
421
- const fetchImpl = fakeFetch();
422
- const frame = await executeOrgTool(
423
- "messaging_send",
424
- { channelId: "C9", body: "As an AI I love this" },
425
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: false, reason: "banned-phrase" }) },
426
- );
427
- assert.equal(frame.ok, false);
428
- assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
429
- assert.match(frame.error.message, /banned-phrase/);
430
- assert.equal(fetchImpl.calls.length, 0, "blocked before dispatch");
431
- });
432
-
433
- test("a THROWING gate fails open with a counted diagnostic (adapter parity)", async () => {
434
- const counted = [];
435
- const fetchImpl = fakeFetch({ ok: true, result: {} });
436
- const frame = await executeOrgTool(
437
- "messaging_send",
438
- { channelId: "C9", body: "hi" },
439
- {
440
- orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl,
441
- screenImpl: async () => { throw new Error("gate exploded"); },
442
- bumpImpl: (name, attrs) => counted.push({ name, attrs }),
443
- },
444
- );
445
- assert.equal(frame.ok, true, "fail-open");
446
- assert.equal(counted[0].name, "channel.send_gate.fail_open");
447
- });
448
-
449
- // ── executor: escape hatches ───────────────────────────────────────────────
450
-
451
- test("org_rpc: unknown method rejected against the protocol table, NO network", async () => {
452
- const fetchImpl = fakeFetch();
453
- const frame = await executeOrgTool("org_rpc", { method: "nope.nothing" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
454
- assert.equal(frame.ok, false);
455
- assert.equal(frame.error.code, "NOT_FOUND");
456
- assert.equal(fetchImpl.calls.length, 0, "validated before any network");
457
- });
458
-
459
- test("org_rpc: escape-hatch parity — messaging.send through org_rpc is ALSO screened", async () => {
460
- const screened = [];
461
- const fetchImpl = fakeFetch({ ok: true, result: {} });
462
- await executeOrgTool(
463
- "org_rpc",
464
- { method: "messaging.send", params: { channelId: "C2", body: "raw lane" } },
465
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
466
- );
467
- assert.equal(screened.length, 1, "escape hatch screened");
468
- assert.match(screened[0].text, /raw lane/, "params blob screened");
469
- // and a blocked verdict stops the dispatch
470
- const fetch2 = fakeFetch();
471
- const blocked = await executeOrgTool(
472
- "org_rpc",
473
- { method: "email.send", params: { to: ["x@y.z"], text: "leak" } },
474
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: fetch2, screenImpl: async () => ({ allow: false, reason: "no" }) },
475
- );
476
- assert.equal(blocked.ok, false);
477
- assert.equal(fetch2.calls.length, 0);
478
- });
479
-
480
- test("org_rpc: non-outbound method dispatches unscreened with optional idempotencyKey", async () => {
481
- const screened = [];
482
- const fetchImpl = fakeFetch({ ok: true, result: { got: 1 } });
483
- const frame = await executeOrgTool(
484
- "org_rpc",
485
- { method: "member.get", params: { memberId: "m-1" }, idempotencyKey: "k-1" },
486
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
487
- );
488
- assert.equal(frame.ok, true);
489
- assert.equal(screened.length, 0, "reads/non-outbound never screened");
490
- assert.match(fetchImpl.calls[0].url, /member\.get$/);
491
- });
492
-
493
- test("org_read: validated against READS; query params encoded; unknown path rejected", async () => {
494
- const fetchImpl = fakeFetch({ events: [] });
495
- const frame = await executeOrgTool("org_read", { path: "events", query: { cursor: 3, limit: 10 } }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
496
- assert.equal(frame.ok, true);
497
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/events\?cursor=3&limit=10$/);
498
- const bad = await executeOrgTool("org_read", { path: "not-a-read" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
499
- assert.equal(bad.error.code, "NOT_FOUND");
500
- assert.equal(fetchImpl.calls.length, 1, "unknown path never dispatched");
501
- });
502
-
503
- // ── executor: email tools ──────────────────────────────────────────────────
504
-
505
- test("email_send: idempotencyId auto-minted; screened; rides email.send", async () => {
506
- const screened = [];
507
- const fetchImpl = fakeFetch({ ok: true, result: { status: "queued", messageId: "e1" } });
508
- const frame = await executeOrgTool(
509
- "email_send",
510
- { to: ["a@ext.com"], subject: "Hi", text: "Body" },
511
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
512
- );
513
- assert.equal(frame.ok, true);
514
- assert.equal(screened[0].recipient, "a@ext.com");
515
- assert.match(screened[0].text, /Subject: Hi/);
516
- const body = JSON.parse(fetchImpl.calls[0].init.body);
517
- assert.ok(body.idempotencyId && body.idempotencyId.length >= 8, "idempotencyId auto-minted");
518
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/email\.send$/);
519
- });
520
-
521
- test("email tools degrade to NOT_FOUND when the family is gated off (pre-sync install)", async () => {
522
- const fetchImpl = fakeFetch();
523
- const frame = await executeOrgTool("email_inbox", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, emailAvailable: false });
524
- assert.equal(frame.ok, false);
525
- assert.equal(frame.error.code, "NOT_FOUND");
526
- assert.match(frame.error.message, /upgrade/i);
527
- assert.equal(fetchImpl.calls.length, 0);
528
- });
529
-
530
- // ── executor: artifact tools ───────────────────────────────────────────────
531
-
532
- test("artifact_create: clientMsgId auto-minted; unscreened (in-thread, not outbound); rides artifact.create", async () => {
533
- const screened = [];
534
- const envelope = { genui: "v2", artifact: { class: "ops.status", version: 1, state: "complete", source: { kind: "native" } }, actions: [], root: { type: "card", tone: "neutral", blocks: [] } };
535
- const fetchImpl = fakeFetch({ ok: true, result: { artifactId: "a1", messageId: "m1" } });
536
- const frame = await executeOrgTool(
537
- "artifact_create",
538
- { channelId: "C1", envelope },
539
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (o) => { screened.push(o); return { allow: true }; } },
540
- );
541
- assert.equal(frame.ok, true);
542
- assert.deepEqual(frame.result, { artifactId: "a1", messageId: "m1" });
543
- assert.equal(screened.length, 0, "artifact posts are in-thread — never send-gate screened");
544
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/artifact\.create$/);
545
- const body = JSON.parse(fetchImpl.calls[0].init.body);
546
- assert.ok(body.clientMsgId && body.clientMsgId.length >= 8, "clientMsgId auto-minted");
547
- assert.deepEqual(body.envelope, envelope, "envelope posted verbatim");
548
- assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined, "sideEffecting:false — no dispatcher key; the service dedupes on clientMsgId");
549
- });
550
-
551
- test("artifact_act: idempotencyKey auto-minted when absent, caller's key preserved; held result is a NORMAL frame", async () => {
552
- const fetchImpl = fakeFetch({ ok: true, result: { held: true, approvalId: "ap-1" } });
553
- const frame = await executeOrgTool(
554
- "artifact_act",
555
- { artifactId: "a1", action: "approve" },
556
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
557
- );
558
- assert.equal(frame.ok, true, "approval-held is a result, not an error");
559
- assert.equal(frame.result.held, true);
560
- assert.equal(frame.result.approvalId, "ap-1");
561
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/artifact\.act$/);
562
- const minted = JSON.parse(fetchImpl.calls[0].init.body);
563
- assert.ok(minted.idempotencyKey && minted.idempotencyKey.length >= 8, "idempotencyKey auto-minted");
564
- // A caller-supplied key (retry / post-approval re-invoke) rides through untouched.
565
- await executeOrgTool(
566
- "artifact_act",
567
- { artifactId: "a1", action: "approve", idempotencyKey: "approve-a1" },
568
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
569
- );
570
- assert.equal(JSON.parse(fetchImpl.calls[1].init.body).idempotencyKey, "approve-a1");
571
- });
572
-
573
- test("artifact reads ride their rpc methods; tools degrade to NOT_FOUND when the family is gated off", async () => {
574
- const fetchImpl = fakeFetch({ ok: true, result: { items: [] } });
575
- await executeOrgTool("artifact_list", { channelId: "C1", state: "complete" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
576
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/artifact\.list$/);
577
- await executeOrgTool("artifact_catalog", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
578
- assert.match(fetchImpl.calls[1].url, /\/api\/v1\/artifact\.catalog$/);
579
- const gated = fakeFetch();
580
- const frame = await executeOrgTool("artifact_get", { artifactId: "a1" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gated, artifactAvailable: false });
581
- assert.equal(frame.ok, false);
582
- assert.equal(frame.error.code, "NOT_FOUND");
583
- assert.match(frame.error.message, /upgrade/i);
584
- assert.equal(gated.calls.length, 0);
585
- });
586
-
587
- // ── executor: directory + design desks (2026-07) ───────────────────────────
588
-
589
- test("directory/design desks gate on their OWN probe, independently of the other desks", () => {
590
- // One DESK_PROBES line lights a desk up on BOTH planes; a desk whose probe
591
- // method is absent from the vendored protocol must not register.
592
- assert.equal(deskFamilyAvailable("directory"), true, "vendored protocol carries directory.search");
593
- assert.equal(deskFamilyAvailable("design"), true, "vendored protocol carries branding.getFoundation");
594
- assert.equal(deskFamilyAvailable("nope"), false, "unknown desk → default-deny");
595
- assert.equal(ORG_TOOLS.filter((t) => t.desk === "directory").length, 14);
596
- assert.equal(ORG_TOOLS.filter((t) => t.desk === "design").length, 8);
597
- // Neither desk smuggles an outbound lane in: no directory/design tool sends.
598
- assert.equal(ORG_TOOLS.filter((t) => (t.desk === "directory" || t.desk === "design") && t.outbound).length, 0);
599
- });
600
-
601
- test("directory reads/writes ride their rpc methods; a side-effecting write carries a dispatcher key", async () => {
602
- const fetchImpl = fakeFetch({ ok: true, result: { people: [] } });
603
- const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
604
- await executeOrgTool("directory_search", { q: "hartmann" }, o);
605
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/directory\.search$/);
606
- assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body), { q: "hartmann" });
607
- assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined, "sideEffecting:false read sends no key");
608
-
609
- await executeOrgTool("directory_list_people", { relationship: "client", sort: "touch" }, o);
610
- assert.match(fetchImpl.calls[1].url, /\/api\/v1\/directory\.listPeople$/);
611
-
612
- await executeOrgTool(
613
- "directory_propose_capture",
614
- { kind: "PERSON", payload: { person: { displayName: "Dr. Lena Hartmann" } }, source: "MAIL", sourceFingerprint: "sig:abc" },
615
- o,
616
- );
617
- assert.match(fetchImpl.calls[2].url, /\/api\/v1\/directory\.proposeCapture$/);
618
- assert.ok(
619
- fetchImpl.calls[2].init.headers["x-idempotency-key"],
620
- "sideEffecting write gets a fresh dispatcher key",
621
- );
622
- assert.equal(JSON.parse(fetchImpl.calls[2].init.body).sourceFingerprint, "sig:abc");
623
- });
624
-
625
- test("design tools ride branding.*; the costed quote call posts budgetCents:0 verbatim", async () => {
626
- const fetchImpl = fakeFetch({ ok: true, result: { slots: [], costCents: 0 } });
627
- const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
628
- await executeOrgTool("design_foundation", { historyLimit: 5 }, o);
629
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/branding\.getFoundation$/);
630
- await executeOrgTool("design_voice", {}, o);
631
- assert.match(fetchImpl.calls[1].url, /\/api\/v1\/branding\.getVoice$/);
632
- await executeOrgTool("design_generate_image", { budgetCents: 0 }, o);
633
- assert.match(fetchImpl.calls[2].url, /\/api\/v1\/branding\.generateImage$/);
634
- assert.equal(JSON.parse(fetchImpl.calls[2].init.body).budgetCents, 0, "0 is the QUOTE call, not a falsy drop");
635
- });
636
-
637
- test("design_rewrite_in_voice produces copy and is NEVER send-gate screened", async () => {
638
- // It returns a rewrite; it sends nothing. Whatever the caller does with the
639
- // result is screened at the send verb, not here.
640
- const screened = [];
641
- const fetchImpl = fakeFetch({ ok: true, result: { text: "Rewritten.", costCents: 3 } });
642
- const frame = await executeOrgTool(
643
- "design_rewrite_in_voice",
644
- { text: "we are excited to announce" },
645
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (x) => { screened.push(x); return { allow: true }; } },
646
- );
647
- assert.equal(frame.ok, true);
648
- assert.equal(screened.length, 0);
649
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/branding\.rewriteInVoice$/);
650
- });
651
-
652
- test("directory/design tools degrade to NOT_FOUND when their desk is gated off (pre-sync install)", async () => {
653
- for (const name of ["directory_search", "design_foundation"]) {
654
- const gated = fakeFetch();
655
- const frame = await executeOrgTool(name, { q: "x" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gated, desksAvailable: false });
656
- assert.equal(frame.ok, false, `${name} gated`);
657
- assert.equal(frame.error.code, "NOT_FOUND");
658
- assert.match(frame.error.message, /upgrade/i);
659
- assert.equal(gated.calls.length, 0, `${name} never dispatched`);
660
- }
661
- });
662
-
663
- // ── executor: calls desk (call working sessions, 2026-08) ──────────────────
664
-
665
- test("calls desk gates on calling.appShareStart; five tools, one outbound verb", () => {
666
- assert.equal(deskFamilyAvailable("calls"), true, "vendored protocol carries calling.appShareStart");
667
- const calls = ORG_TOOLS.filter((t) => t.desk === "calls");
668
- assert.equal(calls.length, 5);
669
- // The share-step verb is the block's ONE outbound lane: its highlight notes
670
- // and typed text are read by every human on the call.
671
- assert.deepEqual(calls.filter((t) => t.outbound).map((t) => t.name), ["org_call_share_step"]);
672
- assert.deepEqual(
673
- calls.filter((t) => t.access === "read").map((t) => t.name).sort(),
674
- ["org_call_events", "org_call_visual_context"],
675
- );
676
- });
677
-
678
- test("org_call_share_start/end ride their rpc methods with a fresh dispatcher key", async () => {
679
- const fetchImpl = fakeFetch({ ok: true, result: { sessionId: "s1" } });
680
- const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
681
- const frame = await executeOrgTool(
682
- "org_call_share_start",
683
- { callId: "call-1", surface: { kind: "doc", ref: "f-1", title: "Q3 plan" } },
684
- o,
685
- );
686
- assert.equal(frame.ok, true);
687
- assert.equal(frame.result.sessionId, "s1");
688
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/calling\.appShareStart$/);
689
- assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "sideEffecting write gets a fresh dispatcher key");
690
- assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body).surface, { kind: "doc", ref: "f-1", title: "Q3 plan" });
691
-
692
- await executeOrgTool("org_call_share_end", { callId: "call-1", sessionId: "s1" }, o);
693
- assert.match(fetchImpl.calls[1].url, /\/api\/v1\/calling\.appShareEnd$/);
694
- });
695
-
696
- test("org_call_share_step: screened through the send-gate (notes + typed text), then rides calling.appShareAct", async () => {
697
- const screened = [];
698
- const fetchImpl = fakeFetch({ ok: true, result: { seq: 4 } });
699
- const steps = [
700
- { k: "scroll", anchor: "block:3" },
701
- { k: "highlight", anchor: "block:3", note: "revenue line" },
702
- { k: "type", anchor: "block:4", text: "Draft summary" },
703
- ];
704
- const frame = await executeOrgTool(
705
- "org_call_share_step",
706
- { callId: "call-1", sessionId: "s1", steps },
707
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async (x) => { screened.push(x); return { allow: true }; } },
708
- );
709
- assert.equal(frame.ok, true);
710
- assert.equal(screened.length, 1, "screenOutbound ran");
711
- assert.equal(screened[0].recipient, "call-1");
712
- assert.match(screened[0].text, /revenue line/);
713
- assert.match(screened[0].text, /Draft summary/);
714
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/calling\.appShareAct$/);
715
- assert.deepEqual(JSON.parse(fetchImpl.calls[0].init.body).steps, steps);
716
-
717
- // A vetoing gate blocks BEFORE any network — messaging_send parity.
718
- const fetch2 = fakeFetch();
719
- const blocked = await executeOrgTool(
720
- "org_call_share_step",
721
- { callId: "call-1", sessionId: "s1", steps: [{ k: "highlight", anchor: "block:1", note: "As an AI" }] },
722
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: fetch2, screenImpl: async () => ({ allow: false, reason: "banned-phrase" }) },
723
- );
724
- assert.equal(blocked.ok, false);
725
- assert.equal(blocked.error.code, "FORBIDDEN_SCOPE");
726
- assert.equal(fetch2.calls.length, 0, "blocked before dispatch");
727
- });
728
-
729
- test("calls reads ride their rpc methods without a dispatcher key", async () => {
730
- const fetchImpl = fakeFetch({ ok: true, result: { events: [] } });
731
- const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
732
- await executeOrgTool("org_call_visual_context", { callId: "call-1", limit: 10 }, o);
733
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/calling\.getVisualContext$/);
734
- assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], undefined, "sideEffecting:false read sends no key");
735
- await executeOrgTool("org_call_events", { callId: "call-1" }, o);
736
- assert.match(fetchImpl.calls[1].url, /\/api\/v1\/calling\.getCallEvents$/);
737
- });
738
-
739
- test("calls tools degrade to NOT_FOUND when the desk is gated off (pre-sync install)", async () => {
740
- const gated = fakeFetch();
741
- const frame = await executeOrgTool(
742
- "org_call_share_start",
743
- { callId: "call-1", surface: { kind: "doc", ref: "f-1" } },
744
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gated, desksAvailable: false },
745
- );
746
- assert.equal(frame.ok, false);
747
- assert.equal(frame.error.code, "NOT_FOUND");
748
- assert.match(frame.error.message, /upgrade/i);
749
- assert.equal(gated.calls.length, 0, "never dispatched");
750
- });
751
-
752
- // ── executor: org_whoami ───────────────────────────────────────────────────
753
-
754
- test("org_whoami: unenrolled → config summary with member:null + note, never an error", async () => {
755
- const frame = await executeOrgTool("org_whoami", {}, { orgConfig: {}, agentRoot: "/tmp/none" });
756
- assert.equal(frame.ok, true);
757
- assert.equal(frame.result.member, null);
758
- assert.equal(frame.result.tokenPresent, false);
759
- assert.match(frame.result.note, /setup --only org|COHORT_API_TOKEN|COHORT_ORG_ID/);
760
- });
761
-
762
- test("org_whoami: resolves the member via the org-data whoami lane; token never in the result", async () => {
763
- const fetchImpl = fakeFetch({ slug: "astra", displayName: "Astra", agentEmail: "astra@agents.example" });
764
- const frame = await executeOrgTool("org_whoami", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
765
- assert.equal(frame.ok, true);
766
- assert.equal(frame.result.member.slug, "astra");
767
- assert.equal(frame.result.tokenPresent, true);
768
- assert.equal(frame.result.protocolVersion, PROTOCOL_VERSION);
769
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/org\/whoami\?orgId=acme/);
770
- assert.ok(!JSON.stringify(frame).includes("nlk_secret"), "token never surfaces");
771
- });
772
-
773
- test("org_whoami: unresolvable member degrades to summary + hint note (no error frame)", async () => {
774
- const fetchImpl = fakeFetch({ error: { code: "NOT_FOUND", message: "ambiguous" } }, { status: 404, ok: false });
775
- const frame = await executeOrgTool("org_whoami", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
776
- assert.equal(frame.ok, true);
777
- assert.equal(frame.result.member, null);
778
- assert.match(frame.result.note, /COHORT_AGENT_ID/);
779
- });
780
-
781
- test("org_whoami: COHORT_AGENT_ID hint routes to the direct member GET", async () => {
782
- const fetchImpl = fakeFetch({ slug: "astra", displayName: "Astra" });
783
- const frame = await executeOrgTool("org_whoami", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, env: { COHORT_AGENT_ID: "astra" } });
784
- assert.equal(frame.ok, true);
785
- assert.match(fetchImpl.calls[0].url, /\/api\/v1\/org\/members\/astra\?orgId=acme/);
786
- });
787
-
788
- // ── engage_colleagues: the judged path through the tool plane ──────────────
789
-
790
- test("engage_colleagues returns an OK frame when it decides NOBODY needs involving", async () => {
791
- // "Nobody needed involving" is an answer. Returning it as an error would teach
792
- // the model to retry until it manages to interrupt somebody.
793
- const f = fakeFetch({ ok: true, result: { members: [] } });
794
- const res = await executeOrgTool(
795
- "engage_colleagues",
796
- { id: "W-1", title: "Refresh the digest", state: "in_progress", interestedParties: ["M-2"] },
797
- { orgConfig: CFG, agentRoot: "/tmp/engage-tool-test", fetchImpl: f, env: {} },
798
- );
799
- assert.equal(res.ok, true);
800
- assert.equal(res.result.engaged, false);
801
- assert.equal(res.result.reasonCode, "self_contained");
802
- assert.deepEqual(res.result.consideredNotEngaged, ["M-2"]);
803
- assert.ok(res.result.why.length, "the reasoning comes back to the model, not just the verdict");
804
- assert.ok(
805
- !f.calls.some((c) => /messaging\.send/.test(c.url)),
806
- "a self-contained decision must not put a message on the wire",
807
- );
808
- });
809
-
810
- test("engage_colleagues refuses an unidentified piece of work rather than guessing", async () => {
811
- const res = await executeOrgTool(
812
- "engage_colleagues",
813
- { title: "no id" },
814
- { orgConfig: CFG, agentRoot: "/tmp/engage-tool-test", fetchImpl: fakeFetch(), env: {} },
815
- );
816
- assert.equal(res.ok, false);
817
- assert.equal(res.error.code, "BAD_REQUEST");
818
- });
819
-
820
- test("messaging_send passes mentions straight through to the wire", async () => {
821
- const f = fakeFetch({ ok: true, result: { id: "m1" } });
822
- await executeOrgTool(
823
- "messaging_send",
824
- { channelId: "C-1", body: "Need a decision here.", mentions: ["M-2"] },
825
- { orgConfig: CFG, agentRoot: "/tmp", fetchImpl: f, env: {}, screenImpl: async () => ({ allow: true }) },
826
- );
827
- const body = JSON.parse(f.calls[0].init.body);
828
- // The model writes the obvious thing — an array of id strings — and the param
829
- // contract coerces it at the one chokepoint. Sending the string form verbatim
830
- // fails hq's zod and 400s the WHOLE message, tags and body alike.
831
- assert.deepEqual(body.mentions, [{ memberId: "M-2" }]);
832
- });
833
-
834
- // ── speaking: messaging_send_voice_note ────────────────────────────────────
835
- //
836
- // The verb that closes "I can't send audio voice notes". hq could always
837
- // synthesise an agent's voice note, but only as a REFLEX answering a human's
838
- // spoken note in kind — asked in text, an agent had nothing to call.
839
- //
840
- // What is pinned here is the composition: record, then post the recording as an
841
- // ORDINARY message attachment. There is no audio-only send path, so the post
842
- // leg carries the same ACL, audit and dedup as any typed message; and a refused
843
- // recording must never be followed by a post that implies one was made.
844
-
845
- test("messaging_send_voice_note: records, then posts the file as an ordinary attachment", async () => {
846
- let n = 0;
847
- const calls = [];
848
- const fetchImpl = async (url, init) => {
849
- calls.push({ url: String(url), body: JSON.parse(init.body) });
850
- n += 1;
851
- return {
852
- ok: true,
853
- status: 200,
854
- headers: { get: () => undefined },
855
- json: async () =>
856
- n === 1
857
- ? { ok: true, result: { fileId: "file_v1", durationMs: 4200 } }
858
- : { ok: true, result: { messageId: "m1" } },
859
- };
860
- };
861
- const frame = await executeOrgTool(
862
- "messaging_send_voice_note",
863
- { channelId: "C1", text: "Shipping Friday." },
864
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
865
- );
866
- assert.equal(frame.ok, true);
867
- assert.match(calls[0].url, /messaging\.synthesizeVoiceNote$/);
868
- assert.equal(calls[0].body.text, "Shipping Friday.");
869
- assert.match(calls[1].url, /messaging\.send$/);
870
- assert.deepEqual(calls[1].body.attachments, [{ fileId: "file_v1" }]);
871
- // The written body defaults to the spoken words, never an empty message.
872
- assert.equal(calls[1].body.body, "Shipping Friday.");
873
- assert.ok(calls[1].body.idempotencyId, "the post is dedup-keyed like any send");
874
- assert.equal(frame.result.fileId, "file_v1");
875
- });
876
-
877
- test("messaging_send_voice_note: a refused recording is NOT followed by a post", async () => {
878
- const calls = [];
879
- const fetchImpl = async (url, init) => {
880
- calls.push(String(url));
881
- return {
882
- ok: false,
883
- status: 403,
884
- headers: { get: () => undefined },
885
- json: async () => ({
886
- ok: false,
887
- error: { code: "FORBIDDEN_SCOPE", message: "voice note not recorded (no-key): ..." },
888
- }),
889
- };
890
- };
891
- const frame = await executeOrgTool(
892
- "messaging_send_voice_note",
893
- { channelId: "C1", text: "hi" },
894
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
895
- );
896
- assert.equal(frame.ok, false);
897
- assert.match(frame.error.message, /no-key/);
898
- assert.equal(calls.length, 1, "no post claiming a voice note that was never recorded");
899
- });
900
-
901
- test("messaging_send_voice_note: the send-gate screens the SPOKEN words before synthesis", async () => {
902
- const fetchImpl = fakeFetch();
903
- const seen = [];
904
- const frame = await executeOrgTool(
905
- "messaging_send_voice_note",
906
- { channelId: "C1", text: "the secret is hunter2", body: "see note" },
907
- {
908
- orgConfig: CFG,
909
- agentRoot: "/tmp/none",
910
- fetchImpl,
911
- screenImpl: async (args) => {
912
- seen.push(args);
913
- return args.text.includes("hunter2") ? { allow: false, reason: "credential" } : { allow: true };
914
- },
915
- },
916
- );
917
- assert.equal(frame.ok, false);
918
- assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
919
- // The gate sees the PROSE — the spoken words and the written body — not a
920
- // stringified params blob. A gate judging JSON would pass sentences it never
921
- // actually read.
922
- assert.equal(seen[0].text, "the secret is hunter2\nsee note");
923
- assert.equal(seen[0].recipient, "C1");
924
- // Blocked BEFORE the network: audio is the hardest content to retract, and a
925
- // blocked note must not cost a billable synthesis either.
926
- assert.equal(fetchImpl.calls.length, 0);
927
- });
928
-
929
- test("messaging_send_voice_note: channelId and text are both required", async () => {
930
- const fetchImpl = fakeFetch();
931
- const frame = await executeOrgTool(
932
- "messaging_send_voice_note",
933
- { channelId: "C1" },
934
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl, screenImpl: async () => ({ allow: true }) },
935
- );
936
- assert.equal(frame.ok, false);
937
- assert.equal(frame.error.code, "BAD_REQUEST");
938
- assert.equal(fetchImpl.calls.length, 0);
939
- });
940
-
941
- // ── mandate / sub-agent registry / preferences (2026-08 parity delta) ───────
942
-
943
- test("mandate tools ride their mandate.* methods and POST the params verbatim", async () => {
944
- const seen = [];
945
- const fetchImpl = async (url, init) => {
946
- seen.push({ url: String(url), body: JSON.parse(init.body) });
947
- return { ok: true, status: 200, headers: { get: () => undefined }, json: async () => ({ ok: true, result: {} }) };
948
- };
949
- fetchImpl.calls = seen;
950
- const o = { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl };
951
- await executeOrgTool("mandate_read", {}, o);
952
- await executeOrgTool("mandate_objectives", { state: "active", includeRetired: false }, o);
953
- await executeOrgTool("mandate_kpi_series", { objectiveId: "ob-1", limit: 20 }, o);
954
- await executeOrgTool("mandate_propose_objective", { key: "pipeline-coverage", text: "3x cover", target: 3 }, o);
955
- await executeOrgTool(
956
- "mandate_record_kpi_sample",
957
- { objectiveId: "ob-1", value: 2.4, window: "2026-W33", evidence: { method: "crm.listDeals" } },
958
- o,
959
- );
960
- assert.deepEqual(seen.map((c) => c.url), [
961
- "https://org.example/api/v1/mandate.get",
962
- "https://org.example/api/v1/mandate.tree",
963
- "https://org.example/api/v1/mandate.series",
964
- "https://org.example/api/v1/mandate.propose",
965
- "https://org.example/api/v1/mandate.sample",
966
- ]);
967
- // Pass-through, byte for byte: hq's mandate schemas read these names, and the
968
- // evidence OBJECT is what the server's honesty gate inspects — flattening it
969
- // to a string here would silently downgrade every sample to `llm`.
970
- assert.deepEqual(seen[3].body, { key: "pipeline-coverage", text: "3x cover", target: 3 });
971
- assert.deepEqual(seen[4].body.evidence, { method: "crm.listDeals" });
972
- });
973
-
974
- test("the mandate belt stops at PROPOSE — no adopt/retire tool exists at any tier", () => {
975
- const names = new Set(ORG_TOOLS.map((t) => t.name));
976
- for (const forbidden of ["mandate_adopt", "mandate_retire"]) {
977
- assert.equal(names.has(forbidden), false, `${forbidden} must not exist`);
978
- }
979
- // The absence is structural, not incidental: no curated tool may bind a
980
- // method whose scope is missing from DEFAULT_AGENT_SCOPES, because a tool the
981
- // seat's own credential can never execute is a promise the model will make
982
- // and fail to keep. `mandate.write` is the anti-Goodhart scope.
983
- const bound = ORG_TOOLS.filter((t) => t.binding.kind === "rpc").map((t) => t.binding.method);
984
- for (const m of bound) {
985
- assert.notEqual(methodDef(m).scope, "mandate.write", `${m} needs mandate.write — not an agent's to hold`);
986
- }
987
- });
988
-
989
- test("subagent tools are READS ONLY — the writes stay with the outbox-owning client", () => {
990
- const subagentTools = ORG_TOOLS.filter(
991
- (t) => t.binding.kind === "rpc" && t.binding.method.startsWith("subagent."),
992
- );
993
- assert.deepEqual(
994
- subagentTools.map((t) => t.name).sort(),
995
- ["subagent_get", "subagent_list", "subagent_resolve"],
996
- );
997
- for (const t of subagentTools) {
998
- assert.equal(t.access, "read", `${t.name} is a read`);
999
- assert.equal(methodDef(t.binding.method).sideEffecting, false, `${t.binding.method} mutates nothing`);
1000
- }
1001
- });
1002
-
1003
- test("preference tools ship as a PAIR — a write-only memory is not parity", async () => {
1004
- const names = ORG_TOOLS.filter(
1005
- (t) => t.binding.kind === "rpc" && t.binding.method.startsWith("preference."),
1006
- );
1007
- assert.deepEqual(names.map((t) => t.name).sort(), ["preference_list", "preference_remember"]);
1008
- assert.equal(orgToolDef("preference_list").access, "read");
1009
- assert.equal(orgToolDef("preference_remember").access, "write");
1010
- // `why` is required in the SCHEMA, not just server-side: the evidence rule
1011
- // has to reach the model, or it learns the rule by BAD_REQUEST.
1012
- assert.ok(orgToolDef("preference_remember").input_schema.required.includes("why"));
1013
-
1014
- const fetchImpl = fakeFetch({ ok: true, result: { id: "pref-1" } });
1015
- const frame = await executeOrgTool(
1016
- "preference_remember",
1017
- { domain: "travel", key: "seat", value: "aisle", why: "said so in #travel" },
1018
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
1019
- );
1020
- assert.equal(frame.ok, true);
1021
- assert.equal(fetchImpl.calls[0].url, "https://org.example/api/v1/preference.upsert");
1022
- // sideEffecting:true at the dispatcher → a fresh header key per invocation.
1023
- assert.ok(fetchImpl.calls[0].init.headers["x-idempotency-key"], "dispatcher key attached");
1024
- });
1025
-
1026
- // ── the access mechanism, TABLE-WIDE ───────────────────────────────────────
1027
- //
1028
- // The per-delta assertion below ("the new tools reach the tiers that need
1029
- // them") lists ten names by hand, so it polices the tools whose author
1030
- // remembered to add them and nothing else — every family that ships after it is
1031
- // unguarded by default. These three assertions are the table-wide version:
1032
- // they iterate ORG_TOOLS, so a capability cannot be parked at the CEO-only tier
1033
- // or given a tier its protocol scope contradicts without one of them going red,
1034
- // whether or not anyone updates a list.
1035
-
1036
- test("ACCESS MECHANISM: `admin` is a CLOSED set of two escape hatches, not a parking space", () => {
1037
- const admins = ORG_TOOLS.filter((t) => t.access === "admin").map((t) => t.name).sort();
1038
- assert.deepEqual(
1039
- admins,
1040
- [...ADMIN_TOOLS].sort(),
1041
- "only org_rpc/org_read may be admin-tier — anything else is a CAPABILITY, and a capability " +
1042
- "at admin is CEO-only, i.e. exactly as unreachable for a leadership or default agent as " +
1043
- "the org_rpc route a curated tool exists to replace",
1044
- );
1045
- for (const name of ADMIN_TOOLS) {
1046
- const def = orgToolDef(name);
1047
- assert.ok(def, `${name} is in the table`);
1048
- assert.equal(def.access, "admin", `${name} is the escape hatch it claims to be`);
1049
- // Both hatches are `local`: they take a method/path NAME and validate it
1050
- // against the frozen table before any I/O. A desk verb can never be one.
1051
- assert.equal(def.binding.kind, "local", `${name} is a console, not a capability`);
1052
- }
1053
- });
1054
-
1055
- test("ACCESS MECHANISM: every rpc-bound tool's tier is DERIVED from its protocol scope", () => {
1056
- // hq gates on SCOPE, so the native clamp has to mirror the scope lane or it
1057
- // either withholds a tool hq would have allowed or offers one hq will refuse.
1058
- // Note the rule this is NOT: `sideEffecting ? "write" : "read"` is wrong for
1059
- // seven tools already in the table — knowledge.search, directory.ask and the
1060
- // three branding renders/exports are AUDITED READS (side-effecting, `.read`
1061
- // scope, runnable by a VIEWER seat), and artifact.create/act are the mirror
1062
- // case. Deriving from sideEffecting would demote all five audited reads out
1063
- // of the lookup set every tier gets.
1064
- let checked = 0;
1065
- for (const t of ORG_TOOLS) {
1066
- if (!t.binding || t.binding.kind !== "rpc") continue;
1067
- checked += 1;
1068
- const want = expectedAccessFor(t.binding.method);
1069
- assert.ok(want, `${t.name} binds a vendored method`);
1070
- assert.equal(
1071
- t.access,
1072
- want,
1073
- `${t.name} (${t.binding.method}, scope ${methodDef(t.binding.method).scope}) must be access:"${want}"`,
1074
- );
1075
- }
1076
- assert.ok(checked >= 120, "the whole rpc-bound belt was checked, not a sample");
1077
- });
1078
-
1079
- test("ACCESS MECHANISM: no tool can fall out of every tier — the partition is exhaustive", () => {
1080
- // Exact string equality used to bucket the table, so a missing or mis-cased
1081
- // `access` matched NO bucket and reached NO native access level, while the
1082
- // MCP plane kept publishing it. normalizeToolAccess is the tool-side twin of
1083
- // normalizeAccessLevel: it normalises casing and sends a genuinely
1084
- // unrecognised tier to the most-restricted bucket, loudly.
1085
- for (const t of ORG_TOOLS) {
1086
- assert.ok(
1087
- ["read", "write", "admin"].includes(t.access),
1088
- `${t.name} declares a known tier verbatim (the normaliser is a safety net, not a licence)`,
1089
- );
1090
- const warnings = [];
1091
- assert.equal(normalizeToolAccess(t.access, t.name, (m) => warnings.push(m)), t.access);
1092
- assert.equal(warnings.length, 0, `${t.name} normalises silently`);
1093
- }
1094
- // And the safety net itself: a typo fails CLOSED and NOISY, never invisible.
1095
- const warnings = [];
1096
- assert.equal(normalizeToolAccess("Read", "x_tool", (m) => warnings.push(m)), "read", "casing is forgiven");
1097
- assert.equal(normalizeToolAccess(undefined, "x_tool", (m) => warnings.push(m)), "admin", "a missing tier fails closed");
1098
- assert.equal(warnings.length, 1, "and says so out loud");
1099
- assert.match(warnings[0], /x_tool/);
1100
- });
1101
-
1102
- test("the new tools reach the tiers that need them — reads everywhere, no new admin surface", () => {
1103
- const added = [
1104
- "mandate_read", "mandate_objectives", "mandate_kpi_series",
1105
- "mandate_propose_objective", "mandate_record_kpi_sample",
1106
- "subagent_list", "subagent_get", "subagent_resolve",
1107
- "preference_remember", "preference_list",
1108
- ];
1109
- for (const n of added) {
1110
- const def = orgToolDef(n);
1111
- assert.ok(def, `${n} is in the table`);
1112
- // The whole point of the delta: NONE of these may land at access:"admin",
1113
- // which is the CEO-only escape-hatch tier. A capability parked there is
1114
- // exactly as unreachable as the org_rpc route it was meant to replace.
1115
- assert.notEqual(def.access, "admin", `${n} must not be admin-tier`);
1116
- assert.equal(def.outbound, undefined, `${n} sends nothing a human reads as a message`);
1117
- }
1118
- assert.equal(
1119
- added.filter((n) => orgToolDef(n).access === "read").length,
1120
- 7,
1121
- "seven reads (3 mandate, 3 subagent, 1 preference) land in EVERY tier's lookup set",
1122
- );
1123
- });
1124
-
1125
- test("the new tools FAIL CLOSED with no credential — refusal, never a silent success", async () => {
1126
- const fetchImpl = fakeFetch();
1127
- for (const n of ["mandate_read", "subagent_list", "preference_remember"]) {
1128
- const frame = await executeOrgTool(n, {}, { orgConfig: {}, agentRoot: "/tmp/none", fetchImpl });
1129
- assert.equal(frame.ok, false, `${n} refuses`);
1130
- assert.equal(frame.error.code, "UNAUTHORIZED");
1131
- assert.match(frame.error.message, /COHORT_API_TOKEN/);
1132
- }
1133
- assert.equal(fetchImpl.calls.length, 0, "nothing dispatched without a credential");
1134
- });
1135
-
1136
- test("a 401/403 from hq surfaces VERBATIM on the new tools (a refusal is an answer)", async () => {
1137
- const denied = fakeFetch(
1138
- { ok: false, error: { code: "FORBIDDEN_SCOPE", message: "resolving another member's sub-agents requires admin" } },
1139
- { ok: false, status: 403 },
1140
- );
1141
- const frame = await executeOrgTool(
1142
- "subagent_resolve",
1143
- { memberId: "someone-else" },
1144
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: denied },
1145
- );
1146
- assert.equal(frame.ok, false);
1147
- assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
1148
- assert.match(frame.error.message, /admin/);
1149
-
1150
- const unauth = fakeFetch({ ok: false, error: { code: "UNAUTHORIZED", message: "bad key" } }, { ok: false, status: 401 });
1151
- const f2 = await executeOrgTool("mandate_read", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: unauth });
1152
- assert.equal(f2.ok, false);
1153
- assert.equal(f2.error.code, "UNAUTHORIZED");
1154
- });
1155
-
1156
- test("transport failure on a new tool fails open into an INTERNAL frame, never a throw", async () => {
1157
- const thrower = async () => { throw new Error("econnrefused"); };
1158
- const frame = await executeOrgTool(
1159
- "mandate_objectives",
1160
- {},
1161
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: thrower },
1162
- );
1163
- assert.equal(frame.ok, false);
1164
- assert.equal(frame.error.code, "INTERNAL");
1165
- });
1166
-
1167
- // ── WP-M4: front-door tools (board_mine / board_track / session_status) ────
1168
-
1169
- test("board_mine: read access, GET /v1/board.mine, items normalised, [] when hq lacks the read (fail-open)", async () => {
1170
- const def = orgToolDef("board_mine");
1171
- assert.ok(def, "board_mine curated");
1172
- assert.equal(def.access, "read");
1173
- assert.equal(def.binding.kind, "local", "local so an hq without board.mine yet degrades to [] instead of a bare 404");
1174
- const fetchImpl = fakeFetch({ items: [{ itemId: "i1", title: "Deck", col: "doing" }] });
1175
- const frame = await executeOrgTool("board_mine", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl });
1176
- assert.equal(frame.ok, true);
1177
- assert.deepEqual(frame.result.items, [{ itemId: "i1", title: "Deck", col: "doing" }]);
1178
- assert.equal(frame.result.count, 1);
1179
- assert.match(fetchImpl.calls[0].url, /\/v1\/board\.mine$/);
1180
- assert.equal(fetchImpl.calls[0].init.method, "GET");
1181
- const gone = fakeFetch({ error: { code: "NOT_FOUND", message: "no such read" } }, { status: 404, ok: false });
1182
- const f2 = await executeOrgTool("board_mine", {}, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: gone });
1183
- assert.equal(f2.ok, true, "fail-open: an older hq is an empty list, not an error the model must interpret");
1184
- assert.deepEqual(f2.result.items, []);
1185
- // No token → UNAUTHORIZED like every other network tool.
1186
- const noTok = await executeOrgTool("board_mine", {}, { orgConfig: { org: { cohort: { enabled: true, base: "https://org.example" } } }, agentRoot: "/tmp/none", fetchImpl });
1187
- assert.equal(noTok.ok, false);
1188
- assert.equal(noTok.error.code, "UNAUTHORIZED");
1189
- });
1190
-
1191
- test("board_track: write access on board.track; title/why/stage pass through; whole ladder incl. accepted|done; bad stage rejected offline", async () => {
1192
- const def = orgToolDef("board_track");
1193
- assert.ok(def, "board_track curated");
1194
- assert.equal(def.access, "write");
1195
- assert.equal(def.binding.method, "board.track");
1196
- assert.deepEqual(def.input_schema.properties.stage.enum, ["accepted", "working", "blocked", "review", "done", "failed"]);
1197
- assert.ok(def.input_schema.properties.title, "title passthrough");
1198
- assert.ok(def.input_schema.properties.why, "why passthrough");
1199
- const fetchImpl = fakeFetch({ ok: true, result: { tracked: true, taskId: "t1", col: "todo", created: true } });
1200
- const frame = await executeOrgTool(
1201
- "board_track",
1202
- { channelId: "c1", messageId: "m1", stage: "accepted", title: "Build the deck", why: "asked in DM", note: "on it" },
1203
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
1204
- );
1205
- assert.equal(frame.ok, true);
1206
- assert.equal(frame.result.taskId, "t1");
1207
- assert.match(fetchImpl.calls[0].url, /\/v1\/board\.track$/);
1208
- const body = JSON.parse(fetchImpl.calls[0].init.body);
1209
- assert.equal(body.stage, "accepted");
1210
- assert.equal(body.title, "Build the deck");
1211
- assert.equal(body.why, "asked in DM");
1212
- assert.equal(body.service, "cohort", "service defaults to cohort");
1213
- assert.equal(body.channelId, "c1");
1214
- assert.equal(body.messageId, "m1");
1215
- assert.equal(fetchImpl.calls[0].init.headers["x-idempotency-key"], "board.track:c1:m1:accepted", "stable per (ask, stage)");
1216
- const bad = fakeFetch();
1217
- const rej = await executeOrgTool("board_track", { channelId: "c1", messageId: "m1", stage: "later" }, { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl: bad });
1218
- assert.equal(rej.ok, false);
1219
- assert.equal(rej.error.code, "BAD_REQUEST");
1220
- assert.equal(bad.calls.length, 0, "validated before any network");
1221
- });
1222
-
1223
- test("session_status: offline local read of state/session + state/org/board-mine; works with no token", async () => {
1224
- const def = orgToolDef("session_status");
1225
- assert.ok(def, "session_status curated");
1226
- assert.equal(def.access, "read");
1227
- assert.equal(def.binding.kind, "local");
1228
- const root = mkdtempSync(join(tmpdir(), "ts-session-"));
1229
- try {
1230
- mkdirSync(join(root, "state", "session", "handoffs"), { recursive: true });
1231
- mkdirSync(join(root, "state", "org"), { recursive: true });
1232
- writeFileSync(join(root, "state", "session", "heartbeat.json"), JSON.stringify({ pid: 1, name: "alex-main", sessionId: "s1", ts: Date.now() }));
1233
- writeFileSync(join(root, "state", "session", "peers.json"), JSON.stringify([{ name: "alex-deck", purpose: "board deck" }]));
1234
- writeFileSync(join(root, "state", "session", "handoffs", "t1.json"), "{}");
1235
- writeFileSync(join(root, "state", "org", "board-mine.json"), JSON.stringify({ items: [{ itemId: "i1" }, { itemId: "i2" }], fetchedAt: new Date().toISOString() }));
1236
- const fetchImpl = fakeFetch();
1237
- const frame = await executeOrgTool("session_status", {}, { orgConfig: { org: { cohort: { enabled: true, base: "https://org.example" } } }, agentRoot: root, fetchImpl });
1238
- assert.equal(frame.ok, true, "no token is fine — this never touches the network");
1239
- assert.equal(fetchImpl.calls.length, 0);
1240
- assert.equal(frame.result.live, true);
1241
- assert.equal(frame.result.name, "alex-main");
1242
- assert.equal(frame.result.peers[0].name, "alex-deck");
1243
- assert.equal(frame.result.handoffsOpen, 1);
1244
- assert.equal(frame.result.boardMineCount, 2);
1245
- assert.equal(typeof frame.result.summary, "string");
1246
- // An empty root is "absent", not an error.
1247
- const empty = await executeOrgTool("session_status", {}, { orgConfig: CFG, agentRoot: join(root, "nope"), fetchImpl });
1248
- assert.equal(empty.ok, true);
1249
- assert.equal(empty.result.state, "absent");
1250
- } finally { rmSync(root, { recursive: true, force: true }); }
1251
- });
1252
-
1253
- test("board_track: the model cannot override service or smuggle extra keys — only the schema's passthrough fields reach hq", async () => {
1254
- const fetchImpl = fakeFetch({ ok: true, result: { tracked: true, taskId: "t2", col: "doing" } });
1255
- const frame = await executeOrgTool(
1256
- "board_track",
1257
- { channelId: "c1", messageId: "m1", stage: "working", service: "slack", junk: "x", note: "halfway", priority: "P1", notify: ["u1"] },
1258
- { orgConfig: CFG, agentRoot: "/tmp/none", fetchImpl },
1259
- );
1260
- assert.equal(frame.ok, true);
1261
- const body = JSON.parse(fetchImpl.calls[0].init.body);
1262
- assert.equal(body.service, "cohort", "service is pinned, not model-supplied");
1263
- assert.equal(body.junk, undefined, "unknown keys are dropped");
1264
- assert.equal(body.note, "halfway");
1265
- assert.equal(body.priority, "P1");
1266
- assert.deepEqual(body.notify, ["u1"]);
1267
- assert.deepEqual(Object.keys(body).sort(), ["channelId", "messageId", "note", "notify", "priority", "service", "stage"]);
1268
- });