@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,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sync-protocol.mjs — THE way to evolve the shared Cohort wire contract.
|
|
4
|
+
*
|
|
5
|
+
* `lib/org/protocol.mjs` is the single CANONICAL source of the org-protocol wire
|
|
6
|
+
* contract (method names, scopes, mutating-method set, event families, board
|
|
7
|
+
* status vocabulary, error codes, and the `computeRowHash` hash-chain function).
|
|
8
|
+
* maestro (ESM .mjs / Node) and Cohort (TS / Next) must agree on it
|
|
9
|
+
* BYTE-FOR-BYTE. The cross-repo parity guard (`lib/org/protocol.checksum.test.mjs`
|
|
10
|
+
* here, plus the mirror test in Cohort) fails the moment the two drift.
|
|
11
|
+
*
|
|
12
|
+
* Workflow to change the protocol:
|
|
13
|
+
* 1. Edit `lib/org/protocol.mjs` (maestro — the canonical copy).
|
|
14
|
+
* 2. Run `node scripts/sync-protocol.mjs` (this script):
|
|
15
|
+
* - recomputes the sha256 of `protocol.mjs`;
|
|
16
|
+
* - copies it to `<cohort>/packages/protocol/index.mjs` (re-vendor);
|
|
17
|
+
* - writes the new checksum to `<cohort>/packages/protocol/protocol.checksum`
|
|
18
|
+
* AND to the maestro checksum reference (`lib/org/protocol.checksum`).
|
|
19
|
+
* - prints the new checksum and the exact files to commit.
|
|
20
|
+
* 3. Commit BOTH repos.
|
|
21
|
+
*
|
|
22
|
+
* No checksum literal lives in any test file. Both parity guards READ the
|
|
23
|
+
* `protocol.checksum` reference at run time:
|
|
24
|
+
* - maestro `lib/org/protocol.checksum.test.mjs` reads `lib/org/protocol.checksum`;
|
|
25
|
+
* - Cohort `packages/protocol/protocol.test.ts` reads `packages/protocol/protocol.checksum`.
|
|
26
|
+
* That is the whole point — there is no frozen `EXPECTED`/`CANONICAL_CHECKSUM`
|
|
27
|
+
* constant for a re-vendor to leave stale; this script only has to keep the two
|
|
28
|
+
* `protocol.checksum` files (and the vendored `index.mjs`) in lock-step with
|
|
29
|
+
* `protocol.mjs`.
|
|
30
|
+
*
|
|
31
|
+
* Idempotent: re-running with an unchanged protocol re-writes the same bytes and
|
|
32
|
+
* the same checksum (no spurious diffs). Fail-soft on a missing Cohort checkout:
|
|
33
|
+
* it still updates + prints the maestro-side checksum so the local guard is never
|
|
34
|
+
* left stale, and it tells you which Cohort files to sync once the repo is present.
|
|
35
|
+
*
|
|
36
|
+
* Usage (the Cohort checkout path is REQUIRED — arg or COHORT_REPO env; there
|
|
37
|
+
* is deliberately no default: a silently-wrong hardcoded path is worse than a
|
|
38
|
+
* hard stop):
|
|
39
|
+
* node scripts/sync-protocol.mjs <cohort-path> # explicit checkout path
|
|
40
|
+
* COHORT_REPO=/path/to/hq node scripts/sync-protocol.mjs
|
|
41
|
+
* node scripts/sync-protocol.mjs <cohort-path> --dry-run # compute + print only
|
|
42
|
+
* node scripts/sync-protocol.mjs <cohort-path> --check # exit non-zero on drift
|
|
43
|
+
*
|
|
44
|
+
* Node builtins only (maestro is "type":"module", near-zero-dep by design).
|
|
45
|
+
*
|
|
46
|
+
* @module scripts/sync-protocol
|
|
47
|
+
*/
|
|
48
|
+
"use strict";
|
|
49
|
+
|
|
50
|
+
import { createHash } from "node:crypto";
|
|
51
|
+
import {
|
|
52
|
+
readFileSync,
|
|
53
|
+
writeFileSync,
|
|
54
|
+
mkdirSync,
|
|
55
|
+
existsSync,
|
|
56
|
+
} from "node:fs";
|
|
57
|
+
import { dirname, join, resolve } from "node:path";
|
|
58
|
+
import { fileURLToPath } from "node:url";
|
|
59
|
+
import { applyBrandEnvCompat } from "../lib/env-compat.mjs";
|
|
60
|
+
|
|
61
|
+
// Fleet back-compat FIRST: bridge NEOLITH_* ⇄ COHORT_* env names so a host
|
|
62
|
+
// still exporting NEOLITH_REPO resolves the COHORT_REPO checkout path below.
|
|
63
|
+
applyBrandEnvCompat();
|
|
64
|
+
|
|
65
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
66
|
+
const REPO_ROOT = resolve(HERE, "..");
|
|
67
|
+
|
|
68
|
+
const PROTOCOL_PATH = join(REPO_ROOT, "lib", "org", "protocol.mjs");
|
|
69
|
+
const MAESTRO_CHECKSUM_PATH = join(REPO_ROOT, "lib", "org", "protocol.checksum");
|
|
70
|
+
|
|
71
|
+
/** sha256 over a file's exact bytes (byte-for-byte — no normalization). */
|
|
72
|
+
function sha256File(path) {
|
|
73
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Parse argv: first non-flag positional is the Cohort path; collect flags. */
|
|
77
|
+
function parseArgs(argv) {
|
|
78
|
+
const flags = new Set();
|
|
79
|
+
let cohortArg = null;
|
|
80
|
+
for (const a of argv) {
|
|
81
|
+
if (a.startsWith("--")) flags.add(a);
|
|
82
|
+
else if (cohortArg === null) cohortArg = a;
|
|
83
|
+
}
|
|
84
|
+
return { cohortArg, flags };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the Cohort checkout root from arg > env. Returns `{ root, source }`
|
|
89
|
+
* (source describes where the path came from for logging), or null when
|
|
90
|
+
* NEITHER is provided — the caller hard-stops rather than writing into a
|
|
91
|
+
* guessed path.
|
|
92
|
+
*/
|
|
93
|
+
function resolveCohortRoot(cohortArg) {
|
|
94
|
+
if (cohortArg) return { root: resolve(cohortArg), source: "arg" };
|
|
95
|
+
if (process.env.COHORT_REPO)
|
|
96
|
+
return { root: resolve(process.env.COHORT_REPO), source: "env COHORT_REPO" };
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function main() {
|
|
101
|
+
const { cohortArg, flags } = parseArgs(process.argv.slice(2));
|
|
102
|
+
const dryRun = flags.has("--dry-run") || flags.has("--dry");
|
|
103
|
+
const checkOnly = flags.has("--check");
|
|
104
|
+
|
|
105
|
+
if (!existsSync(PROTOCOL_PATH)) {
|
|
106
|
+
console.error(`sync-protocol: canonical protocol missing at ${PROTOCOL_PATH}`);
|
|
107
|
+
process.exit(2);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!resolveCohortRoot(cohortArg)) {
|
|
111
|
+
console.error(
|
|
112
|
+
"sync-protocol: no Cohort checkout named — pass the path as the first argument " +
|
|
113
|
+
"(node scripts/sync-protocol.mjs /path/to/hq) or set COHORT_REPO. " +
|
|
114
|
+
"There is deliberately no default: a silently-wrong path would sync the protocol nowhere.",
|
|
115
|
+
);
|
|
116
|
+
process.exit(2);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const digest = sha256File(PROTOCOL_PATH);
|
|
120
|
+
console.log(`sync-protocol: canonical lib/org/protocol.mjs`);
|
|
121
|
+
console.log(`sync-protocol: sha256 = ${digest}`);
|
|
122
|
+
if (dryRun) console.log("sync-protocol: --dry-run — no files will be written.");
|
|
123
|
+
if (checkOnly) console.log("sync-protocol: --check — verifying sync, no files will be written.");
|
|
124
|
+
|
|
125
|
+
const committed = new Set();
|
|
126
|
+
let drift = false;
|
|
127
|
+
|
|
128
|
+
// ---- maestro side: checksum reference + frozen test constant ----
|
|
129
|
+
const curChecksum = existsSync(MAESTRO_CHECKSUM_PATH)
|
|
130
|
+
? readFileSync(MAESTRO_CHECKSUM_PATH, "utf8").trim()
|
|
131
|
+
: null;
|
|
132
|
+
if (curChecksum !== digest) {
|
|
133
|
+
drift = true;
|
|
134
|
+
if (!dryRun && !checkOnly) {
|
|
135
|
+
writeFileSync(MAESTRO_CHECKSUM_PATH, digest + "\n");
|
|
136
|
+
console.log(`sync-protocol: wrote ${MAESTRO_CHECKSUM_PATH}`);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(
|
|
139
|
+
`sync-protocol: OUT OF SYNC ${MAESTRO_CHECKSUM_PATH} (${curChecksum ?? "missing"} != ${digest})`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
console.log(`sync-protocol: up-to-date ${MAESTRO_CHECKSUM_PATH}`);
|
|
144
|
+
}
|
|
145
|
+
committed.add(MAESTRO_CHECKSUM_PATH);
|
|
146
|
+
|
|
147
|
+
// The maestro + Cohort parity guards READ their `protocol.checksum` at run
|
|
148
|
+
// time, so there is no frozen test constant to rewrite here — keeping the two
|
|
149
|
+
// checksum files in sync (below) is sufficient.
|
|
150
|
+
|
|
151
|
+
// ---- Cohort side: re-vendor @cohort/protocol + write its checksum ----
|
|
152
|
+
const { root: cohortRoot, source } = resolveCohortRoot(cohortArg);
|
|
153
|
+
const pkgDir = join(cohortRoot, "packages", "protocol");
|
|
154
|
+
const vendoredIndex = join(pkgDir, "index.mjs");
|
|
155
|
+
const cohortChecksum = join(pkgDir, "protocol.checksum");
|
|
156
|
+
|
|
157
|
+
if (!existsSync(cohortRoot)) {
|
|
158
|
+
console.warn(
|
|
159
|
+
`sync-protocol: WARNING — Cohort checkout not found at ${cohortRoot} (${source}).`,
|
|
160
|
+
);
|
|
161
|
+
console.warn(
|
|
162
|
+
"sync-protocol: skipped re-vendoring @cohort/protocol. Once the repo is present, re-run to sync:",
|
|
163
|
+
);
|
|
164
|
+
console.warn(`sync-protocol: ${vendoredIndex}`);
|
|
165
|
+
console.warn(`sync-protocol: ${cohortChecksum}`);
|
|
166
|
+
} else {
|
|
167
|
+
const protocolBytes = readFileSync(PROTOCOL_PATH);
|
|
168
|
+
const curVendored =
|
|
169
|
+
existsSync(vendoredIndex) ? readFileSync(vendoredIndex) : null;
|
|
170
|
+
const curCohortChecksum =
|
|
171
|
+
existsSync(cohortChecksum)
|
|
172
|
+
? readFileSync(cohortChecksum, "utf8").trim()
|
|
173
|
+
: null;
|
|
174
|
+
const vendoredInSync =
|
|
175
|
+
curVendored !== null && Buffer.compare(curVendored, protocolBytes) === 0;
|
|
176
|
+
const cohortChecksumInSync = curCohortChecksum === digest;
|
|
177
|
+
|
|
178
|
+
if (!vendoredInSync || !cohortChecksumInSync) {
|
|
179
|
+
drift = true;
|
|
180
|
+
if (!dryRun && !checkOnly) {
|
|
181
|
+
mkdirSync(pkgDir, { recursive: true });
|
|
182
|
+
writeFileSync(vendoredIndex, protocolBytes);
|
|
183
|
+
writeFileSync(cohortChecksum, digest + "\n");
|
|
184
|
+
console.log(`sync-protocol: re-vendored ${vendoredIndex}`);
|
|
185
|
+
console.log(`sync-protocol: wrote ${cohortChecksum}`);
|
|
186
|
+
} else {
|
|
187
|
+
console.log(`sync-protocol: OUT OF SYNC ${vendoredIndex}`);
|
|
188
|
+
console.log(
|
|
189
|
+
`sync-protocol: OUT OF SYNC ${cohortChecksum} (${curCohortChecksum ?? "missing"} != ${digest})`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
console.log(`sync-protocol: up-to-date ${vendoredIndex}`);
|
|
194
|
+
console.log(`sync-protocol: up-to-date ${cohortChecksum}`);
|
|
195
|
+
}
|
|
196
|
+
committed.add(vendoredIndex);
|
|
197
|
+
committed.add(cohortChecksum);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---- summary ----
|
|
201
|
+
console.log("");
|
|
202
|
+
console.log(`sync-protocol: checksum = ${digest}`);
|
|
203
|
+
console.log("sync-protocol: files to commit (both repos):");
|
|
204
|
+
for (const f of committed) console.log(` - ${f}`);
|
|
205
|
+
|
|
206
|
+
if (checkOnly) {
|
|
207
|
+
if (drift) {
|
|
208
|
+
console.error("sync-protocol: --check FAILED — protocol artifacts are out of sync. Run `node scripts/sync-protocol.mjs`.");
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
console.log("sync-protocol: --check OK — everything is in sync.");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
main();
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Agent System Verification — checks all components are operational
|
|
3
|
+
# Usage: ./scripts/system-verify.sh
|
|
4
|
+
#
|
|
5
|
+
# Run this anytime to verify the full system is working.
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
9
|
+
cd "$AGENT_REPO_DIR"
|
|
10
|
+
|
|
11
|
+
# Load identity (AGENT_LAUNCHD_PREFIX in particular) — the verification
|
|
12
|
+
# checks read launchd labels with the agent-specific prefix.
|
|
13
|
+
if [ -f "$AGENT_REPO_DIR/config/agent.env" ]; then
|
|
14
|
+
# shellcheck disable=SC1091
|
|
15
|
+
source "$AGENT_REPO_DIR/config/agent.env"
|
|
16
|
+
fi
|
|
17
|
+
LAUNCHD_PREFIX="${AGENT_LAUNCHD_PREFIX:-ai.maestro.agent}"
|
|
18
|
+
|
|
19
|
+
RED='\033[0;31m'
|
|
20
|
+
GREEN='\033[0;32m'
|
|
21
|
+
YELLOW='\033[1;33m'
|
|
22
|
+
NC='\033[0m'
|
|
23
|
+
|
|
24
|
+
pass=0
|
|
25
|
+
warn=0
|
|
26
|
+
fail=0
|
|
27
|
+
|
|
28
|
+
check() {
|
|
29
|
+
if eval "$2" > /dev/null 2>&1; then
|
|
30
|
+
echo -e " ${GREEN}✓${NC} $1"
|
|
31
|
+
((pass++))
|
|
32
|
+
else
|
|
33
|
+
echo -e " ${RED}✗${NC} $1"
|
|
34
|
+
((fail++))
|
|
35
|
+
fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
warn_check() {
|
|
39
|
+
if eval "$2" > /dev/null 2>&1; then
|
|
40
|
+
echo -e " ${GREEN}✓${NC} $1"
|
|
41
|
+
((pass++))
|
|
42
|
+
else
|
|
43
|
+
echo -e " ${YELLOW}⚠${NC} $1"
|
|
44
|
+
((warn++))
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
echo "╔══════════════════════════════════════════════╗"
|
|
49
|
+
printf "║ %-44s║\n" "$(echo "${AGENT_FULL_NAME:-AGENT}" | tr '[:lower:]' '[:upper:]') SYSTEM VERIFICATION"
|
|
50
|
+
echo "║ $(date '+%Y-%m-%d %H:%M %Z') ║"
|
|
51
|
+
echo "╚══════════════════════════════════════════════╝"
|
|
52
|
+
echo ""
|
|
53
|
+
|
|
54
|
+
echo "=== DAEMONS ==="
|
|
55
|
+
# Launchd labels start with AGENT_LAUNCHD_PREFIX (e.g. ai.maestro.ravi).
|
|
56
|
+
# The legacy code searched for "robin-poller" etc.; we now derive the
|
|
57
|
+
# agent-specific component from the prefix's last segment so the check
|
|
58
|
+
# works for any agent.
|
|
59
|
+
LAUNCHD_AGENT_SLUG="$(basename "$LAUNCHD_PREFIX" | tr -d ' ')" # e.g. "ravi" from "ai.maestro.ravi"
|
|
60
|
+
check "Poller daemon loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-poller"
|
|
61
|
+
check "Inbox processor loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-inbox-processor"
|
|
62
|
+
check "Meeting prep loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-meeting-prep"
|
|
63
|
+
check "Meeting capture loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-meeting-action-capture"
|
|
64
|
+
check "Midday sweep loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-midday-sweep"
|
|
65
|
+
check "Weekly hiring loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-weekly-hiring"
|
|
66
|
+
check "Weekly priorities loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-weekly-priorities"
|
|
67
|
+
check "Weekly eng health loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-weekly-engineering-health"
|
|
68
|
+
check "Weekly execution loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-weekly-execution"
|
|
69
|
+
check "Weekly strategic memo loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-weekly-strategic-memo"
|
|
70
|
+
check "Quarterly self-assessment loaded" "launchctl list | grep -q ${LAUNCHD_AGENT_SLUG}-quarterly"
|
|
71
|
+
echo ""
|
|
72
|
+
|
|
73
|
+
echo "=== REMOTE TRIGGERS ==="
|
|
74
|
+
warn_check "Git pushed to origin" "test $(git rev-list --count origin/main..HEAD) -eq 0"
|
|
75
|
+
echo " Remote triggers read from GitHub — must be pushed to work"
|
|
76
|
+
echo ""
|
|
77
|
+
|
|
78
|
+
echo "=== POLLER ACTIVITY ==="
|
|
79
|
+
TODAY=$(date -u +%Y-%m-%d)
|
|
80
|
+
YESTERDAY=$(date -u -v-1d +%Y-%m-%d 2>/dev/null || date -u -d yesterday +%Y-%m-%d 2>/dev/null || echo "unknown")
|
|
81
|
+
POLLER_LOG="logs/polling/${TODAY}-poller.jsonl"
|
|
82
|
+
POLLER_LOG_YESTERDAY="logs/polling/${YESTERDAY}-poller.jsonl"
|
|
83
|
+
if [ -f "$POLLER_LOG" ]; then
|
|
84
|
+
LAST_POLL=$(tail -1 "$POLLER_LOG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('timestamp','unknown'))" 2>/dev/null || echo "unknown")
|
|
85
|
+
POLL_COUNT=$(wc -l < "$POLLER_LOG" | tr -d ' ')
|
|
86
|
+
echo -e " ${GREEN}✓${NC} Poller active today: $POLL_COUNT polls, last at $LAST_POLL"
|
|
87
|
+
((pass++))
|
|
88
|
+
elif [ -f "$POLLER_LOG_YESTERDAY" ]; then
|
|
89
|
+
LAST_POLL=$(tail -1 "$POLLER_LOG_YESTERDAY" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('timestamp','unknown'))" 2>/dev/null || echo "unknown")
|
|
90
|
+
echo -e " ${YELLOW}⚠${NC} No log today, yesterday's last poll: $LAST_POLL"
|
|
91
|
+
((warn++))
|
|
92
|
+
else
|
|
93
|
+
echo -e " ${RED}✗${NC} No poller logs found"
|
|
94
|
+
((fail++))
|
|
95
|
+
fi
|
|
96
|
+
echo ""
|
|
97
|
+
|
|
98
|
+
echo "=== WORKFLOW EXECUTION ==="
|
|
99
|
+
for workflow in inbox-processor meeting-prep meeting-action-capture; do
|
|
100
|
+
LOG="logs/workflows/${TODAY}-${workflow}.jsonl"
|
|
101
|
+
LOG_Y="logs/workflows/${YESTERDAY}-${workflow}.jsonl"
|
|
102
|
+
if [ -f "$LOG" ]; then
|
|
103
|
+
ENTRIES=$(wc -l < "$LOG" | tr -d ' ')
|
|
104
|
+
echo -e " ${GREEN}✓${NC} $workflow: $ENTRIES log entries today"
|
|
105
|
+
((pass++))
|
|
106
|
+
elif [ -f "$LOG_Y" ]; then
|
|
107
|
+
echo -e " ${YELLOW}⚠${NC} $workflow: ran yesterday, not yet today"
|
|
108
|
+
((warn++))
|
|
109
|
+
else
|
|
110
|
+
echo -e " ${RED}✗${NC} $workflow: no recent logs"
|
|
111
|
+
((fail++))
|
|
112
|
+
fi
|
|
113
|
+
done
|
|
114
|
+
echo ""
|
|
115
|
+
|
|
116
|
+
echo "=== QUEUE HEALTH ==="
|
|
117
|
+
TOTAL=0
|
|
118
|
+
for q in state/queues/*.yaml; do
|
|
119
|
+
COUNT=$(grep -c "^ - id:" "$q" 2>/dev/null || echo 0)
|
|
120
|
+
TOTAL=$((TOTAL + COUNT))
|
|
121
|
+
done
|
|
122
|
+
echo " Total items across all queues: $TOTAL"
|
|
123
|
+
if [ "$TOTAL" -gt 0 ]; then
|
|
124
|
+
echo -e " ${GREEN}✓${NC} Queues populated"
|
|
125
|
+
((pass++))
|
|
126
|
+
else
|
|
127
|
+
echo -e " ${RED}✗${NC} All queues empty — backlog not loaded"
|
|
128
|
+
((fail++))
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
CRITICAL=$(grep -r "priority: critical" state/queues/ 2>/dev/null | wc -l | tr -d ' ')
|
|
132
|
+
echo " Critical items: $CRITICAL"
|
|
133
|
+
echo ""
|
|
134
|
+
|
|
135
|
+
echo "=== DASHBOARDS ==="
|
|
136
|
+
for dash in executive-summary strategy-scorecard org-health leadership-accountability engineering-health relationship-pipeline capital-readiness risk-register system-health; do
|
|
137
|
+
check "Dashboard: $dash" "test -f state/dashboards/${dash}.yaml"
|
|
138
|
+
done
|
|
139
|
+
echo ""
|
|
140
|
+
|
|
141
|
+
echo "=== STATE FILES ==="
|
|
142
|
+
check "15+ queue files" "test $(ls state/queues/*.yaml | wc -l) -ge 15"
|
|
143
|
+
check "5 user profiles" "test $(ls memory/profiles/users/*.yaml | wc -l) -ge 5"
|
|
144
|
+
check "10 channel profiles" "test $(ls memory/profiles/channels/*.yaml | wc -l) -ge 10"
|
|
145
|
+
check "Decision library" "test -f knowledge/decisions/index.yaml"
|
|
146
|
+
check "Strategy state" "test -f knowledge/syntheses/strategy-state.yaml"
|
|
147
|
+
check "Blocker playbooks" "test -f knowledge/sources/blocker-playbooks.yaml"
|
|
148
|
+
check "SLA defaults" "test -f config/sla-defaults.yaml"
|
|
149
|
+
check "Defence policy" "test -f policies/prompt-injection-defence.yaml"
|
|
150
|
+
echo ""
|
|
151
|
+
|
|
152
|
+
echo "=== HOOKS ==="
|
|
153
|
+
check "Pre-send audit hook" "test -x scripts/hooks/pre-send-audit.sh"
|
|
154
|
+
check "Post-action log hook" "test -x scripts/hooks/post-action-log.sh"
|
|
155
|
+
check "Session-end log hook" "test -x scripts/hooks/session-end-log.sh"
|
|
156
|
+
check "Hooks configured in settings" "grep -q 'pre-send-audit' .claude/settings.json"
|
|
157
|
+
echo ""
|
|
158
|
+
|
|
159
|
+
echo "=== TRIGGER PROMPTS ==="
|
|
160
|
+
TRIGGER_COUNT=$(ls schedules/triggers/*.md | wc -l | tr -d ' ')
|
|
161
|
+
check "12+ trigger prompts" "test $TRIGGER_COUNT -ge 12"
|
|
162
|
+
echo " Trigger prompts: $TRIGGER_COUNT"
|
|
163
|
+
echo ""
|
|
164
|
+
|
|
165
|
+
echo "=== CLAUDE.MD ==="
|
|
166
|
+
CT_COUNT=$(grep -c "Control Tower" CLAUDE.md)
|
|
167
|
+
check "12 control towers documented" "test $CT_COUNT -ge 12"
|
|
168
|
+
check "Agent teams enabled" "grep -q 'AGENT_TEAMS' .claude/settings.json"
|
|
169
|
+
echo ""
|
|
170
|
+
|
|
171
|
+
echo "══════════════════════════════════════════════"
|
|
172
|
+
echo -e " ${GREEN}PASS: $pass${NC} ${YELLOW}WARN: $warn${NC} ${RED}FAIL: $fail${NC}"
|
|
173
|
+
echo "══════════════════════════════════════════════"
|
|
174
|
+
|
|
175
|
+
if [ "$fail" -gt 0 ]; then
|
|
176
|
+
echo -e " ${RED}System has failures — investigate above${NC}"
|
|
177
|
+
exit 1
|
|
178
|
+
elif [ "$warn" -gt 0 ]; then
|
|
179
|
+
echo -e " ${YELLOW}System operational with warnings${NC}"
|
|
180
|
+
exit 0
|
|
181
|
+
else
|
|
182
|
+
echo -e " ${GREEN}System fully operational${NC}"
|
|
183
|
+
exit 0
|
|
184
|
+
fi
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Tests for email-thread-dedup.py — thread-level email deduplication.
|
|
3
|
+
|
|
4
|
+
Run: python3 scripts/test-email-thread-dedup.py
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
# Add scripts dir to path
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
13
|
+
from email_thread_dedup import (
|
|
14
|
+
_normalize_subject,
|
|
15
|
+
_normalize_recipient,
|
|
16
|
+
_thread_key,
|
|
17
|
+
check_thread_lock,
|
|
18
|
+
acquire_thread_lock,
|
|
19
|
+
confirm_thread_lock,
|
|
20
|
+
release_thread_lock,
|
|
21
|
+
clean_expired_locks,
|
|
22
|
+
LOCK_DIR,
|
|
23
|
+
TTL_SECONDS,
|
|
24
|
+
_lock_path,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
PASS = 0
|
|
28
|
+
FAIL = 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def assert_eq(label, actual, expected):
|
|
32
|
+
global PASS, FAIL
|
|
33
|
+
if actual == expected:
|
|
34
|
+
PASS += 1
|
|
35
|
+
print(f" PASS: {label}")
|
|
36
|
+
else:
|
|
37
|
+
FAIL += 1
|
|
38
|
+
print(f" FAIL: {label}")
|
|
39
|
+
print(f" expected: {expected!r}")
|
|
40
|
+
print(f" actual: {actual!r}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def assert_true(label, value):
|
|
44
|
+
assert_eq(label, bool(value), True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def assert_false(label, value):
|
|
48
|
+
assert_eq(label, bool(value), False)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# --- Test subject normalization ---
|
|
52
|
+
|
|
53
|
+
print("\n=== Subject normalization ===")
|
|
54
|
+
assert_eq("strips Re:", _normalize_subject("Re: Hello"), "Hello")
|
|
55
|
+
assert_eq("strips RE:", _normalize_subject("RE: Hello"), "Hello")
|
|
56
|
+
assert_eq("strips Fwd:", _normalize_subject("Fwd: Hello"), "Hello")
|
|
57
|
+
assert_eq("strips FW:", _normalize_subject("FW: Hello"), "Hello")
|
|
58
|
+
assert_eq("strips nested Re: Re:", _normalize_subject("Re: Re: Hello"), "Hello")
|
|
59
|
+
assert_eq("strips mixed Re: Fwd:", _normalize_subject("Re: Fwd: Hello"), "Hello")
|
|
60
|
+
assert_eq("normalizes whitespace", _normalize_subject(" Re: Hello World "), "Hello World")
|
|
61
|
+
assert_eq("preserves clean subject", _normalize_subject("Hello World"), "Hello World")
|
|
62
|
+
|
|
63
|
+
# --- Test recipient normalization ---
|
|
64
|
+
|
|
65
|
+
print("\n=== Recipient normalization ===")
|
|
66
|
+
assert_eq("lowercases", _normalize_recipient("John@Example.COM"), "john@example.com")
|
|
67
|
+
assert_eq("strips display name", _normalize_recipient("John Doe <john@example.com>"), "john@example.com")
|
|
68
|
+
assert_eq("takes first of multiple", _normalize_recipient("john@a.com, jane@b.com"), "john@a.com")
|
|
69
|
+
assert_eq("strips whitespace", _normalize_recipient(" john@example.com "), "john@example.com")
|
|
70
|
+
|
|
71
|
+
# --- Test thread key consistency ---
|
|
72
|
+
|
|
73
|
+
print("\n=== Thread key consistency ===")
|
|
74
|
+
key1 = _thread_key("john@example.com", "Re: Hello World")
|
|
75
|
+
key2 = _thread_key("john@example.com", "Hello World")
|
|
76
|
+
key3 = _thread_key("john@example.com", "RE: Fwd: Hello World")
|
|
77
|
+
key4 = _thread_key("JOHN@EXAMPLE.COM", "Re: Hello World")
|
|
78
|
+
key5 = _thread_key("john@example.com", "Different Subject")
|
|
79
|
+
|
|
80
|
+
assert_eq("Re: stripped matches base", key1, key2)
|
|
81
|
+
assert_eq("RE: Fwd: stripped matches base", key3, key2)
|
|
82
|
+
assert_eq("case-insensitive recipient", key4, key1)
|
|
83
|
+
assert_true("different subject = different key", key5 != key1)
|
|
84
|
+
|
|
85
|
+
# --- Test lock lifecycle ---
|
|
86
|
+
|
|
87
|
+
print("\n=== Lock lifecycle ===")
|
|
88
|
+
|
|
89
|
+
# Use unique test subjects to avoid interference
|
|
90
|
+
TEST_TO = "test-dedup@example.com"
|
|
91
|
+
TEST_SUBJECT = f"Test Subject {time.time()}"
|
|
92
|
+
|
|
93
|
+
# Clean start
|
|
94
|
+
release_thread_lock(TEST_TO, TEST_SUBJECT)
|
|
95
|
+
|
|
96
|
+
# 1. Check — should be available
|
|
97
|
+
result = check_thread_lock(TEST_TO, TEST_SUBJECT)
|
|
98
|
+
assert_false("initially unlocked", result["locked"])
|
|
99
|
+
|
|
100
|
+
# 2. Acquire — should succeed
|
|
101
|
+
acq = acquire_thread_lock(TEST_TO, TEST_SUBJECT, session_id="test-session-1", preview="Hello this is a test")
|
|
102
|
+
assert_true("first acquire succeeds", acq["acquired"])
|
|
103
|
+
assert_true("thread key returned", len(acq["thread_key"]) > 0)
|
|
104
|
+
|
|
105
|
+
# 3. Check again — should now be locked
|
|
106
|
+
result = check_thread_lock(TEST_TO, TEST_SUBJECT)
|
|
107
|
+
assert_true("now locked", result["locked"])
|
|
108
|
+
assert_eq("locked by correct session", result["lock_data"]["session_id"], "test-session-1")
|
|
109
|
+
|
|
110
|
+
# 4. Second acquire — should be BLOCKED
|
|
111
|
+
acq2 = acquire_thread_lock(TEST_TO, TEST_SUBJECT, session_id="test-session-2", preview="Different wording")
|
|
112
|
+
assert_false("second acquire blocked", acq2["acquired"])
|
|
113
|
+
assert_true("existing lock returned", acq2["existing_lock"] is not None)
|
|
114
|
+
assert_eq("existing lock session", acq2["existing_lock"]["session_id"], "test-session-1")
|
|
115
|
+
|
|
116
|
+
# 5. Confirm — should update status
|
|
117
|
+
confirm_thread_lock(TEST_TO, TEST_SUBJECT, preview="Confirmed preview text")
|
|
118
|
+
result = check_thread_lock(TEST_TO, TEST_SUBJECT)
|
|
119
|
+
assert_true("still locked after confirm", result["locked"])
|
|
120
|
+
assert_eq("status is confirmed", result["lock_data"]["status"], "confirmed")
|
|
121
|
+
|
|
122
|
+
# 6. Release — should unlock
|
|
123
|
+
release_thread_lock(TEST_TO, TEST_SUBJECT)
|
|
124
|
+
result = check_thread_lock(TEST_TO, TEST_SUBJECT)
|
|
125
|
+
assert_false("unlocked after release", result["locked"])
|
|
126
|
+
|
|
127
|
+
# --- Test expired lock handling ---
|
|
128
|
+
|
|
129
|
+
print("\n=== Expired lock handling ===")
|
|
130
|
+
TEST_SUBJECT_EXP = f"Expired Subject {time.time()}"
|
|
131
|
+
|
|
132
|
+
# Create a lock with a timestamp in the past (>24h ago)
|
|
133
|
+
key = _thread_key(TEST_TO, TEST_SUBJECT_EXP)
|
|
134
|
+
path = _lock_path(key)
|
|
135
|
+
os.makedirs(LOCK_DIR, exist_ok=True)
|
|
136
|
+
expired_data = {
|
|
137
|
+
"thread_key": key,
|
|
138
|
+
"timestamp": "2026-01-01T00:00:00Z",
|
|
139
|
+
"timestamp_epoch": time.time() - (TTL_SECONDS + 3600), # 25 hours ago
|
|
140
|
+
"session_id": "old-session",
|
|
141
|
+
"subject_normalized": "Expired Subject",
|
|
142
|
+
"recipient_normalized": TEST_TO,
|
|
143
|
+
"preview": "old message",
|
|
144
|
+
"status": "confirmed",
|
|
145
|
+
}
|
|
146
|
+
with open(path, "w") as f:
|
|
147
|
+
json.dump(expired_data, f)
|
|
148
|
+
|
|
149
|
+
# Check — expired lock should NOT block
|
|
150
|
+
result = check_thread_lock(TEST_TO, TEST_SUBJECT_EXP)
|
|
151
|
+
assert_false("expired lock does not block", result["locked"])
|
|
152
|
+
|
|
153
|
+
# Acquire over expired lock — should succeed
|
|
154
|
+
acq = acquire_thread_lock(TEST_TO, TEST_SUBJECT_EXP, session_id="new-session")
|
|
155
|
+
assert_true("acquire over expired lock succeeds", acq["acquired"])
|
|
156
|
+
|
|
157
|
+
# Clean up
|
|
158
|
+
release_thread_lock(TEST_TO, TEST_SUBJECT_EXP)
|
|
159
|
+
|
|
160
|
+
# --- Test Re:/Fwd: variant blocking ---
|
|
161
|
+
|
|
162
|
+
print("\n=== Subject variant blocking ===")
|
|
163
|
+
TEST_SUBJECT_VAR = f"Important Meeting Notes {time.time()}"
|
|
164
|
+
release_thread_lock(TEST_TO, TEST_SUBJECT_VAR) # clean start
|
|
165
|
+
|
|
166
|
+
# Acquire with base subject
|
|
167
|
+
acq = acquire_thread_lock(TEST_TO, TEST_SUBJECT_VAR, session_id="session-a")
|
|
168
|
+
assert_true("acquire base subject", acq["acquired"])
|
|
169
|
+
|
|
170
|
+
# Attempt with Re: variant — should be blocked (same thread)
|
|
171
|
+
acq2 = acquire_thread_lock(TEST_TO, f"Re: {TEST_SUBJECT_VAR}", session_id="session-b")
|
|
172
|
+
assert_false("Re: variant blocked", acq2["acquired"])
|
|
173
|
+
|
|
174
|
+
# Attempt with RE: Fwd: variant — should also be blocked
|
|
175
|
+
acq3 = acquire_thread_lock(TEST_TO, f"RE: Fwd: {TEST_SUBJECT_VAR}", session_id="session-c")
|
|
176
|
+
assert_false("RE: Fwd: variant blocked", acq3["acquired"])
|
|
177
|
+
|
|
178
|
+
# Clean up
|
|
179
|
+
release_thread_lock(TEST_TO, TEST_SUBJECT_VAR)
|
|
180
|
+
|
|
181
|
+
# --- Test clean_expired_locks ---
|
|
182
|
+
|
|
183
|
+
print("\n=== Cleanup function ===")
|
|
184
|
+
TEST_SUBJECT_CLN = f"Cleanup Test {time.time()}"
|
|
185
|
+
|
|
186
|
+
# Create an expired lock
|
|
187
|
+
key = _thread_key(TEST_TO, TEST_SUBJECT_CLN)
|
|
188
|
+
path = _lock_path(key)
|
|
189
|
+
expired_data = {
|
|
190
|
+
"thread_key": key,
|
|
191
|
+
"timestamp": "2026-01-01T00:00:00Z",
|
|
192
|
+
"timestamp_epoch": time.time() - (TTL_SECONDS + 3600),
|
|
193
|
+
"session_id": "cleanup-test",
|
|
194
|
+
"subject_normalized": "Cleanup Test",
|
|
195
|
+
"recipient_normalized": TEST_TO,
|
|
196
|
+
"status": "confirmed",
|
|
197
|
+
}
|
|
198
|
+
with open(path, "w") as f:
|
|
199
|
+
json.dump(expired_data, f)
|
|
200
|
+
|
|
201
|
+
assert_true("expired lock file exists before clean", os.path.exists(path))
|
|
202
|
+
removed = clean_expired_locks()
|
|
203
|
+
assert_true("cleanup removed at least 1", removed >= 1)
|
|
204
|
+
assert_false("expired lock file gone after clean", os.path.exists(path))
|
|
205
|
+
|
|
206
|
+
# --- Test lock file contents ---
|
|
207
|
+
|
|
208
|
+
print("\n=== Lock file contents ===")
|
|
209
|
+
TEST_SUBJECT_META = f"Metadata Check {time.time()}"
|
|
210
|
+
release_thread_lock(TEST_TO, TEST_SUBJECT_META)
|
|
211
|
+
|
|
212
|
+
acquire_thread_lock(TEST_TO, TEST_SUBJECT_META, session_id="meta-session", preview="Hello, I wanted to follow up on our conversation")
|
|
213
|
+
key = _thread_key(TEST_TO, TEST_SUBJECT_META)
|
|
214
|
+
path = _lock_path(key)
|
|
215
|
+
|
|
216
|
+
with open(path, "r") as f:
|
|
217
|
+
data = json.load(f)
|
|
218
|
+
|
|
219
|
+
assert_eq("has thread_key", data.get("thread_key"), key)
|
|
220
|
+
assert_true("has timestamp", "timestamp" in data)
|
|
221
|
+
assert_true("has timestamp_epoch", "timestamp_epoch" in data)
|
|
222
|
+
assert_eq("has session_id", data.get("session_id"), "meta-session")
|
|
223
|
+
assert_true("has subject_normalized", "subject_normalized" in data)
|
|
224
|
+
assert_true("has recipient_normalized", "recipient_normalized" in data)
|
|
225
|
+
assert_true("has preview", len(data.get("preview", "")) > 0)
|
|
226
|
+
assert_eq("status is acquired", data.get("status"), "acquired")
|
|
227
|
+
|
|
228
|
+
release_thread_lock(TEST_TO, TEST_SUBJECT_META)
|
|
229
|
+
|
|
230
|
+
# --- Summary ---
|
|
231
|
+
|
|
232
|
+
print(f"\n{'='*50}")
|
|
233
|
+
print(f"Results: {PASS} passed, {FAIL} failed, {PASS + FAIL} total")
|
|
234
|
+
if FAIL > 0:
|
|
235
|
+
print("SOME TESTS FAILED")
|
|
236
|
+
sys.exit(1)
|
|
237
|
+
else:
|
|
238
|
+
print("ALL TESTS PASSED")
|
|
239
|
+
sys.exit(0)
|