@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,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* knowledge.test.mjs — agent-facing shared-memory API over the Cohort knowledge
|
|
3
|
+
* plane. Verifies the verb surface (remember/recall/forget/rewrite + contacts/
|
|
4
|
+
* meetings) routes to the right methods, that the client NEVER filters recall
|
|
5
|
+
* results (the server is authoritative for ACL), that forget invalidates (never
|
|
6
|
+
* deletes), and the fail-open contract when the integration is disabled.
|
|
7
|
+
* Uses an injected fetchImpl — no network.
|
|
8
|
+
* Run: node --test lib/org/knowledge.test.mjs
|
|
9
|
+
*/
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
import { test } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
remember,
|
|
17
|
+
rewrite,
|
|
18
|
+
recall,
|
|
19
|
+
recallDetailed,
|
|
20
|
+
forget,
|
|
21
|
+
upsertContact,
|
|
22
|
+
listContacts,
|
|
23
|
+
recordMeeting,
|
|
24
|
+
listMeetings,
|
|
25
|
+
normalizeGroup,
|
|
26
|
+
_internals,
|
|
27
|
+
} from "./knowledge.mjs";
|
|
28
|
+
|
|
29
|
+
const BASE = "https://org.cohortapp.com";
|
|
30
|
+
const CFG = { org: { cohort: { enabled: true, base: BASE, token: "tok" } } };
|
|
31
|
+
const OFF = { org: { cohort: { enabled: false } } };
|
|
32
|
+
|
|
33
|
+
delete process.env.COHORT_API_TOKEN;
|
|
34
|
+
delete process.env.COHORT_TOKEN;
|
|
35
|
+
|
|
36
|
+
/** A fake fetch that records calls and returns a canned res frame / payload. */
|
|
37
|
+
function fakeFetch(responder) {
|
|
38
|
+
const calls = [];
|
|
39
|
+
const impl = async (url, init) => {
|
|
40
|
+
calls.push({
|
|
41
|
+
url,
|
|
42
|
+
init,
|
|
43
|
+
method: init && init.method,
|
|
44
|
+
headers: (init && init.headers) || {},
|
|
45
|
+
body: init && init.body ? JSON.parse(init.body) : null,
|
|
46
|
+
});
|
|
47
|
+
const r = responder ? responder(url, init, calls.length - 1) : null;
|
|
48
|
+
const status = r && typeof r.status === "number" ? r.status : 200;
|
|
49
|
+
const ok = r && typeof r.ok === "boolean" ? r.ok : status >= 200 && status < 300;
|
|
50
|
+
const body = r ? r.body : { ok: true, result: null };
|
|
51
|
+
return { ok, status, headers: { get: () => undefined }, json: async () => body };
|
|
52
|
+
};
|
|
53
|
+
impl.calls = calls;
|
|
54
|
+
return impl;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- normalizeGroup (pure shape helper; NOT an ACL decision) ----------------
|
|
58
|
+
|
|
59
|
+
test("normalizeGroup: org / unit:<id> / barrier:<cell>; rejects unknown kinds", () => {
|
|
60
|
+
assert.equal(normalizeGroup("org"), "org");
|
|
61
|
+
assert.equal(normalizeGroup({ kind: "org" }), "org");
|
|
62
|
+
assert.equal(normalizeGroup({ kind: "unit", id: "eng" }), "unit:eng");
|
|
63
|
+
assert.equal(normalizeGroup({ kind: "barrier", cell: "deals-2026" }), "barrier:deals-2026");
|
|
64
|
+
assert.equal(normalizeGroup("unit:eng"), "unit:eng");
|
|
65
|
+
assert.equal(normalizeGroup("barrier:deals"), "barrier:deals");
|
|
66
|
+
// unknown kinds / bare ids fall through to "let the server decide"
|
|
67
|
+
assert.equal(normalizeGroup("bogus:x"), undefined);
|
|
68
|
+
assert.equal(normalizeGroup("eng"), undefined);
|
|
69
|
+
assert.equal(normalizeGroup(""), undefined);
|
|
70
|
+
assert.equal(normalizeGroup(undefined), undefined);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// --- remember → knowledge.append (default) / knowledge.replace (CAS) ---------
|
|
74
|
+
|
|
75
|
+
test("remember(string): appends an episode (the sole shared-write primitive)", async () => {
|
|
76
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-1", version: 1 } } }));
|
|
77
|
+
const r = await remember("we closed the Q3 deal", { cfg: CFG, group: "org", fetchImpl: f });
|
|
78
|
+
assert.equal(r.ok, true);
|
|
79
|
+
assert.equal(r.mode, "append");
|
|
80
|
+
assert.equal(r.id, "k-1");
|
|
81
|
+
assert.equal(f.calls[0].url, `${BASE}/api/v1/knowledge.append`);
|
|
82
|
+
assert.equal(f.calls[0].method, "POST");
|
|
83
|
+
assert.equal(f.calls[0].headers.authorization, "Bearer tok");
|
|
84
|
+
assert.deepEqual(f.calls[0].body, { text: "we closed the Q3 deal", group: "org" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("remember(fact object): pulls through opts (kind/participants/provenance), normalises group", async () => {
|
|
88
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-2" } } }));
|
|
89
|
+
await remember(
|
|
90
|
+
{ text: "intro call notes" },
|
|
91
|
+
{ cfg: CFG, group: { kind: "unit", id: "sales" }, kind: "note", participants: ["a", "b"], provenance: { src: "call" }, fetchImpl: f },
|
|
92
|
+
);
|
|
93
|
+
assert.deepEqual(f.calls[0].body, {
|
|
94
|
+
text: "intro call notes",
|
|
95
|
+
kind: "note",
|
|
96
|
+
participants: ["a", "b"],
|
|
97
|
+
provenance: { src: "call" },
|
|
98
|
+
group: "unit:sales",
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("remember with {id, expectedVersion}: routes to knowledge.replace (CAS)", async () => {
|
|
103
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-9", version: 4 } } }));
|
|
104
|
+
const r = await remember({ text: "corrected figure" }, { cfg: CFG, id: "k-9", expectedVersion: 3, fetchImpl: f });
|
|
105
|
+
assert.equal(r.mode, "replace");
|
|
106
|
+
assert.equal(f.calls[0].url, `${BASE}/api/v1/knowledge.replace`);
|
|
107
|
+
assert.equal(f.calls[0].body.id, "k-9");
|
|
108
|
+
assert.equal(f.calls[0].body.expectedVersion, 3);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("remember: idempotency key auto-derived from id on append", async () => {
|
|
112
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-5" } } }));
|
|
113
|
+
await remember({ id: "k-5", text: "x" }, { cfg: CFG, fetchImpl: f });
|
|
114
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], "knowledge.append:k-5");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("remember: disabled → fail-open skipped, no fetch", async () => {
|
|
118
|
+
const f = fakeFetch();
|
|
119
|
+
const r = await remember("x", { cfg: OFF, fetchImpl: f });
|
|
120
|
+
assert.deepEqual(r, { ok: false, skipped: true });
|
|
121
|
+
assert.equal(f.calls.length, 0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("remember: empty fact → BAD_REQUEST, no fetch", async () => {
|
|
125
|
+
const f = fakeFetch();
|
|
126
|
+
const r = await remember("", { cfg: CFG, fetchImpl: f });
|
|
127
|
+
assert.equal(r.ok, false);
|
|
128
|
+
assert.equal(r.error.code, "BAD_REQUEST");
|
|
129
|
+
assert.equal(f.calls.length, 0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// --- rewrite → knowledge.rewrite (designated-owner-only) --------------------
|
|
133
|
+
|
|
134
|
+
test("rewrite: routes to knowledge.rewrite with the id + text", async () => {
|
|
135
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-1" } } }));
|
|
136
|
+
const r = await rewrite("k-1", "owner-asserted text", { cfg: CFG, fetchImpl: f });
|
|
137
|
+
assert.equal(r.mode, "rewrite");
|
|
138
|
+
assert.equal(f.calls[0].url, `${BASE}/api/v1/knowledge.rewrite`);
|
|
139
|
+
assert.equal(f.calls[0].body.id, "k-1");
|
|
140
|
+
assert.equal(f.calls[0].body.text, "owner-asserted text");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("rewrite: non-owner FORBIDDEN_SCOPE surfaces in error (server is authoritative)", async () => {
|
|
144
|
+
const f = fakeFetch(() => ({ ok: false, status: 403, body: { ok: false, error: { code: "FORBIDDEN_SCOPE", message: "owner only" } } }));
|
|
145
|
+
const r = await rewrite("k-1", "x", { cfg: CFG, fetchImpl: f });
|
|
146
|
+
assert.equal(r.ok, false);
|
|
147
|
+
assert.equal(r.error.code, "FORBIDDEN_SCOPE");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// --- recall → knowledge.search; CLIENT NEVER FILTERS ------------------------
|
|
151
|
+
|
|
152
|
+
test("remember → recall round-trip: recall returns exactly the server's facts", async () => {
|
|
153
|
+
// The server is the ONLY ACL authority. recall renders whatever it returns —
|
|
154
|
+
// here a fact the agent appended comes back from search.
|
|
155
|
+
const serverFacts = [{ id: "k-1", text: "we closed the Q3 deal", group: "org" }];
|
|
156
|
+
let appended = false;
|
|
157
|
+
const f = fakeFetch((url) => {
|
|
158
|
+
if (url.endsWith("/v1/knowledge.append")) { appended = true; return { body: { ok: true, result: { id: "k-1" } } }; }
|
|
159
|
+
if (url.endsWith("/v1/knowledge.search")) return { body: { ok: true, result: { facts: serverFacts, returned_ids: ["k-1"], groups_considered: ["org"] } } };
|
|
160
|
+
return { body: { ok: true, result: null } };
|
|
161
|
+
});
|
|
162
|
+
await remember("we closed the Q3 deal", { cfg: CFG, group: "org", fetchImpl: f });
|
|
163
|
+
assert.equal(appended, true);
|
|
164
|
+
const facts = await recall("Q3 deal", { cfg: CFG, fetchImpl: f });
|
|
165
|
+
assert.deepEqual(facts, serverFacts);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("recall: passes the query (+ group hint, limit) to the server; does NOT filter results", async () => {
|
|
169
|
+
// Server returns a barrier fact AND an org fact. The client must NOT drop the
|
|
170
|
+
// barrier one — if the server returned it, the agent was granted that cell.
|
|
171
|
+
const returned = [
|
|
172
|
+
{ id: "k-1", group: "org", text: "public" },
|
|
173
|
+
{ id: "k-2", group: "barrier:deals-2026", text: "walled-but-granted" },
|
|
174
|
+
];
|
|
175
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { facts: returned, returned_ids: ["k-1", "k-2"], groups_considered: ["org", "barrier:deals-2026"] } } }));
|
|
176
|
+
const facts = await recall("deal terms", { cfg: CFG, group: { kind: "unit", id: "eng" }, limit: 3, fetchImpl: f });
|
|
177
|
+
// client renders EVERYTHING the server returned, including the barrier fact
|
|
178
|
+
assert.deepEqual(facts, returned);
|
|
179
|
+
// and it forwarded the query + the normalised group hint + limit
|
|
180
|
+
assert.equal(f.calls[0].body.query, "deal terms");
|
|
181
|
+
assert.equal(f.calls[0].body.group, "unit:eng");
|
|
182
|
+
assert.equal(f.calls[0].body.limit, 3);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("recall: when the server returns FEWER facts (RLS floored), the client returns exactly those", async () => {
|
|
186
|
+
// Agent asks broadly; server's RLS floor excludes a barrier fact it isn't
|
|
187
|
+
// granted. The client has no idea a fact was withheld — it just renders [k-1].
|
|
188
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { facts: [{ id: "k-1", group: "org" }], returned_ids: ["k-1"], groups_considered: ["org", "unit:eng"] } } }));
|
|
189
|
+
const facts = await recall("everything", { cfg: CFG, fetchImpl: f });
|
|
190
|
+
assert.deepEqual(facts, [{ id: "k-1", group: "org" }]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("recallDetailed: surfaces returnedIds + groupsConsidered from the read-audit envelope", async () => {
|
|
194
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { facts: [{ id: "k-1" }, { id: "k-2" }], returned_ids: ["k-1", "k-2"], groups_considered: ["org", "unit:eng"] } } }));
|
|
195
|
+
const r = await recallDetailed("q", { cfg: CFG, fetchImpl: f });
|
|
196
|
+
assert.equal(r.ok, true);
|
|
197
|
+
assert.deepEqual(r.returnedIds, ["k-1", "k-2"]);
|
|
198
|
+
assert.deepEqual(r.groupsConsidered, ["org", "unit:eng"]);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("recallDetailed: derives returnedIds from facts when the server omits returned_ids", async () => {
|
|
202
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { facts: [{ id: "k-3" }, { id: "k-4" }] } } }));
|
|
203
|
+
const r = await recallDetailed("q", { cfg: CFG, fetchImpl: f });
|
|
204
|
+
assert.deepEqual(r.returnedIds, ["k-3", "k-4"]);
|
|
205
|
+
assert.deepEqual(r.groupsConsidered, []);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("recall: lexical flag forwarded (keyword stand-in for vector search)", async () => {
|
|
209
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { facts: [] } } }));
|
|
210
|
+
await recall("keyword", { cfg: CFG, lexical: true, fetchImpl: f });
|
|
211
|
+
assert.equal(f.calls[0].body.lexical, true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("recall: disabled or empty query → [] (fail-open), no fetch", async () => {
|
|
215
|
+
const f = fakeFetch();
|
|
216
|
+
assert.deepEqual(await recall("q", { cfg: OFF, fetchImpl: f }), []);
|
|
217
|
+
assert.deepEqual(await recall(" ", { cfg: CFG, fetchImpl: f }), []);
|
|
218
|
+
assert.equal(f.calls.length, 0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("recall: server error → [] (fail-open); recallDetailed reports the error", async () => {
|
|
222
|
+
const f = fakeFetch(() => ({ ok: false, status: 403, body: { ok: false, error: { code: "FORBIDDEN_SCOPE", message: "no knowledge.read" } } }));
|
|
223
|
+
assert.deepEqual(await recall("q", { cfg: CFG, fetchImpl: f }), []);
|
|
224
|
+
const f2 = fakeFetch(() => ({ ok: false, status: 403, body: { ok: false, error: { code: "FORBIDDEN_SCOPE", message: "no knowledge.read" } } }));
|
|
225
|
+
const r = await recallDetailed("q", { cfg: CFG, fetchImpl: f2 });
|
|
226
|
+
assert.equal(r.ok, false);
|
|
227
|
+
assert.deepEqual(r.facts, []);
|
|
228
|
+
assert.equal(r.error.code, "FORBIDDEN_SCOPE");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// --- forget → knowledge.invalidate (tombstone, NEVER hard-delete) -----------
|
|
232
|
+
|
|
233
|
+
test("forget: calls knowledge.invalidate (not a delete); carries reason + idempotency", async () => {
|
|
234
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "k-1", invalid: true } } }));
|
|
235
|
+
const r = await forget("k-1", { cfg: CFG, reason: "superseded by k-2", supersededBy: "k-2", fetchImpl: f });
|
|
236
|
+
assert.equal(r.ok, true);
|
|
237
|
+
assert.equal(r.mode, "invalidate");
|
|
238
|
+
assert.equal(f.calls[0].url, `${BASE}/api/v1/knowledge.invalidate`);
|
|
239
|
+
assert.equal(f.calls[0].body.id, "k-1");
|
|
240
|
+
assert.equal(f.calls[0].body.reason, "superseded by k-2");
|
|
241
|
+
assert.equal(f.calls[0].body.supersededBy, "k-2");
|
|
242
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], "knowledge.invalidate:k-1");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("forget: disabled → skipped; missing id → BAD_REQUEST; no fetch", async () => {
|
|
246
|
+
const f = fakeFetch();
|
|
247
|
+
assert.deepEqual(await forget("k-1", { cfg: OFF, fetchImpl: f }), { ok: false, skipped: true });
|
|
248
|
+
const bad = await forget("", { cfg: CFG, fetchImpl: f });
|
|
249
|
+
assert.equal(bad.error.code, "BAD_REQUEST");
|
|
250
|
+
assert.equal(f.calls.length, 0);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// --- contacts -------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
test("upsertContact: posts the right shape to contacts.upsert (idempotent on id)", async () => {
|
|
256
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "c-1" } } }));
|
|
257
|
+
const r = await upsertContact({ id: "c-1", name: "Jordan", emails: ["j@x.com"], group: { kind: "unit", id: "sales" } }, { cfg: CFG, fetchImpl: f });
|
|
258
|
+
assert.equal(r.ok, true);
|
|
259
|
+
assert.equal(f.calls[0].url, `${BASE}/api/v1/contacts.upsert`);
|
|
260
|
+
assert.deepEqual(f.calls[0].body, { id: "c-1", name: "Jordan", emails: ["j@x.com"], group: "unit:sales" });
|
|
261
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], "contacts.upsert:c-1");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("listContacts: returns the server's ACL-filtered list as-is; [] when disabled", async () => {
|
|
265
|
+
const f = fakeFetch(() => ({ body: { contacts: [{ id: "c-1" }, { id: "c-2" }] } }));
|
|
266
|
+
assert.deepEqual(await listContacts({ cfg: CFG, q: "jord", fetchImpl: f }), [{ id: "c-1" }, { id: "c-2" }]);
|
|
267
|
+
assert.match(f.calls[0].url, /\/v1\/contacts\.list\?q=jord$/);
|
|
268
|
+
assert.deepEqual(await listContacts({ cfg: OFF }), []);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("upsertContact: server NOT_FOUND (handler lane unbuilt) surfaces unimplemented + warns, never a silent swallow", async () => {
|
|
272
|
+
const f = fakeFetch(() => ({ body: { ok: false, error: { code: "NOT_FOUND", message: "unknown method contacts.upsert" } } }));
|
|
273
|
+
const warnings = [];
|
|
274
|
+
const orig = console.warn;
|
|
275
|
+
console.warn = (m) => warnings.push(String(m));
|
|
276
|
+
let r;
|
|
277
|
+
try {
|
|
278
|
+
r = await upsertContact({ id: "c-9", name: "Sam" }, { cfg: CFG, fetchImpl: f });
|
|
279
|
+
} finally {
|
|
280
|
+
console.warn = orig;
|
|
281
|
+
}
|
|
282
|
+
assert.equal(r.ok, false);
|
|
283
|
+
assert.equal(r.unimplemented, true, "NOT_FOUND is flagged as an unimplemented capability, not a plain failure");
|
|
284
|
+
assert.equal(r.error.code, "NOT_FOUND");
|
|
285
|
+
assert.ok(warnings.some((w) => w.includes("contacts.upsert")), "emits a loud, actionable warning naming the missing method");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// --- meetings -------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
test("recordMeeting: posts the right shape to meetings.record (idempotent on id)", async () => {
|
|
291
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "m-1" } } }));
|
|
292
|
+
const r = await recordMeeting({ id: "m-1", title: "board sync", at: "2026-06-11T10:00:00Z", participants: ["a", "b"] }, { cfg: CFG, fetchImpl: f });
|
|
293
|
+
assert.equal(r.ok, true);
|
|
294
|
+
assert.equal(f.calls[0].url, `${BASE}/api/v1/meetings.record`);
|
|
295
|
+
assert.deepEqual(f.calls[0].body, { id: "m-1", title: "board sync", at: "2026-06-11T10:00:00Z", participants: ["a", "b"] });
|
|
296
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], "meetings.record:m-1");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("listMeetings: returns the server's participant-ACL list as-is; [] when disabled", async () => {
|
|
300
|
+
const f = fakeFetch(() => ({ body: { meetings: [{ id: "m-1" }] } }));
|
|
301
|
+
assert.deepEqual(await listMeetings({ cfg: CFG, since: "2026-06-01", fetchImpl: f }), [{ id: "m-1" }]);
|
|
302
|
+
assert.match(f.calls[0].url, /\/v1\/meetings\.list\?since=2026-06-01$/);
|
|
303
|
+
assert.deepEqual(await listMeetings({ cfg: OFF }), []);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// --- pure internals --------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
test("_internals.pickArray: finds the first array key, [] otherwise", () => {
|
|
309
|
+
assert.deepEqual(_internals.pickArray({ facts: [1, 2] }, ["facts", "results"]), [1, 2]);
|
|
310
|
+
assert.deepEqual(_internals.pickArray({ results: [3] }, ["facts", "results"]), [3]);
|
|
311
|
+
assert.deepEqual(_internals.pickArray([9], ["facts"]), [9]);
|
|
312
|
+
assert.deepEqual(_internals.pickArray({ nope: 1 }, ["facts"]), []);
|
|
313
|
+
assert.deepEqual(_internals.pickArray(null, ["facts"]), []);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("_internals.factToParams: string→{text}; opts pull-through; group normalised; id stringified", () => {
|
|
317
|
+
const p = _internals.factToParams("hi", { kind: "note", id: 42, group: { kind: "unit", id: "eng" } });
|
|
318
|
+
assert.deepEqual(p, { text: "hi", kind: "note", id: "42", group: "unit:eng" });
|
|
319
|
+
});
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/org/leases.mjs — agent-side lease helpers over the Cohort lease primitive.
|
|
3
|
+
*
|
|
4
|
+
* The Cohort server exposes one generic lease primitive (lib/org/protocol.mjs
|
|
5
|
+
* LEASE_SCOPES: work-claim / thread-ownership / session-leadership /
|
|
6
|
+
* singleton-cadence). Exactly one holder per (scope, resourceId) until
|
|
7
|
+
* expiry/release — the server resolves the race atomically (single-conditional
|
|
8
|
+
* CAS), the loser gets a CONFLICT/LEASE_HELD error frame.
|
|
9
|
+
*
|
|
10
|
+
* This module is the agent-facing convenience layer over that primitive:
|
|
11
|
+
*
|
|
12
|
+
* - claimLease(scope, resourceId, opts) — generic claim, 3s fail-open.
|
|
13
|
+
* - claimThread(channel, thread, opts) — thread-ownership claim for
|
|
14
|
+
* shared-channel SEND ARBITRATION (§5.4): when ≥2 agents see the same channel
|
|
15
|
+
* message, exactly one wins a `thread-ownership` lease on resource_id
|
|
16
|
+
* `channel:<thread>` and replies; the others stand down.
|
|
17
|
+
* - releaseThread / releaseLease — free a claimed lease.
|
|
18
|
+
* - preSendArbitration(...) — the cancellable pre-send check the
|
|
19
|
+
* channel send path calls; returns {mayReply, leaseToken, ...}. Exactly one
|
|
20
|
+
* agent gets mayReply:true for a given thread.
|
|
21
|
+
*
|
|
22
|
+
* FAIL-OPEN DISCIPLINE (§5.4 rule 4 / design rule 3 "coordination fails open,
|
|
23
|
+
* counted"): a lease call that does not resolve within `failOpenTimeoutMs`
|
|
24
|
+
* (default 3000) is treated as a WIN — the agent proceeds rather than going
|
|
25
|
+
* silent when the coordination plane is down. Every fail-open is surfaced
|
|
26
|
+
* (`failedOpen:true`) so the caller can bump `failOpenSends` in
|
|
27
|
+
* state/org/metrics.json and report it on reconnect.
|
|
28
|
+
*
|
|
29
|
+
* Channels NOT listed `claim-on-receive` in config/ownership.yaml are not
|
|
30
|
+
* arbitrated: preSendArbitration short-circuits to {mayReply:true} for them, so
|
|
31
|
+
* the send path pays no lease round-trip on un-shared channels.
|
|
32
|
+
*
|
|
33
|
+
* Pure-core + injectable IO: the Cohort client is injected as `opts.client`
|
|
34
|
+
* (defaults to the real lib/org/client.mjs helpers) and the ownership config is
|
|
35
|
+
* injected as `opts.ownership` (else read from config/ownership.yaml). Node
|
|
36
|
+
* builtins only. ESM.
|
|
37
|
+
*
|
|
38
|
+
* @module lib/org/leases
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
"use strict";
|
|
42
|
+
|
|
43
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
44
|
+
import { join } from "node:path";
|
|
45
|
+
|
|
46
|
+
import { LEASE_SCOPES } from "./protocol.mjs";
|
|
47
|
+
import * as client from "./client.mjs";
|
|
48
|
+
|
|
49
|
+
/** Default fail-open ceiling for any single lease round-trip (§5.4). */
|
|
50
|
+
export const DEFAULT_FAIL_OPEN_MS = 3000;
|
|
51
|
+
|
|
52
|
+
/** The thread-ownership lease scope + default TTL (10m, per §5.4 rule 1). */
|
|
53
|
+
export const THREAD_OWNERSHIP_SCOPE = "thread-ownership";
|
|
54
|
+
const DEFAULT_THREAD_TTL_MS = 10 * 60 * 1000;
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// ownership.yaml — which channels are arbitrated
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Load + parse config/ownership.yaml. Returns the parsed doc, or a benign empty
|
|
62
|
+
* shape (`{channels:[]}`) when the file is absent/unreadable — so an agent
|
|
63
|
+
* without the config simply arbitrates nothing (no channel is claim-on-receive).
|
|
64
|
+
* Fail-open. Injectable root for tests.
|
|
65
|
+
* @param {string} [agentRoot="."]
|
|
66
|
+
* @returns {Promise<{schemaVersion?:number, channels:object[], mentionTtlSec?:number, arbitration?:object}>}
|
|
67
|
+
*/
|
|
68
|
+
export async function loadOwnership(agentRoot = ".") {
|
|
69
|
+
const path = join(String(agentRoot || "."), "config", "ownership.yaml");
|
|
70
|
+
if (!existsSync(path)) return { channels: [] };
|
|
71
|
+
try {
|
|
72
|
+
const m = await import("js-yaml");
|
|
73
|
+
const yaml = m.default || m;
|
|
74
|
+
const doc = yaml.load(readFileSync(path, "utf8"));
|
|
75
|
+
if (doc && typeof doc === "object") {
|
|
76
|
+
return { channels: Array.isArray(doc.channels) ? doc.channels : [], ...doc };
|
|
77
|
+
}
|
|
78
|
+
return { channels: [] };
|
|
79
|
+
} catch {
|
|
80
|
+
return { channels: [] };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find the ownership rule for a (platform, channel/thread-id) pair, if any.
|
|
86
|
+
* Matches on `channel` or `chat` (telegram) keys, case-insensitively for ids.
|
|
87
|
+
* Pure.
|
|
88
|
+
* @param {{channels:object[]}} ownership
|
|
89
|
+
* @param {string} platform
|
|
90
|
+
* @param {string} channelId
|
|
91
|
+
* @returns {object|null} the matching channel rule, or null
|
|
92
|
+
*/
|
|
93
|
+
export function ownershipRuleFor(ownership, platform, channelId) {
|
|
94
|
+
const chans = (ownership && Array.isArray(ownership.channels)) ? ownership.channels : [];
|
|
95
|
+
const plat = String(platform || "").toLowerCase();
|
|
96
|
+
const id = String(channelId || "");
|
|
97
|
+
for (const c of chans) {
|
|
98
|
+
if (!c || typeof c !== "object") continue;
|
|
99
|
+
if (String(c.platform || "").toLowerCase() !== plat) continue;
|
|
100
|
+
const ruleId = c.channel != null ? String(c.channel) : (c.chat != null ? String(c.chat) : "");
|
|
101
|
+
if (ruleId && (ruleId === id || ruleId.toLowerCase() === id.toLowerCase())) return c;
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Is this channel in `claim-on-receive` arbitration mode? Pure. Channels with no
|
|
108
|
+
* rule (or `send-time-only` / any other mode) are NOT claim-on-receive.
|
|
109
|
+
* @param {{channels:object[]}} ownership @param {string} platform @param {string} channelId
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
export function isClaimOnReceive(ownership, platform, channelId) {
|
|
113
|
+
const rule = ownershipRuleFor(ownership, platform, channelId);
|
|
114
|
+
return !!(rule && rule.mode === "claim-on-receive");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// resource-key canonicalisation
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Canonical thread-ownership resource id. Both racing agents must derive the
|
|
123
|
+
* SAME key for the server to adjudicate a single winner, so we normalise: lower
|
|
124
|
+
* the platform, keep the channel id as-is (Slack ids are already uppercase by the
|
|
125
|
+
* conversation-resolver convention), and join with the thread/ts. Pure.
|
|
126
|
+
* @param {string} channel the channel/chat id (already canonicalised upstream)
|
|
127
|
+
* @param {string} thread the thread root ts / message id (falls back to channel)
|
|
128
|
+
* @returns {string} e.g. "channel:C0123ABC:1700000000.0001"
|
|
129
|
+
*/
|
|
130
|
+
export function threadResourceId(channel, thread) {
|
|
131
|
+
const ch = String(channel || "").trim();
|
|
132
|
+
const th = String(thread || "").trim();
|
|
133
|
+
return th ? `channel:${ch}:${th}` : `channel:${ch}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// fail-open lease round-trip
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve a value, but never wait longer than `ms`; on timeout resolve with
|
|
142
|
+
* `onTimeout`. Used to bound every lease round-trip at the fail-open ceiling so
|
|
143
|
+
* a hung server can never stall a send. Pure over its inputs.
|
|
144
|
+
* @template T
|
|
145
|
+
* @param {Promise<T>} promise
|
|
146
|
+
* @param {number} ms
|
|
147
|
+
* @param {() => T} onTimeout
|
|
148
|
+
* @returns {Promise<T>}
|
|
149
|
+
*/
|
|
150
|
+
function withTimeout(promise, ms, onTimeout) {
|
|
151
|
+
if (!(ms > 0)) return promise;
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
let settled = false;
|
|
154
|
+
const timer = setTimeout(() => {
|
|
155
|
+
if (settled) return;
|
|
156
|
+
settled = true;
|
|
157
|
+
resolve(onTimeout());
|
|
158
|
+
}, ms);
|
|
159
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
160
|
+
Promise.resolve(promise).then(
|
|
161
|
+
(v) => { if (!settled) { settled = true; clearTimeout(timer); resolve(v); } },
|
|
162
|
+
() => { if (!settled) { settled = true; clearTimeout(timer); resolve(onTimeout()); } },
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The Cohort client surface this module needs (injectable for tests). */
|
|
168
|
+
function resolveClient(opts) {
|
|
169
|
+
return (opts && opts.client) || client;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Connection options for a client RPC, distilled from opts. */
|
|
173
|
+
function rpcOpts(opts) {
|
|
174
|
+
return {
|
|
175
|
+
base: opts && opts.base,
|
|
176
|
+
token: opts && opts.token,
|
|
177
|
+
fetchImpl: opts && opts.fetchImpl,
|
|
178
|
+
idempotencyKey: opts && opts.idempotencyKey,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Claim a generic lease over the Cohort lease primitive. 3s fail-open: if the
|
|
184
|
+
* server does not adjudicate within `failOpenTimeoutMs`, the caller WINS (the
|
|
185
|
+
* coordination plane fails open, counted).
|
|
186
|
+
*
|
|
187
|
+
* @param {string} scope one of LEASE_SCOPES
|
|
188
|
+
* @param {string} resourceId the resource the lease guards (e.g. a thread key)
|
|
189
|
+
* @param {object} [opts] - {
|
|
190
|
+
* client?, base, token, fetchImpl?, idempotencyKey?, holder?, ttlMs?,
|
|
191
|
+
* failOpenTimeoutMs?
|
|
192
|
+
* }
|
|
193
|
+
* @returns {Promise<{held:boolean, leaseToken:string|null, failedOpen:boolean, reason:string, frame?:object}>}
|
|
194
|
+
*/
|
|
195
|
+
export async function claimLease(scope, resourceId, opts = {}) {
|
|
196
|
+
if (!LEASE_SCOPES.includes(scope)) {
|
|
197
|
+
return { held: false, leaseToken: null, failedOpen: false, reason: `bad-scope:${scope}` };
|
|
198
|
+
}
|
|
199
|
+
if (!resourceId) {
|
|
200
|
+
return { held: false, leaseToken: null, failedOpen: false, reason: "no-resource" };
|
|
201
|
+
}
|
|
202
|
+
const c = resolveClient(opts);
|
|
203
|
+
const ms = Number.isFinite(opts.failOpenTimeoutMs) ? opts.failOpenTimeoutMs : DEFAULT_FAIL_OPEN_MS;
|
|
204
|
+
const params = {
|
|
205
|
+
scope,
|
|
206
|
+
resourceId,
|
|
207
|
+
holder: opts.holder || undefined,
|
|
208
|
+
ttlMs: Number.isFinite(opts.ttlMs) ? opts.ttlMs : undefined,
|
|
209
|
+
};
|
|
210
|
+
const frame = await withTimeout(
|
|
211
|
+
Promise.resolve(c.leaseClaim(params, rpcOpts(opts))),
|
|
212
|
+
ms,
|
|
213
|
+
() => ({ __failOpen: true }),
|
|
214
|
+
);
|
|
215
|
+
if (frame && frame.__failOpen) {
|
|
216
|
+
// Server didn't answer in time → fail OPEN: proceed as if we hold it, but
|
|
217
|
+
// with no real lease token (so we can't heartbeat/release a phantom lease).
|
|
218
|
+
return { held: true, leaseToken: null, failedOpen: true, reason: "fail-open-timeout" };
|
|
219
|
+
}
|
|
220
|
+
if (frame && frame.ok) {
|
|
221
|
+
const res = frame.result || {};
|
|
222
|
+
const leaseToken = res.token || res.leaseToken || null;
|
|
223
|
+
return { held: true, leaseToken, failedOpen: false, reason: "claimed", frame };
|
|
224
|
+
}
|
|
225
|
+
// Adjudicated loss (CONFLICT/LEASE_HELD) or any other error frame → stand down.
|
|
226
|
+
const code = frame && frame.error && frame.error.code;
|
|
227
|
+
return { held: false, leaseToken: null, failedOpen: false, reason: code || "denied", frame };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Release a generic lease. Best-effort + bounded fail-open; a fail-open claim
|
|
232
|
+
* (no real token) is a no-op release. Never throws.
|
|
233
|
+
* @param {string} scope @param {string} resourceId
|
|
234
|
+
* @param {object} [opts] - { client?, base, token, fetchImpl?, leaseToken, failOpenTimeoutMs? }
|
|
235
|
+
* @returns {Promise<{released:boolean, reason:string}>}
|
|
236
|
+
*/
|
|
237
|
+
export async function releaseLease(scope, resourceId, opts = {}) {
|
|
238
|
+
if (!opts.leaseToken) return { released: false, reason: "no-token" };
|
|
239
|
+
const c = resolveClient(opts);
|
|
240
|
+
const ms = Number.isFinite(opts.failOpenTimeoutMs) ? opts.failOpenTimeoutMs : DEFAULT_FAIL_OPEN_MS;
|
|
241
|
+
const frame = await withTimeout(
|
|
242
|
+
Promise.resolve(c.leaseRelease({ scope, resourceId, leaseToken: opts.leaseToken }, rpcOpts(opts))),
|
|
243
|
+
ms,
|
|
244
|
+
() => ({ __failOpen: true }),
|
|
245
|
+
);
|
|
246
|
+
if (frame && frame.__failOpen) return { released: false, reason: "fail-open-timeout" };
|
|
247
|
+
return { released: !!(frame && frame.ok), reason: frame && frame.ok ? "released" : "error" };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Heartbeat a held lease (slide its expiry). Best-effort, bounded fail-open.
|
|
252
|
+
* @param {string} scope @param {string} resourceId
|
|
253
|
+
* @param {object} [opts] - { client?, base, token, fetchImpl?, leaseToken, failOpenTimeoutMs? }
|
|
254
|
+
* @returns {Promise<{ok:boolean, reason:string}>}
|
|
255
|
+
*/
|
|
256
|
+
export async function heartbeatLease(scope, resourceId, opts = {}) {
|
|
257
|
+
if (!opts.leaseToken) return { ok: false, reason: "no-token" };
|
|
258
|
+
const c = resolveClient(opts);
|
|
259
|
+
const ms = Number.isFinite(opts.failOpenTimeoutMs) ? opts.failOpenTimeoutMs : DEFAULT_FAIL_OPEN_MS;
|
|
260
|
+
const frame = await withTimeout(
|
|
261
|
+
Promise.resolve(c.leaseHeartbeat({ scope, resourceId, leaseToken: opts.leaseToken }, rpcOpts(opts))),
|
|
262
|
+
ms,
|
|
263
|
+
() => ({ __failOpen: true }),
|
|
264
|
+
);
|
|
265
|
+
if (frame && frame.__failOpen) return { ok: false, reason: "fail-open-timeout" };
|
|
266
|
+
return { ok: !!(frame && frame.ok), reason: frame && frame.ok ? "ok" : "error" };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// thread-ownership (send arbitration §5.4)
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Claim the right to reply in a shared channel thread (thread-ownership lease).
|
|
275
|
+
* Exactly one racing agent wins; the others get held:false. 3s fail-open.
|
|
276
|
+
*
|
|
277
|
+
* @param {string} channel the (canonicalised) channel/chat id
|
|
278
|
+
* @param {string} thread the thread root id (or "" for a top-level message)
|
|
279
|
+
* @param {object} [opts] - { client?, base, token, fetchImpl?, holder?, ttlMs?, failOpenTimeoutMs? }
|
|
280
|
+
* @returns {Promise<{held:boolean, leaseToken:string|null, failedOpen:boolean, reason:string, resourceId:string}>}
|
|
281
|
+
*/
|
|
282
|
+
export async function claimThread(channel, thread, opts = {}) {
|
|
283
|
+
const resourceId = threadResourceId(channel, thread);
|
|
284
|
+
const r = await claimLease(THREAD_OWNERSHIP_SCOPE, resourceId, {
|
|
285
|
+
ttlMs: Number.isFinite(opts.ttlMs) ? opts.ttlMs : DEFAULT_THREAD_TTL_MS,
|
|
286
|
+
...opts,
|
|
287
|
+
});
|
|
288
|
+
return { ...r, resourceId };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Release a thread-ownership lease (e.g. after the reply is sent, or on stand-down).
|
|
293
|
+
* @param {string} channel @param {string} thread
|
|
294
|
+
* @param {object} [opts] - { client?, base, token, fetchImpl?, leaseToken, failOpenTimeoutMs? }
|
|
295
|
+
* @returns {Promise<{released:boolean, reason:string}>}
|
|
296
|
+
*/
|
|
297
|
+
export function releaseThread(channel, thread, opts = {}) {
|
|
298
|
+
return releaseLease(THREAD_OWNERSHIP_SCOPE, threadResourceId(channel, thread), opts);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The cancellable PRE-SEND arbitration check the channel send path calls before
|
|
303
|
+
* replying in a shared channel (§5.4 rule 3, the send-time backstop). Returns
|
|
304
|
+
* `{mayReply, leaseToken}`: exactly one agent gets mayReply:true for a given
|
|
305
|
+
* thread (the server adjudicates the single winner). Channels that are NOT
|
|
306
|
+
* claim-on-receive short-circuit to mayReply:true with no lease round-trip.
|
|
307
|
+
*
|
|
308
|
+
* On a 3s server timeout it FAILS OPEN (mayReply:true, failedOpen:true) so a
|
|
309
|
+
* down coordination plane never silences the agent — the caller increments the
|
|
310
|
+
* fail-open counter and reports on reconnect.
|
|
311
|
+
*
|
|
312
|
+
* @param {object} args - {
|
|
313
|
+
* platform, channel, thread?, ownership?, agentRoot?,
|
|
314
|
+
* client?, base, token, fetchImpl?, holder?, ttlMs?, failOpenTimeoutMs?
|
|
315
|
+
* }
|
|
316
|
+
* @returns {Promise<{mayReply:boolean, leaseToken:string|null, failedOpen:boolean, arbitrated:boolean, reason:string, resourceId?:string}>}
|
|
317
|
+
*/
|
|
318
|
+
export async function preSendArbitration(args = {}) {
|
|
319
|
+
const { platform, channel, thread } = args;
|
|
320
|
+
// Resolve the ownership config (injected or read from disk).
|
|
321
|
+
const ownership = args.ownership || (await loadOwnership(args.agentRoot || "."));
|
|
322
|
+
if (!isClaimOnReceive(ownership, platform, channel)) {
|
|
323
|
+
// Not an arbitrated channel — reply freely, no lease spent.
|
|
324
|
+
return { mayReply: true, leaseToken: null, failedOpen: false, arbitrated: false, reason: "not-arbitrated" };
|
|
325
|
+
}
|
|
326
|
+
const r = await claimThread(channel, thread || "", args);
|
|
327
|
+
return {
|
|
328
|
+
mayReply: r.held,
|
|
329
|
+
leaseToken: r.leaseToken,
|
|
330
|
+
failedOpen: r.failedOpen,
|
|
331
|
+
arbitrated: true,
|
|
332
|
+
reason: r.reason,
|
|
333
|
+
resourceId: r.resourceId,
|
|
334
|
+
};
|
|
335
|
+
}
|