@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,297 @@
1
+ /**
2
+ * lib/resource-governor.mjs — one admission gate before every spawn (WS4).
3
+ *
4
+ * `admit({source, priority, mode}, deps?)` decides, deterministically given
5
+ * its inputs, whether a new `claude --print` sub-session may launch:
6
+ *
7
+ * ADMIT — launch now.
8
+ * QUEUE — keep the work in the in-memory dispatcher queue (re-derivable
9
+ * from durable state/queues/*.yaml); it spawns when a slot frees.
10
+ * DEFER — leave the work on disk untouched (backlog: the 10-min sweep
11
+ * retries; cadence: requeueTick). DEFER must NEVER burn retry
12
+ * budget (the callers honour this — they do not call failTick on a
13
+ * governor deferral).
14
+ *
15
+ * The whole point (Invariants): under pressure work is QUEUED or DEFERRED —
16
+ * never dropped — and the agent throttles instead of bricking.
17
+ *
18
+ * Dynamic ceiling (replaces the hardcoded MAX_CONCURRENT=10):
19
+ * dynamicMax = clamp(floor(totalmem * 0.60 / 750MB), 1, min(10, cpus))
20
+ * effectiveMax = min(dynamicMax, throttleCeiling)
21
+ * where throttleCeiling comes from state/throttle.json (written by the memory
22
+ * watchdog's SOFT tier; auto-expiring). The governor only READS that file —
23
+ * it never writes it.
24
+ *
25
+ * Thresholds (env GOV_* overridable; tests inject `deps` instead):
26
+ * liveCount >= effectiveMax → QUEUE
27
+ * freemem < totalmem*0.15 OR < 2*750MB headroom → DEFER
28
+ * loadavg1 > cpus*1.5 → DEFER
29
+ * liveRSS + 750MB > 0.60*totalmem → QUEUE
30
+ * else → ADMIT
31
+ *
32
+ * Plus a budget `mode`: when essential-only (budget at 100%),
33
+ * source==="backlog"/"cadence" → DEFER; inbox → still goes through the normal
34
+ * ADMIT/QUEUE path (never go dark on the user).
35
+ *
36
+ * Purity: `admit` is a pure function of its injected `deps` snapshot. The
37
+ * default `deps` reads real os.* + a 2s-cached `liveClaudeStats()` (via ps) +
38
+ * state/throttle.json, but tests pass an explicit `deps` and get fully
39
+ * deterministic decisions.
40
+ *
41
+ * Constraints (CLAUDE.md): ESM, execFile (not exec), Node built-ins only.
42
+ */
43
+
44
+ import os from "node:os";
45
+ import { execFileSync } from "node:child_process";
46
+ import { existsSync, readFileSync } from "node:fs";
47
+ import { join, resolve } from "node:path";
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Constants
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /** Per-session RAM budget heuristic (Claude Code sub-session working set). */
54
+ export const PER_SESSION_MB = num(process.env.GOV_PER_SESSION_MB, 750);
55
+ const MB = 1024 * 1024;
56
+
57
+ /** Fraction of total RAM the governor is willing to hand to claude sessions. */
58
+ export const RAM_FRACTION = frac(process.env.GOV_RAM_FRACTION, 0.6);
59
+ /** Free-memory floor as a fraction of total RAM. */
60
+ export const FREEMEM_FLOOR_FRACTION = frac(process.env.GOV_FREEMEM_FLOOR, 0.15);
61
+ /** Load-average multiplier over cpu count that triggers a DEFER. */
62
+ export const LOAD_FACTOR = num(process.env.GOV_LOAD_FACTOR, 1.5);
63
+ /** Hard ceiling on dynamicMax regardless of RAM. */
64
+ export const HARD_MAX = num(process.env.GOV_HARD_MAX, 10);
65
+ /** Minimum dynamicMax (always allow at least one session). */
66
+ const MIN_MAX = 1;
67
+
68
+ function num(v, dflt) {
69
+ const n = Number(v);
70
+ return Number.isFinite(n) && n > 0 ? n : dflt;
71
+ }
72
+ function frac(v, dflt) {
73
+ const n = Number(v);
74
+ return Number.isFinite(n) && n > 0 && n < 1 ? n : dflt;
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // throttle.json (written by the watchdog SOFT tier; we only read it)
79
+ // ---------------------------------------------------------------------------
80
+
81
+ function throttlePath(deps) {
82
+ if (deps && deps.throttlePath) return resolve(deps.throttlePath);
83
+ const root =
84
+ (deps && deps.agentRoot) ||
85
+ process.env.AGENT_ROOT ||
86
+ process.env.AGENT_DIR ||
87
+ process.cwd();
88
+ return join(resolve(root), "state", "throttle.json");
89
+ }
90
+
91
+ /**
92
+ * Read the soft-throttle ceiling. Returns Infinity when no (unexpired)
93
+ * throttle is in force, so `min(dynamicMax, ceiling)` is a no-op by default.
94
+ * Never throws.
95
+ *
96
+ * @returns {{ ceiling:number, reason?:string, expiresAt?:number }}
97
+ */
98
+ export function readThrottle(deps) {
99
+ const now = (deps && typeof deps.now === "function" ? deps.now : Date.now)();
100
+ const p = throttlePath(deps);
101
+ if (!existsSync(p)) return { ceiling: Infinity };
102
+ try {
103
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
104
+ const expiresAt = Number(raw.expires_at ?? raw.expiresAt);
105
+ // Auto-expire: a stale throttle file (watchdog died before clearing it)
106
+ // must not pin the ceiling forever.
107
+ if (Number.isFinite(expiresAt) && now >= expiresAt) return { ceiling: Infinity };
108
+ const ceiling = Number(raw.ceiling);
109
+ if (!Number.isFinite(ceiling) || ceiling < 0) return { ceiling: Infinity };
110
+ return { ceiling, reason: raw.reason, expiresAt: Number.isFinite(expiresAt) ? expiresAt : undefined };
111
+ } catch {
112
+ return { ceiling: Infinity };
113
+ }
114
+ }
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Live claude process stats (cached 2s) via ps
118
+ // ---------------------------------------------------------------------------
119
+
120
+ let _liveCache = { at: 0, value: null };
121
+ const LIVE_CACHE_MS = 2_000;
122
+
123
+ /**
124
+ * Count live `claude` CLI processes + their total RSS (MB) via `ps`. Cached
125
+ * for 2s so a burst of admit() calls doesn't fork ps repeatedly. Best-effort:
126
+ * on any failure returns { count:0, rssMB:0 } (fail-open-for-work).
127
+ *
128
+ * @returns {{ count:number, rssMB:number }}
129
+ */
130
+ export function liveClaudeStats(opts = {}) {
131
+ const now = opts.now || Date.now;
132
+ const t = now();
133
+ if (!opts.force && _liveCache.value && t - _liveCache.at < LIVE_CACHE_MS) {
134
+ return _liveCache.value;
135
+ }
136
+ let value = { count: 0, rssMB: 0 };
137
+ try {
138
+ // pid, rss(KB), comm — mirror the watchdog's filter set so the governor's
139
+ // view of "live claude" matches what the watchdog would act on.
140
+ const out = execFileSync("/bin/ps", ["-eo", "pid,rss,comm"], { encoding: "utf-8", timeout: 4000 });
141
+ let count = 0;
142
+ let rssKb = 0;
143
+ for (const line of out.split("\n")) {
144
+ const l = line.trim();
145
+ if (!l) continue;
146
+ if (!/claude/i.test(l)) continue;
147
+ if (/Claude\.app|Claude Helper|watchdog|grep|ps -eo/.test(l)) continue;
148
+ const parts = l.split(/\s+/);
149
+ const rss = Number(parts[1]);
150
+ if (Number.isFinite(rss)) rssKb += rss;
151
+ count += 1;
152
+ }
153
+ value = { count, rssMB: Math.round(rssKb / 1024) };
154
+ } catch {
155
+ value = { count: 0, rssMB: 0 };
156
+ }
157
+ _liveCache = { at: t, value };
158
+ return value;
159
+ }
160
+
161
+ /** Reset the live-stats cache (tests). */
162
+ export function _resetLiveCache() { _liveCache = { at: 0, value: null }; }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Default deps snapshot
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /**
169
+ * Build the real-world deps snapshot the governor reasons over. Tests bypass
170
+ * this entirely by passing their own `deps`.
171
+ */
172
+ export function defaultDeps(extra = {}) {
173
+ const live = liveClaudeStats();
174
+ const throttle = readThrottle(extra);
175
+ return {
176
+ freemem: os.freemem(),
177
+ totalmem: os.totalmem(),
178
+ loadavg: os.loadavg(),
179
+ cpus: os.cpus().length || 1,
180
+ liveClaude: live,
181
+ throttleCeiling: throttle.ceiling,
182
+ throttleReason: throttle.reason,
183
+ ...extra,
184
+ };
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // Dynamic ceiling
189
+ // ---------------------------------------------------------------------------
190
+
191
+ /**
192
+ * dynamicMax = clamp(floor(totalmem*RAM_FRACTION / PER_SESSION_MB), 1, min(HARD_MAX, cpus))
193
+ */
194
+ export function dynamicMax(deps) {
195
+ const totalmem = numField(deps && deps.totalmem, os.totalmem());
196
+ const cpus = Math.max(1, numField(deps && deps.cpus, os.cpus().length || 1));
197
+ const byRam = Math.floor((totalmem * RAM_FRACTION) / (PER_SESSION_MB * MB));
198
+ const upper = Math.min(HARD_MAX, cpus);
199
+ return clamp(byRam, MIN_MAX, Math.max(MIN_MAX, upper));
200
+ }
201
+
202
+ function clamp(v, lo, hi) { return Math.min(hi, Math.max(lo, v)); }
203
+ function numField(v, dflt) { const n = Number(v); return Number.isFinite(n) ? n : dflt; }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // admit()
207
+ // ---------------------------------------------------------------------------
208
+
209
+ const ADMIT = "ADMIT";
210
+ const QUEUE = "QUEUE";
211
+ const DEFER = "DEFER";
212
+
213
+ /**
214
+ * The admission decision.
215
+ *
216
+ * @param {object} req { source, priority, mode }
217
+ * source: "inbox" | "backlog" | "cadence" (default "backlog")
218
+ * priority: "critical"|"high"|"normal"|"low" (default "normal")
219
+ * mode: "essential-only" flips budget gating (else normal)
220
+ * @param {object} [deps] injected snapshot; defaults to defaultDeps()
221
+ * { freemem, totalmem, loadavg, cpus, liveClaude:{count,rssMB}, throttleCeiling }
222
+ * @returns {{ decision:"ADMIT"|"QUEUE"|"DEFER", reason:string, snapshot:object }}
223
+ */
224
+ export function admit(req = {}, deps) {
225
+ const d = deps || defaultDeps();
226
+ const source = req.source || "backlog";
227
+ const mode = req.mode || (req.essentialOnly ? "essential-only" : null);
228
+
229
+ const totalmem = numField(d.totalmem, os.totalmem());
230
+ const freemem = numField(d.freemem, os.freemem());
231
+ const cpus = Math.max(1, numField(d.cpus, os.cpus().length || 1));
232
+ const load1 = Array.isArray(d.loadavg) ? numField(d.loadavg[0], 0) : numField(d.loadavg, 0);
233
+ const live = d.liveClaude || { count: 0, rssMB: 0 };
234
+ const liveCount = numField(live.count, 0);
235
+ const liveRssMB = numField(live.rssMB, 0);
236
+ const throttleCeiling = numField(d.throttleCeiling, Infinity);
237
+
238
+ const dMax = dynamicMax(d);
239
+ const effectiveMax = Math.max(1, Math.min(dMax, throttleCeiling));
240
+
241
+ const snapshot = {
242
+ source,
243
+ mode: mode || "normal",
244
+ dynamicMax: dMax,
245
+ effectiveMax,
246
+ throttleCeiling: throttleCeiling === Infinity ? null : throttleCeiling,
247
+ liveCount,
248
+ liveRssMB,
249
+ freememMB: Math.round(freemem / MB),
250
+ totalmemMB: Math.round(totalmem / MB),
251
+ load1,
252
+ cpus,
253
+ };
254
+
255
+ const decide = (decision, reason) => ({ decision, reason, snapshot });
256
+
257
+ // (0) Budget essential-only: non-inbox work is deferred entirely. Inbox
258
+ // falls through to the normal pressure gating below (we keep replying).
259
+ if (mode === "essential-only" && (source === "backlog" || source === "cadence")) {
260
+ return decide(DEFER, "essential-only: daily budget cap reached; deferring non-inbox work");
261
+ }
262
+
263
+ // (1) Concurrency ceiling → QUEUE (work re-derivable from the queue).
264
+ if (liveCount >= effectiveMax) {
265
+ const why = throttleCeiling < dMax
266
+ ? `at throttled ceiling (${liveCount}/${effectiveMax}; soft-throttle active)`
267
+ : `at concurrency ceiling (${liveCount}/${effectiveMax})`;
268
+ return decide(QUEUE, why);
269
+ }
270
+
271
+ // (2) Free-memory floor → DEFER. Two guards: a percentage floor AND an
272
+ // absolute headroom of at least 2 sessions' worth of RAM.
273
+ const freememFloor = totalmem * FREEMEM_FLOOR_FRACTION;
274
+ const headroomBytes = 2 * PER_SESSION_MB * MB;
275
+ if (freemem < freememFloor || freemem < headroomBytes) {
276
+ return decide(DEFER, `low free memory (${snapshot.freememMB}MB free; need >=${Math.round(Math.max(freememFloor, headroomBytes) / MB)}MB)`);
277
+ }
278
+
279
+ // (3) Load average → DEFER. A box already thrashing shouldn't take more on.
280
+ if (load1 > cpus * LOAD_FACTOR) {
281
+ return decide(DEFER, `load too high (1m load ${load1.toFixed(2)} > ${(cpus * LOAD_FACTOR).toFixed(1)} = cpus*${LOAD_FACTOR})`);
282
+ }
283
+
284
+ // (4) Projected RSS → QUEUE. If admitting one more session would push the
285
+ // claude working set past the RAM fraction, wait for a slot rather than
286
+ // risk an OOM kill.
287
+ const projectedMB = liveRssMB + PER_SESSION_MB;
288
+ const rssCapMB = (totalmem * RAM_FRACTION) / MB;
289
+ if (projectedMB > rssCapMB) {
290
+ return decide(QUEUE, `projected RSS ${projectedMB}MB would exceed ${Math.round(rssCapMB)}MB (${Math.round(RAM_FRACTION * 100)}% of RAM)`);
291
+ }
292
+
293
+ // (5) Healthy.
294
+ return decide(ADMIT, "resources healthy");
295
+ }
296
+
297
+ export const DECISIONS = { ADMIT, QUEUE, DEFER };
@@ -0,0 +1,262 @@
1
+ /**
2
+ * resource-governor.test.mjs — node:test coverage for the admission gate.
3
+ *
4
+ * Every test injects a full `deps` snapshot so admit() is a pure, deterministic
5
+ * function — no real os.* reads, no ps fork. We exercise each decision branch
6
+ * (ADMIT / QUEUE / DEFER), the dynamicMax sizing across 8/16/32GB boxes, the
7
+ * throttle-ceiling clamp, and essential-only mode.
8
+ */
9
+
10
+ import { test } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { promises as fsp } from "node:fs";
13
+ import { writeFileSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+
17
+ import {
18
+ admit,
19
+ dynamicMax,
20
+ readThrottle,
21
+ PER_SESSION_MB,
22
+ RAM_FRACTION,
23
+ DECISIONS,
24
+ } from "./resource-governor.mjs";
25
+
26
+ const GB = 1024 * 1024 * 1024;
27
+ const MB = 1024 * 1024;
28
+
29
+ /**
30
+ * Build a healthy baseline deps snapshot for an N-GB / C-cpu box with `live`
31
+ * sessions already running and `liveRssMB` total. Override any field.
32
+ */
33
+ function deps(over = {}) {
34
+ const totalmem = over.totalmem ?? 16 * GB;
35
+ return {
36
+ totalmem,
37
+ freemem: over.freemem ?? totalmem * 0.5, // plenty free
38
+ loadavg: over.loadavg ?? [0.5, 0.5, 0.5],
39
+ cpus: over.cpus ?? 8,
40
+ liveClaude: over.liveClaude ?? { count: 0, rssMB: 0 },
41
+ throttleCeiling: over.throttleCeiling ?? Infinity,
42
+ ...over,
43
+ };
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // dynamicMax sizing
48
+ // ---------------------------------------------------------------------------
49
+
50
+ test("dynamicMax sizes by RAM, clamped to min(HARD_MAX, cpus)", () => {
51
+ // 8GB: floor(8*0.6/0.75) = floor(6.4) = 6, capped by cpus too.
52
+ assert.equal(dynamicMax({ totalmem: 8 * GB, cpus: 8 }), 6);
53
+ // 16GB: floor(16*0.6/0.75)=12 → clamp to min(10, cpus). cpus=8 → 8.
54
+ assert.equal(dynamicMax({ totalmem: 16 * GB, cpus: 8 }), 8);
55
+ // 16GB with 16 cpus → min(10,16)=10, byRam=12 → clamp to 10.
56
+ assert.equal(dynamicMax({ totalmem: 16 * GB, cpus: 16 }), 10);
57
+ // 32GB with 16 cpus: byRam=floor(32*0.6/0.75)=25 → clamp to min(10,16)=10.
58
+ assert.equal(dynamicMax({ totalmem: 32 * GB, cpus: 16 }), 10);
59
+ // Tiny box always yields at least 1.
60
+ assert.equal(dynamicMax({ totalmem: 1 * GB, cpus: 1 }), 1);
61
+ });
62
+
63
+ test("dynamicMax never returns 0 even on a starved box", () => {
64
+ assert.ok(dynamicMax({ totalmem: 512 * MB, cpus: 2 }) >= 1);
65
+ });
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // ADMIT
69
+ // ---------------------------------------------------------------------------
70
+
71
+ test("admit: healthy box with spare capacity → ADMIT", () => {
72
+ const r = admit({ source: "backlog" }, deps());
73
+ assert.equal(r.decision, DECISIONS.ADMIT);
74
+ assert.equal(r.snapshot.dynamicMax, 8);
75
+ assert.equal(r.snapshot.effectiveMax, 8);
76
+ });
77
+
78
+ test("admit: inbox on a healthy box → ADMIT", () => {
79
+ const r = admit({ source: "inbox", priority: "high" }, deps());
80
+ assert.equal(r.decision, DECISIONS.ADMIT);
81
+ });
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // QUEUE — concurrency ceiling
85
+ // ---------------------------------------------------------------------------
86
+
87
+ test("admit: at the concurrency ceiling → QUEUE", () => {
88
+ // effectiveMax = 8; 8 already live → QUEUE.
89
+ const r = admit({ source: "backlog" }, deps({ liveClaude: { count: 8, rssMB: 2000 } }));
90
+ assert.equal(r.decision, DECISIONS.QUEUE);
91
+ assert.match(r.reason, /concurrency ceiling/);
92
+ });
93
+
94
+ test("admit: throttle ceiling clamps effectiveMax below dynamicMax → QUEUE earlier", () => {
95
+ // dynamicMax 8 but soft-throttle pinned the ceiling to 3; 3 live → QUEUE.
96
+ const r = admit({ source: "backlog" }, deps({ throttleCeiling: 3, liveClaude: { count: 3, rssMB: 1000 } }));
97
+ assert.equal(r.decision, DECISIONS.QUEUE);
98
+ assert.equal(r.snapshot.effectiveMax, 3);
99
+ assert.match(r.reason, /soft-throttle/);
100
+ });
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // QUEUE — projected RSS
104
+ // ---------------------------------------------------------------------------
105
+
106
+ test("admit: projected RSS over the RAM fraction → QUEUE", () => {
107
+ // 16GB → RAM fraction cap = 16*0.6*1024 ≈ 9830MB. Live RSS 9500MB + 750 > cap.
108
+ // Keep count below ceiling and memory/load healthy so RSS is the trigger.
109
+ const r = admit({ source: "backlog" }, deps({
110
+ liveClaude: { count: 2, rssMB: 9500 },
111
+ freemem: 16 * GB * 0.5,
112
+ }));
113
+ assert.equal(r.decision, DECISIONS.QUEUE);
114
+ assert.match(r.reason, /projected RSS/);
115
+ });
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // DEFER — free memory
119
+ // ---------------------------------------------------------------------------
120
+
121
+ test("admit: free memory below the 15% floor → DEFER", () => {
122
+ const r = admit({ source: "backlog" }, deps({ freemem: 16 * GB * 0.10 }));
123
+ assert.equal(r.decision, DECISIONS.DEFER);
124
+ assert.match(r.reason, /low free memory/);
125
+ });
126
+
127
+ test("admit: free memory below the 2-session absolute headroom → DEFER", () => {
128
+ // Big box where 15% is generous, but absolute free < 2*750MB = 1500MB.
129
+ const r = admit({ source: "backlog" }, deps({ totalmem: 64 * GB, freemem: 1000 * MB }));
130
+ assert.equal(r.decision, DECISIONS.DEFER);
131
+ assert.match(r.reason, /low free memory/);
132
+ });
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // DEFER — load average
136
+ // ---------------------------------------------------------------------------
137
+
138
+ test("admit: 1-minute load above cpus*1.5 → DEFER", () => {
139
+ // cpus=8 → threshold 12. load 13 → DEFER. Keep memory healthy.
140
+ const r = admit({ source: "backlog" }, deps({ loadavg: [13, 5, 5] }));
141
+ assert.equal(r.decision, DECISIONS.DEFER);
142
+ assert.match(r.reason, /load too high/);
143
+ });
144
+
145
+ test("admit: load exactly at the threshold is still ADMIT (strict >)", () => {
146
+ const r = admit({ source: "backlog" }, deps({ loadavg: [12, 1, 1], cpus: 8 }));
147
+ assert.equal(r.decision, DECISIONS.ADMIT);
148
+ });
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // essential-only mode
152
+ // ---------------------------------------------------------------------------
153
+
154
+ test("essential-only: backlog → DEFER without burning resources", () => {
155
+ const r = admit({ source: "backlog", mode: "essential-only" }, deps());
156
+ assert.equal(r.decision, DECISIONS.DEFER);
157
+ assert.match(r.reason, /essential-only/);
158
+ });
159
+
160
+ test("essential-only: cadence → DEFER", () => {
161
+ const r = admit({ source: "cadence", mode: "essential-only" }, deps());
162
+ assert.equal(r.decision, DECISIONS.DEFER);
163
+ });
164
+
165
+ test("essential-only: inbox still flows through normal gating (ADMIT when healthy)", () => {
166
+ const r = admit({ source: "inbox", mode: "essential-only" }, deps());
167
+ assert.equal(r.decision, DECISIONS.ADMIT, "inbox must keep replying even at budget cap");
168
+ });
169
+
170
+ test("essential-only: inbox still QUEUEs (not DEFERs) under concurrency pressure", () => {
171
+ const r = admit({ source: "inbox", mode: "essential-only" }, deps({ liveClaude: { count: 8, rssMB: 2000 } }));
172
+ assert.equal(r.decision, DECISIONS.QUEUE);
173
+ });
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // precedence — ceiling QUEUE beats memory DEFER? No: budget→ceiling→mem→load→rss.
177
+ // Verify the documented order is honoured.
178
+ // ---------------------------------------------------------------------------
179
+
180
+ test("decision precedence: essential-only beats everything for backlog", () => {
181
+ // Even with a healthy box, essential-only short-circuits to DEFER.
182
+ const r = admit({ source: "backlog", mode: "essential-only" }, deps({ liveClaude: { count: 0, rssMB: 0 } }));
183
+ assert.equal(r.decision, DECISIONS.DEFER);
184
+ });
185
+
186
+ test("decision precedence: concurrency QUEUE is checked before memory DEFER", () => {
187
+ // At ceiling AND low memory → spec orders ceiling first → QUEUE.
188
+ const r = admit({ source: "backlog" }, deps({
189
+ liveClaude: { count: 8, rssMB: 1000 },
190
+ freemem: 16 * GB * 0.05,
191
+ }));
192
+ assert.equal(r.decision, DECISIONS.QUEUE);
193
+ });
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // readThrottle — auto-expiry
197
+ // ---------------------------------------------------------------------------
198
+
199
+ test("readThrottle returns Infinity when no file present", async () => {
200
+ const dir = join(tmpdir(), `gov-throttle-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
201
+ await fsp.mkdir(join(dir, "state"), { recursive: true });
202
+ try {
203
+ assert.equal(readThrottle({ agentRoot: dir }).ceiling, Infinity);
204
+ } finally { await fsp.rm(dir, { recursive: true, force: true }); }
205
+ });
206
+
207
+ test("readThrottle honours ceiling while unexpired, ignores once expired", async () => {
208
+ const dir = join(tmpdir(), `gov-throttle2-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
209
+ await fsp.mkdir(join(dir, "state"), { recursive: true });
210
+ try {
211
+ const now = 1_000_000;
212
+ writeFileSync(join(dir, "state", "throttle.json"), JSON.stringify({
213
+ ceiling: 2, reason: "SOFT", set_at: now - 1000, expires_at: now + 60_000,
214
+ }));
215
+ // Unexpired → ceiling honoured.
216
+ assert.equal(readThrottle({ agentRoot: dir, now: () => now }).ceiling, 2);
217
+ // After expiry → ignored.
218
+ assert.equal(readThrottle({ agentRoot: dir, now: () => now + 120_000 }).ceiling, Infinity);
219
+ } finally { await fsp.rm(dir, { recursive: true, force: true }); }
220
+ });
221
+
222
+ test("governor reads the EXACT throttle.json byte-shape the watchdog SOFT tier writes", async () => {
223
+ // This is the contract between scripts/watchdog/memory-watchdog.sh (writer)
224
+ // and the governor (reader). The watchdog writes ceiling + reason + EPOCH-MS
225
+ // set_at/expires_at. If either side drifts, throttling silently no-ops — so
226
+ // pin the shape here.
227
+ const dir = join(tmpdir(), `gov-wd-shape-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
228
+ await fsp.mkdir(join(dir, "state"), { recursive: true });
229
+ try {
230
+ const nowSec = 1_700_000_000; // arbitrary epoch seconds
231
+ const setMs = nowSec * 1000;
232
+ const expMs = (nowSec + 300) * 1000; // THROTTLE_TTL_SECONDS = 300
233
+ // Byte-for-byte the printf in write_throttle():
234
+ writeFileSync(join(dir, "state", "throttle.json"),
235
+ `{"ceiling":3,"reason":"soft-tier","set_at":${setMs},"expires_at":${expMs}}\n`);
236
+
237
+ // While unexpired → ceiling honoured.
238
+ const t1 = readThrottle({ agentRoot: dir, now: () => setMs + 1000 });
239
+ assert.equal(t1.ceiling, 3);
240
+ assert.equal(t1.reason, "soft-tier");
241
+ // After TTL → ignored (governor auto-expires even if the watchdog died).
242
+ const t2 = readThrottle({ agentRoot: dir, now: () => expMs + 1 });
243
+ assert.equal(t2.ceiling, Infinity);
244
+ } finally { await fsp.rm(dir, { recursive: true, force: true }); }
245
+ });
246
+
247
+ test("admit reads a real throttle.json from agentRoot and clamps", async () => {
248
+ const dir = join(tmpdir(), `gov-throttle3-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
249
+ await fsp.mkdir(join(dir, "state"), { recursive: true });
250
+ try {
251
+ const now = 2_000_000;
252
+ writeFileSync(join(dir, "state", "throttle.json"), JSON.stringify({
253
+ ceiling: 1, reason: "SOFT", expires_at: now + 60_000,
254
+ }));
255
+ // Build a deps snapshot that reads the throttle file: pass throttleCeiling
256
+ // from readThrottle so the integration is exercised end-to-end.
257
+ const t = readThrottle({ agentRoot: dir, now: () => now });
258
+ const r = admit({ source: "backlog" }, deps({ throttleCeiling: t.ceiling, liveClaude: { count: 1, rssMB: 500 } }));
259
+ assert.equal(r.snapshot.effectiveMax, 1);
260
+ assert.equal(r.decision, DECISIONS.QUEUE);
261
+ } finally { await fsp.rm(dir, { recursive: true, force: true }); }
262
+ });