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,485 @@
1
+ # Brain v1 — Runs read API (sub-05 §2 + sub-01 §6.D4).
2
+ # Pulls envelope rows from brain_runs and projects them to the BrainRun
3
+ # Pydantic contract. Cursor pagination + visibility-passthrough.
4
+ from __future__ import annotations
5
+
6
+ import base64
7
+ import json
8
+ from datetime import datetime, timezone
9
+ from typing import Any
10
+
11
+ import aiosqlite
12
+
13
+ from core.api.db import acquire_db, write_db
14
+ from core.api.models.brain import BrainRun, PipelineCounters, RunsListResponse, RunStatus
15
+
16
+ DEFAULT_LIMIT = 50
17
+ MAX_LIMIT = 200
18
+
19
+ _VALID_STATUSES: tuple[str, ...] = (
20
+ "running",
21
+ "succeeded",
22
+ "partial",
23
+ "failed",
24
+ "superseded",
25
+ )
26
+
27
+
28
+ def _parse_iso(value: str | None) -> datetime | None:
29
+ if value is None:
30
+ return None
31
+ dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
32
+ if dt.tzinfo is None:
33
+ dt = dt.replace(tzinfo=timezone.utc)
34
+ return dt
35
+
36
+
37
+ def _encode_cursor(cycle_key: str, run_id: str) -> str:
38
+ payload = json.dumps({"c": cycle_key, "r": run_id}, separators=(",", ":"))
39
+ return base64.urlsafe_b64encode(payload.encode("utf-8")).decode("ascii")
40
+
41
+
42
+ def _decode_cursor(cursor: str | None) -> tuple[str, str] | None:
43
+ if not cursor:
44
+ return None
45
+ try:
46
+ raw = base64.urlsafe_b64decode(cursor.encode("ascii")).decode("utf-8")
47
+ parsed = json.loads(raw)
48
+ return str(parsed["c"]), str(parsed["r"])
49
+ except (ValueError, KeyError, json.JSONDecodeError):
50
+ return None
51
+
52
+
53
+ def _row_to_run(row: aiosqlite.Row) -> BrainRun:
54
+ failures_raw = row["partial_failures_json"] if "partial_failures_json" in row.keys() else "[]"
55
+ try:
56
+ partial = json.loads(failures_raw or "[]")
57
+ except json.JSONDecodeError:
58
+ partial = []
59
+ return BrainRun(
60
+ run_id=row["run_id"],
61
+ workspace_id=row["workspace_id"],
62
+ cycle_key=row["cycle_key"],
63
+ cycle_window_start_utc=_parse_iso(row["cycle_window_start_utc"]),
64
+ cycle_window_end_utc=_parse_iso(row["cycle_window_end_utc"]),
65
+ cutoff_hour_utc_at_run=int(row["cutoff_hour_utc_at_run"] or 0),
66
+ scope_type="company",
67
+ scope_key=row["scope_key"] or "__company__",
68
+ trigger=row["trigger"],
69
+ triggered_by=row["triggered_by"],
70
+ started_at=_parse_iso(row["started_at"]),
71
+ finished_at=_parse_iso(row["finished_at"]),
72
+ status=row["status"],
73
+ superseded_by_run_id=row["superseded_by_run_id"],
74
+ event_count=int(row["event_count"] or 0),
75
+ partial_failures=partial,
76
+ duration_ms=row["duration_ms"],
77
+ error_summary=row["error_summary"],
78
+ )
79
+
80
+
81
+ async def _resolve_latest_cycle_key(
82
+ db: aiosqlite.Connection, *, workspace_id: str
83
+ ) -> str | None:
84
+ async with db.execute(
85
+ "SELECT cycle_key FROM brain_runs "
86
+ "WHERE workspace_id = ? AND status = 'succeeded' "
87
+ "AND superseded_by_run_id IS NULL "
88
+ "ORDER BY cycle_key DESC LIMIT 1",
89
+ (workspace_id,),
90
+ ) as cur:
91
+ row = await cur.fetchone()
92
+ return row["cycle_key"] if row else None
93
+
94
+
95
+ async def list_runs(
96
+ *,
97
+ cycle_key: str | None,
98
+ status: list[str] | None,
99
+ trigger: list[str] | None,
100
+ include_superseded: bool,
101
+ cursor: str | None,
102
+ limit: int,
103
+ workspace_id: str = "ws_default",
104
+ ) -> RunsListResponse:
105
+ """Stable sort: (cycle_key DESC, run_id ASC)."""
106
+ where = ["workspace_id = ?"]
107
+ params: list[Any] = [workspace_id]
108
+ resolved_cycle: str | None = None
109
+
110
+ if cycle_key == "latest":
111
+ async with acquire_db() as db:
112
+ db.row_factory = aiosqlite.Row
113
+ resolved_cycle = await _resolve_latest_cycle_key(
114
+ db, workspace_id=workspace_id
115
+ )
116
+ if resolved_cycle:
117
+ where.append("cycle_key = ?")
118
+ params.append(resolved_cycle)
119
+ else:
120
+ return RunsListResponse(
121
+ items=[], next_cursor=None, cycle_key=None, total_returned=0
122
+ )
123
+ elif cycle_key:
124
+ where.append("cycle_key = ?")
125
+ params.append(cycle_key)
126
+ resolved_cycle = cycle_key
127
+
128
+ if status:
129
+ valid = [s for s in status if s in _VALID_STATUSES]
130
+ if valid:
131
+ placeholders = ",".join("?" for _ in valid)
132
+ where.append(f"status IN ({placeholders})")
133
+ params.extend(valid)
134
+
135
+ if trigger:
136
+ placeholders = ",".join("?" for _ in trigger)
137
+ where.append(f"trigger IN ({placeholders})")
138
+ params.extend(trigger)
139
+
140
+ if not include_superseded:
141
+ where.append("superseded_by_run_id IS NULL")
142
+
143
+ decoded = _decode_cursor(cursor)
144
+ if decoded:
145
+ cur_cycle, cur_run = decoded
146
+ where.append("(cycle_key < ? OR (cycle_key = ? AND run_id > ?))")
147
+ params.extend([cur_cycle, cur_cycle, cur_run])
148
+
149
+ fetch_limit = min(max(limit, 1), MAX_LIMIT) + 1
150
+ query = (
151
+ "SELECT run_id, workspace_id, cycle_key, "
152
+ "cycle_window_start_utc, cycle_window_end_utc, cutoff_hour_utc_at_run, "
153
+ "scope_type, scope_key, trigger, triggered_by, started_at, finished_at, "
154
+ "status, superseded_by_run_id, event_count, partial_failures_json, "
155
+ "duration_ms, error_summary "
156
+ "FROM brain_runs WHERE " + " AND ".join(where) +
157
+ " ORDER BY cycle_key DESC, run_id ASC LIMIT ?"
158
+ )
159
+ params.append(fetch_limit)
160
+
161
+ async with acquire_db() as db:
162
+ db.row_factory = aiosqlite.Row
163
+ rows = list(await (await db.execute(query, params)).fetchall())
164
+
165
+ next_cursor: str | None = None
166
+ if len(rows) > limit:
167
+ last = rows[limit - 1]
168
+ next_cursor = _encode_cursor(last["cycle_key"], last["run_id"])
169
+ rows = rows[:limit]
170
+
171
+ items = [_row_to_run(r) for r in rows]
172
+ return RunsListResponse(
173
+ items=items,
174
+ next_cursor=next_cursor,
175
+ cycle_key=resolved_cycle,
176
+ total_returned=len(items),
177
+ )
178
+
179
+
180
+ class PromoteError(Exception):
181
+ """Raised when a promote_run pre-condition fails (404 / 422 surface)."""
182
+
183
+ def __init__(self, kind: str, detail: str):
184
+ self.kind = kind
185
+ self.detail = detail
186
+ super().__init__(detail)
187
+
188
+
189
+ async def promote_run(
190
+ run_id: str, *, workspace_id: str = "ws_default"
191
+ ) -> BrainRun:
192
+ """Revert a supersede so a previously-replaced run becomes UX-visible again.
193
+
194
+ Use case: a low-yield manual recompute superseded a richer auto-cron run.
195
+ The richer run still holds its journal/drift/memory_op/finding rows but
196
+ `superseded_by_run_id` keeps them hidden behind the visibility filter
197
+ (`r.superseded_by_run_id IS NULL`) on every read path.
198
+
199
+ Effects:
200
+ 1. Validate target run exists in workspace AND is currently superseded.
201
+ 2. Clear `superseded_by_run_id` on target → it surfaces again.
202
+ 3. Cascade-clear: the successor that pointed at target loses its
203
+ `superseded_by_run_id` reverse-pointer entry only if it itself was
204
+ the chain head (no further superseder). This keeps the chain
205
+ well-formed: at most one head per cycle is UX-visible.
206
+
207
+ Raises `PromoteError` with kind:
208
+ - 'not_found': run_id unknown in workspace.
209
+ - 'not_superseded': target run is already UX-visible (idempotency).
210
+ """
211
+ # Read pre-conditions on the readonly pool (fast path; no writer lock).
212
+ async with acquire_db() as db:
213
+ db.row_factory = aiosqlite.Row
214
+ async with db.execute(
215
+ "SELECT run_id, status, superseded_by_run_id, cycle_key "
216
+ "FROM brain_runs WHERE run_id = ? AND workspace_id = ?",
217
+ (run_id, workspace_id),
218
+ ) as cur:
219
+ target = await cur.fetchone()
220
+ if target is None:
221
+ raise PromoteError("not_found", f"run {run_id} not found")
222
+ successor_run_id = target["superseded_by_run_id"]
223
+ if successor_run_id is None:
224
+ raise PromoteError(
225
+ "not_superseded",
226
+ f"run {run_id} is already the visible head (no superseder to revert)",
227
+ )
228
+
229
+ # SWAP heads on the writer connection (single-writer lock).
230
+ # Order matters: the partial unique index
231
+ # uniq_brain_runs_active_cycle ON (workspace_id, cycle_key)
232
+ # WHERE status IN ('running','succeeded') AND superseded_by_run_id IS NULL
233
+ # caps the cycle at one "head" row. If we mark target as head BEFORE
234
+ # demoting successor, both rows transiently match the index → IntegrityError.
235
+ # Demote successor FIRST (it drops out of the index because status moves
236
+ # to 'superseded'), THEN promote target (it joins the index in successor's
237
+ # place). At most one head per cycle at any point during the txn.
238
+ async with write_db() as db:
239
+ db.row_factory = aiosqlite.Row
240
+ # Step 1 — demote successor: status -> 'superseded' + point at target.
241
+ await db.execute(
242
+ "UPDATE brain_runs SET superseded_by_run_id = ?, "
243
+ "status = CASE WHEN status = 'succeeded' THEN 'superseded' ELSE status END "
244
+ "WHERE run_id = ? AND workspace_id = ?",
245
+ (run_id, successor_run_id, workspace_id),
246
+ )
247
+ # Step 2 — promote target: clear pointer + status -> 'succeeded'.
248
+ await db.execute(
249
+ "UPDATE brain_runs SET superseded_by_run_id = NULL, "
250
+ "status = CASE WHEN status = 'superseded' THEN 'succeeded' ELSE status END "
251
+ "WHERE run_id = ? AND workspace_id = ?",
252
+ (run_id, workspace_id),
253
+ )
254
+
255
+ # Re-read the promoted run for response inside the same writer txn
256
+ # so we observe the post-update state.
257
+ async with db.execute(
258
+ "SELECT run_id, workspace_id, cycle_key, "
259
+ "cycle_window_start_utc, cycle_window_end_utc, cutoff_hour_utc_at_run, "
260
+ "scope_type, scope_key, trigger, triggered_by, started_at, finished_at, "
261
+ "status, superseded_by_run_id, event_count, partial_failures_json, "
262
+ "duration_ms, error_summary "
263
+ "FROM brain_runs WHERE run_id = ? AND workspace_id = ?",
264
+ (run_id, workspace_id),
265
+ ) as cur:
266
+ row = await cur.fetchone()
267
+ return _row_to_run(row)
268
+
269
+
270
+ class DiscardError(Exception):
271
+ """Raised when a discard_run pre-condition fails (404 / 422 surface)."""
272
+
273
+ def __init__(self, kind: str, detail: str):
274
+ self.kind = kind
275
+ self.detail = detail
276
+ super().__init__(detail)
277
+
278
+
279
+ async def discard_run(
280
+ run_id: str, *, workspace_id: str = "ws_default"
281
+ ) -> BrainRun:
282
+ """Mark a head run as 'failed' so it drops out of UX `latest` resolution.
283
+
284
+ Use case: a low-yield manual recompute creates a head run (e.g. cycle
285
+ forward 2026-05-18 with 1 event) that intercepts `cycle_key=latest`
286
+ resolution. Discarding the head removes it from the active-cycle
287
+ partial unique index (which only matches `status IN
288
+ ('running','succeeded') AND superseded_by_run_id IS NULL`), so the
289
+ next visible cycle (the richer one one day prior) wins `latest`.
290
+
291
+ Effects:
292
+ 1. Validate target run exists, is the head (superseded_by_run_id NULL),
293
+ and is currently in a discardable status (succeeded/partial).
294
+ 2. Set status = 'failed' (preserves audit trail vs. delete; row stays
295
+ queryable with `include_superseded=true` for forensics).
296
+
297
+ Raises `DiscardError` with kind:
298
+ - 'not_found': run_id unknown in workspace.
299
+ - 'not_head': run is already superseded (won't surface anyway).
300
+ - 'not_discardable': status is already terminal-non-success.
301
+ """
302
+ async with acquire_db() as db:
303
+ db.row_factory = aiosqlite.Row
304
+ async with db.execute(
305
+ "SELECT run_id, status, superseded_by_run_id, cycle_key "
306
+ "FROM brain_runs WHERE run_id = ? AND workspace_id = ?",
307
+ (run_id, workspace_id),
308
+ ) as cur:
309
+ target = await cur.fetchone()
310
+ if target is None:
311
+ raise DiscardError("not_found", f"run {run_id} not found")
312
+ if target["superseded_by_run_id"] is not None:
313
+ raise DiscardError(
314
+ "not_head",
315
+ f"run {run_id} is already superseded — won't surface in UX latest",
316
+ )
317
+ if target["status"] not in ("succeeded", "partial"):
318
+ raise DiscardError(
319
+ "not_discardable",
320
+ f"run {run_id} has status='{target['status']}' (only succeeded/partial discardable)",
321
+ )
322
+
323
+ async with write_db() as db:
324
+ db.row_factory = aiosqlite.Row
325
+ await db.execute(
326
+ "UPDATE brain_runs SET status = 'failed' "
327
+ "WHERE run_id = ? AND workspace_id = ?",
328
+ (run_id, workspace_id),
329
+ )
330
+ async with db.execute(
331
+ "SELECT run_id, workspace_id, cycle_key, "
332
+ "cycle_window_start_utc, cycle_window_end_utc, cutoff_hour_utc_at_run, "
333
+ "scope_type, scope_key, trigger, triggered_by, started_at, finished_at, "
334
+ "status, superseded_by_run_id, event_count, partial_failures_json, "
335
+ "duration_ms, error_summary "
336
+ "FROM brain_runs WHERE run_id = ? AND workspace_id = ?",
337
+ (run_id, workspace_id),
338
+ ) as cur:
339
+ row = await cur.fetchone()
340
+ return _row_to_run(row)
341
+
342
+
343
+ async def fetch_single_run(
344
+ run_id: str, *, workspace_id: str = "ws_default"
345
+ ) -> BrainRun | None:
346
+ async with acquire_db() as db:
347
+ db.row_factory = aiosqlite.Row
348
+ async with db.execute(
349
+ "SELECT run_id, workspace_id, cycle_key, "
350
+ "cycle_window_start_utc, cycle_window_end_utc, cutoff_hour_utc_at_run, "
351
+ "scope_type, scope_key, trigger, triggered_by, started_at, finished_at, "
352
+ "status, superseded_by_run_id, event_count, partial_failures_json, "
353
+ "duration_ms, error_summary "
354
+ "FROM brain_runs WHERE run_id = ? AND workspace_id = ?",
355
+ (run_id, workspace_id),
356
+ ) as cur:
357
+ row = await cur.fetchone()
358
+ return _row_to_run(row) if row else None
359
+
360
+
361
+ async def get_pipeline_counters(
362
+ *, cycle_key: str | None, workspace_id: str = "ws_default"
363
+ ) -> PipelineCounters:
364
+ """Aggregate 6-station counts for the PipelineSubbar.
365
+
366
+ Resolves `latest` server-side. Counts are unfiltered (workspace-scoped) —
367
+ visibility is applied client-side via redacted_count on downstream calls.
368
+ The subbar is a status indicator, not a privileged surface.
369
+ """
370
+ async with acquire_db() as db:
371
+ db.row_factory = aiosqlite.Row
372
+
373
+ resolved_cycle = cycle_key
374
+ if cycle_key == "latest" or cycle_key is None:
375
+ resolved_cycle = await _resolve_latest_cycle_key(
376
+ db, workspace_id=workspace_id
377
+ )
378
+
379
+ if not resolved_cycle:
380
+ return PipelineCounters(
381
+ cycle_key=cycle_key or "latest",
382
+ run_id=None,
383
+ )
384
+
385
+ async with db.execute(
386
+ "SELECT run_id, event_count FROM brain_runs "
387
+ "WHERE workspace_id = ? AND cycle_key = ? "
388
+ "AND status IN ('succeeded', 'partial') "
389
+ "AND superseded_by_run_id IS NULL "
390
+ "ORDER BY started_at DESC LIMIT 1",
391
+ (workspace_id, resolved_cycle),
392
+ ) as cur:
393
+ run_row = await cur.fetchone()
394
+
395
+ run_id = run_row["run_id"] if run_row else None
396
+ digest_count = int(run_row["event_count"]) if run_row else 0
397
+
398
+ # Journal: count entries for this run
399
+ journal_count = 0
400
+ if run_id:
401
+ async with db.execute(
402
+ "SELECT COUNT(*) AS c FROM brain_journal_entries "
403
+ "WHERE workspace_id = ? AND run_id = ?",
404
+ (workspace_id, run_id),
405
+ ) as cur:
406
+ row = await cur.fetchone()
407
+ journal_count = int(row["c"]) if row else 0
408
+
409
+ # Drift: count signals in 'open' state for this cycle.
410
+ # brain_drift_signals does NOT have a workspace_id column — visibility
411
+ # is enforced on read paths via join to brain_runs. For the counter
412
+ # (status indicator, not privileged surface — see docstring above)
413
+ # cycle_key alone is sufficient.
414
+ drift_count = 0
415
+ try:
416
+ async with db.execute(
417
+ "SELECT COUNT(*) AS c FROM brain_drift_signals "
418
+ "WHERE cycle_key = ? AND state = 'open'",
419
+ (resolved_cycle,),
420
+ ) as cur:
421
+ row = await cur.fetchone()
422
+ drift_count = int(row["c"]) if row else 0
423
+ except aiosqlite.OperationalError:
424
+ drift_count = 0
425
+
426
+ # Memory ops: count pending operations (same rationale as drift).
427
+ mem_count = 0
428
+ try:
429
+ async with db.execute(
430
+ "SELECT COUNT(*) AS c FROM brain_memory_operations "
431
+ "WHERE cycle_key = ? AND approval_state = 'pending'",
432
+ (resolved_cycle,),
433
+ ) as cur:
434
+ row = await cur.fetchone()
435
+ mem_count = int(row["c"]) if row else 0
436
+ except aiosqlite.OperationalError:
437
+ mem_count = 0
438
+
439
+ # Findings: count open findings for this cycle (same rationale).
440
+ findings_count = 0
441
+ try:
442
+ async with db.execute(
443
+ "SELECT COUNT(*) AS c FROM brain_findings "
444
+ "WHERE cycle_key = ? AND approval_state = 'open'",
445
+ (resolved_cycle,),
446
+ ) as cur:
447
+ row = await cur.fetchone()
448
+ findings_count = int(row["c"]) if row else 0
449
+ except aiosqlite.OperationalError:
450
+ findings_count = 0
451
+
452
+ # Ingest: count pending ingest items (system-wide, no cycle scope)
453
+ ingest_count = 0
454
+ try:
455
+ async with db.execute(
456
+ "SELECT COUNT(*) AS c FROM ingest_pending WHERE status = 'pending'",
457
+ ) as cur:
458
+ row = await cur.fetchone()
459
+ ingest_count = int(row["c"]) if row else 0
460
+ except aiosqlite.OperationalError:
461
+ ingest_count = 0
462
+
463
+ return PipelineCounters(
464
+ cycle_key=resolved_cycle,
465
+ run_id=run_id,
466
+ ingest=ingest_count,
467
+ digest=digest_count,
468
+ journal=journal_count,
469
+ drift=drift_count,
470
+ memory_ops=mem_count,
471
+ findings=findings_count,
472
+ )
473
+
474
+
475
+ def _all_run_statuses() -> tuple[str, ...]:
476
+ return _VALID_STATUSES
477
+
478
+
479
+ __all__ = [
480
+ "DEFAULT_LIMIT",
481
+ "MAX_LIMIT",
482
+ "fetch_single_run",
483
+ "get_pipeline_counters",
484
+ "list_runs",
485
+ ]
@@ -0,0 +1,79 @@
1
+ # Project → program resolver. Reads programs.yaml at repo root.
2
+ # Plan: sub-01 §9. Single resolver for Digest/Journal/Drift/Memory-Ops/Learn.
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from threading import Lock
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+ from core.api.paths import repo_path
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _PROGRAMS_YAML_PATH = repo_path(__file__, "programs.yaml")
17
+ _cache_lock = Lock()
18
+ _cached_map: dict[str, str] | None = None
19
+ _cached_mtime: float | None = None
20
+
21
+
22
+ def _load_programs_map() -> dict[str, str]:
23
+ """Return {project_slug: program_key}. Empty dict on missing/invalid yaml.
24
+
25
+ Cached by file mtime — re-parses if the file changes on disk.
26
+ """
27
+ global _cached_map, _cached_mtime
28
+
29
+ path = _PROGRAMS_YAML_PATH
30
+ try:
31
+ mtime = path.stat().st_mtime
32
+ except FileNotFoundError:
33
+ with _cache_lock:
34
+ _cached_map = {}
35
+ _cached_mtime = None
36
+ return {}
37
+
38
+ with _cache_lock:
39
+ if _cached_map is not None and _cached_mtime == mtime:
40
+ return _cached_map
41
+
42
+ try:
43
+ with path.open("r", encoding="utf-8") as fh:
44
+ doc: dict[str, Any] = yaml.safe_load(fh) or {}
45
+ except (OSError, yaml.YAMLError):
46
+ logger.exception("brain.scope: failed to parse %s", path)
47
+ with _cache_lock:
48
+ _cached_map = {}
49
+ _cached_mtime = mtime
50
+ return {}
51
+
52
+ mapping: dict[str, str] = {}
53
+ if isinstance(doc, dict):
54
+ for program_key, payload in doc.items():
55
+ if not isinstance(program_key, str) or not isinstance(payload, dict):
56
+ continue
57
+ for slug in payload.get("projects") or []:
58
+ if isinstance(slug, str):
59
+ mapping[slug] = program_key
60
+
61
+ with _cache_lock:
62
+ _cached_map = mapping
63
+ _cached_mtime = mtime
64
+ return mapping
65
+
66
+
67
+ def resolve_program(project_slug: str | None) -> str | None:
68
+ """Return program_key for the given slug, or None if standalone."""
69
+ if not project_slug:
70
+ return None
71
+ return _load_programs_map().get(project_slug)
72
+
73
+
74
+ def reset_cache() -> None:
75
+ """Reset the cache. Test-only helper."""
76
+ global _cached_map, _cached_mtime
77
+ with _cache_lock:
78
+ _cached_map = None
79
+ _cached_mtime = None
@@ -0,0 +1,46 @@
1
+ # Brain v1.0.1 — Source collector subpackage (sub-01 §3 Sources v1).
2
+ # Each module owns one substrate (ingest / git+KG / PiR tasks / handoffs /
3
+ # learnings) and exposes a SourceCollector descriptor.
4
+ #
5
+ # register_all_collectors() is the single startup hook called from
6
+ # api/main.py lifespan BEFORE the first _periodic_brain_jobs tick. It is
7
+ # idempotent: prior collectors are cleared and the canonical five
8
+ # re-registered, so lifespan restarts never accrete duplicates.
9
+ from __future__ import annotations
10
+
11
+ from core.api.services.brain.digest_collector import (
12
+ clear_registered_collectors,
13
+ register_collector,
14
+ )
15
+ from core.api.services.brain.sources.git_kg import git_collector, kg_collector
16
+ from core.api.services.brain.sources.handoffs import handoffs_collector
17
+ from core.api.services.brain.sources.ingestor import ingestor_collector
18
+ from core.api.services.brain.sources.learnings import learnings_collector
19
+ from core.api.services.brain.sources.pir_tasks import pir_tasks_collector
20
+
21
+
22
+ def register_all_collectors() -> None:
23
+ """Clear + register the six v1.0.1 source collectors. Idempotent.
24
+
25
+ git_kg split in 2 separate collectors (git + kg) — CHECK constraint
26
+ brain_source_watermarks.source_system accepts only single tokens
27
+ ('git', 'kg', etc.), not composite 'git_kg'. Bugfix 2026-05-16.
28
+ """
29
+ clear_registered_collectors()
30
+ register_collector(ingestor_collector)
31
+ register_collector(git_collector)
32
+ register_collector(kg_collector)
33
+ register_collector(pir_tasks_collector)
34
+ register_collector(handoffs_collector)
35
+ register_collector(learnings_collector)
36
+
37
+
38
+ __all__ = [
39
+ "git_collector",
40
+ "kg_collector",
41
+ "handoffs_collector",
42
+ "ingestor_collector",
43
+ "learnings_collector",
44
+ "pir_tasks_collector",
45
+ "register_all_collectors",
46
+ ]
@@ -0,0 +1,86 @@
1
+ # Brain v1.0.1 — Shared helpers for source collectors (sub-01 §3 Sources).
2
+ # These helpers exist to keep individual collector files mechanical and short:
3
+ # substrate-specific reads belong in the collector module, while every-source
4
+ # concerns (timezone normalization, scope resolution, evidence hashing) live
5
+ # here.
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ from datetime import datetime, timezone
11
+ from typing import Any
12
+
13
+ from core.api.services.brain.cycle import evidence_hash as _cycle_evidence_hash
14
+ from core.api.services.brain.cycle import make_event_id as _cycle_make_event_id
15
+ from core.api.services.brain.scope import resolve_program
16
+
17
+
18
+ def evidence_hash(evidence: dict[str, Any]) -> str:
19
+ """Re-export of cycle.evidence_hash for collector code locality."""
20
+ return _cycle_evidence_hash(evidence)
21
+
22
+
23
+ def make_event_id(
24
+ *, cycle_key: str, event_type: str, source_ref: str, evidence: dict[str, Any]
25
+ ) -> str:
26
+ """Wrap cycle.make_event_id so collectors don't have to hash twice."""
27
+ return _cycle_make_event_id(
28
+ cycle_key=cycle_key,
29
+ event_type=event_type,
30
+ source_ref=source_ref,
31
+ evidence_hash_hex=_cycle_evidence_hash(evidence),
32
+ )
33
+
34
+
35
+ def normalize_iso(value: Any) -> datetime | None:
36
+ """Best-effort ISO-8601 parse → aware UTC datetime.
37
+
38
+ Handles the three flavours we encounter across substrate tables:
39
+ * tasks.updated_at: `datetime('now')` → naive LOCAL string. Treated as
40
+ UTC here per repo convention (mirror inbox_digest_jobs).
41
+ * learnings.updated_at: optional `datetime('now','utc')`.
42
+ * inbox_items.updated_at: same as tasks.
43
+ """
44
+ if value is None:
45
+ return None
46
+ if isinstance(value, datetime):
47
+ return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
48
+ try:
49
+ raw = str(value).replace("Z", "+00:00")
50
+ dt = datetime.fromisoformat(raw)
51
+ except (TypeError, ValueError):
52
+ return None
53
+ if dt.tzinfo is None:
54
+ dt = dt.replace(tzinfo=timezone.utc)
55
+ return dt.astimezone(timezone.utc)
56
+
57
+
58
+ def stable_short_hash(value: str, *, length: int = 12) -> str:
59
+ """BLAKE2b-prefixed deterministic short hash (filename anonymizer)."""
60
+ return hashlib.blake2b(value.encode("utf-8"), digest_size=length).hexdigest()
61
+
62
+
63
+ def canonical_payload(payload: dict[str, Any]) -> str:
64
+ """Canonical JSON used for evidence hashing — mirrors cycle.canonical_evidence."""
65
+ return json.dumps(payload, sort_keys=True, ensure_ascii=False, separators=(",", ":"))
66
+
67
+
68
+ def resolve_source_project(slug: str | None) -> tuple[str | None, str | None]:
69
+ """Return (source_project_slug, program_key) — both possibly None.
70
+
71
+ Centralised so each collector calls the same resolver — Drift/Memory-Ops
72
+ can later assume program_key is consistent with source_project.
73
+ """
74
+ if not slug:
75
+ return None, None
76
+ return slug, resolve_program(slug)
77
+
78
+
79
+ __all__ = [
80
+ "canonical_payload",
81
+ "evidence_hash",
82
+ "make_event_id",
83
+ "normalize_iso",
84
+ "resolve_source_project",
85
+ "stable_short_hash",
86
+ ]