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,526 @@
1
+ # v3.0.0 - 2026-05-27 - S1 F1.6: thin adapter over use_cases.tasks (CORNERSTONE — human-approval gate)
2
+ # v2.7.0 - 2026-04-22 - Anti-zombie C: zombie-scan + bulk-reject endpoints (weekly aging cleanup)
3
+ # v2.6.0 - 2026-04-17 - KG bug fix: sync new task to graph_nodes on POST /tasks (closes handoff orphan_reason=task_id_not_in_graph)
4
+ # v2.5.0 - 2026-04-12 - Add stale-reset endpoint for REM HYGIENE step
5
+ """HTTP adapter for the tasks domain (S1 collapse-runtime CORNERSTONE).
6
+
7
+ This router is a thin transport adapter. All CRUD/validation/RBAC/visibility/
8
+ transition logic — including the human-approval four-eyes gate — lives in
9
+ :mod:`core.api.use_cases.tasks` (pure, fastapi-free). Each handler resolves
10
+ identity into a :class:`CallerContext`, calls the use_case inside
11
+ ``try/except ServiceError`` -> ``to_http``, and owns the transport concerns.
12
+
13
+ CORNERSTONE — the human-approval gate. The use_case checks ``ctx.is_human_session``
14
+ and raises ``AuthorizationError(code="approval_requires_human")``. This adapter
15
+ fills ``is_human_session`` from the ``pir_session`` cookie and re-raises that ONE
16
+ error as the legacy PLAIN-STRING 403 (``detail`` is a string, not a ``{code,message}``
17
+ dict), preserving the HTTP contract pinned by
18
+ ``tests/test_tasks.py::test_pending_to_approved_requires_cookie_auth``. Likewise
19
+ the transition + completion guards (``invalid transition`` 422,
20
+ ``review-needs-PR`` 422, ``completed-needs-no-open-PR`` 422,
21
+ ``completed-needs-merged-PR`` 422) keep the legacy PLAIN-STRING detail via
22
+ ``_transition_to_http`` below, so the existing ``"merged pr workflow"`` substring
23
+ assertion keeps matching. Non-transition errors (404 not-found, 409 duplicate /
24
+ delete-in-progress, 403 insufficient-permissions) flow through ``to_http`` as the
25
+ structured ``{code,message}`` body — their tests only check the status code.
26
+
27
+ STAYS IN THE ADAPTER (transport concerns):
28
+ * ``_check_rate_limit`` (in-memory per-process 429 guard) — called before delegating.
29
+ * ``get_task`` ``deep`` KG enrichment (rate-limit + access log + ``kg_context``)
30
+ and the ``list_tasks`` ``deep_requires_filter`` 400 guard (DECISION 2/3).
31
+ * session-owner resolution (``resolve_session_owner`` reads ``X-Session-Name``)
32
+ and visibility resolution (``get_visible_projects``) — resolved here, passed in.
33
+ * ``POST /{id}/cost-entries`` keeps its cookie-only ``Depends(get_current_user)``.
34
+
35
+ CALLABLE SEAMS kept in this module so existing import/monkeypatch tests stay valid
36
+ and the use_case stays fastapi-free:
37
+ * ``sync_task_to_graph`` — imported here and passed into ``create_task`` (the
38
+ create-path source references the symbol; the update-path never does).
39
+ * ``_schedule_embed_task`` + ``_bg_embed_tasks`` — defined here; tests call
40
+ ``tasks_router._schedule_embed_task`` directly.
41
+ * ``_project_requires_pr_gate`` — defined here; passed into ``update_task`` at
42
+ call time so ``monkeypatch.setattr(tasks_router, "_project_requires_pr_gate", ...)``
43
+ takes effect.
44
+ """
45
+ from __future__ import annotations
46
+
47
+ import asyncio
48
+ import logging
49
+ import time
50
+ from collections import defaultdict
51
+
52
+ import aiosqlite
53
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
54
+
55
+ from core.api.config import settings
56
+ from core.api.db import get_db, get_write_db
57
+ from core.api.models import (
58
+ HumanCostEntryCreate,
59
+ ProjectSummary,
60
+ TaskCostSummary,
61
+ TaskCreateRequest,
62
+ TaskDetailResponse,
63
+ TaskSummary,
64
+ TaskUpdateRequest,
65
+ UserInfo,
66
+ )
67
+ from core.api.rbac import require_role
68
+ from core.api.routers._adapter import to_http
69
+ from core.api.security import (
70
+ get_current_user,
71
+ get_current_user_or_agent,
72
+ resolve_session_owner,
73
+ )
74
+ from core.api.services.graph_service import sync_task_to_graph
75
+ from core.api.services.kg.audit import check_deep_rate_limit, log_kg_deep_access
76
+ from core.api.services.kg.lens import build_kg_context_for_task
77
+ from core.api.use_cases import tasks as uc
78
+ from core.api.use_cases._context import CallerContext
79
+ from core.api.use_cases._errors import AuthorizationError, ServiceError, ValidationError
80
+ from core.api.visibility import get_visible_projects
81
+
82
+ # Re-export the moved DTOs + pure helpers + constants from the use_case so that
83
+ # (a) `response_model=` below references the same classes and (b) existing
84
+ # importers keep working unchanged.
85
+ from core.api.use_cases.tasks import ( # noqa: F401 (re-export surface)
86
+ ZOMBIE_THRESHOLD_DAYS_DEFAULT,
87
+ BulkRejectRequest,
88
+ BulkRejectResponse,
89
+ ZombieScanResponse,
90
+ _base_task_fields,
91
+ _PR_STATUS_SUBQUERY,
92
+ _row_to_task,
93
+ _row_to_task_list,
94
+ )
95
+
96
+ logger = logging.getLogger(__name__)
97
+
98
+ # Background task set (prevents GC). Kept in the adapter — task auto-embedding is a
99
+ # per-surface concern and `tests/test_task_auto_embedding.py` references
100
+ # `tasks_router._bg_embed_tasks` / `tasks_router._schedule_embed_task` directly.
101
+ _bg_embed_tasks: set[asyncio.Task] = set()
102
+
103
+
104
+ def _schedule_embed_task(
105
+ task_id: str, title: str, project: str, status: str, workspace_id: str
106
+ ) -> None:
107
+ """Fire-and-forget: embed task in background. Silently no-ops if embedder unavailable.
108
+
109
+ Thin HTTP-surface seam: the embed body itself lives in the fastapi-free
110
+ ``embedding_service.embed_task_document`` (the SAME helper the MCP surface calls
111
+ via ``mcp._adapter.mcp_schedule_embed`` — no fork, S1 F4). This wrapper keeps its
112
+ name/signature/module + ``_bg_embed_tasks`` set because
113
+ ``tests/test_task_auto_embedding.py`` references them directly.
114
+ """
115
+ from core.api.services import embedding_service
116
+
117
+ if not embedding_service.is_available():
118
+ return
119
+
120
+ async def _embed() -> None:
121
+ try:
122
+ await embedding_service.embed_task_document(
123
+ task_id=task_id,
124
+ title=title,
125
+ project=project,
126
+ status=status,
127
+ workspace_id=workspace_id,
128
+ )
129
+ except Exception:
130
+ logger.debug(
131
+ "Auto-embed task %s failed (non-critical)", task_id, exc_info=True
132
+ )
133
+
134
+ t = asyncio.create_task(_embed())
135
+ _bg_embed_tasks.add(t)
136
+ t.add_done_callback(_bg_embed_tasks.discard)
137
+
138
+
139
+ def _project_requires_pr_gate(project_slug: str | None) -> bool:
140
+ """Code/system projects must close through the PR workflow, not direct task completion."""
141
+ if not project_slug:
142
+ return False
143
+ try:
144
+ from core.api.routers.projects import _find_project_entry
145
+
146
+ entry = _find_project_entry(project_slug)
147
+ except Exception:
148
+ logger.warning(
149
+ "Failed to resolve project type for %s", project_slug, exc_info=True
150
+ )
151
+ return False
152
+ return bool(entry and entry.project_type in {"code", "system"})
153
+
154
+
155
+ router = APIRouter(prefix="/api/v1/tasks", tags=["tasks"])
156
+
157
+ # In-memory rate limiter: token -> list of timestamps
158
+ _rate_limits: dict[str, list[float]] = defaultdict(list)
159
+ _RATE_LIMITS_MAX_KEYS = 1000
160
+
161
+
162
+ def _check_rate_limit(identity: str) -> None:
163
+ """Simple sliding window rate limiter (in-memory, per-process)."""
164
+ now = time.time()
165
+ window = 60.0
166
+ max_requests = settings.tasks_rate_limit_per_min
167
+
168
+ timestamps = _rate_limits[identity]
169
+ # Remove old entries
170
+ _rate_limits[identity] = [t for t in timestamps if now - t < window]
171
+ if len(_rate_limits[identity]) >= max_requests:
172
+ raise HTTPException(
173
+ status_code=429,
174
+ detail=(
175
+ f"Rate limit exceeded for '{identity}': {max_requests} requests/minute. "
176
+ "Reason: per-identity sliding window to prevent runaway loops. "
177
+ "Fix: wait 60s before retrying, or batch operations (e.g. fetch task lists once, cache in memory). "
178
+ "If you need a higher limit, ask an admin to bump settings.tasks_rate_limit_per_min."
179
+ ),
180
+ )
181
+ _rate_limits[identity].append(now)
182
+ # Evict stale entries periodically
183
+ if len(_rate_limits) > _RATE_LIMITS_MAX_KEYS:
184
+ stale = [k for k, v in _rate_limits.items() if not v or (now - v[-1]) > window]
185
+ for k in stale:
186
+ del _rate_limits[k]
187
+
188
+
189
+ def _transition_to_http(err: ValidationError) -> HTTPException:
190
+ """Re-raise a transition/guard ValidationError as the legacy PLAIN-STRING body.
191
+
192
+ The original router raised ``HTTPException(422, detail="<plain string>")`` for
193
+ transition + completion-guard errors. Existing tests assert on that string
194
+ (e.g. ``"merged pr workflow" in detail.lower()``), so for this family we keep
195
+ the detail as a plain string rather than ``to_http``'s ``{code,message}`` dict.
196
+ """
197
+ return HTTPException(status_code=err.http_status, detail=err.message)
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # Endpoints (thin adapters)
202
+ # ---------------------------------------------------------------------------
203
+
204
+
205
+ @router.get("/summary")
206
+ async def get_tasks_summary(
207
+ user: UserInfo = Depends(get_current_user_or_agent),
208
+ db: aiosqlite.Connection = Depends(get_db),
209
+ ) -> TaskSummary:
210
+ """Cross-project task summary."""
211
+ _check_rate_limit(user.username)
212
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
213
+ try:
214
+ return await uc.get_tasks_summary(ctx, db)
215
+ except ServiceError as e:
216
+ raise to_http(e)
217
+
218
+
219
+ @router.get("/projects")
220
+ async def get_task_projects(
221
+ user: UserInfo = Depends(get_current_user_or_agent),
222
+ db: aiosqlite.Connection = Depends(get_db),
223
+ ) -> list[ProjectSummary]:
224
+ """List projects with task counts."""
225
+ _check_rate_limit(user.username)
226
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
227
+ try:
228
+ return await uc.get_task_projects(ctx, db)
229
+ except ServiceError as e:
230
+ raise to_http(e)
231
+
232
+
233
+ @router.post("", status_code=201)
234
+ async def create_task(
235
+ body: TaskCreateRequest,
236
+ request: Request,
237
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
238
+ db: aiosqlite.Connection = Depends(get_write_db),
239
+ ) -> TaskDetailResponse:
240
+ """Create a new task."""
241
+ _check_rate_limit(user.username)
242
+
243
+ # Session-based attribution: when an MCP agent sends X-Session-Name, attribute
244
+ # the task to the session's human owner instead of the agent identity. This is
245
+ # a transport concern (reads `request`), resolved here and passed in.
246
+ created_by_value = user.username
247
+ session_owner = await resolve_session_owner(request, db)
248
+ if session_owner:
249
+ created_by_value = session_owner
250
+
251
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
252
+ try:
253
+ return await uc.create_task(
254
+ ctx,
255
+ db,
256
+ body=body,
257
+ created_by=created_by_value,
258
+ sync_graph=sync_task_to_graph,
259
+ schedule_embed=_schedule_embed_task,
260
+ )
261
+ except ServiceError as e:
262
+ raise to_http(e)
263
+
264
+
265
+ @router.get("", response_model=list[TaskDetailResponse])
266
+ async def list_tasks(
267
+ project: str | None = Query(None, pattern=r"^[a-z0-9][a-z0-9_.\-]{0,126}$"),
268
+ status: str | None = None,
269
+ kind: str | None = Query(None, pattern=r"^(normal|idea)$"),
270
+ priority: str | None = None,
271
+ created_by: str | None = None,
272
+ owner_id: str | None = None,
273
+ delegation: str | None = None,
274
+ tags: str | None = Query(None, description="Comma-separated tags (OR logic)"),
275
+ limit: int = Query(20, ge=1, le=500),
276
+ offset: int = Query(0, ge=0),
277
+ sort: str = Query("created_at:desc"),
278
+ include_deleted: bool = False,
279
+ detailed: bool = Query(False, description="Include description and comments"),
280
+ deep: bool = Query(False),
281
+ user: UserInfo = Depends(get_current_user_or_agent),
282
+ db: aiosqlite.Connection = Depends(get_db),
283
+ ) -> list[TaskDetailResponse]:
284
+ """List tasks with filters."""
285
+ _check_rate_limit(user.username)
286
+
287
+ # DECISION 3: deep_requires_filter is an adapter-owned guard tied to the
288
+ # adapter-owned `deep` feature. Keep the exact 400 (NOT via ServiceError).
289
+ if deep and not project:
290
+ raise HTTPException(
291
+ status_code=400,
292
+ detail="deep=true requires ?project=<slug> filter (aggregate KG context needs a project scope)",
293
+ )
294
+
295
+ # DECISION 1 (the visibility template): resolve visibility at the boundary
296
+ # (needs UserInfo.teams, not carried by CallerContext) and pass it in.
297
+ visible_projects = await get_visible_projects(db, user)
298
+
299
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
300
+ try:
301
+ return await uc.list_tasks(
302
+ ctx,
303
+ db,
304
+ project=project,
305
+ status=status,
306
+ kind=kind,
307
+ priority=priority,
308
+ created_by=created_by,
309
+ owner_id=owner_id,
310
+ delegation=delegation,
311
+ tags=tags,
312
+ limit=limit,
313
+ offset=offset,
314
+ sort=sort,
315
+ include_deleted=include_deleted,
316
+ detailed=detailed,
317
+ visible_projects=visible_projects,
318
+ )
319
+ except ServiceError as e:
320
+ raise to_http(e)
321
+
322
+
323
+ @router.get("/{task_id}", response_model=TaskDetailResponse)
324
+ async def get_task(
325
+ task_id: str,
326
+ deep_param: bool | None = Query(None, alias="deep"),
327
+ user: UserInfo = Depends(get_current_user_or_agent),
328
+ db: aiosqlite.Connection = Depends(get_db),
329
+ ) -> TaskDetailResponse:
330
+ """Get a single task by ID."""
331
+ _check_rate_limit(user.username)
332
+
333
+ # DECISION 1: resolve visibility at the boundary, enforce in the use_case.
334
+ visible_projects = await get_visible_projects(db, user)
335
+
336
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
337
+ try:
338
+ result = await uc.get_task(
339
+ ctx, db, task_id=task_id, visible_projects=visible_projects
340
+ )
341
+ except ServiceError as e:
342
+ raise to_http(e)
343
+
344
+ # DECISION 2: deep KG enrichment is a per-surface adapter concern.
345
+ deep = deep_param if deep_param is not None else settings.kg_http_deep_default
346
+ deep_source = "client" if deep_param is not None else "env"
347
+ if deep:
348
+ check_deep_rate_limit(user.username)
349
+ log_kg_deep_access(user.username, "get_task", task_id)
350
+ result.kg_context = await build_kg_context_for_task(db, task_id, deep=True)
351
+ if result.kg_context and "meta" in result.kg_context:
352
+ result.kg_context["meta"]["deep_effective"] = deep
353
+ result.kg_context["meta"]["deep_default_source"] = deep_source
354
+ return result
355
+
356
+
357
+ @router.patch("/{task_id}", response_model=TaskDetailResponse)
358
+ async def update_task(
359
+ task_id: str,
360
+ body: TaskUpdateRequest,
361
+ request: Request,
362
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
363
+ db: aiosqlite.Connection = Depends(get_write_db),
364
+ ) -> TaskDetailResponse:
365
+ """Update a task. Validates status transitions."""
366
+ _check_rate_limit(user.username)
367
+
368
+ # CORNERSTONE: the cookie is the only transport signal for "human session".
369
+ # Fill ctx.is_human_session here; the use_case decides the four-eyes gate.
370
+ ctx = CallerContext.from_user_info(
371
+ user, is_human_session=bool(request.cookies.get("pir_session"))
372
+ )
373
+ try:
374
+ return await uc.update_task(
375
+ ctx,
376
+ db,
377
+ task_id=task_id,
378
+ body=body,
379
+ requires_pr_gate=_project_requires_pr_gate,
380
+ schedule_embed=_schedule_embed_task,
381
+ )
382
+ except AuthorizationError as e:
383
+ if e.code == "approval_requires_human":
384
+ # Preserve the legacy plain-string 403 body (HTTP contract parity).
385
+ raise HTTPException(status_code=403, detail=uc.APPROVAL_REQUIRES_HUMAN_DETAIL)
386
+ raise to_http(e)
387
+ except ValidationError as e:
388
+ # Transition + completion guards keep the legacy plain-string 422 detail.
389
+ raise _transition_to_http(e)
390
+ except ServiceError as e:
391
+ raise to_http(e)
392
+
393
+
394
+ @router.get("/{task_id}/cost-entries", response_model=TaskCostSummary)
395
+ async def get_task_cost_entries(
396
+ task_id: str,
397
+ user: UserInfo = Depends(get_current_user_or_agent),
398
+ db: aiosqlite.Connection = Depends(get_db),
399
+ ) -> TaskCostSummary:
400
+ """Return cost summary + all entries for a task."""
401
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
402
+ try:
403
+ return await uc.get_task_cost_entries(ctx, db, task_id=task_id)
404
+ except ServiceError as e:
405
+ raise to_http(e)
406
+
407
+
408
+ @router.post("/{task_id}/cost-entries", response_model=TaskCostSummary, status_code=201)
409
+ async def create_human_cost_entry(
410
+ task_id: str,
411
+ body: HumanCostEntryCreate,
412
+ user: UserInfo = Depends(get_current_user),
413
+ db: aiosqlite.Connection = Depends(get_write_db),
414
+ ) -> TaskCostSummary:
415
+ """Manually record human time for a task. Cookie auth required."""
416
+ _check_rate_limit(user.username)
417
+ # Cookie-only auth: get_current_user already enforces a human session.
418
+ ctx = CallerContext.from_user_info(user, is_human_session=True)
419
+ try:
420
+ return await uc.create_human_cost_entry(ctx, db, task_id=task_id, body=body)
421
+ except ServiceError as e:
422
+ raise to_http(e)
423
+
424
+
425
+ @router.delete("/{task_id}", status_code=204)
426
+ async def delete_task(
427
+ task_id: str,
428
+ user: UserInfo = Depends(require_role("admin", "super_admin")),
429
+ db: aiosqlite.Connection = Depends(get_write_db),
430
+ ) -> None:
431
+ """Soft delete a task. Cannot delete in_progress tasks."""
432
+ _check_rate_limit(user.username)
433
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
434
+ try:
435
+ await uc.delete_task(ctx, db, task_id=task_id)
436
+ except ServiceError as e:
437
+ raise to_http(e)
438
+
439
+
440
+ @router.post("/reminders/check", status_code=200)
441
+ async def trigger_reminder_check(
442
+ user: UserInfo = Depends(require_role("operator")),
443
+ ) -> dict:
444
+ """Trigger manual reminder check. Called by cron or agent."""
445
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
446
+ try:
447
+ return await uc.trigger_reminder_check(ctx)
448
+ except ServiceError as e:
449
+ raise to_http(e)
450
+
451
+
452
+ @router.post("/stale-reset", status_code=200)
453
+ async def reset_stale_tasks(
454
+ stale_days: int = Query(
455
+ 7, ge=1, le=90, description="Days without update to consider stale"
456
+ ),
457
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
458
+ db: aiosqlite.Connection = Depends(get_write_db),
459
+ ) -> dict:
460
+ """Reset stale in_progress tasks back to approved.
461
+
462
+ Finds tasks with status='in_progress' and updated_at older than
463
+ stale_days ago, resets them to 'approved', and adds a 'stale_reset' tag.
464
+ Called by REM in its HYGIENE step.
465
+ """
466
+ _check_rate_limit(user.username)
467
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
468
+ try:
469
+ return await uc.reset_stale_tasks(ctx, db, stale_days=stale_days)
470
+ except ServiceError as e:
471
+ raise to_http(e)
472
+
473
+
474
+ @router.post("/zombie-scan", response_model=ZombieScanResponse)
475
+ async def zombie_scan(
476
+ threshold_days: int = Query(
477
+ ZOMBIE_THRESHOLD_DAYS_DEFAULT,
478
+ ge=7,
479
+ le=365,
480
+ description="Days without update to flag a task as zombie (default 21)",
481
+ ),
482
+ dry_run: bool = Query(
483
+ False, description="If true, scan only — no notifications written"
484
+ ),
485
+ user: UserInfo = Depends(require_role("operator", "admin", "super_admin")),
486
+ db: aiosqlite.Connection = Depends(get_write_db),
487
+ ) -> ZombieScanResponse:
488
+ """Scan for aging zombie tasks and emit one Console notification per project.
489
+
490
+ Zombie = approved + updated_at < now - threshold_days + no open/draft/merging PR
491
+ + no 'dormant-ok' tag. One notification per project aggregates the task list.
492
+
493
+ Called by weekly systemd user timer (`marvisx-zombie-detect.timer`) or ad-hoc
494
+ from Console. dry_run=true returns the report without writing notifications.
495
+ """
496
+ _check_rate_limit(user.username)
497
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
498
+ try:
499
+ return await uc.zombie_scan(
500
+ ctx, db, threshold_days=threshold_days, dry_run=dry_run
501
+ )
502
+ except ServiceError as e:
503
+ raise to_http(e)
504
+
505
+
506
+ @router.post("/bulk-reject", response_model=BulkRejectResponse)
507
+ async def bulk_reject_tasks(
508
+ body: BulkRejectRequest,
509
+ user: UserInfo = Depends(require_role("admin", "super_admin")),
510
+ db: aiosqlite.Connection = Depends(get_write_db),
511
+ ) -> BulkRejectResponse:
512
+ """Bulk-reject multiple tasks with a reason. Admin-only.
513
+
514
+ Use case: weekly zombie cleanup triggered from Console notification.
515
+ Validates each transition via VALID_TRANSITIONS (approved/pending/failed
516
+ → rejected). Logs one audit entry per successful rejection plus a summary
517
+ audit entry for the batch. Partial failures are reported, not raised.
518
+ """
519
+ _check_rate_limit(user.username)
520
+ ctx = CallerContext.from_user_info(user, is_human_session=False)
521
+ try:
522
+ return await uc.bulk_reject_tasks(
523
+ ctx, db, task_ids=body.task_ids, reason=body.reason
524
+ )
525
+ except ServiceError as e:
526
+ raise to_http(e)