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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Next-task selection 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
|
+
error_text,
|
|
11
|
+
json_text,
|
|
12
|
+
optional_string_argument,
|
|
13
|
+
parse_cli_json,
|
|
14
|
+
run_cli_command,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def handle_next_task(root: Path, db: object, arguments: dict) -> list[TextContent]:
|
|
19
|
+
del db # routed through CLI service layer
|
|
20
|
+
status_filter = optional_string_argument(arguments, "status")
|
|
21
|
+
if status_filter == "":
|
|
22
|
+
return error_text("status must be a string", code="invalid_arguments", argument="status")
|
|
23
|
+
client_id = optional_string_argument(arguments, "client_id")
|
|
24
|
+
if client_id == "":
|
|
25
|
+
return error_text("client_id must be a string", code="invalid_arguments", argument="client_id")
|
|
26
|
+
cli_args = ["next-task", "--json"]
|
|
27
|
+
if status_filter:
|
|
28
|
+
cli_args.extend(["--status", status_filter])
|
|
29
|
+
if client_id:
|
|
30
|
+
cli_args.extend(["--client-id", client_id])
|
|
31
|
+
payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
|
|
32
|
+
if cli_error:
|
|
33
|
+
return cli_error
|
|
34
|
+
assert payload is not None
|
|
35
|
+
return json_text(payload)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Policy and command-recording 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
|
+
json_text,
|
|
12
|
+
optional_string_argument,
|
|
13
|
+
parse_cli_json,
|
|
14
|
+
required_string_argument,
|
|
15
|
+
run_cli_command,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
RECORD_COMMAND_STATUSES = frozenset({"started", "finished", "failed", "blocked"})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def handle_policy_check_write(root: Path, db: object, arguments: dict) -> list[TextContent]:
|
|
22
|
+
del db # routed through CLI service layer
|
|
23
|
+
path, arg_error = required_string_argument(arguments, "path")
|
|
24
|
+
if arg_error:
|
|
25
|
+
return arg_error
|
|
26
|
+
assert path is not None
|
|
27
|
+
task_id = optional_string_argument(arguments, "task_id")
|
|
28
|
+
if task_id == "":
|
|
29
|
+
return error_text("task_id must be a string", code="invalid_arguments", argument="task_id")
|
|
30
|
+
cli_args = ["policy-check", path, "--json"]
|
|
31
|
+
if task_id:
|
|
32
|
+
cli_args.extend(["--task-id", task_id])
|
|
33
|
+
payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
|
|
34
|
+
if cli_error:
|
|
35
|
+
return cli_error
|
|
36
|
+
assert payload is not None
|
|
37
|
+
return json_text(payload)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def handle_record_command(root: Path, db: object, arguments: dict) -> list[TextContent]:
|
|
41
|
+
del db # routed through CLI service layer
|
|
42
|
+
task_id, arg_error = required_string_argument(arguments, "task_id")
|
|
43
|
+
if arg_error:
|
|
44
|
+
return arg_error
|
|
45
|
+
lease_token, arg_error = required_string_argument(arguments, "lease_token")
|
|
46
|
+
if arg_error:
|
|
47
|
+
return arg_error
|
|
48
|
+
command, arg_error = required_string_argument(arguments, "command")
|
|
49
|
+
if arg_error:
|
|
50
|
+
return arg_error
|
|
51
|
+
status, arg_error = required_string_argument(arguments, "status")
|
|
52
|
+
if arg_error:
|
|
53
|
+
return arg_error
|
|
54
|
+
assert task_id is not None and lease_token is not None
|
|
55
|
+
if status not in RECORD_COMMAND_STATUSES:
|
|
56
|
+
return error_text(
|
|
57
|
+
f"status must be one of {sorted(RECORD_COMMAND_STATUSES)}",
|
|
58
|
+
code="invalid_arguments",
|
|
59
|
+
argument="status",
|
|
60
|
+
)
|
|
61
|
+
exit_code = arguments.get("exit_code")
|
|
62
|
+
if exit_code is not None and (not isinstance(exit_code, int) or isinstance(exit_code, bool)):
|
|
63
|
+
return error_text("exit_code must be an integer", code="invalid_arguments")
|
|
64
|
+
cli_args = [
|
|
65
|
+
"record-command", task_id,
|
|
66
|
+
"--lease-token", lease_token,
|
|
67
|
+
"--command", command or "",
|
|
68
|
+
"--status", status or "finished",
|
|
69
|
+
"--json",
|
|
70
|
+
]
|
|
71
|
+
if isinstance(exit_code, int):
|
|
72
|
+
cli_args.extend(["--exit-code", str(exit_code)])
|
|
73
|
+
reason = str(arguments.get("reason") or "")
|
|
74
|
+
if reason:
|
|
75
|
+
cli_args.extend(["--reason", reason])
|
|
76
|
+
payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
|
|
77
|
+
if cli_error:
|
|
78
|
+
return cli_error
|
|
79
|
+
assert payload is not None
|
|
80
|
+
return json_text(payload)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""MCP prompt templates and live status snapshots."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import NamedTuple
|
|
7
|
+
|
|
8
|
+
from mcp.types import GetPromptResult, Prompt, PromptArgument, PromptMessage, TextContent
|
|
9
|
+
|
|
10
|
+
from devcouncil.app.project_status import compute_phase
|
|
11
|
+
from devcouncil.executors.advisor_tool import ADVISOR_STEERING_NUDGE
|
|
12
|
+
from devcouncil.integrations.mcp.util import normalize_arguments
|
|
13
|
+
from devcouncil.storage.db import get_db
|
|
14
|
+
from devcouncil.storage.repositories import ArtifactGraphRepository, StateRepository
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PromptSpec(NamedTuple):
|
|
18
|
+
name: str
|
|
19
|
+
description: str
|
|
20
|
+
arguments: list[PromptArgument]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
PROMPT_SPECS: list[PromptSpec] = [
|
|
24
|
+
PromptSpec(
|
|
25
|
+
name="devcouncil_implement_next_task",
|
|
26
|
+
description="Pick up the next unblocked DevCouncil task and implement it through the policy-gated MCP loop.",
|
|
27
|
+
arguments=[
|
|
28
|
+
PromptArgument(name="client_id", description="Optional stable client id used for the task lease.", required=False),
|
|
29
|
+
],
|
|
30
|
+
),
|
|
31
|
+
PromptSpec(
|
|
32
|
+
name="devcouncil_repair_task",
|
|
33
|
+
description="Repair the blocking verification gaps for a task (defaults to the active running task).",
|
|
34
|
+
arguments=[
|
|
35
|
+
PromptArgument(name="task_id", description="Task id, e.g. TASK-001. Defaults to the active task.", required=False),
|
|
36
|
+
],
|
|
37
|
+
),
|
|
38
|
+
PromptSpec(
|
|
39
|
+
name="devcouncil_verify_task",
|
|
40
|
+
description="Run DevCouncil verification for a task and report blocking gaps.",
|
|
41
|
+
arguments=[
|
|
42
|
+
PromptArgument(name="task_id", description="Task id, e.g. TASK-001. Defaults to the active task.", required=False),
|
|
43
|
+
],
|
|
44
|
+
),
|
|
45
|
+
PromptSpec(
|
|
46
|
+
name="devcouncil_review_live",
|
|
47
|
+
description="Review pending live-review critique cards and resolve the blocking ones.",
|
|
48
|
+
arguments=[
|
|
49
|
+
PromptArgument(name="task_id", description="Optional task scope for the live-review cards.", required=False),
|
|
50
|
+
],
|
|
51
|
+
),
|
|
52
|
+
PromptSpec(
|
|
53
|
+
name="devcouncil_project_status",
|
|
54
|
+
description="Summarize the current DevCouncil project phase, tasks, and blocking gaps.",
|
|
55
|
+
arguments=[],
|
|
56
|
+
),
|
|
57
|
+
PromptSpec(
|
|
58
|
+
name="devcouncil_apply_knowledge",
|
|
59
|
+
description="Select the ingested project knowledge (OKF + design) that applies to a goal and inject it.",
|
|
60
|
+
arguments=[
|
|
61
|
+
PromptArgument(name="goal", description="The task or goal to find applicable project knowledge for.", required=True),
|
|
62
|
+
],
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def status_snapshot(root: Path) -> str:
|
|
68
|
+
"""A short live status block for prompt bodies, or an init hint when uninitialized."""
|
|
69
|
+
db = get_db(root)
|
|
70
|
+
if not db:
|
|
71
|
+
return (
|
|
72
|
+
"DevCouncil is not initialized here yet — run `dev status` or `dev map` "
|
|
73
|
+
"(both auto-bootstrap a minimal project) or `dev init` for full setup."
|
|
74
|
+
)
|
|
75
|
+
try:
|
|
76
|
+
with db.get_session() as session:
|
|
77
|
+
graph = ArtifactGraphRepository(session).load_graph()
|
|
78
|
+
summary = graph.coverage_summary()
|
|
79
|
+
state = StateRepository(session).get_state()
|
|
80
|
+
phase = compute_phase(graph, state.current_phase if state else None)
|
|
81
|
+
return (
|
|
82
|
+
f"Phase: {phase} | "
|
|
83
|
+
f"tasks: {summary['total_tasks']} | "
|
|
84
|
+
f"gaps: {summary['total_gaps']} ({summary['blocking_gaps']} blocking)"
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
return "DevCouncil status unavailable."
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def render_prompt_text(name: str, arguments: dict, root: Path) -> str:
|
|
91
|
+
snapshot = status_snapshot(root)
|
|
92
|
+
if name == "devcouncil_implement_next_task":
|
|
93
|
+
client_id = arguments.get("client_id") or "claude-code"
|
|
94
|
+
return (
|
|
95
|
+
"You are implementing the next DevCouncil task under policy enforcement.\n\n"
|
|
96
|
+
f"Project status: {snapshot}\n\n"
|
|
97
|
+
"Do exactly this:\n"
|
|
98
|
+
"1. Call `devcouncil_next_task` to get the highest-priority unblocked task.\n"
|
|
99
|
+
f"2. Call `devcouncil_checkout_task` with that task_id and client_id='{client_id}' to acquire a lease.\n"
|
|
100
|
+
"3. Read the task scope with `devcouncil_get_task` and `devcouncil_get_prompt`; inspect files with `devcouncil_read_file` and `devcouncil_get_diff`.\n"
|
|
101
|
+
"4. Make changes ONLY through `devcouncil_write_file` / `devcouncil_apply_patch` (the policy gate rejects out-of-scope or protected paths) and run tests with `devcouncil_run_command`.\n"
|
|
102
|
+
f"5. {ADVISOR_STEERING_NUDGE}\n"
|
|
103
|
+
"6. Call `devcouncil_verify_task`; if it reports blocking gaps, fix them and re-verify.\n"
|
|
104
|
+
"7. When verified, call `devcouncil_release_task` with the lease token.\n\n"
|
|
105
|
+
"Never edit files outside the task scope. If a write is rejected, call `devcouncil_update_task_scope` only when the change is legitimately in-scope."
|
|
106
|
+
)
|
|
107
|
+
if name == "devcouncil_repair_task":
|
|
108
|
+
task_id = arguments.get("task_id") or "(the active task)"
|
|
109
|
+
return (
|
|
110
|
+
f"Repair the blocking verification gaps for {task_id}.\n\n"
|
|
111
|
+
f"Project status: {snapshot}\n\n"
|
|
112
|
+
"1. Call `devcouncil_get_gaps` (blocking_only=true) and `devcouncil_get_next_actions` for the task.\n"
|
|
113
|
+
"2. Inspect the relevant files and evidence with `devcouncil_read_file` and `devcouncil_get_evidence`.\n"
|
|
114
|
+
"3. Apply minimal fixes via `devcouncil_apply_patch` / `devcouncil_write_file`.\n"
|
|
115
|
+
f"4. {ADVISOR_STEERING_NUDGE}\n"
|
|
116
|
+
"5. Re-run `devcouncil_verify_task` until no blocking gaps remain, then `devcouncil_release_task`."
|
|
117
|
+
)
|
|
118
|
+
if name == "devcouncil_verify_task":
|
|
119
|
+
task_id = arguments.get("task_id") or "(the active task)"
|
|
120
|
+
return (
|
|
121
|
+
f"Run DevCouncil verification for {task_id} and report the result.\n\n"
|
|
122
|
+
f"Project status: {snapshot}\n\n"
|
|
123
|
+
"Call `devcouncil_verify_task` (you must hold the task lease via `devcouncil_checkout_task`). "
|
|
124
|
+
"Summarize the blocking gaps and proposed next actions; do not mark work complete while blocking gaps remain."
|
|
125
|
+
)
|
|
126
|
+
if name == "devcouncil_review_live":
|
|
127
|
+
scope = arguments.get("task_id")
|
|
128
|
+
scope_line = f" scoped to {scope}" if scope else ""
|
|
129
|
+
return (
|
|
130
|
+
f"Review the pending live-review critique cards{scope_line}.\n\n"
|
|
131
|
+
"1. Call `devcouncil_live_review` for the blocker count and `devcouncil_live_cards` (status='open') for the cards.\n"
|
|
132
|
+
"2. For each blocking card, call `devcouncil_live_repair_prompt` (or `devcouncil_live_repair_all`) to get a ready-to-apply repair.\n"
|
|
133
|
+
"3. Apply the fixes through the policy-gated write tools and re-verify."
|
|
134
|
+
)
|
|
135
|
+
if name == "devcouncil_project_status":
|
|
136
|
+
return (
|
|
137
|
+
"Summarize the DevCouncil project state for the user.\n\n"
|
|
138
|
+
f"Live snapshot: {snapshot}\n\n"
|
|
139
|
+
"Call `devcouncil_status` and `devcouncil_report` for the full coverage report, then give a concise "
|
|
140
|
+
"phase / tasks / blocking-gaps summary and recommend the next action."
|
|
141
|
+
)
|
|
142
|
+
if name == "devcouncil_apply_knowledge":
|
|
143
|
+
goal = arguments.get("goal") or ""
|
|
144
|
+
return (
|
|
145
|
+
f"Find and apply the project knowledge that applies to this goal: {goal!r}.\n\n"
|
|
146
|
+
"Call `devcouncil_select_knowledge` with the goal, then treat the returned preamble as authoritative "
|
|
147
|
+
"project context (design system + OKF docs) for any code you write toward this goal."
|
|
148
|
+
)
|
|
149
|
+
return f"Unknown DevCouncil prompt: {name}"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def list_prompts() -> list[Prompt]:
|
|
153
|
+
return [
|
|
154
|
+
Prompt(name=spec.name, description=spec.description, arguments=list(spec.arguments))
|
|
155
|
+
for spec in PROMPT_SPECS
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_prompt(name: str, arguments: dict | None, root: Path) -> GetPromptResult:
|
|
160
|
+
spec = next((spec for spec in PROMPT_SPECS if spec.name == name), None)
|
|
161
|
+
if spec is None:
|
|
162
|
+
raise ValueError(f"Unknown prompt: {name}")
|
|
163
|
+
args = normalize_arguments(arguments)
|
|
164
|
+
text = render_prompt_text(name, args, root)
|
|
165
|
+
return GetPromptResult(
|
|
166
|
+
description=spec.description,
|
|
167
|
+
messages=[PromptMessage(role="user", content=TextContent(type="text", text=text))],
|
|
168
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Task provenance and MCP corpus resource handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mcp.types import Resource, TextContent
|
|
9
|
+
from pydantic import AnyUrl
|
|
10
|
+
|
|
11
|
+
from devcouncil.integrations.mcp.util import json_text, required_string_argument, run_cli_command
|
|
12
|
+
from devcouncil.knowledge.resource_discovery import (
|
|
13
|
+
discover_knowledge_sources,
|
|
14
|
+
knowledge_source_uri,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"discover_knowledge_sources",
|
|
19
|
+
"handle_get_task_provenance",
|
|
20
|
+
"knowledge_settings",
|
|
21
|
+
"knowledge_source_uri",
|
|
22
|
+
"list_resources",
|
|
23
|
+
"read_resource",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# Re-export for callers that imported from this module.
|
|
27
|
+
from devcouncil.knowledge.resource_discovery import knowledge_settings # noqa: E402
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _cli_json(root: Path, args: list[str]) -> tuple[dict | None, list[TextContent] | None]:
|
|
31
|
+
result = run_cli_command(args, root)
|
|
32
|
+
stdout = str(result.get("stdout") or "").strip()
|
|
33
|
+
if stdout:
|
|
34
|
+
try:
|
|
35
|
+
return json.loads(stdout), None
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
pass
|
|
38
|
+
if not result.get("ok"):
|
|
39
|
+
stderr = str(result.get("stderr") or "CLI command failed")
|
|
40
|
+
from devcouncil.integrations.mcp.util import error_text
|
|
41
|
+
return None, error_text(stderr, code="cli_failed")
|
|
42
|
+
from devcouncil.integrations.mcp.util import error_text
|
|
43
|
+
return None, error_text("CLI command returned invalid JSON", code="cli_parse_error")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def handle_get_task_provenance(root: Path, db: object, arguments: dict) -> list[TextContent]:
|
|
47
|
+
task_id, arg_error = required_string_argument(arguments, "task_id")
|
|
48
|
+
if arg_error:
|
|
49
|
+
return arg_error
|
|
50
|
+
assert task_id is not None
|
|
51
|
+
payload, cli_error = _cli_json(root, ["provenance", task_id, "--json"])
|
|
52
|
+
if cli_error:
|
|
53
|
+
return cli_error
|
|
54
|
+
assert payload is not None
|
|
55
|
+
return json_text(payload)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def list_resources(root: Path) -> list[Resource]:
|
|
59
|
+
"""Expose the DevCouncil corpus as browsable MCP resources."""
|
|
60
|
+
result = run_cli_command(["resource", "list", "--json"], root)
|
|
61
|
+
stdout = str(result.get("stdout") or "").strip()
|
|
62
|
+
if stdout:
|
|
63
|
+
try:
|
|
64
|
+
payload = json.loads(stdout)
|
|
65
|
+
descriptors = payload.get("resources") or []
|
|
66
|
+
if isinstance(descriptors, list):
|
|
67
|
+
return [
|
|
68
|
+
Resource(
|
|
69
|
+
uri=AnyUrl(item["uri"]),
|
|
70
|
+
name=item["name"],
|
|
71
|
+
description=item["description"],
|
|
72
|
+
mimeType=item["mimeType"],
|
|
73
|
+
)
|
|
74
|
+
for item in descriptors
|
|
75
|
+
if isinstance(item, dict) and item.get("uri")
|
|
76
|
+
]
|
|
77
|
+
except json.JSONDecodeError:
|
|
78
|
+
pass
|
|
79
|
+
# Fallback when CLI is unavailable (e.g. during early init).
|
|
80
|
+
from devcouncil.reporting.mcp_resources import list_mcp_resource_uris
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
Resource(
|
|
84
|
+
uri=AnyUrl(item["uri"]),
|
|
85
|
+
name=item["name"],
|
|
86
|
+
description=item["description"],
|
|
87
|
+
mimeType=item["mimeType"],
|
|
88
|
+
)
|
|
89
|
+
for item in list_mcp_resource_uris(root)
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def read_resource(root: Path, uri: AnyUrl) -> str:
|
|
94
|
+
key = str(uri).rstrip("/")
|
|
95
|
+
result = run_cli_command(["resource", "read", key], root)
|
|
96
|
+
stdout = result.get("stdout")
|
|
97
|
+
if result.get("ok") and stdout is not None:
|
|
98
|
+
return str(stdout)
|
|
99
|
+
from devcouncil.reporting.mcp_resources import read_mcp_resource
|
|
100
|
+
|
|
101
|
+
return read_mcp_resource(root, key)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Read-file MCP tool handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mcp.types import TextContent
|
|
9
|
+
|
|
10
|
+
from devcouncil.integrations.mcp.util import (
|
|
11
|
+
error_text,
|
|
12
|
+
int_argument,
|
|
13
|
+
is_secret_path,
|
|
14
|
+
json_text,
|
|
15
|
+
optional_string_argument,
|
|
16
|
+
required_string_argument,
|
|
17
|
+
truncate_text,
|
|
18
|
+
within_root,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def handle_read_file(root: Path, arguments: dict) -> list[TextContent]:
|
|
23
|
+
rel_path, arg_error = required_string_argument(arguments, "path")
|
|
24
|
+
if arg_error:
|
|
25
|
+
return arg_error
|
|
26
|
+
assert rel_path is not None
|
|
27
|
+
if is_secret_path(root, rel_path):
|
|
28
|
+
return error_text(
|
|
29
|
+
"Refusing to read a secret/credential path.",
|
|
30
|
+
code="secret_path", path=rel_path,
|
|
31
|
+
)
|
|
32
|
+
target = within_root(root, rel_path)
|
|
33
|
+
if target is None:
|
|
34
|
+
return error_text("path escapes the project root", code="path_escape", path=rel_path)
|
|
35
|
+
if not target.exists() or not target.is_file():
|
|
36
|
+
return error_text(f"File not found: {rel_path}", code="not_found", path=rel_path)
|
|
37
|
+
try:
|
|
38
|
+
raw = target.read_bytes()
|
|
39
|
+
except OSError as exc:
|
|
40
|
+
return error_text(f"Read failed: {exc}", code="read_failed", path=rel_path)
|
|
41
|
+
sha256 = hashlib.sha256(raw).hexdigest()
|
|
42
|
+
text = raw.decode("utf-8", errors="replace")
|
|
43
|
+
all_lines = text.splitlines()
|
|
44
|
+
line_count = len(all_lines)
|
|
45
|
+
line_range = optional_string_argument(arguments, "line_range")
|
|
46
|
+
if line_range == "":
|
|
47
|
+
return error_text("line_range must be a string", code="invalid_arguments", argument="line_range")
|
|
48
|
+
selected = all_lines
|
|
49
|
+
if line_range:
|
|
50
|
+
try:
|
|
51
|
+
start_str, _, end_str = line_range.partition("-")
|
|
52
|
+
start = max(1, int(start_str))
|
|
53
|
+
end = int(end_str) if end_str else line_count
|
|
54
|
+
except ValueError:
|
|
55
|
+
return error_text("line_range must look like '10-40'", code="invalid_arguments", argument="line_range")
|
|
56
|
+
selected = all_lines[start - 1:end]
|
|
57
|
+
else:
|
|
58
|
+
offset = int_argument(arguments, "offset", 0, minimum=0, maximum=10_000_000)
|
|
59
|
+
limit_value = arguments.get("limit")
|
|
60
|
+
if isinstance(limit_value, int) and not isinstance(limit_value, bool):
|
|
61
|
+
limit = max(1, limit_value)
|
|
62
|
+
selected = all_lines[offset:offset + limit]
|
|
63
|
+
elif offset:
|
|
64
|
+
selected = all_lines[offset:]
|
|
65
|
+
windowed = "\n".join(selected)
|
|
66
|
+
content, truncated = truncate_text(windowed)
|
|
67
|
+
return json_text({
|
|
68
|
+
"ok": True,
|
|
69
|
+
"path": rel_path.replace("\\", "/"),
|
|
70
|
+
"content": content,
|
|
71
|
+
"sha256": sha256,
|
|
72
|
+
"line_count": line_count,
|
|
73
|
+
"truncated": truncated,
|
|
74
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Per-root ModelRouter cache for MCP verify/supervise handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from devcouncil.integrations.mcp.handlers import ast_lsp as ast_lsp_handlers
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
_ROUTER_CACHE: dict[str, tuple[Any, Any]] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def reset_caches() -> None:
|
|
17
|
+
"""Drop all per-root MCP caches (router/ast/lsp). For test isolation."""
|
|
18
|
+
_ROUTER_CACHE.clear()
|
|
19
|
+
ast_lsp_handlers.reset_caches()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_router(root: Path):
|
|
23
|
+
"""Build a ModelRouter from project config, or return None when no provider key
|
|
24
|
+
is configured. Cached per resolved project root with config stat invalidation."""
|
|
25
|
+
key = str(root.resolve())
|
|
26
|
+
try:
|
|
27
|
+
cfg_stat = (root / ".devcouncil" / "config.yaml").stat()
|
|
28
|
+
signature: object = (cfg_stat.st_mtime_ns, cfg_stat.st_size, cfg_stat.st_ino)
|
|
29
|
+
except OSError:
|
|
30
|
+
signature = None
|
|
31
|
+
cached = _ROUTER_CACHE.get(key)
|
|
32
|
+
if cached is not None and cached[0] == signature:
|
|
33
|
+
return cached[1]
|
|
34
|
+
router = _build_router(root)
|
|
35
|
+
_ROUTER_CACHE[key] = (signature, router)
|
|
36
|
+
return router
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _build_router(root: Path):
|
|
40
|
+
try:
|
|
41
|
+
from devcouncil.app.config import get_api_key, load_config
|
|
42
|
+
from devcouncil.llm.provider import create_provider, validate_model_provider
|
|
43
|
+
from devcouncil.llm.router import ModelRouter
|
|
44
|
+
|
|
45
|
+
config = load_config(root)
|
|
46
|
+
validate_model_provider(config.models.provider)
|
|
47
|
+
api_key = get_api_key(config.models.provider, root)
|
|
48
|
+
provider = create_provider(config.models.provider, api_key, project_root=root, provider_prefs=config.provider)
|
|
49
|
+
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
50
|
+
return ModelRouter(provider, role_config, project_root=root)
|
|
51
|
+
except Exception:
|
|
52
|
+
logger.warning("Failed to build MCP model router for %s", root, exc_info=True)
|
|
53
|
+
return None
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Run-command 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
|
+
error_text,
|
|
11
|
+
json_text,
|
|
12
|
+
parse_cli_json,
|
|
13
|
+
required_string_argument,
|
|
14
|
+
run_cli_command,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def handle_run_command(root: Path, db: object, arguments: dict) -> list[TextContent]:
|
|
19
|
+
del db # routed through CLI service layer
|
|
20
|
+
task_id, arg_error = required_string_argument(arguments, "task_id")
|
|
21
|
+
if arg_error:
|
|
22
|
+
return arg_error
|
|
23
|
+
lease_token, arg_error = required_string_argument(arguments, "lease_token")
|
|
24
|
+
if arg_error:
|
|
25
|
+
return arg_error
|
|
26
|
+
command, arg_error = required_string_argument(arguments, "command")
|
|
27
|
+
if arg_error:
|
|
28
|
+
return arg_error
|
|
29
|
+
assert task_id is not None and lease_token is not None and command is not None
|
|
30
|
+
cli_args = [
|
|
31
|
+
"run-cmd", task_id,
|
|
32
|
+
"--lease-token", lease_token,
|
|
33
|
+
"--command", command,
|
|
34
|
+
"--json",
|
|
35
|
+
]
|
|
36
|
+
payload, cli_error = parse_cli_json(run_cli_command(cli_args, root))
|
|
37
|
+
if cli_error:
|
|
38
|
+
return cli_error
|
|
39
|
+
assert payload is not None
|
|
40
|
+
if payload.get("code") == "command_not_allowed":
|
|
41
|
+
return error_text(
|
|
42
|
+
str(payload.get("error") or "Command is not in the task allowlist."),
|
|
43
|
+
code="command_not_allowed",
|
|
44
|
+
task_id=task_id,
|
|
45
|
+
command=str(payload.get("command") or command),
|
|
46
|
+
)
|
|
47
|
+
if payload.get("code") == "run_failed":
|
|
48
|
+
return error_text(
|
|
49
|
+
str(payload.get("error") or "Could not run command"),
|
|
50
|
+
code="run_failed",
|
|
51
|
+
task_id=task_id,
|
|
52
|
+
)
|
|
53
|
+
return json_text(payload)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Agent run listing and inspection 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.integrations.mcp.util import (
|
|
11
|
+
error_text,
|
|
12
|
+
int_argument,
|
|
13
|
+
json_text,
|
|
14
|
+
optional_string_argument,
|
|
15
|
+
required_string_argument,
|
|
16
|
+
run_cli_command,
|
|
17
|
+
truncate_text,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _cli_json(root: Path, args: list[str]) -> tuple[dict | None, list[TextContent] | None]:
|
|
22
|
+
result = run_cli_command(args, root)
|
|
23
|
+
stdout = str(result.get("stdout") or "").strip()
|
|
24
|
+
if stdout:
|
|
25
|
+
try:
|
|
26
|
+
return json.loads(stdout), None
|
|
27
|
+
except json.JSONDecodeError:
|
|
28
|
+
pass
|
|
29
|
+
if not result.get("ok"):
|
|
30
|
+
stderr = str(result.get("stderr") or "CLI command failed")
|
|
31
|
+
return None, error_text(stderr, code="cli_failed")
|
|
32
|
+
return None, error_text("CLI command returned invalid JSON", code="cli_parse_error")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def handle_list_agent_runs(root: Path, arguments: dict) -> list[TextContent]:
|
|
36
|
+
status_filter = optional_string_argument(arguments, "status")
|
|
37
|
+
if status_filter == "":
|
|
38
|
+
return error_text("status must be a string", code="invalid_arguments", argument="status")
|
|
39
|
+
limit = int_argument(arguments, "limit", 20, minimum=1, maximum=500)
|
|
40
|
+
cli_args = ["runs", "list", "--json", "--limit", str(limit)]
|
|
41
|
+
if status_filter:
|
|
42
|
+
cli_args.extend(["--status", status_filter])
|
|
43
|
+
payload, cli_error = _cli_json(root, cli_args)
|
|
44
|
+
if cli_error:
|
|
45
|
+
return cli_error
|
|
46
|
+
assert payload is not None
|
|
47
|
+
runs = payload.get("runs") or []
|
|
48
|
+
if not isinstance(runs, list):
|
|
49
|
+
runs = []
|
|
50
|
+
total = payload.get("total", payload.get("count", len(runs)))
|
|
51
|
+
return json_text({
|
|
52
|
+
"ok": True,
|
|
53
|
+
"runs": runs,
|
|
54
|
+
"total": total,
|
|
55
|
+
"returned": len(runs),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def handle_get_run(root: Path, arguments: dict) -> list[TextContent]:
|
|
60
|
+
run_id, arg_error = required_string_argument(arguments, "run_id")
|
|
61
|
+
if arg_error:
|
|
62
|
+
return arg_error
|
|
63
|
+
assert run_id is not None
|
|
64
|
+
payload, cli_error = _cli_json(root, ["runs", "show", run_id, "--json"])
|
|
65
|
+
if cli_error:
|
|
66
|
+
return cli_error
|
|
67
|
+
assert payload is not None
|
|
68
|
+
if not payload.get("ok", True):
|
|
69
|
+
return error_text(
|
|
70
|
+
str(payload.get("error") or f"Run {run_id} not found."),
|
|
71
|
+
code=str(payload.get("code") or "not_found"),
|
|
72
|
+
run_id=run_id,
|
|
73
|
+
)
|
|
74
|
+
tail = str(payload.get("transcript_tail") or "")
|
|
75
|
+
truncated_tail, truncated = truncate_text(tail)
|
|
76
|
+
return json_text({
|
|
77
|
+
"ok": True,
|
|
78
|
+
"run_id": payload.get("run_id", run_id),
|
|
79
|
+
"manifest": payload.get("manifest"),
|
|
80
|
+
"orphaned": payload.get("orphaned", False),
|
|
81
|
+
"transcript_path": payload.get("transcript_path"),
|
|
82
|
+
"transcript_tail": truncated_tail,
|
|
83
|
+
"transcript_truncated": truncated,
|
|
84
|
+
})
|