devcouncil 0.3.0 → 0.4.0
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 +46 -30
- 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 +49 -30
- 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 +960 -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 +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -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 +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -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 +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -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 +60 -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 +303 -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/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
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
3
6
|
from devcouncil.artifacts.graph import ArtifactGraph
|
|
4
7
|
from devcouncil.reporting.github_check import GitHubCheckGenerator
|
|
5
8
|
|
|
6
9
|
logger = logging.getLogger(__name__)
|
|
7
10
|
|
|
11
|
+
_RETRYABLE_STATUS = {429, 500, 502, 503, 504}
|
|
12
|
+
|
|
13
|
+
|
|
8
14
|
class GitHubIntegration:
|
|
9
15
|
"""Manages interactions with GitHub API, specifically PR Checks."""
|
|
10
16
|
|
|
@@ -14,6 +20,66 @@ class GitHubIntegration:
|
|
|
14
20
|
self.commit_sha = commit_sha
|
|
15
21
|
self.base_url = f"https://api.github.com/repos/{repository}"
|
|
16
22
|
|
|
23
|
+
async def _post_with_retry(
|
|
24
|
+
self,
|
|
25
|
+
client: httpx.AsyncClient,
|
|
26
|
+
url: str,
|
|
27
|
+
*,
|
|
28
|
+
headers: dict,
|
|
29
|
+
json: dict,
|
|
30
|
+
max_attempts: int = 3,
|
|
31
|
+
) -> httpx.Response:
|
|
32
|
+
last_exc: Exception | None = None
|
|
33
|
+
for attempt in range(1, max_attempts + 1):
|
|
34
|
+
try:
|
|
35
|
+
response = await client.post(url, headers=headers, json=json)
|
|
36
|
+
if response.status_code in _RETRYABLE_STATUS and attempt < max_attempts:
|
|
37
|
+
delay = min(30.0, 2.0 ** attempt)
|
|
38
|
+
logger.warning(
|
|
39
|
+
"GitHub POST %s returned %s; retrying %d/%d in %.0fs",
|
|
40
|
+
url,
|
|
41
|
+
response.status_code,
|
|
42
|
+
attempt,
|
|
43
|
+
max_attempts,
|
|
44
|
+
delay,
|
|
45
|
+
)
|
|
46
|
+
await asyncio.sleep(delay)
|
|
47
|
+
continue
|
|
48
|
+
response.raise_for_status()
|
|
49
|
+
return response
|
|
50
|
+
except httpx.TimeoutException as exc:
|
|
51
|
+
last_exc = exc
|
|
52
|
+
if attempt >= max_attempts:
|
|
53
|
+
raise
|
|
54
|
+
delay = min(30.0, 2.0 ** attempt)
|
|
55
|
+
logger.warning(
|
|
56
|
+
"GitHub POST %s timed out; retrying %d/%d in %.0fs",
|
|
57
|
+
url,
|
|
58
|
+
attempt,
|
|
59
|
+
max_attempts,
|
|
60
|
+
delay,
|
|
61
|
+
)
|
|
62
|
+
await asyncio.sleep(delay)
|
|
63
|
+
except httpx.HTTPStatusError:
|
|
64
|
+
raise
|
|
65
|
+
except httpx.HTTPError as exc:
|
|
66
|
+
last_exc = exc
|
|
67
|
+
if attempt >= max_attempts:
|
|
68
|
+
raise
|
|
69
|
+
delay = min(30.0, 2.0 ** attempt)
|
|
70
|
+
logger.warning(
|
|
71
|
+
"GitHub POST %s failed (%s); retrying %d/%d in %.0fs",
|
|
72
|
+
url,
|
|
73
|
+
exc,
|
|
74
|
+
attempt,
|
|
75
|
+
max_attempts,
|
|
76
|
+
delay,
|
|
77
|
+
)
|
|
78
|
+
await asyncio.sleep(delay)
|
|
79
|
+
if last_exc is not None:
|
|
80
|
+
raise last_exc
|
|
81
|
+
raise RuntimeError("GitHub POST failed without a response")
|
|
82
|
+
|
|
17
83
|
async def report_verification(self, graph: ArtifactGraph):
|
|
18
84
|
"""Creates or updates a GitHub Check Run with the current verification status."""
|
|
19
85
|
payload = GitHubCheckGenerator.generate(graph)
|
|
@@ -22,18 +88,18 @@ class GitHubIntegration:
|
|
|
22
88
|
headers = {
|
|
23
89
|
"Authorization": f"Bearer {self.github_token}",
|
|
24
90
|
"Accept": "application/vnd.github.v3+json",
|
|
25
|
-
"Content-Type": "application/json"
|
|
91
|
+
"Content-Type": "application/json",
|
|
26
92
|
}
|
|
27
93
|
|
|
28
94
|
async with httpx.AsyncClient() as client:
|
|
29
95
|
try:
|
|
30
|
-
|
|
96
|
+
await self._post_with_retry(
|
|
97
|
+
client,
|
|
31
98
|
f"{self.base_url}/check-runs",
|
|
32
99
|
headers=headers,
|
|
33
|
-
json=payload
|
|
100
|
+
json=payload,
|
|
34
101
|
)
|
|
35
|
-
|
|
36
|
-
logger.info(f"GitHub PR Check updated for {self.repository} at {self.commit_sha}")
|
|
102
|
+
logger.info("GitHub PR Check updated for %s at %s", self.repository, self.commit_sha)
|
|
37
103
|
except Exception as e:
|
|
38
|
-
logger.error(
|
|
104
|
+
logger.error("Failed to report to GitHub: %s", e)
|
|
39
105
|
raise
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Rich/JSON renderers for dev integrate subcommands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from devcouncil.executors.agent_registry import (
|
|
13
|
+
BUILTIN_CODING_EXECUTOR_NAMES,
|
|
14
|
+
CODING_CLI_INTEGRATION_INFO,
|
|
15
|
+
detect_available_coding_cli,
|
|
16
|
+
integration_tier_label,
|
|
17
|
+
resolve_automated_executor,
|
|
18
|
+
resolve_coding_cli_executable,
|
|
19
|
+
resolve_coding_cli_probe_order,
|
|
20
|
+
)
|
|
21
|
+
from devcouncil.integrations.check import build_integration_check_report, integration_status_summary
|
|
22
|
+
from devcouncil.integrations.clients.common import _load_raw_config
|
|
23
|
+
|
|
24
|
+
PREFERRED_COMMAND = "dev integrate"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_recommendations(root: Path, console: Console) -> None:
|
|
28
|
+
probe_order = resolve_coding_cli_probe_order(root)
|
|
29
|
+
detected = detect_available_coding_cli(root, probe_order=probe_order)
|
|
30
|
+
resolved = resolve_automated_executor(root, None)
|
|
31
|
+
|
|
32
|
+
table = Table(title="DevCouncil Integration Recommendations")
|
|
33
|
+
table.add_column("Client", style="cyan")
|
|
34
|
+
table.add_column("PATH")
|
|
35
|
+
table.add_column("Tier")
|
|
36
|
+
table.add_column("MCP")
|
|
37
|
+
table.add_column("Hooks")
|
|
38
|
+
|
|
39
|
+
for client in probe_order:
|
|
40
|
+
info = CODING_CLI_INTEGRATION_INFO.get(client)
|
|
41
|
+
on_path = resolve_coding_cli_executable(root, client)
|
|
42
|
+
table.add_row(
|
|
43
|
+
client,
|
|
44
|
+
"[green]yes[/green]" if on_path else "[dim]no[/dim]",
|
|
45
|
+
integration_tier_label(client),
|
|
46
|
+
"yes" if info and info.mcp else "no",
|
|
47
|
+
"yes" if info and info.hooks else "no",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
console.print(table)
|
|
51
|
+
if summary := integration_status_summary(root):
|
|
52
|
+
if summary.get("custom_probe_order"):
|
|
53
|
+
console.print(
|
|
54
|
+
f"\n[dim]Probe order:[/dim] {', '.join(summary['probe_order'])} "
|
|
55
|
+
f"(from execution.coding_cli_probe_order)"
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
console.print(f"\n[dim]Probe order:[/dim] {', '.join(summary['probe_order'])} (default)")
|
|
59
|
+
if detected:
|
|
60
|
+
console.print(f"\n[bold]Recommended executor:[/bold] [cyan]{resolved}[/cyan]")
|
|
61
|
+
console.print(f"Run: [dim]dev run TASK-001 --executor {resolved}[/dim]")
|
|
62
|
+
console.print(f"Or: [dim]dev go \"Your goal\" --executor {resolved}[/dim]")
|
|
63
|
+
console.print(f"Setup: [dim]{PREFERRED_COMMAND} {resolved} --apply[/dim]")
|
|
64
|
+
else:
|
|
65
|
+
console.print("\n[yellow]No built-in coding CLI was found on PATH.[/yellow]")
|
|
66
|
+
console.print("Install Codex, Claude Code, Cursor Agent, OpenCode, Antigravity, or register a custom CLI:")
|
|
67
|
+
console.print(f"[dim]{PREFERRED_COMMAND} cli-agent NAME --command TOOL --apply[/dim]")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_integration_status(root: Path, console: Console, *, as_json: bool) -> None:
|
|
71
|
+
summary = integration_status_summary(root)
|
|
72
|
+
raw_config = _load_raw_config(root) if (root / ".devcouncil").exists() else {}
|
|
73
|
+
integrations = raw_config.get("integrations", {})
|
|
74
|
+
|
|
75
|
+
if as_json:
|
|
76
|
+
payload = {
|
|
77
|
+
**summary,
|
|
78
|
+
"integrations_enabled": {
|
|
79
|
+
name: bool(integrations.get(name, {}).get("enabled"))
|
|
80
|
+
for name in ("cursor", "opencode", "antigravity", "warp", "aider")
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
typer.echo(json.dumps(payload, indent=2))
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
table = Table(title="DevCouncil Integration Status")
|
|
87
|
+
table.add_column("Setting", style="cyan")
|
|
88
|
+
table.add_column("Value")
|
|
89
|
+
|
|
90
|
+
table.add_row("Project", "[green]initialized[/green]" if summary["project_initialized"] else "[yellow]not initialized[/yellow]")
|
|
91
|
+
table.add_row("Default executor", summary["default_executor"])
|
|
92
|
+
table.add_row("Resolved executor", summary["resolved_executor"])
|
|
93
|
+
table.add_row("CLIs on PATH", ", ".join(summary["coding_clis_on_path"]) or "[dim]none[/dim]")
|
|
94
|
+
table.add_row("Probe order", ", ".join(summary["probe_order"]))
|
|
95
|
+
table.add_row("Stream CLI output", "yes" if summary["stream_cli_output"] else "no")
|
|
96
|
+
table.add_row("Cursor resume mode", summary["cursor_resume_mode"])
|
|
97
|
+
table.add_row("Grok resume mode", summary.get("grok_resume_mode", "off"))
|
|
98
|
+
|
|
99
|
+
for name in ("cursor", "grok", "opencode", "antigravity", "warp", "aider"):
|
|
100
|
+
enabled = bool(integrations.get(name, {}).get("enabled"))
|
|
101
|
+
table.add_row(f"{name} integration", "[green]enabled[/green]" if enabled else "[dim]off[/dim]")
|
|
102
|
+
|
|
103
|
+
console.print(table)
|
|
104
|
+
if summary["resolved_executor"] not in {"", "manual"}:
|
|
105
|
+
console.print(
|
|
106
|
+
f"\n[dim]Next:[/dim] dev run TASK-001 --executor {summary['resolved_executor']} "
|
|
107
|
+
f"| {PREFERRED_COMMAND} check for full readiness"
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
console.print(f"\n[dim]Next:[/dim] {PREFERRED_COMMAND} recommend | {PREFERRED_COMMAND} check")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def print_integration_matrix(console: Console) -> None:
|
|
114
|
+
table = Table(title="DevCouncil Coding CLI Integration Matrix")
|
|
115
|
+
table.add_column("Client", style="cyan")
|
|
116
|
+
table.add_column("Tier")
|
|
117
|
+
table.add_column("Headless")
|
|
118
|
+
table.add_column("MCP setup")
|
|
119
|
+
table.add_column("Native hooks")
|
|
120
|
+
table.add_column("Enforcement")
|
|
121
|
+
table.add_column("Notes")
|
|
122
|
+
|
|
123
|
+
for client in sorted(BUILTIN_CODING_EXECUTOR_NAMES):
|
|
124
|
+
info = CODING_CLI_INTEGRATION_INFO.get(client)
|
|
125
|
+
posture = info.enforcement if info else "verify-only"
|
|
126
|
+
posture_render = {
|
|
127
|
+
"pre-action": "[green]pre-action[/green]",
|
|
128
|
+
"advisory+verify": "[cyan]advisory+verify[/cyan]",
|
|
129
|
+
}.get(posture, "[yellow]verify-only[/yellow]")
|
|
130
|
+
table.add_row(
|
|
131
|
+
client,
|
|
132
|
+
integration_tier_label(client),
|
|
133
|
+
"yes" if info and info.tier == 1 else "no",
|
|
134
|
+
"yes" if info and info.mcp else "no",
|
|
135
|
+
"yes" if info and info.hooks else "verify only",
|
|
136
|
+
posture_render,
|
|
137
|
+
info.notes if info else "",
|
|
138
|
+
)
|
|
139
|
+
console.print(table)
|
|
140
|
+
console.print(
|
|
141
|
+
"\n[dim]Enforcement:[/dim] [green]pre-action[/green] blocks forbidden writes/commands "
|
|
142
|
+
"before they happen; [cyan]advisory+verify[/cyan] warns in native hooks and "
|
|
143
|
+
"contains through sandbox/verification; [yellow]verify-only[/yellow] catches them at verify time."
|
|
144
|
+
)
|
|
145
|
+
console.print("\nSee [dim]docs/coding-cli-integration.md[/dim] for workflow guidance.")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def run_integration_check(
|
|
149
|
+
root: Path,
|
|
150
|
+
console: Console,
|
|
151
|
+
*,
|
|
152
|
+
strict: bool,
|
|
153
|
+
as_json: bool,
|
|
154
|
+
report_file: Path | None,
|
|
155
|
+
legacy_command: str = "dev setup --integrate",
|
|
156
|
+
) -> None:
|
|
157
|
+
report = build_integration_check_report(root, strict=strict)
|
|
158
|
+
table = Table(title="DevCouncil Integration Check")
|
|
159
|
+
table.add_column("Check", style="cyan")
|
|
160
|
+
table.add_column("Status", style="magenta")
|
|
161
|
+
table.add_column("Details")
|
|
162
|
+
|
|
163
|
+
for row in report.checks:
|
|
164
|
+
if row.status == "ok":
|
|
165
|
+
rendered = "[green]OK[/green]"
|
|
166
|
+
elif row.status == "skip":
|
|
167
|
+
rendered = "[dim]SKIP[/dim]"
|
|
168
|
+
elif row.status == "missing":
|
|
169
|
+
rendered = "[yellow]Missing[/yellow]"
|
|
170
|
+
else:
|
|
171
|
+
rendered = "[red]FAIL[/red]"
|
|
172
|
+
table.add_row(row.name, rendered, row.details)
|
|
173
|
+
|
|
174
|
+
write_json = as_json or report_file is not None
|
|
175
|
+
if write_json:
|
|
176
|
+
json_text = report.to_json()
|
|
177
|
+
if report_file is not None:
|
|
178
|
+
report_path = Path(report_file).expanduser().resolve()
|
|
179
|
+
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
report_path.write_text(json_text + "\n", encoding="utf-8")
|
|
181
|
+
if not as_json:
|
|
182
|
+
console.print(f"[dim]Wrote integration report to[/dim] {report_path}")
|
|
183
|
+
if as_json:
|
|
184
|
+
typer.echo(json_text)
|
|
185
|
+
if not write_json or not as_json:
|
|
186
|
+
console.print(table)
|
|
187
|
+
|
|
188
|
+
if report.failures:
|
|
189
|
+
if not as_json:
|
|
190
|
+
console.print(
|
|
191
|
+
f"\n[yellow]Fix failed checks, then run:[/yellow] {PREFERRED_COMMAND} all --apply "
|
|
192
|
+
f"(or {legacy_command} --apply)."
|
|
193
|
+
)
|
|
194
|
+
raise typer.Exit(code=1)
|
|
195
|
+
|
|
196
|
+
if not as_json:
|
|
197
|
+
console.print(f"\n[green]Ready.[/green] Run: {PREFERRED_COMMAND} all --apply (or {legacy_command} --apply).")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP tool handler modules extracted from server.call_tool."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""AST match and LSP status MCP tool handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mcp.types import TextContent
|
|
9
|
+
|
|
10
|
+
from devcouncil.indexing.ast_matcher import AstMatcher
|
|
11
|
+
from devcouncil.indexing.lsp import LspInspector
|
|
12
|
+
from devcouncil.integrations.mcp.util import (
|
|
13
|
+
error_text,
|
|
14
|
+
int_argument,
|
|
15
|
+
optional_string_argument,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_AST_MATCHER_CACHE: dict[str, AstMatcher] = {}
|
|
19
|
+
_LSP_INSPECTOR_CACHE: dict[str, LspInspector] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def reset_caches() -> None:
|
|
23
|
+
"""Drop per-root AST/LSP caches (for test isolation)."""
|
|
24
|
+
_AST_MATCHER_CACHE.clear()
|
|
25
|
+
_LSP_INSPECTOR_CACHE.clear()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_ast_matcher(root: Path) -> AstMatcher:
|
|
29
|
+
key = str(root.resolve())
|
|
30
|
+
matcher = _AST_MATCHER_CACHE.get(key)
|
|
31
|
+
if matcher is None:
|
|
32
|
+
matcher = AstMatcher(root)
|
|
33
|
+
_AST_MATCHER_CACHE[key] = matcher
|
|
34
|
+
return matcher
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_lsp_inspector(root: Path) -> LspInspector:
|
|
38
|
+
key = str(root.resolve())
|
|
39
|
+
inspector = _LSP_INSPECTOR_CACHE.get(key)
|
|
40
|
+
if inspector is None:
|
|
41
|
+
inspector = LspInspector(root)
|
|
42
|
+
_LSP_INSPECTOR_CACHE[key] = inspector
|
|
43
|
+
return inspector
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def handle_lsp_status(root: Path, arguments: dict) -> list[TextContent]:
|
|
47
|
+
_ = arguments
|
|
48
|
+
client_enabled = False
|
|
49
|
+
try:
|
|
50
|
+
from devcouncil.indexing.lsp_client import lsp_refs_enabled
|
|
51
|
+
|
|
52
|
+
client_enabled = lsp_refs_enabled(root)
|
|
53
|
+
except Exception:
|
|
54
|
+
client_enabled = False
|
|
55
|
+
return [
|
|
56
|
+
TextContent(
|
|
57
|
+
type="text",
|
|
58
|
+
text=get_lsp_inspector(root).summary_json(client_enabled=client_enabled),
|
|
59
|
+
)
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def handle_ast_match(root: Path, arguments: dict) -> list[TextContent]:
|
|
64
|
+
query = optional_string_argument(arguments, "query")
|
|
65
|
+
language = optional_string_argument(arguments, "language")
|
|
66
|
+
kind = optional_string_argument(arguments, "kind")
|
|
67
|
+
for arg_name, value in [("query", query), ("language", language), ("kind", kind)]:
|
|
68
|
+
if value == "":
|
|
69
|
+
return error_text(f"{arg_name} must be a string", code="invalid_arguments", argument=arg_name)
|
|
70
|
+
limit = int_argument(arguments, "limit", 100, minimum=1, maximum=500)
|
|
71
|
+
matches = get_ast_matcher(root).match(
|
|
72
|
+
query=query or "",
|
|
73
|
+
language=language,
|
|
74
|
+
kind=kind,
|
|
75
|
+
limit=limit,
|
|
76
|
+
)
|
|
77
|
+
return [TextContent(type="text", text=json.dumps({"matches": [item.model_dump() for item in matches]}, indent=2))]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Task checkout MCP tool handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
from mcp.types import TextContent
|
|
9
|
+
|
|
10
|
+
from devcouncil.integrations.mcp.util import (
|
|
11
|
+
error_text,
|
|
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_checkout_task(
|
|
21
|
+
root: Path,
|
|
22
|
+
db: object,
|
|
23
|
+
arguments: dict,
|
|
24
|
+
*,
|
|
25
|
+
load_router: Callable[[Path], object | None],
|
|
26
|
+
) -> list[TextContent]:
|
|
27
|
+
del db, load_router # routed through CLI service layer
|
|
28
|
+
task_id, arg_error = required_string_argument(arguments, "task_id")
|
|
29
|
+
if arg_error:
|
|
30
|
+
return arg_error
|
|
31
|
+
client_id, arg_error = required_string_argument(arguments, "client_id")
|
|
32
|
+
if arg_error:
|
|
33
|
+
return arg_error
|
|
34
|
+
assert task_id is not None and client_id is not None
|
|
35
|
+
agent = optional_string_argument(arguments, "agent")
|
|
36
|
+
if agent == "":
|
|
37
|
+
return error_text("agent must be a string", code="invalid_arguments", argument="agent")
|
|
38
|
+
force_value = arguments.get("force", False)
|
|
39
|
+
if not isinstance(force_value, bool):
|
|
40
|
+
return error_text("force must be a boolean", code="invalid_arguments", argument="force")
|
|
41
|
+
cli_args = ["checkout", task_id, "--client-id", client_id, "--json"]
|
|
42
|
+
if agent:
|
|
43
|
+
cli_args.extend(["--agent", agent])
|
|
44
|
+
if force_value:
|
|
45
|
+
cli_args.append("--force")
|
|
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)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Gated devcouncil_cli 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 error_text, json_text, run_cli_command
|
|
10
|
+
|
|
11
|
+
CLI_ALLOWED_ROOTS = frozenset({
|
|
12
|
+
"status", "tasks", "report", "map", "prompt", "show", "trace", "lsp", "ast",
|
|
13
|
+
"verify", "verify-leased", "rollback", "gaps", "doctor", "cost", "check", "export", "requirements",
|
|
14
|
+
"runs", "logs", "watch", "go", "wiki", "okf", "graph-context", "provenance", "resource",
|
|
15
|
+
"checkout", "release", "lease", "write", "apply-patch",
|
|
16
|
+
"next-task", "scope", "policy-check", "record-command", "run-cmd",
|
|
17
|
+
"evidence-append", "evidence-list", "handoff-leased",
|
|
18
|
+
})
|
|
19
|
+
CLI_FORBIDDEN_FLAGS = frozenset({"--project-root", "--github", "--github-pr-comment", "--gitlab-pr-comment"})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def forbidden_cli_flags(args: list[str]) -> list[str]:
|
|
23
|
+
forbidden: set[str] = set()
|
|
24
|
+
for arg in args:
|
|
25
|
+
for flag in CLI_FORBIDDEN_FLAGS:
|
|
26
|
+
if arg == flag or arg.startswith(f"{flag}="):
|
|
27
|
+
forbidden.add(flag)
|
|
28
|
+
return sorted(forbidden)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def handle_cli(root: Path, arguments: dict) -> list[TextContent]:
|
|
32
|
+
args = arguments.get("args")
|
|
33
|
+
if not isinstance(args, list) or not all(isinstance(arg, str) for arg in args) or not args:
|
|
34
|
+
return error_text("args must be a non-empty string array", code="invalid_arguments")
|
|
35
|
+
if args[0] not in CLI_ALLOWED_ROOTS:
|
|
36
|
+
return error_text(f"command {args[0]} is not allowed through MCP", code="command_not_allowed", command=args[0])
|
|
37
|
+
forbidden = forbidden_cli_flags(args)
|
|
38
|
+
if forbidden:
|
|
39
|
+
return error_text("forbidden flag(s) through MCP: " + ", ".join(forbidden), code="forbidden_flags", flags=forbidden)
|
|
40
|
+
try:
|
|
41
|
+
return json_text(run_cli_command(args, root))
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
return error_text(str(exc), code="cli_execution_error")
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Registry-backed MCP tools for transactional code intelligence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Awaitable, Callable
|
|
8
|
+
|
|
9
|
+
from mcp.types import TextContent, Tool
|
|
10
|
+
|
|
11
|
+
from devcouncil.codeintel.query import CodeIntelQueryEngine
|
|
12
|
+
from devcouncil.codeintel.service import canonical_project_root, get_codeintel_service
|
|
13
|
+
from devcouncil.codeintel.sync import get_sync_coordinator
|
|
14
|
+
from devcouncil.integrations.mcp.util import error_text, json_text, with_codeintel_freshness
|
|
15
|
+
|
|
16
|
+
Handler = Callable[[Path, dict], Awaitable[list[TextContent]]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _schema(properties: dict, required: list[str] | None = None) -> dict:
|
|
20
|
+
schema: dict = {"type": "object", "properties": {
|
|
21
|
+
"projectPath": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Explicit repository path; defaults to the MCP server project.",
|
|
24
|
+
},
|
|
25
|
+
**properties,
|
|
26
|
+
}}
|
|
27
|
+
if required:
|
|
28
|
+
schema["required"] = required
|
|
29
|
+
return schema
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def tools() -> list[Tool]:
|
|
33
|
+
return [
|
|
34
|
+
Tool(
|
|
35
|
+
name="devcouncil_code_explore",
|
|
36
|
+
description="Unified code exploration: source, callers/callees, semantic hops, and blast radius.",
|
|
37
|
+
inputSchema=_schema({
|
|
38
|
+
"query": {"type": "string"},
|
|
39
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 100, "default": 20},
|
|
40
|
+
}, ["query"]),
|
|
41
|
+
),
|
|
42
|
+
Tool(
|
|
43
|
+
name="devcouncil_code_search",
|
|
44
|
+
description="FTS5 symbol, qualified-name, and path search over the committed generation.",
|
|
45
|
+
inputSchema=_schema({
|
|
46
|
+
"query": {"type": "string"},
|
|
47
|
+
"limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 50},
|
|
48
|
+
}, ["query"]),
|
|
49
|
+
),
|
|
50
|
+
Tool(
|
|
51
|
+
name="devcouncil_code_path",
|
|
52
|
+
description="Shortest call/import/framework path with confidence and provenance per hop.",
|
|
53
|
+
inputSchema=_schema({
|
|
54
|
+
"from": {"type": "string"},
|
|
55
|
+
"to": {"type": "string"},
|
|
56
|
+
"maxDepth": {"type": "integer", "minimum": 1, "maximum": 64, "default": 32},
|
|
57
|
+
}, ["from", "to"]),
|
|
58
|
+
),
|
|
59
|
+
Tool(
|
|
60
|
+
name="devcouncil_code_impact",
|
|
61
|
+
description="Inbound symbol blast radius for one or more paths/symbols.",
|
|
62
|
+
inputSchema=_schema({
|
|
63
|
+
"targets": {"type": "array", "items": {"type": "string"}},
|
|
64
|
+
"maxDepth": {"type": "integer", "minimum": 1, "maximum": 8, "default": 3},
|
|
65
|
+
}, ["targets"]),
|
|
66
|
+
),
|
|
67
|
+
Tool(
|
|
68
|
+
name="devcouncil_code_dead",
|
|
69
|
+
description="Confidence-tiered dead-code candidates; never deletes code.",
|
|
70
|
+
inputSchema=_schema({
|
|
71
|
+
"minimumConfidence": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"enum": ["extracted", "inferred", "ambiguous"],
|
|
74
|
+
"default": "inferred",
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
78
|
+
Tool(
|
|
79
|
+
name="devcouncil_code_affected_tests",
|
|
80
|
+
description="Tests reachable through the inbound blast radius of paths/symbols.",
|
|
81
|
+
inputSchema=_schema({
|
|
82
|
+
"targets": {"type": "array", "items": {"type": "string"}},
|
|
83
|
+
"maxDepth": {"type": "integer", "minimum": 1, "maximum": 8, "default": 3},
|
|
84
|
+
}, ["targets"]),
|
|
85
|
+
),
|
|
86
|
+
Tool(
|
|
87
|
+
name="devcouncil_code_sync",
|
|
88
|
+
description="Reconcile and commit pending source changes to the canonical index.",
|
|
89
|
+
inputSchema=_schema({
|
|
90
|
+
"paths": {"type": "array", "items": {"type": "string"}},
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
Tool(
|
|
94
|
+
name="devcouncil_code_status",
|
|
95
|
+
description="Canonical generation, native watcher backend, pending files, and degraded state.",
|
|
96
|
+
inputSchema=_schema({}),
|
|
97
|
+
),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def resolve_root(default_root: Path, arguments: dict) -> Path:
|
|
102
|
+
explicit = arguments.get("projectPath")
|
|
103
|
+
return canonical_project_root(Path(explicit) if isinstance(explicit, str) and explicit else default_root)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def _explore(root: Path, arguments: dict) -> list[TextContent]:
|
|
107
|
+
return json_text(CodeIntelQueryEngine(root).explore(str(arguments["query"]), limit=int(arguments.get("limit", 20))))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
async def _search(root: Path, arguments: dict) -> list[TextContent]:
|
|
111
|
+
return json_text(CodeIntelQueryEngine(root).search(str(arguments["query"]), limit=int(arguments.get("limit", 50))))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def _path(root: Path, arguments: dict) -> list[TextContent]:
|
|
115
|
+
return json_text(CodeIntelQueryEngine(root).path(
|
|
116
|
+
str(arguments["from"]), str(arguments["to"]), max_depth=int(arguments.get("maxDepth", 32))
|
|
117
|
+
))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async def _impact(root: Path, arguments: dict) -> list[TextContent]:
|
|
121
|
+
return json_text(CodeIntelQueryEngine(root).impact(
|
|
122
|
+
[str(value) for value in arguments.get("targets") or []],
|
|
123
|
+
max_depth=int(arguments.get("maxDepth", 3)),
|
|
124
|
+
))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
async def _dead(root: Path, arguments: dict) -> list[TextContent]:
|
|
128
|
+
return json_text(CodeIntelQueryEngine(root).dead(
|
|
129
|
+
minimum_confidence=str(arguments.get("minimumConfidence", "inferred"))
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def _affected(root: Path, arguments: dict) -> list[TextContent]:
|
|
134
|
+
return json_text(CodeIntelQueryEngine(root).affected_tests(
|
|
135
|
+
[str(value) for value in arguments.get("targets") or []],
|
|
136
|
+
max_depth=int(arguments.get("maxDepth", 3)),
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
async def _sync(root: Path, arguments: dict) -> list[TextContent]:
|
|
141
|
+
coordinator = get_sync_coordinator(root)
|
|
142
|
+
supplied = [str(value) for value in arguments.get("paths") or []]
|
|
143
|
+
changed = supplied or await asyncio.to_thread(coordinator.reconcile)
|
|
144
|
+
ok = await asyncio.to_thread(coordinator.sync_now, changed)
|
|
145
|
+
payload = {"ok": ok, "reconciled": changed, **coordinator.status().as_dict()}
|
|
146
|
+
return json_text(payload) if ok else error_text(
|
|
147
|
+
coordinator.status().last_error or coordinator.status().degraded_reason or "sync failed",
|
|
148
|
+
code="codeintel_sync_failed",
|
|
149
|
+
**payload,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def _status(root: Path, _arguments: dict) -> list[TextContent]:
|
|
154
|
+
service = get_codeintel_service(root)
|
|
155
|
+
return json_text({**service.status(), "sync": get_sync_coordinator(root).status().as_dict()})
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
REGISTRY: dict[str, Handler] = {
|
|
159
|
+
"devcouncil_code_explore": _explore,
|
|
160
|
+
"devcouncil_code_search": _search,
|
|
161
|
+
"devcouncil_code_path": _path,
|
|
162
|
+
"devcouncil_code_impact": _impact,
|
|
163
|
+
"devcouncil_code_dead": _dead,
|
|
164
|
+
"devcouncil_code_affected_tests": _affected,
|
|
165
|
+
"devcouncil_code_sync": _sync,
|
|
166
|
+
"devcouncil_code_status": _status,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def dispatch(name: str, default_root: Path, arguments: dict) -> list[TextContent] | None:
|
|
171
|
+
handler = REGISTRY.get(name)
|
|
172
|
+
if handler is None:
|
|
173
|
+
return None
|
|
174
|
+
root = resolve_root(default_root, arguments)
|
|
175
|
+
try:
|
|
176
|
+
if name in {"devcouncil_code_sync", "devcouncil_code_status"}:
|
|
177
|
+
return await handler(root, arguments)
|
|
178
|
+
return await with_codeintel_freshness(root, lambda: handler(root, arguments))
|
|
179
|
+
except FileNotFoundError as exc:
|
|
180
|
+
return error_text(str(exc), code="codeintel_not_initialized", project_root=str(root))
|
|
181
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
182
|
+
return error_text(str(exc), code="invalid_arguments", tool=name)
|