@cohortapp/agent-sdk 2.17.0 → 2.18.5

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