@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,525 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-daemon.test.mjs — F1/H2 message-loss window (task #16, 0.8 follow-up).
|
|
3
|
+
*
|
|
4
|
+
* Before this fix, processItem() called markProcessed() (rename → .processed)
|
|
5
|
+
* IMMEDIATELY after a fire-and-forget dispatch(). A crash before the spawned
|
|
6
|
+
* session closed left the user's inbox item `.processed` with redelivery
|
|
7
|
+
* blocked — the message was silently lost.
|
|
8
|
+
*
|
|
9
|
+
* The fix defers the finalize to the dispatch onClose SUCCESS path and records a
|
|
10
|
+
* durable in-flight admission so a restart re-delivers an un-finalized item:
|
|
11
|
+
* • clean close → markProcessed() + clearInFlight() (finalize)
|
|
12
|
+
* • non-zero / spawn error → clearInFlight() + releaseLock() (re-deliverable)
|
|
13
|
+
* • crash mid-flight → admission survives; recoverInFlight() (startup) releases
|
|
14
|
+
* the per-item lock so the still-un-`.processed` file is re-polled.
|
|
15
|
+
*
|
|
16
|
+
* These tests drive the REAL daemon helpers (recordInFlight / clearInFlight /
|
|
17
|
+
* markProcessed / recoverInFlight) and the REAL per-item lock from
|
|
18
|
+
* session-lock.mjs, asserting the on-disk state that the poller would observe.
|
|
19
|
+
*
|
|
20
|
+
* The daemon module statically pulls in the agent-only `imapflow` dep (via the
|
|
21
|
+
* gmail poller) which isn't packaged in CI, so we register a tiny in-process
|
|
22
|
+
* ESM loader that stubs `imapflow` to a no-op class. Everything else in the
|
|
23
|
+
* daemon's import graph resolves normally, so the code under test is the real
|
|
24
|
+
* implementation, not a re-creation.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { test, before } from "node:test";
|
|
28
|
+
import assert from "node:assert/strict";
|
|
29
|
+
import { register } from "node:module";
|
|
30
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync } from "node:fs";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
import { tmpdir } from "node:os";
|
|
33
|
+
|
|
34
|
+
// --- stub the agent-only `imapflow` dep so the daemon module can be imported ---
|
|
35
|
+
const loaderSrc = `
|
|
36
|
+
export async function resolve(specifier, context, next) {
|
|
37
|
+
if (specifier === "imapflow") return { url: "imapflow-stub:main", shortCircuit: true };
|
|
38
|
+
return next(specifier, context);
|
|
39
|
+
}
|
|
40
|
+
export async function load(url, context, next) {
|
|
41
|
+
if (url === "imapflow-stub:main") return { format: "module", shortCircuit: true, source: "export class ImapFlow {}" };
|
|
42
|
+
return next(url, context);
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
register(`data:text/javascript,${encodeURIComponent(loaderSrc)}`);
|
|
46
|
+
|
|
47
|
+
// One shared AGENT_DIR for the whole file: session-lock.mjs (imported transitively
|
|
48
|
+
// by the daemon) reads AGENT_DIR at module-eval time and is cached by URL, so all
|
|
49
|
+
// imports must agree on the dir. Per-test isolation comes from unique item ids and
|
|
50
|
+
// a clean-between-tests of the relevant state subdirs.
|
|
51
|
+
const AGENT_DIR = mkdtempSync(join(tmpdir(), "maestro-agent-daemon-"));
|
|
52
|
+
process.env.AGENT_DIR = AGENT_DIR;
|
|
53
|
+
process.env.MAESTRO_DAEMON_NO_AUTOSTART = "1"; // import without spinning the poll loops
|
|
54
|
+
|
|
55
|
+
let daemon; // the real daemon module (recordInFlight/clearInFlight/markProcessed/recoverInFlight)
|
|
56
|
+
let lock; // the real session-lock module (acquireLock/readLock)
|
|
57
|
+
let bus; // the real hook-bus module (getHookBus/resetHookBus) — shared singleton
|
|
58
|
+
let counters; // the real counters module (snapshot/resetMem) — shared transitive import
|
|
59
|
+
|
|
60
|
+
before(async () => {
|
|
61
|
+
mkdirSync(join(AGENT_DIR, "state", "inbox", "slack"), { recursive: true });
|
|
62
|
+
mkdirSync(join(AGENT_DIR, "state", "sessions", "locks"), { recursive: true });
|
|
63
|
+
const dURL = new URL("./agent-daemon.mjs", import.meta.url);
|
|
64
|
+
daemon = await import(`${dURL.href}?f1h2=${Math.random()}`);
|
|
65
|
+
lock = await import("./session-lock.mjs");
|
|
66
|
+
// These are the SAME singletons the daemon reaches transitively (imported by
|
|
67
|
+
// bare specifier, so the module cache shares them) — that's what lets the test
|
|
68
|
+
// emit on the same bus the daemon subscribed to and read the same counters.
|
|
69
|
+
bus = await import("../../lib/hooks/bus.mjs");
|
|
70
|
+
counters = await import("../../lib/diagnostics/counters.mjs");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Read the diagnostic-event rows the daemon appended to its tmp
|
|
74
|
+
// logs/diagnostics/<UTC-date>.jsonl stream. [] when none written.
|
|
75
|
+
function readDiagnostics() {
|
|
76
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
77
|
+
const path = join(AGENT_DIR, "logs", "diagnostics", `${date}.jsonl`);
|
|
78
|
+
let raw = "";
|
|
79
|
+
try { raw = readFileSync(path, "utf-8"); } catch { return []; }
|
|
80
|
+
return raw.split("\n").filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function slackInbox() { return join(AGENT_DIR, "state", "inbox", "slack"); }
|
|
84
|
+
function inflightDir() { return join(AGENT_DIR, "state", "inbox-inflight"); }
|
|
85
|
+
|
|
86
|
+
function resetState() {
|
|
87
|
+
for (const d of [slackInbox(), inflightDir(), join(AGENT_DIR, "state", "sessions", "locks")]) {
|
|
88
|
+
try { for (const f of readdirSync(d)) rmSync(join(d, f), { force: true }); } catch { /* */ }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Write a live (un-.processed) poller-style inbox file for an item, returning
|
|
94
|
+
* the filename. Mirrors the real poller naming (`<ts>-<item.id>.yaml`) so that
|
|
95
|
+
* markProcessed's id/raw_ref substring match finds it, exactly as in production.
|
|
96
|
+
*/
|
|
97
|
+
function writeLiveItem(item) {
|
|
98
|
+
const name = `2026-06-10T00-00-00Z-${item.id}.yaml`;
|
|
99
|
+
const body = [
|
|
100
|
+
`id: "${item.id}"`,
|
|
101
|
+
`service: "slack"`,
|
|
102
|
+
`channel_id: "${item.channel || "D1"}"`,
|
|
103
|
+
`timestamp: "2026-06-10T00:00:00Z"`,
|
|
104
|
+
`content: |`,
|
|
105
|
+
` hello`,
|
|
106
|
+
"",
|
|
107
|
+
].join("\n");
|
|
108
|
+
writeFileSync(join(slackInbox(), name), body);
|
|
109
|
+
return name;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function inboxFiles() { return readdirSync(slackInbox()).sort(); }
|
|
113
|
+
function inflightFiles() { try { return readdirSync(inflightDir()).filter((f) => f.endsWith(".json")); } catch { return []; } }
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// 1) Crash between dispatch and close → item is RE-DELIVERABLE (not .processed),
|
|
117
|
+
// and recoverInFlight() releases the gating lock so the next poll re-delivers.
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
test("F1/H2: a crash between dispatch and close leaves the item re-deliverable (not .processed)", () => {
|
|
121
|
+
resetState();
|
|
122
|
+
const item = { id: "MSG-CRASH", raw_ref: "slack:C1:1001", channel: "D1", sender: "ceo" };
|
|
123
|
+
const name = writeLiveItem(item);
|
|
124
|
+
|
|
125
|
+
// Simulate processItem up to (but not past) the dispatch: the poll filter holds
|
|
126
|
+
// the per-item lock, and the daemon records the durable in-flight admission.
|
|
127
|
+
const acq = lock.acquireLock(item.raw_ref, { sender: item.sender, channel: item.channel });
|
|
128
|
+
assert.equal(acq.acquired, true, "poll filter acquired the per-item lock");
|
|
129
|
+
daemon.recordInFlight(item, "slack");
|
|
130
|
+
|
|
131
|
+
// --- CRASH HERE: no onClose ran, so markProcessed was never called. ---
|
|
132
|
+
// The user's message must NOT be .processed (the whole bug).
|
|
133
|
+
assert.deepEqual(inboxFiles(), [name], "file is still live (un-.processed) after a mid-flight crash");
|
|
134
|
+
assert.equal(inflightFiles().length, 1, "the in-flight admission survives the crash");
|
|
135
|
+
// The held lock would block a naive re-poll until the 2h stale TTL — that is
|
|
136
|
+
// exactly what recovery must clear.
|
|
137
|
+
assert.equal(lock.acquireLock(item.raw_ref).acquired, false, "lock still held → re-poll blocked pre-recovery");
|
|
138
|
+
|
|
139
|
+
// --- RESTART: recoverInFlight() releases the lock so the next poll re-delivers. ---
|
|
140
|
+
const n = daemon.recoverInFlight();
|
|
141
|
+
assert.equal(n, 1, "one in-flight admission recovered");
|
|
142
|
+
assert.equal(inflightFiles().length, 0, "admission cleared after recovery");
|
|
143
|
+
assert.deepEqual(inboxFiles(), [name], "the message is STILL on disk, un-.processed → re-deliverable");
|
|
144
|
+
assert.equal(lock.acquireLock(item.raw_ref).acquired, true, "the gating lock was released → next poll re-delivers");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// 2) A clean close finalizes the item (markProcessed) and clears the admission.
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
test("F1/H2: a clean close finalizes the item (markProcessed) and clears the in-flight admission", () => {
|
|
152
|
+
resetState();
|
|
153
|
+
const item = { id: "MSG-OK", raw_ref: "slack:C2:2002", channel: "D2", sender: "ceo" };
|
|
154
|
+
const name = writeLiveItem(item);
|
|
155
|
+
lock.acquireLock(item.raw_ref, { sender: item.sender });
|
|
156
|
+
daemon.recordInFlight(item, "slack");
|
|
157
|
+
assert.equal(inflightFiles().length, 1);
|
|
158
|
+
|
|
159
|
+
// onClose({ok:true}) success path (the exact wiring processItem installs).
|
|
160
|
+
daemon.markProcessed(item, "slack");
|
|
161
|
+
daemon.clearInFlight(item);
|
|
162
|
+
|
|
163
|
+
assert.deepEqual(inboxFiles(), [`${name}.processed`], "clean close finalized the item to .processed");
|
|
164
|
+
assert.equal(inflightFiles().length, 0, "admission cleared on finalize");
|
|
165
|
+
// A clean close finalized work that already completed — recovery is a no-op,
|
|
166
|
+
// and there is no live file left for the poller to re-deliver.
|
|
167
|
+
assert.equal(daemon.recoverInFlight(), 0, "nothing to recover after a clean finalize");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// 3) A non-zero close / spawn error leaves the item re-deliverable and releases
|
|
172
|
+
// the lock immediately (no need to wait for restart).
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
test("F1/H2: a failed close leaves the item re-deliverable and releases the lock now", () => {
|
|
176
|
+
resetState();
|
|
177
|
+
const item = { id: "MSG-FAIL", raw_ref: "slack:C3:3003", channel: "D3", sender: "ceo" };
|
|
178
|
+
const name = writeLiveItem(item);
|
|
179
|
+
lock.acquireLock(item.raw_ref, { sender: item.sender });
|
|
180
|
+
daemon.recordInFlight(item, "slack");
|
|
181
|
+
|
|
182
|
+
// onClose({ok:false}) failure path: clear admission + release lock, but DO NOT
|
|
183
|
+
// markProcessed — the message stays re-deliverable.
|
|
184
|
+
daemon.clearInFlight(item);
|
|
185
|
+
lock.releaseLock(item.raw_ref);
|
|
186
|
+
|
|
187
|
+
assert.deepEqual(inboxFiles(), [name], "failed session left the item live (un-.processed)");
|
|
188
|
+
assert.equal(inflightFiles().length, 0, "admission cleared on failure");
|
|
189
|
+
assert.equal(lock.acquireLock(item.raw_ref).acquired, true, "lock released → next poll re-delivers immediately");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// 4) recoverInFlight() is a clean no-op when nothing is pending, and never
|
|
194
|
+
// touches already-.processed files.
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
test("F1/H2: recoverInFlight is a no-op with no pending admissions", () => {
|
|
198
|
+
resetState();
|
|
199
|
+
assert.equal(daemon.recoverInFlight(), 0, "no admissions → recovered 0");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("F1/H2: recordInFlight is a no-op for an item with no lock key (e.g. backlog)", () => {
|
|
203
|
+
resetState();
|
|
204
|
+
daemon.recordInFlight({ title: "deep work" }, "backlog");
|
|
205
|
+
assert.equal(inflightFiles().length, 0, "no raw_ref/id → no admission written (backlog items are durable in queue files)");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ===========================================================================
|
|
209
|
+
// DIAG — the observability spine wired into the LIVE inbound pipeline.
|
|
210
|
+
// ===========================================================================
|
|
211
|
+
//
|
|
212
|
+
// processItem() mints ONE trace_id per item and emits item_received → … → sent
|
|
213
|
+
// (on a clean finalize) all under it. The daemon also registers the ONE real
|
|
214
|
+
// hook-bus subscriber (onGuardFail → counters.bump("guard.fail")) so the bus is
|
|
215
|
+
// live. STRICT additive: emitEvent + counters.bump never throw, and the dispatch
|
|
216
|
+
// /classify/send semantics are untouched (the heavy collaborators are injected
|
|
217
|
+
// here ONLY so the test drives the real instrumented body without spawning a
|
|
218
|
+
// `claude` child — production passes no deps).
|
|
219
|
+
|
|
220
|
+
function clearDiagnostics() {
|
|
221
|
+
for (const sub of ["diagnostics", join("diagnostics", "counters")]) {
|
|
222
|
+
const d = join(AGENT_DIR, "logs", sub);
|
|
223
|
+
try { for (const f of readdirSync(d)) { if (f.endsWith(".jsonl")) rmSync(join(d, f), { force: true }); } } catch { /* */ }
|
|
224
|
+
}
|
|
225
|
+
try { counters.resetMem(); } catch { /* */ }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// 1) A processed item emits item_received → … → sent with ONE stable trace_id.
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
test("DIAG: a processed item emits item_received → sent with ONE stable trace_id (the daemon's minted id)", async () => {
|
|
233
|
+
resetState();
|
|
234
|
+
clearDiagnostics();
|
|
235
|
+
// A DM (channel_id starts with "D") so it always passes the directed-message
|
|
236
|
+
// gate — keeps the test on the dispatch path deterministically.
|
|
237
|
+
const item = { id: "MSG-TRACE", raw_ref: "slack:D1:1", channel: "dm/ceo", channel_id: "D1", sender: "ceo", content: "please draft the board memo" };
|
|
238
|
+
writeLiveItem(item);
|
|
239
|
+
|
|
240
|
+
// Inject the heavy collaborators so the REAL processItem body runs (trace mint,
|
|
241
|
+
// item_received emit, dispatch onClose → sent emit) without a claude spawn:
|
|
242
|
+
// - classify → "respond"/opus so it takes the holding+session (dispatch) path,
|
|
243
|
+
// NOT the quick-reply path.
|
|
244
|
+
// - isQuickReply → false to keep it on the dispatch path deterministically.
|
|
245
|
+
// - sendHoldingMessage → no-op (no Slack post).
|
|
246
|
+
// - dispatch → synchronously invoke onClose({ok:true}) to finalize.
|
|
247
|
+
let dispatchedItem = null;
|
|
248
|
+
await daemon.processItem(item, "slack", {
|
|
249
|
+
classify: async () => ({ priority: "critical", action: "respond", model: "opus", summary: "draft board memo", category: "action_required", directed_at_agent: true }),
|
|
250
|
+
isQuickReply: () => false,
|
|
251
|
+
sendHoldingMessage: async () => ({ sent: false, holdingText: null }),
|
|
252
|
+
dispatch: (_prompt, it, _cr, _src, opts) => { dispatchedItem = it; opts.onClose({ ok: true, code: 0 }); },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// The daemon set the minted id on the item and carried it onto the dispatch.
|
|
256
|
+
assert.ok(typeof item.trace_id === "string" && item.trace_id.length === 26, "a 26-char ULID trace_id was minted onto the item");
|
|
257
|
+
assert.equal(dispatchedItem.trace_id, item.trace_id, "the dispatch received the SAME trace_id");
|
|
258
|
+
|
|
259
|
+
const rows = readDiagnostics();
|
|
260
|
+
const received = rows.filter((r) => r.type === "item_received");
|
|
261
|
+
const sent = rows.filter((r) => r.type === "sent");
|
|
262
|
+
assert.equal(received.length, 1, "exactly one item_received");
|
|
263
|
+
assert.equal(sent.length, 1, "exactly one sent on clean finalize");
|
|
264
|
+
// The single-trace-id invariant: the first hop and the last hop share one id.
|
|
265
|
+
assert.equal(received[0].trace_id, item.trace_id, "item_received carries the minted trace_id");
|
|
266
|
+
assert.equal(sent[0].trace_id, item.trace_id, "sent carries the SAME trace_id (end-to-end correlation)");
|
|
267
|
+
// The in-flight admission also carries the trace (cheap ledger correlation).
|
|
268
|
+
assert.equal(sent[0].attrs.item_id, item.raw_ref, "sent records the item id");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("DIAG: a failed (ok:false) finalize emits error (not sent), still under the one trace_id", async () => {
|
|
272
|
+
resetState();
|
|
273
|
+
clearDiagnostics();
|
|
274
|
+
const item = { id: "MSG-TRACE-FAIL", raw_ref: "slack:D2:1", channel: "dm/ceo", channel_id: "D2", sender: "ceo", content: "do the thing" };
|
|
275
|
+
writeLiveItem(item);
|
|
276
|
+
|
|
277
|
+
await daemon.processItem(item, "slack", {
|
|
278
|
+
classify: async () => ({ priority: "high", action: "respond", model: "opus", summary: "x", category: "action_required", directed_at_agent: true }),
|
|
279
|
+
isQuickReply: () => false,
|
|
280
|
+
sendHoldingMessage: async () => ({ sent: false, holdingText: null }),
|
|
281
|
+
dispatch: (_p, _it, _cr, _s, opts) => opts.onClose({ ok: false, code: 1 }),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const rows = readDiagnostics();
|
|
285
|
+
assert.equal(rows.filter((r) => r.type === "item_received").length, 1, "item_received emitted");
|
|
286
|
+
assert.equal(rows.filter((r) => r.type === "sent").length, 0, "a failed finalize must NOT emit sent");
|
|
287
|
+
const err = rows.filter((r) => r.type === "error");
|
|
288
|
+
assert.equal(err.length, 1, "a failed finalize emits an error hop");
|
|
289
|
+
assert.equal(err[0].trace_id, item.trace_id, "the error hop is under the same interaction trace_id");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// 2) A send-gate block bumps the counter (responder's real instrumented path).
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
test("DIAG: a send-gate block bumps the send.blocked counter (responder validation path)", async () => {
|
|
297
|
+
clearDiagnostics();
|
|
298
|
+
const responder = await import("./responder.mjs");
|
|
299
|
+
// Force the generator to return text the REAL validateQuickReply blocks (AI
|
|
300
|
+
// self-disclosure) — no claude spawn, no Slack post. The block path must bump.
|
|
301
|
+
const restore = responder.setGenerateResponseForTests(async () => "Hi — I am an AI assistant, happy to help.");
|
|
302
|
+
try {
|
|
303
|
+
const before = (counters.snapshot()["send.blocked"]) || 0;
|
|
304
|
+
const res = await responder.sendQuickResponse(
|
|
305
|
+
{ sender: "ceo", service: "slack", channel: "D1", content: "hello" },
|
|
306
|
+
{ priority: "normal", action: "respond", model: "sonnet", summary: "hi" }
|
|
307
|
+
);
|
|
308
|
+
assert.equal(res.sent, false, "the AI-disclosure reply was blocked, not sent");
|
|
309
|
+
assert.equal(res.blocked, true);
|
|
310
|
+
const after = (counters.snapshot()["send.blocked"]) || 0;
|
|
311
|
+
assert.equal(after, before + 1, "the send-gate block bumped send.blocked by exactly one (durable JSONL)");
|
|
312
|
+
} finally { restore(); }
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// 3) The daemon's ONE real onGuardFail subscriber fires (the bus is live).
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
test("DIAG: registerHookSubscribers wires a real onGuardFail subscriber that bumps guard.fail", async () => {
|
|
320
|
+
clearDiagnostics();
|
|
321
|
+
// The daemon registers exactly one onGuardFail subscriber. It's idempotent, so
|
|
322
|
+
// calling it here (the test driver) is safe even though main() also would.
|
|
323
|
+
assert.equal(daemon.registerHookSubscribers(), true, "subscriber registered");
|
|
324
|
+
const hb = bus.getHookBus();
|
|
325
|
+
assert.ok(hb.count("onGuardFail") >= 1, "the bus has a live onGuardFail subscriber (not dead)");
|
|
326
|
+
|
|
327
|
+
const before = (counters.snapshot()["guard.fail"]) || 0;
|
|
328
|
+
// Emit a guard-fail (as the responder's send-gate fail-open path does). The
|
|
329
|
+
// real subscriber must fire and bump the durable counter.
|
|
330
|
+
const decision = await hb.emit("onGuardFail", { guard: "send_gate", reason: "policy unreadable", failed_open: true });
|
|
331
|
+
assert.equal(decision.cancelled, false, "onGuardFail is observe-only (never cancels)");
|
|
332
|
+
assert.equal(decision.errors, 0, "the subscriber did not throw");
|
|
333
|
+
|
|
334
|
+
const after = (counters.snapshot()["guard.fail"]) || 0;
|
|
335
|
+
assert.equal(after, before + 1, "the onGuardFail subscriber bumped guard.fail by exactly one");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("DIAG: the responder's send-gate fail-open emits onGuardFail onto the live bus → guard.fail bumps", async () => {
|
|
339
|
+
clearDiagnostics();
|
|
340
|
+
daemon.registerHookSubscribers(); // ensure the real subscriber is live
|
|
341
|
+
const responder = await import("./responder.mjs");
|
|
342
|
+
// Generate benign text (passes validation), but force the Slack send-gate
|
|
343
|
+
// screen to throw a NON-block infra error so the responder fails OPEN and
|
|
344
|
+
// emits onGuardFail. We can't reach sendSlackMessage without a token, so we
|
|
345
|
+
// assert the wiring via the bus directly using the same emit the responder
|
|
346
|
+
// makes — the prior test proves the subscriber; here we prove a real producer
|
|
347
|
+
// shape lands on the same singleton bus the daemon subscribed to.
|
|
348
|
+
const hb = bus.getHookBus();
|
|
349
|
+
const before = (counters.snapshot()["guard.fail"]) || 0;
|
|
350
|
+
await hb.emit("onGuardFail", { guard: "send_gate", channel: "slack", recipient: "D1", reason: "gate module unreadable", failed_open: true });
|
|
351
|
+
const after = (counters.snapshot()["guard.fail"]) || 0;
|
|
352
|
+
assert.equal(after, before + 1, "a send-gate fail-open shape bumps guard.fail through the live subscriber");
|
|
353
|
+
// Tidy: drop the test reference so it doesn't leak the import.
|
|
354
|
+
void responder;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Post-session org-memory write-back (SP4) — after a clean turn, a bounded
|
|
359
|
+
// distillation is authored to the shared store via remember(). Fail-open when
|
|
360
|
+
// the org integration is disabled; never throws.
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
const ENABLED_ORG_CFG = { org: { cohort: { enabled: true, base: "https://org.example" } } };
|
|
364
|
+
|
|
365
|
+
test("MEMORY: sessionDistillation produces a bounded one-line record", () => {
|
|
366
|
+
const line = daemon.sessionDistillation(
|
|
367
|
+
{ sender: "Alice Smith", subject: "Q2 renewal" },
|
|
368
|
+
{ action: "respond", summary: "Acme renewal terms and next steps" },
|
|
369
|
+
{ agent: { firstName: "Jordan" } }
|
|
370
|
+
);
|
|
371
|
+
assert.ok(line, "should produce a distillation");
|
|
372
|
+
assert.ok(line.includes("Jordan"), "names the agent");
|
|
373
|
+
assert.ok(line.includes("respond"), "names the action");
|
|
374
|
+
assert.ok(line.includes("Alice Smith"), "names the sender");
|
|
375
|
+
assert.ok(line.includes("Acme renewal"), "carries the topic");
|
|
376
|
+
assert.ok(line.length <= 401, `bounded length, got ${line.length}`);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("MEMORY: sessionDistillation returns null when there is nothing to remember", () => {
|
|
380
|
+
const line = daemon.sessionDistillation({}, {}, { agent: { firstName: "Jordan" } });
|
|
381
|
+
assert.equal(line, null, "no sender + no topic ⇒ null");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("MEMORY: writeBackSession authors to the store when the org integration is enabled", async () => {
|
|
385
|
+
const calls = [];
|
|
386
|
+
const res = await daemon.writeBackSession(
|
|
387
|
+
{ sender: "Alice Smith", subject: "Q2 renewal" },
|
|
388
|
+
{ action: "respond", summary: "Acme renewal terms" },
|
|
389
|
+
{
|
|
390
|
+
cfg: ENABLED_ORG_CFG,
|
|
391
|
+
agent: { firstName: "Jordan" },
|
|
392
|
+
sessionId: "sess-xyz",
|
|
393
|
+
rememberImpl: async (text, opts) => { calls.push({ text, opts }); return { ok: true, mode: "append", id: "k1" }; },
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
assert.equal(res.ok, true, "write reported ok");
|
|
397
|
+
assert.equal(calls.length, 1, "remember() called exactly once");
|
|
398
|
+
assert.ok(calls[0].text.includes("Jordan"), "authored the distillation text");
|
|
399
|
+
assert.equal(calls[0].opts.cfg, ENABLED_ORG_CFG, "passed the org cfg through");
|
|
400
|
+
assert.equal(calls[0].opts.provenance.sessionId, "sess-xyz", "carried session provenance");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("MEMORY: writeBackSession is a fail-open no-op when the org integration is disabled", async () => {
|
|
404
|
+
let called = false;
|
|
405
|
+
const res = await daemon.writeBackSession(
|
|
406
|
+
{ sender: "Alice Smith" },
|
|
407
|
+
{ action: "respond", summary: "x" },
|
|
408
|
+
{ cfg: {}, rememberImpl: async () => { called = true; return { ok: true }; } }
|
|
409
|
+
);
|
|
410
|
+
assert.equal(res.skipped, true, "skipped when disabled");
|
|
411
|
+
assert.equal(called, false, "remember() not called when disabled");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("MEMORY: writeBackSession never throws even if remember() rejects", async () => {
|
|
415
|
+
const res = await daemon.writeBackSession(
|
|
416
|
+
{ sender: "Alice Smith", subject: "s" },
|
|
417
|
+
{ action: "respond", summary: "y" },
|
|
418
|
+
{ cfg: ENABLED_ORG_CFG, rememberImpl: async () => { throw new Error("network down"); } }
|
|
419
|
+
);
|
|
420
|
+
assert.equal(res.ok, false, "fail-open result");
|
|
421
|
+
assert.equal(res.skipped, true, "reported skipped on throw");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// Post-session OUTCOME DISCIPLINE — the daemon's parity with hq's meeting chair.
|
|
426
|
+
// A turn that committed to next steps must not leave them in a session log; the
|
|
427
|
+
// close path lands them through the org protocol. OFF unless the flag is set.
|
|
428
|
+
// The parsing/landing itself is covered by session-outcomes.test.mjs — these
|
|
429
|
+
// tests assert the DAEMON's wiring: identity + config injection, and fail-open.
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
test("OUTCOMES: the daemon hook lands a turn's commitments through the org protocol", async () => {
|
|
433
|
+
const created = [];
|
|
434
|
+
const remembered = [];
|
|
435
|
+
const res = await daemon.sweepOutcomesForSession({
|
|
436
|
+
sessionId: "s-daemon-1",
|
|
437
|
+
resultText: "Handled it.\n\nOUTCOMES:\n- Send the revised deck to Alice — due: Monday\n",
|
|
438
|
+
enabled: true,
|
|
439
|
+
cfg: ENABLED_ORG_CFG,
|
|
440
|
+
agent: { firstName: "Robin" },
|
|
441
|
+
item: {
|
|
442
|
+
service: "slack",
|
|
443
|
+
channel: "general",
|
|
444
|
+
channel_id: "C0GENERAL",
|
|
445
|
+
// The PUBLIC signal. A `C…` id alone proves nothing — Slack mints them
|
|
446
|
+
// for private channels too — so the gate needs the conversation type.
|
|
447
|
+
channel_type: "channel",
|
|
448
|
+
sender: "Alice Smith",
|
|
449
|
+
subject: "Deck review",
|
|
450
|
+
},
|
|
451
|
+
classResult: { action: "respond", summary: "deck review" },
|
|
452
|
+
createItemImpl: async (item) => { created.push(item); return { ok: true }; },
|
|
453
|
+
rememberImpl: async (text, opts) => { remembered.push({ text, opts }); return { ok: true }; },
|
|
454
|
+
});
|
|
455
|
+
assert.equal(res.created, 1, "the commitment became a board item");
|
|
456
|
+
assert.equal(created[0].title, "Send the revised deck to Alice");
|
|
457
|
+
assert.ok(created[0].detail.includes("Robin"), "the daemon supplied the agent identity");
|
|
458
|
+
assert.equal(remembered.length, 1, "and one knowledge episode records the set");
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("OUTCOMES: a private DM's commitments never leave the DM", async () => {
|
|
462
|
+
// CONFIDENTIALITY. A landed outcome is an ORG-VISIBLE artifact: board.create
|
|
463
|
+
// with no workstreamId is channel-less, and the episode's knowledge.append
|
|
464
|
+
// carries no group (so hq resolves it to the `org` group). Landing a DM's
|
|
465
|
+
// commitments would republish a one-to-one conversation — including its topic
|
|
466
|
+
// line — to every member of the org. The gate is fail-closed.
|
|
467
|
+
let touched = false;
|
|
468
|
+
const res = await daemon.sweepOutcomesForSession({
|
|
469
|
+
sessionId: "s-daemon-dm",
|
|
470
|
+
resultText: "OUTCOMES:\n- Send Alice the confidential term sheet — due: Monday\n",
|
|
471
|
+
enabled: true,
|
|
472
|
+
cfg: ENABLED_ORG_CFG,
|
|
473
|
+
agent: { firstName: "Robin" },
|
|
474
|
+
item: {
|
|
475
|
+
service: "slack",
|
|
476
|
+
channel: "dm/alice-smith",
|
|
477
|
+
channel_id: "D01ALICE",
|
|
478
|
+
is_dm: true,
|
|
479
|
+
sender: "Alice Smith",
|
|
480
|
+
subject: "DM from Alice Smith",
|
|
481
|
+
},
|
|
482
|
+
classResult: { action: "respond", summary: "confidential term sheet" },
|
|
483
|
+
createItemImpl: async () => { touched = true; return { ok: true }; },
|
|
484
|
+
rememberImpl: async () => { touched = true; return { ok: true }; },
|
|
485
|
+
});
|
|
486
|
+
assert.equal(res.created, 0);
|
|
487
|
+
assert.equal(res.reason, "source-dm");
|
|
488
|
+
assert.equal(touched, false, "no board row and no episode for a DM turn");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("OUTCOMES: the daemon hook is OFF unless DAEMON_SESSION_OUTCOMES=1", async () => {
|
|
492
|
+
let touched = false;
|
|
493
|
+
const prior = process.env.DAEMON_SESSION_OUTCOMES;
|
|
494
|
+
try {
|
|
495
|
+
delete process.env.DAEMON_SESSION_OUTCOMES;
|
|
496
|
+
const res = await daemon.sweepOutcomesForSession({
|
|
497
|
+
sessionId: "s-daemon-2",
|
|
498
|
+
resultText: "OUTCOMES:\n- Do the thing",
|
|
499
|
+
cfg: ENABLED_ORG_CFG,
|
|
500
|
+
agent: { firstName: "Robin" },
|
|
501
|
+
createItemImpl: async () => { touched = true; return { ok: true }; },
|
|
502
|
+
rememberImpl: async () => { touched = true; return { ok: true }; },
|
|
503
|
+
});
|
|
504
|
+
assert.equal(res.skipped, true);
|
|
505
|
+
assert.equal(res.reason, "disabled");
|
|
506
|
+
assert.equal(touched, false, "nothing is written while the flag is off");
|
|
507
|
+
} finally {
|
|
508
|
+
if (prior === undefined) delete process.env.DAEMON_SESSION_OUTCOMES;
|
|
509
|
+
else process.env.DAEMON_SESSION_OUTCOMES = prior;
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("OUTCOMES: the daemon hook never throws into the dispatch path", async () => {
|
|
514
|
+
const res = await daemon.sweepOutcomesForSession({
|
|
515
|
+
sessionId: "s-daemon-3",
|
|
516
|
+
stdout: "{{{ not json",
|
|
517
|
+
enabled: true,
|
|
518
|
+
cfg: ENABLED_ORG_CFG,
|
|
519
|
+
agent: { firstName: "Robin" },
|
|
520
|
+
createItemImpl: async () => { throw new Error("network down"); },
|
|
521
|
+
rememberImpl: async () => { throw new Error("network down"); },
|
|
522
|
+
});
|
|
523
|
+
assert.ok(res && typeof res.created === "number", "benign result, no throw");
|
|
524
|
+
assert.equal(res.created, 0);
|
|
525
|
+
});
|