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.
- core/api/__init__.py +0 -0
- core/api/agents/__init__.py +0 -0
- core/api/agents/session_health.py +59 -0
- core/api/agents/session_manager.py +206 -0
- core/api/bin/marvisx-state-hook.py +182 -0
- core/api/config.py +533 -0
- core/api/db.py +1516 -0
- core/api/dependencies/__init__.py +0 -0
- core/api/dependencies/tenant.py +34 -0
- core/api/main.py +1641 -0
- core/api/mcp/__init__.py +8 -0
- core/api/mcp/_adapter.py +184 -0
- core/api/mcp/server.py +58 -0
- core/api/mcp/tools/__init__.py +59 -0
- core/api/mcp/tools/brain.py +599 -0
- core/api/mcp/tools/graph.py +380 -0
- core/api/mcp/tools/handoffs.py +112 -0
- core/api/mcp/tools/ingest.py +326 -0
- core/api/mcp/tools/learnings.py +144 -0
- core/api/mcp/tools/projects.py +99 -0
- core/api/mcp/tools/pull_requests.py +173 -0
- core/api/mcp/tools/safety.py +111 -0
- core/api/mcp/tools/search.py +79 -0
- core/api/mcp/tools/tasks.py +258 -0
- core/api/middleware/__init__.py +0 -0
- core/api/middleware/tool_call_audit.py +111 -0
- core/api/models/__init__.py +346 -0
- core/api/models/auth.py +48 -0
- core/api/models/brain.py +1006 -0
- core/api/models/common.py +76 -0
- core/api/models/costs.py +91 -0
- core/api/models/graph.py +66 -0
- core/api/models/graph_cosmo.py +125 -0
- core/api/models/graph_pr_impact.py +257 -0
- core/api/models/graph_ux.py +141 -0
- core/api/models/inbox.py +230 -0
- core/api/models/ingest_keys.py +108 -0
- core/api/models/kg.py +41 -0
- core/api/models/llm_config.py +56 -0
- core/api/models/monitoring.py +234 -0
- core/api/models/projects.py +161 -0
- core/api/models/search.py +42 -0
- core/api/models/sessions.py +322 -0
- core/api/models/tasks.py +184 -0
- core/api/models/teams.py +63 -0
- core/api/models/users.py +184 -0
- core/api/observability/__init__.py +0 -0
- core/api/observability/tracing.py +92 -0
- core/api/paths.py +26 -0
- core/api/rate_limit.py +24 -0
- core/api/rbac.py +112 -0
- core/api/routers/__init__.py +0 -0
- core/api/routers/_adapter.py +24 -0
- core/api/routers/admin_pr_impact.py +230 -0
- core/api/routers/admin_settings.py +147 -0
- core/api/routers/agent.py +1079 -0
- core/api/routers/agent_tokens.py +276 -0
- core/api/routers/app_settings.py +112 -0
- core/api/routers/audit.py +89 -0
- core/api/routers/auth.py +586 -0
- core/api/routers/bench.py +161 -0
- core/api/routers/brain.py +881 -0
- core/api/routers/brain_directions.py +527 -0
- core/api/routers/ci_checks.py +140 -0
- core/api/routers/comments.py +273 -0
- core/api/routers/costs.py +148 -0
- core/api/routers/docs_coverage.py +217 -0
- core/api/routers/docs_governance.py +63 -0
- core/api/routers/documents.py +318 -0
- core/api/routers/files.py +163 -0
- core/api/routers/finder.py +987 -0
- core/api/routers/graph.py +836 -0
- core/api/routers/handoffs.py +156 -0
- core/api/routers/inbox.py +496 -0
- core/api/routers/ingest_api_keys.py +205 -0
- core/api/routers/ingest_triage.py +1227 -0
- core/api/routers/judge.py +306 -0
- core/api/routers/kg.py +336 -0
- core/api/routers/learnings.py +253 -0
- core/api/routers/llm_config.py +130 -0
- core/api/routers/monitoring.py +347 -0
- core/api/routers/notifications.py +125 -0
- core/api/routers/pr_impact.py +315 -0
- core/api/routers/projects.py +1061 -0
- core/api/routers/pull_requests.py +312 -0
- core/api/routers/push.py +67 -0
- core/api/routers/raci.py +228 -0
- core/api/routers/search.py +125 -0
- core/api/routers/sessions.py +3100 -0
- core/api/routers/settings.py +90 -0
- core/api/routers/share_repo.py +68 -0
- core/api/routers/status_updates.py +96 -0
- core/api/routers/tags.py +45 -0
- core/api/routers/tasks.py +526 -0
- core/api/routers/teams.py +425 -0
- core/api/routers/terminal.py +105 -0
- core/api/routers/users.py +331 -0
- core/api/routers/webhooks.py +330 -0
- core/api/runtime_settings.py +84 -0
- core/api/security.py +652 -0
- core/api/services/__init__.py +0 -0
- core/api/services/audit.py +58 -0
- core/api/services/auto_approval.py +82 -0
- core/api/services/brain/__init__.py +52 -0
- core/api/services/brain/baseline.py +230 -0
- core/api/services/brain/capabilities.py +75 -0
- core/api/services/brain/cascade_rollup.py +388 -0
- core/api/services/brain/compound_bridge.py +215 -0
- core/api/services/brain/cycle.py +1242 -0
- core/api/services/brain/cycle_snapshot.py +371 -0
- core/api/services/brain/digest_collector.py +147 -0
- core/api/services/brain/direction.py +421 -0
- core/api/services/brain/drift.py +356 -0
- core/api/services/brain/drift_router.py +409 -0
- core/api/services/brain/edge_metrics.py +79 -0
- core/api/services/brain/events_reader.py +222 -0
- core/api/services/brain/findings.py +1379 -0
- core/api/services/brain/findings_reader.py +1006 -0
- core/api/services/brain/jobs.py +733 -0
- core/api/services/brain/journal.py +206 -0
- core/api/services/brain/knowledge_forms.py +92 -0
- core/api/services/brain/llm/__init__.py +37 -0
- core/api/services/brain/llm/_runner.py +62 -0
- core/api/services/brain/llm/base.py +70 -0
- core/api/services/brain/llm/cache.py +99 -0
- core/api/services/brain/llm/constants.py +46 -0
- core/api/services/brain/llm/direction_alignment.py +289 -0
- core/api/services/brain/llm/factory.py +132 -0
- core/api/services/brain/llm/finding_reasoning.py +98 -0
- core/api/services/brain/llm/finding_summary.py +92 -0
- core/api/services/brain/llm/grounding.py +46 -0
- core/api/services/brain/llm/journal_polish.py +96 -0
- core/api/services/brain/llm/local_gateway.py +426 -0
- core/api/services/brain/llm/parsers.py +71 -0
- core/api/services/brain/llm/router_glue.py +422 -0
- core/api/services/brain/memory_ops.py +1677 -0
- core/api/services/brain/models.py +140 -0
- core/api/services/brain/owner_hint.py +211 -0
- core/api/services/brain/recap.py +307 -0
- core/api/services/brain/rules/__init__.py +65 -0
- core/api/services/brain/rules/_signals.py +205 -0
- core/api/services/brain/rules/dr1_activity_without_status.py +108 -0
- core/api/services/brain/rules/dr2_decision_without_adr.py +110 -0
- core/api/services/brain/rules/dr3_stale_open_loop.py +127 -0
- core/api/services/brain/rules/dr4_docs_governance_drift.py +79 -0
- core/api/services/brain/rules/dr5_playbook_changed.py +99 -0
- core/api/services/brain/rules/dr6_external_update_unpropagated.py +100 -0
- core/api/services/brain/rules/dr7_claimed_decision_gap.py +123 -0
- core/api/services/brain/rules/dr8_direction_misalignment.py +230 -0
- core/api/services/brain/runs_reader.py +485 -0
- core/api/services/brain/scope.py +79 -0
- core/api/services/brain/sources/__init__.py +46 -0
- core/api/services/brain/sources/base.py +86 -0
- core/api/services/brain/sources/git_kg.py +393 -0
- core/api/services/brain/sources/handoffs.py +157 -0
- core/api/services/brain/sources/ingestor.py +130 -0
- core/api/services/brain/sources/learnings.py +121 -0
- core/api/services/brain/sources/pir_tasks.py +245 -0
- core/api/services/brain/watermarks.py +147 -0
- core/api/services/brain/ws_emitter.py +170 -0
- core/api/services/cc_tasks_reader.py +76 -0
- core/api/services/ci_service.py +263 -0
- core/api/services/claude_metrics.py +796 -0
- core/api/services/codex_metrics.py +364 -0
- core/api/services/conversation_reader.py +102 -0
- core/api/services/cost_service.py +243 -0
- core/api/services/crypto.py +147 -0
- core/api/services/docs_governance/__init__.py +1 -0
- core/api/services/docs_governance/confidence.py +230 -0
- core/api/services/docs_governance/config.py +87 -0
- core/api/services/docs_governance/enrichment.py +65 -0
- core/api/services/docs_governance/frontmatter_validator.py +83 -0
- core/api/services/docs_governance/hard_gates.py +221 -0
- core/api/services/docs_governance/triage_orchestrator.py +98 -0
- core/api/services/embedding_internal.py +395 -0
- core/api/services/embedding_service.py +832 -0
- core/api/services/event_dispatcher.py +167 -0
- core/api/services/events.py +70 -0
- core/api/services/git_ops.py +621 -0
- core/api/services/graph_cosmo_service.py +440 -0
- core/api/services/graph_ranker.py +306 -0
- core/api/services/graph_service.py +1589 -0
- core/api/services/inbox.py +800 -0
- core/api/services/inbox_digest.py +221 -0
- core/api/services/inbox_digest_deep_research.py +80 -0
- core/api/services/inbox_digest_jobs.py +595 -0
- core/api/services/inbox_gmail_sync.py +167 -0
- core/api/services/inbox_llm_classifier.py +906 -0
- core/api/services/inbox_source_identity.py +116 -0
- core/api/services/inbox_sources.py +456 -0
- core/api/services/inbox_taxonomy.py +195 -0
- core/api/services/inbox_tldr.py +1079 -0
- core/api/services/inbox_triage.py +899 -0
- core/api/services/ingest/__init__.py +13 -0
- core/api/services/ingest/api_key_auth.py +136 -0
- core/api/services/ingest/auto_approve.py +120 -0
- core/api/services/ingest/classifier.py +138 -0
- core/api/services/ingest/confidence.py +173 -0
- core/api/services/ingest/dispatch.py +88 -0
- core/api/services/ingest/embedding_router.py +272 -0
- core/api/services/ingest/events.py +33 -0
- core/api/services/ingest/ignore_patterns.py +79 -0
- core/api/services/ingest/image_probe.py +218 -0
- core/api/services/ingest/ingress.py +263 -0
- core/api/services/ingest/insert_saga.py +793 -0
- core/api/services/ingest/llm/__init__.py +13 -0
- core/api/services/ingest/llm/anthropic_haiku.py +23 -0
- core/api/services/ingest/llm/base.py +59 -0
- core/api/services/ingest/llm/byok_provider.py +130 -0
- core/api/services/ingest/llm/classification_context.py +301 -0
- core/api/services/ingest/llm/config_store.py +246 -0
- core/api/services/ingest/llm/factory.py +24 -0
- core/api/services/ingest/llm/kg_enricher.py +306 -0
- core/api/services/ingest/llm/local_gateway.py +821 -0
- core/api/services/ingest/llm/local_vllm.py +23 -0
- core/api/services/ingest/llm/openai_nano.py +349 -0
- core/api/services/ingest/lock_advisory.py +57 -0
- core/api/services/ingest/parser_router.py +1756 -0
- core/api/services/ingest/parsers/__init__.py +1 -0
- core/api/services/ingest/parsers/docling_parser.py +142 -0
- core/api/services/ingest/parsers/docparse_gateway.py +178 -0
- core/api/services/ingest/parsers/docx_parser.py +127 -0
- core/api/services/ingest/parsers/folder_unpacker.py +85 -0
- core/api/services/ingest/parsers/gateway_aux.py +147 -0
- core/api/services/ingest/parsers/image_parser.py +251 -0
- core/api/services/ingest/parsers/internal_markdown.py +89 -0
- core/api/services/ingest/parsers/ocr_gateway.py +117 -0
- core/api/services/ingest/parsers/ocr_pdf_parser.py +112 -0
- core/api/services/ingest/parsers/pdf_types.py +13 -0
- core/api/services/ingest/parsers/transcript_parser.py +445 -0
- core/api/services/ingest/parsers/vision_gateway.py +186 -0
- core/api/services/ingest/parsers/xlsx_parser.py +91 -0
- core/api/services/ingest/parsers/zip_unpacker.py +126 -0
- core/api/services/ingest/preflight.py +393 -0
- core/api/services/ingest/retry_voyage.py +88 -0
- core/api/services/ingest/routing_policy.py +307 -0
- core/api/services/ingest/serializers/__init__.py +1 -0
- core/api/services/ingest/serializers/xlsx_to_markdown.py +80 -0
- core/api/services/ingest/skip_log.py +74 -0
- core/api/services/ingest/watcher.py +637 -0
- core/api/services/kg/__init__.py +0 -0
- core/api/services/kg/audit.py +49 -0
- core/api/services/kg/hybrid_search.py +691 -0
- core/api/services/kg/lens.py +339 -0
- core/api/services/kg/pr_impact.py +770 -0
- core/api/services/kg/queries.py +152 -0
- core/api/services/kg/ranking.py +89 -0
- core/api/services/kg/rrf.py +143 -0
- core/api/services/kg_watcher_control.py +161 -0
- core/api/services/local_llm/__init__.py +19 -0
- core/api/services/local_llm/async_client.py +385 -0
- core/api/services/local_llm/client.py +173 -0
- core/api/services/local_llm/url_validator.py +44 -0
- core/api/services/metrics_collector.py +646 -0
- core/api/services/metrics_providers.py +65 -0
- core/api/services/model_registry.py +266 -0
- core/api/services/model_router.py +137 -0
- core/api/services/n8n_client.py +77 -0
- core/api/services/newsletter_llm_gateway.py +66 -0
- core/api/services/notification_service.py +134 -0
- core/api/services/openai_responses.py +55 -0
- core/api/services/opencode_metrics.py +375 -0
- core/api/services/opencode_sessions.py +173 -0
- core/api/services/pii_redactor.py +138 -0
- core/api/services/pr_impact_pipeline/__init__.py +21 -0
- core/api/services/pr_impact_pipeline/differ.py +421 -0
- core/api/services/pr_impact_pipeline/dispatcher.py +415 -0
- core/api/services/pr_impact_pipeline/gc.py +93 -0
- core/api/services/pr_impact_pipeline/languages.py +192 -0
- core/api/services/pr_impact_pipeline/parser.py +178 -0
- core/api/services/pr_impact_pipeline/writer.py +394 -0
- core/api/services/pr_service.py +1393 -0
- core/api/services/project_paths.py +70 -0
- core/api/services/project_status_updates.py +265 -0
- core/api/services/providers.py +276 -0
- core/api/services/push_service.py +170 -0
- core/api/services/reminder_service.py +89 -0
- core/api/services/runas.py +41 -0
- core/api/services/salience_service.py +69 -0
- core/api/services/security_collector.py +281 -0
- core/api/services/session_catalog.py +385 -0
- core/api/services/session_metrics_service.py +301 -0
- core/api/services/session_ops.py +272 -0
- core/api/services/session_state.py +173 -0
- core/api/services/share_links.py +222 -0
- core/api/services/task_transitions.py +146 -0
- core/api/services/terminal_metrics.py +462 -0
- core/api/services/terminal_metrics_dump.py +203 -0
- core/api/services/tmux.py +1205 -0
- core/api/services/webhook_service.py +422 -0
- core/api/services/workspace_sync.py +164 -0
- core/api/templates/__init__.py +1 -0
- core/api/templates/markdown_share.py +164 -0
- core/api/terminal.py +1031 -0
- core/api/tests/__init__.py +0 -0
- core/api/tests/test_agent_facing_auth_dependencies.py +132 -0
- core/api/tests/test_audit_permissions.py +133 -0
- core/api/tests/test_backfill_session_conversations.py +90 -0
- core/api/tests/test_backfill_working_seconds_msg.py +129 -0
- core/api/tests/test_claude_metrics.py +326 -0
- core/api/tests/test_codex_metrics.py +189 -0
- core/api/tests/test_finder_paths.py +74 -0
- core/api/tests/test_git_ops_merge.py +155 -0
- core/api/tests/test_learnings_check_search.py +81 -0
- core/api/tests/test_metrics_providers.py +133 -0
- core/api/tests/test_migration_087.py +164 -0
- core/api/tests/test_migration_088.py +94 -0
- core/api/tests/test_migration_089.py +116 -0
- core/api/tests/test_openai_responses.py +24 -0
- core/api/tests/test_opencode_metrics.py +740 -0
- core/api/tests/test_opencode_sessions.py +321 -0
- core/api/tests/test_pr_workflow_e2e.py +457 -0
- core/api/tests/test_projects_handoffs.py +31 -0
- core/api/tests/test_providers.py +138 -0
- core/api/tests/test_safety_bridge.py +347 -0
- core/api/tests/test_session_catalog.py +142 -0
- core/api/tests/test_session_conversations.py +512 -0
- core/api/tests/test_session_metrics_service.py +270 -0
- core/api/tests/test_session_resume_paths.py +548 -0
- core/api/tests/test_session_theme_mode_migration.py +56 -0
- core/api/tests/test_sessions_rbac.py +131 -0
- core/api/tests/test_share_edit.py +398 -0
- core/api/tests/test_share_repo.py +200 -0
- core/api/tests/test_terminal_session_manager.py +98 -0
- core/api/tests/test_terminal_upload.py +34 -0
- core/api/tests/test_tmux.py +272 -0
- core/api/tests/test_workspace_sync.py +186 -0
- core/api/tests/test_ws_ticket_in_memory.py +73 -0
- core/api/use_cases/__init__.py +11 -0
- core/api/use_cases/_context.py +89 -0
- core/api/use_cases/_errors.py +62 -0
- core/api/use_cases/_roles.py +16 -0
- core/api/use_cases/audit.py +171 -0
- core/api/use_cases/brain.py +1232 -0
- core/api/use_cases/costs.py +249 -0
- core/api/use_cases/graph.py +1153 -0
- core/api/use_cases/handoffs.py +506 -0
- core/api/use_cases/ingest_triage.py +1229 -0
- core/api/use_cases/learnings.py +538 -0
- core/api/use_cases/projects.py +705 -0
- core/api/use_cases/pull_requests.py +415 -0
- core/api/use_cases/search.py +926 -0
- core/api/use_cases/tasks.py +1495 -0
- core/api/visibility.py +141 -0
- core/cli/__init__.py +5 -0
- core/cli/_index_source.py +632 -0
- core/cli/_runtime_ctx.py +160 -0
- core/cli/_transmute.py +241 -0
- core/cli/marvis_doctor.py +704 -0
- core/cli/marvis_feedback.py +396 -0
- core/cli/marvis_governance.py +315 -0
- core/cli/marvis_hooks.py +515 -0
- core/cli/marvis_init.py +757 -0
- core/cli/marvis_mcp.py +401 -0
- core/cli/marvis_runtime.py +855 -0
- core/cli/marvis_telemetry.py +228 -0
- core/scripts/_drift_check.py +716 -0
- core/scripts/_frontmatter.py +66 -0
- core/scripts/_graph_writer.py +189 -0
- core/scripts/ast_parser.py +1553 -0
- core/scripts/install_hooks/__init__.py +1 -0
- core/scripts/install_hooks/_config.sh +109 -0
- core/scripts/install_hooks/block-dangerous-bash.sh +23 -0
- core/scripts/install_hooks/block-db-direct-write.sh +23 -0
- core/scripts/install_hooks/block-push-no-task.sh +23 -0
- core/scripts/install_hooks/block-staging-to-prod.sh +23 -0
- core/scripts/install_hooks/block-subtree-push.sh +23 -0
- core/scripts/install_hooks/config.json +53 -0
- core/scripts/install_hooks/enforce-no-merge-main.sh +23 -0
- core/scripts/install_hooks/enforce-worktree.sh +23 -0
- core/scripts/install_hooks/quality-gate.sh +170 -0
- core/scripts/install_hooks/safety_bridge.py +968 -0
- core/scripts/install_hooks/secret-scan.sh +23 -0
- core/scripts/migrate_spike_node_ids.py +122 -0
- core/scripts/populate_artifacts.py +2198 -0
- core/scripts/populate_cross_project.py +2457 -0
- core/scripts/populate_inbox_nodes.py +357 -0
- core/scripts/populate_pr_impact.py +267 -0
- core/scripts/populate_project_nodes.py +603 -0
- core/scripts/populate_touch_counter.py +337 -0
- core/scripts/reparse_failed.py +57 -0
- core/scripts/safety_bridge.py +968 -0
- core/telemetry/__init__.py +9 -0
- core/telemetry/client.py +405 -0
- core/telemetry/schema.py +122 -0
- core/wizard/__init__.py +65 -0
- core/wizard/byok_vault.py +147 -0
- core/wizard/defaults.py +58 -0
- core/wizard/state.py +117 -0
- core/wizard/steps.py +70 -0
- core/wizard/validation.py +136 -0
- marvisx_cli-0.1.0.dist-info/METADATA +201 -0
- marvisx_cli-0.1.0.dist-info/RECORD +587 -0
- marvisx_cli-0.1.0.dist-info/WHEEL +5 -0
- marvisx_cli-0.1.0.dist-info/entry_points.txt +3 -0
- marvisx_cli-0.1.0.dist-info/licenses/LICENSE +98 -0
- marvisx_cli-0.1.0.dist-info/top_level.txt +3 -0
- migrations/001_initial.sql +33 -0
- migrations/002_tasks.sql +30 -0
- migrations/003_session_management.sql +7 -0
- migrations/004_projects_comments.sql +65 -0
- migrations/005_session_intelligence.sql +15 -0
- migrations/006_settings.sql +12 -0
- migrations/007_task_scoring.sql +12 -0
- migrations/008_cost_tracking.sql +31 -0
- migrations/009_session_card_metrics.sql +3 -0
- migrations/010_monitoring.sql +55 -0
- migrations/012_agent_api.sql +21 -0
- migrations/013_session_complete.sql +8 -0
- migrations/015_pull_requests.sql +43 -0
- migrations/015_pull_requests_down.sql +5 -0
- migrations/016_users_raci.sql +116 -0
- migrations/017_task_cost_entries.sql +87 -0
- migrations/018_agents.sql +73 -0
- migrations/018_agents_down.sql +13 -0
- migrations/019_review_feedback.sql +18 -0
- migrations/020_pr_commit_sha.sql +4 -0
- migrations/021_webhook_events.sql +18 -0
- migrations/022_devx_agent_managed.sql +11 -0
- migrations/022_devx_agent_managed_down.sql +6 -0
- migrations/023_devx_p1_gate.sql +7 -0
- migrations/023_devx_p1_gate_down.sql +3 -0
- migrations/024_chat_messages.sql +16 -0
- migrations/024_pr_conversation_id.sql +8 -0
- migrations/024_task_indexes.sql +21 -0
- migrations/024_task_indexes_down.sql +7 -0
- migrations/025_audit_log.sql +17 -0
- migrations/026_agent_tokens.sql +20 -0
- migrations/027_teams_auth_phase_b.sql +35 -0
- migrations/028_learnings.sql +23 -0
- migrations/029_team_roles.sql +14 -0
- migrations/030_finder_pins.sql +10 -0
- migrations/031_pr_deploy_status.sql +9 -0
- migrations/032_task_reminders.sql +7 -0
- migrations/033_events_retry_count.sql +6 -0
- migrations/033_session_owner.sql +9 -0
- migrations/034_notifications.sql +38 -0
- migrations/035_shared_links.sql +15 -0
- migrations/036_session_index_upgrade.sql +29 -0
- migrations/037_pr_approval.sql +15 -0
- migrations/038_pr_submitted_by.sql +6 -0
- migrations/039_push_subscriptions.sql +17 -0
- migrations/040_semantic_search.sql +16 -0
- migrations/041_workspaces.sql +63 -0
- migrations/042_oidc_providers.sql +24 -0
- migrations/043_ci_checks.sql +31 -0
- migrations/044_agent_metrics.sql +30 -0
- migrations/045_documents_doc_type.sql +5 -0
- migrations/046_salience.sql +13 -0
- migrations/047_seed_missing_agents.sql +6 -0
- migrations/048_fix_agent_paths_roles.sql +5 -0
- migrations/049_agent_role_and_learnings_schema.sql +3 -0
- migrations/050_session_provider.sql +2 -0
- migrations/051_session_launch_profile.sql +4 -0
- migrations/052_session_theme_mode.sql +2 -0
- migrations/052_task_kind.sql +4 -0
- migrations/053_inbox_items.sql +31 -0
- migrations/054_inbox_triage_contract.sql +30 -0
- migrations/055_inbox_topic_treatment.sql +12 -0
- migrations/056_inbox_treatment_read_save.sql +57 -0
- migrations/057_session_theme_mode_backfill.sql +4 -0
- migrations/058_inbox_item_status_lifecycle.sql +13 -0
- migrations/059_inbox_tldr_and_source_scores.sql +18 -0
- migrations/060_newsletter.sql +16 -0
- migrations/061_inbox_redesign.sql +69 -0
- migrations/062_fix_inbox_sources_backfill.sql +37 -0
- migrations/063_task_completion_mode.sql +23 -0
- migrations/064_judge_mode_setting.sql +4 -0
- migrations/065_knowledge_graph_spike.sql +40 -0
- migrations/066_digest_ranking_inputs.sql +10 -0
- migrations/066_kg_artifact_nodes.sql +129 -0
- migrations/067_inbox_digest_selections.sql +28 -0
- migrations/067_kg_temporal.sql +53 -0
- migrations/068_inbox_digest_app_settings.sql +9 -0
- migrations/068_kg_touch_counter.sql +52 -0
- migrations/069_kg_doc_types.sql +117 -0
- migrations/070_digest_ranking_inputs_recovery.sql +3 -0
- migrations/071_inbox_digest_selections_recovery.sql +3 -0
- migrations/072_inbox_digest_app_settings_recovery.sql +3 -0
- migrations/073_kg_cross_project.sql +216 -0
- migrations/073_kg_cross_project_down.sql +77 -0
- migrations/074_kg_infra_types.sql +208 -0
- migrations/074_kg_infra_types_down.sql +80 -0
- migrations/075_kg_file_state_recovery.sql +35 -0
- migrations/075_kg_file_state_recovery_down.sql +5 -0
- migrations/076_kg_watcher_state.sql +33 -0
- migrations/076_kg_watcher_state_down.sql +3 -0
- migrations/077_kg_doc_types_extend.sql +226 -0
- migrations/077_kg_doc_types_extend_down.sql +80 -0
- migrations/078_kg_fts5.sql +102 -0
- migrations/078_kg_fts5_down.sql +14 -0
- migrations/079_kg_missing_indexes.sql +31 -0
- migrations/079_kg_missing_indexes_down.sql +10 -0
- migrations/080_kg_fts5_extended.sql +232 -0
- migrations/080_kg_fts5_extended_down.sql +25 -0
- migrations/081_kg_lens_indexes.sql +9 -0
- migrations/081_kg_lens_indexes_down.sql +3 -0
- migrations/082_kg_pins.sql +26 -0
- migrations/082_kg_pins_down.sql +14 -0
- migrations/083_kg_graph_nodes_degree.sql +20 -0
- migrations/083_kg_graph_nodes_degree_down.sql +15 -0
- migrations/084_drop_legacy_scheduler_tables.sql +58 -0
- migrations/084_drop_legacy_scheduler_tables_down.sql +112 -0
- migrations/085_kg_edge_resolves_to.sql +142 -0
- migrations/085_kg_edge_resolves_to_down.sql +66 -0
- migrations/086_project_status_updates_feed.sql +20 -0
- migrations/086_project_status_updates_feed_down.sql +36 -0
- migrations/087_session_metrics_dual.sql +50 -0
- migrations/087_session_metrics_dual_down.sql +21 -0
- migrations/088_rename_context_pct_legacy.sql +23 -0
- migrations/088_rename_context_pct_legacy_down.sql +8 -0
- migrations/089_session_metrics_equivalent_cost.sql +26 -0
- migrations/089_session_metrics_equivalent_cost_down.sql +11 -0
- migrations/090_kg_inbox_node_type.sql +26 -0
- migrations/090_kg_inbox_node_type_down.sql +20 -0
- migrations/091_kg_inbox_node_type_check.sql +265 -0
- migrations/091_kg_inbox_node_type_check_down.sql +129 -0
- migrations/092_sessions_activity_state_ts.sql +29 -0
- migrations/092_sessions_activity_state_ts_down.sql +14 -0
- migrations/093_sessions_activity_state_column.sql +29 -0
- migrations/093_sessions_activity_state_column_down.sql +10 -0
- migrations/094_ingest_pending.sql +55 -0
- migrations/094_ingest_pending_down.sql +15 -0
- migrations/095_kg_intent_first.sql +77 -0
- migrations/095_kg_intent_first_down.sql +25 -0
- migrations/096_kg_xlsx_artifact_prefix.sql +17 -0
- migrations/096_kg_xlsx_artifact_prefix_down.sql +11 -0
- migrations/097_ingest_change_history.sql +37 -0
- migrations/097_ingest_change_history_down.sql +13 -0
- migrations/098_kg_node_type_business.sql +254 -0
- migrations/098_kg_node_type_business_down.sql +195 -0
- migrations/099_kg_edges_restore_weight.sql +58 -0
- migrations/099_kg_edges_restore_weight_down.sql +12 -0
- migrations/100_kg_enriched_at.sql +25 -0
- migrations/100_kg_enriched_at_down.sql +12 -0
- migrations/101_local_llm_shadow_comparisons.sql +66 -0
- migrations/101_local_llm_shadow_comparisons_down.sql +15 -0
- migrations/102_promote_llm_costs.sql +69 -0
- migrations/102_promote_llm_costs_down.sql +19 -0
- migrations/103_ingest_skipped_log.sql +46 -0
- migrations/103_ingest_skipped_log_down.sql +15 -0
- migrations/120_docs_governance.sql +50 -0
- migrations/120_docs_governance_down.sql +11 -0
- migrations/121_notification_event_fk_cleanup.sql +21 -0
- migrations/121_notification_event_fk_cleanup_down.sql +10 -0
- migrations/122_docs_drift_history.sql +34 -0
- migrations/122_docs_drift_history_down.sql +15 -0
- migrations/123_ingest_parser_waiting_status.sql +69 -0
- migrations/123_ingest_parser_waiting_status_down.sql +69 -0
- migrations/124_heypocket_recordings.sql +63 -0
- migrations/124_heypocket_recordings_down.sql +13 -0
- migrations/125_kg_node_type_record.sql +219 -0
- migrations/125_kg_node_type_record_down.sql +205 -0
- migrations/126_ingest_terminal_upload_source_kind.sql +69 -0
- migrations/126_ingest_terminal_upload_source_kind_down.sql +69 -0
- migrations/127_brain_v1_substrate.sql +200 -0
- migrations/127_brain_v1_substrate_down.sql +32 -0
- migrations/128_brain_drift_signals.sql +157 -0
- migrations/128_brain_drift_signals_down.sql +23 -0
- migrations/129_brain_memory_operations.sql +232 -0
- migrations/129_brain_memory_operations_down.sql +27 -0
- migrations/130_brain_findings.sql +258 -0
- migrations/130_brain_findings_down.sql +29 -0
- migrations/132_kg_pr_modifies.sql +242 -0
- migrations/132_kg_pr_modifies_down.sql +99 -0
- migrations/133_brain_v1_2_direction_schema.sql +476 -0
- migrations/133_brain_v1_2_direction_schema_down.sql +273 -0
- migrations/134_brain_journal_narrative_polished.sql +8 -0
- migrations/134_brain_journal_narrative_polished_down.sql +6 -0
- migrations/135_kg_edges_provider.sql +21 -0
- migrations/136_documents_fts.sql +56 -0
- migrations/137_promote_llm_costs.sql +59 -0
- migrations/137_promote_llm_costs_down.sql +19 -0
- migrations/138_ingest_api_keys.sql +39 -0
- migrations/138_ingest_api_keys_down.sql +8 -0
- migrations/139_ingest_pending_ingress.sql +91 -0
- migrations/139_ingest_pending_ingress_down.sql +73 -0
- migrations/140_ingest_idempotency_quota.sql +45 -0
- migrations/140_ingest_idempotency_quota_down.sql +9 -0
- migrations/141_ingest_pending_metadata.sql +16 -0
- migrations/141_ingest_pending_metadata_down.sql +7 -0
- migrations/142_llm_function_config.sql +36 -0
- migrations/142_llm_function_config_down.sql +8 -0
- migrations/143_kg_code_embeddings.sql +25 -0
- migrations/143_kg_code_embeddings_down.sql +5 -0
- migrations/__init__.py +4 -0
- 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())
|