@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.
Files changed (731) hide show
  1. package/.claude/commands/init-agent.md +104 -0
  2. package/.claude/commands/init-maestro.md +1187 -0
  3. package/.claude/settings.json +161 -0
  4. package/.env.example +216 -0
  5. package/README.md +632 -0
  6. package/agents/browser-operator/agent.md +52 -0
  7. package/agents/calendar-ops/agent.md +50 -0
  8. package/agents/communications/agent.md +96 -0
  9. package/agents/decision-log/agent.md +65 -0
  10. package/agents/desktop-operator/agent.md +59 -0
  11. package/agents/gmail-operator/agent.md +62 -0
  12. package/agents/inbound-dispatcher/agent.md +66 -0
  13. package/agents/inbox-processor/agent.md +39 -0
  14. package/agents/pmo-execution/agent.md +60 -0
  15. package/agents/session-spawner/agent.md +64 -0
  16. package/agents/slack-operator/agent.md +60 -0
  17. package/agents/whatsapp-operator/agent.md +60 -0
  18. package/agents/workflow-automation/agent.md +61 -0
  19. package/archetypes/altitudes/c-suite.yaml +58 -0
  20. package/archetypes/altitudes/founder.yaml +68 -0
  21. package/archetypes/altitudes/senior-manager.yaml +63 -0
  22. package/archetypes/altitudes/svp.yaml +60 -0
  23. package/archetypes/altitudes/vp.yaml +50 -0
  24. package/archetypes/archetype.schema.json +77 -0
  25. package/archetypes/base.yaml +47 -0
  26. package/archetypes/capabilities/commercial-leader.yaml +159 -0
  27. package/archetypes/capabilities/compliance-officer.yaml +159 -0
  28. package/archetypes/capabilities/executive-operator.yaml +169 -0
  29. package/archetypes/capabilities/finance-leader.yaml +162 -0
  30. package/archetypes/capabilities/operations-leader.yaml +154 -0
  31. package/archetypes/capabilities/product-leader.yaml +148 -0
  32. package/archetypes/capabilities/technical-leader.yaml +146 -0
  33. package/archetypes/functions/commercial-leader.yaml +62 -0
  34. package/archetypes/functions/compliance-officer.yaml +64 -0
  35. package/archetypes/functions/executive-operator.yaml +70 -0
  36. package/archetypes/functions/finance-leader.yaml +70 -0
  37. package/archetypes/functions/operations-leader.yaml +62 -0
  38. package/archetypes/functions/product-leader.yaml +61 -0
  39. package/archetypes/functions/technical-leader.yaml +57 -0
  40. package/bin/cohort-mcp.mjs +81 -0
  41. package/bin/maestro.mjs +3516 -0
  42. package/bin/maestro.test.mjs +1015 -0
  43. package/desktop-control/README.md +56 -0
  44. package/desktop-control/app-profiles/gmail.yaml +120 -0
  45. package/desktop-control/app-profiles/slack.yaml +315 -0
  46. package/desktop-control/app-profiles/whatsapp.yaml +107 -0
  47. package/docs/architecture/agent-topology.md +2239 -0
  48. package/docs/architecture/archetype-agent-factory.md +110 -0
  49. package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
  50. package/docs/architecture/continuous-monitoring.md +221 -0
  51. package/docs/architecture/mcp-capability-map.md +585 -0
  52. package/docs/architecture/system-architecture.md +1272 -0
  53. package/docs/company-context/README.md +40 -0
  54. package/docs/guides/agent-persona-setup.md +600 -0
  55. package/docs/guides/agents-observe-setup.md +64 -0
  56. package/docs/guides/billing-console-keys.md +88 -0
  57. package/docs/guides/ccxray-diagnostics.md +65 -0
  58. package/docs/guides/channel-bus.md +127 -0
  59. package/docs/guides/claude-mem-setup.md +79 -0
  60. package/docs/guides/claude-pace-setup.md +56 -0
  61. package/docs/guides/claudraband-sessions.md +98 -0
  62. package/docs/guides/clawteam-swarm.md +116 -0
  63. package/docs/guides/code-review-graph-setup.md +86 -0
  64. package/docs/guides/email-setup.md +431 -0
  65. package/docs/guides/mac-mini.md +119 -0
  66. package/docs/guides/media-generation-setup.md +349 -0
  67. package/docs/guides/model-routing.md +162 -0
  68. package/docs/guides/observability-otel.md +265 -0
  69. package/docs/guides/org-onboarding.md +132 -0
  70. package/docs/guides/outbound-governance-setup.md +437 -0
  71. package/docs/guides/pdf-generation-setup.md +315 -0
  72. package/docs/guides/poller-daemon-setup.md +563 -0
  73. package/docs/guides/rag-context-setup.md +459 -0
  74. package/docs/guides/self-optimization-pattern.md +82 -0
  75. package/docs/guides/setup-wizard.md +178 -0
  76. package/docs/guides/slack-setup.md +350 -0
  77. package/docs/guides/telegram-setup.md +227 -0
  78. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  79. package/docs/guides/verification.md +128 -0
  80. package/docs/guides/voice-mode.md +188 -0
  81. package/docs/guides/voice-sms-setup.md +698 -0
  82. package/docs/guides/webhook-relay-setup.md +349 -0
  83. package/docs/guides/whatsapp-setup.md +288 -0
  84. package/docs/prompts/board-pack-cover-template.md +36 -0
  85. package/docs/prompts/decision-recommendation-template.md +88 -0
  86. package/docs/prompts/followup-message-template.md +141 -0
  87. package/docs/prompts/investor-letter-template.md +52 -0
  88. package/docs/prompts/morning-brief-template.md +82 -0
  89. package/docs/prompts/presentation-template.md +58 -0
  90. package/docs/prompts/weekly-strategic-memo-template.md +104 -0
  91. package/docs/research/hallucinated-tool-output-investigation.md +151 -0
  92. package/docs/runbooks/backup-restore.md +205 -0
  93. package/docs/runbooks/cohort-cutover.md +129 -0
  94. package/docs/runbooks/fleet-operations.md +200 -0
  95. package/docs/runbooks/incident-response.md +226 -0
  96. package/docs/runbooks/mac-mini-bootstrap.md +431 -0
  97. package/docs/runbooks/perpetual-operations.md +509 -0
  98. package/docs/runbooks/recovery-and-failover.md +260 -0
  99. package/framework-features.json +267 -0
  100. package/ingest/README.md +87 -0
  101. package/lib/action-executor.js +689 -0
  102. package/lib/action-executor.test.mjs +871 -0
  103. package/lib/agent-root.mjs +37 -0
  104. package/lib/archetype.mjs +236 -0
  105. package/lib/archetype.test.mjs +132 -0
  106. package/lib/autonomy.mjs +114 -0
  107. package/lib/autonomy.test.mjs +66 -0
  108. package/lib/backlog.mjs +358 -0
  109. package/lib/backlog.test.mjs +266 -0
  110. package/lib/budget-guard.mjs +279 -0
  111. package/lib/budget-guard.test.mjs +291 -0
  112. package/lib/cadence-bus-schedule.test.mjs +194 -0
  113. package/lib/cadence-bus.mjs +1120 -0
  114. package/lib/cadence-bus.test.mjs +720 -0
  115. package/lib/cadences.mjs +205 -0
  116. package/lib/cadences.test.mjs +125 -0
  117. package/lib/capability.mjs +154 -0
  118. package/lib/capability.test.mjs +78 -0
  119. package/lib/channels/base-adapter.mjs +719 -0
  120. package/lib/channels/base-adapter.test.mjs +590 -0
  121. package/lib/channels/channel.mjs +128 -0
  122. package/lib/channels/channels.test.mjs +371 -0
  123. package/lib/channels/contract.mjs +215 -0
  124. package/lib/channels/contract.test.mjs +137 -0
  125. package/lib/channels/conversation-resolver.mjs +95 -0
  126. package/lib/channels/gmail/adapter.mjs +87 -0
  127. package/lib/channels/inbox-item.mjs +255 -0
  128. package/lib/channels/inbox-item.test.mjs +335 -0
  129. package/lib/channels/index.mjs +94 -0
  130. package/lib/channels/orgmail/adapter.mjs +353 -0
  131. package/lib/channels/orgmail/adapter.test.mjs +311 -0
  132. package/lib/channels/pairing.mjs +363 -0
  133. package/lib/channels/pairing.test.mjs +270 -0
  134. package/lib/channels/registry.mjs +164 -0
  135. package/lib/channels/slack/adapter.mjs +317 -0
  136. package/lib/channels/slack-adapter.test.mjs +212 -0
  137. package/lib/channels/sms/adapter.mjs +43 -0
  138. package/lib/channels/telegram/adapter.mjs +432 -0
  139. package/lib/channels/telegram-adapter.test.mjs +306 -0
  140. package/lib/channels/voice/adapter.mjs +301 -0
  141. package/lib/channels/voice/adapter.test.mjs +278 -0
  142. package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
  143. package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
  144. package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
  145. package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
  146. package/lib/charter.mjs +256 -0
  147. package/lib/charter.test.mjs +89 -0
  148. package/lib/claude-bin.mjs +134 -0
  149. package/lib/claude-bin.test.mjs +75 -0
  150. package/lib/collective/capture.mjs +185 -0
  151. package/lib/collective/capture.test.mjs +121 -0
  152. package/lib/collective/cards.mjs +201 -0
  153. package/lib/collective/cards.test.mjs +114 -0
  154. package/lib/collective/config.mjs +186 -0
  155. package/lib/collective/config.test.mjs +123 -0
  156. package/lib/collective/global-config.mjs +113 -0
  157. package/lib/collective/global-config.test.mjs +75 -0
  158. package/lib/collective/presence.mjs +201 -0
  159. package/lib/collective/presence.test.mjs +95 -0
  160. package/lib/collective/recall.mjs +215 -0
  161. package/lib/collective/recall.test.mjs +116 -0
  162. package/lib/comms/send-gate.mjs +554 -0
  163. package/lib/comms/send-gate.test.mjs +577 -0
  164. package/lib/comms.mjs +67 -0
  165. package/lib/comms.test.mjs +41 -0
  166. package/lib/diagnostics/alerts.mjs +424 -0
  167. package/lib/diagnostics/alerts.test.mjs +318 -0
  168. package/lib/diagnostics/backup-freshness.mjs +188 -0
  169. package/lib/diagnostics/backup-freshness.test.mjs +185 -0
  170. package/lib/diagnostics/counters.mjs +269 -0
  171. package/lib/diagnostics/counters.test.mjs +206 -0
  172. package/lib/diagnostics/events.mjs +188 -0
  173. package/lib/diagnostics/events.test.mjs +290 -0
  174. package/lib/diagnostics/otel.mjs +237 -0
  175. package/lib/diagnostics/otel.test.mjs +196 -0
  176. package/lib/diagnostics/trace.mjs +216 -0
  177. package/lib/diagnostics/trace.test.mjs +251 -0
  178. package/lib/env-compat.mjs +74 -0
  179. package/lib/env-compat.test.mjs +104 -0
  180. package/lib/feature-init.mjs +331 -0
  181. package/lib/fs-atomic.mjs +112 -0
  182. package/lib/fs-atomic.test.mjs +72 -0
  183. package/lib/fs-ownership.mjs +111 -0
  184. package/lib/fs-ownership.test.mjs +158 -0
  185. package/lib/hooks/bus.mjs +347 -0
  186. package/lib/hooks/bus.test.mjs +387 -0
  187. package/lib/index.js +16 -0
  188. package/lib/learning/config.mjs +106 -0
  189. package/lib/learning/config.test.mjs +75 -0
  190. package/lib/learning/counters.mjs +156 -0
  191. package/lib/learning/counters.test.mjs +69 -0
  192. package/lib/learning/curator-consolidate.test.mjs +238 -0
  193. package/lib/learning/curator.mjs +453 -0
  194. package/lib/learning/curator.test.mjs +106 -0
  195. package/lib/learning/log.mjs +40 -0
  196. package/lib/learning/reflect.mjs +534 -0
  197. package/lib/learning/reflect.test.mjs +0 -0
  198. package/lib/learning/session-index.mjs +352 -0
  199. package/lib/learning/session-index.test.mjs +125 -0
  200. package/lib/learning/skill-writer.mjs +474 -0
  201. package/lib/learning/skill-writer.test.mjs +210 -0
  202. package/lib/mcp/server.mjs +328 -0
  203. package/lib/mcp/server.test.mjs +400 -0
  204. package/lib/model-router/auth-profiles.mjs +758 -0
  205. package/lib/model-router/auth-profiles.test.mjs +580 -0
  206. package/lib/model-router/catalog/anthropic.yaml +153 -0
  207. package/lib/model-router/catalog/deepseek.yaml +86 -0
  208. package/lib/model-router/catalog/moonshot.yaml +81 -0
  209. package/lib/model-router/catalog/qwen.yaml +114 -0
  210. package/lib/model-router/catalog.mjs +925 -0
  211. package/lib/model-router/catalog.test.mjs +385 -0
  212. package/lib/model-router/economics.mjs +564 -0
  213. package/lib/model-router/economics.test.mjs +344 -0
  214. package/lib/model-router/failover.mjs +298 -0
  215. package/lib/model-router/failover.test.mjs +439 -0
  216. package/lib/model-router/health.mjs +453 -0
  217. package/lib/model-router/health.test.mjs +338 -0
  218. package/lib/model-router/integration-coverage.test.mjs +829 -0
  219. package/lib/model-router/integration.test.mjs +564 -0
  220. package/lib/model-router/ledger.mjs +402 -0
  221. package/lib/model-router/ledger.test.mjs +382 -0
  222. package/lib/model-router/llm-task.mjs +515 -0
  223. package/lib/model-router/llm-task.test.mjs +392 -0
  224. package/lib/model-router/org-credentials.mjs +260 -0
  225. package/lib/model-router/org-credentials.test.mjs +265 -0
  226. package/lib/model-router/pricing-refresh.mjs +463 -0
  227. package/lib/model-router/pricing-refresh.test.mjs +286 -0
  228. package/lib/model-router/reconcile.mjs +429 -0
  229. package/lib/model-router/reconcile.test.mjs +316 -0
  230. package/lib/model-router/repair.mjs +471 -0
  231. package/lib/model-router/repair.test.mjs +180 -0
  232. package/lib/model-router/resolve.mjs +1206 -0
  233. package/lib/model-router/spawn.mjs +497 -0
  234. package/lib/model-router/spawn.test.mjs +425 -0
  235. package/lib/model-router/taxonomy.mjs +893 -0
  236. package/lib/model-router/taxonomy.test.mjs +410 -0
  237. package/lib/model-router.mjs +677 -0
  238. package/lib/model-router.test.mjs +907 -0
  239. package/lib/org/activity.mjs +211 -0
  240. package/lib/org/activity.test.mjs +134 -0
  241. package/lib/org/approvals.mjs +448 -0
  242. package/lib/org/approvals.test.mjs +216 -0
  243. package/lib/org/awareness.mjs +222 -0
  244. package/lib/org/awareness.test.mjs +159 -0
  245. package/lib/org/board.mjs +229 -0
  246. package/lib/org/board.test.mjs +177 -0
  247. package/lib/org/bootstrap-context.mjs +169 -0
  248. package/lib/org/bootstrap-context.test.mjs +153 -0
  249. package/lib/org/client.mjs +1628 -0
  250. package/lib/org/client.test.mjs +1107 -0
  251. package/lib/org/cohort-client.mjs +67 -0
  252. package/lib/org/cohort-client.test.mjs +126 -0
  253. package/lib/org/cost-sync.mjs +227 -0
  254. package/lib/org/cost-sync.test.mjs +153 -0
  255. package/lib/org/doctor.mjs +212 -0
  256. package/lib/org/doctor.test.mjs +212 -0
  257. package/lib/org/handoff.mjs +293 -0
  258. package/lib/org/handoff.test.mjs +269 -0
  259. package/lib/org/integration-tools.mjs +182 -0
  260. package/lib/org/integration-tools.test.mjs +160 -0
  261. package/lib/org/keys.mjs +131 -0
  262. package/lib/org/keys.test.mjs +92 -0
  263. package/lib/org/knowledge.mjs +463 -0
  264. package/lib/org/knowledge.test.mjs +319 -0
  265. package/lib/org/leases.mjs +335 -0
  266. package/lib/org/leases.test.mjs +235 -0
  267. package/lib/org/mesh-integration.test.mjs +127 -0
  268. package/lib/org/mesh.mjs +459 -0
  269. package/lib/org/mesh.test.mjs +345 -0
  270. package/lib/org/messaging.mjs +503 -0
  271. package/lib/org/messaging.test.mjs +238 -0
  272. package/lib/org/policy.mjs +345 -0
  273. package/lib/org/policy.test.mjs +237 -0
  274. package/lib/org/protocol.checksum +1 -0
  275. package/lib/org/protocol.checksum.test.mjs +90 -0
  276. package/lib/org/protocol.mjs +967 -0
  277. package/lib/org/protocol.test.mjs +264 -0
  278. package/lib/org/registry.mjs +194 -0
  279. package/lib/org/registry.test.mjs +100 -0
  280. package/lib/org/tool-surface-integration.test.mjs +120 -0
  281. package/lib/org/tool-surface.mjs +2535 -0
  282. package/lib/org/tool-surface.test.mjs +589 -0
  283. package/lib/org/ui-parity.mjs +3236 -0
  284. package/lib/org/ui-parity.test.mjs +348 -0
  285. package/lib/org/verify.mjs +176 -0
  286. package/lib/org/verify.test.mjs +194 -0
  287. package/lib/rag/embed.mjs +188 -0
  288. package/lib/rag/indexer.mjs +425 -0
  289. package/lib/rag/rag.test.mjs +505 -0
  290. package/lib/rag/search.mjs +475 -0
  291. package/lib/rate-guard.mjs +246 -0
  292. package/lib/rate-guard.test.mjs +201 -0
  293. package/lib/render.mjs +112 -0
  294. package/lib/render.test.mjs +68 -0
  295. package/lib/resource-governor.mjs +297 -0
  296. package/lib/resource-governor.test.mjs +262 -0
  297. package/lib/scheduling/dynamic-jobs.mjs +675 -0
  298. package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
  299. package/lib/scheduling/jitter.mjs +0 -0
  300. package/lib/scheduling/jitter.test.mjs +140 -0
  301. package/lib/secrets/broker.mjs +315 -0
  302. package/lib/secrets/broker.test.mjs +280 -0
  303. package/lib/secrets/providers.mjs +461 -0
  304. package/lib/secrets/providers.test.mjs +274 -0
  305. package/lib/security/audit-engine.mjs +684 -0
  306. package/lib/security/audit-engine.test.mjs +389 -0
  307. package/lib/security/coerce-args.mjs +552 -0
  308. package/lib/security/coerce-args.test.mjs +281 -0
  309. package/lib/security/dangerous-tools.mjs +97 -0
  310. package/lib/security/dangerous-tools.test.mjs +68 -0
  311. package/lib/security/external-content.mjs +145 -0
  312. package/lib/security/external-content.test.mjs +67 -0
  313. package/lib/security/redact.mjs +592 -0
  314. package/lib/security/redact.test.mjs +441 -0
  315. package/lib/security/secret-equal.mjs +73 -0
  316. package/lib/security/secret-equal.test.mjs +55 -0
  317. package/lib/session-permissions.mjs +101 -0
  318. package/lib/session-permissions.test.mjs +100 -0
  319. package/lib/setup/claude-probe.mjs +74 -0
  320. package/lib/setup/completeness.mjs +175 -0
  321. package/lib/setup/completeness.test.mjs +110 -0
  322. package/lib/setup/context-pack.mjs +173 -0
  323. package/lib/setup/context-pack.test.mjs +89 -0
  324. package/lib/setup/enrich.mjs +277 -0
  325. package/lib/setup/enrich.test.mjs +115 -0
  326. package/lib/setup/enroll-from-cohort.mjs +441 -0
  327. package/lib/setup/enroll-from-cohort.test.mjs +233 -0
  328. package/lib/setup/integration.test.mjs +162 -0
  329. package/lib/setup/io.mjs +360 -0
  330. package/lib/setup/io.test.mjs +77 -0
  331. package/lib/setup/run-generator.mjs +81 -0
  332. package/lib/setup/runner.mjs +244 -0
  333. package/lib/setup/runner.test.mjs +132 -0
  334. package/lib/setup/sections/comms.mjs +173 -0
  335. package/lib/setup/sections/company.mjs +120 -0
  336. package/lib/setup/sections/enrich.mjs +138 -0
  337. package/lib/setup/sections/identity.mjs +182 -0
  338. package/lib/setup/sections/identity.test.mjs +140 -0
  339. package/lib/setup/sections/learning.mjs +153 -0
  340. package/lib/setup/sections/learning.test.mjs +81 -0
  341. package/lib/setup/sections/messaging.mjs +219 -0
  342. package/lib/setup/sections/messaging.test.mjs +127 -0
  343. package/lib/setup/sections/model.mjs +102 -0
  344. package/lib/setup/sections/operating-model.mjs +78 -0
  345. package/lib/setup/sections/org.mjs +475 -0
  346. package/lib/setup/sections/org.test.mjs +313 -0
  347. package/lib/setup/sections/orgmail.mjs +173 -0
  348. package/lib/setup/sections/orgmail.test.mjs +118 -0
  349. package/lib/setup/sections/recovery.mjs +159 -0
  350. package/lib/setup/sections/recovery.test.mjs +98 -0
  351. package/lib/setup/sections/tools.mjs +132 -0
  352. package/lib/setup/sections/verify.mjs +97 -0
  353. package/lib/setup/sot.mjs +205 -0
  354. package/lib/setup/sot.test.mjs +81 -0
  355. package/lib/setup/state.mjs +151 -0
  356. package/lib/setup/state.test.mjs +92 -0
  357. package/lib/singleton.js +229 -0
  358. package/lib/singleton.test.mjs +135 -0
  359. package/lib/telemetry/alerts.mjs +216 -0
  360. package/lib/telemetry/alerts.test.mjs +109 -0
  361. package/lib/telemetry/collect.mjs +512 -0
  362. package/lib/telemetry/collect.test.mjs +202 -0
  363. package/lib/tool-definitions-integration.test.mjs +83 -0
  364. package/lib/tool-definitions.js +738 -0
  365. package/lib/tool-definitions.test.mjs +437 -0
  366. package/lib/util/fetch-timeout.mjs +136 -0
  367. package/lib/util/fetch-timeout.test.mjs +202 -0
  368. package/lib/util/reconnect.mjs +343 -0
  369. package/lib/util/reconnect.test.mjs +369 -0
  370. package/lib/util/unhandled.mjs +205 -0
  371. package/lib/util/unhandled.test.mjs +216 -0
  372. package/lib/voice/context-loader.mjs +466 -0
  373. package/lib/voice/index.mjs +100 -0
  374. package/lib/voice/openai-realtime.mjs +510 -0
  375. package/lib/voice/outbound.mjs +542 -0
  376. package/lib/voice/outbound.test.mjs +69 -0
  377. package/lib/voice/post-call-brief.mjs +428 -0
  378. package/lib/voice/provider.mjs +52 -0
  379. package/lib/voice/session-rotation.mjs +257 -0
  380. package/lib/voice/stt.mjs +161 -0
  381. package/lib/voice/stt.test.mjs +226 -0
  382. package/lib/voice/tool-bridge.mjs +370 -0
  383. package/lib/voice/tts.mjs +104 -0
  384. package/lib/voice/twilio-sip-bridge.mjs +288 -0
  385. package/lib/voice/voice.test.mjs +990 -0
  386. package/mcp/README.md +80 -0
  387. package/package.json +151 -0
  388. package/plugins/maestro-skills/plugin.json +139 -0
  389. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  390. package/plugins/maestro-skills/skills/board-deck.md +68 -0
  391. package/plugins/maestro-skills/skills/books-close.md +77 -0
  392. package/plugins/maestro-skills/skills/brand-steward.md +121 -0
  393. package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
  394. package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
  395. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  396. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  397. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  398. package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
  399. package/plugins/maestro-skills/skills/decision-brief.md +89 -0
  400. package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
  401. package/plugins/maestro-skills/skills/draft-comms.md +84 -0
  402. package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
  403. package/plugins/maestro-skills/skills/files-find.md +65 -0
  404. package/plugins/maestro-skills/skills/generative-ui.md +228 -0
  405. package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
  406. package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
  407. package/plugins/maestro-skills/skills/mail-triage.md +86 -0
  408. package/plugins/maestro-skills/skills/morning-brief.md +54 -0
  409. package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
  410. package/plugins/maestro-skills/skills/org-board.md +133 -0
  411. package/plugins/maestro-skills/skills/org-credential.md +68 -0
  412. package/plugins/maestro-skills/skills/org-recall.md +81 -0
  413. package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
  414. package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
  415. package/plugins/maestro-skills/skills/router-why.md +78 -0
  416. package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
  417. package/plugins/maestro-skills/skills/session-search.md +71 -0
  418. package/plugins/maestro-skills/skills/set-reminder.md +93 -0
  419. package/plugins/maestro-skills/skills/slack-followup.md +64 -0
  420. package/plugins/maestro-skills/skills/team-activity.md +86 -0
  421. package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
  422. package/policies/action-classification.yaml +114 -0
  423. package/policies/ai-disclosure.yaml +294 -0
  424. package/policies/communication-style.md +139 -0
  425. package/policies/information-barriers.yaml +118 -0
  426. package/policies/prompt-injection-defence.yaml +138 -0
  427. package/public/assets/icon-dark.png +0 -0
  428. package/public/assets/icon-dark.svg +9 -0
  429. package/public/assets/icon-light.svg +9 -0
  430. package/public/assets/logo-dark.svg +15 -0
  431. package/public/assets/logo-light.svg +15 -0
  432. package/scaffold/.mcp.json +7 -0
  433. package/scaffold/CLAUDE.md +368 -0
  434. package/scaffold/config/agent.json +55 -0
  435. package/scaffold/config/agent.ts +76 -0
  436. package/scaffold/config/agent.ts.example +89 -0
  437. package/scaffold/config/alerts.yaml +23 -0
  438. package/scaffold/config/allowlist.yaml.example +25 -0
  439. package/scaffold/config/caller-id-map.yaml +46 -0
  440. package/scaffold/config/collective.yaml +49 -0
  441. package/scaffold/config/company.json +20 -0
  442. package/scaffold/config/known-agents.json +6 -0
  443. package/scaffold/config/learning.yaml +55 -0
  444. package/scaffold/config/model-routing.yaml.example +104 -0
  445. package/scaffold/config/org.yaml +25 -0
  446. package/scaffold/config/orgmail.yaml.example +19 -0
  447. package/scaffold/config/recovery.yaml +72 -0
  448. package/scaffold/config/secrets.yaml +27 -0
  449. package/scaffold/config/slack.yaml.example +35 -0
  450. package/scaffold/config/telegram.yaml.example +38 -0
  451. package/scaffold/config/voice.yaml.example +89 -0
  452. package/scaffold/config/whatsapp.yaml.example +39 -0
  453. package/schedules/README.md +49 -0
  454. package/schedules/triggers/backlog-executor.md +102 -0
  455. package/schedules/triggers/brand-steward.md +72 -0
  456. package/schedules/triggers/daily-evening-wrap.md +159 -0
  457. package/schedules/triggers/daily-midday-sweep.md +58 -0
  458. package/schedules/triggers/daily-morning-brief.md +55 -0
  459. package/schedules/triggers/directory-hygiene.md +81 -0
  460. package/schedules/triggers/dynamic-jobs.md +40 -0
  461. package/schedules/triggers/inbox-processor.md +115 -0
  462. package/schedules/triggers/meeting-action-capture.md +60 -0
  463. package/schedules/triggers/meeting-prep.md +69 -0
  464. package/schedules/triggers/messaging-inbound.md +50 -0
  465. package/schedules/triggers/org-pulse.md +24 -0
  466. package/schedules/triggers/quarterly-self-assessment.md +54 -0
  467. package/schedules/triggers/weekly-engineering-health.md +37 -0
  468. package/schedules/triggers/weekly-execution.md +65 -0
  469. package/schedules/triggers/weekly-hiring.md +53 -0
  470. package/schedules/triggers/weekly-priorities.md +38 -0
  471. package/schedules/triggers/weekly-strategic-memo.md +124 -0
  472. package/scripts/archive-email.sh +55 -0
  473. package/scripts/cadence/cadence-status.mjs +36 -0
  474. package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
  475. package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
  476. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  477. package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
  478. package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
  479. package/scripts/ci/check-docs-accuracy.mjs +493 -0
  480. package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
  481. package/scripts/ci/check-exports-exist.mjs +140 -0
  482. package/scripts/ci/check-files-exist.mjs +107 -0
  483. package/scripts/ci/check-no-build-artifacts.mjs +111 -0
  484. package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
  485. package/scripts/ci/check-no-confidential.mjs +198 -0
  486. package/scripts/ci/check-no-conflict-markers.mjs +169 -0
  487. package/scripts/ci/check-no-residual-identity.mjs +163 -0
  488. package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
  489. package/scripts/ci/check-tarball-fidelity.mjs +205 -0
  490. package/scripts/ci/check-unresolved-tokens.mjs +83 -0
  491. package/scripts/ci/check.mjs +109 -0
  492. package/scripts/ci/check.test.mjs +194 -0
  493. package/scripts/ci/run-coverage.mjs +82 -0
  494. package/scripts/ci/run-tests.mjs +71 -0
  495. package/scripts/cloud-relay/README.md +59 -0
  496. package/scripts/cloud-relay/index.mjs +233 -0
  497. package/scripts/cloud-relay/package.json +15 -0
  498. package/scripts/cloud-relay/railway.json +13 -0
  499. package/scripts/cloud-relay/voice/README.md +94 -0
  500. package/scripts/cloud-relay/voice/package-lock.json +39 -0
  501. package/scripts/cloud-relay/voice/package.json +16 -0
  502. package/scripts/cloud-relay/voice/railway.json +13 -0
  503. package/scripts/cloud-relay/voice/server.mjs +532 -0
  504. package/scripts/collective/hook-runner.mjs +211 -0
  505. package/scripts/collective/hook-runner.test.mjs +90 -0
  506. package/scripts/collective/org-pulse.mjs +72 -0
  507. package/scripts/collective/org-sync.mjs +61 -0
  508. package/scripts/collective/recall.mjs +45 -0
  509. package/scripts/collective/who.mjs +30 -0
  510. package/scripts/comms-monitor.sh +288 -0
  511. package/scripts/configure-whatsapp-sandbox.sh +201 -0
  512. package/scripts/continuous-monitor.sh +91 -0
  513. package/scripts/cost/fleet-digest.mjs +407 -0
  514. package/scripts/cost/fleet-digest.test.mjs +207 -0
  515. package/scripts/cost/track-claude-usage.mjs +169 -0
  516. package/scripts/daemon/agent-daemon.mjs +989 -0
  517. package/scripts/daemon/agent-daemon.test.mjs +525 -0
  518. package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
  519. package/scripts/daemon/cadence-consumer.mjs +1080 -0
  520. package/scripts/daemon/cadence-consumer.test.mjs +770 -0
  521. package/scripts/daemon/cadence-handlers.mjs +1121 -0
  522. package/scripts/daemon/cadence-handlers.test.mjs +617 -0
  523. package/scripts/daemon/classifier.mjs +704 -0
  524. package/scripts/daemon/classifier.test.mjs +238 -0
  525. package/scripts/daemon/classify-kind.mjs +54 -0
  526. package/scripts/daemon/classify-kind.test.mjs +40 -0
  527. package/scripts/daemon/context-compiler.mjs +605 -0
  528. package/scripts/daemon/context-compiler.test.mjs +300 -0
  529. package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
  530. package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
  531. package/scripts/daemon/dispatcher.mjs +1516 -0
  532. package/scripts/daemon/health.mjs +72 -0
  533. package/scripts/daemon/inbox-deferral.mjs +210 -0
  534. package/scripts/daemon/inbox-deferral.test.mjs +242 -0
  535. package/scripts/daemon/integration.test.mjs +149 -0
  536. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  537. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  538. package/scripts/daemon/launchd-wrapper.sh +91 -0
  539. package/scripts/daemon/lib/session-router.mjs +274 -0
  540. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  541. package/scripts/daemon/maestro-daemon.mjs +275 -0
  542. package/scripts/daemon/prompt-builder.mjs +685 -0
  543. package/scripts/daemon/prompt-builder.test.mjs +213 -0
  544. package/scripts/daemon/responder.mjs +854 -0
  545. package/scripts/daemon/session-lock.mjs +721 -0
  546. package/scripts/daemon/session-lock.test.mjs +252 -0
  547. package/scripts/daemon/session-outcomes.mjs +640 -0
  548. package/scripts/daemon/session-outcomes.test.mjs +533 -0
  549. package/scripts/daemon/typing-registry.mjs +90 -0
  550. package/scripts/daemon/typing-registry.test.mjs +77 -0
  551. package/scripts/daemon/voice-webhook-server.mjs +804 -0
  552. package/scripts/decisions/capture-decision.mjs +116 -0
  553. package/scripts/disclosure_assessment.py +873 -0
  554. package/scripts/disclosure_boundaries.py +562 -0
  555. package/scripts/email-signature-principal.html +52 -0
  556. package/scripts/email-signature.html +60 -0
  557. package/scripts/email_quote_thread.py +167 -0
  558. package/scripts/email_thread_dedup.py +362 -0
  559. package/scripts/emergency-stop.sh +81 -0
  560. package/scripts/healthcheck.sh +116 -0
  561. package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
  562. package/scripts/hooks/block-mcp-slack-send.sh +7 -0
  563. package/scripts/hooks/post-action-log.sh +126 -0
  564. package/scripts/hooks/pre-send-audit.sh +174 -0
  565. package/scripts/hooks/pre-send-audit.test.mjs +215 -0
  566. package/scripts/hooks/session-end-log.sh +27 -0
  567. package/scripts/hooks/session-start-banner.sh +115 -0
  568. package/scripts/huddle/audio-bridge.mjs +664 -0
  569. package/scripts/huddle/boot-slack-cdp.sh +102 -0
  570. package/scripts/huddle/huddle-controller.mjs +942 -0
  571. package/scripts/huddle/huddle-server.mjs +1229 -0
  572. package/scripts/huddle/launch-slack.sh +232 -0
  573. package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
  574. package/scripts/huddle/package-lock.json +62 -0
  575. package/scripts/huddle/package.json +22 -0
  576. package/scripts/huddle/setup-audio.sh +239 -0
  577. package/scripts/huddle/start-call.mjs +318 -0
  578. package/scripts/huddle/test-pipeline.mjs +263 -0
  579. package/scripts/learning/consolidate-skills.mjs +72 -0
  580. package/scripts/learning/session-search.mjs +125 -0
  581. package/scripts/llm_email_dedup.py +442 -0
  582. package/scripts/local-triggers/generate-plists.sh +432 -0
  583. package/scripts/local-triggers/generate-plists.test.mjs +413 -0
  584. package/scripts/local-triggers/install-all.sh +49 -0
  585. package/scripts/local-triggers/plists/.gitkeep +0 -0
  586. package/scripts/local-triggers/run-trigger.sh +63 -0
  587. package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
  588. package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
  589. package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
  590. package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
  591. package/scripts/maintenance/backup-to-cloud.sh +124 -0
  592. package/scripts/maintenance/health-check.sh +377 -0
  593. package/scripts/media-generation/README.md +105 -0
  594. package/scripts/media-generation/gemini-image-client.mjs +173 -0
  595. package/scripts/media-generation/generate-assets.mjs +289 -0
  596. package/scripts/media-generation/veo-video-client.mjs +219 -0
  597. package/scripts/org/send-orgmail.mjs +227 -0
  598. package/scripts/outbound-dedup-cleanup.sh +43 -0
  599. package/scripts/outbound-dedup.sh +477 -0
  600. package/scripts/outbound_dedup.py +115 -0
  601. package/scripts/parse-voice-transcript.mjs +481 -0
  602. package/scripts/pdf-generation/README.md +63 -0
  603. package/scripts/pdf-generation/build-document.mjs +247 -0
  604. package/scripts/pdf-generation/templates/board-pack.latex +136 -0
  605. package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
  606. package/scripts/pdf-generation/templates/memo.latex +114 -0
  607. package/scripts/poll-slack-events.sh +35 -0
  608. package/scripts/poller/calendar-poller.mjs +12 -0
  609. package/scripts/poller/gmail-poller.mjs +192 -0
  610. package/scripts/poller/imap-client.mjs +289 -0
  611. package/scripts/poller/inbox-scan-poller.mjs +156 -0
  612. package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
  613. package/scripts/poller/index.mjs +73 -0
  614. package/scripts/poller/intra-session-check.mjs +285 -0
  615. package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
  616. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
  617. package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
  618. package/scripts/poller/secondary-gmail-poller.mjs +132 -0
  619. package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
  620. package/scripts/poller/slack-poller.mjs +854 -0
  621. package/scripts/poller/slack-socket-mode.mjs +917 -0
  622. package/scripts/poller/slack-socket-mode.test.mjs +753 -0
  623. package/scripts/poller/trigger.mjs +75 -0
  624. package/scripts/poller/utils.mjs +371 -0
  625. package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
  626. package/scripts/poller/voice-poller.mjs +236 -0
  627. package/scripts/poller-launchd/install.sh +66 -0
  628. package/scripts/poller-launchd/poller.plist.template +40 -0
  629. package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
  630. package/scripts/post-interaction-indexer.py +1598 -0
  631. package/scripts/pre-draft-context.py +994 -0
  632. package/scripts/pre_draft_lookup.py +258 -0
  633. package/scripts/rag/build-index.mjs +47 -0
  634. package/scripts/rag/ingest.mjs +111 -0
  635. package/scripts/rag/search.mjs +119 -0
  636. package/scripts/rag-indexer.py +629 -0
  637. package/scripts/restore-from-backup.sh +248 -0
  638. package/scripts/restore-from-backup.test.mjs +178 -0
  639. package/scripts/resume-operations.sh +80 -0
  640. package/scripts/search-secondary-inbox.py +181 -0
  641. package/scripts/secondary-inbox-poller.py +437 -0
  642. package/scripts/self-optimization/compute-metrics.py +398 -0
  643. package/scripts/send-email-as-principal.py +369 -0
  644. package/scripts/send-email-threaded.py +392 -0
  645. package/scripts/send-email-with-attachment.py +377 -0
  646. package/scripts/send-email.sh +131 -0
  647. package/scripts/send-sms.sh +175 -0
  648. package/scripts/send-whatsapp.sh +292 -0
  649. package/scripts/session-start.sh +106 -0
  650. package/scripts/setup/boot-claude-session.sh +94 -0
  651. package/scripts/setup/configure-macos.sh +674 -0
  652. package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
  653. package/scripts/setup/configure-voice-tunnel.mjs +182 -0
  654. package/scripts/setup/generate-agent-env.mjs +92 -0
  655. package/scripts/setup/generate-agent-package-json.mjs +222 -0
  656. package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
  657. package/scripts/setup/generate-autonomy.mjs +60 -0
  658. package/scripts/setup/generate-backlog.mjs +101 -0
  659. package/scripts/setup/generate-cadences.mjs +92 -0
  660. package/scripts/setup/generate-capability.mjs +162 -0
  661. package/scripts/setup/generate-charter.mjs +76 -0
  662. package/scripts/setup/generate-comms.mjs +59 -0
  663. package/scripts/setup/generate-company.mjs +92 -0
  664. package/scripts/setup/init-agent.sh +547 -0
  665. package/scripts/setup/init-agent.test.mjs +151 -0
  666. package/scripts/setup/init-archetype.mjs +74 -0
  667. package/scripts/setup/init-backup.mjs +54 -0
  668. package/scripts/setup/init-cadence-bus.mjs +60 -0
  669. package/scripts/setup/init-channel-bus.mjs +46 -0
  670. package/scripts/setup/init-cost-tracking.mjs +45 -0
  671. package/scripts/setup/init-decision-capture.mjs +66 -0
  672. package/scripts/setup/init-known-agents.mjs +57 -0
  673. package/scripts/setup/init-learning.mjs +70 -0
  674. package/scripts/setup/init-memory-executive.mjs +45 -0
  675. package/scripts/setup/init-model-router.mjs +124 -0
  676. package/scripts/setup/init-rag.mjs +174 -0
  677. package/scripts/setup/init-session-router.mjs +38 -0
  678. package/scripts/setup/init-slack-socket-mode.mjs +260 -0
  679. package/scripts/setup/init-telegram.mjs +165 -0
  680. package/scripts/setup/init-voice-realtime.mjs +204 -0
  681. package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
  682. package/scripts/setup/install-dev-tools.sh +150 -0
  683. package/scripts/setup/lib/install-plist.mjs +95 -0
  684. package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
  685. package/scripts/setup/render-environment-yaml.mjs +133 -0
  686. package/scripts/slack-events-ctl.sh +177 -0
  687. package/scripts/slack-events-server.mjs +1045 -0
  688. package/scripts/slack-react.mjs +89 -0
  689. package/scripts/slack-responded.sh +232 -0
  690. package/scripts/slack-send.sh +287 -0
  691. package/scripts/slack-typing.mjs +196 -0
  692. package/scripts/slack-upload-v2.py +95 -0
  693. package/scripts/sms-handler.mjs +450 -0
  694. package/scripts/spawn-session.sh +120 -0
  695. package/scripts/sync-protocol.mjs +217 -0
  696. package/scripts/system-verify.sh +184 -0
  697. package/scripts/test-email-thread-dedup.py +239 -0
  698. package/scripts/test-information-barriers.py +484 -0
  699. package/scripts/test-llm-email-dedup.py +251 -0
  700. package/scripts/test-pre-draft-integration.py +203 -0
  701. package/scripts/test-rag-phase2.sh +442 -0
  702. package/scripts/test-rag-search.sh +251 -0
  703. package/scripts/test-voice-parser.mjs +316 -0
  704. package/scripts/user-context-search.py +659 -0
  705. package/scripts/validate_outbound.py +1504 -0
  706. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
  707. package/scripts/watchdog/force-reboot.sh +157 -0
  708. package/scripts/watchdog/memory-watchdog.sh +473 -0
  709. package/scripts/whatsapp-handler.mjs +538 -0
  710. package/teams/desktop-operations.yaml +34 -0
  711. package/teams/executive-office.yaml +27 -0
  712. package/teams/legal-and-regulatory.yaml +24 -0
  713. package/teams/platform-and-engineering.yaml +23 -0
  714. package/teams/strategy-and-growth.yaml +29 -0
  715. package/workflows/continuous/backlog-executor.yaml +141 -0
  716. package/workflows/continuous/inbound-monitor.yaml +168 -0
  717. package/workflows/daily/applicant-triage.yaml +197 -0
  718. package/workflows/daily/comms-triage.yaml +80 -0
  719. package/workflows/daily/evening-wrap.yaml +105 -0
  720. package/workflows/daily/morning-brief.yaml +164 -0
  721. package/workflows/daily/slack-followup-sweep.yaml +87 -0
  722. package/workflows/event-driven/README.md +50 -0
  723. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  724. package/workflows/event-driven/pr-review.yaml +107 -0
  725. package/workflows/monthly/board-readiness.yaml +76 -0
  726. package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
  727. package/workflows/session-protocol.md +171 -0
  728. package/workflows/weekly/engineering-health.yaml +154 -0
  729. package/workflows/weekly/hiring-review.yaml +169 -0
  730. package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
  731. 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()