@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,279 @@
1
+ /**
2
+ * lib/budget-guard.mjs — daily spend cap + essential-only mode (WS4).
3
+ *
4
+ * Sums today's per-session cost rows from the existing ledger
5
+ * (`state/cost-tracking/<date>.jsonl`, written by
6
+ * scripts/cost/track-claude-usage.mjs) and reports where the day sits against
7
+ * a configured cap. As spend crosses 50 / 75 / 90 %, a single escalating
8
+ * notice is emitted PER BAND PER DAY (tracked in
9
+ * `state/cost-tracking/<date>.budget.json` so we never spam). At 100 % the
10
+ * governor flips to `essentialOnly`: backlog + cadence-escalation work is
11
+ * DEFERRED, but inbox DMs are still answered (Invariant #1 — never brick; we
12
+ * degrade, we don't go dark on the user).
13
+ *
14
+ * Config knobs (env-overridable; tests inject via deps):
15
+ * DAILY_SPEND_CAP_USD hard cap in USD (default 25)
16
+ * DAILY_ITERATION_CAP optional cap on session count (default 0 = off)
17
+ *
18
+ * The window resets at UTC midnight automatically — the ledger + budget files
19
+ * are date-stamped, so "today" is simply the current UTC date.
20
+ *
21
+ * Constraints (CLAUDE.md): ESM, Node built-ins only, injectable clock +
22
+ * ledger path so tests are hermetic. Never throws on I/O failure: a guard
23
+ * that can't read the ledger reports band 0 (essentialOnly:false) — degrade
24
+ * toward keeping work flowing, not toward bricking.
25
+ */
26
+
27
+ import {
28
+ existsSync,
29
+ mkdirSync,
30
+ readFileSync,
31
+ writeFileSync,
32
+ renameSync,
33
+ unlinkSync,
34
+ } from "node:fs";
35
+ import { join, resolve, dirname } from "node:path";
36
+ import { randomBytes } from "node:crypto";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Config
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export const DEFAULT_DAILY_SPEND_CAP_USD = 25;
43
+ export const DEFAULT_DAILY_ITERATION_CAP = 0; // 0 = no session-count cap
44
+
45
+ /**
46
+ * Read the daily spend cap from config/recovery.yaml, if present. We extract the
47
+ * single scalar we need with a targeted regex rather than pulling in a YAML
48
+ * parser — this keeps the hot-path guard zero-dep and never-throwing (per the
49
+ * module contract), matching how lib/setup/sections/recovery.mjs line-edits the
50
+ * same key. Accepts the documented snake_case key under `budget:`
51
+ * (`daily_spend_cap_usd`) and the camelCase aliases the audit referenced
52
+ * (`dailySpendUSD` / `dailySpendCapUsd`), at top level or nested. Returns a
53
+ * finite positive number, or null when the file is absent/unset/malformed.
54
+ */
55
+ function recoveryYamlCap(deps) {
56
+ const root =
57
+ (deps && deps.recoveryConfigPath) ||
58
+ join(
59
+ resolve(
60
+ (deps && deps.agentRoot) ||
61
+ process.env.AGENT_ROOT ||
62
+ process.env.AGENT_DIR ||
63
+ process.cwd()
64
+ ),
65
+ "config",
66
+ "recovery.yaml"
67
+ );
68
+ let body;
69
+ try {
70
+ if (!existsSync(root)) return null;
71
+ body = readFileSync(root, "utf-8");
72
+ } catch {
73
+ return null;
74
+ }
75
+ // Match any of the accepted keys followed by a numeric value (strip an inline
76
+ // `# comment`). Order is irrelevant; the first matching key wins.
77
+ const m = body.match(
78
+ /^\s*(?:daily_spend_cap_usd|dailySpendCapUsd|dailySpendUSD)\s*:\s*([0-9]+(?:\.[0-9]+)?)/m
79
+ );
80
+ if (!m) return null;
81
+ const v = Number(m[1]);
82
+ return Number.isFinite(v) && v > 0 ? v : null;
83
+ }
84
+
85
+ function cfgSpendCap(deps) {
86
+ if (deps && Number.isFinite(deps.capUSD)) return deps.capUSD;
87
+ // Prefer the operator's config/recovery.yaml cap over the env / default so the
88
+ // documented per-agent override is actually honoured at runtime (recovery C1).
89
+ const fromYaml = recoveryYamlCap(deps);
90
+ if (Number.isFinite(fromYaml) && fromYaml > 0) return fromYaml;
91
+ const env = Number(process.env.DAILY_SPEND_CAP_USD);
92
+ return Number.isFinite(env) && env > 0 ? env : DEFAULT_DAILY_SPEND_CAP_USD;
93
+ }
94
+ function cfgIterationCap(deps) {
95
+ if (deps && Number.isFinite(deps.iterationCap)) return deps.iterationCap;
96
+ const env = Number(process.env.DAILY_ITERATION_CAP);
97
+ return Number.isFinite(env) && env >= 0 ? env : DEFAULT_DAILY_ITERATION_CAP;
98
+ }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Paths + clock
102
+ // ---------------------------------------------------------------------------
103
+
104
+ function clock(deps) {
105
+ return deps && typeof deps.now === "function" ? deps.now : Date.now;
106
+ }
107
+
108
+ function todayUtc(deps) {
109
+ return new Date(clock(deps)()).toISOString().slice(0, 10);
110
+ }
111
+
112
+ function ledgerDir(deps) {
113
+ if (deps && deps.ledgerDir) return resolve(deps.ledgerDir);
114
+ const root =
115
+ (deps && deps.agentRoot) ||
116
+ process.env.AGENT_ROOT ||
117
+ process.env.AGENT_DIR ||
118
+ process.cwd();
119
+ return join(resolve(root), "state", "cost-tracking");
120
+ }
121
+
122
+ function ledgerFile(deps) {
123
+ return join(ledgerDir(deps), `${todayUtc(deps)}.jsonl`);
124
+ }
125
+
126
+ function budgetStateFile(deps) {
127
+ return join(ledgerDir(deps), `${todayUtc(deps)}.budget.json`);
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Banding
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /** Bands are the integer thresholds we notify at. 0 = below 50 %. */
135
+ export const BANDS = [50, 75, 90, 100];
136
+
137
+ function bandForPct(pct) {
138
+ if (pct >= 100) return 100;
139
+ if (pct >= 90) return 90;
140
+ if (pct >= 75) return 75;
141
+ if (pct >= 50) return 50;
142
+ return 0;
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Ledger sum
147
+ // ---------------------------------------------------------------------------
148
+
149
+ /**
150
+ * Sum today's spend + session count from the JSONL ledger. Never throws;
151
+ * malformed lines are skipped, a missing file yields zeroes.
152
+ */
153
+ function sumToday(deps) {
154
+ const file = ledgerFile(deps);
155
+ let spentUSD = 0;
156
+ let sessions = 0;
157
+ if (!existsSync(file)) return { spentUSD: 0, sessions: 0 };
158
+ let body;
159
+ try { body = readFileSync(file, "utf-8"); } catch { return { spentUSD: 0, sessions: 0 }; }
160
+ for (const line of body.split("\n")) {
161
+ if (!line.trim()) continue;
162
+ let row;
163
+ try { row = JSON.parse(line); } catch { continue; }
164
+ const usd = Number(row.estimated_usd);
165
+ if (Number.isFinite(usd)) spentUSD += usd;
166
+ sessions += 1;
167
+ }
168
+ return { spentUSD: +spentUSD.toFixed(6), sessions };
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Public API
173
+ // ---------------------------------------------------------------------------
174
+
175
+ /**
176
+ * Compute the day's budget status.
177
+ *
178
+ * @param {object} [deps] { now?, ledgerDir?, agentRoot?, capUSD?, iterationCap? }
179
+ * @returns {{
180
+ * spentUSD:number, capUSD:number, pct:number, band:0|50|75|90|100,
181
+ * essentialOnly:boolean, sessions:number, iterationCap:number, date:string
182
+ * }}
183
+ */
184
+ export function dailyStatus(deps) {
185
+ const capUSD = cfgSpendCap(deps);
186
+ const iterationCap = cfgIterationCap(deps);
187
+ const { spentUSD, sessions } = sumToday(deps);
188
+
189
+ const spendPct = capUSD > 0 ? (spentUSD / capUSD) * 100 : 0;
190
+ const iterPct = iterationCap > 0 ? (sessions / iterationCap) * 100 : 0;
191
+ // The binding constraint is whichever is closer to the cap.
192
+ const pct = Math.max(spendPct, iterPct);
193
+ const band = bandForPct(pct);
194
+
195
+ return {
196
+ spentUSD,
197
+ capUSD,
198
+ pct: +pct.toFixed(2),
199
+ band,
200
+ essentialOnly: band >= 100,
201
+ sessions,
202
+ iterationCap,
203
+ date: todayUtc(deps),
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Read the per-day notification ledger ({ notified_bands: number[] }). Never
209
+ * throws.
210
+ */
211
+ export function readNotified(deps) {
212
+ const p = budgetStateFile(deps);
213
+ if (!existsSync(p)) return { notified_bands: [] };
214
+ try {
215
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
216
+ const arr = Array.isArray(raw.notified_bands) ? raw.notified_bands.filter((n) => BANDS.includes(n)) : [];
217
+ return { notified_bands: arr };
218
+ } catch {
219
+ return { notified_bands: [] };
220
+ }
221
+ }
222
+
223
+ function writeNotified(state, deps) {
224
+ const p = budgetStateFile(deps);
225
+ try {
226
+ mkdirSync(dirname(p), { recursive: true });
227
+ const tmp = `${p}.tmp.${process.pid}.${Date.now()}.${randomBytes(2).toString("hex")}`;
228
+ writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
229
+ try { renameSync(tmp, p); }
230
+ catch (err) { try { unlinkSync(tmp); } catch { /* */ } throw err; }
231
+ } catch {
232
+ /* best-effort */
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Emit AT MOST ONE notice per band per day. If the current band is above any
238
+ * not-yet-notified band threshold(s), returns the highest such band as the
239
+ * notice to surface and records every crossed band as notified (so a jump
240
+ * from 40 %→95 % notifies once at 90, not three times). When `notify` is
241
+ * provided it is invoked with a structured payload; otherwise the caller can
242
+ * use the return value.
243
+ *
244
+ * @param {object} [deps] { now?, ledgerDir?, capUSD?, notify? }
245
+ * @returns {{ notified:boolean, band:0|50|75|90|100, status:object, message?:string }}
246
+ */
247
+ export function maybeNotify(deps) {
248
+ const status = dailyStatus(deps);
249
+ if (status.band === 0) return { notified: false, band: 0, status };
250
+
251
+ const { notified_bands } = readNotified(deps);
252
+ const already = new Set(notified_bands);
253
+
254
+ // Every band threshold at or below the current band that hasn't fired yet.
255
+ const crossed = BANDS.filter((b) => status.band >= b && !already.has(b));
256
+ if (crossed.length === 0) {
257
+ return { notified: false, band: status.band, status };
258
+ }
259
+
260
+ // Mark them all notified; surface ONE notice at the highest crossed band.
261
+ const topBand = crossed[crossed.length - 1];
262
+ writeNotified({ notified_bands: [...notified_bands, ...crossed].sort((a, b) => a - b) }, deps);
263
+
264
+ const message = formatNotice(topBand, status);
265
+ const notifyFn = deps && typeof deps.notify === "function" ? deps.notify : null;
266
+ if (notifyFn) {
267
+ try { notifyFn({ band: topBand, status, message }); } catch { /* notice is best-effort */ }
268
+ }
269
+ return { notified: true, band: topBand, status, message };
270
+ }
271
+
272
+ function formatNotice(band, status) {
273
+ const usd = status.spentUSD.toFixed(2);
274
+ const cap = status.capUSD.toFixed(2);
275
+ if (band >= 100) {
276
+ return `Daily spend cap reached: $${usd} / $${cap} (${status.pct}%). Entering essential-only mode — inbox replies continue; backlog + scheduled work deferred until UTC midnight.`;
277
+ }
278
+ return `Daily spend at ${band}%: $${usd} / $${cap} (${status.pct}%).`;
279
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * budget-guard.test.mjs — node:test coverage for the daily spend cap.
3
+ *
4
+ * Hermetic: each test builds an isolated ledger dir with hand-written JSONL
5
+ * rows and injects a fixed clock + cap so band math is deterministic.
6
+ */
7
+
8
+ import { test } from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { promises as fsp } from "node:fs";
11
+ import { writeFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ import {
16
+ dailyStatus,
17
+ maybeNotify,
18
+ readNotified,
19
+ BANDS,
20
+ DEFAULT_DAILY_SPEND_CAP_USD,
21
+ } from "./budget-guard.mjs";
22
+
23
+ const FIXED = Date.UTC(2026, 5, 9, 12, 0, 0); // 2026-06-09T12:00:00Z
24
+ const DATE = "2026-06-09";
25
+ const clk = () => FIXED;
26
+
27
+ async function makeLedgerDir() {
28
+ const dir = join(tmpdir(), `budget-guard-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
29
+ await fsp.mkdir(dir, { recursive: true });
30
+ return dir;
31
+ }
32
+ async function rm(dir) { try { await fsp.rm(dir, { recursive: true, force: true }); } catch { /* */ } }
33
+
34
+ /** Write `rows` (array of {estimated_usd}) into today's ledger file. */
35
+ function seedLedger(dir, rows) {
36
+ const body = rows.map((r) => JSON.stringify({ ts: `${DATE}T10:00:00.000Z`, estimated_usd: r.usd, model: "sonnet", cadence: "x" })).join("\n") + "\n";
37
+ writeFileSync(join(dir, `${DATE}.jsonl`), body);
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // banding
42
+ // ---------------------------------------------------------------------------
43
+
44
+ test("dailyStatus: below 50% is band 0, not essential-only", async () => {
45
+ const dir = await makeLedgerDir();
46
+ try {
47
+ seedLedger(dir, [{ usd: 4 }]); // $4 of $10 = 40%
48
+ const s = dailyStatus({ ledgerDir: dir, now: clk, capUSD: 10 });
49
+ assert.equal(s.band, 0);
50
+ assert.equal(s.essentialOnly, false);
51
+ assert.equal(s.spentUSD, 4);
52
+ assert.equal(s.pct, 40);
53
+ } finally { await rm(dir); }
54
+ });
55
+
56
+ test("dailyStatus: crossing each threshold lands in the right band", async () => {
57
+ for (const [usd, expected] of [[5, 50], [7.5, 75], [9, 90], [10, 100], [12, 100]]) {
58
+ const dir = await makeLedgerDir();
59
+ try {
60
+ seedLedger(dir, [{ usd }]);
61
+ const s = dailyStatus({ ledgerDir: dir, now: clk, capUSD: 10 });
62
+ assert.equal(s.band, expected, `$${usd}/$10 should be band ${expected}, got ${s.band}`);
63
+ } finally { await rm(dir); }
64
+ }
65
+ });
66
+
67
+ test("dailyStatus: empty/missing ledger reports zero spend, band 0", async () => {
68
+ const dir = await makeLedgerDir();
69
+ try {
70
+ const s = dailyStatus({ ledgerDir: dir, now: clk, capUSD: 10 });
71
+ assert.equal(s.spentUSD, 0);
72
+ assert.equal(s.band, 0);
73
+ assert.equal(s.essentialOnly, false);
74
+ } finally { await rm(dir); }
75
+ });
76
+
77
+ test("dailyStatus uses the default cap when none configured", async () => {
78
+ const dir = await makeLedgerDir();
79
+ try {
80
+ seedLedger(dir, [{ usd: 1 }]);
81
+ const s = dailyStatus({ ledgerDir: dir, now: clk });
82
+ assert.equal(s.capUSD, DEFAULT_DAILY_SPEND_CAP_USD);
83
+ } finally { await rm(dir); }
84
+ });
85
+
86
+ test("dailyStatus: iteration cap can be the binding constraint", async () => {
87
+ const dir = await makeLedgerDir();
88
+ try {
89
+ // 10 cheap rows; spend tiny ($1 of $1000 = 0.1%) but 10/10 iterations = 100%.
90
+ seedLedger(dir, Array.from({ length: 10 }, () => ({ usd: 0.1 })));
91
+ const s = dailyStatus({ ledgerDir: dir, now: clk, capUSD: 1000, iterationCap: 10 });
92
+ assert.equal(s.sessions, 10);
93
+ assert.equal(s.band, 100, "iteration cap should drive band to 100");
94
+ assert.equal(s.essentialOnly, true);
95
+ } finally { await rm(dir); }
96
+ });
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // essential-only at 100%
100
+ // ---------------------------------------------------------------------------
101
+
102
+ test("essentialOnly is true only at/after 100%", async () => {
103
+ const dir = await makeLedgerDir();
104
+ try {
105
+ seedLedger(dir, [{ usd: 9.99 }]);
106
+ assert.equal(dailyStatus({ ledgerDir: dir, now: clk, capUSD: 10 }).essentialOnly, false);
107
+ seedLedger(dir, [{ usd: 10 }]);
108
+ assert.equal(dailyStatus({ ledgerDir: dir, now: clk, capUSD: 10 }).essentialOnly, true);
109
+ } finally { await rm(dir); }
110
+ });
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // one-notice-per-band
114
+ // ---------------------------------------------------------------------------
115
+
116
+ test("maybeNotify fires once per band and never repeats within the day", async () => {
117
+ const dir = await makeLedgerDir();
118
+ try {
119
+ const notices = [];
120
+ const notify = (n) => notices.push(n.band);
121
+
122
+ // 60% → notify band 50 once.
123
+ seedLedger(dir, [{ usd: 6 }]);
124
+ let r = maybeNotify({ ledgerDir: dir, now: clk, capUSD: 10, notify });
125
+ assert.equal(r.notified, true);
126
+ assert.equal(r.band, 50);
127
+
128
+ // Still 60% → no repeat.
129
+ r = maybeNotify({ ledgerDir: dir, now: clk, capUSD: 10, notify });
130
+ assert.equal(r.notified, false);
131
+
132
+ // Climb to 80% → notify band 75 once.
133
+ seedLedger(dir, [{ usd: 8 }]);
134
+ r = maybeNotify({ ledgerDir: dir, now: clk, capUSD: 10, notify });
135
+ assert.equal(r.notified, true);
136
+ assert.equal(r.band, 75);
137
+
138
+ assert.deepEqual(notices, [50, 75]);
139
+ assert.deepEqual(readNotified({ ledgerDir: dir, now: clk }).notified_bands, [50, 75]);
140
+ } finally { await rm(dir); }
141
+ });
142
+
143
+ test("maybeNotify on a big jump notifies once at the highest crossed band", async () => {
144
+ const dir = await makeLedgerDir();
145
+ try {
146
+ const notices = [];
147
+ // Jump straight from $0 to 95% — should surface ONE notice at band 90,
148
+ // and record 50/75/90 as all notified so they never re-fire.
149
+ seedLedger(dir, [{ usd: 9.5 }]);
150
+ const r = maybeNotify({ ledgerDir: dir, now: clk, capUSD: 10, notify: (n) => notices.push(n.band) });
151
+ assert.equal(r.notified, true);
152
+ assert.equal(r.band, 90);
153
+ assert.deepEqual(notices, [90], "exactly one notice surfaced");
154
+ assert.deepEqual(readNotified({ ledgerDir: dir, now: clk }).notified_bands, [50, 75, 90]);
155
+
156
+ // A later check at the same level does nothing.
157
+ const r2 = maybeNotify({ ledgerDir: dir, now: clk, capUSD: 10, notify: (n) => notices.push(n.band) });
158
+ assert.equal(r2.notified, false);
159
+ assert.deepEqual(notices, [90]);
160
+ } finally { await rm(dir); }
161
+ });
162
+
163
+ test("maybeNotify reaching 100% surfaces the essential-only notice once", async () => {
164
+ const dir = await makeLedgerDir();
165
+ try {
166
+ const notices = [];
167
+ seedLedger(dir, [{ usd: 11 }]); // 110%
168
+ const r = maybeNotify({ ledgerDir: dir, now: clk, capUSD: 10, notify: (n) => notices.push(n) });
169
+ assert.equal(r.notified, true);
170
+ assert.equal(r.band, 100);
171
+ assert.match(r.message, /essential-only/i);
172
+ assert.equal(r.status.essentialOnly, true);
173
+ // All bands recorded; a repeat is a no-op.
174
+ assert.deepEqual(readNotified({ ledgerDir: dir, now: clk }).notified_bands, [50, 75, 90, 100]);
175
+ const r2 = maybeNotify({ ledgerDir: dir, now: clk, capUSD: 10, notify: (n) => notices.push(n) });
176
+ assert.equal(r2.notified, false);
177
+ assert.equal(notices.length, 1);
178
+ } finally { await rm(dir); }
179
+ });
180
+
181
+ test("BANDS is the canonical 50/75/90/100 ladder", () => {
182
+ assert.deepEqual(BANDS, [50, 75, 90, 100]);
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // recovery.yaml cap is honoured (recovery C1)
187
+ // ---------------------------------------------------------------------------
188
+
189
+ /** Write a config/recovery.yaml under `agentRoot` with the given cap line. */
190
+ async function seedRecoveryYaml(agentRoot, body) {
191
+ await fsp.mkdir(join(agentRoot, "config"), { recursive: true });
192
+ writeFileSync(join(agentRoot, "config", "recovery.yaml"), body);
193
+ }
194
+
195
+ test("dailyStatus reads the cap from config/recovery.yaml when present", async () => {
196
+ const dir = await makeLedgerDir();
197
+ try {
198
+ // agentRoot holds both config/recovery.yaml and the ledger dir.
199
+ await fsp.mkdir(join(dir, "state", "cost-tracking"), { recursive: true });
200
+ await seedRecoveryYaml(dir, `budget:\n daily_spend_cap_usd: 40 # per-agent override\n`);
201
+ // Ledger lives where ledgerDir() derives it from agentRoot.
202
+ seedLedger(join(dir, "state", "cost-tracking"), [{ usd: 20 }]);
203
+ const s = dailyStatus({ agentRoot: dir, now: clk });
204
+ assert.equal(s.capUSD, 40, "cap should come from recovery.yaml, not the 25 default");
205
+ assert.equal(s.spentUSD, 20);
206
+ assert.equal(s.pct, 50, "20/40 = 50%");
207
+ assert.equal(s.band, 50);
208
+ } finally { await rm(dir); }
209
+ });
210
+
211
+ test("recovery.yaml cap takes precedence over DAILY_SPEND_CAP_USD env", async () => {
212
+ const dir = await makeLedgerDir();
213
+ const prevEnv = process.env.DAILY_SPEND_CAP_USD;
214
+ try {
215
+ process.env.DAILY_SPEND_CAP_USD = "100"; // would be the cap if yaml were ignored
216
+ await fsp.mkdir(join(dir, "state", "cost-tracking"), { recursive: true });
217
+ await seedRecoveryYaml(dir, `budget:\n daily_spend_cap_usd: 10\n`);
218
+ seedLedger(join(dir, "state", "cost-tracking"), [{ usd: 10 }]);
219
+ const s = dailyStatus({ agentRoot: dir, now: clk });
220
+ assert.equal(s.capUSD, 10, "yaml cap must win over the env cap");
221
+ assert.equal(s.band, 100, "10/10 spent under the yaml cap is 100% (essential-only)");
222
+ assert.equal(s.essentialOnly, true);
223
+ } finally {
224
+ if (prevEnv === undefined) delete process.env.DAILY_SPEND_CAP_USD;
225
+ else process.env.DAILY_SPEND_CAP_USD = prevEnv;
226
+ await rm(dir);
227
+ }
228
+ });
229
+
230
+ test("an explicit deps.capUSD still overrides the recovery.yaml cap", async () => {
231
+ const dir = await makeLedgerDir();
232
+ try {
233
+ await fsp.mkdir(join(dir, "state", "cost-tracking"), { recursive: true });
234
+ await seedRecoveryYaml(dir, `budget:\n daily_spend_cap_usd: 40\n`);
235
+ seedLedger(join(dir, "state", "cost-tracking"), [{ usd: 5 }]);
236
+ const s = dailyStatus({ agentRoot: dir, now: clk, capUSD: 10 });
237
+ assert.equal(s.capUSD, 10, "explicit injected cap wins (test seam preserved)");
238
+ } finally { await rm(dir); }
239
+ });
240
+
241
+ test("malformed / capless recovery.yaml falls back to the default cap", async () => {
242
+ const dir = await makeLedgerDir();
243
+ try {
244
+ await fsp.mkdir(join(dir, "state", "cost-tracking"), { recursive: true });
245
+ await seedRecoveryYaml(dir, `governor:\n hard_max: 10\n# no budget key here\n`);
246
+ seedLedger(join(dir, "state", "cost-tracking"), [{ usd: 1 }]);
247
+ const s = dailyStatus({ agentRoot: dir, now: clk });
248
+ assert.equal(s.capUSD, DEFAULT_DAILY_SPEND_CAP_USD);
249
+ } finally { await rm(dir); }
250
+ });
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // summing REAL ledger rows yields nonzero spend (the $0-forever bug guard)
254
+ // ---------------------------------------------------------------------------
255
+
256
+ test("summing real cost-ledger rows (with token-derived estimated_usd) yields nonzero spend", async () => {
257
+ const dir = await makeLedgerDir();
258
+ try {
259
+ // Rows shaped exactly like track-claude-usage.mjs#record emits them:
260
+ // estimated_usd is computed from token counts. A daemon that passed REAL
261
+ // counts produces nonzero rows; the old --input-tokens 0 bug produced 0.
262
+ const rows = [
263
+ // sonnet: (1500/1000*0.003)+(320/1000*0.015) = 0.0045+0.0048 = 0.0093
264
+ { ts: `${DATE}T10:00:00.000Z`, estimated_usd: 0.0093, model: "sonnet", cadence: "inbox-processor", input_tokens: 1500, output_tokens: 320 },
265
+ // opus: (5000/1000*0.015)+(900/1000*0.075) = 0.075+0.0675 = 0.1425
266
+ { ts: `${DATE}T11:00:00.000Z`, estimated_usd: 0.1425, model: "opus", cadence: "backlog-executor", input_tokens: 5000, output_tokens: 900 },
267
+ ];
268
+ writeFileSync(join(dir, `${DATE}.jsonl`), rows.map((r) => JSON.stringify(r)).join("\n") + "\n");
269
+ const s = dailyStatus({ ledgerDir: dir, now: clk, capUSD: 25 });
270
+ assert.ok(s.spentUSD > 0, `expected nonzero spend, got ${s.spentUSD}`);
271
+ assert.equal(s.spentUSD, +(0.0093 + 0.1425).toFixed(6));
272
+ assert.equal(s.sessions, 2);
273
+ } finally { await rm(dir); }
274
+ });
275
+
276
+ test("a ledger of only zero-token rows sums to $0 (proves the bug is the input, not the guard)", async () => {
277
+ const dir = await makeLedgerDir();
278
+ try {
279
+ // The pre-fix daemon wrote rows with input_tokens=0/output_tokens=0, so
280
+ // estimated_usd=0 — the guard reported $0 spend forever. This documents that
281
+ // the guard math is correct; the defect was the caller's fabricated zeros.
282
+ const rows = [
283
+ { ts: `${DATE}T10:00:00.000Z`, estimated_usd: 0, model: "sonnet", cadence: "x", input_tokens: 0, output_tokens: 0 },
284
+ { ts: `${DATE}T11:00:00.000Z`, estimated_usd: 0, model: "sonnet", cadence: "x", input_tokens: 0, output_tokens: 0 },
285
+ ];
286
+ writeFileSync(join(dir, `${DATE}.jsonl`), rows.map((r) => JSON.stringify(r)).join("\n") + "\n");
287
+ const s = dailyStatus({ ledgerDir: dir, now: clk, capUSD: 25 });
288
+ assert.equal(s.spentUSD, 0);
289
+ assert.equal(s.sessions, 2);
290
+ } finally { await rm(dir); }
291
+ });