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
core/api/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,59 @@
1
+ # v1.0.0 - 2026-03-02 - DevX session health checker (v1: dimension C only)
2
+ """Session health check — DevX Layer Sprint 3.
3
+
4
+ v1 implementa solo Dimensione C (Continuita): sessione idle senza input richiesto.
5
+ Le dimensioni A (task hygiene), B (context%), D (output quality) sono deferred Sprint 4.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from dataclasses import dataclass
11
+ from datetime import datetime, timezone
12
+
13
+ from core.api.models import SessionInfo
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ IDLE_THRESHOLD_MINUTES = 15
18
+ SESSION_MANAGER_COOLDOWN_MINUTES = 30 # Rate limit: max 1 msg per sessione per 30 min
19
+
20
+
21
+ @dataclass
22
+ class HealthCheckResult:
23
+ session_name: str
24
+ action: str # "send_message" | "escalate" | "ok"
25
+ message: str | None
26
+ escalation_reason: str | None
27
+
28
+
29
+ async def check_session_health(
30
+ session: SessionInfo,
31
+ last_action_at: datetime | None,
32
+ ) -> HealthCheckResult:
33
+ """v1: Solo dimensione C — sessione idle senza input richiesto.
34
+
35
+ Le dimensioni A, B, D sono deferred alla prossima sprint
36
+ dopo validazione di C in produzione.
37
+ """
38
+ # Rate limit: non mandare piu di 1 messaggio per cooldown period
39
+ if last_action_at is not None:
40
+ elapsed = (datetime.now(timezone.utc) - last_action_at).total_seconds() / 60
41
+ if elapsed < SESSION_MANAGER_COOLDOWN_MINUTES:
42
+ return HealthCheckResult(session.name, "ok", None, None)
43
+
44
+ # C — Continuita: sessione idle senza input richiesto
45
+ if session.activity_state == "idle" and not _session_waiting_for_input(session):
46
+ return HealthCheckResult(
47
+ session.name,
48
+ "send_message",
49
+ "La sessione sembra ferma. Continua con il task corrente. "
50
+ "Ricorda di aggiornare i task e fare handoff a fine sessione.",
51
+ None,
52
+ )
53
+
54
+ return HealthCheckResult(session.name, "ok", None, None)
55
+
56
+
57
+ def _session_waiting_for_input(session: SessionInfo) -> bool:
58
+ """True se la sessione CC sta aspettando input (needs_input activity state)."""
59
+ return session.activity_state == "needs_input"
@@ -0,0 +1,206 @@
1
+ # v1.0.0 - 2026-03-02 - DevX Session Manager Agent
2
+ """Session Manager Agent — DevX Layer Sprint 3.
3
+
4
+ Runs every 10 minutes via host cron or manual trigger.
5
+ Monitors sessions with agent_managed=True, runs health checks, acts on results.
6
+
7
+ Usage:
8
+ cd /data/pir && python -m api.agents.session_manager
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ import logging
15
+ import os
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+ import aiosqlite
20
+
21
+ from core.api.config import settings
22
+ from core.api.agents.session_health import check_session_health
23
+ from core.api.models import SessionInfo
24
+ from core.api.services import tmux
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ ERRORS_DIR = Path("/data/pir/logs/errors")
29
+
30
+ # ── helpers ──────────────────────────────────────────────────────────────────
31
+
32
+ async def list_agent_managed_sessions() -> list[SessionInfo]:
33
+ """Return all sessions with agent_managed=True via the sessions API.
34
+
35
+ Uses the internal API endpoint to get live activity_state (computed from tmux,
36
+ not stored in DB). Falls back to empty list on error.
37
+ """
38
+ import urllib.request
39
+ from core.api.config import settings as cfg
40
+ token = cfg.tasks_api_token if hasattr(cfg, "tasks_api_token") else os.environ.get("TASKS_API_TOKEN", "")
41
+ api_base = os.environ.get("PIR_INTERNAL_BASE", "http://localhost:8100")
42
+ try:
43
+ req = urllib.request.Request(
44
+ f"{api_base}/api/v1/sessions?agent_managed=true",
45
+ headers={
46
+ "Authorization": f"Bearer {token}",
47
+ "X-Agent-Name": "marvisx",
48
+ },
49
+ )
50
+ with urllib.request.urlopen(req, timeout=10) as resp:
51
+ data = json.loads(resp.read())
52
+ return [SessionInfo(**s) for s in data]
53
+ except Exception as exc:
54
+ logger.error("Failed to list agent_managed sessions: %s", exc)
55
+ return []
56
+
57
+
58
+ async def get_last_agent_action(db: aiosqlite.Connection, session_name: str) -> datetime | None:
59
+ """Return the last time an automated action was taken on this session."""
60
+ try:
61
+ cursor = await db.execute(
62
+ "SELECT MAX(created_at) FROM agent_actions WHERE session_name = ?",
63
+ (session_name,),
64
+ )
65
+ row = await cursor.fetchone()
66
+ if row and row[0]:
67
+ # Parse ISO string from SQLite — may be naive (no tz offset)
68
+ ts_str: str = row[0]
69
+ ts_str = ts_str.replace("Z", "+00:00")
70
+ dt = datetime.fromisoformat(ts_str)
71
+ # SQLite stores UTC without offset — assume UTC if naive
72
+ if dt.tzinfo is None:
73
+ dt = dt.replace(tzinfo=timezone.utc)
74
+ return dt
75
+ except Exception as exc:
76
+ logger.warning("Failed to get last agent action for %s: %s", session_name, exc)
77
+ return None
78
+
79
+
80
+ async def log_agent_action(
81
+ db: aiosqlite.Connection,
82
+ session_name: str,
83
+ action: str,
84
+ detail: str | None,
85
+ ) -> None:
86
+ """Log an automated action taken on a session."""
87
+ try:
88
+ await db.execute(
89
+ """
90
+ INSERT INTO agent_actions (agent_name, session_name, action, detail, created_at)
91
+ VALUES (?, ?, ?, ?, ?)
92
+ """,
93
+ ("session-manager", session_name, action, detail, datetime.now(timezone.utc).isoformat()),
94
+ )
95
+ await db.commit()
96
+ except Exception as exc:
97
+ logger.error("Failed to log agent action: %s", exc)
98
+
99
+
100
+ async def send_session_message(session_name: str, text: str) -> bool:
101
+ """Send a message to a tmux session."""
102
+ try:
103
+ return await tmux.send_keys(session_name, text)
104
+ except Exception as exc:
105
+ logger.error("Failed to send message to session %s: %s", session_name, exc)
106
+ return False
107
+
108
+
109
+ async def send_telegram_alert(text: str) -> None:
110
+ """Send a Telegram alert (best-effort)."""
111
+ import urllib.request
112
+ if not settings.telegram_bot_token or not settings.telegram_owner_chat_id:
113
+ logger.info("Telegram not configured, skipping alert")
114
+ return
115
+ try:
116
+ url = f"https://api.telegram.org/bot{settings.telegram_bot_token}/sendMessage"
117
+ data = json.dumps({
118
+ "chat_id": settings.telegram_owner_chat_id,
119
+ "text": text,
120
+ "parse_mode": "HTML",
121
+ }).encode("utf-8")
122
+ req = urllib.request.Request(
123
+ url, data=data,
124
+ headers={"Content-Type": "application/json"},
125
+ method="POST",
126
+ )
127
+ with urllib.request.urlopen(req, timeout=5) as resp:
128
+ resp.read()
129
+ except Exception as exc:
130
+ logger.warning("Telegram alert failed: %s", exc)
131
+
132
+
133
+ async def log_error_to_knowledge(session_name: str, error_text: str) -> None:
134
+ """Log an error to the knowledge errors store for downstream analysis."""
135
+ ERRORS_DIR.mkdir(parents=True, exist_ok=True)
136
+ ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
137
+ error_file = ERRORS_DIR / f"{ts}-session-manager-{session_name}.txt"
138
+ try:
139
+ error_file.write_text(
140
+ f"session: {session_name}\nerror: {error_text}\nts: {ts}\n"
141
+ )
142
+ except Exception as exc:
143
+ logger.error("Failed to write error file: %s", exc)
144
+
145
+
146
+ # ── core loop ────────────────────────────────────────────────────────────────
147
+
148
+ async def _process_session(
149
+ db: aiosqlite.Connection,
150
+ session: SessionInfo,
151
+ ) -> None:
152
+ """Process a single session — isolated to not block others."""
153
+ last_action_at = await get_last_agent_action(db, session.name)
154
+ result = await check_session_health(session, last_action_at)
155
+
156
+ if result.action == "send_message" and result.message:
157
+ success = await send_session_message(session.name, result.message)
158
+ if success:
159
+ await log_agent_action(db, session.name, "health_check:C", result.message)
160
+ logger.info("Sent health check message to session %s", session.name)
161
+ else:
162
+ logger.warning("Failed to send message to session %s", session.name)
163
+
164
+ elif result.action == "escalate" and result.escalation_reason:
165
+ await send_telegram_alert(
166
+ f"<b>DevX Escalation</b> — <code>{session.name}</code>\n\n{result.escalation_reason}"
167
+ )
168
+ await log_agent_action(db, session.name, "escalate", result.escalation_reason)
169
+ logger.warning("Escalated session %s: %s", session.name, result.escalation_reason)
170
+
171
+
172
+ async def run_session_manager() -> None:
173
+ """Main loop: monitor agent_managed sessions, run health checks."""
174
+ logger.info("Session Manager starting...")
175
+ from core.api.db import write_db
176
+ async with write_db() as db:
177
+ sessions = await list_agent_managed_sessions()
178
+ if not sessions:
179
+ logger.info("No agent_managed sessions to monitor")
180
+ return
181
+
182
+ logger.info("Checking %d agent_managed session(s)...", len(sessions))
183
+
184
+ results = await asyncio.gather(
185
+ *[_process_session(db, s) for s in sessions],
186
+ return_exceptions=True,
187
+ )
188
+
189
+ for session, result in zip(sessions, results):
190
+ if isinstance(result, Exception):
191
+ logger.error(
192
+ "Health check failed for session %s: %s", session.name, result
193
+ )
194
+ await log_error_to_knowledge(session.name, str(result))
195
+
196
+ logger.info("Session Manager complete")
197
+
198
+
199
+ # ── CLI entrypoint ────────────────────────────────────────────────────────────
200
+
201
+ if __name__ == "__main__":
202
+ logging.basicConfig(
203
+ level=logging.INFO,
204
+ format="%(asctime)s %(levelname)s %(name)s — %(message)s",
205
+ )
206
+ asyncio.run(run_session_manager())
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ # v1.0.0 - 2026-04-26 - Claude Code session state hook (PR2 plan 2026-04-26)
3
+ """Push Claude Code session lifecycle events to MarvisX backend.
4
+
5
+ Wired to ~/.claude/settings.json hooks (PreToolUse, Stop, StopFailure,
6
+ PermissionRequest, SessionStart, SessionEnd). Reads the Claude hook JSON
7
+ payload from stdin, resolves the tmux session name, POSTs to
8
+ `/api/v1/sessions/{name}/state`.
9
+
10
+ Design choices (plan §M2/M3/M5/M6):
11
+
12
+ - **Python over bash** (M5): single-process startup ~25ms vs bash+jq
13
+ fork chain at 80-150ms. Anthropic's <50ms PreToolUse target is reachable.
14
+
15
+ - **fork + setsid + urllib** (M3): the parent process exits immediately so
16
+ Claude's hook completes fast. The child detaches from the parent's process
17
+ group via `os.setsid()` and uses urllib in-process (no curl exec, no token
18
+ leaking into argv visible in `ps -ef`). Survives parent SIGHUP — critical
19
+ for `Stop`/`SessionEnd` events fired during Claude shutdown.
20
+
21
+ - **Token from file, not env** (M6): `~/.marvisx/agent-token` (mode 0600).
22
+ Env vars leak via `/proc/<pid>/environ` to other users on shared machines.
23
+
24
+ - **Client-emitted ts** (M2): the backend uses this as the LWW key so that
25
+ out-of-order arrival between uvicorn workers doesn't leave the session
26
+ stuck in a stale state.
27
+
28
+ - **Fail silent**: any error → exit 0. Hooks are fire-and-forget; surfacing
29
+ errors to Claude would be more noise than signal.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import os
35
+ import socket
36
+ import sys
37
+ from datetime import datetime, timezone
38
+ from pathlib import Path
39
+ from urllib import error, request
40
+
41
+ API_URL = os.environ.get("MARVISX_API_URL", "http://127.0.0.1:8100")
42
+ TOKEN_FILE = Path.home() / ".marvisx" / "agent-token"
43
+ TIMEOUT_SECS = 5
44
+
45
+
46
+ def _read_token() -> str | None:
47
+ try:
48
+ if not TOKEN_FILE.exists():
49
+ return None
50
+ st = TOKEN_FILE.stat()
51
+ # Defense-in-depth: warn on loose perms but don't fail.
52
+ if st.st_mode & 0o077:
53
+ print(
54
+ f"[marvisx-state-hook] WARN: {TOKEN_FILE} permissive (chmod 600)",
55
+ file=sys.stderr,
56
+ )
57
+ return TOKEN_FILE.read_text(encoding="utf-8").strip() or None
58
+ except OSError:
59
+ return None
60
+
61
+
62
+ def _resolve_session_name() -> str | None:
63
+ """Find the tmux session this hook is running inside.
64
+
65
+ Priority:
66
+ 1. `TMUX_SESSION_NAME` env (exported by the launcher, plan §M9)
67
+ 2. `MARVISX_SESSION_NAME` env (alternative explicit override)
68
+ 3. `tmux display -p '#{session_name}'` if `$TMUX` is set
69
+ """
70
+ name = os.environ.get("TMUX_SESSION_NAME") or os.environ.get(
71
+ "MARVISX_SESSION_NAME"
72
+ )
73
+ if name:
74
+ return name
75
+ if not os.environ.get("TMUX"):
76
+ return None
77
+ import subprocess
78
+
79
+ try:
80
+ result = subprocess.run(
81
+ ["tmux", "display", "-p", "#{session_name}"],
82
+ capture_output=True,
83
+ text=True,
84
+ timeout=2,
85
+ check=False,
86
+ )
87
+ if result.returncode == 0:
88
+ stripped = result.stdout.strip()
89
+ return stripped or None
90
+ except (OSError, subprocess.SubprocessError):
91
+ return None
92
+ return None
93
+
94
+
95
+ def _post(payload: dict, token: str, session_name: str) -> None:
96
+ """Send POST to backend. Called from the detached child only."""
97
+ url = f"{API_URL}/api/v1/sessions/{session_name}/state"
98
+ body = json.dumps(payload).encode("utf-8")
99
+ req = request.Request(
100
+ url,
101
+ data=body,
102
+ method="POST",
103
+ headers={
104
+ "Content-Type": "application/json",
105
+ "Authorization": f"Bearer {token}",
106
+ "X-Agent-Name": "marvisx",
107
+ },
108
+ )
109
+ try:
110
+ with request.urlopen(req, timeout=TIMEOUT_SECS) as resp: # noqa: S310
111
+ resp.read()
112
+ except (error.URLError, error.HTTPError, OSError, socket.timeout):
113
+ pass # silent — hook is fire-and-forget
114
+
115
+
116
+ def main() -> int:
117
+ try:
118
+ raw = sys.stdin.read()
119
+ except OSError:
120
+ return 0
121
+ if not raw:
122
+ return 0
123
+ try:
124
+ data = json.loads(raw)
125
+ except json.JSONDecodeError:
126
+ return 0
127
+
128
+ event = data.get("hook_event_name")
129
+ if not event:
130
+ return 0
131
+
132
+ session_name = _resolve_session_name()
133
+ if not session_name:
134
+ return 0
135
+
136
+ token = _read_token()
137
+ if not token:
138
+ return 0
139
+
140
+ payload = {
141
+ "provider": "claude",
142
+ "event": event,
143
+ "conv_id": data.get("session_id"),
144
+ "ts": datetime.now(timezone.utc).isoformat(),
145
+ }
146
+
147
+ # Fork-and-detach so the parent (hook) returns to Claude immediately and
148
+ # the child completes the POST even if Claude exits (Stop/SessionEnd
149
+ # races, julik R3). Posix-only — Marvis is Linux.
150
+ try:
151
+ pid = os.fork()
152
+ except OSError:
153
+ # Couldn't fork — last-resort inline POST.
154
+ _post(payload, token, session_name)
155
+ return 0
156
+
157
+ if pid > 0:
158
+ # Parent: hook returns success immediately.
159
+ return 0
160
+
161
+ # Child: detach from parent's session/process group + close inherited
162
+ # std streams to avoid keeping pipes alive after the parent exits.
163
+ try:
164
+ os.setsid()
165
+ except OSError:
166
+ pass
167
+ try:
168
+ for fd in (0, 1, 2):
169
+ os.close(fd)
170
+ devnull = os.open(os.devnull, os.O_RDWR)
171
+ os.dup2(devnull, 0)
172
+ os.dup2(devnull, 1)
173
+ os.dup2(devnull, 2)
174
+ except OSError:
175
+ pass
176
+
177
+ _post(payload, token, session_name)
178
+ os._exit(0)
179
+
180
+
181
+ if __name__ == "__main__":
182
+ sys.exit(main())