devcouncil 0.3.1 → 0.4.1
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.
- package/README.md +92 -33
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +56 -40
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +964 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +325 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/summary.py +21 -3
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Registry-backed MCP debugger and runtime-trace tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Awaitable, Callable
|
|
7
|
+
|
|
8
|
+
from mcp.types import TextContent, Tool
|
|
9
|
+
|
|
10
|
+
from devcouncil.codeintel.debug.consent import require_debug_consent, set_debug_consent
|
|
11
|
+
from devcouncil.codeintel.debug.discovery import adapter_by_id, discover_adapters
|
|
12
|
+
from devcouncil.codeintel.debug.session import get_debug_manager
|
|
13
|
+
from devcouncil.codeintel.debug.tracing import NodeCpuProfileProvider, PythonTraceProvider, import_runtime_trace
|
|
14
|
+
from devcouncil.codeintel.service import canonical_project_root
|
|
15
|
+
from devcouncil.integrations.mcp.util import error_text, json_text
|
|
16
|
+
|
|
17
|
+
Handler = Callable[[Path, dict], Awaitable[list[TextContent]]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _schema(properties: dict, required: list[str] | None = None) -> dict:
|
|
21
|
+
schema: dict = {"type": "object", "properties": {
|
|
22
|
+
"projectPath": {"type": "string", "description": "Explicit repository path."},
|
|
23
|
+
**properties,
|
|
24
|
+
}}
|
|
25
|
+
if required:
|
|
26
|
+
schema["required"] = required
|
|
27
|
+
return schema
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def tools() -> list[Tool]:
|
|
31
|
+
return [
|
|
32
|
+
Tool(
|
|
33
|
+
name="devcouncil_debug_discover",
|
|
34
|
+
description=(
|
|
35
|
+
"Discover installed DAP adapters after explicit one-time consent; "
|
|
36
|
+
"returns paths, versions, hashes, and launch/attach support."
|
|
37
|
+
),
|
|
38
|
+
inputSchema=_schema({"consent": {"type": "boolean", "default": False}}),
|
|
39
|
+
),
|
|
40
|
+
Tool(
|
|
41
|
+
name="devcouncil_debug_start",
|
|
42
|
+
description="Launch or attach a capability-negotiated DAP session.",
|
|
43
|
+
inputSchema=_schema({
|
|
44
|
+
"adapterId": {"type": "string"},
|
|
45
|
+
"adapterCommand": {"type": "array", "items": {"type": "string"}},
|
|
46
|
+
"request": {"type": "string", "enum": ["launch", "attach"], "default": "launch"},
|
|
47
|
+
"configuration": {"type": "object"},
|
|
48
|
+
"initialBreakpoints": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"additionalProperties": {"type": "array", "items": {"type": "integer"}},
|
|
51
|
+
},
|
|
52
|
+
"timeout": {"type": "number", "minimum": 1, "maximum": 120, "default": 30},
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
Tool(
|
|
56
|
+
name="devcouncil_debug_breakpoints",
|
|
57
|
+
description="Replace all breakpoints for one source in a DAP session.",
|
|
58
|
+
inputSchema=_schema({
|
|
59
|
+
"sessionId": {"type": "string"},
|
|
60
|
+
"source": {"type": "string"},
|
|
61
|
+
"lines": {"type": "array", "items": {"type": "integer"}},
|
|
62
|
+
}, ["sessionId", "source", "lines"]),
|
|
63
|
+
),
|
|
64
|
+
Tool(
|
|
65
|
+
name="devcouncil_debug_control",
|
|
66
|
+
description="Continue, pause, or step a DAP session.",
|
|
67
|
+
inputSchema=_schema({
|
|
68
|
+
"sessionId": {"type": "string"},
|
|
69
|
+
"action": {"type": "string", "enum": ["continue", "pause", "next", "stepIn", "stepOut"]},
|
|
70
|
+
"threadId": {"type": "integer"},
|
|
71
|
+
}, ["sessionId", "action"]),
|
|
72
|
+
),
|
|
73
|
+
Tool(
|
|
74
|
+
name="devcouncil_debug_inspect",
|
|
75
|
+
description="Inspect threads, stack frames, scopes, variables, source, or disassembly.",
|
|
76
|
+
inputSchema=_schema({
|
|
77
|
+
"sessionId": {"type": "string"},
|
|
78
|
+
"operation": {"type": "string", "enum": ["threads", "stackTrace", "scopes", "variables", "source", "disassemble"]},
|
|
79
|
+
"arguments": {"type": "object"},
|
|
80
|
+
}, ["sessionId", "operation"]),
|
|
81
|
+
),
|
|
82
|
+
Tool(
|
|
83
|
+
name="devcouncil_debug_evaluate",
|
|
84
|
+
description="Side-effectful DAP evaluate; allowSideEffects must be explicitly true.",
|
|
85
|
+
inputSchema=_schema({
|
|
86
|
+
"sessionId": {"type": "string"},
|
|
87
|
+
"expression": {"type": "string"},
|
|
88
|
+
"frameId": {"type": "integer"},
|
|
89
|
+
"allowSideEffects": {"type": "boolean", "const": True},
|
|
90
|
+
}, ["sessionId", "expression", "allowSideEffects"]),
|
|
91
|
+
),
|
|
92
|
+
Tool(
|
|
93
|
+
name="devcouncil_debug_trace",
|
|
94
|
+
description="Capture a DAP stack, run exact Python tracing, or import JSONL/Node CPU profile evidence.",
|
|
95
|
+
inputSchema=_schema({
|
|
96
|
+
"provider": {"type": "string", "enum": ["dap-stack", "python", "node", "import"]},
|
|
97
|
+
"sessionId": {"type": "string"},
|
|
98
|
+
"threadId": {"type": "integer"},
|
|
99
|
+
"script": {"type": "string"},
|
|
100
|
+
"args": {"type": "array", "items": {"type": "string"}},
|
|
101
|
+
"path": {"type": "string"},
|
|
102
|
+
}, ["provider"]),
|
|
103
|
+
),
|
|
104
|
+
Tool(
|
|
105
|
+
name="devcouncil_debug_stop",
|
|
106
|
+
description="Disconnect a DAP session and optionally leave the debuggee running.",
|
|
107
|
+
inputSchema=_schema({
|
|
108
|
+
"sessionId": {"type": "string"},
|
|
109
|
+
"terminateDebuggee": {"type": "boolean", "default": True},
|
|
110
|
+
}, ["sessionId"]),
|
|
111
|
+
),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
async def _discover(root: Path, arguments: dict) -> list[TextContent]:
|
|
116
|
+
if arguments.get("consent") is True:
|
|
117
|
+
set_debug_consent(root, True)
|
|
118
|
+
require_debug_consent(root)
|
|
119
|
+
return json_text({"consent": True, "adapters": [adapter.as_dict() for adapter in discover_adapters()]})
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _command(arguments: dict) -> list[str]:
|
|
123
|
+
supplied = arguments.get("adapterCommand")
|
|
124
|
+
if isinstance(supplied, list) and supplied:
|
|
125
|
+
return [str(value) for value in supplied]
|
|
126
|
+
adapter_id = str(arguments.get("adapterId") or "debugpy")
|
|
127
|
+
adapter = adapter_by_id(adapter_id)
|
|
128
|
+
if adapter is None:
|
|
129
|
+
raise ValueError(f"adapter {adapter_id!r} was not discovered")
|
|
130
|
+
return list(adapter.command)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def _start(root: Path, arguments: dict) -> list[TextContent]:
|
|
134
|
+
require_debug_consent(root)
|
|
135
|
+
session = get_debug_manager().start(
|
|
136
|
+
root,
|
|
137
|
+
_command(arguments),
|
|
138
|
+
request=str(arguments.get("request", "launch")),
|
|
139
|
+
arguments=dict(arguments.get("configuration") or {}),
|
|
140
|
+
initial_breakpoints={
|
|
141
|
+
str(source): [int(line) for line in lines]
|
|
142
|
+
for source, lines in dict(arguments.get("initialBreakpoints") or {}).items()
|
|
143
|
+
},
|
|
144
|
+
timeout=float(arguments.get("timeout", 30.0)),
|
|
145
|
+
)
|
|
146
|
+
return json_text(session.as_dict())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def _breakpoints(root: Path, arguments: dict) -> list[TextContent]:
|
|
150
|
+
require_debug_consent(root)
|
|
151
|
+
return json_text(get_debug_manager().set_breakpoints(
|
|
152
|
+
str(arguments["sessionId"]), str(arguments["source"]), [int(value) for value in arguments["lines"]]
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def _control(root: Path, arguments: dict) -> list[TextContent]:
|
|
157
|
+
require_debug_consent(root)
|
|
158
|
+
return json_text(get_debug_manager().control(
|
|
159
|
+
str(arguments["sessionId"]),
|
|
160
|
+
str(arguments["action"]),
|
|
161
|
+
thread_id=int(arguments["threadId"]) if arguments.get("threadId") is not None else None,
|
|
162
|
+
))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def _inspect(root: Path, arguments: dict) -> list[TextContent]:
|
|
166
|
+
require_debug_consent(root)
|
|
167
|
+
return json_text(get_debug_manager().inspect(
|
|
168
|
+
str(arguments["sessionId"]), str(arguments["operation"]), dict(arguments.get("arguments") or {})
|
|
169
|
+
))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def _evaluate(root: Path, arguments: dict) -> list[TextContent]:
|
|
173
|
+
require_debug_consent(root)
|
|
174
|
+
if arguments.get("allowSideEffects") is not True:
|
|
175
|
+
raise PermissionError("debug evaluate requires allowSideEffects=true")
|
|
176
|
+
return json_text(get_debug_manager().evaluate(
|
|
177
|
+
str(arguments["sessionId"]),
|
|
178
|
+
str(arguments["expression"]),
|
|
179
|
+
frame_id=int(arguments["frameId"]) if arguments.get("frameId") is not None else None,
|
|
180
|
+
allow_side_effects=True,
|
|
181
|
+
))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def _trace(root: Path, arguments: dict) -> list[TextContent]:
|
|
185
|
+
require_debug_consent(root)
|
|
186
|
+
provider = str(arguments["provider"])
|
|
187
|
+
if provider == "dap-stack":
|
|
188
|
+
return json_text(get_debug_manager().capture_stack(
|
|
189
|
+
str(arguments["sessionId"]), thread_id=int(arguments["threadId"])
|
|
190
|
+
))
|
|
191
|
+
if provider == "python":
|
|
192
|
+
return json_text(PythonTraceProvider(root).run(
|
|
193
|
+
Path(str(arguments["script"])), [str(value) for value in arguments.get("args") or []]
|
|
194
|
+
))
|
|
195
|
+
if provider == "node":
|
|
196
|
+
return json_text(NodeCpuProfileProvider(root).run(
|
|
197
|
+
Path(str(arguments["script"])), [str(value) for value in arguments.get("args") or []]
|
|
198
|
+
))
|
|
199
|
+
if provider == "import":
|
|
200
|
+
return json_text(import_runtime_trace(root, Path(str(arguments["path"]))))
|
|
201
|
+
raise ValueError(f"unsupported trace provider: {provider}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def _stop(root: Path, arguments: dict) -> list[TextContent]:
|
|
205
|
+
require_debug_consent(root)
|
|
206
|
+
session_id = str(arguments["sessionId"])
|
|
207
|
+
get_debug_manager().stop(session_id, terminate_debuggee=bool(arguments.get("terminateDebuggee", True)))
|
|
208
|
+
return json_text({"stopped": session_id})
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
REGISTRY: dict[str, Handler] = {
|
|
212
|
+
"devcouncil_debug_discover": _discover,
|
|
213
|
+
"devcouncil_debug_start": _start,
|
|
214
|
+
"devcouncil_debug_breakpoints": _breakpoints,
|
|
215
|
+
"devcouncil_debug_control": _control,
|
|
216
|
+
"devcouncil_debug_inspect": _inspect,
|
|
217
|
+
"devcouncil_debug_evaluate": _evaluate,
|
|
218
|
+
"devcouncil_debug_trace": _trace,
|
|
219
|
+
"devcouncil_debug_stop": _stop,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def dispatch(name: str, default_root: Path, arguments: dict) -> list[TextContent] | None:
|
|
224
|
+
handler = REGISTRY.get(name)
|
|
225
|
+
if handler is None:
|
|
226
|
+
return None
|
|
227
|
+
explicit = arguments.get("projectPath")
|
|
228
|
+
root = canonical_project_root(Path(explicit) if isinstance(explicit, str) and explicit else default_root)
|
|
229
|
+
try:
|
|
230
|
+
return await handler(root, arguments)
|
|
231
|
+
except PermissionError as exc:
|
|
232
|
+
return error_text(str(exc), code="debug_consent_required", tool=name)
|
|
233
|
+
except FileNotFoundError as exc:
|
|
234
|
+
return error_text(str(exc), code="not_found", tool=name)
|
|
235
|
+
except (KeyError, TypeError, ValueError, RuntimeError, TimeoutError) as exc:
|
|
236
|
+
return error_text(str(exc), code="debug_error", tool=name)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Evidence read/write MCP tool handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mcp.types import TextContent
|
|
8
|
+
|
|
9
|
+
from devcouncil.integrations.mcp.util import (
|
|
10
|
+
error_text,
|
|
11
|
+
int_argument,
|
|
12
|
+
json_text,
|
|
13
|
+
optional_string_argument,
|
|
14
|
+
parse_cli_json,
|
|
15
|
+
required_string_argument,
|
|
16
|
+
run_cli_command,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def handle_append_evidence(root: Path, db: object, arguments: dict) -> list[TextContent]:
|
|
21
|
+
del db # routed through CLI service layer
|
|
22
|
+
task_id, arg_error = required_string_argument(arguments, "task_id")
|
|
23
|
+
if arg_error:
|
|
24
|
+
return arg_error
|
|
25
|
+
lease_token, arg_error = required_string_argument(arguments, "lease_token")
|
|
26
|
+
if arg_error:
|
|
27
|
+
return arg_error
|
|
28
|
+
command, arg_error = required_string_argument(arguments, "command")
|
|
29
|
+
if arg_error:
|
|
30
|
+
return arg_error
|
|
31
|
+
summary_text, arg_error = required_string_argument(arguments, "summary")
|
|
32
|
+
if arg_error:
|
|
33
|
+
return arg_error
|
|
34
|
+
assert task_id is not None and lease_token is not None
|
|
35
|
+
exit_code = arguments.get("exit_code", 0)
|
|
36
|
+
if not isinstance(exit_code, int) or isinstance(exit_code, bool):
|
|
37
|
+
return error_text("exit_code must be an integer", code="invalid_arguments")
|
|
38
|
+
cli_args = [
|
|
39
|
+
"evidence-append", task_id,
|
|
40
|
+
"--lease-token", lease_token,
|
|
41
|
+
"--command", command or "",
|
|
42
|
+
"--summary", summary_text or "",
|
|
43
|
+
"--exit-code", str(exit_code),
|
|
44
|
+
"--json",
|
|
45
|
+
]
|
|
46
|
+
payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
|
|
47
|
+
if cli_error:
|
|
48
|
+
return cli_error
|
|
49
|
+
assert payload is not None
|
|
50
|
+
return json_text(payload)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def handle_get_evidence(root: Path, db: object, arguments: dict) -> list[TextContent]:
|
|
54
|
+
del db # routed through CLI service layer
|
|
55
|
+
task_id, arg_error = required_string_argument(arguments, "task_id")
|
|
56
|
+
if arg_error:
|
|
57
|
+
return arg_error
|
|
58
|
+
assert task_id is not None
|
|
59
|
+
command_filter = optional_string_argument(arguments, "command")
|
|
60
|
+
if command_filter == "":
|
|
61
|
+
return error_text("command must be a string", code="invalid_arguments", argument="command")
|
|
62
|
+
limit = int_argument(arguments, "limit", 20, minimum=1, maximum=100)
|
|
63
|
+
cli_args = ["evidence-list", task_id, "--limit", str(limit), "--json"]
|
|
64
|
+
if command_filter:
|
|
65
|
+
cli_args.extend(["--command", command_filter])
|
|
66
|
+
payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
|
|
67
|
+
if cli_error:
|
|
68
|
+
return cli_error
|
|
69
|
+
assert payload is not None
|
|
70
|
+
return json_text(payload)
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Git diff MCP tool handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from mcp.types import TextContent
|
|
10
|
+
|
|
11
|
+
from devcouncil.integrations.mcp.util import (
|
|
12
|
+
CLI_TIMEOUT_SECONDS,
|
|
13
|
+
error_text,
|
|
14
|
+
is_git_repo,
|
|
15
|
+
json_text,
|
|
16
|
+
optional_string_argument,
|
|
17
|
+
optional_string_list_argument,
|
|
18
|
+
truncate_text,
|
|
19
|
+
)
|
|
20
|
+
from devcouncil.storage.db import Database
|
|
21
|
+
from devcouncil.storage.repositories import TaskRepository
|
|
22
|
+
|
|
23
|
+
_BINARY_PROBE_BYTES = 8192
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _run_git(root: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
27
|
+
return subprocess.run(
|
|
28
|
+
args,
|
|
29
|
+
cwd=root,
|
|
30
|
+
capture_output=True,
|
|
31
|
+
text=True,
|
|
32
|
+
encoding="utf-8",
|
|
33
|
+
errors="replace",
|
|
34
|
+
timeout=CLI_TIMEOUT_SECONDS,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_name_status_z(data: str) -> dict[str, str]:
|
|
39
|
+
"""Parse ``git diff --name-status -z`` into ``{path: status}``.
|
|
40
|
+
|
|
41
|
+
Rename/copy records are ``STATUS\\0OLD\\0NEW\\0``; other records are
|
|
42
|
+
``STATUS\\0PATH\\0``. The new path is the authoritative key for renames.
|
|
43
|
+
"""
|
|
44
|
+
status_by_path: dict[str, str] = {}
|
|
45
|
+
parts = data.split("\0")
|
|
46
|
+
i = 0
|
|
47
|
+
while i < len(parts):
|
|
48
|
+
status = parts[i]
|
|
49
|
+
if not status:
|
|
50
|
+
i += 1
|
|
51
|
+
continue
|
|
52
|
+
kind = status[0]
|
|
53
|
+
if kind in "RC" and i + 2 < len(parts):
|
|
54
|
+
new_path = parts[i + 2].replace("\\", "/")
|
|
55
|
+
if new_path:
|
|
56
|
+
status_by_path[new_path] = status
|
|
57
|
+
i += 3
|
|
58
|
+
continue
|
|
59
|
+
if i + 1 < len(parts):
|
|
60
|
+
path = parts[i + 1].replace("\\", "/")
|
|
61
|
+
if path:
|
|
62
|
+
status_by_path[path] = status
|
|
63
|
+
i += 2
|
|
64
|
+
continue
|
|
65
|
+
break
|
|
66
|
+
return status_by_path
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _format_untracked_file_diff(rel_path: str, full_path: Path) -> tuple[str, int]:
|
|
70
|
+
"""Return ``(unified_diff_fragment, addition_count)`` for a new untracked file."""
|
|
71
|
+
try:
|
|
72
|
+
raw = full_path.read_bytes()
|
|
73
|
+
except OSError:
|
|
74
|
+
return "", 0
|
|
75
|
+
|
|
76
|
+
header = [
|
|
77
|
+
f"diff --git a/{rel_path} b/{rel_path}",
|
|
78
|
+
"new file mode 100644",
|
|
79
|
+
"--- /dev/null",
|
|
80
|
+
f"+++ b/{rel_path}",
|
|
81
|
+
]
|
|
82
|
+
if b"\0" in raw[:_BINARY_PROBE_BYTES]:
|
|
83
|
+
return "\n".join([*header, f"Binary files /dev/null and b/{rel_path} differ"]), 0
|
|
84
|
+
|
|
85
|
+
text = raw.decode("utf-8", errors="replace")
|
|
86
|
+
if not text:
|
|
87
|
+
return "\n".join(header) + "\n", 0
|
|
88
|
+
|
|
89
|
+
lines = text.splitlines()
|
|
90
|
+
if text.endswith(("\n", "\r")):
|
|
91
|
+
line_count = len(lines)
|
|
92
|
+
else:
|
|
93
|
+
line_count = max(len(lines), 1)
|
|
94
|
+
|
|
95
|
+
diff_lines = [*header, f"@@ -0,0 +1,{line_count} @@"]
|
|
96
|
+
diff_lines.extend(f"+{line}" for line in lines)
|
|
97
|
+
return "\n".join(diff_lines), line_count
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _list_untracked(root: Path, paths: list[str]) -> tuple[list[str], str | None]:
|
|
101
|
+
"""List untracked paths; return ``(paths, error)`` when the Git call fails."""
|
|
102
|
+
args = ["git", "ls-files", "--others", "--exclude-standard", "-z"]
|
|
103
|
+
if paths:
|
|
104
|
+
args.append("--")
|
|
105
|
+
args.extend(paths)
|
|
106
|
+
try:
|
|
107
|
+
proc = _run_git(root, args)
|
|
108
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
109
|
+
return [], str(exc)
|
|
110
|
+
if proc.returncode != 0:
|
|
111
|
+
detail = (proc.stderr or proc.stdout or f"git ls-files exited {proc.returncode}").strip()
|
|
112
|
+
return [], detail or f"git ls-files exited {proc.returncode}"
|
|
113
|
+
return [p.replace("\\", "/") for p in proc.stdout.split("\0") if p.strip()], None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _collect_untracked(
|
|
117
|
+
root: Path,
|
|
118
|
+
paths: list[str],
|
|
119
|
+
*,
|
|
120
|
+
known_paths: set[str],
|
|
121
|
+
) -> tuple[list[dict[str, object]], str, str | None]:
|
|
122
|
+
"""Build file entries and unified diff for untracked files in scope."""
|
|
123
|
+
untracked, err = _list_untracked(root, paths)
|
|
124
|
+
if err is not None:
|
|
125
|
+
return [], "", err
|
|
126
|
+
|
|
127
|
+
files: list[dict[str, object]] = []
|
|
128
|
+
fragments: list[str] = []
|
|
129
|
+
for rel in untracked:
|
|
130
|
+
if rel in known_paths:
|
|
131
|
+
continue
|
|
132
|
+
full = root / rel
|
|
133
|
+
if not full.is_file():
|
|
134
|
+
continue
|
|
135
|
+
fragment, additions = _format_untracked_file_diff(rel, full)
|
|
136
|
+
if not fragment:
|
|
137
|
+
continue
|
|
138
|
+
files.append({
|
|
139
|
+
"path": rel,
|
|
140
|
+
"status": "A",
|
|
141
|
+
"additions": additions,
|
|
142
|
+
"deletions": 0,
|
|
143
|
+
})
|
|
144
|
+
fragments.append(fragment.rstrip("\n"))
|
|
145
|
+
unified = "\n".join(fragments)
|
|
146
|
+
if unified:
|
|
147
|
+
unified += "\n"
|
|
148
|
+
return files, unified, None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def git_diff(root: Path, paths: list[str], staged: bool) -> dict[str, object]:
|
|
152
|
+
"""Compute a (optionally path-scoped, optionally staged) git diff."""
|
|
153
|
+
diff_args = ["git", "diff"]
|
|
154
|
+
numstat_args = ["git", "diff", "--numstat"]
|
|
155
|
+
namestatus_args = ["git", "diff", "--name-status", "-z"]
|
|
156
|
+
if staged:
|
|
157
|
+
for args in (diff_args, numstat_args, namestatus_args):
|
|
158
|
+
args.append("--cached")
|
|
159
|
+
if paths:
|
|
160
|
+
for args in (diff_args, numstat_args, namestatus_args):
|
|
161
|
+
args.append("--")
|
|
162
|
+
args.extend(paths)
|
|
163
|
+
|
|
164
|
+
def _run(args: list[str]) -> subprocess.CompletedProcess[str]:
|
|
165
|
+
return _run_git(root, args)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
loop = asyncio.get_event_loop()
|
|
169
|
+
diff_proc, numstat_proc, namestatus_proc = await asyncio.gather(
|
|
170
|
+
loop.run_in_executor(None, _run, diff_args),
|
|
171
|
+
loop.run_in_executor(None, _run, numstat_args),
|
|
172
|
+
loop.run_in_executor(None, _run, namestatus_args),
|
|
173
|
+
)
|
|
174
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
175
|
+
return {"ok": False, "files": [], "unified_diff": "", "truncated": False, "error": str(exc)}
|
|
176
|
+
|
|
177
|
+
for proc, label in (
|
|
178
|
+
(diff_proc, "git diff"),
|
|
179
|
+
(numstat_proc, "git diff --numstat"),
|
|
180
|
+
(namestatus_proc, "git diff --name-status"),
|
|
181
|
+
):
|
|
182
|
+
if proc.returncode != 0:
|
|
183
|
+
detail = (proc.stderr or proc.stdout or f"{label} exited {proc.returncode}").strip()
|
|
184
|
+
return {
|
|
185
|
+
"ok": False,
|
|
186
|
+
"files": [],
|
|
187
|
+
"unified_diff": "",
|
|
188
|
+
"truncated": False,
|
|
189
|
+
"error": detail or f"{label} exited {proc.returncode}",
|
|
190
|
+
"staged": staged,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
status_by_path = _parse_name_status_z(namestatus_proc.stdout)
|
|
194
|
+
|
|
195
|
+
files: list[dict[str, object]] = []
|
|
196
|
+
for line in numstat_proc.stdout.splitlines():
|
|
197
|
+
parts = line.split("\t")
|
|
198
|
+
if len(parts) < 3:
|
|
199
|
+
continue
|
|
200
|
+
added_str, deleted_str, file_path = parts[0], parts[1], parts[-1]
|
|
201
|
+
# Rename numstat without -z may use "old => new"; prefer the new side.
|
|
202
|
+
if " => " in file_path:
|
|
203
|
+
file_path = file_path.split(" => ", 1)[-1]
|
|
204
|
+
file_path = file_path.replace("\\", "/")
|
|
205
|
+
files.append({
|
|
206
|
+
"path": file_path,
|
|
207
|
+
"status": status_by_path.get(file_path, "M"),
|
|
208
|
+
"additions": int(added_str) if added_str.isdigit() else 0,
|
|
209
|
+
"deletions": int(deleted_str) if deleted_str.isdigit() else 0,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
unified_parts = [diff_proc.stdout.rstrip("\n")] if diff_proc.stdout else []
|
|
213
|
+
if not staged:
|
|
214
|
+
known = {str(entry["path"]) for entry in files}
|
|
215
|
+
untracked_files, untracked_diff, untracked_err = await asyncio.get_event_loop().run_in_executor(
|
|
216
|
+
None,
|
|
217
|
+
lambda: _collect_untracked(root, paths, known_paths=known),
|
|
218
|
+
)
|
|
219
|
+
if untracked_err is not None:
|
|
220
|
+
return {
|
|
221
|
+
"ok": False,
|
|
222
|
+
"files": [],
|
|
223
|
+
"unified_diff": "",
|
|
224
|
+
"truncated": False,
|
|
225
|
+
"error": untracked_err,
|
|
226
|
+
"staged": staged,
|
|
227
|
+
}
|
|
228
|
+
files.extend(untracked_files)
|
|
229
|
+
if untracked_diff:
|
|
230
|
+
unified_parts.append(untracked_diff.rstrip("\n"))
|
|
231
|
+
|
|
232
|
+
combined = "\n".join(part for part in unified_parts if part)
|
|
233
|
+
if combined:
|
|
234
|
+
combined += "\n"
|
|
235
|
+
unified_diff, truncated = truncate_text(combined)
|
|
236
|
+
return {"ok": True, "files": files, "unified_diff": unified_diff, "truncated": truncated, "staged": staged}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _empty_diff_payload(staged: bool) -> dict[str, object]:
|
|
240
|
+
return {"ok": True, "files": [], "unified_diff": "", "truncated": False, "staged": staged}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
async def handle_get_diff(root: Path, db: Database | None, arguments: dict) -> list[TextContent]:
|
|
244
|
+
if not is_git_repo(root):
|
|
245
|
+
return error_text("get_diff requires a git repository.", code="not_a_git_repo")
|
|
246
|
+
task_id = optional_string_argument(arguments, "task_id")
|
|
247
|
+
if task_id == "":
|
|
248
|
+
return error_text("task_id must be a string", code="invalid_arguments", argument="task_id")
|
|
249
|
+
explicit_paths, arg_error = optional_string_list_argument(arguments, "paths")
|
|
250
|
+
if arg_error:
|
|
251
|
+
return arg_error
|
|
252
|
+
staged_value = arguments.get("staged", False)
|
|
253
|
+
if not isinstance(staged_value, bool):
|
|
254
|
+
return error_text("staged must be a boolean", code="invalid_arguments", argument="staged")
|
|
255
|
+
|
|
256
|
+
scope_paths: list[str] = [p.replace("\\", "/") for p in explicit_paths]
|
|
257
|
+
task_scoped = False
|
|
258
|
+
if task_id:
|
|
259
|
+
if db is None:
|
|
260
|
+
return error_text(
|
|
261
|
+
"DevCouncil not initialized in this directory.",
|
|
262
|
+
code="not_initialized",
|
|
263
|
+
)
|
|
264
|
+
with db.get_session() as session:
|
|
265
|
+
task = TaskRepository(session).get_by_id(task_id)
|
|
266
|
+
if task is None:
|
|
267
|
+
return error_text(f"Task {task_id} not found.", code="not_found", task_id=task_id)
|
|
268
|
+
planned = [pf.path.replace("\\", "/") for pf in task.planned_files]
|
|
269
|
+
planned_set = set(planned)
|
|
270
|
+
task_scoped = True
|
|
271
|
+
if explicit_paths:
|
|
272
|
+
# Intersect only — explicit paths must never broaden task scope.
|
|
273
|
+
scope_paths = [p for p in scope_paths if p in planned_set]
|
|
274
|
+
else:
|
|
275
|
+
scope_paths = planned
|
|
276
|
+
|
|
277
|
+
if task_scoped and not scope_paths:
|
|
278
|
+
# Empty planned scope or empty intersection: fail closed to empty, never full repo.
|
|
279
|
+
return json_text(_empty_diff_payload(staged_value))
|
|
280
|
+
|
|
281
|
+
return json_text(await git_diff(root, scope_paths, staged_value))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Code-review graph context MCP tool handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mcp.types import TextContent
|
|
8
|
+
|
|
9
|
+
from devcouncil.integrations.mcp.util import (
|
|
10
|
+
json_text,
|
|
11
|
+
parse_cli_json,
|
|
12
|
+
run_cli_command,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def handle_graph_context(root: Path, arguments: dict) -> list[TextContent]:
|
|
17
|
+
files = arguments.get("files", [])
|
|
18
|
+
if not isinstance(files, list):
|
|
19
|
+
files = []
|
|
20
|
+
file_args: list[str] = []
|
|
21
|
+
for item in files:
|
|
22
|
+
if isinstance(item, str) and item:
|
|
23
|
+
file_args.extend(["--file", item])
|
|
24
|
+
payload, _cli_error = parse_cli_json(
|
|
25
|
+
run_cli_command(["graph-context", "--json", *file_args], root, truncate=False),
|
|
26
|
+
)
|
|
27
|
+
if payload is not None:
|
|
28
|
+
return json_text(payload)
|
|
29
|
+
from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
|
|
30
|
+
|
|
31
|
+
context = CodeReviewGraphAdapter(root).get_context(
|
|
32
|
+
[file for file in files if isinstance(file, str)]
|
|
33
|
+
)
|
|
34
|
+
return [TextContent(type="text", text=context.model_dump_json(indent=2))]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Agent handoff MCP tool handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mcp.types import TextContent
|
|
8
|
+
|
|
9
|
+
from devcouncil.integrations.mcp.util import (
|
|
10
|
+
json_text,
|
|
11
|
+
parse_cli_json,
|
|
12
|
+
required_string_argument,
|
|
13
|
+
run_cli_command,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def handle_handoff_agent(root: Path, db: object, arguments: dict) -> list[TextContent]:
|
|
18
|
+
del db # routed through CLI service layer
|
|
19
|
+
task_id, arg_error = required_string_argument(arguments, "task_id")
|
|
20
|
+
if arg_error:
|
|
21
|
+
return arg_error
|
|
22
|
+
lease_token, arg_error = required_string_argument(arguments, "lease_token")
|
|
23
|
+
if arg_error:
|
|
24
|
+
return arg_error
|
|
25
|
+
from_agent, arg_error = required_string_argument(arguments, "from_agent")
|
|
26
|
+
if arg_error:
|
|
27
|
+
return arg_error
|
|
28
|
+
to_agent, arg_error = required_string_argument(arguments, "to_agent")
|
|
29
|
+
if arg_error:
|
|
30
|
+
return arg_error
|
|
31
|
+
assert task_id is not None and lease_token is not None
|
|
32
|
+
cli_args = [
|
|
33
|
+
"handoff-leased", task_id,
|
|
34
|
+
"--lease-token", lease_token,
|
|
35
|
+
"--from", from_agent or "",
|
|
36
|
+
"--to", to_agent or "",
|
|
37
|
+
"--json",
|
|
38
|
+
]
|
|
39
|
+
instruction = str(arguments.get("instruction") or "")
|
|
40
|
+
if instruction:
|
|
41
|
+
cli_args.extend(["--instruction", instruction])
|
|
42
|
+
payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
|
|
43
|
+
if cli_error:
|
|
44
|
+
return cli_error
|
|
45
|
+
assert payload is not None
|
|
46
|
+
if not payload.get("ok"):
|
|
47
|
+
from devcouncil.integrations.mcp.util import error_text
|
|
48
|
+
return error_text(
|
|
49
|
+
str(payload.get("error") or "handoff failed"),
|
|
50
|
+
code=str(payload.get("code") or "handoff_failed"),
|
|
51
|
+
task_id=task_id,
|
|
52
|
+
)
|
|
53
|
+
return json_text(payload)
|