@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,994 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pre-draft fact retrieval module — Memory Enhancement Phase 4.
|
|
3
|
+
|
|
4
|
+
Pulls relevant entity context BEFORE composing any outbound message.
|
|
5
|
+
Closes the gap where the agent drafts messages without first checking what
|
|
6
|
+
they know about the recipient and related entities.
|
|
7
|
+
|
|
8
|
+
Integration points — call this script before any of these send scripts:
|
|
9
|
+
- scripts/send-email-threaded.py (email composition)
|
|
10
|
+
- scripts/slack-send.sh (Slack messaging)
|
|
11
|
+
- scripts/send-whatsapp.sh (WhatsApp messaging)
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python3 scripts/pre-draft-context.py --recipient "Graham Syder"
|
|
15
|
+
python3 scripts/pre-draft-context.py --recipient "Graham Syder" --topic "fitout"
|
|
16
|
+
python3 scripts/pre-draft-context.py --recipient "sam@example.com" --format brief
|
|
17
|
+
python3 scripts/pre-draft-context.py --recipient "Jordan" --topic "83b election" --format brief
|
|
18
|
+
|
|
19
|
+
Output:
|
|
20
|
+
--format json (default) → Full structured JSON to stdout
|
|
21
|
+
--format brief → 3-5 line summary suitable for prompt injection
|
|
22
|
+
|
|
23
|
+
Performance target: < 500ms (no API calls — file reads only).
|
|
24
|
+
Exit codes: 0 = context found, 1 = recipient not found, 2 = error
|
|
25
|
+
|
|
26
|
+
Created: 2026-04-04 — Memory Enhancement Phase 4
|
|
27
|
+
"""
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import re
|
|
31
|
+
import sys
|
|
32
|
+
import time
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
# Try to import PyYAML at module level (avoids repeated import overhead)
|
|
36
|
+
_yaml_mod = None
|
|
37
|
+
_yaml_loader = None
|
|
38
|
+
try:
|
|
39
|
+
import yaml as _yaml_mod
|
|
40
|
+
try:
|
|
41
|
+
_yaml_loader = _yaml_mod.CSafeLoader # C extension — 10x faster
|
|
42
|
+
except AttributeError:
|
|
43
|
+
_yaml_loader = None
|
|
44
|
+
except ImportError:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
# ─────────────────────────────────────────────
|
|
48
|
+
# Path resolution
|
|
49
|
+
# ─────────────────────────────────────────────
|
|
50
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
51
|
+
REPO_ROOT = SCRIPT_DIR.parent
|
|
52
|
+
|
|
53
|
+
ENTITY_INDEX_PATH = REPO_ROOT / "memory" / "indexes" / "entity-relationships.yaml"
|
|
54
|
+
ENTITY_INDEX_CACHE_PATH = REPO_ROOT / "memory" / "indexes" / ".entity-relationships.json"
|
|
55
|
+
USER_PROFILES_DIR = REPO_ROOT / "memory" / "profiles" / "users"
|
|
56
|
+
QUEUES_DIR = REPO_ROOT / "state" / "queues"
|
|
57
|
+
INTERACTIONS_DIR = REPO_ROOT / "memory" / "interactions"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ─────────────────────────────────────────────
|
|
61
|
+
# YAML parser — reused from validate-outbound.py
|
|
62
|
+
# ─────────────────────────────────────────────
|
|
63
|
+
def _parse_yaml(text):
|
|
64
|
+
"""Parse YAML text into nested Python objects.
|
|
65
|
+
|
|
66
|
+
Uses PyYAML with C loader (fastest) if available, then safe_load,
|
|
67
|
+
falls back to built-in recursive parser.
|
|
68
|
+
"""
|
|
69
|
+
if _yaml_mod is not None:
|
|
70
|
+
if _yaml_loader is not None:
|
|
71
|
+
return _yaml_mod.load(text, Loader=_yaml_loader) or {}
|
|
72
|
+
else:
|
|
73
|
+
return _yaml_mod.safe_load(text) or {}
|
|
74
|
+
lines = text.split('\n')
|
|
75
|
+
return _parse_yaml_block(lines, 0, 0)[0]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _parse_yaml_block(lines, start, base_indent):
|
|
79
|
+
"""Recursively parse a YAML block."""
|
|
80
|
+
result = {}
|
|
81
|
+
i = start
|
|
82
|
+
while i < len(lines):
|
|
83
|
+
raw = lines[i]
|
|
84
|
+
stripped = raw.lstrip()
|
|
85
|
+
if not stripped or stripped.startswith('#'):
|
|
86
|
+
i += 1
|
|
87
|
+
continue
|
|
88
|
+
indent = len(raw) - len(raw.lstrip())
|
|
89
|
+
if indent < base_indent and i > start:
|
|
90
|
+
break
|
|
91
|
+
if indent > base_indent and i > start:
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
if stripped.startswith('- '):
|
|
95
|
+
lst, i = _parse_yaml_list(lines, i, indent)
|
|
96
|
+
if not result:
|
|
97
|
+
return lst, i
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
if ':' in stripped and not stripped.startswith('-'):
|
|
101
|
+
key, _, rest = stripped.partition(':')
|
|
102
|
+
key = key.strip()
|
|
103
|
+
rest = rest.strip()
|
|
104
|
+
if rest:
|
|
105
|
+
result[key] = _yaml_scalar(rest)
|
|
106
|
+
i += 1
|
|
107
|
+
else:
|
|
108
|
+
child_indent, child_idx = _next_content_line(lines, i + 1)
|
|
109
|
+
if child_indent is not None and child_indent > indent:
|
|
110
|
+
child_stripped = lines[child_idx].lstrip()
|
|
111
|
+
if child_stripped.startswith('- '):
|
|
112
|
+
value, i = _parse_yaml_list(lines, child_idx, child_indent)
|
|
113
|
+
else:
|
|
114
|
+
value, i = _parse_yaml_block(lines, child_idx, child_indent)
|
|
115
|
+
result[key] = value
|
|
116
|
+
else:
|
|
117
|
+
result[key] = None
|
|
118
|
+
i += 1
|
|
119
|
+
else:
|
|
120
|
+
i += 1
|
|
121
|
+
return result, i
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _parse_yaml_list(lines, start, base_indent):
|
|
125
|
+
"""Parse a YAML list."""
|
|
126
|
+
result = []
|
|
127
|
+
i = start
|
|
128
|
+
while i < len(lines):
|
|
129
|
+
raw = lines[i]
|
|
130
|
+
stripped = raw.lstrip()
|
|
131
|
+
if not stripped or stripped.startswith('#'):
|
|
132
|
+
i += 1
|
|
133
|
+
continue
|
|
134
|
+
indent = len(raw) - len(raw.lstrip())
|
|
135
|
+
if indent < base_indent:
|
|
136
|
+
break
|
|
137
|
+
if indent > base_indent:
|
|
138
|
+
break
|
|
139
|
+
if not stripped.startswith('- '):
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
item_content = stripped[2:].strip()
|
|
143
|
+
if ':' in item_content and not item_content.startswith('"') and not item_content.startswith("'"):
|
|
144
|
+
k, _, v = item_content.partition(':')
|
|
145
|
+
k = k.strip()
|
|
146
|
+
v = v.strip()
|
|
147
|
+
item_dict = {k: _yaml_scalar(v) if v else None}
|
|
148
|
+
child_indent, child_idx = _next_content_line(lines, i + 1)
|
|
149
|
+
if child_indent is not None and child_indent > indent:
|
|
150
|
+
extra, i = _parse_yaml_block(lines, child_idx, child_indent)
|
|
151
|
+
if isinstance(extra, dict):
|
|
152
|
+
item_dict.update(extra)
|
|
153
|
+
else:
|
|
154
|
+
i += 1
|
|
155
|
+
result.append(item_dict)
|
|
156
|
+
else:
|
|
157
|
+
result.append(_yaml_scalar(item_content))
|
|
158
|
+
i += 1
|
|
159
|
+
return result, i
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _next_content_line(lines, start):
|
|
163
|
+
"""Find next non-blank, non-comment line."""
|
|
164
|
+
i = start
|
|
165
|
+
while i < len(lines):
|
|
166
|
+
stripped = lines[i].lstrip()
|
|
167
|
+
if stripped and not stripped.startswith('#'):
|
|
168
|
+
indent = len(lines[i]) - len(lines[i].lstrip())
|
|
169
|
+
return indent, i
|
|
170
|
+
i += 1
|
|
171
|
+
return None, None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _yaml_scalar(text):
|
|
175
|
+
"""Convert a YAML scalar string to Python type."""
|
|
176
|
+
if not text:
|
|
177
|
+
return None
|
|
178
|
+
if (text.startswith('"') and text.endswith('"')) or \
|
|
179
|
+
(text.startswith("'") and text.endswith("'")):
|
|
180
|
+
return text[1:-1]
|
|
181
|
+
if ' #' in text:
|
|
182
|
+
text = text[:text.index(' #')].strip()
|
|
183
|
+
if text.lower() in ('true', 'yes'):
|
|
184
|
+
return True
|
|
185
|
+
if text.lower() in ('false', 'no'):
|
|
186
|
+
return False
|
|
187
|
+
if text.lower() in ('null', '~', ''):
|
|
188
|
+
return None
|
|
189
|
+
try:
|
|
190
|
+
return int(text)
|
|
191
|
+
except ValueError:
|
|
192
|
+
pass
|
|
193
|
+
try:
|
|
194
|
+
return float(text)
|
|
195
|
+
except ValueError:
|
|
196
|
+
pass
|
|
197
|
+
return text.strip('"').strip("'")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ─────────────────────────────────────────────
|
|
201
|
+
# Entity index loading (mirrors validate-outbound.py)
|
|
202
|
+
# ─────────────────────────────────────────────
|
|
203
|
+
_entity_index_cache = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _load_entity_data():
|
|
207
|
+
"""Load raw entity data from YAML, using JSON cache when fresh.
|
|
208
|
+
|
|
209
|
+
The JSON cache is regenerated when the YAML file is newer.
|
|
210
|
+
Returns parsed dict with 'entities' and 'relationships' keys.
|
|
211
|
+
"""
|
|
212
|
+
if not ENTITY_INDEX_PATH.exists():
|
|
213
|
+
return {'entities': [], 'relationships': []}
|
|
214
|
+
|
|
215
|
+
# Check if JSON cache is fresh
|
|
216
|
+
if ENTITY_INDEX_CACHE_PATH.exists():
|
|
217
|
+
yaml_mtime = ENTITY_INDEX_PATH.stat().st_mtime
|
|
218
|
+
json_mtime = ENTITY_INDEX_CACHE_PATH.stat().st_mtime
|
|
219
|
+
if json_mtime >= yaml_mtime:
|
|
220
|
+
try:
|
|
221
|
+
return json.loads(ENTITY_INDEX_CACHE_PATH.read_text(encoding='utf-8'))
|
|
222
|
+
except Exception:
|
|
223
|
+
pass # Fall through to YAML parsing
|
|
224
|
+
|
|
225
|
+
# Parse YAML and write JSON cache
|
|
226
|
+
text = ENTITY_INDEX_PATH.read_text(encoding='utf-8')
|
|
227
|
+
data = _parse_yaml(text)
|
|
228
|
+
if not isinstance(data, dict):
|
|
229
|
+
return {'entities': [], 'relationships': []}
|
|
230
|
+
|
|
231
|
+
# Write cache (best-effort, non-blocking)
|
|
232
|
+
try:
|
|
233
|
+
ENTITY_INDEX_CACHE_PATH.write_text(
|
|
234
|
+
json.dumps(data, default=str, ensure_ascii=False),
|
|
235
|
+
encoding='utf-8'
|
|
236
|
+
)
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
return data
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def load_entity_index():
|
|
244
|
+
"""Load and index entity-relationships.yaml.
|
|
245
|
+
|
|
246
|
+
Returns dict with:
|
|
247
|
+
- entities: list of entity dicts
|
|
248
|
+
- relationships: list of relationship dicts
|
|
249
|
+
- by_name: dict mapping lowercase name/alias -> entity dict
|
|
250
|
+
- by_email: dict mapping lowercase email -> entity dict
|
|
251
|
+
- rels_by_subject: dict mapping entity id -> list of rels
|
|
252
|
+
- rels_by_object: dict mapping entity id -> list of rels
|
|
253
|
+
"""
|
|
254
|
+
global _entity_index_cache
|
|
255
|
+
if _entity_index_cache is not None:
|
|
256
|
+
return _entity_index_cache
|
|
257
|
+
|
|
258
|
+
empty = {
|
|
259
|
+
'entities': [], 'relationships': [],
|
|
260
|
+
'by_name': {}, 'by_email': {},
|
|
261
|
+
'rels_by_subject': {}, 'rels_by_object': {},
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if not ENTITY_INDEX_PATH.exists():
|
|
265
|
+
_entity_index_cache = empty
|
|
266
|
+
return _entity_index_cache
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
data = _load_entity_data()
|
|
270
|
+
if not isinstance(data, dict):
|
|
271
|
+
_entity_index_cache = empty
|
|
272
|
+
return _entity_index_cache
|
|
273
|
+
|
|
274
|
+
entities = data.get('entities', []) or []
|
|
275
|
+
relationships = data.get('relationships', []) or []
|
|
276
|
+
|
|
277
|
+
by_name = {}
|
|
278
|
+
by_email = {}
|
|
279
|
+
for entity in entities:
|
|
280
|
+
if not isinstance(entity, dict):
|
|
281
|
+
continue
|
|
282
|
+
name = entity.get('name', '')
|
|
283
|
+
if name and isinstance(name, str):
|
|
284
|
+
by_name[name.lower()] = entity
|
|
285
|
+
parts = name.split()
|
|
286
|
+
if len(parts) >= 2:
|
|
287
|
+
short = f"{parts[0]} {parts[-1]}"
|
|
288
|
+
by_name[short.lower()] = entity
|
|
289
|
+
if len(parts) >= 1:
|
|
290
|
+
first = parts[0].lower()
|
|
291
|
+
if first not in by_name:
|
|
292
|
+
by_name[first] = entity
|
|
293
|
+
|
|
294
|
+
aliases = entity.get('aliases', []) or []
|
|
295
|
+
if isinstance(aliases, list):
|
|
296
|
+
for alias in aliases:
|
|
297
|
+
if alias and isinstance(alias, str):
|
|
298
|
+
by_name[alias.lower()] = entity
|
|
299
|
+
|
|
300
|
+
email = entity.get('email', '')
|
|
301
|
+
if email and isinstance(email, str):
|
|
302
|
+
by_email[email.lower()] = entity
|
|
303
|
+
|
|
304
|
+
rels_by_subject = {}
|
|
305
|
+
rels_by_object = {}
|
|
306
|
+
for rel in relationships:
|
|
307
|
+
if not isinstance(rel, dict):
|
|
308
|
+
continue
|
|
309
|
+
subj = rel.get('subject', '')
|
|
310
|
+
obj = rel.get('object', '')
|
|
311
|
+
if subj:
|
|
312
|
+
rels_by_subject.setdefault(subj, []).append(rel)
|
|
313
|
+
if obj:
|
|
314
|
+
rels_by_object.setdefault(obj, []).append(rel)
|
|
315
|
+
|
|
316
|
+
_entity_index_cache = {
|
|
317
|
+
'entities': entities,
|
|
318
|
+
'relationships': relationships,
|
|
319
|
+
'by_name': by_name,
|
|
320
|
+
'by_email': by_email,
|
|
321
|
+
'rels_by_subject': rels_by_subject,
|
|
322
|
+
'rels_by_object': rels_by_object,
|
|
323
|
+
}
|
|
324
|
+
except Exception:
|
|
325
|
+
_entity_index_cache = empty
|
|
326
|
+
|
|
327
|
+
return _entity_index_cache
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def lookup_entity(name_or_email):
|
|
331
|
+
"""Find an entity by name, alias, or email.
|
|
332
|
+
|
|
333
|
+
Returns the entity dict or None.
|
|
334
|
+
"""
|
|
335
|
+
idx = load_entity_index()
|
|
336
|
+
key = name_or_email.lower().strip()
|
|
337
|
+
if not key:
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
# Try email first
|
|
341
|
+
entity = idx['by_email'].get(key)
|
|
342
|
+
if entity:
|
|
343
|
+
return entity
|
|
344
|
+
|
|
345
|
+
# Try name/alias
|
|
346
|
+
entity = idx['by_name'].get(key)
|
|
347
|
+
if entity:
|
|
348
|
+
return entity
|
|
349
|
+
|
|
350
|
+
# Try first+last from multi-word input
|
|
351
|
+
parts = key.split()
|
|
352
|
+
if len(parts) >= 2:
|
|
353
|
+
short = f"{parts[0]} {parts[-1]}"
|
|
354
|
+
entity = idx['by_name'].get(short)
|
|
355
|
+
if entity:
|
|
356
|
+
return entity
|
|
357
|
+
|
|
358
|
+
# Single name
|
|
359
|
+
if len(parts) == 1:
|
|
360
|
+
entity = idx['by_name'].get(parts[0])
|
|
361
|
+
if entity:
|
|
362
|
+
return entity
|
|
363
|
+
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ─────────────────────────────────────────────
|
|
368
|
+
# Relationship classification
|
|
369
|
+
# ─────────────────────────────────────────────
|
|
370
|
+
ADAPTIC_IDS = {
|
|
371
|
+
'company:northwind-group', 'company:northwind-holdings',
|
|
372
|
+
'company:northwind-am-difc', 'company:northwind-technologies',
|
|
373
|
+
'company:northwind-group-investments', 'company:northwind-investments',
|
|
374
|
+
'company:northwind-inc', 'company:northwind-ai-uk',
|
|
375
|
+
'company:northwind-am-uk', 'company:northwind-am-cayman',
|
|
376
|
+
'company:northwind-am-asia', 'company:northwind-asia',
|
|
377
|
+
'company:northwind-am-eu', 'company:northwind-am-us',
|
|
378
|
+
'company:northwind-am-ksa', 'company:northwind-ksa',
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def classify_relationship(entity_id):
|
|
383
|
+
"""Classify a person or company's relationship to Northwind.
|
|
384
|
+
|
|
385
|
+
Returns dict: relationship_type, is_internal, relationships_to_northwind.
|
|
386
|
+
"""
|
|
387
|
+
idx = load_entity_index()
|
|
388
|
+
rels = idx['rels_by_subject'].get(entity_id, [])
|
|
389
|
+
|
|
390
|
+
company_rels = []
|
|
391
|
+
is_internal = False
|
|
392
|
+
rel_type = 'unknown'
|
|
393
|
+
|
|
394
|
+
for rel in rels:
|
|
395
|
+
obj = rel.get('object', '')
|
|
396
|
+
pred = rel.get('predicate', '')
|
|
397
|
+
if obj in ADAPTIC_IDS or obj.startswith('company:northwind'):
|
|
398
|
+
company_rels.append(rel)
|
|
399
|
+
if pred in ('employed_by', 'directs'):
|
|
400
|
+
is_internal = True
|
|
401
|
+
rel_type = 'internal'
|
|
402
|
+
elif pred == 'vendor_candidate_for' and rel_type != 'internal':
|
|
403
|
+
rel_type = 'vendor_candidate'
|
|
404
|
+
elif pred == 'vendor_for' and rel_type not in ('internal', 'vendor_candidate'):
|
|
405
|
+
rel_type = 'external_vendor'
|
|
406
|
+
elif pred == 'advises' and rel_type not in ('internal', 'vendor_candidate', 'external_vendor'):
|
|
407
|
+
rel_type = 'external_advisor'
|
|
408
|
+
elif pred == 'invests_in' and rel_type not in ('internal',):
|
|
409
|
+
rel_type = 'investor'
|
|
410
|
+
elif pred == 'regulates' and rel_type not in ('internal',):
|
|
411
|
+
rel_type = 'regulator'
|
|
412
|
+
|
|
413
|
+
if rel_type == 'unknown':
|
|
414
|
+
for rel in rels:
|
|
415
|
+
if rel.get('predicate') == 'employed_by':
|
|
416
|
+
rel_type = 'external'
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
'relationship_type': rel_type,
|
|
421
|
+
'is_internal': is_internal,
|
|
422
|
+
'relationships_to_northwind': company_rels,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
# ─────────────────────────────────────────────
|
|
427
|
+
# User profile loading
|
|
428
|
+
# ─────────────────────────────────────────────
|
|
429
|
+
def load_user_profile(entity):
|
|
430
|
+
"""Load the full user profile YAML for an entity.
|
|
431
|
+
|
|
432
|
+
Returns parsed dict or None.
|
|
433
|
+
"""
|
|
434
|
+
profile_path = entity.get('profile_path', '')
|
|
435
|
+
if profile_path:
|
|
436
|
+
full_path = REPO_ROOT / profile_path
|
|
437
|
+
if full_path.exists():
|
|
438
|
+
try:
|
|
439
|
+
text = full_path.read_text(encoding='utf-8')
|
|
440
|
+
return _parse_yaml(text)
|
|
441
|
+
except Exception:
|
|
442
|
+
pass
|
|
443
|
+
|
|
444
|
+
# Fallback: try to derive slug from entity name
|
|
445
|
+
name = entity.get('name', '')
|
|
446
|
+
if name:
|
|
447
|
+
slug = name.lower().replace(' ', '-')
|
|
448
|
+
# Remove middle names for slug
|
|
449
|
+
parts = name.lower().split()
|
|
450
|
+
if len(parts) >= 2:
|
|
451
|
+
slug = f"{parts[0]}-{parts[-1]}"
|
|
452
|
+
candidate = USER_PROFILES_DIR / f"{slug}.yaml"
|
|
453
|
+
if candidate.exists():
|
|
454
|
+
try:
|
|
455
|
+
text = candidate.read_text(encoding='utf-8')
|
|
456
|
+
return _parse_yaml(text)
|
|
457
|
+
except Exception:
|
|
458
|
+
pass
|
|
459
|
+
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
# ─────────────────────────────────────────────
|
|
464
|
+
# Queue scanning — find active items mentioning a person
|
|
465
|
+
# ─────────────────────────────────────────────
|
|
466
|
+
def scan_queues_for_person(name, entity_id=None, slug=None):
|
|
467
|
+
"""Scan all queue YAML files for items mentioning this person.
|
|
468
|
+
|
|
469
|
+
Uses fast text search to skip files that don't mention the person,
|
|
470
|
+
then parses only matching item blocks to extract structured data.
|
|
471
|
+
|
|
472
|
+
Returns list of dicts with: queue, item_id, title, status, priority.
|
|
473
|
+
"""
|
|
474
|
+
if not QUEUES_DIR.exists():
|
|
475
|
+
return []
|
|
476
|
+
|
|
477
|
+
# Build search terms for initial text-based filtering
|
|
478
|
+
search_terms = set()
|
|
479
|
+
search_terms.add(name.lower())
|
|
480
|
+
if entity_id:
|
|
481
|
+
parts = entity_id.split(':')
|
|
482
|
+
if len(parts) == 2:
|
|
483
|
+
search_terms.add(parts[1].lower())
|
|
484
|
+
if slug:
|
|
485
|
+
search_terms.add(slug.lower())
|
|
486
|
+
|
|
487
|
+
results = []
|
|
488
|
+
for qfile in sorted(QUEUES_DIR.glob('*.yaml')):
|
|
489
|
+
try:
|
|
490
|
+
text = qfile.read_text(encoding='utf-8')
|
|
491
|
+
text_lower = text.lower()
|
|
492
|
+
|
|
493
|
+
# Fast check: skip entire file if no search term appears
|
|
494
|
+
if not any(term in text_lower for term in search_terms):
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
# Extract queue_name from early lines (avoid full parse)
|
|
498
|
+
queue_name = qfile.stem
|
|
499
|
+
for line in text.split('\n')[:5]:
|
|
500
|
+
if line.startswith('queue_name:'):
|
|
501
|
+
queue_name = line.split(':', 1)[1].strip().strip('"').strip("'")
|
|
502
|
+
break
|
|
503
|
+
|
|
504
|
+
# Parse item blocks using text-based extraction
|
|
505
|
+
# Split on ' - id:' which marks each queue item
|
|
506
|
+
item_blocks = re.split(r'\n - id:', text)
|
|
507
|
+
if len(item_blocks) < 2:
|
|
508
|
+
# Try alternate format
|
|
509
|
+
item_blocks = re.split(r'\n -\s+id:', text)
|
|
510
|
+
|
|
511
|
+
for i, block in enumerate(item_blocks):
|
|
512
|
+
if i == 0:
|
|
513
|
+
continue # Skip preamble before first item
|
|
514
|
+
|
|
515
|
+
block_lower = block.lower()
|
|
516
|
+
|
|
517
|
+
# Check if this block mentions the person
|
|
518
|
+
if not any(term in block_lower for term in search_terms):
|
|
519
|
+
continue
|
|
520
|
+
|
|
521
|
+
# Extract fields with simple regex
|
|
522
|
+
item_id = ''
|
|
523
|
+
id_match = re.match(r'\s*(\S+)', block)
|
|
524
|
+
if id_match:
|
|
525
|
+
item_id = id_match.group(1).strip()
|
|
526
|
+
|
|
527
|
+
def _extract_field(field_name, text_block):
|
|
528
|
+
pattern = rf'^\s*{field_name}:\s*(.+?)$'
|
|
529
|
+
m = re.search(pattern, text_block, re.MULTILINE)
|
|
530
|
+
if m:
|
|
531
|
+
val = m.group(1).strip().strip('"').strip("'")
|
|
532
|
+
return val
|
|
533
|
+
return ''
|
|
534
|
+
|
|
535
|
+
status = _extract_field('status', block)
|
|
536
|
+
# Only include active items
|
|
537
|
+
if status.lower() in ('closed', 'resolved', 'cancelled'):
|
|
538
|
+
continue
|
|
539
|
+
|
|
540
|
+
title = _extract_field('title', block)
|
|
541
|
+
priority = _extract_field('priority', block)
|
|
542
|
+
due = _extract_field('due', block)
|
|
543
|
+
|
|
544
|
+
results.append({
|
|
545
|
+
'queue': queue_name,
|
|
546
|
+
'item_id': item_id,
|
|
547
|
+
'title': title,
|
|
548
|
+
'status': status,
|
|
549
|
+
'priority': priority,
|
|
550
|
+
'due': due if due else None,
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
except Exception:
|
|
554
|
+
continue
|
|
555
|
+
|
|
556
|
+
return results
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
# ─────────────────────────────────────────────
|
|
560
|
+
# Interaction history — recent communications
|
|
561
|
+
# ─────────────────────────────────────────────
|
|
562
|
+
def get_recent_interactions(entity, max_items=10):
|
|
563
|
+
"""Load recent interaction history for the entity.
|
|
564
|
+
|
|
565
|
+
Checks memory/interactions/{email,slack,meetings}/ for the entity's slug.
|
|
566
|
+
Returns list of interaction summaries (most recent first).
|
|
567
|
+
"""
|
|
568
|
+
name = entity.get('name', '')
|
|
569
|
+
parts = name.lower().split()
|
|
570
|
+
if len(parts) >= 2:
|
|
571
|
+
slug = f"{parts[0]}-{parts[-1]}"
|
|
572
|
+
elif parts:
|
|
573
|
+
slug = parts[0]
|
|
574
|
+
else:
|
|
575
|
+
return []
|
|
576
|
+
|
|
577
|
+
interactions = []
|
|
578
|
+
|
|
579
|
+
def _parse_interaction(entry, channel):
|
|
580
|
+
"""Normalize an interaction entry from any JSONL schema variant."""
|
|
581
|
+
# Handle both timestamp formats: 'timestamp' and 'date'
|
|
582
|
+
ts = entry.get('timestamp', '') or entry.get('date', '') or ''
|
|
583
|
+
# Handle both direction formats: 'direction' and 'type'
|
|
584
|
+
direction = entry.get('direction', '') or entry.get('type', '') or ''
|
|
585
|
+
# Handle summary: 'summary', 'content', or fall back to 'action_taken'
|
|
586
|
+
summary = entry.get('summary', '') or entry.get('content', '') or entry.get('action_taken', '') or ''
|
|
587
|
+
return {
|
|
588
|
+
'channel': channel,
|
|
589
|
+
'timestamp': ts,
|
|
590
|
+
'direction': direction,
|
|
591
|
+
'subject': entry.get('subject', ''),
|
|
592
|
+
'summary': summary,
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
# Check email interactions
|
|
596
|
+
email_dir = INTERACTIONS_DIR / "email" / slug
|
|
597
|
+
if email_dir.exists():
|
|
598
|
+
for f in sorted(email_dir.glob('*.jsonl'), reverse=True):
|
|
599
|
+
try:
|
|
600
|
+
for line in f.read_text(encoding='utf-8').strip().split('\n'):
|
|
601
|
+
line = line.strip()
|
|
602
|
+
if not line:
|
|
603
|
+
continue
|
|
604
|
+
try:
|
|
605
|
+
entry = json.loads(line)
|
|
606
|
+
interactions.append(_parse_interaction(entry, 'email'))
|
|
607
|
+
except json.JSONDecodeError:
|
|
608
|
+
continue
|
|
609
|
+
except Exception:
|
|
610
|
+
continue
|
|
611
|
+
|
|
612
|
+
# Check Slack DM interactions
|
|
613
|
+
slack_dm_dir = INTERACTIONS_DIR / "slack" / "dm" / slug if INTERACTIONS_DIR.exists() else None
|
|
614
|
+
if slack_dm_dir and slack_dm_dir.exists():
|
|
615
|
+
for f in sorted(slack_dm_dir.glob('*.jsonl'), reverse=True):
|
|
616
|
+
try:
|
|
617
|
+
for line in f.read_text(encoding='utf-8').strip().split('\n'):
|
|
618
|
+
line = line.strip()
|
|
619
|
+
if not line:
|
|
620
|
+
continue
|
|
621
|
+
try:
|
|
622
|
+
entry = json.loads(line)
|
|
623
|
+
interactions.append(_parse_interaction(entry, 'slack_dm'))
|
|
624
|
+
except json.JSONDecodeError:
|
|
625
|
+
continue
|
|
626
|
+
except Exception:
|
|
627
|
+
continue
|
|
628
|
+
|
|
629
|
+
# Check meeting interactions
|
|
630
|
+
meetings_dir = INTERACTIONS_DIR / "meetings" / slug if INTERACTIONS_DIR.exists() else None
|
|
631
|
+
if meetings_dir and meetings_dir.exists():
|
|
632
|
+
for f in sorted(meetings_dir.glob('*.jsonl'), reverse=True):
|
|
633
|
+
try:
|
|
634
|
+
for line in f.read_text(encoding='utf-8').strip().split('\n'):
|
|
635
|
+
line = line.strip()
|
|
636
|
+
if not line:
|
|
637
|
+
continue
|
|
638
|
+
try:
|
|
639
|
+
entry = json.loads(line)
|
|
640
|
+
interactions.append(_parse_interaction(entry, 'meeting'))
|
|
641
|
+
except json.JSONDecodeError:
|
|
642
|
+
continue
|
|
643
|
+
except Exception:
|
|
644
|
+
continue
|
|
645
|
+
|
|
646
|
+
# Sort by timestamp descending
|
|
647
|
+
interactions.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
|
|
648
|
+
return interactions[:max_items]
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
# ─────────────────────────────────────────────
|
|
652
|
+
# Related entities
|
|
653
|
+
# ─────────────────────────────────────────────
|
|
654
|
+
def get_related_entities(entity_id):
|
|
655
|
+
"""Find all entities related to this one via the relationship graph.
|
|
656
|
+
|
|
657
|
+
Returns list of dicts: related_entity_id, related_entity_name,
|
|
658
|
+
predicate, direction ('outgoing'/'incoming'), notes.
|
|
659
|
+
"""
|
|
660
|
+
idx = load_entity_index()
|
|
661
|
+
related = []
|
|
662
|
+
|
|
663
|
+
# Entities we look up by id
|
|
664
|
+
entity_by_id = {}
|
|
665
|
+
for e in idx['entities']:
|
|
666
|
+
if isinstance(e, dict) and e.get('id'):
|
|
667
|
+
entity_by_id[e['id']] = e
|
|
668
|
+
|
|
669
|
+
# Outgoing relationships (this entity is subject)
|
|
670
|
+
for rel in idx['rels_by_subject'].get(entity_id, []):
|
|
671
|
+
obj_id = rel.get('object', '')
|
|
672
|
+
obj_entity = entity_by_id.get(obj_id, {})
|
|
673
|
+
related.append({
|
|
674
|
+
'entity_id': obj_id,
|
|
675
|
+
'entity_name': obj_entity.get('name', obj_id),
|
|
676
|
+
'predicate': rel.get('predicate', ''),
|
|
677
|
+
'direction': 'outgoing',
|
|
678
|
+
'scope': rel.get('scope', ''),
|
|
679
|
+
'status': rel.get('status', ''),
|
|
680
|
+
'notes': rel.get('notes', ''),
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
# Incoming relationships (this entity is object)
|
|
684
|
+
for rel in idx['rels_by_object'].get(entity_id, []):
|
|
685
|
+
subj_id = rel.get('subject', '')
|
|
686
|
+
subj_entity = entity_by_id.get(subj_id, {})
|
|
687
|
+
related.append({
|
|
688
|
+
'entity_id': subj_id,
|
|
689
|
+
'entity_name': subj_entity.get('name', subj_id),
|
|
690
|
+
'predicate': rel.get('predicate', ''),
|
|
691
|
+
'direction': 'incoming',
|
|
692
|
+
'scope': rel.get('scope', ''),
|
|
693
|
+
'status': rel.get('status', ''),
|
|
694
|
+
'notes': rel.get('notes', ''),
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
return related
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
# ─────────────────────────────────────────────
|
|
701
|
+
# Topic filtering
|
|
702
|
+
# ─────────────────────────────────────────────
|
|
703
|
+
def filter_by_topic(context, topic):
|
|
704
|
+
"""Boost relevance of context items matching the given topic.
|
|
705
|
+
|
|
706
|
+
Adds a 'topic_relevant' flag to queue items and interactions
|
|
707
|
+
that mention the topic.
|
|
708
|
+
"""
|
|
709
|
+
if not topic:
|
|
710
|
+
return context
|
|
711
|
+
|
|
712
|
+
topic_lower = topic.lower()
|
|
713
|
+
topic_words = topic_lower.split()
|
|
714
|
+
|
|
715
|
+
# Tag queue items
|
|
716
|
+
for item in context.get('active_queue_items', []):
|
|
717
|
+
searchable = f"{item.get('title', '')} {item.get('queue', '')}".lower()
|
|
718
|
+
item['topic_relevant'] = any(w in searchable for w in topic_words)
|
|
719
|
+
|
|
720
|
+
# Tag interactions
|
|
721
|
+
for item in context.get('recent_interactions', []):
|
|
722
|
+
searchable = f"{item.get('subject', '')} {item.get('summary', '')}".lower()
|
|
723
|
+
item['topic_relevant'] = any(w in searchable for w in topic_words)
|
|
724
|
+
|
|
725
|
+
# Tag related entities
|
|
726
|
+
for item in context.get('related_entities', []):
|
|
727
|
+
searchable = f"{item.get('entity_name', '')} {item.get('scope', '')} {item.get('notes', '')}".lower()
|
|
728
|
+
item['topic_relevant'] = any(w in searchable for w in topic_words)
|
|
729
|
+
|
|
730
|
+
return context
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# ─────────────────────────────────────────────
|
|
734
|
+
# Main retrieval function
|
|
735
|
+
# ─────────────────────────────────────────────
|
|
736
|
+
def retrieve_context(recipient, topic=None):
|
|
737
|
+
"""Retrieve full pre-draft context for a recipient.
|
|
738
|
+
|
|
739
|
+
Args:
|
|
740
|
+
recipient: Name or email of the person to look up.
|
|
741
|
+
topic: Optional topic to filter/boost relevant items.
|
|
742
|
+
|
|
743
|
+
Returns:
|
|
744
|
+
dict with full context, or None if recipient not found.
|
|
745
|
+
"""
|
|
746
|
+
t0 = time.time()
|
|
747
|
+
|
|
748
|
+
# 1. Find entity in index
|
|
749
|
+
entity = lookup_entity(recipient)
|
|
750
|
+
if entity is None:
|
|
751
|
+
return None
|
|
752
|
+
|
|
753
|
+
entity_id = entity.get('id', '')
|
|
754
|
+
entity_name = entity.get('name', '')
|
|
755
|
+
|
|
756
|
+
# 2. Classify relationship
|
|
757
|
+
rel_info = classify_relationship(entity_id)
|
|
758
|
+
|
|
759
|
+
# 3. Load user profile
|
|
760
|
+
profile = load_user_profile(entity)
|
|
761
|
+
|
|
762
|
+
# 4. Extract key profile fields
|
|
763
|
+
standing_instructions = []
|
|
764
|
+
communication_prefs = {}
|
|
765
|
+
last_interaction = None
|
|
766
|
+
interaction_summary_notes = ''
|
|
767
|
+
|
|
768
|
+
if profile:
|
|
769
|
+
si = profile.get('standing_instructions', [])
|
|
770
|
+
if isinstance(si, list):
|
|
771
|
+
standing_instructions = si
|
|
772
|
+
elif isinstance(si, str) and si.strip() in ('[]', ''):
|
|
773
|
+
standing_instructions = []
|
|
774
|
+
else:
|
|
775
|
+
standing_instructions = [si] if si else []
|
|
776
|
+
communication_prefs = profile.get('communication_preferences', {}) or {}
|
|
777
|
+
isummary = profile.get('interaction_summary', {}) or {}
|
|
778
|
+
last_interaction = isummary.get('last_interaction')
|
|
779
|
+
interaction_summary_notes = str(isummary.get('notes', '') or '').strip()
|
|
780
|
+
|
|
781
|
+
# 5. Scan queues for active items about this person
|
|
782
|
+
slug = None
|
|
783
|
+
if entity_id and ':' in entity_id:
|
|
784
|
+
slug = entity_id.split(':')[1]
|
|
785
|
+
queue_items = scan_queues_for_person(entity_name, entity_id, slug)
|
|
786
|
+
|
|
787
|
+
# 6. Get recent interaction history
|
|
788
|
+
interactions = get_recent_interactions(entity)
|
|
789
|
+
|
|
790
|
+
# 7. Get related entities
|
|
791
|
+
related = get_related_entities(entity_id)
|
|
792
|
+
|
|
793
|
+
# 8. Build context object
|
|
794
|
+
context = {
|
|
795
|
+
'recipient': {
|
|
796
|
+
'name': entity_name,
|
|
797
|
+
'entity_id': entity_id,
|
|
798
|
+
'type': entity.get('type', ''),
|
|
799
|
+
'role': entity.get('primary_role', ''),
|
|
800
|
+
'company': entity.get('primary_company', ''),
|
|
801
|
+
'email': entity.get('email', ''),
|
|
802
|
+
'timezone': entity.get('timezone', ''),
|
|
803
|
+
'notes': entity.get('notes', ''),
|
|
804
|
+
},
|
|
805
|
+
'relationship': {
|
|
806
|
+
'type': rel_info['relationship_type'],
|
|
807
|
+
'is_internal': rel_info['is_internal'],
|
|
808
|
+
'company_relationships': [
|
|
809
|
+
{
|
|
810
|
+
'predicate': r.get('predicate', ''),
|
|
811
|
+
'object': r.get('object', ''),
|
|
812
|
+
'status': r.get('status', ''),
|
|
813
|
+
'scope': r.get('scope', ''),
|
|
814
|
+
'notes': r.get('notes', ''),
|
|
815
|
+
}
|
|
816
|
+
for r in rel_info['relationships_to_northwind']
|
|
817
|
+
],
|
|
818
|
+
},
|
|
819
|
+
'communication': {
|
|
820
|
+
'preferences': communication_prefs,
|
|
821
|
+
'standing_instructions': standing_instructions,
|
|
822
|
+
'last_interaction': str(last_interaction) if last_interaction else None,
|
|
823
|
+
'interaction_notes': interaction_summary_notes,
|
|
824
|
+
},
|
|
825
|
+
'related_entities': related,
|
|
826
|
+
'active_queue_items': queue_items,
|
|
827
|
+
'recent_interactions': interactions,
|
|
828
|
+
'topic': topic,
|
|
829
|
+
'retrieval_time_ms': round((time.time() - t0) * 1000, 1),
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
# 9. Apply topic filtering if provided
|
|
833
|
+
if topic:
|
|
834
|
+
context = filter_by_topic(context, topic)
|
|
835
|
+
|
|
836
|
+
return context
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
# ─────────────────────────────────────────────
|
|
840
|
+
# Brief format — concise summary for prompt injection
|
|
841
|
+
# ─────────────────────────────────────────────
|
|
842
|
+
def format_brief(context):
|
|
843
|
+
"""Format context as a 3-5 line summary for prompt injection.
|
|
844
|
+
|
|
845
|
+
Returns a string.
|
|
846
|
+
"""
|
|
847
|
+
if context is None:
|
|
848
|
+
return "RECIPIENT NOT FOUND — no entity data available. Verify recipient identity before drafting."
|
|
849
|
+
|
|
850
|
+
r = context['recipient']
|
|
851
|
+
rel = context['relationship']
|
|
852
|
+
comm = context['communication']
|
|
853
|
+
|
|
854
|
+
lines = []
|
|
855
|
+
|
|
856
|
+
# Line 1: Identity + relationship
|
|
857
|
+
rel_label = rel['type'].replace('_', ' ')
|
|
858
|
+
company_name = r.get('company', '')
|
|
859
|
+
if company_name and ':' in company_name:
|
|
860
|
+
# Resolve company entity id to name
|
|
861
|
+
idx = load_entity_index()
|
|
862
|
+
for e in idx['entities']:
|
|
863
|
+
if isinstance(e, dict) and e.get('id') == company_name:
|
|
864
|
+
company_name = e.get('name', company_name)
|
|
865
|
+
break
|
|
866
|
+
role_str = f"{r['role']}, {company_name}" if r['role'] and company_name else r['role'] or ''
|
|
867
|
+
lines.append(f"RECIPIENT: {r['name']} — {role_str} [{rel_label}]")
|
|
868
|
+
|
|
869
|
+
# Line 2: Communication preferences + standing instructions
|
|
870
|
+
prefs = comm.get('preferences', {})
|
|
871
|
+
tone = prefs.get('tone', prefs.get('voice', ''))
|
|
872
|
+
channel = prefs.get('preferred_channel', prefs.get('channel_preference', ''))
|
|
873
|
+
pref_parts = []
|
|
874
|
+
if tone:
|
|
875
|
+
pref_parts.append(f"tone: {tone}")
|
|
876
|
+
if channel:
|
|
877
|
+
pref_parts.append(f"prefers: {channel}")
|
|
878
|
+
instructions = comm.get('standing_instructions', [])
|
|
879
|
+
if instructions and isinstance(instructions, list) and len(instructions) > 0:
|
|
880
|
+
pref_parts.append(f"instructions: {'; '.join(str(i) for i in instructions[:2])}")
|
|
881
|
+
if pref_parts:
|
|
882
|
+
lines.append(f"COMMS: {', '.join(pref_parts)}")
|
|
883
|
+
|
|
884
|
+
# Line 3: Active items
|
|
885
|
+
active = context.get('active_queue_items', [])
|
|
886
|
+
topic = context.get('topic', '')
|
|
887
|
+
if topic:
|
|
888
|
+
topic_items = [i for i in active if i.get('topic_relevant')]
|
|
889
|
+
if topic_items:
|
|
890
|
+
lines.append(f"ACTIVE ({topic}): {len(topic_items)} item(s) — " +
|
|
891
|
+
'; '.join(i['title'][:60] for i in topic_items[:3]))
|
|
892
|
+
elif active:
|
|
893
|
+
lines.append(f"ACTIVE: {len(active)} item(s) — " +
|
|
894
|
+
'; '.join(i['title'][:60] for i in active[:3]))
|
|
895
|
+
elif active:
|
|
896
|
+
lines.append(f"ACTIVE: {len(active)} item(s) — " +
|
|
897
|
+
'; '.join(i['title'][:60] for i in active[:3]))
|
|
898
|
+
|
|
899
|
+
# Line 4: Last interaction
|
|
900
|
+
last = comm.get('last_interaction')
|
|
901
|
+
interactions = context.get('recent_interactions', [])
|
|
902
|
+
# Find the most recent interaction with a non-empty timestamp
|
|
903
|
+
latest_with_ts = None
|
|
904
|
+
for ix in interactions:
|
|
905
|
+
if ix.get('timestamp'):
|
|
906
|
+
latest_with_ts = ix
|
|
907
|
+
break
|
|
908
|
+
if latest_with_ts:
|
|
909
|
+
channel = latest_with_ts.get('channel', '')
|
|
910
|
+
summary = latest_with_ts.get('summary', '') or latest_with_ts.get('subject', '')
|
|
911
|
+
if summary and len(summary) > 80:
|
|
912
|
+
summary = summary[:77] + '...'
|
|
913
|
+
lines.append(f"LAST CONTACT: {latest_with_ts['timestamp']} via {channel} — {summary}")
|
|
914
|
+
elif interactions:
|
|
915
|
+
# Have interactions but none with timestamps — use first one's subject
|
|
916
|
+
latest = interactions[0]
|
|
917
|
+
channel = latest.get('channel', '')
|
|
918
|
+
summary = latest.get('summary', '') or latest.get('subject', '')
|
|
919
|
+
if summary and len(summary) > 80:
|
|
920
|
+
summary = summary[:77] + '...'
|
|
921
|
+
if summary:
|
|
922
|
+
lines.append(f"LAST CONTACT: via {channel} — {summary}")
|
|
923
|
+
elif last:
|
|
924
|
+
lines.append(f"LAST CONTACT: {last}")
|
|
925
|
+
|
|
926
|
+
# Line 5: Key notes/warnings
|
|
927
|
+
notes = r.get('notes', '') or ''
|
|
928
|
+
interaction_notes = comm.get('interaction_notes', '') or ''
|
|
929
|
+
# Prefer interaction notes for internal people (richer context)
|
|
930
|
+
note_text = interaction_notes if (rel['is_internal'] and interaction_notes) else notes
|
|
931
|
+
if not note_text:
|
|
932
|
+
note_text = interaction_notes or notes
|
|
933
|
+
if note_text:
|
|
934
|
+
# Take first sentence if long
|
|
935
|
+
if len(note_text) > 100:
|
|
936
|
+
first_sentence = note_text.split('.')[0].strip()
|
|
937
|
+
if first_sentence and len(first_sentence) < 100:
|
|
938
|
+
note_text = first_sentence + '.'
|
|
939
|
+
else:
|
|
940
|
+
note_text = note_text[:97] + '...'
|
|
941
|
+
lines.append(f"NOTE: {note_text}")
|
|
942
|
+
|
|
943
|
+
return '\n'.join(lines)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
# ─────────────────────────────────────────────
|
|
947
|
+
# CLI entry point
|
|
948
|
+
# ─────────────────────────────────────────────
|
|
949
|
+
def main():
|
|
950
|
+
parser = argparse.ArgumentParser(
|
|
951
|
+
description='Pre-draft fact retrieval — retrieve entity context before composing messages.'
|
|
952
|
+
)
|
|
953
|
+
parser.add_argument('--recipient', required=True,
|
|
954
|
+
help='Recipient name or email address')
|
|
955
|
+
parser.add_argument('--topic', default=None,
|
|
956
|
+
help='Optional topic to filter/boost relevant context')
|
|
957
|
+
parser.add_argument('--format', dest='output_format', default='json',
|
|
958
|
+
choices=['json', 'brief'],
|
|
959
|
+
help='Output format: json (full) or brief (3-5 line summary)')
|
|
960
|
+
|
|
961
|
+
args = parser.parse_args()
|
|
962
|
+
|
|
963
|
+
try:
|
|
964
|
+
context = retrieve_context(args.recipient, args.topic)
|
|
965
|
+
|
|
966
|
+
if context is None:
|
|
967
|
+
if args.output_format == 'brief':
|
|
968
|
+
print(format_brief(None))
|
|
969
|
+
else:
|
|
970
|
+
print(json.dumps({
|
|
971
|
+
'status': 'not_found',
|
|
972
|
+
'recipient_query': args.recipient,
|
|
973
|
+
'message': f'No entity found matching "{args.recipient}"',
|
|
974
|
+
}, indent=2))
|
|
975
|
+
sys.exit(1)
|
|
976
|
+
|
|
977
|
+
if args.output_format == 'brief':
|
|
978
|
+
print(format_brief(context))
|
|
979
|
+
else:
|
|
980
|
+
# Sanitize for JSON serialization
|
|
981
|
+
print(json.dumps(context, indent=2, default=str))
|
|
982
|
+
|
|
983
|
+
sys.exit(0)
|
|
984
|
+
|
|
985
|
+
except Exception as e:
|
|
986
|
+
print(json.dumps({
|
|
987
|
+
'status': 'error',
|
|
988
|
+
'message': str(e),
|
|
989
|
+
}), file=sys.stderr)
|
|
990
|
+
sys.exit(2)
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
if __name__ == '__main__':
|
|
994
|
+
main()
|