@cohortapp/agent-sdk 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (731) hide show
  1. package/.claude/commands/init-agent.md +104 -0
  2. package/.claude/commands/init-maestro.md +1187 -0
  3. package/.claude/settings.json +161 -0
  4. package/.env.example +216 -0
  5. package/README.md +632 -0
  6. package/agents/browser-operator/agent.md +52 -0
  7. package/agents/calendar-ops/agent.md +50 -0
  8. package/agents/communications/agent.md +96 -0
  9. package/agents/decision-log/agent.md +65 -0
  10. package/agents/desktop-operator/agent.md +59 -0
  11. package/agents/gmail-operator/agent.md +62 -0
  12. package/agents/inbound-dispatcher/agent.md +66 -0
  13. package/agents/inbox-processor/agent.md +39 -0
  14. package/agents/pmo-execution/agent.md +60 -0
  15. package/agents/session-spawner/agent.md +64 -0
  16. package/agents/slack-operator/agent.md +60 -0
  17. package/agents/whatsapp-operator/agent.md +60 -0
  18. package/agents/workflow-automation/agent.md +61 -0
  19. package/archetypes/altitudes/c-suite.yaml +58 -0
  20. package/archetypes/altitudes/founder.yaml +68 -0
  21. package/archetypes/altitudes/senior-manager.yaml +63 -0
  22. package/archetypes/altitudes/svp.yaml +60 -0
  23. package/archetypes/altitudes/vp.yaml +50 -0
  24. package/archetypes/archetype.schema.json +77 -0
  25. package/archetypes/base.yaml +47 -0
  26. package/archetypes/capabilities/commercial-leader.yaml +159 -0
  27. package/archetypes/capabilities/compliance-officer.yaml +159 -0
  28. package/archetypes/capabilities/executive-operator.yaml +169 -0
  29. package/archetypes/capabilities/finance-leader.yaml +162 -0
  30. package/archetypes/capabilities/operations-leader.yaml +154 -0
  31. package/archetypes/capabilities/product-leader.yaml +148 -0
  32. package/archetypes/capabilities/technical-leader.yaml +146 -0
  33. package/archetypes/functions/commercial-leader.yaml +62 -0
  34. package/archetypes/functions/compliance-officer.yaml +64 -0
  35. package/archetypes/functions/executive-operator.yaml +70 -0
  36. package/archetypes/functions/finance-leader.yaml +70 -0
  37. package/archetypes/functions/operations-leader.yaml +62 -0
  38. package/archetypes/functions/product-leader.yaml +61 -0
  39. package/archetypes/functions/technical-leader.yaml +57 -0
  40. package/bin/cohort-mcp.mjs +81 -0
  41. package/bin/maestro.mjs +3516 -0
  42. package/bin/maestro.test.mjs +1015 -0
  43. package/desktop-control/README.md +56 -0
  44. package/desktop-control/app-profiles/gmail.yaml +120 -0
  45. package/desktop-control/app-profiles/slack.yaml +315 -0
  46. package/desktop-control/app-profiles/whatsapp.yaml +107 -0
  47. package/docs/architecture/agent-topology.md +2239 -0
  48. package/docs/architecture/archetype-agent-factory.md +110 -0
  49. package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
  50. package/docs/architecture/continuous-monitoring.md +221 -0
  51. package/docs/architecture/mcp-capability-map.md +585 -0
  52. package/docs/architecture/system-architecture.md +1272 -0
  53. package/docs/company-context/README.md +40 -0
  54. package/docs/guides/agent-persona-setup.md +600 -0
  55. package/docs/guides/agents-observe-setup.md +64 -0
  56. package/docs/guides/billing-console-keys.md +88 -0
  57. package/docs/guides/ccxray-diagnostics.md +65 -0
  58. package/docs/guides/channel-bus.md +127 -0
  59. package/docs/guides/claude-mem-setup.md +79 -0
  60. package/docs/guides/claude-pace-setup.md +56 -0
  61. package/docs/guides/claudraband-sessions.md +98 -0
  62. package/docs/guides/clawteam-swarm.md +116 -0
  63. package/docs/guides/code-review-graph-setup.md +86 -0
  64. package/docs/guides/email-setup.md +431 -0
  65. package/docs/guides/mac-mini.md +119 -0
  66. package/docs/guides/media-generation-setup.md +349 -0
  67. package/docs/guides/model-routing.md +162 -0
  68. package/docs/guides/observability-otel.md +265 -0
  69. package/docs/guides/org-onboarding.md +132 -0
  70. package/docs/guides/outbound-governance-setup.md +437 -0
  71. package/docs/guides/pdf-generation-setup.md +315 -0
  72. package/docs/guides/poller-daemon-setup.md +563 -0
  73. package/docs/guides/rag-context-setup.md +459 -0
  74. package/docs/guides/self-optimization-pattern.md +82 -0
  75. package/docs/guides/setup-wizard.md +178 -0
  76. package/docs/guides/slack-setup.md +350 -0
  77. package/docs/guides/telegram-setup.md +227 -0
  78. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  79. package/docs/guides/verification.md +128 -0
  80. package/docs/guides/voice-mode.md +188 -0
  81. package/docs/guides/voice-sms-setup.md +698 -0
  82. package/docs/guides/webhook-relay-setup.md +349 -0
  83. package/docs/guides/whatsapp-setup.md +288 -0
  84. package/docs/prompts/board-pack-cover-template.md +36 -0
  85. package/docs/prompts/decision-recommendation-template.md +88 -0
  86. package/docs/prompts/followup-message-template.md +141 -0
  87. package/docs/prompts/investor-letter-template.md +52 -0
  88. package/docs/prompts/morning-brief-template.md +82 -0
  89. package/docs/prompts/presentation-template.md +58 -0
  90. package/docs/prompts/weekly-strategic-memo-template.md +104 -0
  91. package/docs/research/hallucinated-tool-output-investigation.md +151 -0
  92. package/docs/runbooks/backup-restore.md +205 -0
  93. package/docs/runbooks/cohort-cutover.md +129 -0
  94. package/docs/runbooks/fleet-operations.md +200 -0
  95. package/docs/runbooks/incident-response.md +226 -0
  96. package/docs/runbooks/mac-mini-bootstrap.md +431 -0
  97. package/docs/runbooks/perpetual-operations.md +509 -0
  98. package/docs/runbooks/recovery-and-failover.md +260 -0
  99. package/framework-features.json +267 -0
  100. package/ingest/README.md +87 -0
  101. package/lib/action-executor.js +689 -0
  102. package/lib/action-executor.test.mjs +871 -0
  103. package/lib/agent-root.mjs +37 -0
  104. package/lib/archetype.mjs +236 -0
  105. package/lib/archetype.test.mjs +132 -0
  106. package/lib/autonomy.mjs +114 -0
  107. package/lib/autonomy.test.mjs +66 -0
  108. package/lib/backlog.mjs +358 -0
  109. package/lib/backlog.test.mjs +266 -0
  110. package/lib/budget-guard.mjs +279 -0
  111. package/lib/budget-guard.test.mjs +291 -0
  112. package/lib/cadence-bus-schedule.test.mjs +194 -0
  113. package/lib/cadence-bus.mjs +1120 -0
  114. package/lib/cadence-bus.test.mjs +720 -0
  115. package/lib/cadences.mjs +205 -0
  116. package/lib/cadences.test.mjs +125 -0
  117. package/lib/capability.mjs +154 -0
  118. package/lib/capability.test.mjs +78 -0
  119. package/lib/channels/base-adapter.mjs +719 -0
  120. package/lib/channels/base-adapter.test.mjs +590 -0
  121. package/lib/channels/channel.mjs +128 -0
  122. package/lib/channels/channels.test.mjs +371 -0
  123. package/lib/channels/contract.mjs +215 -0
  124. package/lib/channels/contract.test.mjs +137 -0
  125. package/lib/channels/conversation-resolver.mjs +95 -0
  126. package/lib/channels/gmail/adapter.mjs +87 -0
  127. package/lib/channels/inbox-item.mjs +255 -0
  128. package/lib/channels/inbox-item.test.mjs +335 -0
  129. package/lib/channels/index.mjs +94 -0
  130. package/lib/channels/orgmail/adapter.mjs +353 -0
  131. package/lib/channels/orgmail/adapter.test.mjs +311 -0
  132. package/lib/channels/pairing.mjs +363 -0
  133. package/lib/channels/pairing.test.mjs +270 -0
  134. package/lib/channels/registry.mjs +164 -0
  135. package/lib/channels/slack/adapter.mjs +317 -0
  136. package/lib/channels/slack-adapter.test.mjs +212 -0
  137. package/lib/channels/sms/adapter.mjs +43 -0
  138. package/lib/channels/telegram/adapter.mjs +432 -0
  139. package/lib/channels/telegram-adapter.test.mjs +306 -0
  140. package/lib/channels/voice/adapter.mjs +301 -0
  141. package/lib/channels/voice/adapter.test.mjs +278 -0
  142. package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
  143. package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
  144. package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
  145. package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
  146. package/lib/charter.mjs +256 -0
  147. package/lib/charter.test.mjs +89 -0
  148. package/lib/claude-bin.mjs +134 -0
  149. package/lib/claude-bin.test.mjs +75 -0
  150. package/lib/collective/capture.mjs +185 -0
  151. package/lib/collective/capture.test.mjs +121 -0
  152. package/lib/collective/cards.mjs +201 -0
  153. package/lib/collective/cards.test.mjs +114 -0
  154. package/lib/collective/config.mjs +186 -0
  155. package/lib/collective/config.test.mjs +123 -0
  156. package/lib/collective/global-config.mjs +113 -0
  157. package/lib/collective/global-config.test.mjs +75 -0
  158. package/lib/collective/presence.mjs +201 -0
  159. package/lib/collective/presence.test.mjs +95 -0
  160. package/lib/collective/recall.mjs +215 -0
  161. package/lib/collective/recall.test.mjs +116 -0
  162. package/lib/comms/send-gate.mjs +554 -0
  163. package/lib/comms/send-gate.test.mjs +577 -0
  164. package/lib/comms.mjs +67 -0
  165. package/lib/comms.test.mjs +41 -0
  166. package/lib/diagnostics/alerts.mjs +424 -0
  167. package/lib/diagnostics/alerts.test.mjs +318 -0
  168. package/lib/diagnostics/backup-freshness.mjs +188 -0
  169. package/lib/diagnostics/backup-freshness.test.mjs +185 -0
  170. package/lib/diagnostics/counters.mjs +269 -0
  171. package/lib/diagnostics/counters.test.mjs +206 -0
  172. package/lib/diagnostics/events.mjs +188 -0
  173. package/lib/diagnostics/events.test.mjs +290 -0
  174. package/lib/diagnostics/otel.mjs +237 -0
  175. package/lib/diagnostics/otel.test.mjs +196 -0
  176. package/lib/diagnostics/trace.mjs +216 -0
  177. package/lib/diagnostics/trace.test.mjs +251 -0
  178. package/lib/env-compat.mjs +74 -0
  179. package/lib/env-compat.test.mjs +104 -0
  180. package/lib/feature-init.mjs +331 -0
  181. package/lib/fs-atomic.mjs +112 -0
  182. package/lib/fs-atomic.test.mjs +72 -0
  183. package/lib/fs-ownership.mjs +111 -0
  184. package/lib/fs-ownership.test.mjs +158 -0
  185. package/lib/hooks/bus.mjs +347 -0
  186. package/lib/hooks/bus.test.mjs +387 -0
  187. package/lib/index.js +16 -0
  188. package/lib/learning/config.mjs +106 -0
  189. package/lib/learning/config.test.mjs +75 -0
  190. package/lib/learning/counters.mjs +156 -0
  191. package/lib/learning/counters.test.mjs +69 -0
  192. package/lib/learning/curator-consolidate.test.mjs +238 -0
  193. package/lib/learning/curator.mjs +453 -0
  194. package/lib/learning/curator.test.mjs +106 -0
  195. package/lib/learning/log.mjs +40 -0
  196. package/lib/learning/reflect.mjs +534 -0
  197. package/lib/learning/reflect.test.mjs +0 -0
  198. package/lib/learning/session-index.mjs +352 -0
  199. package/lib/learning/session-index.test.mjs +125 -0
  200. package/lib/learning/skill-writer.mjs +474 -0
  201. package/lib/learning/skill-writer.test.mjs +210 -0
  202. package/lib/mcp/server.mjs +328 -0
  203. package/lib/mcp/server.test.mjs +400 -0
  204. package/lib/model-router/auth-profiles.mjs +758 -0
  205. package/lib/model-router/auth-profiles.test.mjs +580 -0
  206. package/lib/model-router/catalog/anthropic.yaml +153 -0
  207. package/lib/model-router/catalog/deepseek.yaml +86 -0
  208. package/lib/model-router/catalog/moonshot.yaml +81 -0
  209. package/lib/model-router/catalog/qwen.yaml +114 -0
  210. package/lib/model-router/catalog.mjs +925 -0
  211. package/lib/model-router/catalog.test.mjs +385 -0
  212. package/lib/model-router/economics.mjs +564 -0
  213. package/lib/model-router/economics.test.mjs +344 -0
  214. package/lib/model-router/failover.mjs +298 -0
  215. package/lib/model-router/failover.test.mjs +439 -0
  216. package/lib/model-router/health.mjs +453 -0
  217. package/lib/model-router/health.test.mjs +338 -0
  218. package/lib/model-router/integration-coverage.test.mjs +829 -0
  219. package/lib/model-router/integration.test.mjs +564 -0
  220. package/lib/model-router/ledger.mjs +402 -0
  221. package/lib/model-router/ledger.test.mjs +382 -0
  222. package/lib/model-router/llm-task.mjs +515 -0
  223. package/lib/model-router/llm-task.test.mjs +392 -0
  224. package/lib/model-router/org-credentials.mjs +260 -0
  225. package/lib/model-router/org-credentials.test.mjs +265 -0
  226. package/lib/model-router/pricing-refresh.mjs +463 -0
  227. package/lib/model-router/pricing-refresh.test.mjs +286 -0
  228. package/lib/model-router/reconcile.mjs +429 -0
  229. package/lib/model-router/reconcile.test.mjs +316 -0
  230. package/lib/model-router/repair.mjs +471 -0
  231. package/lib/model-router/repair.test.mjs +180 -0
  232. package/lib/model-router/resolve.mjs +1206 -0
  233. package/lib/model-router/spawn.mjs +497 -0
  234. package/lib/model-router/spawn.test.mjs +425 -0
  235. package/lib/model-router/taxonomy.mjs +893 -0
  236. package/lib/model-router/taxonomy.test.mjs +410 -0
  237. package/lib/model-router.mjs +677 -0
  238. package/lib/model-router.test.mjs +907 -0
  239. package/lib/org/activity.mjs +211 -0
  240. package/lib/org/activity.test.mjs +134 -0
  241. package/lib/org/approvals.mjs +448 -0
  242. package/lib/org/approvals.test.mjs +216 -0
  243. package/lib/org/awareness.mjs +222 -0
  244. package/lib/org/awareness.test.mjs +159 -0
  245. package/lib/org/board.mjs +229 -0
  246. package/lib/org/board.test.mjs +177 -0
  247. package/lib/org/bootstrap-context.mjs +169 -0
  248. package/lib/org/bootstrap-context.test.mjs +153 -0
  249. package/lib/org/client.mjs +1628 -0
  250. package/lib/org/client.test.mjs +1107 -0
  251. package/lib/org/cohort-client.mjs +67 -0
  252. package/lib/org/cohort-client.test.mjs +126 -0
  253. package/lib/org/cost-sync.mjs +227 -0
  254. package/lib/org/cost-sync.test.mjs +153 -0
  255. package/lib/org/doctor.mjs +212 -0
  256. package/lib/org/doctor.test.mjs +212 -0
  257. package/lib/org/handoff.mjs +293 -0
  258. package/lib/org/handoff.test.mjs +269 -0
  259. package/lib/org/integration-tools.mjs +182 -0
  260. package/lib/org/integration-tools.test.mjs +160 -0
  261. package/lib/org/keys.mjs +131 -0
  262. package/lib/org/keys.test.mjs +92 -0
  263. package/lib/org/knowledge.mjs +463 -0
  264. package/lib/org/knowledge.test.mjs +319 -0
  265. package/lib/org/leases.mjs +335 -0
  266. package/lib/org/leases.test.mjs +235 -0
  267. package/lib/org/mesh-integration.test.mjs +127 -0
  268. package/lib/org/mesh.mjs +459 -0
  269. package/lib/org/mesh.test.mjs +345 -0
  270. package/lib/org/messaging.mjs +503 -0
  271. package/lib/org/messaging.test.mjs +238 -0
  272. package/lib/org/policy.mjs +345 -0
  273. package/lib/org/policy.test.mjs +237 -0
  274. package/lib/org/protocol.checksum +1 -0
  275. package/lib/org/protocol.checksum.test.mjs +90 -0
  276. package/lib/org/protocol.mjs +967 -0
  277. package/lib/org/protocol.test.mjs +264 -0
  278. package/lib/org/registry.mjs +194 -0
  279. package/lib/org/registry.test.mjs +100 -0
  280. package/lib/org/tool-surface-integration.test.mjs +120 -0
  281. package/lib/org/tool-surface.mjs +2535 -0
  282. package/lib/org/tool-surface.test.mjs +589 -0
  283. package/lib/org/ui-parity.mjs +3236 -0
  284. package/lib/org/ui-parity.test.mjs +348 -0
  285. package/lib/org/verify.mjs +176 -0
  286. package/lib/org/verify.test.mjs +194 -0
  287. package/lib/rag/embed.mjs +188 -0
  288. package/lib/rag/indexer.mjs +425 -0
  289. package/lib/rag/rag.test.mjs +505 -0
  290. package/lib/rag/search.mjs +475 -0
  291. package/lib/rate-guard.mjs +246 -0
  292. package/lib/rate-guard.test.mjs +201 -0
  293. package/lib/render.mjs +112 -0
  294. package/lib/render.test.mjs +68 -0
  295. package/lib/resource-governor.mjs +297 -0
  296. package/lib/resource-governor.test.mjs +262 -0
  297. package/lib/scheduling/dynamic-jobs.mjs +675 -0
  298. package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
  299. package/lib/scheduling/jitter.mjs +0 -0
  300. package/lib/scheduling/jitter.test.mjs +140 -0
  301. package/lib/secrets/broker.mjs +315 -0
  302. package/lib/secrets/broker.test.mjs +280 -0
  303. package/lib/secrets/providers.mjs +461 -0
  304. package/lib/secrets/providers.test.mjs +274 -0
  305. package/lib/security/audit-engine.mjs +684 -0
  306. package/lib/security/audit-engine.test.mjs +389 -0
  307. package/lib/security/coerce-args.mjs +552 -0
  308. package/lib/security/coerce-args.test.mjs +281 -0
  309. package/lib/security/dangerous-tools.mjs +97 -0
  310. package/lib/security/dangerous-tools.test.mjs +68 -0
  311. package/lib/security/external-content.mjs +145 -0
  312. package/lib/security/external-content.test.mjs +67 -0
  313. package/lib/security/redact.mjs +592 -0
  314. package/lib/security/redact.test.mjs +441 -0
  315. package/lib/security/secret-equal.mjs +73 -0
  316. package/lib/security/secret-equal.test.mjs +55 -0
  317. package/lib/session-permissions.mjs +101 -0
  318. package/lib/session-permissions.test.mjs +100 -0
  319. package/lib/setup/claude-probe.mjs +74 -0
  320. package/lib/setup/completeness.mjs +175 -0
  321. package/lib/setup/completeness.test.mjs +110 -0
  322. package/lib/setup/context-pack.mjs +173 -0
  323. package/lib/setup/context-pack.test.mjs +89 -0
  324. package/lib/setup/enrich.mjs +277 -0
  325. package/lib/setup/enrich.test.mjs +115 -0
  326. package/lib/setup/enroll-from-cohort.mjs +441 -0
  327. package/lib/setup/enroll-from-cohort.test.mjs +233 -0
  328. package/lib/setup/integration.test.mjs +162 -0
  329. package/lib/setup/io.mjs +360 -0
  330. package/lib/setup/io.test.mjs +77 -0
  331. package/lib/setup/run-generator.mjs +81 -0
  332. package/lib/setup/runner.mjs +244 -0
  333. package/lib/setup/runner.test.mjs +132 -0
  334. package/lib/setup/sections/comms.mjs +173 -0
  335. package/lib/setup/sections/company.mjs +120 -0
  336. package/lib/setup/sections/enrich.mjs +138 -0
  337. package/lib/setup/sections/identity.mjs +182 -0
  338. package/lib/setup/sections/identity.test.mjs +140 -0
  339. package/lib/setup/sections/learning.mjs +153 -0
  340. package/lib/setup/sections/learning.test.mjs +81 -0
  341. package/lib/setup/sections/messaging.mjs +219 -0
  342. package/lib/setup/sections/messaging.test.mjs +127 -0
  343. package/lib/setup/sections/model.mjs +102 -0
  344. package/lib/setup/sections/operating-model.mjs +78 -0
  345. package/lib/setup/sections/org.mjs +475 -0
  346. package/lib/setup/sections/org.test.mjs +313 -0
  347. package/lib/setup/sections/orgmail.mjs +173 -0
  348. package/lib/setup/sections/orgmail.test.mjs +118 -0
  349. package/lib/setup/sections/recovery.mjs +159 -0
  350. package/lib/setup/sections/recovery.test.mjs +98 -0
  351. package/lib/setup/sections/tools.mjs +132 -0
  352. package/lib/setup/sections/verify.mjs +97 -0
  353. package/lib/setup/sot.mjs +205 -0
  354. package/lib/setup/sot.test.mjs +81 -0
  355. package/lib/setup/state.mjs +151 -0
  356. package/lib/setup/state.test.mjs +92 -0
  357. package/lib/singleton.js +229 -0
  358. package/lib/singleton.test.mjs +135 -0
  359. package/lib/telemetry/alerts.mjs +216 -0
  360. package/lib/telemetry/alerts.test.mjs +109 -0
  361. package/lib/telemetry/collect.mjs +512 -0
  362. package/lib/telemetry/collect.test.mjs +202 -0
  363. package/lib/tool-definitions-integration.test.mjs +83 -0
  364. package/lib/tool-definitions.js +738 -0
  365. package/lib/tool-definitions.test.mjs +437 -0
  366. package/lib/util/fetch-timeout.mjs +136 -0
  367. package/lib/util/fetch-timeout.test.mjs +202 -0
  368. package/lib/util/reconnect.mjs +343 -0
  369. package/lib/util/reconnect.test.mjs +369 -0
  370. package/lib/util/unhandled.mjs +205 -0
  371. package/lib/util/unhandled.test.mjs +216 -0
  372. package/lib/voice/context-loader.mjs +466 -0
  373. package/lib/voice/index.mjs +100 -0
  374. package/lib/voice/openai-realtime.mjs +510 -0
  375. package/lib/voice/outbound.mjs +542 -0
  376. package/lib/voice/outbound.test.mjs +69 -0
  377. package/lib/voice/post-call-brief.mjs +428 -0
  378. package/lib/voice/provider.mjs +52 -0
  379. package/lib/voice/session-rotation.mjs +257 -0
  380. package/lib/voice/stt.mjs +161 -0
  381. package/lib/voice/stt.test.mjs +226 -0
  382. package/lib/voice/tool-bridge.mjs +370 -0
  383. package/lib/voice/tts.mjs +104 -0
  384. package/lib/voice/twilio-sip-bridge.mjs +288 -0
  385. package/lib/voice/voice.test.mjs +990 -0
  386. package/mcp/README.md +80 -0
  387. package/package.json +151 -0
  388. package/plugins/maestro-skills/plugin.json +139 -0
  389. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  390. package/plugins/maestro-skills/skills/board-deck.md +68 -0
  391. package/plugins/maestro-skills/skills/books-close.md +77 -0
  392. package/plugins/maestro-skills/skills/brand-steward.md +121 -0
  393. package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
  394. package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
  395. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  396. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  397. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  398. package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
  399. package/plugins/maestro-skills/skills/decision-brief.md +89 -0
  400. package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
  401. package/plugins/maestro-skills/skills/draft-comms.md +84 -0
  402. package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
  403. package/plugins/maestro-skills/skills/files-find.md +65 -0
  404. package/plugins/maestro-skills/skills/generative-ui.md +228 -0
  405. package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
  406. package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
  407. package/plugins/maestro-skills/skills/mail-triage.md +86 -0
  408. package/plugins/maestro-skills/skills/morning-brief.md +54 -0
  409. package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
  410. package/plugins/maestro-skills/skills/org-board.md +133 -0
  411. package/plugins/maestro-skills/skills/org-credential.md +68 -0
  412. package/plugins/maestro-skills/skills/org-recall.md +81 -0
  413. package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
  414. package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
  415. package/plugins/maestro-skills/skills/router-why.md +78 -0
  416. package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
  417. package/plugins/maestro-skills/skills/session-search.md +71 -0
  418. package/plugins/maestro-skills/skills/set-reminder.md +93 -0
  419. package/plugins/maestro-skills/skills/slack-followup.md +64 -0
  420. package/plugins/maestro-skills/skills/team-activity.md +86 -0
  421. package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
  422. package/policies/action-classification.yaml +114 -0
  423. package/policies/ai-disclosure.yaml +294 -0
  424. package/policies/communication-style.md +139 -0
  425. package/policies/information-barriers.yaml +118 -0
  426. package/policies/prompt-injection-defence.yaml +138 -0
  427. package/public/assets/icon-dark.png +0 -0
  428. package/public/assets/icon-dark.svg +9 -0
  429. package/public/assets/icon-light.svg +9 -0
  430. package/public/assets/logo-dark.svg +15 -0
  431. package/public/assets/logo-light.svg +15 -0
  432. package/scaffold/.mcp.json +7 -0
  433. package/scaffold/CLAUDE.md +368 -0
  434. package/scaffold/config/agent.json +55 -0
  435. package/scaffold/config/agent.ts +76 -0
  436. package/scaffold/config/agent.ts.example +89 -0
  437. package/scaffold/config/alerts.yaml +23 -0
  438. package/scaffold/config/allowlist.yaml.example +25 -0
  439. package/scaffold/config/caller-id-map.yaml +46 -0
  440. package/scaffold/config/collective.yaml +49 -0
  441. package/scaffold/config/company.json +20 -0
  442. package/scaffold/config/known-agents.json +6 -0
  443. package/scaffold/config/learning.yaml +55 -0
  444. package/scaffold/config/model-routing.yaml.example +104 -0
  445. package/scaffold/config/org.yaml +25 -0
  446. package/scaffold/config/orgmail.yaml.example +19 -0
  447. package/scaffold/config/recovery.yaml +72 -0
  448. package/scaffold/config/secrets.yaml +27 -0
  449. package/scaffold/config/slack.yaml.example +35 -0
  450. package/scaffold/config/telegram.yaml.example +38 -0
  451. package/scaffold/config/voice.yaml.example +89 -0
  452. package/scaffold/config/whatsapp.yaml.example +39 -0
  453. package/schedules/README.md +49 -0
  454. package/schedules/triggers/backlog-executor.md +102 -0
  455. package/schedules/triggers/brand-steward.md +72 -0
  456. package/schedules/triggers/daily-evening-wrap.md +159 -0
  457. package/schedules/triggers/daily-midday-sweep.md +58 -0
  458. package/schedules/triggers/daily-morning-brief.md +55 -0
  459. package/schedules/triggers/directory-hygiene.md +81 -0
  460. package/schedules/triggers/dynamic-jobs.md +40 -0
  461. package/schedules/triggers/inbox-processor.md +115 -0
  462. package/schedules/triggers/meeting-action-capture.md +60 -0
  463. package/schedules/triggers/meeting-prep.md +69 -0
  464. package/schedules/triggers/messaging-inbound.md +50 -0
  465. package/schedules/triggers/org-pulse.md +24 -0
  466. package/schedules/triggers/quarterly-self-assessment.md +54 -0
  467. package/schedules/triggers/weekly-engineering-health.md +37 -0
  468. package/schedules/triggers/weekly-execution.md +65 -0
  469. package/schedules/triggers/weekly-hiring.md +53 -0
  470. package/schedules/triggers/weekly-priorities.md +38 -0
  471. package/schedules/triggers/weekly-strategic-memo.md +124 -0
  472. package/scripts/archive-email.sh +55 -0
  473. package/scripts/cadence/cadence-status.mjs +36 -0
  474. package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
  475. package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
  476. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  477. package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
  478. package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
  479. package/scripts/ci/check-docs-accuracy.mjs +493 -0
  480. package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
  481. package/scripts/ci/check-exports-exist.mjs +140 -0
  482. package/scripts/ci/check-files-exist.mjs +107 -0
  483. package/scripts/ci/check-no-build-artifacts.mjs +111 -0
  484. package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
  485. package/scripts/ci/check-no-confidential.mjs +198 -0
  486. package/scripts/ci/check-no-conflict-markers.mjs +169 -0
  487. package/scripts/ci/check-no-residual-identity.mjs +163 -0
  488. package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
  489. package/scripts/ci/check-tarball-fidelity.mjs +205 -0
  490. package/scripts/ci/check-unresolved-tokens.mjs +83 -0
  491. package/scripts/ci/check.mjs +109 -0
  492. package/scripts/ci/check.test.mjs +194 -0
  493. package/scripts/ci/run-coverage.mjs +82 -0
  494. package/scripts/ci/run-tests.mjs +71 -0
  495. package/scripts/cloud-relay/README.md +59 -0
  496. package/scripts/cloud-relay/index.mjs +233 -0
  497. package/scripts/cloud-relay/package.json +15 -0
  498. package/scripts/cloud-relay/railway.json +13 -0
  499. package/scripts/cloud-relay/voice/README.md +94 -0
  500. package/scripts/cloud-relay/voice/package-lock.json +39 -0
  501. package/scripts/cloud-relay/voice/package.json +16 -0
  502. package/scripts/cloud-relay/voice/railway.json +13 -0
  503. package/scripts/cloud-relay/voice/server.mjs +532 -0
  504. package/scripts/collective/hook-runner.mjs +211 -0
  505. package/scripts/collective/hook-runner.test.mjs +90 -0
  506. package/scripts/collective/org-pulse.mjs +72 -0
  507. package/scripts/collective/org-sync.mjs +61 -0
  508. package/scripts/collective/recall.mjs +45 -0
  509. package/scripts/collective/who.mjs +30 -0
  510. package/scripts/comms-monitor.sh +288 -0
  511. package/scripts/configure-whatsapp-sandbox.sh +201 -0
  512. package/scripts/continuous-monitor.sh +91 -0
  513. package/scripts/cost/fleet-digest.mjs +407 -0
  514. package/scripts/cost/fleet-digest.test.mjs +207 -0
  515. package/scripts/cost/track-claude-usage.mjs +169 -0
  516. package/scripts/daemon/agent-daemon.mjs +989 -0
  517. package/scripts/daemon/agent-daemon.test.mjs +525 -0
  518. package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
  519. package/scripts/daemon/cadence-consumer.mjs +1080 -0
  520. package/scripts/daemon/cadence-consumer.test.mjs +770 -0
  521. package/scripts/daemon/cadence-handlers.mjs +1121 -0
  522. package/scripts/daemon/cadence-handlers.test.mjs +617 -0
  523. package/scripts/daemon/classifier.mjs +704 -0
  524. package/scripts/daemon/classifier.test.mjs +238 -0
  525. package/scripts/daemon/classify-kind.mjs +54 -0
  526. package/scripts/daemon/classify-kind.test.mjs +40 -0
  527. package/scripts/daemon/context-compiler.mjs +605 -0
  528. package/scripts/daemon/context-compiler.test.mjs +300 -0
  529. package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
  530. package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
  531. package/scripts/daemon/dispatcher.mjs +1516 -0
  532. package/scripts/daemon/health.mjs +72 -0
  533. package/scripts/daemon/inbox-deferral.mjs +210 -0
  534. package/scripts/daemon/inbox-deferral.test.mjs +242 -0
  535. package/scripts/daemon/integration.test.mjs +149 -0
  536. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  537. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  538. package/scripts/daemon/launchd-wrapper.sh +91 -0
  539. package/scripts/daemon/lib/session-router.mjs +274 -0
  540. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  541. package/scripts/daemon/maestro-daemon.mjs +275 -0
  542. package/scripts/daemon/prompt-builder.mjs +685 -0
  543. package/scripts/daemon/prompt-builder.test.mjs +213 -0
  544. package/scripts/daemon/responder.mjs +854 -0
  545. package/scripts/daemon/session-lock.mjs +721 -0
  546. package/scripts/daemon/session-lock.test.mjs +252 -0
  547. package/scripts/daemon/session-outcomes.mjs +640 -0
  548. package/scripts/daemon/session-outcomes.test.mjs +533 -0
  549. package/scripts/daemon/typing-registry.mjs +90 -0
  550. package/scripts/daemon/typing-registry.test.mjs +77 -0
  551. package/scripts/daemon/voice-webhook-server.mjs +804 -0
  552. package/scripts/decisions/capture-decision.mjs +116 -0
  553. package/scripts/disclosure_assessment.py +873 -0
  554. package/scripts/disclosure_boundaries.py +562 -0
  555. package/scripts/email-signature-principal.html +52 -0
  556. package/scripts/email-signature.html +60 -0
  557. package/scripts/email_quote_thread.py +167 -0
  558. package/scripts/email_thread_dedup.py +362 -0
  559. package/scripts/emergency-stop.sh +81 -0
  560. package/scripts/healthcheck.sh +116 -0
  561. package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
  562. package/scripts/hooks/block-mcp-slack-send.sh +7 -0
  563. package/scripts/hooks/post-action-log.sh +126 -0
  564. package/scripts/hooks/pre-send-audit.sh +174 -0
  565. package/scripts/hooks/pre-send-audit.test.mjs +215 -0
  566. package/scripts/hooks/session-end-log.sh +27 -0
  567. package/scripts/hooks/session-start-banner.sh +115 -0
  568. package/scripts/huddle/audio-bridge.mjs +664 -0
  569. package/scripts/huddle/boot-slack-cdp.sh +102 -0
  570. package/scripts/huddle/huddle-controller.mjs +942 -0
  571. package/scripts/huddle/huddle-server.mjs +1229 -0
  572. package/scripts/huddle/launch-slack.sh +232 -0
  573. package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
  574. package/scripts/huddle/package-lock.json +62 -0
  575. package/scripts/huddle/package.json +22 -0
  576. package/scripts/huddle/setup-audio.sh +239 -0
  577. package/scripts/huddle/start-call.mjs +318 -0
  578. package/scripts/huddle/test-pipeline.mjs +263 -0
  579. package/scripts/learning/consolidate-skills.mjs +72 -0
  580. package/scripts/learning/session-search.mjs +125 -0
  581. package/scripts/llm_email_dedup.py +442 -0
  582. package/scripts/local-triggers/generate-plists.sh +432 -0
  583. package/scripts/local-triggers/generate-plists.test.mjs +413 -0
  584. package/scripts/local-triggers/install-all.sh +49 -0
  585. package/scripts/local-triggers/plists/.gitkeep +0 -0
  586. package/scripts/local-triggers/run-trigger.sh +63 -0
  587. package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
  588. package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
  589. package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
  590. package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
  591. package/scripts/maintenance/backup-to-cloud.sh +124 -0
  592. package/scripts/maintenance/health-check.sh +377 -0
  593. package/scripts/media-generation/README.md +105 -0
  594. package/scripts/media-generation/gemini-image-client.mjs +173 -0
  595. package/scripts/media-generation/generate-assets.mjs +289 -0
  596. package/scripts/media-generation/veo-video-client.mjs +219 -0
  597. package/scripts/org/send-orgmail.mjs +227 -0
  598. package/scripts/outbound-dedup-cleanup.sh +43 -0
  599. package/scripts/outbound-dedup.sh +477 -0
  600. package/scripts/outbound_dedup.py +115 -0
  601. package/scripts/parse-voice-transcript.mjs +481 -0
  602. package/scripts/pdf-generation/README.md +63 -0
  603. package/scripts/pdf-generation/build-document.mjs +247 -0
  604. package/scripts/pdf-generation/templates/board-pack.latex +136 -0
  605. package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
  606. package/scripts/pdf-generation/templates/memo.latex +114 -0
  607. package/scripts/poll-slack-events.sh +35 -0
  608. package/scripts/poller/calendar-poller.mjs +12 -0
  609. package/scripts/poller/gmail-poller.mjs +192 -0
  610. package/scripts/poller/imap-client.mjs +289 -0
  611. package/scripts/poller/inbox-scan-poller.mjs +156 -0
  612. package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
  613. package/scripts/poller/index.mjs +73 -0
  614. package/scripts/poller/intra-session-check.mjs +285 -0
  615. package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
  616. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
  617. package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
  618. package/scripts/poller/secondary-gmail-poller.mjs +132 -0
  619. package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
  620. package/scripts/poller/slack-poller.mjs +854 -0
  621. package/scripts/poller/slack-socket-mode.mjs +917 -0
  622. package/scripts/poller/slack-socket-mode.test.mjs +753 -0
  623. package/scripts/poller/trigger.mjs +75 -0
  624. package/scripts/poller/utils.mjs +371 -0
  625. package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
  626. package/scripts/poller/voice-poller.mjs +236 -0
  627. package/scripts/poller-launchd/install.sh +66 -0
  628. package/scripts/poller-launchd/poller.plist.template +40 -0
  629. package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
  630. package/scripts/post-interaction-indexer.py +1598 -0
  631. package/scripts/pre-draft-context.py +994 -0
  632. package/scripts/pre_draft_lookup.py +258 -0
  633. package/scripts/rag/build-index.mjs +47 -0
  634. package/scripts/rag/ingest.mjs +111 -0
  635. package/scripts/rag/search.mjs +119 -0
  636. package/scripts/rag-indexer.py +629 -0
  637. package/scripts/restore-from-backup.sh +248 -0
  638. package/scripts/restore-from-backup.test.mjs +178 -0
  639. package/scripts/resume-operations.sh +80 -0
  640. package/scripts/search-secondary-inbox.py +181 -0
  641. package/scripts/secondary-inbox-poller.py +437 -0
  642. package/scripts/self-optimization/compute-metrics.py +398 -0
  643. package/scripts/send-email-as-principal.py +369 -0
  644. package/scripts/send-email-threaded.py +392 -0
  645. package/scripts/send-email-with-attachment.py +377 -0
  646. package/scripts/send-email.sh +131 -0
  647. package/scripts/send-sms.sh +175 -0
  648. package/scripts/send-whatsapp.sh +292 -0
  649. package/scripts/session-start.sh +106 -0
  650. package/scripts/setup/boot-claude-session.sh +94 -0
  651. package/scripts/setup/configure-macos.sh +674 -0
  652. package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
  653. package/scripts/setup/configure-voice-tunnel.mjs +182 -0
  654. package/scripts/setup/generate-agent-env.mjs +92 -0
  655. package/scripts/setup/generate-agent-package-json.mjs +222 -0
  656. package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
  657. package/scripts/setup/generate-autonomy.mjs +60 -0
  658. package/scripts/setup/generate-backlog.mjs +101 -0
  659. package/scripts/setup/generate-cadences.mjs +92 -0
  660. package/scripts/setup/generate-capability.mjs +162 -0
  661. package/scripts/setup/generate-charter.mjs +76 -0
  662. package/scripts/setup/generate-comms.mjs +59 -0
  663. package/scripts/setup/generate-company.mjs +92 -0
  664. package/scripts/setup/init-agent.sh +547 -0
  665. package/scripts/setup/init-agent.test.mjs +151 -0
  666. package/scripts/setup/init-archetype.mjs +74 -0
  667. package/scripts/setup/init-backup.mjs +54 -0
  668. package/scripts/setup/init-cadence-bus.mjs +60 -0
  669. package/scripts/setup/init-channel-bus.mjs +46 -0
  670. package/scripts/setup/init-cost-tracking.mjs +45 -0
  671. package/scripts/setup/init-decision-capture.mjs +66 -0
  672. package/scripts/setup/init-known-agents.mjs +57 -0
  673. package/scripts/setup/init-learning.mjs +70 -0
  674. package/scripts/setup/init-memory-executive.mjs +45 -0
  675. package/scripts/setup/init-model-router.mjs +124 -0
  676. package/scripts/setup/init-rag.mjs +174 -0
  677. package/scripts/setup/init-session-router.mjs +38 -0
  678. package/scripts/setup/init-slack-socket-mode.mjs +260 -0
  679. package/scripts/setup/init-telegram.mjs +165 -0
  680. package/scripts/setup/init-voice-realtime.mjs +204 -0
  681. package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
  682. package/scripts/setup/install-dev-tools.sh +150 -0
  683. package/scripts/setup/lib/install-plist.mjs +95 -0
  684. package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
  685. package/scripts/setup/render-environment-yaml.mjs +133 -0
  686. package/scripts/slack-events-ctl.sh +177 -0
  687. package/scripts/slack-events-server.mjs +1045 -0
  688. package/scripts/slack-react.mjs +89 -0
  689. package/scripts/slack-responded.sh +232 -0
  690. package/scripts/slack-send.sh +287 -0
  691. package/scripts/slack-typing.mjs +196 -0
  692. package/scripts/slack-upload-v2.py +95 -0
  693. package/scripts/sms-handler.mjs +450 -0
  694. package/scripts/spawn-session.sh +120 -0
  695. package/scripts/sync-protocol.mjs +217 -0
  696. package/scripts/system-verify.sh +184 -0
  697. package/scripts/test-email-thread-dedup.py +239 -0
  698. package/scripts/test-information-barriers.py +484 -0
  699. package/scripts/test-llm-email-dedup.py +251 -0
  700. package/scripts/test-pre-draft-integration.py +203 -0
  701. package/scripts/test-rag-phase2.sh +442 -0
  702. package/scripts/test-rag-search.sh +251 -0
  703. package/scripts/test-voice-parser.mjs +316 -0
  704. package/scripts/user-context-search.py +659 -0
  705. package/scripts/validate_outbound.py +1504 -0
  706. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
  707. package/scripts/watchdog/force-reboot.sh +157 -0
  708. package/scripts/watchdog/memory-watchdog.sh +473 -0
  709. package/scripts/whatsapp-handler.mjs +538 -0
  710. package/teams/desktop-operations.yaml +34 -0
  711. package/teams/executive-office.yaml +27 -0
  712. package/teams/legal-and-regulatory.yaml +24 -0
  713. package/teams/platform-and-engineering.yaml +23 -0
  714. package/teams/strategy-and-growth.yaml +29 -0
  715. package/workflows/continuous/backlog-executor.yaml +141 -0
  716. package/workflows/continuous/inbound-monitor.yaml +168 -0
  717. package/workflows/daily/applicant-triage.yaml +197 -0
  718. package/workflows/daily/comms-triage.yaml +80 -0
  719. package/workflows/daily/evening-wrap.yaml +105 -0
  720. package/workflows/daily/morning-brief.yaml +164 -0
  721. package/workflows/daily/slack-followup-sweep.yaml +87 -0
  722. package/workflows/event-driven/README.md +50 -0
  723. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  724. package/workflows/event-driven/pr-review.yaml +107 -0
  725. package/workflows/monthly/board-readiness.yaml +76 -0
  726. package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
  727. package/workflows/session-protocol.md +171 -0
  728. package/workflows/weekly/engineering-health.yaml +154 -0
  729. package/workflows/weekly/hiring-review.yaml +169 -0
  730. package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
  731. package/workflows/weekly/strategic-memo.yaml +79 -0
@@ -0,0 +1,907 @@
1
+ /**
2
+ * model-router.test.mjs — node:test coverage for the model router.
3
+ *
4
+ * Tests write configs to a temp dir + load them via loadRoutingConfig so
5
+ * we exercise the full file-system path. No real network, no real Claude.
6
+ */
7
+
8
+ import { test } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { promises as fsp } from "fs";
11
+ import { mkdirSync, writeFileSync } from "node:fs";
12
+ import { tmpdir } from "os";
13
+ import { join } from "path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ import {
17
+ loadRoutingConfig,
18
+ defaultRoutingConfig,
19
+ resolveBackend,
20
+ estimateCost,
21
+ listBackends,
22
+ describeBackend,
23
+ modelFlagFor,
24
+ requestFromClassifierResult,
25
+ CONFIG_RELATIVE_PATH,
26
+ resolveChain,
27
+ validateRoutingConfig,
28
+ } from "./model-router.mjs";
29
+ import { loadCatalog } from "./model-router/catalog.mjs";
30
+
31
+ async function makeAgentRoot() {
32
+ const path = join(
33
+ tmpdir(),
34
+ `model-router-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
35
+ );
36
+ await fsp.mkdir(join(path, "config"), { recursive: true });
37
+ return path;
38
+ }
39
+
40
+ async function rmRoot(path) {
41
+ try { await fsp.rm(path, { recursive: true, force: true }); } catch { /* */ }
42
+ }
43
+
44
+ function writeJsonConfig(root, body) {
45
+ // We write JSON (not YAML) so the tests don't depend on js-yaml being
46
+ // installed in CI. The loader treats .json identically.
47
+ writeFileSync(join(root, "config/model-routing.json"), JSON.stringify(body, null, 2));
48
+ }
49
+
50
+ const ANTHROPIC_BACKEND = {
51
+ transport: "anthropic-cli",
52
+ base_url: "https://api.anthropic.com",
53
+ capabilities: ["thinking", "vision", "prompt_cache_1h", "tool_use", "parallel_tools", "long_context_1m"],
54
+ pricing: { input_per_m: 3.0, output_per_m: 15.0 },
55
+ models: {
56
+ classifier: "claude-haiku-4-5-20251001",
57
+ default: "claude-sonnet-4-6",
58
+ premium: "claude-opus-4-7",
59
+ fast: "claude-haiku-4-5-20251001",
60
+ },
61
+ };
62
+
63
+ const MOONSHOT_BACKEND = {
64
+ transport: "anthropic-native",
65
+ base_url: "https://api.moonshot.ai/anthropic",
66
+ auth_env: "MOONSHOT_API_KEY",
67
+ capabilities: ["tool_use", "prompt_cache_hit", "long_context_262k"],
68
+ pricing: { input_per_m: 0.95, output_per_m: 4.0, cache_hit_per_m: 0.16 },
69
+ models: { default: "kimi-k2.6", fast: "kimi-k2.6" },
70
+ };
71
+
72
+ const QWEN_BACKEND = {
73
+ transport: "openai-compat",
74
+ base_url: "https://openrouter.ai/api/v1",
75
+ auth_env: "OPENROUTER_API_KEY",
76
+ capabilities: ["tool_use", "long_context_262k", "degraded_tool_use"], // mimic real-world OR bug
77
+ pricing: { input_per_m: 0.22, output_per_m: 1.8 },
78
+ models: { default: "qwen/qwen3-coder", fast: "qwen/qwen3-coder-flash" },
79
+ };
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // loadRoutingConfig
83
+ // ---------------------------------------------------------------------------
84
+
85
+ test("loadRoutingConfig returns null when no config exists", async () => {
86
+ const root = await makeAgentRoot();
87
+ try {
88
+ assert.equal(loadRoutingConfig(root), null);
89
+ } finally { await rmRoot(root); }
90
+ });
91
+
92
+ test("loadRoutingConfig parses JSON and synthesizes anthropic fallback", async () => {
93
+ const root = await makeAgentRoot();
94
+ try {
95
+ writeJsonConfig(root, {
96
+ backends: { moonshot: MOONSHOT_BACKEND },
97
+ routing_policy: [{ default: true, backend: "moonshot" }],
98
+ });
99
+ const cfg = loadRoutingConfig(root);
100
+ assert.ok(cfg, "config should load");
101
+ // Anthropic synthesised because fallback_to_anthropic defaulted to true.
102
+ assert.ok(cfg.backends.anthropic, "anthropic synthesised");
103
+ assert.equal(cfg.backends.moonshot.base_url, "https://api.moonshot.ai/anthropic");
104
+ assert.equal(cfg.routing_policy[0].backend, "moonshot");
105
+ assert.equal(cfg.fallback_to_anthropic, true);
106
+ } finally { await rmRoot(root); }
107
+ });
108
+
109
+ test("loadRoutingConfig respects explicit fallback_to_anthropic: false", async () => {
110
+ const root = await makeAgentRoot();
111
+ try {
112
+ writeJsonConfig(root, {
113
+ backends: { moonshot: MOONSHOT_BACKEND },
114
+ routing_policy: [{ default: true, backend: "moonshot" }],
115
+ fallback_to_anthropic: false,
116
+ });
117
+ const cfg = loadRoutingConfig(root);
118
+ assert.equal(cfg.backends.anthropic, undefined, "anthropic NOT synthesised");
119
+ assert.equal(cfg.fallback_to_anthropic, false);
120
+ } finally { await rmRoot(root); }
121
+ });
122
+
123
+ test("loadRoutingConfig throws on malformed JSON", async () => {
124
+ const root = await makeAgentRoot();
125
+ try {
126
+ writeFileSync(join(root, "config/model-routing.json"), "{ not valid json");
127
+ assert.throws(() => loadRoutingConfig(root), /not valid|JSON|parse/);
128
+ } finally { await rmRoot(root); }
129
+ });
130
+
131
+ test("loadRoutingConfig accepts $MAESTRO_ROUTING_CONFIG override", async () => {
132
+ const root = await makeAgentRoot();
133
+ const alt = await makeAgentRoot();
134
+ try {
135
+ const altPath = join(alt, "config/model-routing.json");
136
+ writeFileSync(altPath, JSON.stringify({
137
+ backends: { moonshot: MOONSHOT_BACKEND },
138
+ routing_policy: [{ default: true, backend: "moonshot" }],
139
+ }));
140
+ const prev = process.env.MAESTRO_ROUTING_CONFIG;
141
+ process.env.MAESTRO_ROUTING_CONFIG = altPath;
142
+ try {
143
+ const cfg = loadRoutingConfig(root);
144
+ assert.ok(cfg.backends.moonshot);
145
+ } finally {
146
+ if (prev === undefined) delete process.env.MAESTRO_ROUTING_CONFIG;
147
+ else process.env.MAESTRO_ROUTING_CONFIG = prev;
148
+ }
149
+ } finally {
150
+ await rmRoot(root);
151
+ await rmRoot(alt);
152
+ }
153
+ });
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // resolveBackend
157
+ // ---------------------------------------------------------------------------
158
+
159
+ test("resolveBackend returns null when no config is loaded", () => {
160
+ const r = resolveBackend({ agent_role: "responder" }, { config: null });
161
+ assert.equal(r, null, "no config → no resolution (callers preserve current behaviour)");
162
+ });
163
+
164
+ test("resolveBackend picks default rule when nothing else matches", () => {
165
+ const config = {
166
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND },
167
+ routing_policy: [{ default: true, backend: "moonshot" }],
168
+ fallback_to_anthropic: true,
169
+ strip_attribution_header: true,
170
+ disable_experimental_betas: true,
171
+ };
172
+ const r = resolveBackend({ agent_role: "responder", tier: "default" }, { config, env: { MOONSHOT_API_KEY: "msk-test" } });
173
+ assert.equal(r.name, "moonshot");
174
+ assert.equal(r.model, "kimi-k2.6");
175
+ assert.equal(r.envForSpawn.ANTHROPIC_BASE_URL, "https://api.moonshot.ai/anthropic");
176
+ assert.equal(r.envForSpawn.ANTHROPIC_AUTH_TOKEN, "msk-test");
177
+ // Critical: API_KEY must be explicit empty string, not unset.
178
+ assert.equal(r.envForSpawn.ANTHROPIC_API_KEY, "");
179
+ assert.equal(r.envForSpawn.ANTHROPIC_MODEL, "kimi-k2.6");
180
+ assert.equal(r.envForSpawn.CLAUDE_CODE_ATTRIBUTION_HEADER, "0");
181
+ assert.equal(r.envForSpawn.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS, "1");
182
+ });
183
+
184
+ test("resolveBackend forces anthropic for sensitive roles via agent_role_in", () => {
185
+ const config = {
186
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND, qwen: QWEN_BACKEND },
187
+ routing_policy: [
188
+ { match: { agent_role_in: ["ceo_pre_pass", "audit", "decision_writer"] }, backend: "anthropic" },
189
+ { default: true, backend: "qwen" },
190
+ ],
191
+ };
192
+ const sensitive = resolveBackend({ agent_role: "ceo_pre_pass" }, { config });
193
+ assert.equal(sensitive.name, "anthropic");
194
+ assert.equal(sensitive.transport, "anthropic-cli");
195
+ // anthropic-cli mode injects no env — current CLI behaviour preserved.
196
+ assert.deepEqual(sensitive.envForSpawn, {});
197
+
198
+ const routine = resolveBackend({ agent_role: "log_triage" }, { config, env: { OPENROUTER_API_KEY: "or-test" } });
199
+ assert.equal(routine.name, "qwen");
200
+ assert.equal(routine.envForSpawn.ANTHROPIC_AUTH_TOKEN, "or-test");
201
+ });
202
+
203
+ test("resolveBackend falls through when backend lacks capability (no_thinking)", () => {
204
+ const config = {
205
+ backends: { anthropic: ANTHROPIC_BACKEND, qwen: QWEN_BACKEND },
206
+ routing_policy: [
207
+ { match: { needs_thinking: true }, backend: "qwen", fallback: ["anthropic"] },
208
+ { default: true, backend: "qwen" },
209
+ ],
210
+ fallback_to_anthropic: true,
211
+ };
212
+ const r = resolveBackend({ agent_role: "responder", needs_thinking: true }, { config });
213
+ // Qwen has no `thinking` cap → falls back to anthropic.
214
+ assert.equal(r.name, "anthropic");
215
+ assert.equal(r.tried[0].backend, "qwen");
216
+ assert.equal(r.tried[0].reason, "no_thinking");
217
+ });
218
+
219
+ test("resolveBackend rejects degraded_tool_use when caller needs reliable tools", () => {
220
+ const config = {
221
+ backends: { anthropic: ANTHROPIC_BACKEND, qwen: QWEN_BACKEND },
222
+ routing_policy: [
223
+ { match: { needs_tool_use: true }, backend: "qwen", fallback: ["anthropic"] },
224
+ ],
225
+ fallback_to_anthropic: true,
226
+ };
227
+ const r = resolveBackend({ agent_role: "responder", needs_tool_use: true }, { config });
228
+ assert.equal(r.name, "anthropic");
229
+ assert.equal(r.tried[0].backend, "qwen");
230
+ assert.equal(r.tried[0].reason, "degraded_tool_use");
231
+ });
232
+
233
+ test("resolveBackend honours token_estimate_gte for long-context routing", () => {
234
+ const config = {
235
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND },
236
+ routing_policy: [
237
+ { match: { token_estimate_gte: 60000 }, backend: "moonshot" },
238
+ { default: true, backend: "anthropic" },
239
+ ],
240
+ };
241
+ const short = resolveBackend({ token_estimate: 20000 }, { config });
242
+ const long = resolveBackend({ token_estimate: 120000 }, { config, env: { MOONSHOT_API_KEY: "msk" } });
243
+ assert.equal(short.name, "anthropic");
244
+ assert.equal(long.name, "moonshot");
245
+ });
246
+
247
+ test("resolveBackend honours model_hint legacy values (opus/sonnet/haiku)", () => {
248
+ const config = {
249
+ backends: { anthropic: ANTHROPIC_BACKEND },
250
+ routing_policy: [{ default: true, backend: "anthropic" }],
251
+ };
252
+ const opus = resolveBackend({ model_hint: "opus" }, { config });
253
+ const sonnet = resolveBackend({ model_hint: "sonnet" }, { config });
254
+ const haiku = resolveBackend({ model_hint: "haiku" }, { config });
255
+ assert.equal(opus.model, "claude-opus-4-7");
256
+ assert.equal(sonnet.model, "claude-sonnet-4-6");
257
+ assert.equal(haiku.model, "claude-haiku-4-5-20251001");
258
+ assert.equal(modelFlagFor(opus, { model_hint: "opus" }), "opus");
259
+ assert.equal(modelFlagFor(sonnet, { model_hint: "sonnet" }), "sonnet");
260
+ assert.equal(modelFlagFor(haiku, { model_hint: "haiku" }), "haiku");
261
+ });
262
+
263
+ test("resolveBackend returns null when no backend satisfies request and fallback disabled", () => {
264
+ const config = {
265
+ backends: { qwen: QWEN_BACKEND },
266
+ routing_policy: [{ default: true, backend: "qwen" }],
267
+ fallback_to_anthropic: false,
268
+ };
269
+ const r = resolveBackend({ needs_thinking: true }, { config });
270
+ assert.equal(r, null, "no fallback and no compatible backend → null");
271
+ });
272
+
273
+ test("resolveBackend tags fallback_reason when falling through to anthropic safety net", () => {
274
+ const config = {
275
+ backends: { anthropic: ANTHROPIC_BACKEND, qwen: QWEN_BACKEND },
276
+ routing_policy: [{ default: true, backend: "qwen" }],
277
+ fallback_to_anthropic: true,
278
+ };
279
+ const r = resolveBackend({ needs_thinking: true }, { config });
280
+ assert.equal(r.name, "anthropic");
281
+ assert.equal(r.fallback_reason, "no_compatible_backend");
282
+ });
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // estimateCost
286
+ // ---------------------------------------------------------------------------
287
+
288
+ test("estimateCost returns null without pricing data", () => {
289
+ const fake = { pricing: {} };
290
+ assert.equal(estimateCost({ token_estimate: 1000 }, fake), null);
291
+ });
292
+
293
+ test("estimateCost computes per-million-token cost without cache hits", () => {
294
+ const fake = { pricing: { input_per_m: 3.0, output_per_m: 15.0 } };
295
+ const cost = estimateCost({ token_estimate: 1_000_000, token_estimate_out: 500_000 }, fake);
296
+ // 1M input @ $3 + 0.5M output @ $15 = $3 + $7.5 = $10.5
297
+ assert.equal(cost, 10.5);
298
+ });
299
+
300
+ test("estimateCost honours cache_hit_per_m for Moonshot-style caching", () => {
301
+ const fake = { pricing: { input_per_m: 0.95, output_per_m: 4.0, cache_hit_per_m: 0.16 } };
302
+ // 1M tokens, 80% cache hit, no explicit output → defaults to 0.5M output
303
+ const cost = estimateCost({ token_estimate: 1_000_000, cache_hit_ratio: 0.8 }, fake);
304
+ // input: 200k @ 0.95 + 800k @ 0.16 = 0.19 + 0.128 = 0.318
305
+ // output: 500k @ 4.0 = 2.0
306
+ // total: 2.318
307
+ assert.ok(Math.abs(cost - 2.318) < 0.001, `got ${cost}`);
308
+ });
309
+
310
+ // ---------------------------------------------------------------------------
311
+ // listBackends / describeBackend
312
+ // ---------------------------------------------------------------------------
313
+
314
+ test("listBackends defaults to ['anthropic'] when no config", () => {
315
+ assert.deepEqual(listBackends({ config: null }), ["anthropic"]);
316
+ });
317
+
318
+ test("listBackends returns all configured backend keys", () => {
319
+ const config = {
320
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND, qwen: QWEN_BACKEND },
321
+ routing_policy: [],
322
+ };
323
+ assert.deepEqual(listBackends({ config }).sort(), ["anthropic", "moonshot", "qwen"]);
324
+ });
325
+
326
+ test("describeBackend returns model/transport/pricing for a backend", () => {
327
+ const config = {
328
+ backends: { moonshot: MOONSHOT_BACKEND },
329
+ routing_policy: [],
330
+ };
331
+ const d = describeBackend("moonshot", { config });
332
+ assert.equal(d.name, "moonshot");
333
+ assert.equal(d.transport, "anthropic-native");
334
+ assert.equal(d.base_url, "https://api.moonshot.ai/anthropic");
335
+ assert.equal(d.models.default, "kimi-k2.6");
336
+ });
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // requestFromClassifierResult
340
+ // ---------------------------------------------------------------------------
341
+
342
+ test("requestFromClassifierResult preserves priority + model hint", () => {
343
+ const req = requestFromClassifierResult(
344
+ { priority: "critical", model: "opus", summary: "CEO request" },
345
+ { role: "responder", source: "inbox" }
346
+ );
347
+ assert.equal(req.priority, "critical");
348
+ assert.equal(req.model_hint, "opus");
349
+ assert.equal(req.needs_thinking, true);
350
+ assert.equal(req.needs_tool_use, true);
351
+ assert.equal(req.agent_role, "responder");
352
+ });
353
+
354
+ test("requestFromClassifierResult does NOT set needs_thinking for routine sonnet items", () => {
355
+ const req = requestFromClassifierResult(
356
+ { priority: "normal", model: "sonnet" },
357
+ { role: "responder" }
358
+ );
359
+ assert.equal(req.needs_thinking, undefined);
360
+ assert.equal(req.model_hint, "sonnet");
361
+ });
362
+
363
+ // ---------------------------------------------------------------------------
364
+ // Foot-gun fixes (audit W5 / W8 + kill switch)
365
+ // ---------------------------------------------------------------------------
366
+
367
+ test("W5: session work defaults needs_tool_use=true even for routine items", () => {
368
+ // A normal-priority backlog item is still a full agent session — it must
369
+ // declare tool use so a degraded_tool_use backend is rejected.
370
+ const req = requestFromClassifierResult(
371
+ { priority: "normal", model: "sonnet" },
372
+ { role: "responder", source: "backlog" }
373
+ );
374
+ assert.equal(req.needs_tool_use, true, "session work needs tool use by default");
375
+ });
376
+
377
+ test("W5: one-shot lookups may opt out of needs_tool_use", () => {
378
+ const req = requestFromClassifierResult(
379
+ { priority: "normal", model: "sonnet" },
380
+ { role: "lookup", oneShot: true }
381
+ );
382
+ assert.equal(req.needs_tool_use, false, "one-shot lookups opt out");
383
+ });
384
+
385
+ test("W5: shipped-example backlog item is kept off the degraded_tool_use backend", () => {
386
+ // Mirrors scaffold/config/model-routing.yaml.example: a normal backlog item
387
+ // matched `source: backlog → openrouter_qwen` (degraded_tool_use). With
388
+ // needs_tool_use defaulting true, the capability gate now fires and the
389
+ // request falls through to a tool-capable backend.
390
+ const config = {
391
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND, qwen: QWEN_BACKEND },
392
+ routing_policy: [
393
+ { match: { source: "backlog" }, backend: "qwen", fallback: ["moonshot", "anthropic"] },
394
+ { default: true, backend: "moonshot", fallback: ["anthropic"] },
395
+ ],
396
+ fallback_to_anthropic: true,
397
+ };
398
+ const req = requestFromClassifierResult(
399
+ { priority: "normal", model: "sonnet" },
400
+ { role: "responder", source: "backlog" }
401
+ );
402
+ const r = resolveBackend(req, { config, env: { MOONSHOT_API_KEY: "msk" } });
403
+ assert.notEqual(r.name, "qwen", "must NOT route a tool-using session to degraded_tool_use backend");
404
+ assert.equal(r.name, "moonshot");
405
+ assert.equal(r.tried[0].backend, "qwen");
406
+ assert.equal(r.tried[0].reason, "degraded_tool_use");
407
+ });
408
+
409
+ test("W8: candidate with missing auth env is skipped, not spawned with empty token", () => {
410
+ const config = {
411
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND },
412
+ routing_policy: [{ default: true, backend: "moonshot", fallback: ["anthropic"] }],
413
+ fallback_to_anthropic: true,
414
+ };
415
+ // MOONSHOT_API_KEY deliberately absent from env.
416
+ const r = resolveBackend({ agent_role: "responder", tier: "default" }, { config, env: {} });
417
+ assert.equal(r.name, "anthropic", "skips moonshot, lands on anthropic safety net");
418
+ assert.equal(r.tried[0].backend, "moonshot");
419
+ assert.equal(r.tried[0].reason, "missing_auth_env");
420
+ // The anthropic-cli safety net injects no empty credential.
421
+ assert.deepEqual(r.envForSpawn, {});
422
+ });
423
+
424
+ test("W8: empty/whitespace auth env counts as absent and is skipped", () => {
425
+ const config = {
426
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND },
427
+ routing_policy: [{ default: true, backend: "moonshot", fallback: ["anthropic"] }],
428
+ fallback_to_anthropic: true,
429
+ };
430
+ const r = resolveBackend({ agent_role: "responder" }, { config, env: { MOONSHOT_API_KEY: " " } });
431
+ assert.equal(r.name, "anthropic");
432
+ assert.equal(r.tried[0].reason, "missing_auth_env");
433
+ });
434
+
435
+ test("W8: all candidates missing auth + no fallback → null (never empty spawn)", () => {
436
+ const config = {
437
+ backends: { moonshot: MOONSHOT_BACKEND },
438
+ routing_policy: [{ default: true, backend: "moonshot" }],
439
+ fallback_to_anthropic: false,
440
+ };
441
+ const r = resolveBackend({ agent_role: "responder" }, { config, env: {} });
442
+ assert.equal(r, null, "no creds anywhere and no fallback → no resolution");
443
+ });
444
+
445
+ test("W8: present auth env still resolves normally (no regression)", () => {
446
+ const config = {
447
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND },
448
+ routing_policy: [{ default: true, backend: "moonshot", fallback: ["anthropic"] }],
449
+ fallback_to_anthropic: true,
450
+ };
451
+ const r = resolveBackend({ agent_role: "responder" }, { config, env: { MOONSHOT_API_KEY: "msk-ok" } });
452
+ assert.equal(r.name, "moonshot");
453
+ assert.equal(r.envForSpawn.ANTHROPIC_AUTH_TOKEN, "msk-ok");
454
+ });
455
+
456
+ test("kill switch: MAESTRO_ROUTER_FORCE_ANTHROPIC=1 short-circuits to stock behaviour", () => {
457
+ const config = {
458
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND },
459
+ routing_policy: [{ default: true, backend: "moonshot" }],
460
+ fallback_to_anthropic: true,
461
+ };
462
+ // Even with a valid Moonshot key and a default-moonshot policy, the kill
463
+ // switch returns null — the same path as no config, so callers preserve
464
+ // the legacy keychain-OAuth `claude --print` behaviour.
465
+ const r = resolveBackend(
466
+ { agent_role: "responder" },
467
+ { config, env: { MAESTRO_ROUTER_FORCE_ANTHROPIC: "1", MOONSHOT_API_KEY: "msk" } }
468
+ );
469
+ assert.equal(r, null, "kill switch → null (stock Anthropic CLI behaviour)");
470
+ });
471
+
472
+ test("kill switch: accepts 'true' as well as '1'", () => {
473
+ const config = {
474
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND },
475
+ routing_policy: [{ default: true, backend: "moonshot" }],
476
+ };
477
+ const r = resolveBackend(
478
+ { agent_role: "responder" },
479
+ { config, env: { MAESTRO_ROUTER_FORCE_ANTHROPIC: "true", MOONSHOT_API_KEY: "msk" } }
480
+ );
481
+ assert.equal(r, null);
482
+ });
483
+
484
+ test("kill switch: any other value does NOT short-circuit", () => {
485
+ const config = {
486
+ backends: { anthropic: ANTHROPIC_BACKEND, moonshot: MOONSHOT_BACKEND },
487
+ routing_policy: [{ default: true, backend: "moonshot" }],
488
+ };
489
+ const r = resolveBackend(
490
+ { agent_role: "responder" },
491
+ { config, env: { MAESTRO_ROUTER_FORCE_ANTHROPIC: "0", MOONSHOT_API_KEY: "msk" } }
492
+ );
493
+ assert.equal(r.name, "moonshot", "0/unset → routing still active");
494
+ });
495
+
496
+ // ---------------------------------------------------------------------------
497
+ // defaultRoutingConfig wiring (audit L21)
498
+ // ---------------------------------------------------------------------------
499
+
500
+ test("defaultRoutingConfig returns a usable Anthropic-only config", () => {
501
+ const cfg = defaultRoutingConfig();
502
+ assert.ok(cfg, "default config should be an object");
503
+ assert.ok(cfg.backends.anthropic, "default config has an anthropic backend");
504
+ assert.equal(cfg.backends.anthropic.transport, "anthropic-cli");
505
+ assert.equal(cfg.fallback_to_anthropic, true);
506
+ // resolveBackend must accept it and preserve current CLI behaviour (no env).
507
+ const r = resolveBackend({ agent_role: "responder" }, { config: cfg });
508
+ assert.equal(r.name, "anthropic");
509
+ assert.equal(r.transport, "anthropic-cli");
510
+ assert.deepEqual(r.envForSpawn, {}, "anthropic-cli default injects no env");
511
+ });
512
+
513
+ test("defaultRoutingConfig returns a fresh, mutation-safe copy each call", () => {
514
+ const a = defaultRoutingConfig();
515
+ const b = defaultRoutingConfig();
516
+ assert.notEqual(a, b, "each call returns a distinct object");
517
+ a.backends.anthropic.models.default = "MUTATED";
518
+ assert.notEqual(
519
+ b.backends.anthropic.models.default,
520
+ "MUTATED",
521
+ "mutating one copy must not affect another",
522
+ );
523
+ });
524
+
525
+ test("loadRoutingConfig still returns null with no config and no useDefault flag", async () => {
526
+ const root = await makeAgentRoot();
527
+ try {
528
+ assert.equal(loadRoutingConfig(root), null, "default contract preserved");
529
+ } finally { await rmRoot(root); }
530
+ });
531
+
532
+ test("loadRoutingConfig({ useDefault: true }) returns the default when no file exists", async () => {
533
+ const root = await makeAgentRoot();
534
+ try {
535
+ const cfg = loadRoutingConfig(root, { useDefault: true });
536
+ assert.ok(cfg, "useDefault yields a config instead of null");
537
+ assert.ok(cfg.backends.anthropic, "it is the anthropic default");
538
+ assert.equal(cfg.fallback_to_anthropic, true);
539
+ } finally { await rmRoot(root); }
540
+ });
541
+
542
+ test("loadRoutingConfig prefers a real file over the useDefault fallback", async () => {
543
+ const root = await makeAgentRoot();
544
+ try {
545
+ writeJsonConfig(root, {
546
+ backends: { moonshot: MOONSHOT_BACKEND },
547
+ routing_policy: [{ default: true, backend: "moonshot" }],
548
+ });
549
+ const cfg = loadRoutingConfig(root, { useDefault: true });
550
+ assert.ok(cfg.backends.moonshot, "real config wins over the default fallback");
551
+ } finally { await rmRoot(root); }
552
+ });
553
+
554
+ // ---------------------------------------------------------------------------
555
+ // Constants exported
556
+ // ---------------------------------------------------------------------------
557
+
558
+ test("CONFIG_RELATIVE_PATH points at config/model-routing.yaml", () => {
559
+ assert.equal(CONFIG_RELATIVE_PATH, "config/model-routing.yaml");
560
+ });
561
+
562
+ // ===========================================================================
563
+ // resolveChain — the v2 policy brain (additive; v1 above stays byte-compatible)
564
+ // ===========================================================================
565
+
566
+ // One shared bundled catalog snapshot (read-only) for the resolve tests. Loaded
567
+ // from the framework's own lib/model-router/catalog/*.yaml so the refs are real.
568
+ // This file lives at <root>/lib/, so the framework root is one dir up.
569
+ const MAESTRO_ROOT_FOR_TESTS = () =>
570
+ join(fileURLToPath(new URL(".", import.meta.url)), "..");
571
+ const CATALOG = loadCatalog(MAESTRO_ROOT_FOR_TESTS(), {});
572
+
573
+ const V2_CONFIG = Object.freeze({
574
+ schema_version: 2,
575
+ aliases: {
576
+ frontier: "anthropic/claude-opus-4-8",
577
+ default: "anthropic/claude-sonnet-4-6",
578
+ fast: "anthropic/claude-haiku-4-5",
579
+ cheap: "deepseek/deepseek-v4-flash",
580
+ "cheap-session": "moonshot/kimi-k2.6",
581
+ },
582
+ defaults: { needs_tool_use_for_sessions: true, data_class: "sensitive", cache_ttl: "1h" },
583
+ backends: {
584
+ anthropic: { allowed_data_classes: ["public", "internal", "sensitive"] },
585
+ deepseek: { allowed_data_classes: ["public"] },
586
+ moonshot: { allowed_data_classes: ["public"] },
587
+ },
588
+ routing_policy: [
589
+ { match: { agent_role_in: ["regulatory", "audit"] }, chain: ["frontier", "default"], pin: true },
590
+ { match: { task_class: "classify.inbox" }, harness: "direct", chain: ["fast", "cheap", "rules"], needs_tool_use: false },
591
+ { match: { task_class: "lookup.gmail" }, harness: "direct", chain: ["fast", "cheap"], needs_tool_use: false },
592
+ { match: { source: "backlog", data_class: "public" }, chain: ["cheap", "default"] },
593
+ { match: { token_estimate_gte: 250000 }, chain: ["default", "frontier"] },
594
+ { default: true, chain: ["default", "fast"] },
595
+ ],
596
+ budget_ladder: {
597
+ "75": { downgrade_tiers: 1 },
598
+ "90": { force_alias: "cheap_or_fast" },
599
+ "100": { essential_only: true },
600
+ },
601
+ fallback_to_anthropic: true,
602
+ });
603
+
604
+ const CLOCK = () => 1_718_000_000_000;
605
+ function rc(req, extra = {}) {
606
+ return resolveChain(req, { catalog: CATALOG, config: V2_CONFIG, env: {}, now: CLOCK, ...extra });
607
+ }
608
+
609
+ // ── rule match + chain build ───────────────────────────────────────────────
610
+
611
+ test("resolveChain: default rule routes a session to the default workhorse", () => {
612
+ const d = rc({ task_class: "session.responder", source: "inbox", data_class: "sensitive", token_estimate: 5000 });
613
+ assert.equal(d.chosen.provider, "anthropic");
614
+ assert.equal(d.chosen.model, "claude-sonnet-4-6");
615
+ assert.equal(d.chosen.harness, "session");
616
+ assert.ok(d.decision_id, "every decision has an id");
617
+ assert.ok(d.explain && typeof d.explain === "string", "explain is mandatory");
618
+ });
619
+
620
+ test("resolveChain: first-match picks the regulatory pin rule (frontier)", () => {
621
+ const d = rc({ agent_role: "regulatory", task_class: "decision.writer", data_class: "sensitive", token_estimate: 1000 });
622
+ assert.equal(d.chosen.model, "claude-opus-4-8", "regulatory → frontier");
623
+ // The visible chain leads with the matched rule's first alias.
624
+ assert.equal(d.chain[0].ref, "anthropic/claude-opus-4-8");
625
+ });
626
+
627
+ test("resolveChain: classify.inbox resolves direct harness + tool-less", () => {
628
+ // Anthropic direct needs the key; with a key present, fast (haiku) is chosen.
629
+ const d = resolveChain(
630
+ { task_class: "classify.inbox", source: "inbox", data_class: "public", token_estimate: 1500 },
631
+ { catalog: CATALOG, config: V2_CONFIG, env: { ANTHROPIC_API_KEY: "ak" }, now: CLOCK }
632
+ );
633
+ assert.equal(d.chosen.harness, "direct");
634
+ assert.equal(d.chosen.model, "claude-haiku-4-5");
635
+ });
636
+
637
+ test("resolveChain: the chain[] carries the failover tail, not just the winner", () => {
638
+ const d = rc({ task_class: "session.responder", data_class: "sensitive", token_estimate: 1000 });
639
+ // default chain is [default, fast]; both Anthropic session rows survive.
640
+ const refs = d.chain.map((c) => c.ref);
641
+ assert.deepEqual(refs, ["anthropic/claude-sonnet-4-6", "anthropic/claude-haiku-4-5"]);
642
+ });
643
+
644
+ // ── credential gate (key-absence-as-enforcement, §7.1) ──────────────────────
645
+
646
+ test("resolveChain: a third-party row without its key is skipped (missing_credential)", () => {
647
+ // lookup.gmail is harness:direct, needs_tool_use:false so deepseek (grade B)
648
+ // clears the capability gate — the ONLY reason left to skip it is the absent
649
+ // DEEPSEEK_API_KEY (key-absence-as-enforcement, §7.1). Anthropic haiku direct
650
+ // also needs its key (absent) so the chain ends on the Anthropic safety net.
651
+ const d = rc({ task_class: "lookup.gmail", data_class: "public", token_estimate: 1000 });
652
+ const skipped = d.tried.find((t) => t.ref === "deepseek/deepseek-v4-flash");
653
+ assert.ok(skipped, "deepseek recorded in tried[]");
654
+ assert.equal(skipped.reason, "missing_credential");
655
+ });
656
+
657
+ test("resolveChain: present third-party key still gated by grade for sessions (B<A)", () => {
658
+ // deepseek is grade B; a session needing tools requires grade A (§6.6), so even
659
+ // WITH the key it is skipped grade_too_low and we fall to Anthropic.
660
+ const d = resolveChain(
661
+ { task_class: "backlog.work", source: "backlog", data_class: "public", token_estimate: 1000 },
662
+ { catalog: CATALOG, config: V2_CONFIG, env: { DEEPSEEK_API_KEY: "dk" }, now: CLOCK }
663
+ );
664
+ assert.equal(d.chosen.provider, "anthropic");
665
+ const skipped = d.tried.find((t) => t.ref === "deepseek/deepseek-v4-flash");
666
+ assert.equal(skipped.reason, "grade_too_low");
667
+ });
668
+
669
+ test("resolveChain: grade-B row IS reachable on the direct (tool-less) lane", () => {
670
+ // lookup.gmail is harness:direct, needs_tool_use:false; deepseek (B) is fine.
671
+ // Anthropic haiku (direct) needs ANTHROPIC_API_KEY (absent) so it is skipped,
672
+ // landing on deepseek with its key present.
673
+ const d = resolveChain(
674
+ { task_class: "lookup.gmail", data_class: "public", token_estimate: 1000 },
675
+ { catalog: CATALOG, config: V2_CONFIG, env: { DEEPSEEK_API_KEY: "dk" }, now: CLOCK }
676
+ );
677
+ assert.equal(d.chosen.provider, "deepseek");
678
+ assert.equal(d.chosen.harness, "direct");
679
+ });
680
+
681
+ // ── data_class gate (deny-by-default, §7.6) ─────────────────────────────────
682
+
683
+ test("resolveChain: sensitive data never routes to a public-only backend", () => {
684
+ // Even with the key AND a direct lane, deepseek (allowed: public) is denied for
685
+ // a sensitive request and recorded as data_class_denied.
686
+ const cfg = { ...V2_CONFIG, routing_policy: [{ match: { task_class: "lookup.gmail" }, harness: "direct", chain: ["cheap", "fast"], needs_tool_use: false }, { default: true, chain: ["default"] }] };
687
+ const d = resolveChain(
688
+ { task_class: "lookup.gmail", data_class: "sensitive", token_estimate: 1000 },
689
+ { catalog: CATALOG, config: cfg, env: { DEEPSEEK_API_KEY: "dk", ANTHROPIC_API_KEY: "ak" }, now: CLOCK }
690
+ );
691
+ assert.equal(d.chosen.provider, "anthropic", "sensitive falls back to anthropic");
692
+ const denied = d.tried.find((t) => t.ref === "deepseek/deepseek-v4-flash");
693
+ assert.equal(denied.reason, "data_class_denied");
694
+ });
695
+
696
+ // ── breaker gate (health.isOpen) ────────────────────────────────────────────
697
+
698
+ test("resolveChain: an open breaker skips the candidate (breaker_open in tried[])", () => {
699
+ // Inject an isOpen that reports the sonnet model breaker open; chain falls to
700
+ // the next survivor (haiku).
701
+ const isOpen = (key) => ({
702
+ open: key === "anthropic:claude-sonnet-4-6",
703
+ until: 1_718_000_100_000,
704
+ reason: "rate_limit",
705
+ strikes: 1,
706
+ });
707
+ const d = resolveChain(
708
+ { task_class: "session.responder", data_class: "sensitive", token_estimate: 1000 },
709
+ { catalog: CATALOG, config: V2_CONFIG, env: {}, now: CLOCK, isOpen }
710
+ );
711
+ assert.equal(d.chosen.model, "claude-haiku-4-5", "skips open sonnet, lands on haiku");
712
+ const skipped = d.tried.find((t) => t.ref === "anthropic/claude-sonnet-4-6");
713
+ assert.equal(skipped.reason, "breaker_open");
714
+ });
715
+
716
+ // ── context gate ────────────────────────────────────────────────────────────
717
+
718
+ test("resolveChain: a too-large request skips a small-context row (context_overflow)", () => {
719
+ // haiku context_tokens is 190000; a 250k request matches the long-context rule
720
+ // [default, frontier] (both 950k) and never even tries a small row. To assert
721
+ // the gate directly, force a chain through haiku for a huge prompt.
722
+ const cfg = { ...V2_CONFIG, routing_policy: [{ default: true, chain: ["fast", "default"] }] };
723
+ const d = resolveChain(
724
+ { task_class: "session.responder", data_class: "sensitive", token_estimate: 500000 },
725
+ { catalog: CATALOG, config: cfg, env: {}, now: CLOCK }
726
+ );
727
+ assert.equal(d.chosen.model, "claude-sonnet-4-6", "haiku too small → sonnet");
728
+ const skipped = d.tried.find((t) => t.ref === "anthropic/claude-haiku-4-5");
729
+ assert.equal(skipped.reason, "context_overflow");
730
+ });
731
+
732
+ test("resolveChain: long-context rule routes 250k+ to a 1M-ctx row", () => {
733
+ const d = rc({ task_class: "session.responder", data_class: "sensitive", token_estimate: 300000 });
734
+ assert.equal(d.chosen.model, "claude-sonnet-4-6");
735
+ });
736
+
737
+ // ── envForSpawn (session retarget for Kimi/DeepSeek) ─────────────────────────
738
+
739
+ test("resolveChain: a third-party SESSION row builds the ANTHROPIC_BASE_URL retarget env", () => {
740
+ // Allow a grade override so kimi (B) can host a session, and route public work
741
+ // to it with the key present.
742
+ const cfg = {
743
+ ...V2_CONFIG,
744
+ catalog_overrides: undefined,
745
+ routing_policy: [{ match: { source: "backlog" }, chain: ["cheap-session", "default"] }, { default: true, chain: ["default"] }],
746
+ };
747
+ // Override kimi grade to A via catalog_overrides so the session grade gate passes.
748
+ const cat = loadCatalog(MAESTRO_ROOT_FOR_TESTS(), {
749
+ agentConfig: { catalog_overrides: [{ ref: "moonshot/kimi-k2.6", tool_reliability: "A" }] },
750
+ });
751
+ const d = resolveChain(
752
+ { task_class: "backlog.work", source: "backlog", data_class: "public", token_estimate: 1000 },
753
+ { catalog: cat, config: cfg, env: { MOONSHOT_API_KEY: "mk-test" }, now: CLOCK }
754
+ );
755
+ assert.equal(d.chosen.provider, "moonshot");
756
+ assert.equal(d.chosen.transport, "anthropic-native");
757
+ assert.equal(d.envForSpawn.ANTHROPIC_BASE_URL, "https://api.moonshot.ai/anthropic");
758
+ assert.equal(d.envForSpawn.ANTHROPIC_AUTH_TOKEN, "mk-test");
759
+ assert.equal(d.envForSpawn.ANTHROPIC_API_KEY, "", "empty string, NOT unset (keychain-fallthrough guard)");
760
+ assert.equal(d.envForSpawn.ANTHROPIC_MODEL, "kimi-k2.6");
761
+ });
762
+
763
+ test("resolveChain: an Anthropic session injects NO retarget env (stock CLI)", () => {
764
+ const d = rc({ task_class: "session.responder", data_class: "sensitive", token_estimate: 1000 });
765
+ assert.deepEqual(d.envForSpawn, {});
766
+ });
767
+
768
+ // ── budget ladder ────────────────────────────────────────────────────────────
769
+
770
+ test("resolveChain: band ≥75 downgrades one tier (default→fast) for non-pinned work", () => {
771
+ const d = rc({ task_class: "session.responder", data_class: "sensitive", token_estimate: 1000, budget_band: 75 });
772
+ assert.equal(d.chosen.model, "claude-haiku-4-5", "default downgraded to fast at band 75");
773
+ assert.match(d.explain, /band 75/);
774
+ });
775
+
776
+ test("resolveChain: a pinned rule is exempt from the budget ladder", () => {
777
+ // regulatory rule sets pin:true on the request indirectly via the rule pin flag;
778
+ // pass pin on the request to exercise the exemption path.
779
+ const d = rc({ agent_role: "regulatory", task_class: "decision", data_class: "sensitive", token_estimate: 1000, budget_band: 90, pin: { ref: "anthropic/claude-opus-4-8" } });
780
+ assert.equal(d.chosen.model, "claude-opus-4-8", "pin beats the band-90 downgrade");
781
+ });
782
+
783
+ // ── affinity pin ─────────────────────────────────────────────────────────────
784
+
785
+ test("resolveChain: a live affinity pin is moved to the chain head when it passes gates", () => {
786
+ const d = rc({ task_class: "session.responder", data_class: "sensitive", token_estimate: 1000, session_key: "thread-1", pin: { ref: "anthropic/claude-haiku-4-5" } });
787
+ assert.equal(d.chosen.model, "claude-haiku-4-5");
788
+ assert.equal(d.chain[0].ref, "anthropic/claude-haiku-4-5", "pin at head");
789
+ assert.match(d.explain, /pin /);
790
+ });
791
+
792
+ test("resolveChain: a pin that fails a gate emits pin_overridden and routes normally", () => {
793
+ // Pin a public-only deepseek for sensitive work → data_class_denied → overridden.
794
+ const d = resolveChain(
795
+ { task_class: "session.responder", data_class: "sensitive", token_estimate: 1000, pin: { ref: "deepseek/deepseek-v4-flash" } },
796
+ { catalog: CATALOG, config: V2_CONFIG, env: { DEEPSEEK_API_KEY: "dk" }, now: CLOCK }
797
+ );
798
+ assert.equal(d.chosen.provider, "anthropic", "overridden pin falls to normal routing");
799
+ const overridden = d.tried.find((t) => String(t.reason).startsWith("pin_overridden"));
800
+ assert.ok(overridden, "pin_overridden recorded in tried[]");
801
+ });
802
+
803
+ // ── kill switch ──────────────────────────────────────────────────────────────
804
+
805
+ test("resolveChain: MAESTRO_ROUTER_FORCE_ANTHROPIC forces an Anthropic-only chain", () => {
806
+ const d = resolveChain(
807
+ { task_class: "backlog.work", source: "backlog", data_class: "public", token_estimate: 1000 },
808
+ { catalog: CATALOG, config: V2_CONFIG, env: { MAESTRO_ROUTER_FORCE_ANTHROPIC: "1", DEEPSEEK_API_KEY: "dk" }, now: CLOCK }
809
+ );
810
+ assert.equal(d.chosen.provider, "anthropic");
811
+ assert.equal(d.audit.fallback_reason, "kill_switch");
812
+ assert.match(d.explain, /kill switch/);
813
+ // No third-party retarget env under the kill switch.
814
+ assert.deepEqual(d.envForSpawn, {});
815
+ });
816
+
817
+ test("resolveChain: kill switch picks frontier for critical/thinking work", () => {
818
+ const d = resolveChain(
819
+ { task_class: "decision", needs_thinking: true, data_class: "sensitive", token_estimate: 1000 },
820
+ { catalog: CATALOG, config: V2_CONFIG, env: { MAESTRO_ROUTER_FORCE_ANTHROPIC: "true" }, now: CLOCK }
821
+ );
822
+ assert.equal(d.chosen.model, "claude-opus-4-8");
823
+ });
824
+
825
+ // ── inert / no-config collapse ───────────────────────────────────────────────
826
+
827
+ test("resolveChain: no v2 config collapses to the Anthropic safety net", () => {
828
+ const d = resolveChain(
829
+ { task_class: "session.responder", data_class: "sensitive", token_estimate: 1000 },
830
+ { catalog: CATALOG, config: null, env: {}, now: CLOCK }
831
+ );
832
+ assert.equal(d.chosen.provider, "anthropic");
833
+ assert.equal(d.audit.fallback_reason, "no_v2_config");
834
+ });
835
+
836
+ // ── estCostUSD + ledger join fields ──────────────────────────────────────────
837
+
838
+ test("resolveChain: stamps a finite estCostUSD and a decision_id for the ledger join", () => {
839
+ const d = rc({ task_class: "session.responder", data_class: "sensitive", token_estimate: 100000, token_estimate_out: 20000 });
840
+ assert.ok(typeof d.estCostUSD === "number" && d.estCostUSD > 0, "priced from the catalog row");
841
+ assert.ok(/^[0-9a-z]{18}$/.test(d.decision_id), "ulid-ish decision_id");
842
+ assert.equal(d.spawnArgs.bare, true);
843
+ assert.ok(d.spawnArgs.maxTurns > 0);
844
+ });
845
+
846
+ // ── deterministic "rules" fallback ───────────────────────────────────────────
847
+
848
+ test("resolveChain: falls to the deterministic rules member when no model survives", () => {
849
+ // classify.inbox chain is [fast, cheap, rules]; with no keys at all, both
850
+ // model rows are unreachable and the caller's deterministic fallback wins.
851
+ const d = rc({ task_class: "classify.inbox", source: "inbox", data_class: "public", token_estimate: 1000 });
852
+ assert.equal(d.chosen.provider, "rules");
853
+ assert.equal(d.chosen.harness, "direct");
854
+ });
855
+
856
+ // ===========================================================================
857
+ // validateRoutingConfig — strict v2 config validation
858
+ // ===========================================================================
859
+
860
+ test("validateRoutingConfig: a clean v2 config has zero errors", () => {
861
+ const { errors } = validateRoutingConfig(V2_CONFIG, { catalog: CATALOG, env: {} });
862
+ assert.equal(errors.length, 0, JSON.stringify(errors));
863
+ });
864
+
865
+ test("validateRoutingConfig: unknown match key is an error (capability typo)", () => {
866
+ const cfg = { ...V2_CONFIG, routing_policy: [{ match: { taks_class: "x" }, chain: ["default"] }, { default: true, chain: ["default"] }] };
867
+ const { errors } = validateRoutingConfig(cfg, { catalog: CATALOG });
868
+ assert.ok(errors.some((e) => /unknown match key "taks_class"/.test(e.error)));
869
+ });
870
+
871
+ test("validateRoutingConfig: a chain ref not in the catalog is an error", () => {
872
+ const cfg = { ...V2_CONFIG, aliases: { ...V2_CONFIG.aliases, ghost: "ghost/model-x" }, routing_policy: [{ default: true, chain: ["ghost"] }] };
873
+ const { errors } = validateRoutingConfig(cfg, { catalog: CATALOG });
874
+ assert.ok(errors.some((e) => /not in catalog/.test(e.error)));
875
+ });
876
+
877
+ test("validateRoutingConfig: a chain member that is neither alias nor ref is an error", () => {
878
+ const cfg = { ...V2_CONFIG, routing_policy: [{ default: true, chain: ["notanalias"] }] };
879
+ const { errors } = validateRoutingConfig(cfg, { catalog: CATALOG });
880
+ assert.ok(errors.some((e) => /neither a known alias nor/.test(e.error)));
881
+ });
882
+
883
+ test("validateRoutingConfig: an unknown harness is an error", () => {
884
+ const cfg = { ...V2_CONFIG, routing_policy: [{ match: { task_class: "x" }, harness: "telepathy", chain: ["default"] }, { default: true, chain: ["default"] }] };
885
+ const { errors } = validateRoutingConfig(cfg, { catalog: CATALOG });
886
+ assert.ok(errors.some((e) => /unknown harness "telepathy"/.test(e.error)));
887
+ });
888
+
889
+ test("validateRoutingConfig: an overlay-excluded backend in config is an error (tighten-only)", () => {
890
+ const overlay = { version: "t1", constraints: { backends_allowed: ["anthropic", "deepseek"] } };
891
+ // config declares a moonshot backend the overlay does not permit.
892
+ const { errors } = validateRoutingConfig(V2_CONFIG, { catalog: CATALOG, overlay });
893
+ assert.ok(errors.some((e) => /not in the org overlay's backends_allowed/.test(e.error)));
894
+ });
895
+
896
+ test("validateRoutingConfig: a missing third-party key is a WARNING, not an error", () => {
897
+ const { errors, warnings } = validateRoutingConfig(V2_CONFIG, { catalog: CATALOG, env: {} });
898
+ assert.equal(errors.length, 0);
899
+ assert.ok(warnings.some((w) => /DEEPSEEK_API_KEY/.test(w.warning)));
900
+ // Anthropic's key is never warned (session rides keychain OAuth).
901
+ assert.ok(!warnings.some((w) => /ANTHROPIC_API_KEY/.test(w.warning)));
902
+ });
903
+
904
+ test("validateRoutingConfig: a v1 config is not strict-validated (returns clean)", () => {
905
+ const { errors } = validateRoutingConfig({ backends: {}, routing_policy: [] }, { catalog: CATALOG });
906
+ assert.equal(errors.length, 0);
907
+ });