@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,167 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fetch and format the most recent message in an email thread for quoting in replies.
|
|
3
|
+
|
|
4
|
+
Used by send-email-threaded.py and send-email-as-principal.py to include the
|
|
5
|
+
original email chain in reply bodies — matching standard email client behaviour.
|
|
6
|
+
"""
|
|
7
|
+
import imaplib
|
|
8
|
+
import email
|
|
9
|
+
from email.utils import parsedate_to_datetime, formatdate
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def fetch_thread_body(imap_user, imap_password, subject_search, exclude_from=None):
|
|
15
|
+
"""Fetch the most recent message body from a thread for quoting.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
imap_user: Gmail address for IMAP login
|
|
19
|
+
imap_password: App password for IMAP login
|
|
20
|
+
subject_search: Subject line to search for (Re:/Fwd: prefixes stripped)
|
|
21
|
+
exclude_from: Email address to exclude (avoid quoting our own messages)
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
dict with keys: from_header, date_header, to_header, body_html, body_text
|
|
25
|
+
or None if no matching message found.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
mail = imaplib.IMAP4_SSL("imap.gmail.com")
|
|
29
|
+
mail.login(imap_user, imap_password)
|
|
30
|
+
|
|
31
|
+
clean_subject = subject_search.replace("Re: ", "").replace("RE: ", "").replace("Fwd: ", "").strip()
|
|
32
|
+
# Sanitise for IMAP search
|
|
33
|
+
ascii_subject = (
|
|
34
|
+
clean_subject
|
|
35
|
+
.replace('\u2014', '-').replace('\u2013', '-')
|
|
36
|
+
.replace('\u2018', "'").replace('\u2019', "'")
|
|
37
|
+
.replace('\u201c', '"').replace('\u201d', '"')
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
best = None # (date, result_dict, raw_msg)
|
|
41
|
+
|
|
42
|
+
for folder in ["INBOX", "[Gmail]/Sent Mail"]:
|
|
43
|
+
try:
|
|
44
|
+
mail.select(folder, readonly=True)
|
|
45
|
+
status, messages = mail.search(None, f'(SUBJECT "{ascii_subject}")')
|
|
46
|
+
if status != "OK" or not messages[0]:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
for num in messages[0].split():
|
|
50
|
+
status, data = mail.fetch(num, "(BODY[])")
|
|
51
|
+
if status != "OK":
|
|
52
|
+
continue
|
|
53
|
+
raw = data[0][1]
|
|
54
|
+
msg = email.message_from_bytes(raw)
|
|
55
|
+
|
|
56
|
+
# Skip our own messages so we quote the other party
|
|
57
|
+
from_hdr = msg.get("From", "")
|
|
58
|
+
if exclude_from and exclude_from.lower() in from_hdr.lower():
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
# Parse date for recency comparison
|
|
62
|
+
date_str = msg.get("Date", "")
|
|
63
|
+
try:
|
|
64
|
+
dt = parsedate_to_datetime(date_str)
|
|
65
|
+
except Exception:
|
|
66
|
+
dt = datetime.min
|
|
67
|
+
|
|
68
|
+
if best is None or dt > best[0]:
|
|
69
|
+
body_html, body_text = _extract_body(msg)
|
|
70
|
+
best = (dt, {
|
|
71
|
+
"from_header": from_hdr,
|
|
72
|
+
"date_header": date_str,
|
|
73
|
+
"to_header": msg.get("To", ""),
|
|
74
|
+
"cc_header": msg.get("Cc", ""),
|
|
75
|
+
"subject": msg.get("Subject", ""),
|
|
76
|
+
"body_html": body_html,
|
|
77
|
+
"body_text": body_text,
|
|
78
|
+
}, None)
|
|
79
|
+
except Exception:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
mail.logout()
|
|
83
|
+
return best[1] if best else None
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
import sys
|
|
87
|
+
print(f"QUOTE_FETCH_WARN: {e}", file=sys.stderr)
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _extract_body(msg):
|
|
92
|
+
"""Extract HTML and plain-text body from an email.message.Message."""
|
|
93
|
+
html_body = None
|
|
94
|
+
text_body = None
|
|
95
|
+
|
|
96
|
+
if msg.is_multipart():
|
|
97
|
+
for part in msg.walk():
|
|
98
|
+
ct = part.get_content_type()
|
|
99
|
+
disp = str(part.get("Content-Disposition", ""))
|
|
100
|
+
if "attachment" in disp:
|
|
101
|
+
continue
|
|
102
|
+
try:
|
|
103
|
+
payload = part.get_payload(decode=True)
|
|
104
|
+
if payload is None:
|
|
105
|
+
continue
|
|
106
|
+
charset = part.get_content_charset() or "utf-8"
|
|
107
|
+
decoded = payload.decode(charset, errors="replace")
|
|
108
|
+
except Exception:
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
if ct == "text/html" and html_body is None:
|
|
112
|
+
html_body = decoded
|
|
113
|
+
elif ct == "text/plain" and text_body is None:
|
|
114
|
+
text_body = decoded
|
|
115
|
+
else:
|
|
116
|
+
try:
|
|
117
|
+
payload = msg.get_payload(decode=True)
|
|
118
|
+
charset = msg.get_content_charset() or "utf-8"
|
|
119
|
+
decoded = payload.decode(charset, errors="replace")
|
|
120
|
+
if msg.get_content_type() == "text/html":
|
|
121
|
+
html_body = decoded
|
|
122
|
+
else:
|
|
123
|
+
text_body = decoded
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
return html_body, text_body
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def format_quoted_html(thread_msg):
|
|
131
|
+
"""Format a fetched thread message as a Gmail-style quoted block.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
thread_msg: dict returned by fetch_thread_body()
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
HTML string with the quoted content, or empty string if no content.
|
|
138
|
+
"""
|
|
139
|
+
if not thread_msg:
|
|
140
|
+
return ""
|
|
141
|
+
|
|
142
|
+
# Build the "On <date>, <from> wrote:" line
|
|
143
|
+
from_display = thread_msg["from_header"]
|
|
144
|
+
date_display = thread_msg["date_header"]
|
|
145
|
+
wrote_line = f"On {date_display}, {from_display} wrote:"
|
|
146
|
+
|
|
147
|
+
# Prefer HTML body; fall back to plain text converted to HTML
|
|
148
|
+
if thread_msg["body_html"]:
|
|
149
|
+
# Strip outer <html>/<body> tags to embed cleanly
|
|
150
|
+
quoted_content = thread_msg["body_html"]
|
|
151
|
+
quoted_content = re.sub(r'</?html[^>]*>', '', quoted_content, flags=re.IGNORECASE)
|
|
152
|
+
quoted_content = re.sub(r'</?body[^>]*>', '', quoted_content, flags=re.IGNORECASE)
|
|
153
|
+
quoted_content = re.sub(r'<!DOCTYPE[^>]*>', '', quoted_content, flags=re.IGNORECASE)
|
|
154
|
+
quoted_content = re.sub(r'<head>.*?</head>', '', quoted_content, flags=re.IGNORECASE | re.DOTALL)
|
|
155
|
+
elif thread_msg["body_text"]:
|
|
156
|
+
quoted_content = thread_msg["body_text"].replace('\n', '<br>')
|
|
157
|
+
else:
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
return f"""<br>
|
|
161
|
+
<div class="gmail_quote">
|
|
162
|
+
<div style="margin:0 0 0 .8ex;border-left:1px solid #ccc;padding-left:1ex;">
|
|
163
|
+
<div style="font-size:small;color:#888;">{wrote_line}</div>
|
|
164
|
+
<br>
|
|
165
|
+
{quoted_content}
|
|
166
|
+
</div>
|
|
167
|
+
</div>"""
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Email thread-level deduplication — prevents replying twice to the same thread.
|
|
3
|
+
|
|
4
|
+
Unlike the content-hash dedup in outbound_dedup.py (which catches identical sends),
|
|
5
|
+
this module keys on the *thread identity* (normalized subject + primary recipient).
|
|
6
|
+
Two replies with completely different wording to the same thread will be caught.
|
|
7
|
+
|
|
8
|
+
Lock dir: state/locks/outbound/email-threads/
|
|
9
|
+
Lock file: {thread_key}.lock (JSON with metadata)
|
|
10
|
+
TTL: 24 hours
|
|
11
|
+
|
|
12
|
+
Usage (standalone):
|
|
13
|
+
python3 email-thread-dedup.py check <to> <subject>
|
|
14
|
+
python3 email-thread-dedup.py acquire <to> <subject> [--session-id ID] [--preview TEXT]
|
|
15
|
+
python3 email-thread-dedup.py release <to> <subject>
|
|
16
|
+
python3 email-thread-dedup.py clean
|
|
17
|
+
|
|
18
|
+
Usage (as module):
|
|
19
|
+
from email_thread_dedup import check_thread_lock, acquire_thread_lock
|
|
20
|
+
|
|
21
|
+
result = check_thread_lock(to, subject)
|
|
22
|
+
if result["locked"]:
|
|
23
|
+
print(f"DEDUP_SKIP: Thread already replied to at {result['timestamp']}")
|
|
24
|
+
sys.exit(2)
|
|
25
|
+
|
|
26
|
+
acquire_thread_lock(to, subject, session_id, preview)
|
|
27
|
+
# ... send the email ...
|
|
28
|
+
"""
|
|
29
|
+
import hashlib
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import sys
|
|
34
|
+
import time
|
|
35
|
+
from datetime import datetime, timezone
|
|
36
|
+
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
AGENT_REPO_DIR = os.environ.get("AGENT_DIR", str(Path(__file__).resolve().parent.parent))
|
|
39
|
+
LOCK_DIR = os.path.join(AGENT_REPO_DIR, "state", "locks", "outbound", "email-threads")
|
|
40
|
+
AUDIT_DIR = os.path.join(AGENT_REPO_DIR, "logs", "audit")
|
|
41
|
+
TTL_SECONDS = 24 * 60 * 60 # 24 hours
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _normalize_subject(subject: str) -> str:
|
|
45
|
+
"""Normalize subject for thread matching.
|
|
46
|
+
|
|
47
|
+
Strips Re:/Fwd: prefixes, dates, times, timezones, day names, and
|
|
48
|
+
normalizes punctuation/whitespace. This ensures subjects like:
|
|
49
|
+
"Weekly Strategic Review Brief — 7 April 2026"
|
|
50
|
+
"Weekly Strategic Review Brief — Tue 7 April 2026, 14:00 GST"
|
|
51
|
+
"Re: Weekly Strategic Review Brief — 7 April 2026"
|
|
52
|
+
all hash to the same key.
|
|
53
|
+
"""
|
|
54
|
+
s = subject.strip()
|
|
55
|
+
# Iteratively strip reply/forward prefixes (case-insensitive)
|
|
56
|
+
while True:
|
|
57
|
+
new_s = re.sub(r'^(Re|RE|Fwd|FWD|Fw|FW)\s*:\s*', '', s).strip()
|
|
58
|
+
if new_s == s:
|
|
59
|
+
break
|
|
60
|
+
s = new_s
|
|
61
|
+
# Lowercase for normalization
|
|
62
|
+
s = s.lower()
|
|
63
|
+
# Strip day names (Mon, Monday, etc.)
|
|
64
|
+
s = re.sub(r'\b(mon|tue|wed|thu|fri|sat|sun)(day|nesday|rsday|urday)?\b', '', s, flags=re.IGNORECASE)
|
|
65
|
+
# Strip times (HH:MM, HH:MM:SS)
|
|
66
|
+
s = re.sub(r'\d{1,2}:\d{2}(:\d{2})?', '', s)
|
|
67
|
+
# Strip timezone abbreviations
|
|
68
|
+
s = re.sub(r'\b(gst|utc|est|pst|cst|mst|gmt|bst|cet|aest|aedt|ast|ict|jst|ist)\b', '', s, flags=re.IGNORECASE)
|
|
69
|
+
# Strip full dates: "7 April 2026", "7 Apr 2026"
|
|
70
|
+
s = re.sub(r',?\s*\d{1,2}\s+(january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{4}', '', s, flags=re.IGNORECASE)
|
|
71
|
+
s = re.sub(r',?\s*\d{1,2}\s+(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\s+\d{4}', '', s, flags=re.IGNORECASE)
|
|
72
|
+
# Strip ISO dates: 2026-04-07
|
|
73
|
+
s = re.sub(r'\d{4}-\d{2}-\d{2}', '', s)
|
|
74
|
+
# Collapse repeated em-dashes and commas
|
|
75
|
+
s = re.sub(r',[\s,]*', ' ', s)
|
|
76
|
+
s = re.sub(r'\s*[—–-]+\s*[—–-]+\s*', ' — ', s)
|
|
77
|
+
s = re.sub(r'\s*[—–-]+\s*$', '', s)
|
|
78
|
+
# Normalize whitespace
|
|
79
|
+
s = re.sub(r'\s+', ' ', s).strip()
|
|
80
|
+
return s
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _normalize_recipient(to: str) -> str:
|
|
84
|
+
"""Normalize recipient: lowercase, strip whitespace, take first address if multiple."""
|
|
85
|
+
to = to.strip().lower()
|
|
86
|
+
# If comma-separated, take the first address
|
|
87
|
+
if ',' in to:
|
|
88
|
+
to = to.split(',')[0].strip()
|
|
89
|
+
# Strip display name if present: "Name <email>" -> "email"
|
|
90
|
+
match = re.search(r'<([^>]+)>', to)
|
|
91
|
+
if match:
|
|
92
|
+
to = match.group(1)
|
|
93
|
+
return to
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _thread_key(to: str, subject: str) -> str:
|
|
97
|
+
"""Generate a deterministic key for a thread: hash of normalized subject + primary recipient."""
|
|
98
|
+
norm_subject = _normalize_subject(subject)
|
|
99
|
+
norm_recipient = _normalize_recipient(to)
|
|
100
|
+
raw = f"{norm_subject}|{norm_recipient}"
|
|
101
|
+
return hashlib.sha256(raw.encode('utf-8')).hexdigest()[:24]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _lock_path(thread_key: str) -> str:
|
|
105
|
+
"""Return the full path to a thread lock file."""
|
|
106
|
+
return os.path.join(LOCK_DIR, f"{thread_key}.lock")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _now_iso() -> str:
|
|
110
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _log_audit(action: str, thread_key: str, to: str, subject: str, detail: str = ""):
|
|
114
|
+
"""Append a dedup decision to the daily audit log."""
|
|
115
|
+
os.makedirs(AUDIT_DIR, exist_ok=True)
|
|
116
|
+
date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
117
|
+
audit_file = os.path.join(AUDIT_DIR, f"{date_str}-actions.jsonl")
|
|
118
|
+
session_id = os.environ.get("SOPHIE_SESSION_ID", str(os.getpid()))
|
|
119
|
+
entry = {
|
|
120
|
+
"timestamp": _now_iso(),
|
|
121
|
+
"agent": "email-thread-dedup",
|
|
122
|
+
"action": action,
|
|
123
|
+
"thread_key": thread_key,
|
|
124
|
+
"to": to,
|
|
125
|
+
"subject": subject,
|
|
126
|
+
"detail": detail[:200],
|
|
127
|
+
"session": session_id,
|
|
128
|
+
}
|
|
129
|
+
try:
|
|
130
|
+
with open(audit_file, "a") as f:
|
|
131
|
+
f.write(json.dumps(entry) + "\n")
|
|
132
|
+
except Exception:
|
|
133
|
+
pass # Never let audit logging block sends
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def check_thread_lock(to: str, subject: str) -> dict:
|
|
137
|
+
"""Check if a thread lock exists and is still valid (within TTL).
|
|
138
|
+
|
|
139
|
+
Returns dict with:
|
|
140
|
+
locked: bool — True if a valid lock exists (should BLOCK send)
|
|
141
|
+
thread_key: str — the computed thread key
|
|
142
|
+
lock_data: dict|None — the lock metadata if locked
|
|
143
|
+
"""
|
|
144
|
+
os.makedirs(LOCK_DIR, exist_ok=True)
|
|
145
|
+
key = _thread_key(to, subject)
|
|
146
|
+
path = _lock_path(key)
|
|
147
|
+
|
|
148
|
+
if not os.path.exists(path):
|
|
149
|
+
return {"locked": False, "thread_key": key, "lock_data": None}
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
with open(path, "r") as f:
|
|
153
|
+
data = json.load(f)
|
|
154
|
+
except (json.JSONDecodeError, IOError):
|
|
155
|
+
# Corrupt lock file — treat as unlocked
|
|
156
|
+
return {"locked": False, "thread_key": key, "lock_data": None}
|
|
157
|
+
|
|
158
|
+
# Check TTL
|
|
159
|
+
lock_ts = data.get("timestamp_epoch", 0)
|
|
160
|
+
age_seconds = time.time() - lock_ts
|
|
161
|
+
if age_seconds >= TTL_SECONDS:
|
|
162
|
+
# Lock expired — treat as unlocked
|
|
163
|
+
return {"locked": False, "thread_key": key, "lock_data": data}
|
|
164
|
+
|
|
165
|
+
# Valid lock exists
|
|
166
|
+
return {"locked": True, "thread_key": key, "lock_data": data}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def acquire_thread_lock(to: str, subject: str, session_id: str = "", preview: str = "") -> dict:
|
|
170
|
+
"""Attempt to acquire a thread lock.
|
|
171
|
+
|
|
172
|
+
Returns dict with:
|
|
173
|
+
acquired: bool — True if lock was acquired (send may proceed)
|
|
174
|
+
thread_key: str
|
|
175
|
+
existing_lock: dict|None — if not acquired, the existing lock data
|
|
176
|
+
"""
|
|
177
|
+
os.makedirs(LOCK_DIR, exist_ok=True)
|
|
178
|
+
if not session_id:
|
|
179
|
+
session_id = os.environ.get("SOPHIE_SESSION_ID", str(os.getpid()))
|
|
180
|
+
|
|
181
|
+
key = _thread_key(to, subject)
|
|
182
|
+
path = _lock_path(key)
|
|
183
|
+
norm_subject = _normalize_subject(subject)
|
|
184
|
+
norm_recipient = _normalize_recipient(to)
|
|
185
|
+
|
|
186
|
+
# Check existing lock
|
|
187
|
+
check = check_thread_lock(to, subject)
|
|
188
|
+
if check["locked"]:
|
|
189
|
+
_log_audit(
|
|
190
|
+
"DEDUP_SKIP",
|
|
191
|
+
key, to, subject,
|
|
192
|
+
f"Thread already replied to by session {check['lock_data'].get('session_id', '?')} at {check['lock_data'].get('timestamp', '?')}"
|
|
193
|
+
)
|
|
194
|
+
return {"acquired": False, "thread_key": key, "existing_lock": check["lock_data"]}
|
|
195
|
+
|
|
196
|
+
# Write lock file (atomic via write-to-temp-then-rename)
|
|
197
|
+
lock_data = {
|
|
198
|
+
"thread_key": key,
|
|
199
|
+
"timestamp": _now_iso(),
|
|
200
|
+
"timestamp_epoch": time.time(),
|
|
201
|
+
"session_id": session_id,
|
|
202
|
+
"subject_normalized": norm_subject,
|
|
203
|
+
"recipient_normalized": norm_recipient,
|
|
204
|
+
"subject_original": subject[:200],
|
|
205
|
+
"recipient_original": to[:200],
|
|
206
|
+
"preview": (preview or "")[:100],
|
|
207
|
+
"status": "acquired",
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
tmp_path = path + f".tmp.{os.getpid()}"
|
|
211
|
+
try:
|
|
212
|
+
with open(tmp_path, "w") as f:
|
|
213
|
+
json.dump(lock_data, f, indent=2)
|
|
214
|
+
os.rename(tmp_path, path) # atomic on POSIX
|
|
215
|
+
except Exception as e:
|
|
216
|
+
# Clean up temp file on failure
|
|
217
|
+
try:
|
|
218
|
+
os.unlink(tmp_path)
|
|
219
|
+
except OSError:
|
|
220
|
+
pass
|
|
221
|
+
# Fail-open: if we can't write the lock, allow the send
|
|
222
|
+
_log_audit("LOCK_WRITE_ERROR", key, to, subject, str(e))
|
|
223
|
+
return {"acquired": True, "thread_key": key, "existing_lock": None}
|
|
224
|
+
|
|
225
|
+
_log_audit("SEND", key, to, subject, f"Lock acquired by session {session_id}")
|
|
226
|
+
return {"acquired": True, "thread_key": key, "existing_lock": None}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def confirm_thread_lock(to: str, subject: str, preview: str = ""):
|
|
230
|
+
"""Update a lock file after successful send with confirmation details."""
|
|
231
|
+
key = _thread_key(to, subject)
|
|
232
|
+
path = _lock_path(key)
|
|
233
|
+
|
|
234
|
+
if not os.path.exists(path):
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
with open(path, "r") as f:
|
|
239
|
+
data = json.load(f)
|
|
240
|
+
except (json.JSONDecodeError, IOError):
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
data["status"] = "confirmed"
|
|
244
|
+
data["confirmed_at"] = _now_iso()
|
|
245
|
+
if preview:
|
|
246
|
+
data["preview"] = preview[:100]
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
with open(path, "w") as f:
|
|
250
|
+
json.dump(data, f, indent=2)
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def release_thread_lock(to: str, subject: str):
|
|
256
|
+
"""Explicitly remove a thread lock (for testing or correction)."""
|
|
257
|
+
key = _thread_key(to, subject)
|
|
258
|
+
path = _lock_path(key)
|
|
259
|
+
try:
|
|
260
|
+
os.unlink(path)
|
|
261
|
+
except OSError:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def clean_expired_locks():
|
|
266
|
+
"""Purge all lock files older than TTL. Returns count of removed locks."""
|
|
267
|
+
os.makedirs(LOCK_DIR, exist_ok=True)
|
|
268
|
+
removed = 0
|
|
269
|
+
now = time.time()
|
|
270
|
+
for filename in os.listdir(LOCK_DIR):
|
|
271
|
+
if not filename.endswith(".lock"):
|
|
272
|
+
continue
|
|
273
|
+
path = os.path.join(LOCK_DIR, filename)
|
|
274
|
+
try:
|
|
275
|
+
with open(path, "r") as f:
|
|
276
|
+
data = json.load(f)
|
|
277
|
+
lock_ts = data.get("timestamp_epoch", 0)
|
|
278
|
+
if now - lock_ts >= TTL_SECONDS:
|
|
279
|
+
os.unlink(path)
|
|
280
|
+
removed += 1
|
|
281
|
+
except (json.JSONDecodeError, IOError, OSError):
|
|
282
|
+
# Remove corrupt files
|
|
283
|
+
try:
|
|
284
|
+
os.unlink(path)
|
|
285
|
+
removed += 1
|
|
286
|
+
except OSError:
|
|
287
|
+
pass
|
|
288
|
+
return removed
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# --- CLI Interface ---
|
|
292
|
+
|
|
293
|
+
def main():
|
|
294
|
+
import argparse
|
|
295
|
+
|
|
296
|
+
parser = argparse.ArgumentParser(
|
|
297
|
+
description="Email thread-level deduplication. Prevents replying twice to the same thread within 24h."
|
|
298
|
+
)
|
|
299
|
+
subparsers = parser.add_subparsers(dest="command", help="Command")
|
|
300
|
+
|
|
301
|
+
# check
|
|
302
|
+
check_p = subparsers.add_parser("check", help="Check if a thread lock exists")
|
|
303
|
+
check_p.add_argument("to", help="Recipient email address")
|
|
304
|
+
check_p.add_argument("subject", help="Email subject line")
|
|
305
|
+
|
|
306
|
+
# acquire
|
|
307
|
+
acq_p = subparsers.add_parser("acquire", help="Acquire a thread lock before sending")
|
|
308
|
+
acq_p.add_argument("to", help="Recipient email address")
|
|
309
|
+
acq_p.add_argument("subject", help="Email subject line")
|
|
310
|
+
acq_p.add_argument("--session-id", default="", help="Session ID for audit trail")
|
|
311
|
+
acq_p.add_argument("--preview", default="", help="First ~100 chars of email body")
|
|
312
|
+
|
|
313
|
+
# release
|
|
314
|
+
rel_p = subparsers.add_parser("release", help="Explicitly remove a thread lock")
|
|
315
|
+
rel_p.add_argument("to", help="Recipient email address")
|
|
316
|
+
rel_p.add_argument("subject", help="Email subject line")
|
|
317
|
+
|
|
318
|
+
# clean
|
|
319
|
+
subparsers.add_parser("clean", help="Purge expired locks (>24h)")
|
|
320
|
+
|
|
321
|
+
args = parser.parse_args()
|
|
322
|
+
|
|
323
|
+
if args.command == "check":
|
|
324
|
+
result = check_thread_lock(args.to, args.subject)
|
|
325
|
+
if result["locked"]:
|
|
326
|
+
lock = result["lock_data"]
|
|
327
|
+
print(f"LOCKED: Thread already replied to at {lock.get('timestamp', '?')} by session {lock.get('session_id', '?')}")
|
|
328
|
+
print(f" Subject: {lock.get('subject_normalized', '?')}")
|
|
329
|
+
print(f" Recipient: {lock.get('recipient_normalized', '?')}")
|
|
330
|
+
print(f" Preview: {lock.get('preview', '')[:80]}")
|
|
331
|
+
sys.exit(2)
|
|
332
|
+
else:
|
|
333
|
+
print("AVAILABLE: No active thread lock")
|
|
334
|
+
sys.exit(0)
|
|
335
|
+
|
|
336
|
+
elif args.command == "acquire":
|
|
337
|
+
result = acquire_thread_lock(args.to, args.subject, args.session_id, args.preview)
|
|
338
|
+
if result["acquired"]:
|
|
339
|
+
print(f"ACQUIRED: Thread lock created (key={result['thread_key']})")
|
|
340
|
+
sys.exit(0)
|
|
341
|
+
else:
|
|
342
|
+
lock = result["existing_lock"]
|
|
343
|
+
print(f"DEDUP_SKIP: Thread already replied to at {lock.get('timestamp', '?')} by session {lock.get('session_id', '?')}")
|
|
344
|
+
sys.exit(2)
|
|
345
|
+
|
|
346
|
+
elif args.command == "release":
|
|
347
|
+
release_thread_lock(args.to, args.subject)
|
|
348
|
+
print("RELEASED")
|
|
349
|
+
sys.exit(0)
|
|
350
|
+
|
|
351
|
+
elif args.command == "clean":
|
|
352
|
+
removed = clean_expired_locks()
|
|
353
|
+
print(f"CLEANED: Removed {removed} expired lock(s)")
|
|
354
|
+
sys.exit(0)
|
|
355
|
+
|
|
356
|
+
else:
|
|
357
|
+
parser.print_help()
|
|
358
|
+
sys.exit(1)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
main()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Emergency Stop — Immediately halts all Maestro agent operations.
|
|
3
|
+
# Usage: ./scripts/emergency-stop.sh
|
|
4
|
+
#
|
|
5
|
+
# This script is the kill switch for all autonomous operations:
|
|
6
|
+
# 1. Drops .emergency-stop flag (every workflow / cadence consumer / enqueue
|
|
7
|
+
# script honours this on the next tick).
|
|
8
|
+
# 2. Unloads every installed `ai.maestro.<agent>-*` (and legacy
|
|
9
|
+
# `ai.adaptic.<agent>-*`) launchd job.
|
|
10
|
+
# 3. Kills running Claude Code subagent processes.
|
|
11
|
+
#
|
|
12
|
+
# Plist resolution: the agent's first-name slug is read from config/agent.json
|
|
13
|
+
# (SOT) so unload targets the correct labels; falls back to the directory
|
|
14
|
+
# basename if agent.json isn't readable.
|
|
15
|
+
|
|
16
|
+
set -e
|
|
17
|
+
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
AGENT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
20
|
+
LOG_FILE="$AGENT_DIR/logs/emergency-stop.log"
|
|
21
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
22
|
+
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
|
23
|
+
|
|
24
|
+
# Resolve agent first-name slug from SOT (config/agent.json) so the unload
|
|
25
|
+
# loop targets the right launchd labels. Falls back to the basename of the
|
|
26
|
+
# agent directory (stripping -ai suffix).
|
|
27
|
+
AGENT_FIRST=""
|
|
28
|
+
if [ -f "$AGENT_DIR/config/agent.json" ]; then
|
|
29
|
+
if command -v jq >/dev/null 2>&1; then
|
|
30
|
+
AGENT_FIRST=$(jq -r '.firstName // empty' "$AGENT_DIR/config/agent.json" | tr '[:upper:]' '[:lower:]')
|
|
31
|
+
else
|
|
32
|
+
AGENT_FIRST=$(awk -F'"' '/"firstName"[[:space:]]*:/ { print tolower($4); exit }' "$AGENT_DIR/config/agent.json")
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
[ -z "$AGENT_FIRST" ] && AGENT_FIRST=$(basename "$AGENT_DIR" | sed 's/-ai$//')
|
|
36
|
+
|
|
37
|
+
LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
|
|
38
|
+
# Match the current ai.maestro.* namespace AND the legacy ai.adaptic.* one
|
|
39
|
+
# (deployed agents whose plists predate the rename). The loops guard with [ -f ].
|
|
40
|
+
PLIST_GLOB="$LAUNCH_AGENTS_DIR/ai.maestro.${AGENT_FIRST}-*.plist $LAUNCH_AGENTS_DIR/ai.adaptic.${AGENT_FIRST}-*.plist"
|
|
41
|
+
|
|
42
|
+
echo "[$TIMESTAMP] EMERGENCY STOP INITIATED (agent=$AGENT_FIRST)" | tee -a "$LOG_FILE"
|
|
43
|
+
|
|
44
|
+
# 1. Drop the stop flag FIRST so any in-flight work sees it on next tick.
|
|
45
|
+
echo "$TIMESTAMP" > "$AGENT_DIR/.emergency-stop"
|
|
46
|
+
echo "[$TIMESTAMP] Stop flag created" >> "$LOG_FILE"
|
|
47
|
+
|
|
48
|
+
# 2. Unload installed launchd jobs.
|
|
49
|
+
echo "Unloading installed launchd jobs ($PLIST_GLOB)..."
|
|
50
|
+
unloaded=0
|
|
51
|
+
for plist in $PLIST_GLOB; do
|
|
52
|
+
if [ -f "$plist" ]; then
|
|
53
|
+
launchctl unload "$plist" 2>/dev/null || true
|
|
54
|
+
echo "[$TIMESTAMP] Unloaded: $(basename "$plist")" >> "$LOG_FILE"
|
|
55
|
+
unloaded=$((unloaded + 1))
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
echo "[$TIMESTAMP] Unloaded $unloaded launchd job(s)" >> "$LOG_FILE"
|
|
59
|
+
|
|
60
|
+
# 3. Kill running Claude Code subagent processes. Filter to processes that
|
|
61
|
+
# have AGENT_ROOT or this agent's directory in their cwd to avoid
|
|
62
|
+
# killing unrelated claude sessions the operator may have running.
|
|
63
|
+
echo "Stopping Claude Code agent processes..."
|
|
64
|
+
# pgrep -lf is more selective than pkill -f
|
|
65
|
+
pids=$(pgrep -f "claude" 2>/dev/null | tr '\n' ' ' || true)
|
|
66
|
+
if [ -n "$pids" ]; then
|
|
67
|
+
# Send SIGTERM first, give 3s, then SIGKILL stragglers.
|
|
68
|
+
kill -TERM $pids 2>/dev/null || true
|
|
69
|
+
sleep 3
|
|
70
|
+
still=$(pgrep -f "claude" 2>/dev/null | tr '\n' ' ' || true)
|
|
71
|
+
[ -n "$still" ] && kill -KILL $still 2>/dev/null || true
|
|
72
|
+
echo "[$TIMESTAMP] Claude processes terminated ($pids)" >> "$LOG_FILE"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# 4. Log completion.
|
|
76
|
+
echo "[$TIMESTAMP] EMERGENCY STOP COMPLETE — All operations halted" | tee -a "$LOG_FILE"
|
|
77
|
+
echo ""
|
|
78
|
+
echo "To resume operations:"
|
|
79
|
+
echo " ./scripts/resume-operations.sh"
|
|
80
|
+
echo ""
|
|
81
|
+
echo "All actions have been logged to: $LOG_FILE"
|