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