@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,675 @@
1
+ /**
2
+ * dynamic-jobs.mjs — agent-created one-off and recurring scheduled jobs.
3
+ *
4
+ * Why this exists
5
+ * ---------------
6
+ * The cadence bus handles the FRAMEWORK's standard cadences (inbox-processor,
7
+ * morning brief, …). It does not give an agent the everyday-employee primitive:
8
+ * "remind X this Friday at 9am", "every weekday at 8am summarize the channel".
9
+ * gaps-product-quality P1-5 calls this out as the missing core: "no
10
+ * agent-created dynamic jobs (one-shot 'at'/interval/cron with per-job payload —
11
+ * the core 'remind X Friday 9am' primitive a fleet of employees needs)".
12
+ *
13
+ * This module owns that primitive end to end EXCEPT the firing wiring:
14
+ * - parseWhen(text) natural-ish "when" → {at} or {cron} (+ tz)
15
+ * - createJob/listJobs/cancelJob CRUD over state/scheduling/jobs.json
16
+ * - dueJobs(now) which jobs should fire right now
17
+ *
18
+ * The daemon / cadence consumer is the thing that calls dueJobs() on its timer
19
+ * and actually delivers each job's payload — that wiring is a deliberate
20
+ * follow-up, NOT in this lane. Everything here is pure logic over an injectable
21
+ * clock and an atomic JSON file, so it is fully unit-testable offline.
22
+ *
23
+ * Robustness contract (matches the cadence-bus house idiom)
24
+ * ---------------------------------------------------------
25
+ * - Atomic, durable writes via lib/fs-atomic.mjs (write-temp → fsync →
26
+ * rename), so a crash mid-write never corrupts jobs.json.
27
+ * - Fail-open reads: an unreadable/corrupt jobs.json reads as an empty store
28
+ * rather than throwing, so one bad byte never wedges every job.
29
+ * - Consume-time coalescing + TTL: after an outage, a recurring job that
30
+ * missed three morning windows fires ONCE on the next due-check (catch up to
31
+ * "now", do not replay a burst), and a one-off whose window is older than
32
+ * its TTL is skipped (and cancelled) rather than fired stale.
33
+ * - notBefore redelivery: dueJobs can mark a job "not before T" (e.g. the
34
+ * consumer deferred it under load); it stays invisible until T, so deferred
35
+ * jobs do not thrash in a claim→requeue loop.
36
+ *
37
+ * Node builtins + lib/fs-atomic only (maestro is near-zero-dep by design).
38
+ *
39
+ * @module lib/scheduling/dynamic-jobs
40
+ */
41
+
42
+ import { existsSync, readFileSync } from "node:fs";
43
+ import { join, resolve } from "node:path";
44
+ import { randomBytes } from "node:crypto";
45
+
46
+ import { writeJsonAtomic } from "../fs-atomic.mjs";
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Constants
50
+ // ---------------------------------------------------------------------------
51
+
52
+ export const JOBS_RELATIVE = "state/scheduling/jobs.json";
53
+
54
+ /**
55
+ * Default TTL for a one-off job whose fire window was missed (outage / daemon
56
+ * down). A one-off due more than this long ago is considered stale: skipped and
57
+ * cancelled rather than delivered late. 6h is long enough to survive a normal
58
+ * reboot/outage window but short enough that a "9am reminder" never surfaces at
59
+ * 6pm. Recurring jobs are never stale (they always re-target the next window) —
60
+ * the TTL only bounds the one-off / catch-up window.
61
+ */
62
+ export const DEFAULT_TTL_MS = 6 * 60 * 60 * 1000;
63
+
64
+ const DAY_MS = 24 * 60 * 60 * 1000;
65
+
66
+ const WEEKDAYS = [
67
+ "sunday",
68
+ "monday",
69
+ "tuesday",
70
+ "wednesday",
71
+ "thursday",
72
+ "friday",
73
+ "saturday",
74
+ ];
75
+ const WEEKDAY_INDEX = new Map(WEEKDAYS.map((d, i) => [d, i]));
76
+ // Common short forms.
77
+ for (const [k, v] of [
78
+ ["sun", 0], ["mon", 1], ["tue", 2], ["tues", 2], ["wed", 3],
79
+ ["thu", 4], ["thur", 4], ["thurs", 4], ["fri", 5], ["sat", 6],
80
+ ]) {
81
+ WEEKDAY_INDEX.set(k, v);
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Path resolution
86
+ // ---------------------------------------------------------------------------
87
+
88
+ /**
89
+ * Resolve the jobs.json path for an agent root. Mirrors cadence-bus path
90
+ * resolution: explicit arg, then AGENT_ROOT, then AGENT_DIR, then cwd.
91
+ * @param {string} [agentRoot]
92
+ * @returns {string} absolute path to state/scheduling/jobs.json
93
+ */
94
+ export function getJobsPath(agentRoot) {
95
+ const root = resolve(
96
+ agentRoot ||
97
+ process.env.AGENT_ROOT ||
98
+ process.env.AGENT_DIR ||
99
+ process.cwd()
100
+ );
101
+ return join(root, JOBS_RELATIVE);
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Store I/O (fail-open read, atomic durable write)
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Read the jobs store. Returns { jobs: [...] }. Fails open: a missing or
110
+ * corrupt file reads as an empty store rather than throwing — one bad byte must
111
+ * never wedge every scheduled job.
112
+ * @param {string} [agentRoot]
113
+ * @returns {{ jobs: object[] }}
114
+ */
115
+ export function readStore(agentRoot) {
116
+ const path = getJobsPath(agentRoot);
117
+ if (!existsSync(path)) return { jobs: [] };
118
+ try {
119
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
120
+ if (parsed && Array.isArray(parsed.jobs)) return { jobs: parsed.jobs };
121
+ return { jobs: [] };
122
+ } catch {
123
+ return { jobs: [] };
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Persist the jobs store atomically (write-temp → fsync → rename).
129
+ * @param {string} agentRoot
130
+ * @param {{ jobs: object[] }} store
131
+ * @returns {void}
132
+ */
133
+ function writeStore(agentRoot, store) {
134
+ writeJsonAtomic(getJobsPath(agentRoot), { jobs: store.jobs });
135
+ }
136
+
137
+ /**
138
+ * Generate a unique, sortable job id: job-<iso>-<hex>.
139
+ * @param {Date} [now]
140
+ * @returns {string}
141
+ */
142
+ export function nextJobId(now = new Date()) {
143
+ return `job-${now.toISOString()}-${randomBytes(4).toString("hex")}`;
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // parseWhen — natural-ish "when" → {at} | {cron}
148
+ // ---------------------------------------------------------------------------
149
+
150
+ /**
151
+ * Parse a clock phrase like "9am", "8:30am", "14:00", "9 am", "noon",
152
+ * "midnight" into {hour, minute} (24h). Returns null if no time is present —
153
+ * callers default to a sensible hour.
154
+ * @param {string} text
155
+ * @returns {{hour:number, minute:number}|null}
156
+ */
157
+ function parseClock(text) {
158
+ if (/\bnoon\b/.test(text)) return { hour: 12, minute: 0 };
159
+ if (/\bmidnight\b/.test(text)) return { hour: 0, minute: 0 };
160
+ // 9am | 9:30am | 9 am | 14:00 | 14h
161
+ const m = /\b(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/.exec(text);
162
+ if (!m) return null;
163
+ let hour = Number(m[1]);
164
+ const minute = m[2] ? Number(m[2]) : 0;
165
+ const ap = m[3];
166
+ if (ap === "pm" && hour < 12) hour += 12;
167
+ if (ap === "am" && hour === 12) hour = 0;
168
+ if (hour > 23 || minute > 59) return null;
169
+ return { hour, minute };
170
+ }
171
+
172
+ /**
173
+ * Find a weekday name in the text → index 0-6 (Sun=0), or null.
174
+ * @param {string} text
175
+ * @returns {number|null}
176
+ */
177
+ function parseWeekday(text) {
178
+ for (const [name, idx] of WEEKDAY_INDEX) {
179
+ if (new RegExp(`\\b${name}\\b`).test(text)) return idx;
180
+ }
181
+ return null;
182
+ }
183
+
184
+ /**
185
+ * Build a 5-field cron expression (minute hour dom month dow).
186
+ * @param {string} minute @param {string} hour @param {string} dow
187
+ * @returns {string}
188
+ */
189
+ function cron({ minute = "*", hour = "*", dom = "*", month = "*", dow = "*" }) {
190
+ return `${minute} ${hour} ${dom} ${month} ${dow}`;
191
+ }
192
+
193
+ /**
194
+ * Parse a natural-ish scheduling phrase into a concrete schedule descriptor.
195
+ *
196
+ * Returns one of:
197
+ * { at: <ms epoch>, tz } — a single future instant (one-off)
198
+ * { cron: "<m h dom mon dow>", tz } — a recurring schedule
199
+ *
200
+ * Recognised, in priority order:
201
+ * - "in 2 hours" / "in 30 minutes" / "in 3 days" → one-off relative {at}
202
+ * - "every weekday at 8am" → cron "0 8 * * 1-5"
203
+ * - "every day at 7am" / "daily 7am" → cron "0 7 * * *"
204
+ * - "every monday 9am" / "every fri 17:00" → cron "0 9 * * 1"
205
+ * - "tomorrow 9am" → one-off {at}
206
+ * - "today 5pm" / a bare time "9am" → one-off {at} (next
207
+ * such instant)
208
+ * - "friday 9am" / "next friday 9am" → one-off {at} (next
209
+ * occurrence of that
210
+ * weekday at that time)
211
+ *
212
+ * Times with no am/pm and hour < 8 are treated as 24h (so "14:00" works);
213
+ * a phrase with no recognised time defaults to 09:00 local.
214
+ *
215
+ * Timezone: `opts.tz` is recorded on the descriptor and used to resolve
216
+ * "9am" to the right wall-clock. Defaults to UTC. Returns null if nothing
217
+ * schedule-like is found (caller can ask the user to rephrase).
218
+ *
219
+ * @param {string} text
220
+ * @param {object} [opts]
221
+ * @param {Date|number} [opts.now=Date.now()] injectable clock
222
+ * @param {string} [opts.tz="UTC"] IANA tz for local-time resolution
223
+ * @returns {{at:number, tz:string}|{cron:string, tz:string}|null}
224
+ */
225
+ export function parseWhen(text, opts = {}) {
226
+ if (typeof text !== "string" || !text.trim()) return null;
227
+ const tz = opts.tz || "UTC";
228
+ const nowMs = opts.now instanceof Date ? opts.now.getTime()
229
+ : typeof opts.now === "number" ? opts.now
230
+ : Date.now();
231
+ const s = text.toLowerCase().trim();
232
+
233
+ // --- relative one-off: "in N unit(s)" ------------------------------------
234
+ const rel = /\bin\s+(\d+)\s*(second|sec|minute|min|hour|hr|day|week)s?\b/.exec(s);
235
+ if (rel) {
236
+ const n = Number(rel[1]);
237
+ const unit = rel[2];
238
+ const unitMs =
239
+ unit.startsWith("sec") ? 1000 :
240
+ unit.startsWith("min") ? 60_000 :
241
+ unit.startsWith("hour") || unit === "hr" ? 3_600_000 :
242
+ unit === "week" ? 7 * DAY_MS :
243
+ DAY_MS; // day
244
+ return { at: nowMs + n * unitMs, tz };
245
+ }
246
+
247
+ const clock = parseClock(s);
248
+ const time = clock || { hour: 9, minute: 0 };
249
+ const recurring = /\bevery\b|\bdaily\b|\beach\b/.test(s);
250
+
251
+ // --- recurring → cron ----------------------------------------------------
252
+ if (recurring) {
253
+ if (/\bweekday(s)?\b/.test(s)) {
254
+ return { cron: cron({ minute: String(time.minute), hour: String(time.hour), dow: "1-5" }), tz };
255
+ }
256
+ if (/\bweekend(s)?\b/.test(s)) {
257
+ return { cron: cron({ minute: String(time.minute), hour: String(time.hour), dow: "0,6" }), tz };
258
+ }
259
+ const wd = parseWeekday(s);
260
+ if (wd !== null) {
261
+ return { cron: cron({ minute: String(time.minute), hour: String(time.hour), dow: String(wd) }), tz };
262
+ }
263
+ // "every day" / "daily" / "every morning"
264
+ return { cron: cron({ minute: String(time.minute), hour: String(time.hour) }), tz };
265
+ }
266
+
267
+ // --- one-off absolute → {at} ---------------------------------------------
268
+ // "tomorrow", "today", a specific weekday, or a bare time (next occurrence).
269
+ const local = localPartsAt(nowMs, tz);
270
+ const wantWeekday = parseWeekday(s);
271
+
272
+ if (/\btomorrow\b/.test(s)) {
273
+ return { at: nextLocalInstant(nowMs, tz, time, { addDays: 1 }), tz };
274
+ }
275
+ if (/\btoday\b/.test(s)) {
276
+ // Today at the given time; if already past, this still resolves to today's
277
+ // instant (caller's TTL/coalescing decides whether to skip a stale one).
278
+ return { at: localInstant(local, time, tz), tz };
279
+ }
280
+ if (wantWeekday !== null) {
281
+ return { at: nextWeekdayInstant(nowMs, tz, wantWeekday, time), tz };
282
+ }
283
+ if (clock) {
284
+ // Bare time, e.g. "9am" → the next time it is 9am locally.
285
+ return { at: nextLocalInstant(nowMs, tz, time), tz };
286
+ }
287
+
288
+ return null;
289
+ }
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // Local-time arithmetic (tz-aware via Intl, no tz database dependency)
293
+ // ---------------------------------------------------------------------------
294
+
295
+ /**
296
+ * Decompose a ms instant into local wall-clock parts for `tz`.
297
+ * Fails open to UTC parts for an invalid tz.
298
+ * @returns {{year,month,day,hour,minute,second,weekday}}
299
+ */
300
+ function localPartsAt(ms, tz) {
301
+ let dtf;
302
+ try {
303
+ dtf = new Intl.DateTimeFormat("en-US", {
304
+ timeZone: tz,
305
+ hour12: false,
306
+ weekday: "short",
307
+ year: "numeric", month: "2-digit", day: "2-digit",
308
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
309
+ });
310
+ } catch {
311
+ const d = new Date(ms);
312
+ return {
313
+ year: d.getUTCFullYear(), month: d.getUTCMonth() + 1, day: d.getUTCDate(),
314
+ hour: d.getUTCHours(), minute: d.getUTCMinutes(), second: d.getUTCSeconds(),
315
+ weekday: d.getUTCDay(),
316
+ };
317
+ }
318
+ const p = {};
319
+ for (const part of dtf.formatToParts(new Date(ms))) {
320
+ if (part.type !== "literal") p[part.type] = part.value;
321
+ }
322
+ const hour = p.hour === "24" ? 0 : Number(p.hour);
323
+ const wdShort = String(p.weekday || "").toLowerCase().slice(0, 3);
324
+ return {
325
+ year: Number(p.year), month: Number(p.month), day: Number(p.day),
326
+ hour, minute: Number(p.minute), second: Number(p.second),
327
+ weekday: WEEKDAY_INDEX.has(wdShort) ? WEEKDAY_INDEX.get(wdShort) : 0,
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Given local Y/M/D parts and a target {hour,minute}, produce the ms-epoch
333
+ * instant of that local wall-clock time in `tz`. Resolves the tz offset AT the
334
+ * target so DST is handled. Fails open to UTC.
335
+ */
336
+ function localInstant(parts, time, tz) {
337
+ // First guess: interpret the wanted wall-clock as if UTC.
338
+ const guess = Date.UTC(parts.year, parts.month - 1, parts.day, time.hour, time.minute, 0);
339
+ // Correct by the tz offset at that guess (offset minutes: UTC + off = local).
340
+ const offMin = tzOffsetMinutes(tz, guess);
341
+ return guess - offMin * 60_000;
342
+ }
343
+
344
+ /**
345
+ * tz offset in minutes at instant `ms` (UTC + offset = local). Fail-open 0.
346
+ */
347
+ function tzOffsetMinutes(tz, ms) {
348
+ let dtf;
349
+ try {
350
+ dtf = new Intl.DateTimeFormat("en-US", {
351
+ timeZone: tz, hour12: false,
352
+ year: "numeric", month: "2-digit", day: "2-digit",
353
+ hour: "2-digit", minute: "2-digit", second: "2-digit",
354
+ });
355
+ } catch { return 0; }
356
+ const p = {};
357
+ for (const part of dtf.formatToParts(new Date(ms))) {
358
+ if (part.type !== "literal") p[part.type] = part.value;
359
+ }
360
+ const hour = p.hour === "24" ? 0 : Number(p.hour);
361
+ const asUtc = Date.UTC(Number(p.year), Number(p.month) - 1, Number(p.day), hour, Number(p.minute), Number(p.second));
362
+ return Math.round((asUtc - ms) / 60_000);
363
+ }
364
+
365
+ /**
366
+ * The next instant at local `time` in `tz`, strictly after `nowMs`, optionally
367
+ * forced forward by `addDays`. Used for "tomorrow 9am" and bare "9am".
368
+ */
369
+ function nextLocalInstant(nowMs, tz, time, { addDays = 0 } = {}) {
370
+ const base = localPartsAt(nowMs + addDays * DAY_MS, tz);
371
+ let inst = localInstant(base, time, tz);
372
+ // If we did not force a day and the computed instant is already past, roll to
373
+ // the next day.
374
+ while (inst <= nowMs) {
375
+ const next = localPartsAt(inst + DAY_MS, tz);
376
+ inst = localInstant(next, time, tz);
377
+ }
378
+ return inst;
379
+ }
380
+
381
+ /**
382
+ * The next instant whose local weekday is `targetWeekday` (Sun=0) at local
383
+ * `time` in `tz`, strictly after `nowMs`.
384
+ */
385
+ function nextWeekdayInstant(nowMs, tz, targetWeekday, time) {
386
+ const today = localPartsAt(nowMs, tz);
387
+ let delta = (targetWeekday - today.weekday + 7) % 7;
388
+ let candidate = localInstant(localPartsAt(nowMs + delta * DAY_MS, tz), time, tz);
389
+ // If it's today (delta 0) but already past, jump a full week.
390
+ if (candidate <= nowMs) {
391
+ delta += 7;
392
+ candidate = localInstant(localPartsAt(nowMs + delta * DAY_MS, tz), time, tz);
393
+ }
394
+ return candidate;
395
+ }
396
+
397
+ // ---------------------------------------------------------------------------
398
+ // cron → next fire time
399
+ // ---------------------------------------------------------------------------
400
+
401
+ /**
402
+ * Parse one cron field into a membership predicate over [min,max]. Supports
403
+ * "*", a list "a,b", a range "a-b", and a step "*\/n" / "a-b/n". This is a
404
+ * deliberately small subset — enough for the schedules parseWhen emits.
405
+ * @returns {(v:number)=>boolean}
406
+ */
407
+ function cronFieldMatcher(field, min, max) {
408
+ if (field === "*") return () => true;
409
+ const allowed = new Set();
410
+ for (const part of field.split(",")) {
411
+ const [rangePart, stepPart] = part.split("/");
412
+ const step = stepPart ? Number(stepPart) : 1;
413
+ let lo = min;
414
+ let hi = max;
415
+ if (rangePart !== "*") {
416
+ const r = rangePart.split("-");
417
+ lo = Number(r[0]);
418
+ hi = r.length > 1 ? Number(r[1]) : Number(r[0]);
419
+ }
420
+ for (let v = lo; v <= hi; v += step) allowed.add(v);
421
+ }
422
+ return (v) => allowed.has(v);
423
+ }
424
+
425
+ /**
426
+ * Compute the next instant (ms epoch) strictly after `afterMs` that matches a
427
+ * 5-field cron expression, evaluated in `tz`. Minute-resolution scan bounded to
428
+ * ~400 days (covers monthly/weekday schedules). Returns null if no match in the
429
+ * horizon (malformed cron) — fail-open: caller treats null as "not due".
430
+ *
431
+ * Exported for testing and for the consumer's next-fire bookkeeping.
432
+ *
433
+ * @param {string} expr "m h dom mon dow"
434
+ * @param {number} afterMs
435
+ * @param {string} [tz="UTC"]
436
+ * @returns {number|null}
437
+ */
438
+ export function nextCronAfter(expr, afterMs, tz = "UTC") {
439
+ const fields = String(expr).trim().split(/\s+/);
440
+ if (fields.length !== 5) return null;
441
+ const [minF, hourF, domF, monF, dowF] = fields;
442
+ let mMin, mHour, mDom, mMon, mDow;
443
+ try {
444
+ mMin = cronFieldMatcher(minF, 0, 59);
445
+ mHour = cronFieldMatcher(hourF, 0, 23);
446
+ mDom = cronFieldMatcher(domF, 1, 31);
447
+ mMon = cronFieldMatcher(monF, 1, 12);
448
+ mDow = cronFieldMatcher(dowF, 0, 6);
449
+ } catch {
450
+ return null;
451
+ }
452
+ // Start at the next whole minute after afterMs.
453
+ let cursor = Math.floor(afterMs / 60_000) * 60_000 + 60_000;
454
+ const horizon = afterMs + 400 * DAY_MS;
455
+ while (cursor <= horizon) {
456
+ const lp = localPartsAt(cursor, tz);
457
+ // Standard cron dom/dow OR-semantics: if both restricted, either matches.
458
+ const domRestricted = domF !== "*";
459
+ const dowRestricted = dowF !== "*";
460
+ let dayOk;
461
+ if (domRestricted && dowRestricted) dayOk = mDom(lp.day) || mDow(lp.weekday);
462
+ else dayOk = mDom(lp.day) && mDow(lp.weekday);
463
+ if (mMin(lp.minute) && mHour(lp.hour) && mMon(lp.month) && dayOk) {
464
+ return cursor;
465
+ }
466
+ cursor += 60_000;
467
+ }
468
+ return null;
469
+ }
470
+
471
+ // ---------------------------------------------------------------------------
472
+ // CRUD
473
+ // ---------------------------------------------------------------------------
474
+
475
+ /**
476
+ * Create and persist a scheduled job.
477
+ *
478
+ * @param {object} input
479
+ * @param {string} [input.agentRoot]
480
+ * @param {{at:number,tz?:string}|{cron:string,tz?:string}} input.when
481
+ * a parseWhen() result (or equivalent). Required.
482
+ * @param {string} input.action handler/route the consumer will invoke
483
+ * (e.g. "reminder", "channel-summary"). Required.
484
+ * @param {object} [input.payload] free-form per-job payload handed to the action.
485
+ * @param {string} [input.tz] override the tz on `when`.
486
+ * @param {number} [input.ttlMs] one-off staleness window (default 6h).
487
+ * @param {Date|number} [input.now] injectable clock.
488
+ * @returns {object} the stored job record.
489
+ */
490
+ export function createJob(input = {}) {
491
+ const { when, action } = input;
492
+ if (!when || typeof when !== "object" || (when.at == null && !when.cron)) {
493
+ throw new Error("createJob: `when` must be a {at} or {cron} descriptor");
494
+ }
495
+ if (!action || typeof action !== "string") {
496
+ throw new Error("createJob: `action` (string) is required");
497
+ }
498
+ const now = input.now instanceof Date ? input.now : new Date(typeof input.now === "number" ? input.now : Date.now());
499
+ const nowMs = now.getTime();
500
+ const tz = input.tz || when.tz || "UTC";
501
+
502
+ const job = {
503
+ id: nextJobId(now),
504
+ action,
505
+ payload: input.payload && typeof input.payload === "object" ? input.payload : {},
506
+ tz,
507
+ status: "active",
508
+ created_at: now.toISOString(),
509
+ ttl_ms: Number.isFinite(input.ttlMs) && input.ttlMs > 0 ? input.ttlMs : DEFAULT_TTL_MS,
510
+ };
511
+
512
+ if (when.cron) {
513
+ job.kind = "recurring";
514
+ job.cron = String(when.cron);
515
+ // Pre-compute the first fire so dueJobs has a concrete target.
516
+ job.next_run_at = nextCronAfter(job.cron, nowMs, tz);
517
+ } else {
518
+ job.kind = "once";
519
+ job.next_run_at = Number(when.at);
520
+ }
521
+
522
+ const store = readStore(input.agentRoot);
523
+ store.jobs.push(job);
524
+ writeStore(input.agentRoot, store);
525
+ return job;
526
+ }
527
+
528
+ /**
529
+ * List jobs. By default returns only active (non-cancelled, non-done) jobs;
530
+ * pass { all: true } to include cancelled/completed.
531
+ * @param {string} [agentRoot]
532
+ * @param {object} [opts] @param {boolean} [opts.all=false]
533
+ * @returns {object[]}
534
+ */
535
+ export function listJobs(agentRoot, opts = {}) {
536
+ const store = readStore(agentRoot);
537
+ if (opts.all) return store.jobs.slice();
538
+ return store.jobs.filter((j) => j.status === "active");
539
+ }
540
+
541
+ /**
542
+ * Cancel a job by id. Marks it cancelled (kept for audit) rather than deleting.
543
+ * Returns true if a matching active job was found and cancelled.
544
+ * @param {string} agentRoot
545
+ * @param {string} id
546
+ * @param {object} [opts] @param {Date|number} [opts.now]
547
+ * @returns {boolean}
548
+ */
549
+ export function cancelJob(agentRoot, id, opts = {}) {
550
+ const store = readStore(agentRoot);
551
+ const job = store.jobs.find((j) => j.id === id && j.status === "active");
552
+ if (!job) return false;
553
+ job.status = "cancelled";
554
+ job.cancelled_at = isoNow(opts.now);
555
+ writeStore(agentRoot, store);
556
+ return true;
557
+ }
558
+
559
+ /**
560
+ * Mark a job "do not fire before T". Used by the consumer to defer a job under
561
+ * load without losing it — it stays invisible to dueJobs until `notBeforeMs`,
562
+ * so deferred jobs do not thrash a claim→requeue loop. Returns true if updated.
563
+ * @param {string} agentRoot
564
+ * @param {string} id
565
+ * @param {number} notBeforeMs ms epoch the job becomes eligible again
566
+ * @returns {boolean}
567
+ */
568
+ export function deferJob(agentRoot, id, notBeforeMs) {
569
+ const store = readStore(agentRoot);
570
+ const job = store.jobs.find((j) => j.id === id && j.status === "active");
571
+ if (!job) return false;
572
+ job.not_before = Number(notBeforeMs);
573
+ writeStore(agentRoot, store);
574
+ return true;
575
+ }
576
+
577
+ function isoNow(now) {
578
+ if (now instanceof Date) return now.toISOString();
579
+ if (typeof now === "number") return new Date(now).toISOString();
580
+ return new Date().toISOString();
581
+ }
582
+
583
+ // ---------------------------------------------------------------------------
584
+ // dueJobs — the firing decision (coalescing + TTL + notBefore)
585
+ // ---------------------------------------------------------------------------
586
+
587
+ /**
588
+ * Determine which active jobs should fire at `now`, and ADVANCE the store to
589
+ * reflect the consume. This is the consume-time coalescing point:
590
+ *
591
+ * - A one-off whose `next_run_at <= now`:
592
+ * · if it is within its TTL → returned to fire, then marked done.
593
+ * · if it is older than its TTL (stale, e.g. an outage) → SKIPPED and
594
+ * cancelled (status "expired"), NOT fired late. No stale "9am reminder
595
+ * at 6pm".
596
+ * - A recurring job whose `next_run_at <= now`:
597
+ * · returned to fire ONCE, then `next_run_at` is fast-forwarded to the
598
+ * next future cron boundary after `now`. A three-day outage therefore
599
+ * fires the morning job ONCE on recovery and re-targets tomorrow — it
600
+ * never replays the whole missed burst.
601
+ * - A job with `not_before > now` is invisible (deferred redelivery).
602
+ *
603
+ * The write happens whether or not anything fired-and-changed, but only when
604
+ * something actually changed, to keep the common "nothing due" case write-free.
605
+ *
606
+ * @param {string} agentRoot
607
+ * @param {object} [opts]
608
+ * @param {Date|number} [opts.now=Date.now()] injectable clock
609
+ * @returns {object[]} the jobs that should fire now (each with its payload)
610
+ */
611
+ export function dueJobs(agentRoot, opts = {}) {
612
+ const nowMs = opts.now instanceof Date ? opts.now.getTime()
613
+ : typeof opts.now === "number" ? opts.now
614
+ : Date.now();
615
+
616
+ const store = readStore(agentRoot);
617
+ const fired = [];
618
+ let changed = false;
619
+
620
+ for (const job of store.jobs) {
621
+ if (job.status !== "active") continue;
622
+ if (Number.isFinite(job.not_before) && job.not_before > nowMs) continue;
623
+ const next = Number(job.next_run_at);
624
+ if (!Number.isFinite(next) || next > nowMs) continue;
625
+
626
+ // Once we fire (or expire) we clear any defer marker.
627
+ if (job.not_before != null) { delete job.not_before; changed = true; }
628
+
629
+ if (job.kind === "recurring") {
630
+ const ttl = Number.isFinite(job.ttl_ms) ? job.ttl_ms : DEFAULT_TTL_MS;
631
+ const tz = job.tz || "UTC";
632
+ // Coalesce: collapse every boundary in [next, now] into ONE fire, never
633
+ // a burst. After an outage there may be several missed windows (Thu/Fri/
634
+ // Sat 08:00); we represent that single catch-up by the MOST RECENT missed
635
+ // boundary, then re-target the next future one. Staleness is judged
636
+ // against that most-recent boundary — so a 3-day outage whose last window
637
+ // was an hour ago still fires once, while a job whose only missed window
638
+ // is older than the TTL is silently skipped (and re-targeted).
639
+ let lastMissed = next;
640
+ let advanced = nextCronAfter(job.cron, next, tz);
641
+ while (advanced != null && advanced <= nowMs) {
642
+ lastMissed = advanced;
643
+ advanced = nextCronAfter(job.cron, advanced, tz);
644
+ }
645
+ if (nowMs - lastMissed <= ttl) {
646
+ fired.push(job);
647
+ }
648
+ if (advanced != null) {
649
+ job.next_run_at = advanced; // next future boundary after now
650
+ } else {
651
+ // Malformed cron or past horizon: stop the job rather than spin.
652
+ job.status = "expired";
653
+ job.expired_at = new Date(nowMs).toISOString();
654
+ }
655
+ job.last_fired_at = new Date(nowMs).toISOString();
656
+ changed = true;
657
+ } else {
658
+ // one-off
659
+ const ttl = Number.isFinite(job.ttl_ms) ? job.ttl_ms : DEFAULT_TTL_MS;
660
+ if (nowMs - next <= ttl) {
661
+ fired.push(job);
662
+ job.status = "done";
663
+ job.fired_at = new Date(nowMs).toISOString();
664
+ } else {
665
+ // Stale: window long past. Expire rather than deliver late.
666
+ job.status = "expired";
667
+ job.expired_at = new Date(nowMs).toISOString();
668
+ }
669
+ changed = true;
670
+ }
671
+ }
672
+
673
+ if (changed) writeStore(agentRoot, store);
674
+ return fired;
675
+ }