@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.
- package/.claude/commands/init-agent.md +104 -0
- package/.claude/commands/init-maestro.md +1187 -0
- package/.claude/settings.json +161 -0
- package/.env.example +216 -0
- package/README.md +632 -0
- package/agents/browser-operator/agent.md +52 -0
- package/agents/calendar-ops/agent.md +50 -0
- package/agents/communications/agent.md +96 -0
- package/agents/decision-log/agent.md +65 -0
- package/agents/desktop-operator/agent.md +59 -0
- package/agents/gmail-operator/agent.md +62 -0
- package/agents/inbound-dispatcher/agent.md +66 -0
- package/agents/inbox-processor/agent.md +39 -0
- package/agents/pmo-execution/agent.md +60 -0
- package/agents/session-spawner/agent.md +64 -0
- package/agents/slack-operator/agent.md +60 -0
- package/agents/whatsapp-operator/agent.md +60 -0
- package/agents/workflow-automation/agent.md +61 -0
- package/archetypes/altitudes/c-suite.yaml +58 -0
- package/archetypes/altitudes/founder.yaml +68 -0
- package/archetypes/altitudes/senior-manager.yaml +63 -0
- package/archetypes/altitudes/svp.yaml +60 -0
- package/archetypes/altitudes/vp.yaml +50 -0
- package/archetypes/archetype.schema.json +77 -0
- package/archetypes/base.yaml +47 -0
- package/archetypes/capabilities/commercial-leader.yaml +159 -0
- package/archetypes/capabilities/compliance-officer.yaml +159 -0
- package/archetypes/capabilities/executive-operator.yaml +169 -0
- package/archetypes/capabilities/finance-leader.yaml +162 -0
- package/archetypes/capabilities/operations-leader.yaml +154 -0
- package/archetypes/capabilities/product-leader.yaml +148 -0
- package/archetypes/capabilities/technical-leader.yaml +146 -0
- package/archetypes/functions/commercial-leader.yaml +62 -0
- package/archetypes/functions/compliance-officer.yaml +64 -0
- package/archetypes/functions/executive-operator.yaml +70 -0
- package/archetypes/functions/finance-leader.yaml +70 -0
- package/archetypes/functions/operations-leader.yaml +62 -0
- package/archetypes/functions/product-leader.yaml +61 -0
- package/archetypes/functions/technical-leader.yaml +57 -0
- package/bin/cohort-mcp.mjs +81 -0
- package/bin/maestro.mjs +3516 -0
- package/bin/maestro.test.mjs +1015 -0
- package/desktop-control/README.md +56 -0
- package/desktop-control/app-profiles/gmail.yaml +120 -0
- package/desktop-control/app-profiles/slack.yaml +315 -0
- package/desktop-control/app-profiles/whatsapp.yaml +107 -0
- package/docs/architecture/agent-topology.md +2239 -0
- package/docs/architecture/archetype-agent-factory.md +110 -0
- package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
- package/docs/architecture/continuous-monitoring.md +221 -0
- package/docs/architecture/mcp-capability-map.md +585 -0
- package/docs/architecture/system-architecture.md +1272 -0
- package/docs/company-context/README.md +40 -0
- package/docs/guides/agent-persona-setup.md +600 -0
- package/docs/guides/agents-observe-setup.md +64 -0
- package/docs/guides/billing-console-keys.md +88 -0
- package/docs/guides/ccxray-diagnostics.md +65 -0
- package/docs/guides/channel-bus.md +127 -0
- package/docs/guides/claude-mem-setup.md +79 -0
- package/docs/guides/claude-pace-setup.md +56 -0
- package/docs/guides/claudraband-sessions.md +98 -0
- package/docs/guides/clawteam-swarm.md +116 -0
- package/docs/guides/code-review-graph-setup.md +86 -0
- package/docs/guides/email-setup.md +431 -0
- package/docs/guides/mac-mini.md +119 -0
- package/docs/guides/media-generation-setup.md +349 -0
- package/docs/guides/model-routing.md +162 -0
- package/docs/guides/observability-otel.md +265 -0
- package/docs/guides/org-onboarding.md +132 -0
- package/docs/guides/outbound-governance-setup.md +437 -0
- package/docs/guides/pdf-generation-setup.md +315 -0
- package/docs/guides/poller-daemon-setup.md +563 -0
- package/docs/guides/rag-context-setup.md +459 -0
- package/docs/guides/self-optimization-pattern.md +82 -0
- package/docs/guides/setup-wizard.md +178 -0
- package/docs/guides/slack-setup.md +350 -0
- package/docs/guides/telegram-setup.md +227 -0
- package/docs/guides/twilio-subaccounts-setup.md +223 -0
- package/docs/guides/verification.md +128 -0
- package/docs/guides/voice-mode.md +188 -0
- package/docs/guides/voice-sms-setup.md +698 -0
- package/docs/guides/webhook-relay-setup.md +349 -0
- package/docs/guides/whatsapp-setup.md +288 -0
- package/docs/prompts/board-pack-cover-template.md +36 -0
- package/docs/prompts/decision-recommendation-template.md +88 -0
- package/docs/prompts/followup-message-template.md +141 -0
- package/docs/prompts/investor-letter-template.md +52 -0
- package/docs/prompts/morning-brief-template.md +82 -0
- package/docs/prompts/presentation-template.md +58 -0
- package/docs/prompts/weekly-strategic-memo-template.md +104 -0
- package/docs/research/hallucinated-tool-output-investigation.md +151 -0
- package/docs/runbooks/backup-restore.md +205 -0
- package/docs/runbooks/cohort-cutover.md +129 -0
- package/docs/runbooks/fleet-operations.md +200 -0
- package/docs/runbooks/incident-response.md +226 -0
- package/docs/runbooks/mac-mini-bootstrap.md +431 -0
- package/docs/runbooks/perpetual-operations.md +509 -0
- package/docs/runbooks/recovery-and-failover.md +260 -0
- package/framework-features.json +267 -0
- package/ingest/README.md +87 -0
- package/lib/action-executor.js +689 -0
- package/lib/action-executor.test.mjs +871 -0
- package/lib/agent-root.mjs +37 -0
- package/lib/archetype.mjs +236 -0
- package/lib/archetype.test.mjs +132 -0
- package/lib/autonomy.mjs +114 -0
- package/lib/autonomy.test.mjs +66 -0
- package/lib/backlog.mjs +358 -0
- package/lib/backlog.test.mjs +266 -0
- package/lib/budget-guard.mjs +279 -0
- package/lib/budget-guard.test.mjs +291 -0
- package/lib/cadence-bus-schedule.test.mjs +194 -0
- package/lib/cadence-bus.mjs +1120 -0
- package/lib/cadence-bus.test.mjs +720 -0
- package/lib/cadences.mjs +205 -0
- package/lib/cadences.test.mjs +125 -0
- package/lib/capability.mjs +154 -0
- package/lib/capability.test.mjs +78 -0
- package/lib/channels/base-adapter.mjs +719 -0
- package/lib/channels/base-adapter.test.mjs +590 -0
- package/lib/channels/channel.mjs +128 -0
- package/lib/channels/channels.test.mjs +371 -0
- package/lib/channels/contract.mjs +215 -0
- package/lib/channels/contract.test.mjs +137 -0
- package/lib/channels/conversation-resolver.mjs +95 -0
- package/lib/channels/gmail/adapter.mjs +87 -0
- package/lib/channels/inbox-item.mjs +255 -0
- package/lib/channels/inbox-item.test.mjs +335 -0
- package/lib/channels/index.mjs +94 -0
- package/lib/channels/orgmail/adapter.mjs +353 -0
- package/lib/channels/orgmail/adapter.test.mjs +311 -0
- package/lib/channels/pairing.mjs +363 -0
- package/lib/channels/pairing.test.mjs +270 -0
- package/lib/channels/registry.mjs +164 -0
- package/lib/channels/slack/adapter.mjs +317 -0
- package/lib/channels/slack-adapter.test.mjs +212 -0
- package/lib/channels/sms/adapter.mjs +43 -0
- package/lib/channels/telegram/adapter.mjs +432 -0
- package/lib/channels/telegram-adapter.test.mjs +306 -0
- package/lib/channels/voice/adapter.mjs +301 -0
- package/lib/channels/voice/adapter.test.mjs +278 -0
- package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
- package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
- package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
- package/lib/charter.mjs +256 -0
- package/lib/charter.test.mjs +89 -0
- package/lib/claude-bin.mjs +134 -0
- package/lib/claude-bin.test.mjs +75 -0
- package/lib/collective/capture.mjs +185 -0
- package/lib/collective/capture.test.mjs +121 -0
- package/lib/collective/cards.mjs +201 -0
- package/lib/collective/cards.test.mjs +114 -0
- package/lib/collective/config.mjs +186 -0
- package/lib/collective/config.test.mjs +123 -0
- package/lib/collective/global-config.mjs +113 -0
- package/lib/collective/global-config.test.mjs +75 -0
- package/lib/collective/presence.mjs +201 -0
- package/lib/collective/presence.test.mjs +95 -0
- package/lib/collective/recall.mjs +215 -0
- package/lib/collective/recall.test.mjs +116 -0
- package/lib/comms/send-gate.mjs +554 -0
- package/lib/comms/send-gate.test.mjs +577 -0
- package/lib/comms.mjs +67 -0
- package/lib/comms.test.mjs +41 -0
- package/lib/diagnostics/alerts.mjs +424 -0
- package/lib/diagnostics/alerts.test.mjs +318 -0
- package/lib/diagnostics/backup-freshness.mjs +188 -0
- package/lib/diagnostics/backup-freshness.test.mjs +185 -0
- package/lib/diagnostics/counters.mjs +269 -0
- package/lib/diagnostics/counters.test.mjs +206 -0
- package/lib/diagnostics/events.mjs +188 -0
- package/lib/diagnostics/events.test.mjs +290 -0
- package/lib/diagnostics/otel.mjs +237 -0
- package/lib/diagnostics/otel.test.mjs +196 -0
- package/lib/diagnostics/trace.mjs +216 -0
- package/lib/diagnostics/trace.test.mjs +251 -0
- package/lib/env-compat.mjs +74 -0
- package/lib/env-compat.test.mjs +104 -0
- package/lib/feature-init.mjs +331 -0
- package/lib/fs-atomic.mjs +112 -0
- package/lib/fs-atomic.test.mjs +72 -0
- package/lib/fs-ownership.mjs +111 -0
- package/lib/fs-ownership.test.mjs +158 -0
- package/lib/hooks/bus.mjs +347 -0
- package/lib/hooks/bus.test.mjs +387 -0
- package/lib/index.js +16 -0
- package/lib/learning/config.mjs +106 -0
- package/lib/learning/config.test.mjs +75 -0
- package/lib/learning/counters.mjs +156 -0
- package/lib/learning/counters.test.mjs +69 -0
- package/lib/learning/curator-consolidate.test.mjs +238 -0
- package/lib/learning/curator.mjs +453 -0
- package/lib/learning/curator.test.mjs +106 -0
- package/lib/learning/log.mjs +40 -0
- package/lib/learning/reflect.mjs +534 -0
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.mjs +352 -0
- package/lib/learning/session-index.test.mjs +125 -0
- package/lib/learning/skill-writer.mjs +474 -0
- package/lib/learning/skill-writer.test.mjs +210 -0
- package/lib/mcp/server.mjs +328 -0
- package/lib/mcp/server.test.mjs +400 -0
- package/lib/model-router/auth-profiles.mjs +758 -0
- package/lib/model-router/auth-profiles.test.mjs +580 -0
- package/lib/model-router/catalog/anthropic.yaml +153 -0
- package/lib/model-router/catalog/deepseek.yaml +86 -0
- package/lib/model-router/catalog/moonshot.yaml +81 -0
- package/lib/model-router/catalog/qwen.yaml +114 -0
- package/lib/model-router/catalog.mjs +925 -0
- package/lib/model-router/catalog.test.mjs +385 -0
- package/lib/model-router/economics.mjs +564 -0
- package/lib/model-router/economics.test.mjs +344 -0
- package/lib/model-router/failover.mjs +298 -0
- package/lib/model-router/failover.test.mjs +439 -0
- package/lib/model-router/health.mjs +453 -0
- package/lib/model-router/health.test.mjs +338 -0
- package/lib/model-router/integration-coverage.test.mjs +829 -0
- package/lib/model-router/integration.test.mjs +564 -0
- package/lib/model-router/ledger.mjs +402 -0
- package/lib/model-router/ledger.test.mjs +382 -0
- package/lib/model-router/llm-task.mjs +515 -0
- package/lib/model-router/llm-task.test.mjs +392 -0
- package/lib/model-router/org-credentials.mjs +260 -0
- package/lib/model-router/org-credentials.test.mjs +265 -0
- package/lib/model-router/pricing-refresh.mjs +463 -0
- package/lib/model-router/pricing-refresh.test.mjs +286 -0
- package/lib/model-router/reconcile.mjs +429 -0
- package/lib/model-router/reconcile.test.mjs +316 -0
- package/lib/model-router/repair.mjs +471 -0
- package/lib/model-router/repair.test.mjs +180 -0
- package/lib/model-router/resolve.mjs +1206 -0
- package/lib/model-router/spawn.mjs +497 -0
- package/lib/model-router/spawn.test.mjs +425 -0
- package/lib/model-router/taxonomy.mjs +893 -0
- package/lib/model-router/taxonomy.test.mjs +410 -0
- package/lib/model-router.mjs +677 -0
- package/lib/model-router.test.mjs +907 -0
- package/lib/org/activity.mjs +211 -0
- package/lib/org/activity.test.mjs +134 -0
- package/lib/org/approvals.mjs +448 -0
- package/lib/org/approvals.test.mjs +216 -0
- package/lib/org/awareness.mjs +222 -0
- package/lib/org/awareness.test.mjs +159 -0
- package/lib/org/board.mjs +229 -0
- package/lib/org/board.test.mjs +177 -0
- package/lib/org/bootstrap-context.mjs +169 -0
- package/lib/org/bootstrap-context.test.mjs +153 -0
- package/lib/org/client.mjs +1628 -0
- package/lib/org/client.test.mjs +1107 -0
- package/lib/org/cohort-client.mjs +67 -0
- package/lib/org/cohort-client.test.mjs +126 -0
- package/lib/org/cost-sync.mjs +227 -0
- package/lib/org/cost-sync.test.mjs +153 -0
- package/lib/org/doctor.mjs +212 -0
- package/lib/org/doctor.test.mjs +212 -0
- package/lib/org/handoff.mjs +293 -0
- package/lib/org/handoff.test.mjs +269 -0
- package/lib/org/integration-tools.mjs +182 -0
- package/lib/org/integration-tools.test.mjs +160 -0
- package/lib/org/keys.mjs +131 -0
- package/lib/org/keys.test.mjs +92 -0
- package/lib/org/knowledge.mjs +463 -0
- package/lib/org/knowledge.test.mjs +319 -0
- package/lib/org/leases.mjs +335 -0
- package/lib/org/leases.test.mjs +235 -0
- package/lib/org/mesh-integration.test.mjs +127 -0
- package/lib/org/mesh.mjs +459 -0
- package/lib/org/mesh.test.mjs +345 -0
- package/lib/org/messaging.mjs +503 -0
- package/lib/org/messaging.test.mjs +238 -0
- package/lib/org/policy.mjs +345 -0
- package/lib/org/policy.test.mjs +237 -0
- package/lib/org/protocol.checksum +1 -0
- package/lib/org/protocol.checksum.test.mjs +90 -0
- package/lib/org/protocol.mjs +967 -0
- package/lib/org/protocol.test.mjs +264 -0
- package/lib/org/registry.mjs +194 -0
- package/lib/org/registry.test.mjs +100 -0
- package/lib/org/tool-surface-integration.test.mjs +120 -0
- package/lib/org/tool-surface.mjs +2535 -0
- package/lib/org/tool-surface.test.mjs +589 -0
- package/lib/org/ui-parity.mjs +3236 -0
- package/lib/org/ui-parity.test.mjs +348 -0
- package/lib/org/verify.mjs +176 -0
- package/lib/org/verify.test.mjs +194 -0
- package/lib/rag/embed.mjs +188 -0
- package/lib/rag/indexer.mjs +425 -0
- package/lib/rag/rag.test.mjs +505 -0
- package/lib/rag/search.mjs +475 -0
- package/lib/rate-guard.mjs +246 -0
- package/lib/rate-guard.test.mjs +201 -0
- package/lib/render.mjs +112 -0
- package/lib/render.test.mjs +68 -0
- package/lib/resource-governor.mjs +297 -0
- package/lib/resource-governor.test.mjs +262 -0
- package/lib/scheduling/dynamic-jobs.mjs +675 -0
- package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
- package/lib/scheduling/jitter.mjs +0 -0
- package/lib/scheduling/jitter.test.mjs +140 -0
- package/lib/secrets/broker.mjs +315 -0
- package/lib/secrets/broker.test.mjs +280 -0
- package/lib/secrets/providers.mjs +461 -0
- package/lib/secrets/providers.test.mjs +274 -0
- package/lib/security/audit-engine.mjs +684 -0
- package/lib/security/audit-engine.test.mjs +389 -0
- package/lib/security/coerce-args.mjs +552 -0
- package/lib/security/coerce-args.test.mjs +281 -0
- package/lib/security/dangerous-tools.mjs +97 -0
- package/lib/security/dangerous-tools.test.mjs +68 -0
- package/lib/security/external-content.mjs +145 -0
- package/lib/security/external-content.test.mjs +67 -0
- package/lib/security/redact.mjs +592 -0
- package/lib/security/redact.test.mjs +441 -0
- package/lib/security/secret-equal.mjs +73 -0
- package/lib/security/secret-equal.test.mjs +55 -0
- package/lib/session-permissions.mjs +101 -0
- package/lib/session-permissions.test.mjs +100 -0
- package/lib/setup/claude-probe.mjs +74 -0
- package/lib/setup/completeness.mjs +175 -0
- package/lib/setup/completeness.test.mjs +110 -0
- package/lib/setup/context-pack.mjs +173 -0
- package/lib/setup/context-pack.test.mjs +89 -0
- package/lib/setup/enrich.mjs +277 -0
- package/lib/setup/enrich.test.mjs +115 -0
- package/lib/setup/enroll-from-cohort.mjs +441 -0
- package/lib/setup/enroll-from-cohort.test.mjs +233 -0
- package/lib/setup/integration.test.mjs +162 -0
- package/lib/setup/io.mjs +360 -0
- package/lib/setup/io.test.mjs +77 -0
- package/lib/setup/run-generator.mjs +81 -0
- package/lib/setup/runner.mjs +244 -0
- package/lib/setup/runner.test.mjs +132 -0
- package/lib/setup/sections/comms.mjs +173 -0
- package/lib/setup/sections/company.mjs +120 -0
- package/lib/setup/sections/enrich.mjs +138 -0
- package/lib/setup/sections/identity.mjs +182 -0
- package/lib/setup/sections/identity.test.mjs +140 -0
- package/lib/setup/sections/learning.mjs +153 -0
- package/lib/setup/sections/learning.test.mjs +81 -0
- package/lib/setup/sections/messaging.mjs +219 -0
- package/lib/setup/sections/messaging.test.mjs +127 -0
- package/lib/setup/sections/model.mjs +102 -0
- package/lib/setup/sections/operating-model.mjs +78 -0
- package/lib/setup/sections/org.mjs +475 -0
- package/lib/setup/sections/org.test.mjs +313 -0
- package/lib/setup/sections/orgmail.mjs +173 -0
- package/lib/setup/sections/orgmail.test.mjs +118 -0
- package/lib/setup/sections/recovery.mjs +159 -0
- package/lib/setup/sections/recovery.test.mjs +98 -0
- package/lib/setup/sections/tools.mjs +132 -0
- package/lib/setup/sections/verify.mjs +97 -0
- package/lib/setup/sot.mjs +205 -0
- package/lib/setup/sot.test.mjs +81 -0
- package/lib/setup/state.mjs +151 -0
- package/lib/setup/state.test.mjs +92 -0
- package/lib/singleton.js +229 -0
- package/lib/singleton.test.mjs +135 -0
- package/lib/telemetry/alerts.mjs +216 -0
- package/lib/telemetry/alerts.test.mjs +109 -0
- package/lib/telemetry/collect.mjs +512 -0
- package/lib/telemetry/collect.test.mjs +202 -0
- package/lib/tool-definitions-integration.test.mjs +83 -0
- package/lib/tool-definitions.js +738 -0
- package/lib/tool-definitions.test.mjs +437 -0
- package/lib/util/fetch-timeout.mjs +136 -0
- package/lib/util/fetch-timeout.test.mjs +202 -0
- package/lib/util/reconnect.mjs +343 -0
- package/lib/util/reconnect.test.mjs +369 -0
- package/lib/util/unhandled.mjs +205 -0
- package/lib/util/unhandled.test.mjs +216 -0
- package/lib/voice/context-loader.mjs +466 -0
- package/lib/voice/index.mjs +100 -0
- package/lib/voice/openai-realtime.mjs +510 -0
- package/lib/voice/outbound.mjs +542 -0
- package/lib/voice/outbound.test.mjs +69 -0
- package/lib/voice/post-call-brief.mjs +428 -0
- package/lib/voice/provider.mjs +52 -0
- package/lib/voice/session-rotation.mjs +257 -0
- package/lib/voice/stt.mjs +161 -0
- package/lib/voice/stt.test.mjs +226 -0
- package/lib/voice/tool-bridge.mjs +370 -0
- package/lib/voice/tts.mjs +104 -0
- package/lib/voice/twilio-sip-bridge.mjs +288 -0
- package/lib/voice/voice.test.mjs +990 -0
- package/mcp/README.md +80 -0
- package/package.json +151 -0
- package/plugins/maestro-skills/plugin.json +139 -0
- package/plugins/maestro-skills/skills/agents-observe.md +110 -0
- package/plugins/maestro-skills/skills/board-deck.md +68 -0
- package/plugins/maestro-skills/skills/books-close.md +77 -0
- package/plugins/maestro-skills/skills/brand-steward.md +121 -0
- package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
- package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
- package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
- package/plugins/maestro-skills/skills/claude-pace.md +61 -0
- package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
- package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
- package/plugins/maestro-skills/skills/decision-brief.md +89 -0
- package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
- package/plugins/maestro-skills/skills/draft-comms.md +84 -0
- package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
- package/plugins/maestro-skills/skills/files-find.md +65 -0
- package/plugins/maestro-skills/skills/generative-ui.md +228 -0
- package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
- package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
- package/plugins/maestro-skills/skills/mail-triage.md +86 -0
- package/plugins/maestro-skills/skills/morning-brief.md +54 -0
- package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
- package/plugins/maestro-skills/skills/org-board.md +133 -0
- package/plugins/maestro-skills/skills/org-credential.md +68 -0
- package/plugins/maestro-skills/skills/org-recall.md +81 -0
- package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
- package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
- package/plugins/maestro-skills/skills/router-why.md +78 -0
- package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
- package/plugins/maestro-skills/skills/session-search.md +71 -0
- package/plugins/maestro-skills/skills/set-reminder.md +93 -0
- package/plugins/maestro-skills/skills/slack-followup.md +64 -0
- package/plugins/maestro-skills/skills/team-activity.md +86 -0
- package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
- package/policies/action-classification.yaml +114 -0
- package/policies/ai-disclosure.yaml +294 -0
- package/policies/communication-style.md +139 -0
- package/policies/information-barriers.yaml +118 -0
- package/policies/prompt-injection-defence.yaml +138 -0
- package/public/assets/icon-dark.png +0 -0
- package/public/assets/icon-dark.svg +9 -0
- package/public/assets/icon-light.svg +9 -0
- package/public/assets/logo-dark.svg +15 -0
- package/public/assets/logo-light.svg +15 -0
- package/scaffold/.mcp.json +7 -0
- package/scaffold/CLAUDE.md +368 -0
- package/scaffold/config/agent.json +55 -0
- package/scaffold/config/agent.ts +76 -0
- package/scaffold/config/agent.ts.example +89 -0
- package/scaffold/config/alerts.yaml +23 -0
- package/scaffold/config/allowlist.yaml.example +25 -0
- package/scaffold/config/caller-id-map.yaml +46 -0
- package/scaffold/config/collective.yaml +49 -0
- package/scaffold/config/company.json +20 -0
- package/scaffold/config/known-agents.json +6 -0
- package/scaffold/config/learning.yaml +55 -0
- package/scaffold/config/model-routing.yaml.example +104 -0
- package/scaffold/config/org.yaml +25 -0
- package/scaffold/config/orgmail.yaml.example +19 -0
- package/scaffold/config/recovery.yaml +72 -0
- package/scaffold/config/secrets.yaml +27 -0
- package/scaffold/config/slack.yaml.example +35 -0
- package/scaffold/config/telegram.yaml.example +38 -0
- package/scaffold/config/voice.yaml.example +89 -0
- package/scaffold/config/whatsapp.yaml.example +39 -0
- package/schedules/README.md +49 -0
- package/schedules/triggers/backlog-executor.md +102 -0
- package/schedules/triggers/brand-steward.md +72 -0
- package/schedules/triggers/daily-evening-wrap.md +159 -0
- package/schedules/triggers/daily-midday-sweep.md +58 -0
- package/schedules/triggers/daily-morning-brief.md +55 -0
- package/schedules/triggers/directory-hygiene.md +81 -0
- package/schedules/triggers/dynamic-jobs.md +40 -0
- package/schedules/triggers/inbox-processor.md +115 -0
- package/schedules/triggers/meeting-action-capture.md +60 -0
- package/schedules/triggers/meeting-prep.md +69 -0
- package/schedules/triggers/messaging-inbound.md +50 -0
- package/schedules/triggers/org-pulse.md +24 -0
- package/schedules/triggers/quarterly-self-assessment.md +54 -0
- package/schedules/triggers/weekly-engineering-health.md +37 -0
- package/schedules/triggers/weekly-execution.md +65 -0
- package/schedules/triggers/weekly-hiring.md +53 -0
- package/schedules/triggers/weekly-priorities.md +38 -0
- package/schedules/triggers/weekly-strategic-memo.md +124 -0
- package/scripts/archive-email.sh +55 -0
- package/scripts/cadence/cadence-status.mjs +36 -0
- package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
- package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
- package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/ci/check-docs-accuracy.mjs +493 -0
- package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
- package/scripts/ci/check-exports-exist.mjs +140 -0
- package/scripts/ci/check-files-exist.mjs +107 -0
- package/scripts/ci/check-no-build-artifacts.mjs +111 -0
- package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
- package/scripts/ci/check-no-confidential.mjs +198 -0
- package/scripts/ci/check-no-conflict-markers.mjs +169 -0
- package/scripts/ci/check-no-residual-identity.mjs +163 -0
- package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
- package/scripts/ci/check-tarball-fidelity.mjs +205 -0
- package/scripts/ci/check-unresolved-tokens.mjs +83 -0
- package/scripts/ci/check.mjs +109 -0
- package/scripts/ci/check.test.mjs +194 -0
- package/scripts/ci/run-coverage.mjs +82 -0
- package/scripts/ci/run-tests.mjs +71 -0
- package/scripts/cloud-relay/README.md +59 -0
- package/scripts/cloud-relay/index.mjs +233 -0
- package/scripts/cloud-relay/package.json +15 -0
- package/scripts/cloud-relay/railway.json +13 -0
- package/scripts/cloud-relay/voice/README.md +94 -0
- package/scripts/cloud-relay/voice/package-lock.json +39 -0
- package/scripts/cloud-relay/voice/package.json +16 -0
- package/scripts/cloud-relay/voice/railway.json +13 -0
- package/scripts/cloud-relay/voice/server.mjs +532 -0
- package/scripts/collective/hook-runner.mjs +211 -0
- package/scripts/collective/hook-runner.test.mjs +90 -0
- package/scripts/collective/org-pulse.mjs +72 -0
- package/scripts/collective/org-sync.mjs +61 -0
- package/scripts/collective/recall.mjs +45 -0
- package/scripts/collective/who.mjs +30 -0
- package/scripts/comms-monitor.sh +288 -0
- package/scripts/configure-whatsapp-sandbox.sh +201 -0
- package/scripts/continuous-monitor.sh +91 -0
- package/scripts/cost/fleet-digest.mjs +407 -0
- package/scripts/cost/fleet-digest.test.mjs +207 -0
- package/scripts/cost/track-claude-usage.mjs +169 -0
- package/scripts/daemon/agent-daemon.mjs +989 -0
- package/scripts/daemon/agent-daemon.test.mjs +525 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
- package/scripts/daemon/cadence-consumer.mjs +1080 -0
- package/scripts/daemon/cadence-consumer.test.mjs +770 -0
- package/scripts/daemon/cadence-handlers.mjs +1121 -0
- package/scripts/daemon/cadence-handlers.test.mjs +617 -0
- package/scripts/daemon/classifier.mjs +704 -0
- package/scripts/daemon/classifier.test.mjs +238 -0
- package/scripts/daemon/classify-kind.mjs +54 -0
- package/scripts/daemon/classify-kind.test.mjs +40 -0
- package/scripts/daemon/context-compiler.mjs +605 -0
- package/scripts/daemon/context-compiler.test.mjs +300 -0
- package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
- package/scripts/daemon/dispatcher.mjs +1516 -0
- package/scripts/daemon/health.mjs +72 -0
- package/scripts/daemon/inbox-deferral.mjs +210 -0
- package/scripts/daemon/inbox-deferral.test.mjs +242 -0
- package/scripts/daemon/integration.test.mjs +149 -0
- package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
- package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
- package/scripts/daemon/launchd-wrapper.sh +91 -0
- package/scripts/daemon/lib/session-router.mjs +274 -0
- package/scripts/daemon/lib/session-router.test.mjs +295 -0
- package/scripts/daemon/maestro-daemon.mjs +275 -0
- package/scripts/daemon/prompt-builder.mjs +685 -0
- package/scripts/daemon/prompt-builder.test.mjs +213 -0
- package/scripts/daemon/responder.mjs +854 -0
- package/scripts/daemon/session-lock.mjs +721 -0
- package/scripts/daemon/session-lock.test.mjs +252 -0
- package/scripts/daemon/session-outcomes.mjs +640 -0
- package/scripts/daemon/session-outcomes.test.mjs +533 -0
- package/scripts/daemon/typing-registry.mjs +90 -0
- package/scripts/daemon/typing-registry.test.mjs +77 -0
- package/scripts/daemon/voice-webhook-server.mjs +804 -0
- package/scripts/decisions/capture-decision.mjs +116 -0
- package/scripts/disclosure_assessment.py +873 -0
- package/scripts/disclosure_boundaries.py +562 -0
- package/scripts/email-signature-principal.html +52 -0
- package/scripts/email-signature.html +60 -0
- package/scripts/email_quote_thread.py +167 -0
- package/scripts/email_thread_dedup.py +362 -0
- package/scripts/emergency-stop.sh +81 -0
- package/scripts/healthcheck.sh +116 -0
- package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
- package/scripts/hooks/block-mcp-slack-send.sh +7 -0
- package/scripts/hooks/post-action-log.sh +126 -0
- package/scripts/hooks/pre-send-audit.sh +174 -0
- package/scripts/hooks/pre-send-audit.test.mjs +215 -0
- package/scripts/hooks/session-end-log.sh +27 -0
- package/scripts/hooks/session-start-banner.sh +115 -0
- package/scripts/huddle/audio-bridge.mjs +664 -0
- package/scripts/huddle/boot-slack-cdp.sh +102 -0
- package/scripts/huddle/huddle-controller.mjs +942 -0
- package/scripts/huddle/huddle-server.mjs +1229 -0
- package/scripts/huddle/launch-slack.sh +232 -0
- package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
- package/scripts/huddle/package-lock.json +62 -0
- package/scripts/huddle/package.json +22 -0
- package/scripts/huddle/setup-audio.sh +239 -0
- package/scripts/huddle/start-call.mjs +318 -0
- package/scripts/huddle/test-pipeline.mjs +263 -0
- package/scripts/learning/consolidate-skills.mjs +72 -0
- package/scripts/learning/session-search.mjs +125 -0
- package/scripts/llm_email_dedup.py +442 -0
- package/scripts/local-triggers/generate-plists.sh +432 -0
- package/scripts/local-triggers/generate-plists.test.mjs +413 -0
- package/scripts/local-triggers/install-all.sh +49 -0
- package/scripts/local-triggers/plists/.gitkeep +0 -0
- package/scripts/local-triggers/run-trigger.sh +63 -0
- package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
- package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
- package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
- package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
- package/scripts/maintenance/backup-to-cloud.sh +124 -0
- package/scripts/maintenance/health-check.sh +377 -0
- package/scripts/media-generation/README.md +105 -0
- package/scripts/media-generation/gemini-image-client.mjs +173 -0
- package/scripts/media-generation/generate-assets.mjs +289 -0
- package/scripts/media-generation/veo-video-client.mjs +219 -0
- package/scripts/org/send-orgmail.mjs +227 -0
- package/scripts/outbound-dedup-cleanup.sh +43 -0
- package/scripts/outbound-dedup.sh +477 -0
- package/scripts/outbound_dedup.py +115 -0
- package/scripts/parse-voice-transcript.mjs +481 -0
- package/scripts/pdf-generation/README.md +63 -0
- package/scripts/pdf-generation/build-document.mjs +247 -0
- package/scripts/pdf-generation/templates/board-pack.latex +136 -0
- package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
- package/scripts/pdf-generation/templates/memo.latex +114 -0
- package/scripts/poll-slack-events.sh +35 -0
- package/scripts/poller/calendar-poller.mjs +12 -0
- package/scripts/poller/gmail-poller.mjs +192 -0
- package/scripts/poller/imap-client.mjs +289 -0
- package/scripts/poller/inbox-scan-poller.mjs +156 -0
- package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
- package/scripts/poller/index.mjs +73 -0
- package/scripts/poller/intra-session-check.mjs +285 -0
- package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
- package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
- package/scripts/poller/secondary-gmail-poller.mjs +132 -0
- package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
- package/scripts/poller/slack-poller.mjs +854 -0
- package/scripts/poller/slack-socket-mode.mjs +917 -0
- package/scripts/poller/slack-socket-mode.test.mjs +753 -0
- package/scripts/poller/trigger.mjs +75 -0
- package/scripts/poller/utils.mjs +371 -0
- package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
- package/scripts/poller/voice-poller.mjs +236 -0
- package/scripts/poller-launchd/install.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +40 -0
- package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
- package/scripts/post-interaction-indexer.py +1598 -0
- package/scripts/pre-draft-context.py +994 -0
- package/scripts/pre_draft_lookup.py +258 -0
- package/scripts/rag/build-index.mjs +47 -0
- package/scripts/rag/ingest.mjs +111 -0
- package/scripts/rag/search.mjs +119 -0
- package/scripts/rag-indexer.py +629 -0
- package/scripts/restore-from-backup.sh +248 -0
- package/scripts/restore-from-backup.test.mjs +178 -0
- package/scripts/resume-operations.sh +80 -0
- package/scripts/search-secondary-inbox.py +181 -0
- package/scripts/secondary-inbox-poller.py +437 -0
- package/scripts/self-optimization/compute-metrics.py +398 -0
- package/scripts/send-email-as-principal.py +369 -0
- package/scripts/send-email-threaded.py +392 -0
- package/scripts/send-email-with-attachment.py +377 -0
- package/scripts/send-email.sh +131 -0
- package/scripts/send-sms.sh +175 -0
- package/scripts/send-whatsapp.sh +292 -0
- package/scripts/session-start.sh +106 -0
- package/scripts/setup/boot-claude-session.sh +94 -0
- package/scripts/setup/configure-macos.sh +674 -0
- package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
- package/scripts/setup/configure-voice-tunnel.mjs +182 -0
- package/scripts/setup/generate-agent-env.mjs +92 -0
- package/scripts/setup/generate-agent-package-json.mjs +222 -0
- package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
- package/scripts/setup/generate-autonomy.mjs +60 -0
- package/scripts/setup/generate-backlog.mjs +101 -0
- package/scripts/setup/generate-cadences.mjs +92 -0
- package/scripts/setup/generate-capability.mjs +162 -0
- package/scripts/setup/generate-charter.mjs +76 -0
- package/scripts/setup/generate-comms.mjs +59 -0
- package/scripts/setup/generate-company.mjs +92 -0
- package/scripts/setup/init-agent.sh +547 -0
- package/scripts/setup/init-agent.test.mjs +151 -0
- package/scripts/setup/init-archetype.mjs +74 -0
- package/scripts/setup/init-backup.mjs +54 -0
- package/scripts/setup/init-cadence-bus.mjs +60 -0
- package/scripts/setup/init-channel-bus.mjs +46 -0
- package/scripts/setup/init-cost-tracking.mjs +45 -0
- package/scripts/setup/init-decision-capture.mjs +66 -0
- package/scripts/setup/init-known-agents.mjs +57 -0
- package/scripts/setup/init-learning.mjs +70 -0
- package/scripts/setup/init-memory-executive.mjs +45 -0
- package/scripts/setup/init-model-router.mjs +124 -0
- package/scripts/setup/init-rag.mjs +174 -0
- package/scripts/setup/init-session-router.mjs +38 -0
- package/scripts/setup/init-slack-socket-mode.mjs +260 -0
- package/scripts/setup/init-telegram.mjs +165 -0
- package/scripts/setup/init-voice-realtime.mjs +204 -0
- package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
- package/scripts/setup/install-dev-tools.sh +150 -0
- package/scripts/setup/lib/install-plist.mjs +95 -0
- package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
- package/scripts/setup/render-environment-yaml.mjs +133 -0
- package/scripts/slack-events-ctl.sh +177 -0
- package/scripts/slack-events-server.mjs +1045 -0
- package/scripts/slack-react.mjs +89 -0
- package/scripts/slack-responded.sh +232 -0
- package/scripts/slack-send.sh +287 -0
- package/scripts/slack-typing.mjs +196 -0
- package/scripts/slack-upload-v2.py +95 -0
- package/scripts/sms-handler.mjs +450 -0
- package/scripts/spawn-session.sh +120 -0
- package/scripts/sync-protocol.mjs +217 -0
- package/scripts/system-verify.sh +184 -0
- package/scripts/test-email-thread-dedup.py +239 -0
- package/scripts/test-information-barriers.py +484 -0
- package/scripts/test-llm-email-dedup.py +251 -0
- package/scripts/test-pre-draft-integration.py +203 -0
- package/scripts/test-rag-phase2.sh +442 -0
- package/scripts/test-rag-search.sh +251 -0
- package/scripts/test-voice-parser.mjs +316 -0
- package/scripts/user-context-search.py +659 -0
- package/scripts/validate_outbound.py +1504 -0
- package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
- package/scripts/watchdog/force-reboot.sh +157 -0
- package/scripts/watchdog/memory-watchdog.sh +473 -0
- package/scripts/whatsapp-handler.mjs +538 -0
- package/teams/desktop-operations.yaml +34 -0
- package/teams/executive-office.yaml +27 -0
- package/teams/legal-and-regulatory.yaml +24 -0
- package/teams/platform-and-engineering.yaml +23 -0
- package/teams/strategy-and-growth.yaml +29 -0
- package/workflows/continuous/backlog-executor.yaml +141 -0
- package/workflows/continuous/inbound-monitor.yaml +168 -0
- package/workflows/daily/applicant-triage.yaml +197 -0
- package/workflows/daily/comms-triage.yaml +80 -0
- package/workflows/daily/evening-wrap.yaml +105 -0
- package/workflows/daily/morning-brief.yaml +164 -0
- package/workflows/daily/slack-followup-sweep.yaml +87 -0
- package/workflows/event-driven/README.md +50 -0
- package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
- package/workflows/event-driven/pr-review.yaml +107 -0
- package/workflows/monthly/board-readiness.yaml +76 -0
- package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
- package/workflows/session-protocol.md +171 -0
- package/workflows/weekly/engineering-health.yaml +154 -0
- package/workflows/weekly/hiring-review.yaml +169 -0
- package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
- 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
|
+
}
|