@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,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* org.test.mjs — org-enrollment setup section (order 75) tests.
|
|
3
|
+
* Run: node --test lib/setup/sections/org.test.mjs
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { mkdtempSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join, dirname } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import yaml from "js-yaml";
|
|
14
|
+
|
|
15
|
+
import org, { buildOrgConfig, readOrgConfig, pullProfileIntoAgentConfig, orgContextShapesBacklog, requestPairing, deriveAgentIdentity } from "./org.mjs";
|
|
16
|
+
import { isEnabled } from "../../org/client.mjs";
|
|
17
|
+
import { readAgentJson, writeAgentJson } from "../sot.mjs";
|
|
18
|
+
import * as io from "../io.mjs";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A deterministic fake fetch for the pairing handshake: records the calls and
|
|
22
|
+
* replies with a pending pairing frame (the shape hq's pairing.request returns).
|
|
23
|
+
* Never touches the network so the pairing tests stay hermetic + fast.
|
|
24
|
+
*/
|
|
25
|
+
function fakePairFetch(reply) {
|
|
26
|
+
const calls = [];
|
|
27
|
+
const fn = async (url, init) => {
|
|
28
|
+
calls.push({ url: String(url), init, body: init && init.body ? JSON.parse(init.body) : null });
|
|
29
|
+
const r = reply ? reply(url, init) : { ok: true, status: 200, body: { ok: true, result: { status: "pending", code: "PAIR-99" } } };
|
|
30
|
+
return { ok: r.ok !== false, status: r.status || 200, headers: { get: () => undefined }, json: async () => r.body };
|
|
31
|
+
};
|
|
32
|
+
fn.calls = calls;
|
|
33
|
+
return fn;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MAESTRO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
37
|
+
|
|
38
|
+
function tmpRoot() {
|
|
39
|
+
const root = mkdtempSync(join(tmpdir(), "maestro-orgsec-"));
|
|
40
|
+
mkdirSync(join(root, "config"), { recursive: true });
|
|
41
|
+
return root;
|
|
42
|
+
}
|
|
43
|
+
function ctxFor(root, answers = {}) {
|
|
44
|
+
return { agentRoot: root, maestroRoot: MAESTRO_ROOT, flags: { headless: true }, answers, io };
|
|
45
|
+
}
|
|
46
|
+
function readCfg(root) {
|
|
47
|
+
return yaml.load(readFileSync(join(root, "config", "org.yaml"), "utf8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("section shape: id/order=75/hooks", () => {
|
|
51
|
+
assert.equal(org.id, "org");
|
|
52
|
+
assert.equal(org.order, 75);
|
|
53
|
+
for (const h of ["detect", "prompt", "apply", "verify"]) assert.equal(typeof org[h], "function");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("orgContextShapesBacklog: true iff strategy streams or a ready board are present", () => {
|
|
57
|
+
assert.equal(orgContextShapesBacklog(null), false);
|
|
58
|
+
assert.equal(orgContextShapesBacklog({}), false);
|
|
59
|
+
assert.equal(orgContextShapesBacklog({ strategy: {}, board: [] }), false);
|
|
60
|
+
assert.equal(orgContextShapesBacklog({ strategy: { streams: [{ code: "GTM" }] } }), true);
|
|
61
|
+
assert.equal(orgContextShapesBacklog({ board: [{ title: "do a thing" }] }), true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("detect: unconfigured before apply", async () => {
|
|
65
|
+
const root = tmpRoot();
|
|
66
|
+
const before = await org.detect(ctxFor(root));
|
|
67
|
+
assert.equal(before.status, "unconfigured");
|
|
68
|
+
assert.ok(before.missing.includes("config/org.yaml"));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("apply with base + token writes a valid, ENABLED config/org.yaml", async () => {
|
|
72
|
+
const root = tmpRoot();
|
|
73
|
+
await org.apply(ctxFor(root, { orgBase: "https://cohort.example", orgId: "acme", orgToken: "tok-123" }));
|
|
74
|
+
assert.ok(existsSync(join(root, "config", "org.yaml")));
|
|
75
|
+
const cfg = readCfg(root);
|
|
76
|
+
assert.equal(cfg.org.cohort.base, "https://cohort.example");
|
|
77
|
+
assert.equal(cfg.org.cohort.orgId, "acme");
|
|
78
|
+
assert.equal(cfg.org.cohort.token, "tok-123");
|
|
79
|
+
assert.equal(cfg.org.cohort.enabled, true);
|
|
80
|
+
// The shape is exactly what lib/org/client.isEnabled() consumes.
|
|
81
|
+
assert.equal(isEnabled(cfg), true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("apply trims a trailing slash from the base URL", async () => {
|
|
85
|
+
const root = tmpRoot();
|
|
86
|
+
await org.apply(ctxFor(root, { orgBase: "https://cohort.example/", orgToken: "tok" }));
|
|
87
|
+
assert.equal(readCfg(root).org.cohort.base, "https://cohort.example");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("apply with a pairing code but no token records intent but stays DISABLED", async () => {
|
|
91
|
+
const root = tmpRoot();
|
|
92
|
+
const f = fakePairFetch();
|
|
93
|
+
const ctx = { ...ctxFor(root, { orgBase: "https://cohort.example", orgPairingCode: "PAIR-99" }), pairFetchImpl: f };
|
|
94
|
+
await org.apply(ctx);
|
|
95
|
+
const cfg = readCfg(root);
|
|
96
|
+
assert.equal(cfg.org.cohort.base, "https://cohort.example");
|
|
97
|
+
assert.equal(cfg.org.cohort.enabled, false, "no token yet → fail-closed on the credential");
|
|
98
|
+
assert.equal(isEnabled(cfg), false);
|
|
99
|
+
// The handshake ran (pre-auth, no bearer) and the intent is persisted.
|
|
100
|
+
assert.equal(f.calls.length, 1);
|
|
101
|
+
assert.equal(f.calls[0].url, "https://cohort.example/api/v1/pairing.request");
|
|
102
|
+
assert.equal(f.calls[0].init.headers.authorization, undefined, "pairing.request is UNAUTHENTICATED");
|
|
103
|
+
assert.equal(f.calls[0].body.code, "PAIR-99");
|
|
104
|
+
assert.ok(f.calls[0].body.pubkey, "the device public key rides in the handshake");
|
|
105
|
+
assert.equal(cfg.org.cohort.pairing.code, "PAIR-99");
|
|
106
|
+
assert.equal(cfg.org.cohort.pairing.status, "pending");
|
|
107
|
+
assert.equal(ctx.answers.pairingRequested, true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("requestPairing: derives agentId from agent.json, persists pairing intent, stays disabled", async () => {
|
|
111
|
+
const root = tmpRoot();
|
|
112
|
+
writeAgentJson(root, { firstName: "Alex", lastName: "Rivera", email: "alex.rivera@acme.com", fullName: "Alex Rivera" });
|
|
113
|
+
const f = fakePairFetch();
|
|
114
|
+
const res = await requestPairing(root, { base: "https://cohort.example/", code: "TEAM-7", orgId: "acme", fetchImpl: f });
|
|
115
|
+
assert.equal(res.ok, true);
|
|
116
|
+
assert.equal(res.agentId, "alex-rivera");
|
|
117
|
+
assert.equal(res.status, "pending");
|
|
118
|
+
assert.equal(f.calls[0].body.agentId, "alex-rivera");
|
|
119
|
+
assert.equal(f.calls[0].body.displayName, "Alex Rivera");
|
|
120
|
+
const cfg = readCfg(root);
|
|
121
|
+
assert.equal(cfg.org.cohort.pairing.code, "TEAM-7");
|
|
122
|
+
assert.equal(cfg.org.cohort.orgId, "acme");
|
|
123
|
+
assert.equal(isEnabled(cfg), false, "pairing alone never enables the plane");
|
|
124
|
+
// A device key was materialised under state/org/.
|
|
125
|
+
assert.ok(existsSync(join(root, "state", "org", "device-key.pem")));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("requestPairing: short/absent code → fail-open error, no fetch", async () => {
|
|
129
|
+
const root = tmpRoot();
|
|
130
|
+
const f = fakePairFetch();
|
|
131
|
+
assert.equal((await requestPairing(root, { base: "https://n", code: "ab", fetchImpl: f })).ok, false);
|
|
132
|
+
assert.equal((await requestPairing(root, { code: "abcd", fetchImpl: f })).ok, false, "missing base");
|
|
133
|
+
assert.equal(f.calls.length, 0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("requestPairing: unreachable server records intent + returns ok:false (fail-open)", async () => {
|
|
137
|
+
const root = tmpRoot();
|
|
138
|
+
const f = fakePairFetch(() => ({ ok: false, status: 0, body: null }));
|
|
139
|
+
const res = await requestPairing(root, { base: "https://cohort.example", code: "DOWN-1", fetchImpl: f });
|
|
140
|
+
assert.equal(res.ok, false);
|
|
141
|
+
// The intent is still persisted so a later `maestro pair` / setup knows about it.
|
|
142
|
+
const cfg = readCfg(root);
|
|
143
|
+
assert.equal(cfg.org.cohort.pairing.code, "DOWN-1");
|
|
144
|
+
assert.equal(cfg.org.cohort.pairing.status, "error");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("deriveAgentIdentity: COHORT_AGENT_ID wins, else email local-part, else name", () => {
|
|
148
|
+
const root = tmpRoot();
|
|
149
|
+
writeAgentJson(root, { email: "robin.vega@x.io", fullName: "Robin Vega" });
|
|
150
|
+
assert.equal(deriveAgentIdentity(root).agentId, "robin-vega");
|
|
151
|
+
const saved = process.env.COHORT_AGENT_ID;
|
|
152
|
+
process.env.COHORT_AGENT_ID = "Robin The Bot";
|
|
153
|
+
try { assert.equal(deriveAgentIdentity(root).agentId, "robin-the-bot"); }
|
|
154
|
+
finally { if (saved === undefined) delete process.env.COHORT_AGENT_ID; else process.env.COHORT_AGENT_ID = saved; }
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("detect: surfaces a pending pairing handshake honestly", async () => {
|
|
158
|
+
const root = tmpRoot();
|
|
159
|
+
const f = fakePairFetch();
|
|
160
|
+
await org.apply({ ...ctxFor(root, { orgBase: "https://cohort.example", orgPairingCode: "SEE-ME" }), pairFetchImpl: f });
|
|
161
|
+
const d = await org.detect(ctxFor(root));
|
|
162
|
+
assert.equal(d.status, "partial");
|
|
163
|
+
assert.match(d.summary, /pairing pending \(code SEE-ME\)/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("apply with nothing supplied writes a disabled stub (idempotent artefact)", async () => {
|
|
167
|
+
const root = tmpRoot();
|
|
168
|
+
await org.apply(ctxFor(root, {}));
|
|
169
|
+
assert.ok(existsSync(join(root, "config", "org.yaml")));
|
|
170
|
+
const cfg = readCfg(root);
|
|
171
|
+
assert.equal(isEnabled(cfg), false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("apply is idempotent and preserves operator fields (trustedSignersPem)", async () => {
|
|
175
|
+
const root = tmpRoot();
|
|
176
|
+
// Operator pre-seeds pinned signer PEMs.
|
|
177
|
+
writeFileSync(
|
|
178
|
+
join(root, "config", "org.yaml"),
|
|
179
|
+
yaml.dump({ org: { cohort: { base: "https://old", token: "old-tok", trustedSignersPem: ["-----PEM-----"] } } })
|
|
180
|
+
);
|
|
181
|
+
await org.apply(ctxFor(root, { orgBase: "https://cohort.new", orgToken: "new-tok" }));
|
|
182
|
+
const cfg = readCfg(root);
|
|
183
|
+
assert.equal(cfg.org.cohort.base, "https://cohort.new");
|
|
184
|
+
assert.equal(cfg.org.cohort.token, "new-tok");
|
|
185
|
+
// The pinned signer PEMs survive the merge.
|
|
186
|
+
assert.deepEqual(cfg.org.cohort.trustedSignersPem, ["-----PEM-----"]);
|
|
187
|
+
// Re-applying with no answers must not clobber the now-enabled config.
|
|
188
|
+
await org.apply(ctxFor(root, {}));
|
|
189
|
+
const cfg2 = readCfg(root);
|
|
190
|
+
assert.equal(cfg2.org.cohort.base, "https://cohort.new");
|
|
191
|
+
assert.equal(cfg2.org.cohort.token, "new-tok");
|
|
192
|
+
assert.equal(isEnabled(cfg2), true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("buildOrgConfig: pure merge keeps existing cohort fields", () => {
|
|
196
|
+
const existing = { org: { cohort: { base: "https://a", token: "t", orgId: "x" } }, other: 1 };
|
|
197
|
+
const next = buildOrgConfig(existing, { orgId: undefined });
|
|
198
|
+
assert.equal(next.other, 1, "unrelated top-level keys preserved");
|
|
199
|
+
assert.equal(next.org.cohort.base, "https://a");
|
|
200
|
+
assert.equal(next.org.cohort.orgId, "x");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("readOrgConfig returns {} when file absent (never throws)", async () => {
|
|
204
|
+
const root = tmpRoot();
|
|
205
|
+
assert.deepEqual(await readOrgConfig(root), {});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("detect: complete after an enabled apply", async () => {
|
|
209
|
+
const root = tmpRoot();
|
|
210
|
+
await org.apply(ctxFor(root, { orgBase: "https://cohort.example", orgToken: "tok" }));
|
|
211
|
+
const d = await org.detect(ctxFor(root));
|
|
212
|
+
assert.equal(d.status, "complete");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("detect: partial when file present but not enabled", async () => {
|
|
216
|
+
const root = tmpRoot();
|
|
217
|
+
// A too-short code never opens a handshake (requestPairing fail-opens before
|
|
218
|
+
// any fetch), so this exercises the plain not-enabled partial branch.
|
|
219
|
+
const f = fakePairFetch();
|
|
220
|
+
await org.apply({ ...ctxFor(root, { orgBase: "https://cohort.example", orgPairingCode: "P" }), pairFetchImpl: f });
|
|
221
|
+
assert.equal(f.calls.length, 0, "a <4-char code does not hit the network");
|
|
222
|
+
const d = await org.detect(ctxFor(root));
|
|
223
|
+
assert.equal(d.status, "partial");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("verify is NON-BLOCKING and never hard-fails when not enrolled", async () => {
|
|
227
|
+
const root = tmpRoot();
|
|
228
|
+
await org.apply(ctxFor(root, {})); // disabled stub
|
|
229
|
+
const res = await org.verify(ctxFor(root));
|
|
230
|
+
assert.equal(res.ok, true, "an un-enrolled agent must not fail setup");
|
|
231
|
+
const byName = Object.fromEntries(res.checks.map((c) => [c.name, c.ok]));
|
|
232
|
+
assert.equal(byName["config/org.yaml present"], true);
|
|
233
|
+
// The optional-enrollment warn is present with a remedy.
|
|
234
|
+
const warn = res.checks.find((c) => /not enrolled/.test(c.name));
|
|
235
|
+
assert.ok(warn && warn.remedy);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// --- Cohort-driven setup pull (COHORT_API_KEY + COHORT_ORG_ID) ----------
|
|
239
|
+
|
|
240
|
+
const PULL_PROFILE = {
|
|
241
|
+
slug: "robin", kind: "agent", displayName: "Robin Vega", firstName: "Robin", lastName: "Vega",
|
|
242
|
+
title: "VP Eng", team: "Platform", role: "engineering-lead", agentEmail: "robin@example.com",
|
|
243
|
+
supervisor: { displayName: "Layla Al-Masri", title: "CTO", email: "layla@example.com" },
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function withCohortEnv(vars, fn) {
|
|
247
|
+
const keys = ["COHORT_API_KEY", "COHORT_ORG_ID", "COHORT_AGENT_ID", "COHORT_BASE"];
|
|
248
|
+
const saved = Object.fromEntries(keys.map((k) => [k, process.env[k]]));
|
|
249
|
+
for (const k of keys) delete process.env[k];
|
|
250
|
+
Object.assign(process.env, vars);
|
|
251
|
+
return Promise.resolve()
|
|
252
|
+
.then(fn)
|
|
253
|
+
.finally(() => { for (const k of keys) { if (saved[k] === undefined) delete process.env[k]; else process.env[k] = saved[k]; } });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
test("pullProfileIntoAgentConfig: env + mocked profile populates config/agent.json", async () => {
|
|
257
|
+
const root = tmpRoot();
|
|
258
|
+
await withCohortEnv({ COHORT_API_KEY: "k", COHORT_ORG_ID: "acme", COHORT_AGENT_ID: "robin", COHORT_BASE: "https://cohort.example" }, async () => {
|
|
259
|
+
const plan = await pullProfileIntoAgentConfig(root, { fetchSelfProfileImpl: async () => ({ ok: true, result: PULL_PROFILE }) });
|
|
260
|
+
assert.equal(plan.pulled, true);
|
|
261
|
+
});
|
|
262
|
+
const id = readAgentJson(root);
|
|
263
|
+
assert.equal(id.firstName, "Robin");
|
|
264
|
+
assert.equal(id.email, "robin@example.com");
|
|
265
|
+
assert.equal(id.function, "technical-leader");
|
|
266
|
+
assert.equal(id.principal.fullName, "Layla Al-Masri");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("apply: with COHORT_* env present, pulls the profile into config/agent.json", async () => {
|
|
270
|
+
const root = tmpRoot();
|
|
271
|
+
// The real fetchSelfProfile is exercised via an injected global fetch.
|
|
272
|
+
const savedFetch = globalThis.fetch;
|
|
273
|
+
const calls = [];
|
|
274
|
+
globalThis.fetch = async (url) => {
|
|
275
|
+
calls.push(String(url));
|
|
276
|
+
return { ok: true, status: 200, headers: { get: () => undefined }, json: async () => PULL_PROFILE };
|
|
277
|
+
};
|
|
278
|
+
try {
|
|
279
|
+
await withCohortEnv({ COHORT_API_KEY: "k", COHORT_ORG_ID: "acme", COHORT_AGENT_ID: "robin", COHORT_BASE: "https://cohort.example" }, async () => {
|
|
280
|
+
const ctx = ctxFor(root, { orgBase: "https://cohort.example", orgToken: "tok" });
|
|
281
|
+
await org.apply(ctx);
|
|
282
|
+
assert.equal(ctx.answers.cohortPulled, true, "apply records that a pull happened");
|
|
283
|
+
});
|
|
284
|
+
} finally {
|
|
285
|
+
globalThis.fetch = savedFetch;
|
|
286
|
+
}
|
|
287
|
+
assert.ok(calls.some((u) => u.includes("/api/v1/org/members/robin")), "hit the org-data members endpoint");
|
|
288
|
+
const id = readAgentJson(root);
|
|
289
|
+
assert.equal(id.firstName, "Robin");
|
|
290
|
+
assert.equal(id.title, "VP Eng");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("apply: absent COHORT_* env → no pull, config/agent.json untouched (manual path)", async () => {
|
|
294
|
+
const root = tmpRoot();
|
|
295
|
+
await withCohortEnv({}, async () => {
|
|
296
|
+
const ctx = ctxFor(root, { orgBase: "https://cohort.example", orgToken: "tok" });
|
|
297
|
+
await org.apply(ctx);
|
|
298
|
+
assert.equal(ctx.answers.cohortPulled, undefined);
|
|
299
|
+
});
|
|
300
|
+
assert.equal(readAgentJson(root), null, "no agent.json written by the org section");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("verify does NOT block / hard-fail on an unreachable enabled server (fail-open)", async () => {
|
|
304
|
+
const root = tmpRoot();
|
|
305
|
+
// Point at an unroutable endpoint; the client's fetch will fail fast.
|
|
306
|
+
await org.apply(ctxFor(root, { orgBase: "http://127.0.0.1:0", orgToken: "tok" }));
|
|
307
|
+
const res = await org.verify(ctxFor(root));
|
|
308
|
+
// Reachability is best-effort: ok stays true so setup never blocks on the network.
|
|
309
|
+
assert.equal(res.ok, true);
|
|
310
|
+
const reach = res.checks.find((c) => /reachab/i.test(c.name));
|
|
311
|
+
assert.ok(reach, "a reachability check is recorded");
|
|
312
|
+
assert.equal(reach.ok, true, "reachability must never hard-fail setup");
|
|
313
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sections/orgmail.mjs — workspace email (Cohort-hosted mailbox) enablement
|
|
3
|
+
* (order 77).
|
|
4
|
+
*
|
|
5
|
+
* The wizard step that turns on the `orgmail` channel: the agent's REAL email
|
|
6
|
+
* address at the org's verified domain, transported entirely over org RPC (no
|
|
7
|
+
* IMAP, no app passwords — companion spec 2026-07-21). It is a thin follow-on
|
|
8
|
+
* to org (75) and messaging (76): it does NOT capture credentials (org.yaml is
|
|
9
|
+
* the enrollment SoT) — it confirms enrollment, surfaces the agent's mailbox
|
|
10
|
+
* address when one is discoverable (COHORT_AGENT_EMAIL hint or the pulled
|
|
11
|
+
* MemberProfileDTO's agentEmail in config/agent.json), and writes the gate
|
|
12
|
+
* file config/orgmail.yaml `{enabled, poll_seconds, mark_read}` that the
|
|
13
|
+
* daemon's platform loop keys on.
|
|
14
|
+
*
|
|
15
|
+
* Order 77 sits after org (75) + messaging (76) and before recovery (85) +
|
|
16
|
+
* verify (90), so the mailbox lands once the endpoint+token are known and is
|
|
17
|
+
* still verified before the final gate.
|
|
18
|
+
*
|
|
19
|
+
* Non-blocking by contract: detect/apply/verify NEVER read the TTY; only
|
|
20
|
+
* prompt() touches it. apply() is idempotent (re-writes the same gate file)
|
|
21
|
+
* and never destructive. verify() best-effort probes `email.inbox {limit:1}`
|
|
22
|
+
* and WARNs — never hard-fails — when the org has no verified domain or this
|
|
23
|
+
* agent has no mailbox yet (that is the admin's Settings → Email job).
|
|
24
|
+
*
|
|
25
|
+
* @module lib/setup/sections/orgmail
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
"use strict";
|
|
29
|
+
|
|
30
|
+
import { join } from "node:path";
|
|
31
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
32
|
+
import * as io from "../io.mjs";
|
|
33
|
+
import { isEnabled, configFromAgent, loadOrgConfig } from "../../org/client.mjs";
|
|
34
|
+
import { emailInbox } from "../../org/ui-parity.mjs";
|
|
35
|
+
|
|
36
|
+
const GATE_REL = join("config", "orgmail.yaml");
|
|
37
|
+
|
|
38
|
+
/** Lazily load js-yaml the same way the sibling sections do. */
|
|
39
|
+
async function loadYaml() {
|
|
40
|
+
const m = await import("js-yaml");
|
|
41
|
+
return m && m.default ? m.default : m;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Parse config/orgmail.yaml → the inner {enabled, poll_seconds, mark_read} (or null). */
|
|
45
|
+
export async function readOrgmailConfig(agentRoot) {
|
|
46
|
+
const p = join(agentRoot, GATE_REL);
|
|
47
|
+
if (!existsSync(p)) return null;
|
|
48
|
+
try {
|
|
49
|
+
const yaml = await loadYaml();
|
|
50
|
+
const doc = yaml.load(readFileSync(p, "utf8"));
|
|
51
|
+
if (!doc || typeof doc !== "object") return null;
|
|
52
|
+
return doc.orgmail && typeof doc.orgmail === "object" ? doc.orgmail : doc;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Best-effort mailbox-address hint: COHORT_AGENT_EMAIL env → config/agent.json
|
|
60
|
+
* `agentEmail`/`email` (the pull-enrollment writes the MemberProfileDTO there).
|
|
61
|
+
* Pure lookup, never throws.
|
|
62
|
+
*/
|
|
63
|
+
export function mailboxHint(agentRoot, env = process.env) {
|
|
64
|
+
if (env.COHORT_AGENT_EMAIL) return String(env.COHORT_AGENT_EMAIL);
|
|
65
|
+
try {
|
|
66
|
+
const p = join(agentRoot, "config", "agent.json");
|
|
67
|
+
if (!existsSync(p)) return "";
|
|
68
|
+
const doc = JSON.parse(readFileSync(p, "utf8"));
|
|
69
|
+
return String(doc.agentEmail || doc.email || "");
|
|
70
|
+
} catch {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default {
|
|
76
|
+
id: "orgmail",
|
|
77
|
+
label: "Workspace email (Cohort mailbox)",
|
|
78
|
+
order: 77,
|
|
79
|
+
|
|
80
|
+
async detect(ctx) {
|
|
81
|
+
const orgCfg = loadOrgConfig(ctx.agentRoot);
|
|
82
|
+
if (!isEnabled(orgCfg)) {
|
|
83
|
+
return { status: "unconfigured", missing: ["org enrollment (run the org section first)"] };
|
|
84
|
+
}
|
|
85
|
+
const gate = await readOrgmailConfig(ctx.agentRoot);
|
|
86
|
+
if (gate && gate.enabled) {
|
|
87
|
+
return { status: "complete", summary: "workspace mailbox enabled (config/orgmail.yaml)" };
|
|
88
|
+
}
|
|
89
|
+
if (gate) {
|
|
90
|
+
return { status: "partial", summary: "config/orgmail.yaml present but not enabled", missing: ["orgmail.enabled: true"] };
|
|
91
|
+
}
|
|
92
|
+
return { status: "unconfigured", missing: ["config/orgmail.yaml (the channel gate file)"] };
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async prompt(ctx) {
|
|
96
|
+
const hint = mailboxHint(ctx.agentRoot);
|
|
97
|
+
io.note(
|
|
98
|
+
"Workspace email gives this agent a real address at the org's verified domain " +
|
|
99
|
+
"(admin: Cohort → Settings → Email). Inbound + replies ride org RPC — no IMAP, no app passwords." +
|
|
100
|
+
(hint ? ` Detected mailbox hint: ${hint}` : ""),
|
|
101
|
+
{ dim: true },
|
|
102
|
+
);
|
|
103
|
+
const enable = await io.confirm(ctx, {
|
|
104
|
+
message: "Enable the workspace mailbox channel (orgmail)?",
|
|
105
|
+
key: "orgmailEnabled",
|
|
106
|
+
env: "COHORT_ORGMAIL_ENABLED",
|
|
107
|
+
default: true,
|
|
108
|
+
});
|
|
109
|
+
ctx.answers.orgmailEnabled = !!enable;
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
async apply(ctx) {
|
|
113
|
+
const enabled = ctx.answers.orgmailEnabled !== false; // headless default: enable
|
|
114
|
+
const existing = (await readOrgmailConfig(ctx.agentRoot)) || {};
|
|
115
|
+
const next = {
|
|
116
|
+
orgmail: {
|
|
117
|
+
enabled,
|
|
118
|
+
poll_seconds: Number.isFinite(Number(existing.poll_seconds)) && Number(existing.poll_seconds) > 0 ? Number(existing.poll_seconds) : 45,
|
|
119
|
+
mark_read: existing.mark_read !== false,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
mkdirSync(join(ctx.agentRoot, "config"), { recursive: true });
|
|
123
|
+
const yaml = await loadYaml();
|
|
124
|
+
const header =
|
|
125
|
+
"# config/orgmail.yaml — workspace email channel (Cohort-hosted mailbox).\n" +
|
|
126
|
+
"# GATE FILE: presence enables the daemon's orgmail channel; delete to disable.\n" +
|
|
127
|
+
"# Credentials come from config/org.yaml (org enrollment). mark_read advances\n" +
|
|
128
|
+
"# the SERVER-side cursor after a clean handoff — no local cursor file.\n";
|
|
129
|
+
writeFileSync(join(ctx.agentRoot, GATE_REL), header + yaml.dump(next, { lineWidth: 120, sortKeys: false }));
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
async verify(ctx) {
|
|
133
|
+
const checks = [];
|
|
134
|
+
const gate = await readOrgmailConfig(ctx.agentRoot);
|
|
135
|
+
const gateOk = !!(gate && gate.enabled);
|
|
136
|
+
checks.push({
|
|
137
|
+
name: "config/orgmail.yaml present + enabled",
|
|
138
|
+
ok: gateOk,
|
|
139
|
+
remedy: gateOk ? undefined : "run `maestro setup --only orgmail` (or copy scaffold/config/orgmail.yaml.example)",
|
|
140
|
+
});
|
|
141
|
+
if (!gateOk) return { ok: false, checks };
|
|
142
|
+
|
|
143
|
+
const orgCfg = configFromAgent(loadOrgConfig(ctx.agentRoot));
|
|
144
|
+
if (!orgCfg.enabled || !orgCfg.token) {
|
|
145
|
+
// WARN-shaped, not a hard fail: orgmail is dormant until enrollment.
|
|
146
|
+
checks.push({ name: "workspace mailbox (dormant — org not enrolled)", ok: true, remedy: "enroll the org (setup --only org) to activate the mailbox" });
|
|
147
|
+
return { ok: checks.every((c) => c.ok), checks };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Best-effort, NON-BLOCKING probe: one email.inbox {limit:1} round-trip.
|
|
151
|
+
// A missing verified domain / unassigned mailbox is the ADMIN's job in
|
|
152
|
+
// Cohort → Settings → Email — warn, never fail the wizard on it.
|
|
153
|
+
let detail = "";
|
|
154
|
+
let reachable = false;
|
|
155
|
+
try {
|
|
156
|
+
const r = await emailInbox({ limit: 1 }, { base: orgCfg.base, token: orgCfg.token, orgId: orgCfg.orgId, fetchImpl: ctx.fetchImpl });
|
|
157
|
+
if (r && r.ok) {
|
|
158
|
+
reachable = true;
|
|
159
|
+
detail = r.result?.mailbox?.address ? `mailbox ${r.result.mailbox.address}` : "mailbox reachable";
|
|
160
|
+
} else {
|
|
161
|
+
detail = `${r?.error?.code || "?"}: ${r?.error?.message || "unreachable"}`;
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
detail = "probe failed";
|
|
165
|
+
}
|
|
166
|
+
checks.push({
|
|
167
|
+
name: reachable ? `Cohort mailbox reachable (${detail})` : "Cohort mailbox reachability (best-effort)",
|
|
168
|
+
ok: true, // never hard-fail — the admin may not have verified a domain yet
|
|
169
|
+
remedy: reachable ? undefined : `email.inbox probe: ${detail} — an admin must verify a domain + assign this agent a mailbox in Cohort → Settings → Email`,
|
|
170
|
+
});
|
|
171
|
+
return { ok: checks.every((c) => c.ok), checks };
|
|
172
|
+
},
|
|
173
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orgmail.test.mjs — workspace-email setup section (order 77) tests.
|
|
3
|
+
* Section shape, gate-file write/idempotency, non-blocking verify with an
|
|
4
|
+
* injected fetchImpl (org/messaging section-test pattern).
|
|
5
|
+
* Run: node --test lib/setup/sections/orgmail.test.mjs
|
|
6
|
+
*/
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
import { test } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import yaml from "js-yaml";
|
|
16
|
+
|
|
17
|
+
import orgmail, { readOrgmailConfig, mailboxHint } from "./orgmail.mjs";
|
|
18
|
+
import * as io from "../io.mjs";
|
|
19
|
+
|
|
20
|
+
const MAESTRO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
21
|
+
|
|
22
|
+
// Ambient COHORT_* vars must not leak into hint/enrollment resolution.
|
|
23
|
+
for (const k of ["COHORT_AGENT_EMAIL", "COHORT_API_TOKEN", "COHORT_TOKEN", "COHORT_API_KEY", "COHORT_ORG_ID", "COHORT_ORGMAIL_ENABLED"]) delete process.env[k];
|
|
24
|
+
|
|
25
|
+
function tmpRoot() {
|
|
26
|
+
const root = mkdtempSync(join(tmpdir(), "maestro-orgmail-"));
|
|
27
|
+
mkdirSync(join(root, "config"), { recursive: true });
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
function ctxFor(root, answers = {}, fetchImpl) {
|
|
31
|
+
return { agentRoot: root, maestroRoot: MAESTRO_ROOT, flags: { headless: true }, answers, io, fetchImpl };
|
|
32
|
+
}
|
|
33
|
+
function writeOrg(root, cohort) {
|
|
34
|
+
writeFileSync(join(root, "config", "org.yaml"), yaml.dump({ org: { cohort } }));
|
|
35
|
+
}
|
|
36
|
+
const ENROLLED = { enabled: true, base: "https://org.example", orgId: "acme", token: "tok-1" };
|
|
37
|
+
|
|
38
|
+
test("section shape: id/order=77/hooks are functions", () => {
|
|
39
|
+
assert.equal(orgmail.id, "orgmail");
|
|
40
|
+
assert.equal(orgmail.order, 77);
|
|
41
|
+
for (const h of ["detect", "prompt", "apply", "verify"]) assert.equal(typeof orgmail[h], "function");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("detect: unenrolled org → unconfigured pointing at the org section", async () => {
|
|
45
|
+
const root = tmpRoot();
|
|
46
|
+
const d = await orgmail.detect(ctxFor(root));
|
|
47
|
+
assert.equal(d.status, "unconfigured");
|
|
48
|
+
assert.match(d.missing[0], /org/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("detect: enrolled without a gate file → unconfigured; enabled gate → complete", async () => {
|
|
52
|
+
const root = tmpRoot();
|
|
53
|
+
writeOrg(root, ENROLLED);
|
|
54
|
+
assert.equal((await orgmail.detect(ctxFor(root))).status, "unconfigured");
|
|
55
|
+
writeFileSync(join(root, "config", "orgmail.yaml"), yaml.dump({ orgmail: { enabled: true } }));
|
|
56
|
+
assert.equal((await orgmail.detect(ctxFor(root))).status, "complete");
|
|
57
|
+
writeFileSync(join(root, "config", "orgmail.yaml"), yaml.dump({ orgmail: { enabled: false } }));
|
|
58
|
+
assert.equal((await orgmail.detect(ctxFor(root))).status, "partial");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("apply: writes the gate file with defaults; idempotent; preserves operator overrides", async () => {
|
|
62
|
+
const root = tmpRoot();
|
|
63
|
+
writeOrg(root, ENROLLED);
|
|
64
|
+
await orgmail.apply(ctxFor(root, { orgmailEnabled: true }));
|
|
65
|
+
const first = await readOrgmailConfig(root);
|
|
66
|
+
assert.deepEqual(first, { enabled: true, poll_seconds: 45, mark_read: true });
|
|
67
|
+
// Operator tunes the cadence; re-apply must not clobber it.
|
|
68
|
+
writeFileSync(join(root, "config", "orgmail.yaml"), yaml.dump({ orgmail: { enabled: true, poll_seconds: 120, mark_read: false } }));
|
|
69
|
+
await orgmail.apply(ctxFor(root, { orgmailEnabled: true }));
|
|
70
|
+
const second = await readOrgmailConfig(root);
|
|
71
|
+
assert.deepEqual(second, { enabled: true, poll_seconds: 120, mark_read: false });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("mailboxHint: env wins, else config/agent.json agentEmail/email, else empty", () => {
|
|
75
|
+
const root = tmpRoot();
|
|
76
|
+
assert.equal(mailboxHint(root, {}), "");
|
|
77
|
+
writeFileSync(join(root, "config", "agent.json"), JSON.stringify({ email: "astra@agents.example" }));
|
|
78
|
+
assert.equal(mailboxHint(root, {}), "astra@agents.example");
|
|
79
|
+
writeFileSync(join(root, "config", "agent.json"), JSON.stringify({ agentEmail: "astra@corp.example", email: "x@y.z" }));
|
|
80
|
+
assert.equal(mailboxHint(root, {}), "astra@corp.example", "agentEmail (the MemberProfileDTO field) preferred");
|
|
81
|
+
assert.equal(mailboxHint(root, { COHORT_AGENT_EMAIL: "hint@env.example" }), "hint@env.example");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("verify: missing gate file fails with a remedy; dormant enrollment stays ok-with-remedy", async () => {
|
|
85
|
+
const root = tmpRoot();
|
|
86
|
+
const v = await orgmail.verify(ctxFor(root));
|
|
87
|
+
assert.equal(v.ok, false);
|
|
88
|
+
assert.match(v.checks[0].remedy, /setup --only orgmail/);
|
|
89
|
+
// gate present but org unenrolled → dormant (ok:true, remedy points at enrollment)
|
|
90
|
+
writeFileSync(join(root, "config", "orgmail.yaml"), yaml.dump({ orgmail: { enabled: true } }));
|
|
91
|
+
const v2 = await orgmail.verify(ctxFor(root));
|
|
92
|
+
assert.equal(v2.ok, true);
|
|
93
|
+
assert.match(v2.checks.at(-1).name, /dormant/);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("verify: enrolled probe — reachable mailbox surfaces the address; failure WARNS, never hard-fails", async () => {
|
|
97
|
+
const root = tmpRoot();
|
|
98
|
+
writeOrg(root, ENROLLED);
|
|
99
|
+
writeFileSync(join(root, "config", "orgmail.yaml"), yaml.dump({ orgmail: { enabled: true } }));
|
|
100
|
+
|
|
101
|
+
const okFetch = async (url) => ({ ok: true, status: 200, headers: { get: () => undefined }, json: async () => ({ ok: true, result: { mailbox: { address: "astra@agents.example" }, messages: [] } }) });
|
|
102
|
+
const v = await orgmail.verify(ctxFor(root, {}, okFetch));
|
|
103
|
+
assert.equal(v.ok, true);
|
|
104
|
+
assert.match(v.checks.at(-1).name, /astra@agents\.example/);
|
|
105
|
+
|
|
106
|
+
const notFound = async () => ({ ok: false, status: 404, headers: { get: () => undefined }, json: async () => ({ ok: false, error: { code: "NOT_FOUND", message: "no mailbox for member" } }) });
|
|
107
|
+
const v2 = await orgmail.verify(ctxFor(root, {}, notFound));
|
|
108
|
+
assert.equal(v2.ok, true, "no-mailbox-yet is a WARN (admin's Settings → Email job), not a wizard failure");
|
|
109
|
+
assert.match(v2.checks.at(-1).remedy, /Settings → Email/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("prompt: headless ctx takes the default (enable) without touching the TTY", async () => {
|
|
113
|
+
const root = tmpRoot();
|
|
114
|
+
writeOrg(root, ENROLLED);
|
|
115
|
+
const ctx = ctxFor(root);
|
|
116
|
+
await orgmail.prompt(ctx);
|
|
117
|
+
assert.equal(ctx.answers.orgmailEnabled, true);
|
|
118
|
+
});
|