devcouncil 0.3.1 → 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,303 @@
|
|
|
1
|
+
"""Shared helpers for MCP tool handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Awaitable, Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from mcp.types import TextContent
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
_CLI_TIMEOUT_SECONDS = 120
|
|
17
|
+
CLI_TIMEOUT_SECONDS = _CLI_TIMEOUT_SECONDS
|
|
18
|
+
_GIT_APPLY_TIMEOUT_SECONDS = 120
|
|
19
|
+
_CLI_OUTPUT_LIMIT = 20_000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def read_log_file(path: str | None) -> str:
|
|
23
|
+
"""Best-effort read of a persisted stdout/stderr log."""
|
|
24
|
+
if not path:
|
|
25
|
+
return ""
|
|
26
|
+
try:
|
|
27
|
+
return Path(path).read_text(encoding="utf-8", errors="replace")
|
|
28
|
+
except OSError:
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def optional_string_list_argument(arguments: dict, name: str) -> tuple[list[str], list[TextContent] | None]:
|
|
33
|
+
value = arguments.get(name)
|
|
34
|
+
if value is None:
|
|
35
|
+
return [], None
|
|
36
|
+
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
37
|
+
return [], error_text(f"{name} must be a string array", code="invalid_arguments", argument=name)
|
|
38
|
+
return value, None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def json_text(payload: dict[str, object]) -> list[TextContent]:
|
|
42
|
+
return [TextContent(type="text", text=json.dumps(payload, indent=2))]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def error_text(message: str, *, code: str = "error", **details: object) -> list[TextContent]:
|
|
46
|
+
return json_text({"ok": False, "error": message, "code": code, **details})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def annotate_stale(contents: list[TextContent], coordinator: object) -> list[TextContent]:
|
|
50
|
+
"""Attach top-level stale/sync metadata when a freshness wait timed out."""
|
|
51
|
+
text = contents[0].text if contents else ""
|
|
52
|
+
try:
|
|
53
|
+
payload = json.loads(text)
|
|
54
|
+
except json.JSONDecodeError:
|
|
55
|
+
return contents
|
|
56
|
+
if not isinstance(payload, dict):
|
|
57
|
+
return contents
|
|
58
|
+
status = coordinator.status() # type: ignore[attr-defined]
|
|
59
|
+
payload["stale"] = True
|
|
60
|
+
payload["fresh"] = False
|
|
61
|
+
existing_sync = payload.get("sync")
|
|
62
|
+
sync_base = existing_sync if isinstance(existing_sync, dict) else {}
|
|
63
|
+
payload["sync"] = {
|
|
64
|
+
**sync_base,
|
|
65
|
+
"pending": list(getattr(status, "pending", []) or []),
|
|
66
|
+
"state": getattr(status, "state", "pending"),
|
|
67
|
+
"fresh": False,
|
|
68
|
+
}
|
|
69
|
+
return json_text(payload)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def with_codeintel_freshness(
|
|
73
|
+
root: Path,
|
|
74
|
+
produce: Callable[[], Awaitable[list[TextContent]]],
|
|
75
|
+
*,
|
|
76
|
+
timeout: float = 2.0,
|
|
77
|
+
) -> list[TextContent]:
|
|
78
|
+
"""Await pending sync, run ``produce``, and annotate stale responses."""
|
|
79
|
+
import asyncio
|
|
80
|
+
|
|
81
|
+
from devcouncil.codeintel.sync import get_sync_coordinator
|
|
82
|
+
|
|
83
|
+
coordinator = get_sync_coordinator(root)
|
|
84
|
+
fresh = await asyncio.to_thread(coordinator.wait_until_fresh, timeout=timeout)
|
|
85
|
+
contents = await produce()
|
|
86
|
+
if not fresh and contents:
|
|
87
|
+
return annotate_stale(contents, coordinator)
|
|
88
|
+
return contents
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def normalize_arguments(arguments: object) -> dict:
|
|
92
|
+
return arguments if isinstance(arguments, dict) else {}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def int_argument(arguments: dict, name: str, default: int, *, minimum: int, maximum: int) -> int:
|
|
96
|
+
value = arguments.get(name, default)
|
|
97
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
98
|
+
value = default
|
|
99
|
+
return max(minimum, min(value, maximum))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def optional_string_argument(arguments: dict, name: str) -> str | None:
|
|
103
|
+
value = arguments.get(name)
|
|
104
|
+
if value is None:
|
|
105
|
+
return None
|
|
106
|
+
return value if isinstance(value, str) else ""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def optional_bool_argument(arguments: dict, name: str) -> tuple[bool | None, list[TextContent] | None]:
|
|
110
|
+
"""Return ``(value, None)`` when absent/valid, or ``(None, error)`` when malformed.
|
|
111
|
+
|
|
112
|
+
Absent → ``(None, None)`` so callers can apply their own default.
|
|
113
|
+
"""
|
|
114
|
+
if name not in arguments or arguments.get(name) is None:
|
|
115
|
+
return None, None
|
|
116
|
+
value = arguments.get(name)
|
|
117
|
+
if not isinstance(value, bool):
|
|
118
|
+
return None, error_text(f"{name} must be a boolean", code="invalid_arguments", argument=name)
|
|
119
|
+
return value, None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def required_string_argument(arguments: dict, name: str) -> tuple[str | None, list[TextContent] | None]:
|
|
123
|
+
value = arguments.get(name)
|
|
124
|
+
if value is None or value == "":
|
|
125
|
+
return None, error_text(f"Missing {name}", code="missing_argument", argument=name)
|
|
126
|
+
if not isinstance(value, str):
|
|
127
|
+
return None, error_text(f"{name} must be a string", code="invalid_arguments", argument=name)
|
|
128
|
+
return value, None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def truncate_text(value: str | bytes | None, limit: int = _CLI_OUTPUT_LIMIT) -> tuple[str, bool]:
|
|
132
|
+
if value is None:
|
|
133
|
+
return "", False
|
|
134
|
+
if isinstance(value, bytes):
|
|
135
|
+
value = value.decode("utf-8", errors="replace")
|
|
136
|
+
if len(value) <= limit:
|
|
137
|
+
return value, False
|
|
138
|
+
marker = f"\n...[truncated to {limit} characters]"
|
|
139
|
+
return value[:limit] + marker, True
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def is_git_repo(root: Path) -> bool:
|
|
143
|
+
try:
|
|
144
|
+
from devcouncil.utils.proc import run_git
|
|
145
|
+
|
|
146
|
+
result = run_git(["rev-parse", "--is-inside-work-tree"], cwd=root)
|
|
147
|
+
return result.returncode == 0 and result.stdout.strip() == "true"
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
logger.debug("git work-tree check failed for %s: %s", root, exc)
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def within_root(root: Path, rel_or_abs: str) -> Path | None:
|
|
154
|
+
raw = rel_or_abs.strip().strip('"').replace("\\", "/")
|
|
155
|
+
try:
|
|
156
|
+
candidate = Path(raw)
|
|
157
|
+
resolved = candidate.resolve() if candidate.is_absolute() else (root / raw).resolve()
|
|
158
|
+
resolved.relative_to(root.resolve())
|
|
159
|
+
return resolved
|
|
160
|
+
except (OSError, ValueError):
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def diff_target_paths(unified_diff: str) -> list[str]:
|
|
165
|
+
targets: list[str] = []
|
|
166
|
+
seen: set[str] = set()
|
|
167
|
+
|
|
168
|
+
def _clean(token: str) -> str | None:
|
|
169
|
+
token = token.strip()
|
|
170
|
+
if len(token) >= 2 and token.startswith('"') and token.endswith('"'):
|
|
171
|
+
try:
|
|
172
|
+
token = token[1:-1].encode("utf-8").decode("unicode_escape")
|
|
173
|
+
except Exception:
|
|
174
|
+
token = token[1:-1]
|
|
175
|
+
if not token or token == "/dev/null":
|
|
176
|
+
return None
|
|
177
|
+
if token[:2] in ("a/", "b/"):
|
|
178
|
+
token = token[2:]
|
|
179
|
+
return token or None
|
|
180
|
+
|
|
181
|
+
def _add(path: str | None) -> None:
|
|
182
|
+
if path and path not in seen:
|
|
183
|
+
seen.add(path)
|
|
184
|
+
targets.append(path)
|
|
185
|
+
|
|
186
|
+
for line in unified_diff.splitlines():
|
|
187
|
+
if line.startswith("--- ") or line.startswith("+++ "):
|
|
188
|
+
_add(_clean(line[4:]))
|
|
189
|
+
elif line.startswith(("rename from ", "rename to ", "copy from ", "copy to ")):
|
|
190
|
+
_add(_clean(line.split(" ", 2)[2]))
|
|
191
|
+
elif line.startswith("diff --git "):
|
|
192
|
+
parts = line[len("diff --git "):].split()
|
|
193
|
+
if len(parts) == 2:
|
|
194
|
+
_add(_clean(parts[0]))
|
|
195
|
+
_add(_clean(parts[1]))
|
|
196
|
+
return targets
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def lease_ttl_seconds(root: Path) -> int:
|
|
200
|
+
try:
|
|
201
|
+
from devcouncil.app.config import load_config
|
|
202
|
+
|
|
203
|
+
return max(0, int(load_config(root).execution.lease_ttl_seconds))
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
logger.debug("Could not load lease_ttl_seconds for %s: %s", root, exc)
|
|
206
|
+
return 1800
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def allowed_next_tools(status: str, has_blocking_gaps: bool) -> list[str]:
|
|
210
|
+
if status == "verified":
|
|
211
|
+
return ["devcouncil_release_task"]
|
|
212
|
+
if status in {"done", "cancelled"}:
|
|
213
|
+
return []
|
|
214
|
+
if status in {"running", "blocked"} or has_blocking_gaps:
|
|
215
|
+
return [
|
|
216
|
+
"devcouncil_read_file",
|
|
217
|
+
"devcouncil_get_evidence",
|
|
218
|
+
"devcouncil_get_diff",
|
|
219
|
+
"devcouncil_run_command",
|
|
220
|
+
"devcouncil_apply_patch",
|
|
221
|
+
"devcouncil_write_file",
|
|
222
|
+
"devcouncil_update_task_scope",
|
|
223
|
+
"devcouncil_verify_task",
|
|
224
|
+
]
|
|
225
|
+
return [
|
|
226
|
+
"devcouncil_checkout_task",
|
|
227
|
+
"devcouncil_read_file",
|
|
228
|
+
"devcouncil_get_diff",
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def parse_cli_json(result: dict[str, object]) -> tuple[dict | None, list[TextContent] | None]:
|
|
233
|
+
"""Parse JSON stdout from a CLI subprocess, even when exit code is non-zero."""
|
|
234
|
+
stdout = str(result.get("stdout") or "").strip()
|
|
235
|
+
if stdout:
|
|
236
|
+
try:
|
|
237
|
+
return json.loads(stdout), None
|
|
238
|
+
except json.JSONDecodeError:
|
|
239
|
+
pass
|
|
240
|
+
if not result.get("ok"):
|
|
241
|
+
stderr = str(result.get("stderr") or "CLI command failed")
|
|
242
|
+
return None, error_text(stderr, code="cli_failed")
|
|
243
|
+
return None, error_text("CLI command returned invalid JSON", code="cli_parse_error")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def run_cli_command(args: list[str], root: Path) -> dict[str, object]:
|
|
247
|
+
command = [sys.executable, "-m", "devcouncil", *args, "--project-root", str(root)]
|
|
248
|
+
try:
|
|
249
|
+
result = subprocess.run(
|
|
250
|
+
command,
|
|
251
|
+
cwd=root,
|
|
252
|
+
capture_output=True,
|
|
253
|
+
text=True,
|
|
254
|
+
encoding="utf-8",
|
|
255
|
+
errors="replace",
|
|
256
|
+
timeout=_CLI_TIMEOUT_SECONDS,
|
|
257
|
+
)
|
|
258
|
+
stdout, stdout_truncated = truncate_text(result.stdout)
|
|
259
|
+
stderr, stderr_truncated = truncate_text(result.stderr)
|
|
260
|
+
return {
|
|
261
|
+
"ok": result.returncode == 0,
|
|
262
|
+
"returncode": result.returncode,
|
|
263
|
+
"stdout": stdout,
|
|
264
|
+
"stderr": stderr,
|
|
265
|
+
"stdout_truncated": stdout_truncated,
|
|
266
|
+
"stderr_truncated": stderr_truncated,
|
|
267
|
+
"timed_out": False,
|
|
268
|
+
}
|
|
269
|
+
except subprocess.TimeoutExpired as exc:
|
|
270
|
+
stdout, stdout_truncated = truncate_text(exc.output)
|
|
271
|
+
stderr, stderr_truncated = truncate_text(exc.stderr)
|
|
272
|
+
return {
|
|
273
|
+
"ok": False,
|
|
274
|
+
"returncode": None,
|
|
275
|
+
"stdout": stdout,
|
|
276
|
+
"stderr": stderr,
|
|
277
|
+
"stdout_truncated": stdout_truncated,
|
|
278
|
+
"stderr_truncated": stderr_truncated,
|
|
279
|
+
"timed_out": True,
|
|
280
|
+
"timeout_seconds": _CLI_TIMEOUT_SECONDS,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
GIT_APPLY_TIMEOUT_SECONDS = _GIT_APPLY_TIMEOUT_SECONDS
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def is_secret_path(root: Path, rel_or_abs: str) -> bool:
|
|
288
|
+
"""True when a path matches a protected secret/credential glob."""
|
|
289
|
+
from devcouncil.execution.policy_engine import SECRET_PATH_PATTERNS
|
|
290
|
+
import fnmatch as _fnmatch
|
|
291
|
+
|
|
292
|
+
normalized = rel_or_abs.strip().strip('"').replace("\\", "/")
|
|
293
|
+
try:
|
|
294
|
+
candidate = Path(normalized)
|
|
295
|
+
if candidate.is_absolute():
|
|
296
|
+
resolved = candidate.resolve()
|
|
297
|
+
try:
|
|
298
|
+
normalized = resolved.relative_to(root.resolve()).as_posix()
|
|
299
|
+
except ValueError:
|
|
300
|
+
normalized = resolved.as_posix()
|
|
301
|
+
except OSError:
|
|
302
|
+
pass
|
|
303
|
+
return any(_fnmatch.fnmatch(normalized, pattern) for pattern in SECRET_PATH_PATTERNS)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Integration doctor and optional companion setup helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from devcouncil.executors.agent_registry import (
|
|
12
|
+
VALID_INPUT_MODES,
|
|
13
|
+
load_agent_profiles,
|
|
14
|
+
load_cli_agent_specs,
|
|
15
|
+
)
|
|
16
|
+
from devcouncil.integrations.clients.common import _config_path, _load_raw_config, _save_raw_config
|
|
17
|
+
from devcouncil.utils.fsio import atomic_write_text
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_integrations_doctor_table(root: Path) -> Table:
|
|
23
|
+
"""Build the integration doctor table (PATH probes + config presence)."""
|
|
24
|
+
table = Table(title="DevCouncil Integration Doctor")
|
|
25
|
+
table.add_column("Integration", style="cyan", no_wrap=True)
|
|
26
|
+
table.add_column("Status")
|
|
27
|
+
table.add_column("Notes", overflow="fold")
|
|
28
|
+
|
|
29
|
+
checks = [
|
|
30
|
+
("Agent Flow", "agent-flow-app", "Optional live/replay visualizer for trace JSONL."),
|
|
31
|
+
("code-review-graph", "code-review-graph", "Optional structural graph context adapter."),
|
|
32
|
+
("Claude Code", "claude", "Optional MCP client and native hook runtime for pre-tool-use enforcement."),
|
|
33
|
+
("Codex CLI", "codex", "Optional MCP client, headless executor companion, and native hook runtime."),
|
|
34
|
+
("Gemini CLI (deprecated)", "gemini", "Deprecated — migrate to Google Antigravity CLI (dev integrate antigravity --apply)."),
|
|
35
|
+
("Cursor", "cursor-agent", "Optional MCP client, agent/cursor-agent executor, and native hooks."),
|
|
36
|
+
("Grok Build", "grok", "Optional MCP client, grok headless executor, and native hooks."),
|
|
37
|
+
("OpenCode", "opencode", "Optional MCP client and headless coding-agent executor."),
|
|
38
|
+
("Google Antigravity CLI", "agy", "Optional Antigravity CLI companion and headless coding-agent executor."),
|
|
39
|
+
("Warp / Oz", "oz", "Optional Warp/Oz CLI companion and agent executor."),
|
|
40
|
+
("Aider", "aider", "Optional headless executor via `dev run --executor aider` (no MCP)."),
|
|
41
|
+
]
|
|
42
|
+
for label, executable, notes in checks:
|
|
43
|
+
found = shutil.which(executable)
|
|
44
|
+
table.add_row(label, "[green]OK[/green]" if found else "[yellow]Missing[/yellow]", found or notes)
|
|
45
|
+
|
|
46
|
+
profiles = load_agent_profiles(root)
|
|
47
|
+
for name, spec in load_cli_agent_specs(root).items():
|
|
48
|
+
if spec.built_in:
|
|
49
|
+
continue
|
|
50
|
+
found = shutil.which(spec.executable)
|
|
51
|
+
mode_ok = spec.input_mode in VALID_INPUT_MODES
|
|
52
|
+
profile_ok = spec.default_profile in profiles
|
|
53
|
+
status = "[green]OK[/green]" if found and mode_ok and profile_ok else "[red]Invalid[/red]"
|
|
54
|
+
if not found:
|
|
55
|
+
status = "[yellow]Missing[/yellow]"
|
|
56
|
+
details = found or f"{spec.executable} not found on PATH"
|
|
57
|
+
if not mode_ok:
|
|
58
|
+
details = f"invalid input_mode={spec.input_mode}"
|
|
59
|
+
if not profile_ok:
|
|
60
|
+
details = f"{details}; missing profile={spec.default_profile}"
|
|
61
|
+
table.add_row(f"CLI agent: {name}", status, details)
|
|
62
|
+
|
|
63
|
+
config = _config_path(root)
|
|
64
|
+
table.add_row(
|
|
65
|
+
"DevCouncil config",
|
|
66
|
+
"[green]OK[/green]" if config.exists() else "[red]Missing[/red]",
|
|
67
|
+
str(config) if config.exists() else "Run dev init first.",
|
|
68
|
+
)
|
|
69
|
+
return table
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def apply_agent_flow_setup(root: Path) -> Path:
|
|
73
|
+
"""Record Agent Flow trace integration in config and write docs stub."""
|
|
74
|
+
trace_path = root / ".devcouncil" / "logs" / "traces.jsonl"
|
|
75
|
+
config = _load_raw_config(root)
|
|
76
|
+
integrations = config.setdefault("integrations", {})
|
|
77
|
+
integrations["agent_flow"] = {
|
|
78
|
+
"enabled": True,
|
|
79
|
+
"trace_path": str(trace_path),
|
|
80
|
+
"mode": "jsonl",
|
|
81
|
+
}
|
|
82
|
+
_save_raw_config(root, config)
|
|
83
|
+
docs_dir = root / ".devcouncil" / "integrations"
|
|
84
|
+
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
doc_path = docs_dir / "agent-flow.md"
|
|
86
|
+
doc_path.write_text(
|
|
87
|
+
"\n".join([
|
|
88
|
+
"# Agent Flow",
|
|
89
|
+
"",
|
|
90
|
+
f"DevCouncil writes trace events to `{trace_path}`.",
|
|
91
|
+
"",
|
|
92
|
+
"Local replay:",
|
|
93
|
+
"",
|
|
94
|
+
"```bash",
|
|
95
|
+
"dev trace tail --follow",
|
|
96
|
+
"```",
|
|
97
|
+
"",
|
|
98
|
+
"External visualizers can watch the JSONL file directly. DevCouncil does not modify global editor or Claude Code settings from this setup command.",
|
|
99
|
+
"",
|
|
100
|
+
]),
|
|
101
|
+
encoding="utf-8",
|
|
102
|
+
)
|
|
103
|
+
return trace_path
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def apply_code_review_graph_setup(root: Path) -> tuple[str | None, Path, bool]:
|
|
107
|
+
"""Record code-review-graph integration, ensure ignore file, and write docs stub."""
|
|
108
|
+
executable = shutil.which("code-review-graph")
|
|
109
|
+
ignore_path = root / ".code-review-graphignore"
|
|
110
|
+
created = False
|
|
111
|
+
if not ignore_path.exists():
|
|
112
|
+
# Guarded by exists(): a torn write here would never be repaired, so write atomically.
|
|
113
|
+
atomic_write_text(
|
|
114
|
+
ignore_path,
|
|
115
|
+
"\n".join([
|
|
116
|
+
".devcouncil/**",
|
|
117
|
+
".git/**",
|
|
118
|
+
".venv/**",
|
|
119
|
+
"dist/**",
|
|
120
|
+
"node_modules/**",
|
|
121
|
+
"",
|
|
122
|
+
]),
|
|
123
|
+
)
|
|
124
|
+
created = True
|
|
125
|
+
|
|
126
|
+
config = _load_raw_config(root)
|
|
127
|
+
integrations = config.setdefault("integrations", {})
|
|
128
|
+
integrations["code_review_graph"] = {
|
|
129
|
+
"enabled": True,
|
|
130
|
+
"command": "code-review-graph",
|
|
131
|
+
"optional": True,
|
|
132
|
+
}
|
|
133
|
+
_save_raw_config(root, config)
|
|
134
|
+
docs_dir = root / ".devcouncil" / "integrations"
|
|
135
|
+
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
(docs_dir / "code-review-graph.md").write_text(
|
|
137
|
+
"\n".join([
|
|
138
|
+
"# code-review-graph",
|
|
139
|
+
"",
|
|
140
|
+
"Install and build the graph outside DevCouncil:",
|
|
141
|
+
"",
|
|
142
|
+
"```bash",
|
|
143
|
+
"pipx install code-review-graph",
|
|
144
|
+
"code-review-graph build",
|
|
145
|
+
"```",
|
|
146
|
+
"",
|
|
147
|
+
"DevCouncil uses this as an optional context adapter for mapping, prompts, verification traces, and MCP graph context.",
|
|
148
|
+
"",
|
|
149
|
+
]),
|
|
150
|
+
encoding="utf-8",
|
|
151
|
+
)
|
|
152
|
+
return executable, ignore_path, created
|
|
@@ -208,7 +208,11 @@ def _clone_git(url: str) -> FetchedBundle:
|
|
|
208
208
|
["git", "clone", "--depth", "1", url, str(target)],
|
|
209
209
|
capture_output=True,
|
|
210
210
|
text=True,
|
|
211
|
+
timeout=600, # network clone; generous, but a stalled remote must not hang fetch
|
|
211
212
|
)
|
|
213
|
+
except subprocess.TimeoutExpired as exc:
|
|
214
|
+
shutil.rmtree(parent, ignore_errors=True)
|
|
215
|
+
raise RuntimeError(f"git clone of {url!r} timed out after 600s") from exc
|
|
212
216
|
except OSError as exc: # pragma: no cover - git present but unexecutable
|
|
213
217
|
shutil.rmtree(parent, ignore_errors=True)
|
|
214
218
|
raise RuntimeError(f"git clone failed to start: {exc}") from exc
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Goal-driven knowledge source selection shared by CLI and MCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def select_knowledge_payload(root: Path, goal: str) -> dict[str, object]:
|
|
9
|
+
"""Return MCP/CLI-compatible knowledge selection for ``goal``."""
|
|
10
|
+
from devcouncil.knowledge.resource_discovery import knowledge_settings
|
|
11
|
+
from devcouncil.knowledge.sources import render_knowledge_preamble, select_knowledge_sources
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
directory, design_always = knowledge_settings(root)
|
|
15
|
+
if directory is None:
|
|
16
|
+
sources = []
|
|
17
|
+
else:
|
|
18
|
+
sources = select_knowledge_sources(
|
|
19
|
+
goal, root, directory=directory, design_always=design_always
|
|
20
|
+
)
|
|
21
|
+
preamble = render_knowledge_preamble(sources)
|
|
22
|
+
return {
|
|
23
|
+
"ok": True,
|
|
24
|
+
"goal": goal,
|
|
25
|
+
"sources": [
|
|
26
|
+
{"name": source.name, "kind": source.kind, "description": source.description}
|
|
27
|
+
for source in sources
|
|
28
|
+
],
|
|
29
|
+
"preamble": preamble,
|
|
30
|
+
}
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
return {
|
|
33
|
+
"ok": True,
|
|
34
|
+
"goal": goal,
|
|
35
|
+
"sources": [],
|
|
36
|
+
"preamble": "",
|
|
37
|
+
"note": f"knowledge unavailable: {exc}",
|
|
38
|
+
}
|
|
@@ -22,6 +22,7 @@ from typing import Any
|
|
|
22
22
|
from pydantic import BaseModel, Field
|
|
23
23
|
|
|
24
24
|
from devcouncil.knowledge.frontmatter import build_frontmatter_markdown, split_frontmatter
|
|
25
|
+
from devcouncil.utils.fsio import atomic_write_text
|
|
25
26
|
|
|
26
27
|
# Markdown inline links: [text](target). We only resolve relative, non-anchor, non-URL
|
|
27
28
|
# targets into intra-bundle edges; external resources live in the `resource` field.
|
|
@@ -179,7 +180,7 @@ def write_bundle(bundle: OKFBundle, bundle_dir: Path) -> list[Path]:
|
|
|
179
180
|
continue
|
|
180
181
|
target = bundle_dir / doc.rel_path
|
|
181
182
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
182
|
-
target
|
|
183
|
+
atomic_write_text(target, doc.to_markdown())
|
|
183
184
|
written.append(target)
|
|
184
185
|
return written
|
|
185
186
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Knowledge source discovery for MCP resources and OKF selection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def knowledge_source_uri(kind: str, name: str) -> str:
|
|
13
|
+
"""Stable, parseable resource URI for one ingested knowledge source."""
|
|
14
|
+
return f"devcouncil://knowledge/{kind}/{quote(name, safe='')}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def knowledge_settings(root: Path) -> tuple[str | None, bool]:
|
|
18
|
+
"""Resolve (directory, design_always) for knowledge exposure from project config."""
|
|
19
|
+
try:
|
|
20
|
+
from devcouncil.app.config import load_config
|
|
21
|
+
|
|
22
|
+
cfg = load_config(root).knowledge
|
|
23
|
+
return (None if not cfg.enabled else cfg.directory), cfg.design_always
|
|
24
|
+
except Exception as exc:
|
|
25
|
+
logger.debug("Could not load knowledge settings for %s: %s", root, exc)
|
|
26
|
+
return ".devcouncil/knowledge", True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def discover_knowledge_sources(root: Path) -> list:
|
|
30
|
+
"""Best-effort enumeration of ingested OKF/design knowledge for the project."""
|
|
31
|
+
try:
|
|
32
|
+
from devcouncil.knowledge.sources import discover_knowledge_sources as _discover
|
|
33
|
+
|
|
34
|
+
directory, design_always = knowledge_settings(root)
|
|
35
|
+
if directory is None:
|
|
36
|
+
return []
|
|
37
|
+
return _discover(root, directory=directory, design_always=design_always)
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
logger.debug("Knowledge source discovery failed for %s: %s", root, exc)
|
|
40
|
+
return []
|