marvisx-cli 0.1.0__py3-none-any.whl

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 (587) hide show
  1. core/api/__init__.py +0 -0
  2. core/api/agents/__init__.py +0 -0
  3. core/api/agents/session_health.py +59 -0
  4. core/api/agents/session_manager.py +206 -0
  5. core/api/bin/marvisx-state-hook.py +182 -0
  6. core/api/config.py +533 -0
  7. core/api/db.py +1516 -0
  8. core/api/dependencies/__init__.py +0 -0
  9. core/api/dependencies/tenant.py +34 -0
  10. core/api/main.py +1641 -0
  11. core/api/mcp/__init__.py +8 -0
  12. core/api/mcp/_adapter.py +184 -0
  13. core/api/mcp/server.py +58 -0
  14. core/api/mcp/tools/__init__.py +59 -0
  15. core/api/mcp/tools/brain.py +599 -0
  16. core/api/mcp/tools/graph.py +380 -0
  17. core/api/mcp/tools/handoffs.py +112 -0
  18. core/api/mcp/tools/ingest.py +326 -0
  19. core/api/mcp/tools/learnings.py +144 -0
  20. core/api/mcp/tools/projects.py +99 -0
  21. core/api/mcp/tools/pull_requests.py +173 -0
  22. core/api/mcp/tools/safety.py +111 -0
  23. core/api/mcp/tools/search.py +79 -0
  24. core/api/mcp/tools/tasks.py +258 -0
  25. core/api/middleware/__init__.py +0 -0
  26. core/api/middleware/tool_call_audit.py +111 -0
  27. core/api/models/__init__.py +346 -0
  28. core/api/models/auth.py +48 -0
  29. core/api/models/brain.py +1006 -0
  30. core/api/models/common.py +76 -0
  31. core/api/models/costs.py +91 -0
  32. core/api/models/graph.py +66 -0
  33. core/api/models/graph_cosmo.py +125 -0
  34. core/api/models/graph_pr_impact.py +257 -0
  35. core/api/models/graph_ux.py +141 -0
  36. core/api/models/inbox.py +230 -0
  37. core/api/models/ingest_keys.py +108 -0
  38. core/api/models/kg.py +41 -0
  39. core/api/models/llm_config.py +56 -0
  40. core/api/models/monitoring.py +234 -0
  41. core/api/models/projects.py +161 -0
  42. core/api/models/search.py +42 -0
  43. core/api/models/sessions.py +322 -0
  44. core/api/models/tasks.py +184 -0
  45. core/api/models/teams.py +63 -0
  46. core/api/models/users.py +184 -0
  47. core/api/observability/__init__.py +0 -0
  48. core/api/observability/tracing.py +92 -0
  49. core/api/paths.py +26 -0
  50. core/api/rate_limit.py +24 -0
  51. core/api/rbac.py +112 -0
  52. core/api/routers/__init__.py +0 -0
  53. core/api/routers/_adapter.py +24 -0
  54. core/api/routers/admin_pr_impact.py +230 -0
  55. core/api/routers/admin_settings.py +147 -0
  56. core/api/routers/agent.py +1079 -0
  57. core/api/routers/agent_tokens.py +276 -0
  58. core/api/routers/app_settings.py +112 -0
  59. core/api/routers/audit.py +89 -0
  60. core/api/routers/auth.py +586 -0
  61. core/api/routers/bench.py +161 -0
  62. core/api/routers/brain.py +881 -0
  63. core/api/routers/brain_directions.py +527 -0
  64. core/api/routers/ci_checks.py +140 -0
  65. core/api/routers/comments.py +273 -0
  66. core/api/routers/costs.py +148 -0
  67. core/api/routers/docs_coverage.py +217 -0
  68. core/api/routers/docs_governance.py +63 -0
  69. core/api/routers/documents.py +318 -0
  70. core/api/routers/files.py +163 -0
  71. core/api/routers/finder.py +987 -0
  72. core/api/routers/graph.py +836 -0
  73. core/api/routers/handoffs.py +156 -0
  74. core/api/routers/inbox.py +496 -0
  75. core/api/routers/ingest_api_keys.py +205 -0
  76. core/api/routers/ingest_triage.py +1227 -0
  77. core/api/routers/judge.py +306 -0
  78. core/api/routers/kg.py +336 -0
  79. core/api/routers/learnings.py +253 -0
  80. core/api/routers/llm_config.py +130 -0
  81. core/api/routers/monitoring.py +347 -0
  82. core/api/routers/notifications.py +125 -0
  83. core/api/routers/pr_impact.py +315 -0
  84. core/api/routers/projects.py +1061 -0
  85. core/api/routers/pull_requests.py +312 -0
  86. core/api/routers/push.py +67 -0
  87. core/api/routers/raci.py +228 -0
  88. core/api/routers/search.py +125 -0
  89. core/api/routers/sessions.py +3100 -0
  90. core/api/routers/settings.py +90 -0
  91. core/api/routers/share_repo.py +68 -0
  92. core/api/routers/status_updates.py +96 -0
  93. core/api/routers/tags.py +45 -0
  94. core/api/routers/tasks.py +526 -0
  95. core/api/routers/teams.py +425 -0
  96. core/api/routers/terminal.py +105 -0
  97. core/api/routers/users.py +331 -0
  98. core/api/routers/webhooks.py +330 -0
  99. core/api/runtime_settings.py +84 -0
  100. core/api/security.py +652 -0
  101. core/api/services/__init__.py +0 -0
  102. core/api/services/audit.py +58 -0
  103. core/api/services/auto_approval.py +82 -0
  104. core/api/services/brain/__init__.py +52 -0
  105. core/api/services/brain/baseline.py +230 -0
  106. core/api/services/brain/capabilities.py +75 -0
  107. core/api/services/brain/cascade_rollup.py +388 -0
  108. core/api/services/brain/compound_bridge.py +215 -0
  109. core/api/services/brain/cycle.py +1242 -0
  110. core/api/services/brain/cycle_snapshot.py +371 -0
  111. core/api/services/brain/digest_collector.py +147 -0
  112. core/api/services/brain/direction.py +421 -0
  113. core/api/services/brain/drift.py +356 -0
  114. core/api/services/brain/drift_router.py +409 -0
  115. core/api/services/brain/edge_metrics.py +79 -0
  116. core/api/services/brain/events_reader.py +222 -0
  117. core/api/services/brain/findings.py +1379 -0
  118. core/api/services/brain/findings_reader.py +1006 -0
  119. core/api/services/brain/jobs.py +733 -0
  120. core/api/services/brain/journal.py +206 -0
  121. core/api/services/brain/knowledge_forms.py +92 -0
  122. core/api/services/brain/llm/__init__.py +37 -0
  123. core/api/services/brain/llm/_runner.py +62 -0
  124. core/api/services/brain/llm/base.py +70 -0
  125. core/api/services/brain/llm/cache.py +99 -0
  126. core/api/services/brain/llm/constants.py +46 -0
  127. core/api/services/brain/llm/direction_alignment.py +289 -0
  128. core/api/services/brain/llm/factory.py +132 -0
  129. core/api/services/brain/llm/finding_reasoning.py +98 -0
  130. core/api/services/brain/llm/finding_summary.py +92 -0
  131. core/api/services/brain/llm/grounding.py +46 -0
  132. core/api/services/brain/llm/journal_polish.py +96 -0
  133. core/api/services/brain/llm/local_gateway.py +426 -0
  134. core/api/services/brain/llm/parsers.py +71 -0
  135. core/api/services/brain/llm/router_glue.py +422 -0
  136. core/api/services/brain/memory_ops.py +1677 -0
  137. core/api/services/brain/models.py +140 -0
  138. core/api/services/brain/owner_hint.py +211 -0
  139. core/api/services/brain/recap.py +307 -0
  140. core/api/services/brain/rules/__init__.py +65 -0
  141. core/api/services/brain/rules/_signals.py +205 -0
  142. core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
  143. core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
  144. core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
  145. core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
  146. core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
  147. core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
  148. core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
  149. core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
  150. core/api/services/brain/runs_reader.py +485 -0
  151. core/api/services/brain/scope.py +79 -0
  152. core/api/services/brain/sources/__init__.py +46 -0
  153. core/api/services/brain/sources/base.py +86 -0
  154. core/api/services/brain/sources/git_kg.py +393 -0
  155. core/api/services/brain/sources/handoffs.py +157 -0
  156. core/api/services/brain/sources/ingestor.py +130 -0
  157. core/api/services/brain/sources/learnings.py +121 -0
  158. core/api/services/brain/sources/pir_tasks.py +245 -0
  159. core/api/services/brain/watermarks.py +147 -0
  160. core/api/services/brain/ws_emitter.py +170 -0
  161. core/api/services/cc_tasks_reader.py +76 -0
  162. core/api/services/ci_service.py +263 -0
  163. core/api/services/claude_metrics.py +796 -0
  164. core/api/services/codex_metrics.py +364 -0
  165. core/api/services/conversation_reader.py +102 -0
  166. core/api/services/cost_service.py +243 -0
  167. core/api/services/crypto.py +147 -0
  168. core/api/services/docs_governance/__init__.py +1 -0
  169. core/api/services/docs_governance/confidence.py +230 -0
  170. core/api/services/docs_governance/config.py +87 -0
  171. core/api/services/docs_governance/enrichment.py +65 -0
  172. core/api/services/docs_governance/frontmatter_validator.py +83 -0
  173. core/api/services/docs_governance/hard_gates.py +221 -0
  174. core/api/services/docs_governance/triage_orchestrator.py +98 -0
  175. core/api/services/embedding_internal.py +395 -0
  176. core/api/services/embedding_service.py +832 -0
  177. core/api/services/event_dispatcher.py +167 -0
  178. core/api/services/events.py +70 -0
  179. core/api/services/git_ops.py +621 -0
  180. core/api/services/graph_cosmo_service.py +440 -0
  181. core/api/services/graph_ranker.py +306 -0
  182. core/api/services/graph_service.py +1589 -0
  183. core/api/services/inbox.py +800 -0
  184. core/api/services/inbox_digest.py +221 -0
  185. core/api/services/inbox_digest_deep_research.py +80 -0
  186. core/api/services/inbox_digest_jobs.py +595 -0
  187. core/api/services/inbox_gmail_sync.py +167 -0
  188. core/api/services/inbox_llm_classifier.py +906 -0
  189. core/api/services/inbox_source_identity.py +116 -0
  190. core/api/services/inbox_sources.py +456 -0
  191. core/api/services/inbox_taxonomy.py +195 -0
  192. core/api/services/inbox_tldr.py +1079 -0
  193. core/api/services/inbox_triage.py +899 -0
  194. core/api/services/ingest/__init__.py +13 -0
  195. core/api/services/ingest/api_key_auth.py +136 -0
  196. core/api/services/ingest/auto_approve.py +120 -0
  197. core/api/services/ingest/classifier.py +138 -0
  198. core/api/services/ingest/confidence.py +173 -0
  199. core/api/services/ingest/dispatch.py +88 -0
  200. core/api/services/ingest/embedding_router.py +272 -0
  201. core/api/services/ingest/events.py +33 -0
  202. core/api/services/ingest/ignore_patterns.py +79 -0
  203. core/api/services/ingest/image_probe.py +218 -0
  204. core/api/services/ingest/ingress.py +263 -0
  205. core/api/services/ingest/insert_saga.py +793 -0
  206. core/api/services/ingest/llm/__init__.py +13 -0
  207. core/api/services/ingest/llm/anthropic_haiku.py +23 -0
  208. core/api/services/ingest/llm/base.py +59 -0
  209. core/api/services/ingest/llm/byok_provider.py +130 -0
  210. core/api/services/ingest/llm/classification_context.py +301 -0
  211. core/api/services/ingest/llm/config_store.py +246 -0
  212. core/api/services/ingest/llm/factory.py +24 -0
  213. core/api/services/ingest/llm/kg_enricher.py +306 -0
  214. core/api/services/ingest/llm/local_gateway.py +821 -0
  215. core/api/services/ingest/llm/local_vllm.py +23 -0
  216. core/api/services/ingest/llm/openai_nano.py +349 -0
  217. core/api/services/ingest/lock_advisory.py +57 -0
  218. core/api/services/ingest/parser_router.py +1756 -0
  219. core/api/services/ingest/parsers/__init__.py +1 -0
  220. core/api/services/ingest/parsers/docling_parser.py +142 -0
  221. core/api/services/ingest/parsers/docparse_gateway.py +178 -0
  222. core/api/services/ingest/parsers/docx_parser.py +127 -0
  223. core/api/services/ingest/parsers/folder_unpacker.py +85 -0
  224. core/api/services/ingest/parsers/gateway_aux.py +147 -0
  225. core/api/services/ingest/parsers/image_parser.py +251 -0
  226. core/api/services/ingest/parsers/internal_markdown.py +89 -0
  227. core/api/services/ingest/parsers/ocr_gateway.py +117 -0
  228. core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
  229. core/api/services/ingest/parsers/pdf_types.py +13 -0
  230. core/api/services/ingest/parsers/transcript_parser.py +445 -0
  231. core/api/services/ingest/parsers/vision_gateway.py +186 -0
  232. core/api/services/ingest/parsers/xlsx_parser.py +91 -0
  233. core/api/services/ingest/parsers/zip_unpacker.py +126 -0
  234. core/api/services/ingest/preflight.py +393 -0
  235. core/api/services/ingest/retry_voyage.py +88 -0
  236. core/api/services/ingest/routing_policy.py +307 -0
  237. core/api/services/ingest/serializers/__init__.py +1 -0
  238. core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
  239. core/api/services/ingest/skip_log.py +74 -0
  240. core/api/services/ingest/watcher.py +637 -0
  241. core/api/services/kg/__init__.py +0 -0
  242. core/api/services/kg/audit.py +49 -0
  243. core/api/services/kg/hybrid_search.py +691 -0
  244. core/api/services/kg/lens.py +339 -0
  245. core/api/services/kg/pr_impact.py +770 -0
  246. core/api/services/kg/queries.py +152 -0
  247. core/api/services/kg/ranking.py +89 -0
  248. core/api/services/kg/rrf.py +143 -0
  249. core/api/services/kg_watcher_control.py +161 -0
  250. core/api/services/local_llm/__init__.py +19 -0
  251. core/api/services/local_llm/async_client.py +385 -0
  252. core/api/services/local_llm/client.py +173 -0
  253. core/api/services/local_llm/url_validator.py +44 -0
  254. core/api/services/metrics_collector.py +646 -0
  255. core/api/services/metrics_providers.py +65 -0
  256. core/api/services/model_registry.py +266 -0
  257. core/api/services/model_router.py +137 -0
  258. core/api/services/n8n_client.py +77 -0
  259. core/api/services/newsletter_llm_gateway.py +66 -0
  260. core/api/services/notification_service.py +134 -0
  261. core/api/services/openai_responses.py +55 -0
  262. core/api/services/opencode_metrics.py +375 -0
  263. core/api/services/opencode_sessions.py +173 -0
  264. core/api/services/pii_redactor.py +138 -0
  265. core/api/services/pr_impact_pipeline/__init__.py +21 -0
  266. core/api/services/pr_impact_pipeline/differ.py +421 -0
  267. core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
  268. core/api/services/pr_impact_pipeline/gc.py +93 -0
  269. core/api/services/pr_impact_pipeline/languages.py +192 -0
  270. core/api/services/pr_impact_pipeline/parser.py +178 -0
  271. core/api/services/pr_impact_pipeline/writer.py +394 -0
  272. core/api/services/pr_service.py +1393 -0
  273. core/api/services/project_paths.py +70 -0
  274. core/api/services/project_status_updates.py +265 -0
  275. core/api/services/providers.py +276 -0
  276. core/api/services/push_service.py +170 -0
  277. core/api/services/reminder_service.py +89 -0
  278. core/api/services/runas.py +41 -0
  279. core/api/services/salience_service.py +69 -0
  280. core/api/services/security_collector.py +281 -0
  281. core/api/services/session_catalog.py +385 -0
  282. core/api/services/session_metrics_service.py +301 -0
  283. core/api/services/session_ops.py +272 -0
  284. core/api/services/session_state.py +173 -0
  285. core/api/services/share_links.py +222 -0
  286. core/api/services/task_transitions.py +146 -0
  287. core/api/services/terminal_metrics.py +462 -0
  288. core/api/services/terminal_metrics_dump.py +203 -0
  289. core/api/services/tmux.py +1205 -0
  290. core/api/services/webhook_service.py +422 -0
  291. core/api/services/workspace_sync.py +164 -0
  292. core/api/templates/__init__.py +1 -0
  293. core/api/templates/markdown_share.py +164 -0
  294. core/api/terminal.py +1031 -0
  295. core/api/tests/__init__.py +0 -0
  296. core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
  297. core/api/tests/test_audit_permissions.py +133 -0
  298. core/api/tests/test_backfill_session_conversations.py +90 -0
  299. core/api/tests/test_backfill_working_seconds_msg.py +129 -0
  300. core/api/tests/test_claude_metrics.py +326 -0
  301. core/api/tests/test_codex_metrics.py +189 -0
  302. core/api/tests/test_finder_paths.py +74 -0
  303. core/api/tests/test_git_ops_merge.py +155 -0
  304. core/api/tests/test_learnings_check_search.py +81 -0
  305. core/api/tests/test_metrics_providers.py +133 -0
  306. core/api/tests/test_migration_087.py +164 -0
  307. core/api/tests/test_migration_088.py +94 -0
  308. core/api/tests/test_migration_089.py +116 -0
  309. core/api/tests/test_openai_responses.py +24 -0
  310. core/api/tests/test_opencode_metrics.py +740 -0
  311. core/api/tests/test_opencode_sessions.py +321 -0
  312. core/api/tests/test_pr_workflow_e2e.py +457 -0
  313. core/api/tests/test_projects_handoffs.py +31 -0
  314. core/api/tests/test_providers.py +138 -0
  315. core/api/tests/test_safety_bridge.py +347 -0
  316. core/api/tests/test_session_catalog.py +142 -0
  317. core/api/tests/test_session_conversations.py +512 -0
  318. core/api/tests/test_session_metrics_service.py +270 -0
  319. core/api/tests/test_session_resume_paths.py +548 -0
  320. core/api/tests/test_session_theme_mode_migration.py +56 -0
  321. core/api/tests/test_sessions_rbac.py +131 -0
  322. core/api/tests/test_share_edit.py +398 -0
  323. core/api/tests/test_share_repo.py +200 -0
  324. core/api/tests/test_terminal_session_manager.py +98 -0
  325. core/api/tests/test_terminal_upload.py +34 -0
  326. core/api/tests/test_tmux.py +272 -0
  327. core/api/tests/test_workspace_sync.py +186 -0
  328. core/api/tests/test_ws_ticket_in_memory.py +73 -0
  329. core/api/use_cases/__init__.py +11 -0
  330. core/api/use_cases/_context.py +89 -0
  331. core/api/use_cases/_errors.py +62 -0
  332. core/api/use_cases/_roles.py +16 -0
  333. core/api/use_cases/audit.py +171 -0
  334. core/api/use_cases/brain.py +1232 -0
  335. core/api/use_cases/costs.py +249 -0
  336. core/api/use_cases/graph.py +1153 -0
  337. core/api/use_cases/handoffs.py +506 -0
  338. core/api/use_cases/ingest_triage.py +1229 -0
  339. core/api/use_cases/learnings.py +538 -0
  340. core/api/use_cases/projects.py +705 -0
  341. core/api/use_cases/pull_requests.py +415 -0
  342. core/api/use_cases/search.py +926 -0
  343. core/api/use_cases/tasks.py +1495 -0
  344. core/api/visibility.py +141 -0
  345. core/cli/__init__.py +5 -0
  346. core/cli/_index_source.py +632 -0
  347. core/cli/_runtime_ctx.py +160 -0
  348. core/cli/_transmute.py +241 -0
  349. core/cli/marvis_doctor.py +704 -0
  350. core/cli/marvis_feedback.py +396 -0
  351. core/cli/marvis_governance.py +315 -0
  352. core/cli/marvis_hooks.py +515 -0
  353. core/cli/marvis_init.py +757 -0
  354. core/cli/marvis_mcp.py +401 -0
  355. core/cli/marvis_runtime.py +855 -0
  356. core/cli/marvis_telemetry.py +228 -0
  357. core/scripts/_drift_check.py +716 -0
  358. core/scripts/_frontmatter.py +66 -0
  359. core/scripts/_graph_writer.py +189 -0
  360. core/scripts/ast_parser.py +1553 -0
  361. core/scripts/install_hooks/__init__.py +1 -0
  362. core/scripts/install_hooks/_config.sh +109 -0
  363. core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
  364. core/scripts/install_hooks/block-db-direct-write.sh +23 -0
  365. core/scripts/install_hooks/block-push-no-task.sh +23 -0
  366. core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
  367. core/scripts/install_hooks/block-subtree-push.sh +23 -0
  368. core/scripts/install_hooks/config.json +53 -0
  369. core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
  370. core/scripts/install_hooks/enforce-worktree.sh +23 -0
  371. core/scripts/install_hooks/quality-gate.sh +170 -0
  372. core/scripts/install_hooks/safety_bridge.py +968 -0
  373. core/scripts/install_hooks/secret-scan.sh +23 -0
  374. core/scripts/migrate_spike_node_ids.py +122 -0
  375. core/scripts/populate_artifacts.py +2198 -0
  376. core/scripts/populate_cross_project.py +2457 -0
  377. core/scripts/populate_inbox_nodes.py +357 -0
  378. core/scripts/populate_pr_impact.py +267 -0
  379. core/scripts/populate_project_nodes.py +603 -0
  380. core/scripts/populate_touch_counter.py +337 -0
  381. core/scripts/reparse_failed.py +57 -0
  382. core/scripts/safety_bridge.py +968 -0
  383. core/telemetry/__init__.py +9 -0
  384. core/telemetry/client.py +405 -0
  385. core/telemetry/schema.py +122 -0
  386. core/wizard/__init__.py +65 -0
  387. core/wizard/byok_vault.py +147 -0
  388. core/wizard/defaults.py +58 -0
  389. core/wizard/state.py +117 -0
  390. core/wizard/steps.py +70 -0
  391. core/wizard/validation.py +136 -0
  392. marvisx_cli-0.1.0.dist-info/METADATA +201 -0
  393. marvisx_cli-0.1.0.dist-info/RECORD +587 -0
  394. marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
  395. marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
  396. marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
  397. marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
  398. migrations/001_initial.sql +33 -0
  399. migrations/002_tasks.sql +30 -0
  400. migrations/003_session_management.sql +7 -0
  401. migrations/004_projects_comments.sql +65 -0
  402. migrations/005_session_intelligence.sql +15 -0
  403. migrations/006_settings.sql +12 -0
  404. migrations/007_task_scoring.sql +12 -0
  405. migrations/008_cost_tracking.sql +31 -0
  406. migrations/009_session_card_metrics.sql +3 -0
  407. migrations/010_monitoring.sql +55 -0
  408. migrations/012_agent_api.sql +21 -0
  409. migrations/013_session_complete.sql +8 -0
  410. migrations/015_pull_requests.sql +43 -0
  411. migrations/015_pull_requests_down.sql +5 -0
  412. migrations/016_users_raci.sql +116 -0
  413. migrations/017_task_cost_entries.sql +87 -0
  414. migrations/018_agents.sql +73 -0
  415. migrations/018_agents_down.sql +13 -0
  416. migrations/019_review_feedback.sql +18 -0
  417. migrations/020_pr_commit_sha.sql +4 -0
  418. migrations/021_webhook_events.sql +18 -0
  419. migrations/022_devx_agent_managed.sql +11 -0
  420. migrations/022_devx_agent_managed_down.sql +6 -0
  421. migrations/023_devx_p1_gate.sql +7 -0
  422. migrations/023_devx_p1_gate_down.sql +3 -0
  423. migrations/024_chat_messages.sql +16 -0
  424. migrations/024_pr_conversation_id.sql +8 -0
  425. migrations/024_task_indexes.sql +21 -0
  426. migrations/024_task_indexes_down.sql +7 -0
  427. migrations/025_audit_log.sql +17 -0
  428. migrations/026_agent_tokens.sql +20 -0
  429. migrations/027_teams_auth_phase_b.sql +35 -0
  430. migrations/028_learnings.sql +23 -0
  431. migrations/029_team_roles.sql +14 -0
  432. migrations/030_finder_pins.sql +10 -0
  433. migrations/031_pr_deploy_status.sql +9 -0
  434. migrations/032_task_reminders.sql +7 -0
  435. migrations/033_events_retry_count.sql +6 -0
  436. migrations/033_session_owner.sql +9 -0
  437. migrations/034_notifications.sql +38 -0
  438. migrations/035_shared_links.sql +15 -0
  439. migrations/036_session_index_upgrade.sql +29 -0
  440. migrations/037_pr_approval.sql +15 -0
  441. migrations/038_pr_submitted_by.sql +6 -0
  442. migrations/039_push_subscriptions.sql +17 -0
  443. migrations/040_semantic_search.sql +16 -0
  444. migrations/041_workspaces.sql +63 -0
  445. migrations/042_oidc_providers.sql +24 -0
  446. migrations/043_ci_checks.sql +31 -0
  447. migrations/044_agent_metrics.sql +30 -0
  448. migrations/045_documents_doc_type.sql +5 -0
  449. migrations/046_salience.sql +13 -0
  450. migrations/047_seed_missing_agents.sql +6 -0
  451. migrations/048_fix_agent_paths_roles.sql +5 -0
  452. migrations/049_agent_role_and_learnings_schema.sql +3 -0
  453. migrations/050_session_provider.sql +2 -0
  454. migrations/051_session_launch_profile.sql +4 -0
  455. migrations/052_session_theme_mode.sql +2 -0
  456. migrations/052_task_kind.sql +4 -0
  457. migrations/053_inbox_items.sql +31 -0
  458. migrations/054_inbox_triage_contract.sql +30 -0
  459. migrations/055_inbox_topic_treatment.sql +12 -0
  460. migrations/056_inbox_treatment_read_save.sql +57 -0
  461. migrations/057_session_theme_mode_backfill.sql +4 -0
  462. migrations/058_inbox_item_status_lifecycle.sql +13 -0
  463. migrations/059_inbox_tldr_and_source_scores.sql +18 -0
  464. migrations/060_newsletter.sql +16 -0
  465. migrations/061_inbox_redesign.sql +69 -0
  466. migrations/062_fix_inbox_sources_backfill.sql +37 -0
  467. migrations/063_task_completion_mode.sql +23 -0
  468. migrations/064_judge_mode_setting.sql +4 -0
  469. migrations/065_knowledge_graph_spike.sql +40 -0
  470. migrations/066_digest_ranking_inputs.sql +10 -0
  471. migrations/066_kg_artifact_nodes.sql +129 -0
  472. migrations/067_inbox_digest_selections.sql +28 -0
  473. migrations/067_kg_temporal.sql +53 -0
  474. migrations/068_inbox_digest_app_settings.sql +9 -0
  475. migrations/068_kg_touch_counter.sql +52 -0
  476. migrations/069_kg_doc_types.sql +117 -0
  477. migrations/070_digest_ranking_inputs_recovery.sql +3 -0
  478. migrations/071_inbox_digest_selections_recovery.sql +3 -0
  479. migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
  480. migrations/073_kg_cross_project.sql +216 -0
  481. migrations/073_kg_cross_project_down.sql +77 -0
  482. migrations/074_kg_infra_types.sql +208 -0
  483. migrations/074_kg_infra_types_down.sql +80 -0
  484. migrations/075_kg_file_state_recovery.sql +35 -0
  485. migrations/075_kg_file_state_recovery_down.sql +5 -0
  486. migrations/076_kg_watcher_state.sql +33 -0
  487. migrations/076_kg_watcher_state_down.sql +3 -0
  488. migrations/077_kg_doc_types_extend.sql +226 -0
  489. migrations/077_kg_doc_types_extend_down.sql +80 -0
  490. migrations/078_kg_fts5.sql +102 -0
  491. migrations/078_kg_fts5_down.sql +14 -0
  492. migrations/079_kg_missing_indexes.sql +31 -0
  493. migrations/079_kg_missing_indexes_down.sql +10 -0
  494. migrations/080_kg_fts5_extended.sql +232 -0
  495. migrations/080_kg_fts5_extended_down.sql +25 -0
  496. migrations/081_kg_lens_indexes.sql +9 -0
  497. migrations/081_kg_lens_indexes_down.sql +3 -0
  498. migrations/082_kg_pins.sql +26 -0
  499. migrations/082_kg_pins_down.sql +14 -0
  500. migrations/083_kg_graph_nodes_degree.sql +20 -0
  501. migrations/083_kg_graph_nodes_degree_down.sql +15 -0
  502. migrations/084_drop_legacy_scheduler_tables.sql +58 -0
  503. migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
  504. migrations/085_kg_edge_resolves_to.sql +142 -0
  505. migrations/085_kg_edge_resolves_to_down.sql +66 -0
  506. migrations/086_project_status_updates_feed.sql +20 -0
  507. migrations/086_project_status_updates_feed_down.sql +36 -0
  508. migrations/087_session_metrics_dual.sql +50 -0
  509. migrations/087_session_metrics_dual_down.sql +21 -0
  510. migrations/088_rename_context_pct_legacy.sql +23 -0
  511. migrations/088_rename_context_pct_legacy_down.sql +8 -0
  512. migrations/089_session_metrics_equivalent_cost.sql +26 -0
  513. migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
  514. migrations/090_kg_inbox_node_type.sql +26 -0
  515. migrations/090_kg_inbox_node_type_down.sql +20 -0
  516. migrations/091_kg_inbox_node_type_check.sql +265 -0
  517. migrations/091_kg_inbox_node_type_check_down.sql +129 -0
  518. migrations/092_sessions_activity_state_ts.sql +29 -0
  519. migrations/092_sessions_activity_state_ts_down.sql +14 -0
  520. migrations/093_sessions_activity_state_column.sql +29 -0
  521. migrations/093_sessions_activity_state_column_down.sql +10 -0
  522. migrations/094_ingest_pending.sql +55 -0
  523. migrations/094_ingest_pending_down.sql +15 -0
  524. migrations/095_kg_intent_first.sql +77 -0
  525. migrations/095_kg_intent_first_down.sql +25 -0
  526. migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
  527. migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
  528. migrations/097_ingest_change_history.sql +37 -0
  529. migrations/097_ingest_change_history_down.sql +13 -0
  530. migrations/098_kg_node_type_business.sql +254 -0
  531. migrations/098_kg_node_type_business_down.sql +195 -0
  532. migrations/099_kg_edges_restore_weight.sql +58 -0
  533. migrations/099_kg_edges_restore_weight_down.sql +12 -0
  534. migrations/100_kg_enriched_at.sql +25 -0
  535. migrations/100_kg_enriched_at_down.sql +12 -0
  536. migrations/101_local_llm_shadow_comparisons.sql +66 -0
  537. migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
  538. migrations/102_promote_llm_costs.sql +69 -0
  539. migrations/102_promote_llm_costs_down.sql +19 -0
  540. migrations/103_ingest_skipped_log.sql +46 -0
  541. migrations/103_ingest_skipped_log_down.sql +15 -0
  542. migrations/120_docs_governance.sql +50 -0
  543. migrations/120_docs_governance_down.sql +11 -0
  544. migrations/121_notification_event_fk_cleanup.sql +21 -0
  545. migrations/121_notification_event_fk_cleanup_down.sql +10 -0
  546. migrations/122_docs_drift_history.sql +34 -0
  547. migrations/122_docs_drift_history_down.sql +15 -0
  548. migrations/123_ingest_parser_waiting_status.sql +69 -0
  549. migrations/123_ingest_parser_waiting_status_down.sql +69 -0
  550. migrations/124_heypocket_recordings.sql +63 -0
  551. migrations/124_heypocket_recordings_down.sql +13 -0
  552. migrations/125_kg_node_type_record.sql +219 -0
  553. migrations/125_kg_node_type_record_down.sql +205 -0
  554. migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
  555. migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
  556. migrations/127_brain_v1_substrate.sql +200 -0
  557. migrations/127_brain_v1_substrate_down.sql +32 -0
  558. migrations/128_brain_drift_signals.sql +157 -0
  559. migrations/128_brain_drift_signals_down.sql +23 -0
  560. migrations/129_brain_memory_operations.sql +232 -0
  561. migrations/129_brain_memory_operations_down.sql +27 -0
  562. migrations/130_brain_findings.sql +258 -0
  563. migrations/130_brain_findings_down.sql +29 -0
  564. migrations/132_kg_pr_modifies.sql +242 -0
  565. migrations/132_kg_pr_modifies_down.sql +99 -0
  566. migrations/133_brain_v1_2_direction_schema.sql +476 -0
  567. migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
  568. migrations/134_brain_journal_narrative_polished.sql +8 -0
  569. migrations/134_brain_journal_narrative_polished_down.sql +6 -0
  570. migrations/135_kg_edges_provider.sql +21 -0
  571. migrations/136_documents_fts.sql +56 -0
  572. migrations/137_promote_llm_costs.sql +59 -0
  573. migrations/137_promote_llm_costs_down.sql +19 -0
  574. migrations/138_ingest_api_keys.sql +39 -0
  575. migrations/138_ingest_api_keys_down.sql +8 -0
  576. migrations/139_ingest_pending_ingress.sql +91 -0
  577. migrations/139_ingest_pending_ingress_down.sql +73 -0
  578. migrations/140_ingest_idempotency_quota.sql +45 -0
  579. migrations/140_ingest_idempotency_quota_down.sql +9 -0
  580. migrations/141_ingest_pending_metadata.sql +16 -0
  581. migrations/141_ingest_pending_metadata_down.sql +7 -0
  582. migrations/142_llm_function_config.sql +36 -0
  583. migrations/142_llm_function_config_down.sql +8 -0
  584. migrations/143_kg_code_embeddings.sql +25 -0
  585. migrations/143_kg_code_embeddings_down.sql +5 -0
  586. migrations/__init__.py +4 -0
  587. projects/_template/project.yaml +46 -0
@@ -0,0 +1,99 @@
1
+ # DR5 — Procedure / Playbook Changed (sub-02 §5 DR5, CE4 axis=context).
2
+ # If a handoff_changed/learning_changed event's summary matches the compiled
3
+ # procedure regex AND no playbook/guide artifact updated in the same cycle,
4
+ # emit `playbook_changed`.
5
+ #
6
+ # CycleSnapshot pre-filters procedure_keyword_hits — scan ONLY summary text
7
+ # of digest events. NEVER read handoff files from disk (anti-pattern).
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime
11
+
12
+ from core.api.models.brain import DriftSignal
13
+ from core.api.services.brain.cycle_snapshot import CycleSnapshot
14
+ from core.api.services.brain.rules._signals import build_signal
15
+
16
+
17
+ _ELIGIBLE_EVENT_TYPES = frozenset({"handoff_changed", "learning_changed"})
18
+
19
+
20
+ def _has_playbook_artifact_updated(snapshot: CycleSnapshot) -> bool:
21
+ """Check if any doc_changed event in this cycle is a playbook/guide."""
22
+ for ev in snapshot.by_event_type.get("doc_changed", []):
23
+ ref = ev.source_ref.lower()
24
+ if "playbook" in ref or "guide" in ref:
25
+ return True
26
+ if isinstance(ev.evidence, dict):
27
+ kind = ev.evidence.get("kind")
28
+ if isinstance(kind, str) and kind in {"playbook", "guide"}:
29
+ return True
30
+ return False
31
+
32
+
33
+ async def build_signals(
34
+ snapshot: CycleSnapshot, *, run_id: str, now: datetime
35
+ ) -> list[DriftSignal]:
36
+ signals: list[DriftSignal] = []
37
+ if not snapshot.procedure_keyword_hits:
38
+ return signals
39
+ playbook_updated = _has_playbook_artifact_updated(snapshot)
40
+ if playbook_updated:
41
+ return signals
42
+
43
+ seen_refs: set[str] = set()
44
+ for event in snapshot.procedure_keyword_hits:
45
+ if event.event_type not in _ELIGIBLE_EVENT_TYPES:
46
+ continue
47
+ if event.source_ref in seen_refs:
48
+ continue
49
+ seen_refs.add(event.source_ref)
50
+
51
+ if event.source_project:
52
+ scope_type, scope_key = "project", event.source_project
53
+ elif event.program_key:
54
+ scope_type, scope_key = "program", event.program_key
55
+ else:
56
+ scope_type, scope_key = "company", "__company__"
57
+
58
+ involved_projects = sorted(
59
+ {
60
+ p
61
+ for p in (event.source_project, event.target_project)
62
+ if isinstance(p, str) and p
63
+ }
64
+ )
65
+ modifiers = 0
66
+ if len(involved_projects) > 1:
67
+ modifiers += 1
68
+
69
+ observed_ref = f"event:{event.event_id}"
70
+ observed_delta = (
71
+ f"Procedure-change summary in {event.source_ref} ({event.event_type}) "
72
+ f"without matching playbook/guide update."
73
+ )
74
+ signals.append(
75
+ build_signal(
76
+ run_id=run_id,
77
+ cycle_key=snapshot.cycle_key,
78
+ detected_at=now,
79
+ rule_id="DR5",
80
+ scope_type=scope_type, # type: ignore[arg-type]
81
+ scope_key=scope_key,
82
+ program_key=event.program_key,
83
+ signal_type="playbook_changed",
84
+ expected_direction_source="doc",
85
+ expected_direction_ref=None,
86
+ observed_direction_ref=observed_ref,
87
+ observed_delta=observed_delta,
88
+ evidence_refs=[observed_ref],
89
+ severity_base="high",
90
+ drift_axis="context",
91
+ involved_projects=involved_projects,
92
+ severity_modifiers=modifiers,
93
+ observed_event=event,
94
+ )
95
+ )
96
+ return signals
97
+
98
+
99
+ __all__ = ["build_signals"]
@@ -0,0 +1,100 @@
1
+ # DR6 — External Update Unpropagated (sub-02 §5 DR6, CE4 axis=context).
2
+ # If `external_update_seen` event has no linked internal artifact within
3
+ # `brain_drift_propagation_window_cycles` (default 3), emit
4
+ # `external_update_unpropagated`. Severity escalates per missed cycle.
5
+ #
6
+ # v1.01 note: external_update_seen producer is deferred (sub-01 §3). DR6 will
7
+ # be a no-op until that producer ships — tests treat it accordingly.
8
+ from __future__ import annotations
9
+
10
+ from datetime import datetime
11
+ from typing import Any
12
+
13
+ from core.api.models.brain import DriftSignal, Severity
14
+ from core.api.services.brain.cycle_snapshot import CycleSnapshot, DigestEventRow
15
+ from core.api.services.brain.rules._signals import build_signal
16
+
17
+ # Severity ladder: medium → high @ 2 missed cycles → critical @ 3+ (max).
18
+ def _severity_for_missed(missed: int) -> Severity:
19
+ if missed >= 3:
20
+ return "critical"
21
+ if missed >= 2:
22
+ return "high"
23
+ return "medium"
24
+
25
+
26
+ def _propagation_linked(event: DigestEventRow) -> bool:
27
+ if not isinstance(event.evidence, dict):
28
+ return False
29
+ if event.evidence.get("propagation_link_ref"):
30
+ return True
31
+ linked = event.evidence.get("linked_artifacts")
32
+ if isinstance(linked, list) and linked:
33
+ return True
34
+ return False
35
+
36
+
37
+ def _scope(event: DigestEventRow) -> tuple[str, str]:
38
+ if event.source_project:
39
+ return ("project", event.source_project)
40
+ if event.program_key:
41
+ return ("program", event.program_key)
42
+ return ("company", "__company__")
43
+
44
+
45
+ async def build_signals(
46
+ snapshot: CycleSnapshot, *, run_id: str, now: datetime
47
+ ) -> list[DriftSignal]:
48
+ signals: list[DriftSignal] = []
49
+ seen_refs: set[str] = set()
50
+ for event in snapshot.external_update_events:
51
+ if _propagation_linked(event):
52
+ continue
53
+ if event.source_ref in seen_refs:
54
+ continue
55
+ seen_refs.add(event.source_ref)
56
+
57
+ scope_type, scope_key = _scope(event)
58
+ observed_ref = f"event:{event.event_id}"
59
+ # Missed cycles: derived from evidence if available, else default 1.
60
+ missed = 1
61
+ if isinstance(event.evidence, dict):
62
+ raw = event.evidence.get("missed_cycles")
63
+ if isinstance(raw, int) and raw > 0:
64
+ missed = min(raw, 5)
65
+ severity_base = _severity_for_missed(missed)
66
+ observed_delta = (
67
+ f"External update {event.source_ref} observed without internal "
68
+ f"propagation (missed {missed} cycle(s))."
69
+ )
70
+ signals.append(
71
+ build_signal(
72
+ run_id=run_id,
73
+ cycle_key=snapshot.cycle_key,
74
+ detected_at=now,
75
+ rule_id="DR6",
76
+ scope_type=scope_type, # type: ignore[arg-type]
77
+ scope_key=scope_key,
78
+ program_key=event.program_key,
79
+ signal_type="external_update_unpropagated",
80
+ expected_direction_source="none",
81
+ expected_direction_ref=None,
82
+ observed_direction_ref=observed_ref,
83
+ observed_delta=observed_delta,
84
+ evidence_refs=[observed_ref],
85
+ severity_base=severity_base,
86
+ drift_axis="context",
87
+ involved_projects=sorted(
88
+ {
89
+ p
90
+ for p in (event.source_project, event.target_project)
91
+ if isinstance(p, str) and p
92
+ }
93
+ ),
94
+ observed_event=event,
95
+ )
96
+ )
97
+ return signals
98
+
99
+
100
+ __all__ = ["build_signals"]
@@ -0,0 +1,123 @@
1
+ # DR7 — Claimed Decision Gap (sub-02 §5 DR7, CE4 axis=intent).
2
+ # If an event has decision_marker in {approved, signed_off} AND no matching
3
+ # audit_log row with actor_user_id IS NOT NULL referencing the same artifact,
4
+ # emit `claimed_decision_gap`. Severity high; critical if claim is external.
5
+ #
6
+ # Cross-reference: rogue-agent-creates-task-and-PR learning (2026-05-12).
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from datetime import datetime
11
+
12
+ from core.api.db import acquire_db
13
+ from core.api.models.brain import DriftSignal
14
+ from core.api.services.brain.cycle_snapshot import CycleSnapshot, DigestEventRow
15
+ from core.api.services.brain.rules._signals import build_signal
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ _CLAIM_MARKERS = frozenset({"approved", "signed_off"})
20
+
21
+
22
+ async def _audit_lookup(source_refs: list[str]) -> set[str]:
23
+ """Return the subset of source_refs that have a human-actor audit row."""
24
+ if not source_refs:
25
+ return set()
26
+ placeholders = ",".join("?" for _ in source_refs)
27
+ try:
28
+ async with acquire_db() as db:
29
+ cursor = await db.execute(
30
+ f"SELECT DISTINCT entity_ref FROM audit_log "
31
+ f"WHERE entity_ref IN ({placeholders}) "
32
+ f"AND actor_user_id IS NOT NULL",
33
+ source_refs,
34
+ )
35
+ rows = await cursor.fetchall()
36
+ except Exception as exc: # noqa: BLE001 — table may not exist
37
+ logger.debug("DR7: audit_log lookup skipped (%s)", exc)
38
+ return set()
39
+ return {row[0] for row in rows}
40
+
41
+
42
+ def _scope(event: DigestEventRow) -> tuple[str, str]:
43
+ if event.source_project:
44
+ return ("project", event.source_project)
45
+ if event.program_key:
46
+ return ("program", event.program_key)
47
+ return ("company", "__company__")
48
+
49
+
50
+ async def build_signals(
51
+ snapshot: CycleSnapshot, *, run_id: str, now: datetime
52
+ ) -> list[DriftSignal]:
53
+ candidates: list[tuple[DigestEventRow, str]] = []
54
+ for event in snapshot.decision_marker_events:
55
+ marker = event.evidence.get("decision_marker") if isinstance(event.evidence, dict) else None
56
+ if not isinstance(marker, str) or marker not in _CLAIM_MARKERS:
57
+ continue
58
+ candidates.append((event, event.source_ref))
59
+ if not candidates:
60
+ return []
61
+
62
+ # Batched lookup (chunked IN, max 500 per chunk — §5 DR7).
63
+ refs = [src_ref for _, src_ref in candidates]
64
+ matched: set[str] = set()
65
+ chunk = 500
66
+ for i in range(0, len(refs), chunk):
67
+ matched |= await _audit_lookup(refs[i : i + chunk])
68
+
69
+ signals: list[DriftSignal] = []
70
+ seen: set[str] = set()
71
+ for event, src_ref in candidates:
72
+ if src_ref in matched:
73
+ continue
74
+ if src_ref in seen:
75
+ continue
76
+ seen.add(src_ref)
77
+ scope_type, scope_key = _scope(event)
78
+ observed_ref = f"event:{event.event_id}"
79
+ marker = (
80
+ event.evidence.get("decision_marker")
81
+ if isinstance(event.evidence, dict)
82
+ else "approved"
83
+ )
84
+ observed_delta = (
85
+ f"Decision claim '{marker}' on {src_ref} with no human actor in audit_log."
86
+ )
87
+ external = bool(
88
+ isinstance(event.evidence, dict)
89
+ and event.evidence.get("claimed_by_external")
90
+ )
91
+ severity_base = "critical" if external else "high"
92
+ signals.append(
93
+ build_signal(
94
+ run_id=run_id,
95
+ cycle_key=snapshot.cycle_key,
96
+ detected_at=now,
97
+ rule_id="DR7",
98
+ scope_type=scope_type, # type: ignore[arg-type]
99
+ scope_key=scope_key,
100
+ program_key=event.program_key,
101
+ signal_type="claimed_decision_gap",
102
+ expected_direction_source="none",
103
+ expected_direction_ref=None,
104
+ observed_direction_ref=observed_ref,
105
+ observed_delta=observed_delta,
106
+ evidence_refs=[observed_ref, f"audit_query:{src_ref}"],
107
+ severity_base=severity_base,
108
+ drift_axis="intent",
109
+ involved_projects=sorted(
110
+ {
111
+ p
112
+ for p in (event.source_project, event.target_project)
113
+ if isinstance(p, str) and p
114
+ }
115
+ ),
116
+ observed_event=event,
117
+ decision_marker=marker if isinstance(marker, str) else None,
118
+ )
119
+ )
120
+ return signals
121
+
122
+
123
+ __all__ = ["build_signals"]
@@ -0,0 +1,230 @@
1
+ # DR8 — Direction Misalignment (Brain v1.2, signal_type=direction_misalignment,
2
+ # axis=intent).
3
+ #
4
+ # Compares the project's declared direction (project_directions.summary +
5
+ # out_of_scope) against the events observed in the cycle. Emits a drift signal
6
+ # when the LLM classifier (tier-fast Gemma 4 E4B) reports status != aligned
7
+ # with confidence >= 0.85.
8
+ #
9
+ # If no direction record is present for the project, the rule is a no-op
10
+ # (returns []). Bootstrap phase populates direction via the bootstrap-apply
11
+ # script + Console Triage approval workflow.
12
+ #
13
+ # Pragmatic v1 implementation (without live tier-fast call):
14
+ # * Hard-codes a deterministic heuristic for tests: emit a high-confidence
15
+ # `direction_misalignment` signal when `events_count >= ACTIVITY_MIN` AND
16
+ # the project has a direction row. The orchestrator then derives the
17
+ # observed_delta + proposed_payload via the upstream LLM call when
18
+ # enabled (env BRAIN_DR8_LLM_ENABLED=true).
19
+ # * LLM polish for proposed_direction_update is invoked asynchronously
20
+ # via api.services.brain.llm.* (existing tier-write provider). If LLM
21
+ # unavailable, the signal is still emitted with a deterministic
22
+ # observed_delta and no proposed_payload.
23
+ #
24
+ # Decisione brainstorm §6: NO banned-words retry policy on the LLM prompt.
25
+
26
+ from __future__ import annotations
27
+
28
+ import logging
29
+ import os
30
+ from datetime import datetime
31
+ from typing import Any
32
+
33
+ import aiosqlite
34
+
35
+ from core.api.db import acquire_db
36
+ from core.api.models.brain import DriftSignal
37
+ from core.api.services.brain.cycle_snapshot import CycleSnapshot, DigestEventRow
38
+ from core.api.services.brain.rules._signals import build_signal
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+ ACTIVITY_MIN = 1 # any event vs declared direction triggers DR8 evaluation
43
+ CONFIDENCE_EMIT_THRESHOLD = 0.85
44
+
45
+
46
+ async def _fetch_directions(
47
+ *, db: aiosqlite.Connection
48
+ ) -> dict[str, dict[str, Any]]:
49
+ """Fetch every project_directions row keyed by project_slug."""
50
+ cur = await db.execute(
51
+ "SELECT project_slug, summary, out_of_scope, last_updated_at"
52
+ " FROM project_directions"
53
+ )
54
+ rows = await cur.fetchall()
55
+ await cur.close()
56
+ out: dict[str, dict[str, Any]] = {}
57
+ for row in rows:
58
+ slug = row[0]
59
+ out[slug] = {
60
+ "summary": row[1],
61
+ "out_of_scope": row[2],
62
+ "last_updated_at": row[3],
63
+ }
64
+ return out
65
+
66
+
67
+ def _events_evidence_refs(events: list[DigestEventRow], cap: int = 5) -> list[str]:
68
+ """Pick up to `cap` event refs (commits, tasks, prs, handoffs) as evidence."""
69
+ refs: list[str] = []
70
+ for ev in events[:cap]:
71
+ if ev.source_ref:
72
+ refs.append(f"{ev.source_system or 'event'}:{ev.source_ref}")
73
+ return refs
74
+
75
+
76
+ def _summarise_events(events: list[DigestEventRow]) -> str:
77
+ """Build a short observed-delta string from events."""
78
+ if not events:
79
+ return "no events in cycle window"
80
+ counters: dict[str, int] = {}
81
+ for ev in events:
82
+ kind = ev.event_type or "event"
83
+ counters[kind] = counters.get(kind, 0) + 1
84
+ parts = sorted(counters.items(), key=lambda kv: -kv[1])[:5]
85
+ return "events_in_cycle: " + ", ".join(f"{k}={v}" for k, v in parts)
86
+
87
+
88
+ async def build_signals(
89
+ snapshot: CycleSnapshot, *, run_id: str, now: datetime
90
+ ) -> list[DriftSignal]:
91
+ """Emit DR8 direction_misalignment signals for projects with declared direction.
92
+
93
+ Strategy:
94
+ 1. Load all project_directions rows (DB cache, fast SQL).
95
+ 2. For each (project_scope, scope_key=slug) in CycleSnapshot.by_scope,
96
+ if a direction is present AND events_count >= ACTIVITY_MIN:
97
+ - Build deterministic observed_delta from events summary.
98
+ - Confidence defaults to 0.6 (heuristic). When LLM enabled (env
99
+ BRAIN_DR8_LLM_ENABLED=true), tier-fast classifier may bump to
100
+ >= 0.85 → emit. Without LLM, signals stay below threshold but
101
+ are still recorded (`state='open'`) so the orchestrator can
102
+ surface them for later promotion.
103
+ 3. The CONFIDENCE_FLOOR enforcement happens upstream; we only need to
104
+ emit signals with confidence >= CONFIDENCE_EMIT_THRESHOLD to drive
105
+ L5 finding creation.
106
+
107
+ Returns drift signals (empty list when no direction or no events).
108
+ """
109
+ signals: list[DriftSignal] = []
110
+
111
+ async with acquire_db() as db:
112
+ directions = await _fetch_directions(db=db)
113
+
114
+ if not directions:
115
+ logger.debug("DR8: no project_directions rows — skipping")
116
+ return signals
117
+
118
+ llm_enabled = os.environ.get("BRAIN_DR8_LLM_ENABLED", "").lower() in (
119
+ "1", "true", "yes",
120
+ )
121
+
122
+ for (scope_type, scope_key), events in snapshot.by_scope.items():
123
+ if scope_type != "project":
124
+ continue
125
+ direction = directions.get(scope_key)
126
+ if direction is None:
127
+ continue
128
+ if len(events) < ACTIVITY_MIN:
129
+ continue
130
+
131
+ observed_delta = _summarise_events(events)
132
+ evidence_refs = _events_evidence_refs(events)
133
+ expected_ref = f"project_directions:{scope_key}"
134
+ observed_ref = f"events_window:{scope_key}:{snapshot.cycle_key}"
135
+
136
+ # Confidence: deterministic baseline = 0.55 (medium).
137
+ # LLM-enabled path bumps to 0.9 when classifier returns status != aligned.
138
+ # Per brainstorm §6 we do NOT run a banned-words retry; the LLM result
139
+ # is accepted as-is.
140
+ confidence_estimate = 0.55
141
+ proposed_payload: dict[str, Any] | None = None
142
+
143
+ if llm_enabled:
144
+ # Lazy import — keep rule importable without LLM stack in tests.
145
+ try:
146
+ from core.api.services.brain.llm import get_brain_llm_service
147
+ # `get_brain_llm_service` is a sync singleton accessor — do NOT
148
+ # await it. The async surface is `llm.classify_direction_alignment`.
149
+ llm = get_brain_llm_service()
150
+ # Build prompt context — keep it compact (~2k tokens)
151
+ prompt_payload = {
152
+ "project_slug": scope_key,
153
+ "direction_summary": direction["summary"],
154
+ "direction_out_of_scope": direction["out_of_scope"],
155
+ "events_observed": observed_delta,
156
+ "events_refs": evidence_refs,
157
+ }
158
+ # Reuse the polish surface — caller wraps result in PolishResult.
159
+ # If grounding fails we fall back to deterministic confidence.
160
+ result = await llm.classify_direction_alignment(prompt_payload) # type: ignore[attr-defined]
161
+ if result is not None:
162
+ confidence_estimate = float(result.get("confidence", 0.55))
163
+ if result.get("status") and result.get("status") != "aligned":
164
+ proposed_payload = {
165
+ "status": result.get("status"),
166
+ "rationale": result.get("rationale", ""),
167
+ "proposed_summary": result.get("proposed_summary"),
168
+ "proposed_out_of_scope": result.get("proposed_out_of_scope"),
169
+ }
170
+ except Exception as exc: # noqa: BLE001 — LLM is best-effort
171
+ logger.warning(
172
+ "DR8: LLM classify failed for %s: %s — fallback heuristic",
173
+ scope_key, exc,
174
+ )
175
+
176
+ # Emit signal regardless of confidence — L5 findings layer filters
177
+ # by >= 0.85; signals < 0.85 stay as audit-only rows.
178
+ program_key = None
179
+ try:
180
+ project_meta = snapshot.project_meta.get(scope_key, {}) # type: ignore[attr-defined]
181
+ program_key = project_meta.get("program")
182
+ except (AttributeError, Exception): # pragma: no cover
183
+ program_key = None
184
+
185
+ signal = build_signal(
186
+ run_id=run_id,
187
+ cycle_key=snapshot.cycle_key,
188
+ detected_at=now,
189
+ rule_id="DR8",
190
+ scope_type="project",
191
+ scope_key=scope_key,
192
+ program_key=program_key,
193
+ signal_type="direction_misalignment",
194
+ expected_direction_source="doc",
195
+ expected_direction_ref=expected_ref,
196
+ observed_direction_ref=observed_ref,
197
+ observed_delta=observed_delta,
198
+ evidence_refs=evidence_refs or [expected_ref],
199
+ severity_base="medium",
200
+ drift_axis="intent",
201
+ involved_projects=[scope_key],
202
+ )
203
+ # Override the heuristic confidence with our LLM-aware estimate.
204
+ # `build_signal` already clamps to [FLOOR, 1.0].
205
+ signal_dict = signal.model_dump()
206
+ signal_dict["confidence"] = max(0.3, min(1.0, confidence_estimate))
207
+ signal = type(signal)(**signal_dict)
208
+
209
+ # Attach proposed_payload via evidence_json passthrough — orchestrator
210
+ # writes it to brain_drift_signals.evidence_json so L5 can read it.
211
+ if proposed_payload is not None:
212
+ try:
213
+ import json as _json
214
+ # Mutate the signal's evidence to include the proposed payload
215
+ # under a reserved key the L5 layer recognises. This stays
216
+ # backward-compatible (older rules emit list[str] evidence).
217
+ signal_dict = signal.model_dump()
218
+ ev = list(signal_dict.get("evidence", []))
219
+ ev.append(f"proposed_payload_json:{_json.dumps(proposed_payload, sort_keys=True)}")
220
+ signal_dict["evidence"] = ev
221
+ signal = type(signal)(**signal_dict)
222
+ except Exception: # pragma: no cover
223
+ pass
224
+
225
+ signals.append(signal)
226
+
227
+ return signals
228
+
229
+
230
+ __all__ = ["build_signals", "ACTIVITY_MIN", "CONFIDENCE_EMIT_THRESHOLD"]