@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,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Health — periodic health metrics and status logging
|
|
3
|
+
|
|
4
|
+
import { writeFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { getStatus } from "./dispatcher.mjs";
|
|
7
|
+
|
|
8
|
+
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
|
|
9
|
+
|
|
10
|
+
let stats = {
|
|
11
|
+
daemon_start: new Date().toISOString(),
|
|
12
|
+
polls_total: 0,
|
|
13
|
+
items_detected: 0,
|
|
14
|
+
items_classified: 0,
|
|
15
|
+
sessions_spawned: 0,
|
|
16
|
+
sessions_completed: 0,
|
|
17
|
+
sessions_failed: 0,
|
|
18
|
+
classifications_failed: 0,
|
|
19
|
+
last_poll: null,
|
|
20
|
+
last_item: null,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function recordPoll(itemCount) {
|
|
24
|
+
stats.polls_total++;
|
|
25
|
+
stats.items_detected += itemCount;
|
|
26
|
+
stats.last_poll = new Date().toISOString();
|
|
27
|
+
if (itemCount > 0) stats.last_item = stats.last_poll;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function recordClassification(success) {
|
|
31
|
+
if (success) stats.items_classified++;
|
|
32
|
+
else stats.classifications_failed++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function recordSession(completed, success) {
|
|
36
|
+
if (!completed) {
|
|
37
|
+
stats.sessions_spawned++;
|
|
38
|
+
} else if (success) {
|
|
39
|
+
stats.sessions_completed++;
|
|
40
|
+
} else {
|
|
41
|
+
stats.sessions_failed++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function writeHealthDashboard() {
|
|
46
|
+
const dispatcher = getStatus();
|
|
47
|
+
const dashboard = {
|
|
48
|
+
updated: new Date().toISOString(),
|
|
49
|
+
uptime_s: Math.floor((Date.now() - new Date(stats.daemon_start).getTime()) / 1000),
|
|
50
|
+
...stats,
|
|
51
|
+
dispatcher,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const dir = join(AGENT_REPO_DIR, "state", "dashboards");
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
const path = join(dir, "daemon-health.yaml");
|
|
57
|
+
|
|
58
|
+
const yaml = Object.entries(dashboard)
|
|
59
|
+
.map(([k, v]) => {
|
|
60
|
+
if (typeof v === "object" && v !== null) {
|
|
61
|
+
return `${k}:\n${JSON.stringify(v, null, 2).split("\n").map(l => ` ${l}`).join("\n")}`;
|
|
62
|
+
}
|
|
63
|
+
return `${k}: ${v}`;
|
|
64
|
+
})
|
|
65
|
+
.join("\n");
|
|
66
|
+
|
|
67
|
+
writeFileSync(path, yaml + "\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getStats() {
|
|
71
|
+
return { ...stats };
|
|
72
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Inbox deferral utilities
|
|
3
|
+
*
|
|
4
|
+
* When the dispatcher hits a thread/channel-level dedup lock, the
|
|
5
|
+
* incoming message used to be marked `.processed` and dropped on the
|
|
6
|
+
* floor. That meant rapid-fire DMs (or thread bursts) silently lost
|
|
7
|
+
* every message after the first.
|
|
8
|
+
*
|
|
9
|
+
* This module replaces that with a defer-then-promote pattern:
|
|
10
|
+
*
|
|
11
|
+
* markDeferred(item, service, agentRoot)
|
|
12
|
+
* Renames the inbox file from `<name>` to `<name>.deferred`.
|
|
13
|
+
* The daemon's inbox scanner skips `.deferred` files, so the
|
|
14
|
+
* message is parked but not lost.
|
|
15
|
+
*
|
|
16
|
+
* promoteDeferred(channel, agentRoot)
|
|
17
|
+
* Called from the dispatcher's session-close release path. Finds
|
|
18
|
+
* every `.deferred` file in any service inbox dir whose body
|
|
19
|
+
* references `channel`. If exactly one is found, renames it back
|
|
20
|
+
* to its original name (next poll picks it up). If N>1 are found,
|
|
21
|
+
* keeps only the LATEST (by timestamp), promotes that one, and
|
|
22
|
+
* marks the others `.processed-bundled` for the audit trail —
|
|
23
|
+
* the latest item's `thread_context` already contains the prior
|
|
24
|
+
* messages as conversation history, so Claude sees everything
|
|
25
|
+
* and can compose a single coherent reply.
|
|
26
|
+
*
|
|
27
|
+
* The file format is the YAML emitted by the slack/gmail/calendar
|
|
28
|
+
* pollers (a flat top-level object with quoted scalar fields, plus an
|
|
29
|
+
* optional block-scalar body). We deliberately avoid pulling in a YAML
|
|
30
|
+
* parser — extraction of the COLUMN-0 top-level `channel_id:` and
|
|
31
|
+
* `timestamp:` fields is sufficient for the routing decision and keeps
|
|
32
|
+
* this module dependency-free. Extraction is anchored to top-level keys so
|
|
33
|
+
* an indented `channel_id:`/`timestamp:` inside a message body block scalar
|
|
34
|
+
* can never be mistaken for the real field (audit L7).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { readdirSync, readFileSync, renameSync, existsSync } from "node:fs";
|
|
38
|
+
import { join } from "node:path";
|
|
39
|
+
|
|
40
|
+
const SERVICE_DIRS = ["slack", "gmail", "calendar", "internal", "sms", "whatsapp"];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Rename a live inbox file to its `.deferred` suffix so the scanner
|
|
44
|
+
* skips it until the channel/thread lock releases.
|
|
45
|
+
*
|
|
46
|
+
* Mirrors markProcessed's file-matching strategy (id or raw_ref
|
|
47
|
+
* substring) so we hit the same file that markProcessed would have.
|
|
48
|
+
*
|
|
49
|
+
* Idempotent: if the file is already `.deferred` or `.processed`, no-op.
|
|
50
|
+
*
|
|
51
|
+
* @returns {number} number of files renamed (usually 1, occasionally 0
|
|
52
|
+
* for backlog items with no inbox file)
|
|
53
|
+
*/
|
|
54
|
+
export function markDeferred(item, service, agentRoot) {
|
|
55
|
+
if (!item || !service || !agentRoot) return 0;
|
|
56
|
+
const inboxDir = join(agentRoot, "state", "inbox", service);
|
|
57
|
+
if (!existsSync(inboxDir)) return 0;
|
|
58
|
+
let renamed = 0;
|
|
59
|
+
try {
|
|
60
|
+
const needle = item.id || item.raw_ref;
|
|
61
|
+
if (!needle) return 0;
|
|
62
|
+
const files = readdirSync(inboxDir).filter(
|
|
63
|
+
(f) => !f.endsWith(".processed") &&
|
|
64
|
+
!f.endsWith(".deferred") &&
|
|
65
|
+
!f.endsWith(".processed-bundled") &&
|
|
66
|
+
(f.includes(item.id || "") || f.includes(item.raw_ref || ""))
|
|
67
|
+
);
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
renameSync(join(inboxDir, file), join(inboxDir, `${file}.deferred`));
|
|
70
|
+
renamed++;
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Inbox dir may not exist for non-poller-backed items (e.g. backlog).
|
|
74
|
+
}
|
|
75
|
+
return renamed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Escape regex metacharacters in an interpolated field name so a field like
|
|
79
|
+
// `a.b` can't be read as a wildcard (audit L7).
|
|
80
|
+
function escapeRegExp(s) {
|
|
81
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Extract a TOP-LEVEL scalar value from a poller-written inbox YAML file.
|
|
86
|
+
* Returns `null` if the field is absent.
|
|
87
|
+
*
|
|
88
|
+
* We only need this for `channel_id` (routing) and `timestamp`
|
|
89
|
+
* (latest-wins selection) — both are written as quoted single-line
|
|
90
|
+
* scalars by every poller.
|
|
91
|
+
*
|
|
92
|
+
* Audit L7: the previous regex used `^\s*field:` with the `m` flag, so any
|
|
93
|
+
* INDENTED line inside a block scalar (e.g. a message body that itself
|
|
94
|
+
* contains ` channel_id: D-OTHER` or ` timestamp: ...`) could match and be
|
|
95
|
+
* mistaken for the real top-level field — mis-routing the promote to the
|
|
96
|
+
* wrong channel. We now scan only the column-0 top-level region: keys with NO
|
|
97
|
+
* leading whitespace, stopping at the first block/indented region. A YAML
|
|
98
|
+
* block scalar (`key: |` / `key: >`) introduces an indented body, so once we
|
|
99
|
+
* see one we skip its indented continuation lines instead of matching inside
|
|
100
|
+
* them. The interpolated field name is regex-escaped.
|
|
101
|
+
*/
|
|
102
|
+
function readScalar(body, field) {
|
|
103
|
+
if (typeof body !== "string") return null;
|
|
104
|
+
const key = escapeRegExp(field);
|
|
105
|
+
// Top-level key: starts at column 0 (no indentation), value on same line.
|
|
106
|
+
// Capture group handles optional double-quotes around the value.
|
|
107
|
+
const topLevel = new RegExp(`^${key}\\s*:\\s*"?([^"\\n]*?)"?\\s*$`);
|
|
108
|
+
// A column-0 line that opens a block scalar or a nested mapping/sequence —
|
|
109
|
+
// its indented continuation lines are NOT top-level and must be skipped.
|
|
110
|
+
const opensBlock = /^[^\s:][^:]*:\s*(?:[|>][+-]?\d*)?\s*$/;
|
|
111
|
+
|
|
112
|
+
const lines = body.split("\n");
|
|
113
|
+
for (let i = 0; i < lines.length; i++) {
|
|
114
|
+
const line = lines[i];
|
|
115
|
+
// Skip indented (non-top-level) lines entirely — they belong to a block
|
|
116
|
+
// scalar body or a nested structure, never a top-level scalar.
|
|
117
|
+
if (/^\s/.test(line)) continue;
|
|
118
|
+
if (line.trim() === "") continue;
|
|
119
|
+
const m = line.match(topLevel);
|
|
120
|
+
if (m) return m[1].trim();
|
|
121
|
+
// If this top-level line opens a block/nested region, the following
|
|
122
|
+
// indented lines are its body — they're skipped by the `^\s` guard above,
|
|
123
|
+
// so no extra bookkeeping is needed here. (Documented for clarity.)
|
|
124
|
+
void opensBlock;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Promote every `.deferred` item targeting `channel` back into the
|
|
131
|
+
* live inbox. Bundles bursts: if multiple deferred items exist for
|
|
132
|
+
* the same channel, only the latest is re-queued (the others are
|
|
133
|
+
* marked `.processed-bundled` since their content is already present
|
|
134
|
+
* in the latest item's thread_context).
|
|
135
|
+
*
|
|
136
|
+
* Called from the dispatcher's session-close path, AFTER the channel
|
|
137
|
+
* lock has been released, so the next poll cycle is free to re-acquire.
|
|
138
|
+
*
|
|
139
|
+
* @returns {{promoted: number, bundled: number, service: string|null}}
|
|
140
|
+
*/
|
|
141
|
+
export function promoteDeferred(channel, agentRoot) {
|
|
142
|
+
const result = { promoted: 0, bundled: 0, service: null };
|
|
143
|
+
if (!channel || !agentRoot) return result;
|
|
144
|
+
|
|
145
|
+
for (const service of SERVICE_DIRS) {
|
|
146
|
+
const inboxDir = join(agentRoot, "state", "inbox", service);
|
|
147
|
+
if (!existsSync(inboxDir)) continue;
|
|
148
|
+
|
|
149
|
+
let deferredFiles;
|
|
150
|
+
try {
|
|
151
|
+
deferredFiles = readdirSync(inboxDir).filter((f) => f.endsWith(".deferred"));
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (deferredFiles.length === 0) continue;
|
|
156
|
+
|
|
157
|
+
// Filter to items whose body references this channel. Reading
|
|
158
|
+
// every .deferred file is cheap (a handful at most) — we stay
|
|
159
|
+
// dependency-free by regex-scanning rather than parsing YAML.
|
|
160
|
+
const matches = [];
|
|
161
|
+
for (const file of deferredFiles) {
|
|
162
|
+
let body;
|
|
163
|
+
try {
|
|
164
|
+
body = readFileSync(join(inboxDir, file), "utf-8");
|
|
165
|
+
} catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const fileChannel = readScalar(body, "channel_id");
|
|
169
|
+
if (fileChannel !== channel) continue;
|
|
170
|
+
matches.push({
|
|
171
|
+
file,
|
|
172
|
+
timestamp: readScalar(body, "timestamp") || "",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (matches.length === 0) continue;
|
|
176
|
+
|
|
177
|
+
// Latest-wins: lex-sort ISO timestamps, take the most recent.
|
|
178
|
+
matches.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
179
|
+
const latest = matches[matches.length - 1];
|
|
180
|
+
const older = matches.slice(0, -1);
|
|
181
|
+
|
|
182
|
+
// Promote the latest back to live inbox.
|
|
183
|
+
try {
|
|
184
|
+
const live = latest.file.replace(/\.deferred$/, "");
|
|
185
|
+
renameSync(join(inboxDir, latest.file), join(inboxDir, live));
|
|
186
|
+
result.promoted++;
|
|
187
|
+
result.service = service;
|
|
188
|
+
} catch {
|
|
189
|
+
// If the rename failed (race with another process), leave it
|
|
190
|
+
// as .deferred — the next session-close will retry.
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Mark older bursts as bundled — their content is already part of
|
|
194
|
+
// the latest item's thread_context, so they don't need their own
|
|
195
|
+
// session, but we preserve the file for audit.
|
|
196
|
+
for (const m of older) {
|
|
197
|
+
try {
|
|
198
|
+
renameSync(
|
|
199
|
+
join(inboxDir, m.file),
|
|
200
|
+
join(inboxDir, m.file.replace(/\.deferred$/, ".processed-bundled"))
|
|
201
|
+
);
|
|
202
|
+
result.bundled++;
|
|
203
|
+
} catch {
|
|
204
|
+
// Best-effort; missing file is fine.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import { markDeferred, promoteDeferred } from "./inbox-deferral.mjs";
|
|
8
|
+
|
|
9
|
+
function makeAgentRoot() {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), "maestro-deferral-"));
|
|
11
|
+
mkdirSync(join(root, "state", "inbox", "slack"), { recursive: true });
|
|
12
|
+
return root;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeInboxItem(root, name, { channel_id, timestamp, id = "test-id" }) {
|
|
16
|
+
const body = [
|
|
17
|
+
`id: "${id}"`,
|
|
18
|
+
`service: "slack"`,
|
|
19
|
+
`channel_id: "${channel_id}"`,
|
|
20
|
+
`timestamp: "${timestamp}"`,
|
|
21
|
+
`content: |`,
|
|
22
|
+
` body`,
|
|
23
|
+
"",
|
|
24
|
+
].join("\n");
|
|
25
|
+
writeFileSync(join(root, "state", "inbox", "slack", name), body);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("markDeferred renames live file to .deferred", () => {
|
|
29
|
+
const root = makeAgentRoot();
|
|
30
|
+
try {
|
|
31
|
+
writeInboxItem(root, "msg-A.yaml", {
|
|
32
|
+
channel_id: "D001",
|
|
33
|
+
timestamp: "2026-05-13T00:00:00Z",
|
|
34
|
+
id: "msg-A",
|
|
35
|
+
});
|
|
36
|
+
const n = markDeferred({ id: "msg-A" }, "slack", root);
|
|
37
|
+
assert.equal(n, 1);
|
|
38
|
+
const files = readdirSync(join(root, "state", "inbox", "slack"));
|
|
39
|
+
assert.deepEqual(files, ["msg-A.yaml.deferred"]);
|
|
40
|
+
} finally {
|
|
41
|
+
rmSync(root, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("markDeferred is idempotent — already-deferred files are skipped", () => {
|
|
46
|
+
const root = makeAgentRoot();
|
|
47
|
+
try {
|
|
48
|
+
writeInboxItem(root, "msg-B.yaml.deferred", {
|
|
49
|
+
channel_id: "D001",
|
|
50
|
+
timestamp: "2026-05-13T00:00:00Z",
|
|
51
|
+
id: "msg-B",
|
|
52
|
+
});
|
|
53
|
+
const n = markDeferred({ id: "msg-B" }, "slack", root);
|
|
54
|
+
assert.equal(n, 0);
|
|
55
|
+
} finally {
|
|
56
|
+
rmSync(root, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("markDeferred is no-op when item has no matching file (e.g. backlog)", () => {
|
|
61
|
+
const root = makeAgentRoot();
|
|
62
|
+
try {
|
|
63
|
+
const n = markDeferred({ id: "no-such-id" }, "slack", root);
|
|
64
|
+
assert.equal(n, 0);
|
|
65
|
+
} finally {
|
|
66
|
+
rmSync(root, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("promoteDeferred with single item: renames back to live", () => {
|
|
71
|
+
const root = makeAgentRoot();
|
|
72
|
+
try {
|
|
73
|
+
writeInboxItem(root, "msg-C.yaml.deferred", {
|
|
74
|
+
channel_id: "D099N1JEA10",
|
|
75
|
+
timestamp: "2026-05-13T00:00:00Z",
|
|
76
|
+
id: "msg-C",
|
|
77
|
+
});
|
|
78
|
+
const r = promoteDeferred("D099N1JEA10", root);
|
|
79
|
+
assert.equal(r.promoted, 1);
|
|
80
|
+
assert.equal(r.bundled, 0);
|
|
81
|
+
assert.equal(r.service, "slack");
|
|
82
|
+
const files = readdirSync(join(root, "state", "inbox", "slack"));
|
|
83
|
+
assert.deepEqual(files, ["msg-C.yaml"]);
|
|
84
|
+
} finally {
|
|
85
|
+
rmSync(root, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("promoteDeferred with multiple items: keeps latest, bundles rest", () => {
|
|
90
|
+
const root = makeAgentRoot();
|
|
91
|
+
try {
|
|
92
|
+
writeInboxItem(root, "msg-1.yaml.deferred", {
|
|
93
|
+
channel_id: "D001",
|
|
94
|
+
timestamp: "2026-05-13T00:00:00Z",
|
|
95
|
+
id: "msg-1",
|
|
96
|
+
});
|
|
97
|
+
writeInboxItem(root, "msg-2.yaml.deferred", {
|
|
98
|
+
channel_id: "D001",
|
|
99
|
+
timestamp: "2026-05-13T00:01:00Z",
|
|
100
|
+
id: "msg-2",
|
|
101
|
+
});
|
|
102
|
+
writeInboxItem(root, "msg-3.yaml.deferred", {
|
|
103
|
+
channel_id: "D001",
|
|
104
|
+
timestamp: "2026-05-13T00:02:00Z",
|
|
105
|
+
id: "msg-3",
|
|
106
|
+
});
|
|
107
|
+
const r = promoteDeferred("D001", root);
|
|
108
|
+
assert.equal(r.promoted, 1);
|
|
109
|
+
assert.equal(r.bundled, 2);
|
|
110
|
+
const files = readdirSync(join(root, "state", "inbox", "slack")).sort();
|
|
111
|
+
assert.deepEqual(files, [
|
|
112
|
+
"msg-1.yaml.processed-bundled",
|
|
113
|
+
"msg-2.yaml.processed-bundled",
|
|
114
|
+
"msg-3.yaml", // latest survives as live
|
|
115
|
+
]);
|
|
116
|
+
} finally {
|
|
117
|
+
rmSync(root, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("promoteDeferred ignores items in other channels", () => {
|
|
122
|
+
const root = makeAgentRoot();
|
|
123
|
+
try {
|
|
124
|
+
writeInboxItem(root, "for-A.yaml.deferred", {
|
|
125
|
+
channel_id: "D-A",
|
|
126
|
+
timestamp: "2026-05-13T00:00:00Z",
|
|
127
|
+
id: "for-A",
|
|
128
|
+
});
|
|
129
|
+
writeInboxItem(root, "for-B.yaml.deferred", {
|
|
130
|
+
channel_id: "D-B",
|
|
131
|
+
timestamp: "2026-05-13T00:00:00Z",
|
|
132
|
+
id: "for-B",
|
|
133
|
+
});
|
|
134
|
+
const r = promoteDeferred("D-A", root);
|
|
135
|
+
assert.equal(r.promoted, 1);
|
|
136
|
+
assert.equal(r.bundled, 0);
|
|
137
|
+
const files = readdirSync(join(root, "state", "inbox", "slack")).sort();
|
|
138
|
+
assert.deepEqual(files, ["for-A.yaml", "for-B.yaml.deferred"]);
|
|
139
|
+
} finally {
|
|
140
|
+
rmSync(root, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("promoteDeferred no-op when nothing to promote", () => {
|
|
145
|
+
const root = makeAgentRoot();
|
|
146
|
+
try {
|
|
147
|
+
const r = promoteDeferred("D999", root);
|
|
148
|
+
assert.equal(r.promoted, 0);
|
|
149
|
+
assert.equal(r.bundled, 0);
|
|
150
|
+
assert.equal(r.service, null);
|
|
151
|
+
} finally {
|
|
152
|
+
rmSync(root, { recursive: true, force: true });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Audit L7 — block-scalar body must not be mis-parsed for routing fields
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Write an inbox item whose `content` block scalar body itself contains lines
|
|
162
|
+
* that look like top-level `channel_id:` / `timestamp:` keys. A naive
|
|
163
|
+
* `^\s*field:` regex would match the INDENTED body line and mis-route.
|
|
164
|
+
*/
|
|
165
|
+
function writeInboxItemWithTrickyBody(root, name, { channel_id, timestamp, id, bodyChannel, bodyTimestamp }) {
|
|
166
|
+
const lines = [
|
|
167
|
+
`id: "${id}"`,
|
|
168
|
+
`service: "slack"`,
|
|
169
|
+
`channel_id: "${channel_id}"`,
|
|
170
|
+
`timestamp: "${timestamp}"`,
|
|
171
|
+
`content: |`,
|
|
172
|
+
` Hi team — quoting an old config below:`,
|
|
173
|
+
` channel_id: ${bodyChannel}`,
|
|
174
|
+
` timestamp: ${bodyTimestamp}`,
|
|
175
|
+
` please review.`,
|
|
176
|
+
"",
|
|
177
|
+
];
|
|
178
|
+
writeFileSync(join(root, "state", "inbox", "slack", name), lines.join("\n"));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
test("L7: body line `channel_id:` in a block scalar does not mis-route", () => {
|
|
182
|
+
const root = makeAgentRoot();
|
|
183
|
+
try {
|
|
184
|
+
// Real top-level channel is D-REAL. The body mentions D-EVIL on an
|
|
185
|
+
// indented `channel_id:` line — promote must target D-REAL only.
|
|
186
|
+
writeInboxItemWithTrickyBody(root, "tricky.yaml.deferred", {
|
|
187
|
+
channel_id: "D-REAL",
|
|
188
|
+
timestamp: "2026-05-13T00:00:00Z",
|
|
189
|
+
id: "tricky",
|
|
190
|
+
bodyChannel: "D-EVIL",
|
|
191
|
+
bodyTimestamp: "1999-01-01T00:00:00Z",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Promoting the body's channel must find NOTHING (the body line is not a
|
|
195
|
+
// real top-level field).
|
|
196
|
+
const evil = promoteDeferred("D-EVIL", root);
|
|
197
|
+
assert.equal(evil.promoted, 0, "must not match the indented body channel_id");
|
|
198
|
+
assert.equal(evil.bundled, 0);
|
|
199
|
+
|
|
200
|
+
// Promoting the genuine top-level channel must succeed.
|
|
201
|
+
const real = promoteDeferred("D-REAL", root);
|
|
202
|
+
assert.equal(real.promoted, 1, "real top-level channel_id must match");
|
|
203
|
+
assert.equal(real.bundled, 0);
|
|
204
|
+
const files = readdirSync(join(root, "state", "inbox", "slack"));
|
|
205
|
+
assert.deepEqual(files, ["tricky.yaml"]);
|
|
206
|
+
} finally {
|
|
207
|
+
rmSync(root, { recursive: true, force: true });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("L7: latest-wins uses top-level timestamp, not a body `timestamp:` line", () => {
|
|
212
|
+
const root = makeAgentRoot();
|
|
213
|
+
try {
|
|
214
|
+
// Two items in the same channel. Item B is genuinely later by its
|
|
215
|
+
// top-level timestamp, but its block-scalar body contains an EARLY
|
|
216
|
+
// `timestamp:` line. A naive parser could read the body line, pick A as
|
|
217
|
+
// "latest" and bundle B. Correct behaviour: B wins (later top-level ts).
|
|
218
|
+
writeInboxItem(root, "A.yaml.deferred", {
|
|
219
|
+
channel_id: "D-TS",
|
|
220
|
+
timestamp: "2026-05-13T00:00:00Z",
|
|
221
|
+
id: "A",
|
|
222
|
+
});
|
|
223
|
+
writeInboxItemWithTrickyBody(root, "B.yaml.deferred", {
|
|
224
|
+
channel_id: "D-TS",
|
|
225
|
+
timestamp: "2026-05-13T09:00:00Z", // genuinely later
|
|
226
|
+
id: "B",
|
|
227
|
+
bodyChannel: "D-TS",
|
|
228
|
+
bodyTimestamp: "2000-01-01T00:00:00Z", // decoy early ts in the body
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const r = promoteDeferred("D-TS", root);
|
|
232
|
+
assert.equal(r.promoted, 1);
|
|
233
|
+
assert.equal(r.bundled, 1);
|
|
234
|
+
const files = readdirSync(join(root, "state", "inbox", "slack")).sort();
|
|
235
|
+
assert.deepEqual(files, [
|
|
236
|
+
"A.yaml.processed-bundled", // earlier item bundled
|
|
237
|
+
"B.yaml", // later top-level timestamp survives as live
|
|
238
|
+
]);
|
|
239
|
+
} finally {
|
|
240
|
+
rmSync(root, { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* test-integration.mjs — End-to-end integration tests for session lock + context compiler pipeline
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from "fs";
|
|
9
|
+
import { join, dirname } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const TEST_DIR = join(__dirname, "../../.test-integration");
|
|
14
|
+
|
|
15
|
+
// Set env BEFORE importing modules. context-compiler.mjs reads its repo root
|
|
16
|
+
// from __TEST_AGENT_DIR (the legacy __TEST_SOPHIE_DIR name was never honoured by
|
|
17
|
+
// the module, so the disclosure-boundary fixtures were written here but read
|
|
18
|
+
// from the real repo — the cause of the end-to-end failure). Set both: the
|
|
19
|
+
// session-lock module reads __TEST_SESSION_DIR; the compiler reads
|
|
20
|
+
// __TEST_AGENT_DIR + __TEST_SESSION_DIR.
|
|
21
|
+
process.env.__TEST_SESSION_DIR = join(TEST_DIR, "state", "sessions");
|
|
22
|
+
process.env.__TEST_AGENT_DIR = TEST_DIR;
|
|
23
|
+
process.env.__TEST_SOPHIE_DIR = TEST_DIR; // kept for any legacy reader; harmless
|
|
24
|
+
|
|
25
|
+
function setupTestEnv() {
|
|
26
|
+
mkdirSync(join(TEST_DIR, "state", "sessions", "locks"), { recursive: true });
|
|
27
|
+
mkdirSync(join(TEST_DIR, "memory", "profiles", "users"), { recursive: true });
|
|
28
|
+
mkdirSync(join(TEST_DIR, "memory", "interactions", "slack", "dm-test-user"), { recursive: true });
|
|
29
|
+
mkdirSync(join(TEST_DIR, "policies"), { recursive: true });
|
|
30
|
+
writeFileSync(join(TEST_DIR, "state", "sessions", "active.json"), "{}");
|
|
31
|
+
|
|
32
|
+
writeFileSync(join(TEST_DIR, "policies", "information-barriers.yaml"), `domains:
|
|
33
|
+
internal-legal:
|
|
34
|
+
description: "Northwind legal strategy, GC advice, privilege-protected"
|
|
35
|
+
default_sensitivity: high
|
|
36
|
+
strategic-initiative:
|
|
37
|
+
description: "the strategic initiative M&A targets, deal terms, pipeline"
|
|
38
|
+
default_sensitivity: critical
|
|
39
|
+
public:
|
|
40
|
+
description: "Published information, press, website"
|
|
41
|
+
default_sensitivity: none
|
|
42
|
+
`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cleanTestEnv() {
|
|
46
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { acquireLock, releaseLock, registerSent, checkRecentlySent } = await import("./session-lock.mjs");
|
|
50
|
+
const { compileContext } = await import("./context-compiler.mjs");
|
|
51
|
+
|
|
52
|
+
// ─── Integration tests ─────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
describe("Integration: lock → compile → prompt pipeline", () => {
|
|
55
|
+
beforeEach(setupTestEnv);
|
|
56
|
+
afterEach(cleanTestEnv);
|
|
57
|
+
|
|
58
|
+
it("acquireLock prevents duplicate processing of same item", () => {
|
|
59
|
+
const first = acquireLock("slack-C001-12345", { sender: "Test", channel: "C001" });
|
|
60
|
+
assert.equal(first.acquired, true);
|
|
61
|
+
|
|
62
|
+
const second = acquireLock("slack-C001-12345", { sender: "Test", channel: "C001" });
|
|
63
|
+
assert.equal(second.acquired, false);
|
|
64
|
+
|
|
65
|
+
releaseLock("slack-C001-12345");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("sent-registry prevents duplicate sends across quick-reply and holding paths", () => {
|
|
69
|
+
// Holding message sent
|
|
70
|
+
registerSent("C001", "12345.6789", "holding", "s-1", "Looking into this now");
|
|
71
|
+
|
|
72
|
+
// A quick reply to the SAME channel+thread is blocked by the thread-cooldown
|
|
73
|
+
// rule (Rule 2): once anything has been sent to a thread, the whole thread
|
|
74
|
+
// cools down so the quick-reply path and the full-session path can't both
|
|
75
|
+
// fire into the same thread. (The original assertion expected the different
|
|
76
|
+
// type to be allowed — that predated Rule 2 and was stale; Rule 2 is the
|
|
77
|
+
// whole point of this test, "prevents duplicate sends across paths".)
|
|
78
|
+
const quickCheck = checkRecentlySent("C001", "12345.6789", "quick_reply");
|
|
79
|
+
assert.equal(quickCheck.allowed, false);
|
|
80
|
+
assert.match(quickCheck.reason, /thread cooldown/);
|
|
81
|
+
|
|
82
|
+
// Another holding to same channel+thread is likewise blocked.
|
|
83
|
+
const holdCheck = checkRecentlySent("C001", "12345.6789", "holding");
|
|
84
|
+
assert.equal(holdCheck.allowed, false);
|
|
85
|
+
|
|
86
|
+
// But a different, untouched thread is still free to receive a quick reply —
|
|
87
|
+
// the cooldown is scoped to the thread, not the whole channel (non-DM).
|
|
88
|
+
const otherThread = checkRecentlySent("C001", "99999.0000", "quick_reply");
|
|
89
|
+
assert.equal(otherThread.allowed, true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("context compiler respects disclosure boundaries end-to-end", async () => {
|
|
93
|
+
// Create a profile with restrictions
|
|
94
|
+
writeFileSync(
|
|
95
|
+
join(TEST_DIR, "memory", "profiles", "users", "test-user.yaml"),
|
|
96
|
+
`name: Test User\ninformation_boundaries:\n excluded_domains:\n - strategic-initiative\n`
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Create history with mixed content
|
|
100
|
+
const today = new Date().toISOString().split("T")[0];
|
|
101
|
+
writeFileSync(
|
|
102
|
+
join(TEST_DIR, "memory", "interactions", "slack", "dm-test-user", `${today}.jsonl`),
|
|
103
|
+
[
|
|
104
|
+
JSON.stringify({ ts: "100", from: "test-user", content: "What is the status?", received_at: "2026-04-06T10:00:00Z" }),
|
|
105
|
+
JSON.stringify({ ts: "101", from: "robin", content: "the strategic initiative deal terms are looking good", received_at: "2026-04-06T10:01:00Z" }),
|
|
106
|
+
JSON.stringify({ ts: "102", from: "robin", content: "The public website update is live", received_at: "2026-04-06T10:02:00Z" }),
|
|
107
|
+
].join("\n") + "\n"
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const { contextBlock } = await compileContext(
|
|
111
|
+
{ sender: "Test User", channel: "D001", channel_id: "D001" },
|
|
112
|
+
{ priority: "normal", model: "sonnet", action: "respond" },
|
|
113
|
+
{}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Public content should be included
|
|
117
|
+
assert.ok(contextBlock.includes("website update is live"));
|
|
118
|
+
// Strategic-initiative content should be filtered. (The old assertion
|
|
119
|
+
// checked for "Aether deal terms" — absent from the fixture — so it was a
|
|
120
|
+
// tautology; assert against the actual excluded fixture text.)
|
|
121
|
+
assert.ok(!contextBlock.includes("strategic initiative deal terms"));
|
|
122
|
+
// Should have disclosure boundaries
|
|
123
|
+
assert.ok(contextBlock.includes("DO NOT mention"));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("active sessions are visible in compiled context", async () => {
|
|
127
|
+
writeFileSync(
|
|
128
|
+
join(TEST_DIR, "state", "sessions", "active.json"),
|
|
129
|
+
JSON.stringify({
|
|
130
|
+
"s-999": {
|
|
131
|
+
sender: "Alice",
|
|
132
|
+
channel: "C002",
|
|
133
|
+
summary: "Preparing board pack",
|
|
134
|
+
model: "opus",
|
|
135
|
+
startTime: Date.now() - 120000,
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const { contextBlock } = await compileContext(
|
|
141
|
+
{ sender: "Test", channel: "C001" },
|
|
142
|
+
{ priority: "normal", model: "sonnet", action: "respond" },
|
|
143
|
+
{}
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
assert.ok(contextBlock.includes("Preparing board pack"));
|
|
147
|
+
assert.ok(contextBlock.includes("Active Sessions"));
|
|
148
|
+
});
|
|
149
|
+
});
|