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,312 @@
1
+ # v2.0.0 - 2026-05-27 - S1 F1.7: thin adapter over use_cases.pull_requests (merge human-gate kept at Depends)
2
+ # v1.7.0 - 2026-03-11 - Fase 2: approve and request-changes endpoints (four-eyes gate)
3
+ """HTTP adapter for the pull-requests domain (S1 collapse-runtime).
4
+
5
+ This router is a thin transport adapter. All branch/PR lifecycle + RBAC +
6
+ error-translation logic lives in :mod:`core.api.use_cases.pull_requests` (pure,
7
+ fastapi-free). Each handler resolves identity into a :class:`CallerContext`, calls
8
+ the use_case inside ``try/except`` -> ``to_http``, and owns the transport concerns.
9
+
10
+ MERGE HUMAN-GATE — kept at the ``Depends`` layer. ``POST /{task_id}/merge`` and
11
+ ``/{task_id}/revert`` keep ``Depends(require_role(..., human_only=True))``.
12
+ ``human_only=True`` swaps the auth dependency to cookie-only ``get_current_user``,
13
+ so a Bearer-only agent is rejected with ``401 Not authenticated`` upstream of the
14
+ handler (pinned by the merge-gate regression test ``test_br03_merge_pr_blocked_without_cookie``).
15
+ A use_case receiving a ``ctx`` cannot reproduce that 401, so the gate is NOT moved
16
+ into the domain — it stays exactly where it fires today.
17
+
18
+ ERROR BOUNDARIES kept in the adapter (legacy body parity):
19
+ * ``MergeConflictError`` (raised inside ``pr_service`` -> propagates through the
20
+ use_case) -> the exact legacy ``HTTPException(409, {message, conflicting_files})``
21
+ dict body. ``to_http`` would drop ``conflicting_files``.
22
+ * plain ``GitOpsError`` -> legacy ``HTTPException(500, str(exc))`` (pinned by
23
+ ``tests/test_db_contention_task_flows.py::test_merge_push_failure_*`` which
24
+ asserts the raw message in the 500 body). ``to_http``'s structured dict would
25
+ break that substring assertion.
26
+ * ``HTTPException``s raised inside ``pr_service`` (title/migration/push paths)
27
+ propagate natively — Fase-2 conversion, out of scope here.
28
+ Domain :class:`ServiceError`s (404/409/422/403 the use_case raises) flow through
29
+ ``to_http`` as the structured ``{code,message}`` body — their tests check status.
30
+
31
+ STAYS IN THE ADAPTER (transport concerns):
32
+ * ``get_pull_request`` ``deep`` KG enrichment (rate-limit + access log +
33
+ ``build_kg_context_for_pr``); ``core.api.services.kg.audit`` imports ``fastapi``
34
+ so this MUST NOT touch the use_case (DECISION 2).
35
+ * visibility resolution on ``get_merge_conflicts`` (``get_visible_projects`` needs
36
+ ``UserInfo.teams``) — resolved here, passed in (DECISION 1).
37
+ * approve/request-changes pass the full ``UserInfo`` (``user_id`` + team lookups)
38
+ through to the use_case, which forwards it to ``pr_service``.
39
+
40
+ ``MergeConflictResponse`` (response DTO) is imported from ``core.api.models``
41
+ (unchanged, like costs/tasks). REQUEST models stay defined here.
42
+ """
43
+ from __future__ import annotations
44
+
45
+ import logging
46
+
47
+ import aiosqlite
48
+ from fastapi import APIRouter, Depends, HTTPException, Query
49
+ from pydantic import BaseModel
50
+
51
+ from core.api.db import get_db, get_write_db
52
+ from core.api.models import MergeConflictResponse, UserInfo
53
+ from core.api.rbac import require_role
54
+ from core.api.routers._adapter import to_http
55
+ from core.api.security import get_current_user_or_agent, require_any_auth
56
+ from core.api.services.kg.audit import check_deep_rate_limit, log_kg_deep_access
57
+ from core.api.services.kg.lens import build_kg_context_for_pr
58
+ from core.api.use_cases import pull_requests as uc
59
+ from core.api.use_cases._context import CallerContext
60
+ from core.api.use_cases._errors import ServiceError
61
+ from core.api.visibility import get_visible_projects
62
+
63
+ # Re-export the git-ops error classes from the use_case so existing importers of
64
+ # ``core.api.routers.pull_requests.GitOpsError`` / ``MergeConflictError`` keep
65
+ # working unchanged.
66
+ from core.api.use_cases.pull_requests import ( # noqa: F401 (re-export surface)
67
+ GitOpsError,
68
+ MergeConflictError,
69
+ )
70
+
71
+ logger = logging.getLogger(__name__)
72
+
73
+ router = APIRouter(prefix="/api/v1/pull_requests", tags=["pull-requests"])
74
+
75
+
76
+ # --- Request models (kept in the adapter) ---
77
+
78
+ class SubmitPrRequest(BaseModel):
79
+ title: str
80
+ body: str = ""
81
+
82
+ class ClosePrRequest(BaseModel):
83
+ reason: str = ""
84
+
85
+ class UpdatePrRequest(BaseModel):
86
+ title: str | None = None
87
+ body: str | None = None
88
+
89
+ class RegisterBranchRequest(BaseModel):
90
+ branch_name: str
91
+ worktree_path: str | None = None
92
+
93
+ class RequestChangesBody(BaseModel):
94
+ comment: str
95
+
96
+
97
+ # --- Endpoints (thin adapters) ---
98
+
99
+ @router.get("/merge-conflicts", response_model=MergeConflictResponse)
100
+ async def get_merge_conflicts(
101
+ project: str = Query(..., description="Project slug to check for migration conflicts"),
102
+ user: UserInfo = Depends(require_any_auth),
103
+ db: aiosqlite.Connection = Depends(get_db),
104
+ ):
105
+ """Detect migration number conflicts across open PRs for a project."""
106
+ # DECISION 1: resolve visibility at the boundary, enforce in the use_case
107
+ # (404 on a non-visible slug — does not reveal existence).
108
+ visible_projects = await get_visible_projects(db, user)
109
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
110
+ try:
111
+ conflicts = await uc.get_merge_conflicts(
112
+ ctx, db, project=project, visible_projects=visible_projects
113
+ )
114
+ return MergeConflictResponse(conflicts=conflicts)
115
+ except ServiceError as e:
116
+ raise to_http(e)
117
+ except GitOpsError as exc:
118
+ raise HTTPException(status_code=500, detail=str(exc))
119
+
120
+
121
+ @router.post("/{task_id}/branch")
122
+ async def create_branch(
123
+ task_id: str,
124
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
125
+ db: aiosqlite.Connection = Depends(get_db),
126
+ ):
127
+ """Create git worktree and branch for a task. Idempotent."""
128
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
129
+ try:
130
+ return await uc.create_branch(ctx, db, task_id=task_id)
131
+ except ServiceError as e:
132
+ raise to_http(e)
133
+ except GitOpsError as exc:
134
+ raise HTTPException(status_code=500, detail=str(exc))
135
+
136
+
137
+ @router.post("/{task_id}/register")
138
+ async def register_branch(
139
+ task_id: str,
140
+ body: RegisterBranchRequest,
141
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
142
+ db: aiosqlite.Connection = Depends(get_write_db),
143
+ ):
144
+ """Register an externally-created branch as a draft PR record.
145
+
146
+ Used when an agent creates a worktree via git directly (not via /branch endpoint).
147
+ Idempotent: returns existing active PR if one already exists.
148
+ """
149
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
150
+ try:
151
+ return await uc.register_branch(
152
+ ctx, db, task_id=task_id, branch_name=body.branch_name,
153
+ worktree_path=body.worktree_path,
154
+ )
155
+ except ServiceError as e:
156
+ raise to_http(e)
157
+
158
+
159
+ @router.get("/{task_id}")
160
+ async def get_pull_request(
161
+ task_id: str,
162
+ deep: bool = Query(False),
163
+ user: UserInfo = Depends(get_current_user_or_agent),
164
+ db: aiosqlite.Connection = Depends(get_db),
165
+ ):
166
+ """Get PR status and diff for a task."""
167
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
168
+ try:
169
+ result = await uc.get_pull_request(ctx, db, task_id=task_id)
170
+ except ServiceError as e:
171
+ raise to_http(e)
172
+
173
+ # DECISION 2: deep KG enrichment is a per-surface adapter concern.
174
+ if deep:
175
+ check_deep_rate_limit(user.username)
176
+ log_kg_deep_access(user.username, "get_pull_request", task_id)
177
+ result["kg_context"] = await build_kg_context_for_pr(db, task_id, deep=True)
178
+ return result
179
+
180
+
181
+ @router.post("/{task_id}/submit")
182
+ async def submit_pull_request(
183
+ task_id: str,
184
+ body: SubmitPrRequest,
185
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
186
+ db: aiosqlite.Connection = Depends(get_write_db),
187
+ ):
188
+ """Submit branch for review. Moves PR from draft to open."""
189
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
190
+ try:
191
+ return await uc.submit_pull_request(
192
+ ctx, db, task_id=task_id, title=body.title, body=body.body
193
+ )
194
+ except ServiceError as e:
195
+ raise to_http(e)
196
+ except GitOpsError as exc:
197
+ raise HTTPException(status_code=500, detail=str(exc))
198
+
199
+
200
+ @router.post("/{task_id}/merge")
201
+ async def merge_pull_request(
202
+ task_id: str,
203
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin", human_only=True)),
204
+ db: aiosqlite.Connection = Depends(get_write_db),
205
+ ):
206
+ """Merge PR. Human operator+ authorization required (no agent tokens).
207
+
208
+ The human-only gate is the ``human_only=True`` dependency above (cookie-only
209
+ auth) — it fires upstream of this handler and is NOT duplicated in the use_case.
210
+ """
211
+ ctx = CallerContext.from_user_info(user, is_human_session=True)
212
+ try:
213
+ return await uc.merge_pull_request(ctx, db, task_id=task_id)
214
+ except MergeConflictError as exc:
215
+ # Preserve the exact legacy 409 dict body (message + conflicting_files).
216
+ raise HTTPException(
217
+ status_code=409,
218
+ detail={
219
+ "message": (
220
+ "Merge conflict: cannot fast-forward or 3-way merge the PR branch into main. "
221
+ "Reason: main has changes that touch the same lines as this PR since the branch diverged. "
222
+ "Fix: (1) checkout the PR branch in its worktree, (2) `git fetch origin && git rebase origin/main`, "
223
+ "(3) resolve conflicts in the files listed below, (4) push the rebased branch, (5) retry merge."
224
+ ),
225
+ "conflicting_files": exc.conflicting_files,
226
+ },
227
+ )
228
+ except GitOpsError as exc:
229
+ # Plain GitOpsError -> legacy 500 plain-string body.
230
+ raise HTTPException(status_code=500, detail=str(exc))
231
+ except ServiceError as e:
232
+ raise to_http(e)
233
+
234
+
235
+ @router.post("/{task_id}/approve")
236
+ async def approve_pull_request(
237
+ task_id: str,
238
+ user: UserInfo = Depends(require_role("admin", "super_admin", "operator")),
239
+ db: aiosqlite.Connection = Depends(get_write_db),
240
+ ):
241
+ """Approva PR. Richiede team_admin del progetto o admin+. Four-eyes: non puoi approvare la tua PR."""
242
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
243
+ try:
244
+ return await uc.approve_pull_request(ctx, db, task_id=task_id, reviewer=user)
245
+ except ServiceError as e:
246
+ raise to_http(e)
247
+
248
+
249
+ @router.post("/{task_id}/request-changes")
250
+ async def request_pr_changes(
251
+ task_id: str,
252
+ body: RequestChangesBody,
253
+ user: UserInfo = Depends(require_role("admin", "super_admin", "operator")),
254
+ db: aiosqlite.Connection = Depends(get_write_db),
255
+ ):
256
+ """Request changes. Revoca approvazione e rimanda task in_progress."""
257
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
258
+ try:
259
+ return await uc.request_pull_request_changes(
260
+ ctx, db, task_id=task_id, reviewer=user, comment=body.comment
261
+ )
262
+ except ServiceError as e:
263
+ raise to_http(e)
264
+
265
+
266
+ @router.post("/{task_id}/close")
267
+ async def close_pull_request(
268
+ task_id: str,
269
+ body: ClosePrRequest,
270
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
271
+ db: aiosqlite.Connection = Depends(get_write_db),
272
+ ):
273
+ """Close PR without merge. Task returns to in_progress."""
274
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
275
+ try:
276
+ return await uc.close_pull_request(ctx, db, task_id=task_id, reason=body.reason)
277
+ except ServiceError as e:
278
+ raise to_http(e)
279
+
280
+
281
+ @router.patch("/{task_id}")
282
+ async def update_pull_request(
283
+ task_id: str,
284
+ body: UpdatePrRequest,
285
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
286
+ db: aiosqlite.Connection = Depends(get_write_db),
287
+ ):
288
+ """Update PR title or body."""
289
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
290
+ try:
291
+ return await uc.update_pull_request(
292
+ ctx, db, task_id=task_id, title=body.title, body=body.body
293
+ )
294
+ except ServiceError as e:
295
+ raise to_http(e)
296
+
297
+
298
+ @router.post("/{task_id}/revert")
299
+ async def revert_pull_request(
300
+ task_id: str,
301
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin", human_only=True)),
302
+ db: aiosqlite.Connection = Depends(get_write_db),
303
+ ):
304
+ """Revert a merged PR. Creates a new task + new PR with git revert commit.
305
+ Human operator+ authorization required (cookie-only ``human_only`` dependency)."""
306
+ ctx = CallerContext.from_user_info(user, is_human_session=True)
307
+ try:
308
+ return await uc.revert_pull_request(ctx, db, task_id=task_id)
309
+ except ServiceError as e:
310
+ raise to_http(e)
311
+ except GitOpsError as exc:
312
+ raise HTTPException(status_code=500, detail=str(exc))
@@ -0,0 +1,67 @@
1
+ # v1.0.0 - 2026-03-13 - Push subscription management (POST/DELETE)
2
+ from __future__ import annotations
3
+
4
+ import aiosqlite
5
+ from fastapi import APIRouter, Depends, HTTPException
6
+ from pydantic import BaseModel
7
+
8
+ from core.api.db import get_db, get_write_db
9
+ from core.api.models import UserInfo
10
+ from core.api.rbac import require_role
11
+ from core.api.services.push_service import MAX_SUBSCRIPTIONS_PER_USER, validate_push_endpoint
12
+
13
+ router = APIRouter(prefix="/api/v1/push-subscriptions", tags=["push"])
14
+
15
+
16
+ class SubscribeRequest(BaseModel):
17
+ endpoint: str
18
+ p256dh: str
19
+ auth: str
20
+
21
+
22
+ class UnsubscribeRequest(BaseModel):
23
+ endpoint: str
24
+
25
+
26
+ @router.post("", status_code=201)
27
+ async def subscribe(
28
+ body: SubscribeRequest,
29
+ user: UserInfo = Depends(require_role("operator", human_only=True)),
30
+ db: aiosqlite.Connection = Depends(get_write_db),
31
+ ) -> dict:
32
+ """Register a push subscription for the current user."""
33
+ if not validate_push_endpoint(body.endpoint):
34
+ raise HTTPException(400, "Invalid push endpoint")
35
+
36
+ # Cap subscriptions per user
37
+ cursor = await db.execute(
38
+ "SELECT COUNT(*) FROM push_subscriptions WHERE user_id = ?",
39
+ [user.user_id],
40
+ )
41
+ row = await cursor.fetchone()
42
+ if row[0] >= MAX_SUBSCRIPTIONS_PER_USER:
43
+ raise HTTPException(400, f"Max {MAX_SUBSCRIPTIONS_PER_USER} subscriptions per user")
44
+
45
+ await db.execute(
46
+ "INSERT OR REPLACE INTO push_subscriptions (endpoint, user_id, p256dh, auth) VALUES (?, ?, ?, ?)",
47
+ [body.endpoint, user.user_id, body.p256dh, body.auth],
48
+ )
49
+ await db.commit()
50
+ return {"status": "subscribed"}
51
+
52
+
53
+ @router.delete("")
54
+ async def unsubscribe(
55
+ body: UnsubscribeRequest,
56
+ user: UserInfo = Depends(require_role("operator", human_only=True)),
57
+ db: aiosqlite.Connection = Depends(get_write_db),
58
+ ) -> dict:
59
+ """Remove a push subscription for the current user."""
60
+ result = await db.execute(
61
+ "DELETE FROM push_subscriptions WHERE endpoint = ? AND user_id = ?",
62
+ [body.endpoint, user.user_id],
63
+ )
64
+ await db.commit()
65
+ if result.rowcount == 0:
66
+ raise HTTPException(404, "Subscription not found")
67
+ return {"status": "unsubscribed"}
@@ -0,0 +1,228 @@
1
+ # v1.3.0 - 2026-03-10 - Remove hardcoded _SYSTEM_ACTOR, use authenticated user for attribution
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from datetime import datetime, timezone
6
+
7
+ import aiosqlite
8
+ from fastapi import APIRouter, Depends, HTTPException
9
+
10
+ from core.api.db import get_db, get_write_db
11
+ from core.api.models import RaciEntry, RaciAddRequest, RaciReplaceRequest, UserInfo, UserSummary
12
+ from core.api.rbac import require_role
13
+ from core.api.security import get_current_user, get_current_user_or_agent
14
+ from core.api.services.events import emit_event
15
+ from core.api.visibility import check_project_access
16
+
17
+ logger = logging.getLogger(__name__)
18
+ router = APIRouter(prefix="/api/v1/projects", tags=["raci"])
19
+
20
+
21
+ async def _get_raci_with_users(
22
+ db: aiosqlite.Connection, project: str
23
+ ) -> list[RaciEntry]:
24
+ """Fetch RACI entries with embedded user summaries, excluding soft-deleted users."""
25
+ db.row_factory = aiosqlite.Row
26
+ async with db.execute(
27
+ """
28
+ SELECT r.role, u.id, u.slug, u.display_name, u.avatar_color
29
+ FROM project_raci r
30
+ JOIN users u ON u.id = r.user_id
31
+ WHERE r.project = ? AND u.deleted_at IS NULL
32
+ ORDER BY
33
+ CASE r.role
34
+ WHEN 'responsible' THEN 1
35
+ WHEN 'accountable' THEN 2
36
+ WHEN 'consulted' THEN 3
37
+ WHEN 'informed' THEN 4
38
+ END,
39
+ u.display_name
40
+ """,
41
+ (project,),
42
+ ) as cursor:
43
+ rows = await cursor.fetchall()
44
+
45
+ return [
46
+ RaciEntry(
47
+ user=UserSummary(
48
+ id=r["id"],
49
+ slug=r["slug"],
50
+ display_name=r["display_name"],
51
+ avatar_color=r["avatar_color"] or "#6366f1",
52
+ ),
53
+ role=r["role"],
54
+ )
55
+ for r in rows
56
+ ]
57
+
58
+
59
+ @router.get("/{slug}/raci", response_model=list[RaciEntry])
60
+ async def get_raci(
61
+ slug: str,
62
+ user=Depends(get_current_user_or_agent),
63
+ db: aiosqlite.Connection = Depends(get_db),
64
+ ):
65
+ """Lista RACI del progetto con dati utente embedded."""
66
+ await check_project_access(slug, user, db)
67
+ return await _get_raci_with_users(db, slug)
68
+
69
+
70
+ @router.post("/{slug}/raci", response_model=list[RaciEntry], status_code=201)
71
+ async def add_raci_entry(
72
+ slug: str,
73
+ body: RaciAddRequest,
74
+ user=Depends(require_role("operator", "admin", "super_admin", human_only=True)),
75
+ db: aiosqlite.Connection = Depends(get_write_db),
76
+ ):
77
+ """Aggiungi singola entry RACI.
78
+
79
+ Fallisce con 409 se Responsible o Accountable sono gia presenti nel progetto
80
+ (partial unique index garantisce uno solo per progetto).
81
+ """
82
+ db.row_factory = aiosqlite.Row
83
+
84
+ # Verifica utente esiste e non e eliminato
85
+ u = await (
86
+ await db.execute(
87
+ "SELECT id FROM users WHERE id = ? AND deleted_at IS NULL",
88
+ (body.user_id,),
89
+ )
90
+ ).fetchone()
91
+ if not u:
92
+ raise HTTPException(status_code=404, detail="User not found")
93
+
94
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
95
+
96
+ try:
97
+ await db.execute(
98
+ "INSERT INTO project_raci (project, user_id, role) VALUES (?, ?, ?)",
99
+ (slug, body.user_id, body.role),
100
+ )
101
+ await db.execute(
102
+ "INSERT INTO project_raci_history "
103
+ "(project, user_id, role, action, changed_by, changed_at, reason) "
104
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
105
+ (slug, body.user_id, body.role, "assign", user.user_id or user.username, now, body.reason),
106
+ )
107
+ await db.commit()
108
+ except Exception as exc:
109
+ if "UNIQUE" in str(exc):
110
+ raise HTTPException(
111
+ status_code=409,
112
+ detail=f"Conflict: role '{body.role}' already assigned in project '{slug}'",
113
+ )
114
+ raise
115
+
116
+ await emit_event(
117
+ db,
118
+ "raci.updated",
119
+ project=slug,
120
+ target_type="raci",
121
+ target_id=slug,
122
+ payload={"action": "assign", "user_id": body.user_id, "role": body.role},
123
+ )
124
+ return await _get_raci_with_users(db, slug)
125
+
126
+
127
+ @router.put("/{slug}/raci", response_model=list[RaciEntry])
128
+ async def replace_raci(
129
+ slug: str,
130
+ body: RaciReplaceRequest,
131
+ user=Depends(require_role("operator", "admin", "super_admin", human_only=True)),
132
+ db: aiosqlite.Connection = Depends(get_write_db),
133
+ ):
134
+ """Replace completo RACI del progetto (idempotente).
135
+
136
+ Revoca tutte le entry esistenti e reinserisce quelle fornite.
137
+ Tutto registrato in project_raci_history per audit trail.
138
+ """
139
+ await check_project_access(slug, user, db)
140
+ db.row_factory = aiosqlite.Row
141
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
142
+
143
+ # Leggi entries esistenti per audit trail
144
+ async with db.execute(
145
+ "SELECT user_id, role FROM project_raci WHERE project = ?", (slug,)
146
+ ) as cursor:
147
+ existing = await cursor.fetchall()
148
+
149
+ # Cancella entries correnti
150
+ await db.execute("DELETE FROM project_raci WHERE project = ?", (slug,))
151
+
152
+ # Registra revoca per audit
153
+ for row in existing:
154
+ await db.execute(
155
+ "INSERT INTO project_raci_history "
156
+ "(project, user_id, role, action, changed_by, changed_at) "
157
+ "VALUES (?, ?, ?, ?, ?, ?)",
158
+ (slug, row["user_id"], row["role"], "revoke", user.user_id or user.username, now),
159
+ )
160
+
161
+ # Reinserisci nuove entries
162
+ for entry in body.entries:
163
+ await db.execute(
164
+ "INSERT OR IGNORE INTO project_raci (project, user_id, role) VALUES (?, ?, ?)",
165
+ (slug, entry.user_id, entry.role),
166
+ )
167
+ await db.execute(
168
+ "INSERT INTO project_raci_history "
169
+ "(project, user_id, role, action, changed_by, changed_at, reason) "
170
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
171
+ (
172
+ slug,
173
+ entry.user_id,
174
+ entry.role,
175
+ "assign",
176
+ user.user_id or user.username,
177
+ now,
178
+ entry.reason,
179
+ ),
180
+ )
181
+
182
+ await db.commit()
183
+
184
+ await emit_event(
185
+ db,
186
+ "raci.updated",
187
+ project=slug,
188
+ target_type="raci",
189
+ target_id=slug,
190
+ payload={"action": "replace", "entries": len(body.entries)},
191
+ )
192
+ return await _get_raci_with_users(db, slug)
193
+
194
+
195
+ @router.delete("/{slug}/raci/{user_id}/{role}", status_code=204)
196
+ async def remove_raci_entry(
197
+ slug: str,
198
+ user_id: str,
199
+ role: str,
200
+ user=Depends(require_role("operator", "admin", "super_admin", human_only=True)),
201
+ db: aiosqlite.Connection = Depends(get_write_db),
202
+ ):
203
+ """Rimuovi singola entry RACI. Registra revoca in audit trail."""
204
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
205
+
206
+ result = await db.execute(
207
+ "DELETE FROM project_raci WHERE project = ? AND user_id = ? AND role = ?",
208
+ (slug, user_id, role),
209
+ )
210
+ if result.rowcount == 0:
211
+ raise HTTPException(status_code=404, detail="RACI entry not found")
212
+
213
+ await db.execute(
214
+ "INSERT INTO project_raci_history "
215
+ "(project, user_id, role, action, changed_by, changed_at) "
216
+ "VALUES (?, ?, ?, ?, ?, ?)",
217
+ (slug, user_id, role, "revoke", user.user_id or user.username, now),
218
+ )
219
+ await db.commit()
220
+
221
+ await emit_event(
222
+ db,
223
+ "raci.updated",
224
+ project=slug,
225
+ target_type="raci",
226
+ target_id=slug,
227
+ payload={"action": "revoke", "user_id": user_id, "role": role},
228
+ )