devcouncil 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +49 -30
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +960 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +303 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -1,1089 +1,107 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
from devcouncil.utils.json_persist import dump_json
|
|
2
|
+
import logging
|
|
3
3
|
import shutil
|
|
4
|
-
import subprocess
|
|
5
|
-
import sys
|
|
6
|
-
from contextlib import contextmanager
|
|
7
4
|
from pathlib import Path
|
|
8
5
|
|
|
9
6
|
import typer
|
|
10
|
-
import yaml # type: ignore[import-untyped]
|
|
11
7
|
from rich.console import Console
|
|
12
8
|
from rich.table import Table
|
|
9
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
13
10
|
|
|
14
11
|
from devcouncil.executors.agent_registry import (
|
|
15
|
-
|
|
16
|
-
CODING_CLI_INTEGRATION_INFO,
|
|
12
|
+
GEMINI_DEPRECATION_MESSAGE,
|
|
17
13
|
VALID_INPUT_MODES,
|
|
18
14
|
agent_config_entry,
|
|
19
|
-
detect_available_coding_cli,
|
|
20
|
-
integration_tier_label,
|
|
21
15
|
is_reserved_agent_name,
|
|
22
16
|
load_agent_profiles,
|
|
23
|
-
load_cli_agent_specs,
|
|
24
17
|
normalize_agent_name,
|
|
25
|
-
resolve_automated_executor,
|
|
26
|
-
resolve_coding_cli_executable,
|
|
27
|
-
resolve_coding_cli_probe_order,
|
|
28
18
|
)
|
|
29
19
|
from devcouncil.integrations.actions import apply_integration_target
|
|
30
|
-
from devcouncil.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
from devcouncil.integrations.integration_cli import (
|
|
21
|
+
print_integration_matrix,
|
|
22
|
+
print_integration_status,
|
|
23
|
+
print_recommendations,
|
|
24
|
+
run_integration_check,
|
|
25
|
+
)
|
|
26
|
+
from devcouncil.integrations.setup import (
|
|
27
|
+
apply_agent_flow_setup,
|
|
28
|
+
apply_code_review_graph_setup,
|
|
29
|
+
build_integrations_doctor_table,
|
|
30
|
+
)
|
|
31
|
+
from devcouncil.integrations.clients import (
|
|
32
|
+
antigravity as antigravity_client,
|
|
33
|
+
aider as aider_client,
|
|
34
|
+
claude as claude_client,
|
|
35
|
+
codex as codex_client,
|
|
36
|
+
common,
|
|
37
|
+
cursor as cursor_client,
|
|
38
|
+
gemini as gemini_client,
|
|
39
|
+
grok as grok_client,
|
|
40
|
+
hooks as hooks_client,
|
|
41
|
+
opencode as opencode_client,
|
|
42
|
+
warp as warp_client,
|
|
34
43
|
)
|
|
35
44
|
|
|
36
45
|
app = typer.Typer(help="Set up DevCouncil integrations with coding CLIs.")
|
|
37
46
|
setup_app = typer.Typer(help="Set up optional external companion integrations.")
|
|
38
47
|
app.add_typer(setup_app, name="setup")
|
|
39
48
|
console = Console()
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
40
50
|
|
|
41
|
-
SUPPORTED_TOOLS = ("
|
|
42
|
-
SUPPORTED_HOOK_TOOLS =
|
|
43
|
-
OPENCODE_HOOK_PLUGIN_NAME =
|
|
51
|
+
SUPPORTED_TOOLS = ("claude", "codex", "cursor", "grok", "opencode", "antigravity", "warp", "aider", "gemini")
|
|
52
|
+
SUPPORTED_HOOK_TOOLS = common.SUPPORTED_HOOK_TOOLS
|
|
53
|
+
OPENCODE_HOOK_PLUGIN_NAME = common.OPENCODE_HOOK_PLUGIN_NAME
|
|
44
54
|
PREFERRED_COMMAND = "dev integrate"
|
|
45
55
|
LEGACY_COMMAND = "dev setup --integrate"
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _claude_command(project_root: Path, scope: str) -> list[str]:
|
|
98
|
-
# The server name must come BEFORE --env: the current Claude CLI treats --env
|
|
99
|
-
# as variadic, so `--env KEY=VALUE devcouncil` swallows the name `devcouncil`
|
|
100
|
-
# as a second (invalid) env var. Putting the name first — matching the working
|
|
101
|
-
# codex form — and terminating options with `--` avoids that.
|
|
102
|
-
return [
|
|
103
|
-
"claude",
|
|
104
|
-
"mcp",
|
|
105
|
-
"add",
|
|
106
|
-
"--scope",
|
|
107
|
-
scope,
|
|
108
|
-
"devcouncil",
|
|
109
|
-
"--env",
|
|
110
|
-
f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
|
|
111
|
-
"--",
|
|
112
|
-
*_server_args(project_root),
|
|
113
|
-
]
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _cursor_config_path(project_root: Path) -> Path:
|
|
117
|
-
return project_root / ".cursor" / "mcp.json"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _cursor_mcp_config(project_root: Path) -> dict:
|
|
121
|
-
return {
|
|
122
|
-
"mcpServers": {
|
|
123
|
-
"devcouncil": {
|
|
124
|
-
"type": "stdio",
|
|
125
|
-
"command": "devcouncil",
|
|
126
|
-
"args": ["mcp-server"],
|
|
127
|
-
"env": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def _warp_mcp_config(project_root: Path) -> dict:
|
|
134
|
-
return {
|
|
135
|
-
"devcouncil": {
|
|
136
|
-
"command": "devcouncil",
|
|
137
|
-
"args": ["mcp-server"],
|
|
138
|
-
"env": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _warp_mcp_path(project_root: Path) -> Path:
|
|
144
|
-
return project_root / ".devcouncil" / "integrations" / "warp-mcp.json"
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _opencode_config_path(project_root: Path) -> Path:
|
|
148
|
-
return project_root / "opencode.json"
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _opencode_mcp_entry(project_root: Path) -> dict:
|
|
152
|
-
return {
|
|
153
|
-
"type": "local",
|
|
154
|
-
"command": ["devcouncil", "mcp-server"],
|
|
155
|
-
"environment": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
|
|
156
|
-
"enabled": True,
|
|
157
|
-
"timeout": 10000,
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def _antigravity_mcp_path(project_root: Path) -> Path:
|
|
162
|
-
return project_root / ".agents" / "mcp_config.json"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _antigravity_mcp_config(project_root: Path) -> dict:
|
|
166
|
-
return {
|
|
167
|
-
"mcpServers": {
|
|
168
|
-
"devcouncil": {
|
|
169
|
-
"command": "devcouncil",
|
|
170
|
-
"args": ["mcp-server"],
|
|
171
|
-
"env": {"DEVCOUNCIL_PROJECT_ROOT": str(project_root)},
|
|
172
|
-
"cwd": str(project_root),
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def _write_warp_mcp_config(project_root: Path) -> Path:
|
|
179
|
-
path = _warp_mcp_path(project_root)
|
|
180
|
-
_save_json(path, _warp_mcp_config(project_root))
|
|
181
|
-
return path
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def _write_cursor_config(project_root: Path) -> Path:
|
|
185
|
-
path = _cursor_config_path(project_root)
|
|
186
|
-
data = _load_json_strict(path, "Cursor")
|
|
187
|
-
mcp_servers = data.setdefault("mcpServers", {})
|
|
188
|
-
mcp_servers["devcouncil"] = _cursor_mcp_config(project_root)["mcpServers"]["devcouncil"]
|
|
189
|
-
_save_json(path, data)
|
|
190
|
-
return path
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
# When set, _mutate_raw_config applies record mutations in memory and
|
|
194
|
-
# _batched_raw_config saves config.yaml once at the end (used by
|
|
195
|
-
# `dev integrate all --apply`, which otherwise re-parses YAML per tool).
|
|
196
|
-
_PENDING_RAW_CONFIG: dict | None = None
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
@contextmanager
|
|
200
|
-
def _batched_raw_config(project_root: Path):
|
|
201
|
-
global _PENDING_RAW_CONFIG
|
|
202
|
-
# Re-entrant: if a batch is already active, participate in it instead of
|
|
203
|
-
# starting a nested load/save (which would otherwise reset the shared
|
|
204
|
-
# buffer to None on inner exit and drop the outer batch's mutations).
|
|
205
|
-
if _PENDING_RAW_CONFIG is not None:
|
|
206
|
-
yield
|
|
207
|
-
return
|
|
208
|
-
_PENDING_RAW_CONFIG = _load_raw_config(project_root)
|
|
209
|
-
try:
|
|
210
|
-
yield
|
|
211
|
-
finally:
|
|
212
|
-
# Persist whatever mutations accumulated, even if an inner installer raised
|
|
213
|
-
# partway through — matching the old per-installer save, which committed each
|
|
214
|
-
# installer's change immediately rather than dropping the whole batch on a
|
|
215
|
-
# mid-loop failure.
|
|
216
|
-
pending = _PENDING_RAW_CONFIG
|
|
217
|
-
_PENDING_RAW_CONFIG = None
|
|
218
|
-
if pending is not None:
|
|
219
|
-
_save_raw_config(project_root, pending)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def _mutate_raw_config(project_root: Path, mutate) -> None:
|
|
223
|
-
if _PENDING_RAW_CONFIG is not None:
|
|
224
|
-
mutate(_PENDING_RAW_CONFIG)
|
|
225
|
-
return
|
|
226
|
-
config = _load_raw_config(project_root)
|
|
227
|
-
mutate(config)
|
|
228
|
-
_save_raw_config(project_root, config)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def _record_cursor_config(project_root: Path) -> None:
|
|
232
|
-
def mutate(config: dict) -> None:
|
|
233
|
-
cursor = config.setdefault("integrations", {}).setdefault("cursor", {})
|
|
234
|
-
cursor.update({
|
|
235
|
-
"enabled": True,
|
|
236
|
-
"config_path": str(_cursor_config_path(project_root).relative_to(project_root)),
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
_mutate_raw_config(project_root, mutate)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
def _record_warp_config(project_root: Path) -> None:
|
|
243
|
-
def mutate(config: dict) -> None:
|
|
244
|
-
warp = config.setdefault("integrations", {}).setdefault("warp", {})
|
|
245
|
-
warp.update({
|
|
246
|
-
"enabled": True,
|
|
247
|
-
"command": warp.get("command", "oz"),
|
|
248
|
-
"run_mode": warp.get("run_mode", "local"),
|
|
249
|
-
"mcp_config_path": str(_warp_mcp_path(project_root).relative_to(project_root)),
|
|
250
|
-
})
|
|
251
|
-
|
|
252
|
-
_mutate_raw_config(project_root, mutate)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def _record_opencode_config(project_root: Path) -> None:
|
|
256
|
-
def mutate(config: dict) -> None:
|
|
257
|
-
opencode = config.setdefault("integrations", {}).setdefault("opencode", {})
|
|
258
|
-
opencode.update({
|
|
259
|
-
"enabled": True,
|
|
260
|
-
"config_path": str(_opencode_config_path(project_root).relative_to(project_root)),
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
_mutate_raw_config(project_root, mutate)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def _record_antigravity_config(project_root: Path) -> None:
|
|
267
|
-
def mutate(config: dict) -> None:
|
|
268
|
-
antigravity = config.setdefault("integrations", {}).setdefault("antigravity", {})
|
|
269
|
-
antigravity.update({
|
|
270
|
-
"enabled": True,
|
|
271
|
-
"mcp_config_path": str(_antigravity_mcp_path(project_root).relative_to(project_root)),
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
_mutate_raw_config(project_root, mutate)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def _load_json_strict(path: Path, label: str = "JSON") -> dict:
|
|
278
|
-
if not path.exists():
|
|
279
|
-
return {}
|
|
280
|
-
try:
|
|
281
|
-
return json.loads(path.read_text(encoding="utf-8")) or {}
|
|
282
|
-
except json.JSONDecodeError as exc:
|
|
283
|
-
raise ValueError(f"{path} is not valid JSON. Fix the {label} config before rerunning integration setup.") from exc
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _write_opencode_config(project_root: Path) -> Path:
|
|
287
|
-
path = _opencode_config_path(project_root)
|
|
288
|
-
data = _load_json_strict(path, "OpenCode")
|
|
289
|
-
data.setdefault("$schema", "https://opencode.ai/config.json")
|
|
290
|
-
mcp = data.setdefault("mcp", {})
|
|
291
|
-
mcp["devcouncil"] = _opencode_mcp_entry(project_root)
|
|
292
|
-
_save_json(path, data)
|
|
293
|
-
return path
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def _write_antigravity_mcp_config(project_root: Path) -> Path:
|
|
297
|
-
path = _antigravity_mcp_path(project_root)
|
|
298
|
-
data = _load_json_strict(path, "Antigravity")
|
|
299
|
-
mcp_servers = data.setdefault("mcpServers", {})
|
|
300
|
-
mcp_servers["devcouncil"] = _antigravity_mcp_config(project_root)["mcpServers"]["devcouncil"]
|
|
301
|
-
_save_json(path, data)
|
|
302
|
-
return path
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def _configure_cursor(project_root: Path, apply: bool) -> bool:
|
|
306
|
-
path = _cursor_config_path(project_root)
|
|
307
|
-
config = _cursor_mcp_config(project_root)
|
|
308
|
-
if not apply:
|
|
309
|
-
console.print("[bold]Cursor[/bold]")
|
|
310
|
-
console.print(f"Project MCP config file: [dim]{path}[/dim]")
|
|
311
|
-
console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
|
|
312
|
-
console.print("Verify in Cursor CLI with: [dim]cursor-agent mcp list[/dim]")
|
|
313
|
-
return True
|
|
314
|
-
|
|
315
|
-
if not shutil.which("cursor") and not shutil.which("cursor-agent"):
|
|
316
|
-
console.print("[yellow]Cursor CLI not found on PATH. Project MCP config will still be available to Cursor.[/yellow]")
|
|
317
|
-
try:
|
|
318
|
-
written = _write_cursor_config(project_root)
|
|
319
|
-
except ValueError as exc:
|
|
320
|
-
console.print(f"[red]{exc}[/red]")
|
|
321
|
-
return False
|
|
322
|
-
_record_cursor_config(project_root)
|
|
323
|
-
console.print(f"[green]Cursor MCP config written:[/green] {written}")
|
|
324
|
-
return True
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def _configure_opencode(project_root: Path, apply: bool) -> bool:
|
|
328
|
-
path = _opencode_config_path(project_root)
|
|
329
|
-
config = {
|
|
330
|
-
"$schema": "https://opencode.ai/config.json",
|
|
331
|
-
"mcp": {"devcouncil": _opencode_mcp_entry(project_root)},
|
|
332
|
-
}
|
|
333
|
-
if not apply:
|
|
334
|
-
console.print("[bold]OpenCode[/bold]")
|
|
335
|
-
console.print(f"Project config file: [dim]{path}[/dim]")
|
|
336
|
-
console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
|
|
337
|
-
console.print(
|
|
338
|
-
"Direct executor command: "
|
|
339
|
-
"[dim]opencode run --file .devcouncil/TASK-001-opencode-task.md "
|
|
340
|
-
'"Execute the DevCouncil task described in the attached prompt file."[/dim]'
|
|
341
|
-
)
|
|
342
|
-
return True
|
|
343
|
-
|
|
344
|
-
if not shutil.which("opencode"):
|
|
345
|
-
console.print("[yellow]OpenCode CLI not found on PATH. Install it before using `dev run --executor opencode`.[/yellow]")
|
|
346
|
-
try:
|
|
347
|
-
written = _write_opencode_config(project_root)
|
|
348
|
-
except ValueError as exc:
|
|
349
|
-
console.print(f"[red]{exc}[/red]")
|
|
350
|
-
return False
|
|
351
|
-
_record_opencode_config(project_root)
|
|
352
|
-
console.print(f"[green]OpenCode MCP config written:[/green] {written}")
|
|
353
|
-
return True
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
def _configure_antigravity(project_root: Path, apply: bool) -> bool:
|
|
357
|
-
path = _antigravity_mcp_path(project_root)
|
|
358
|
-
config = _antigravity_mcp_config(project_root)
|
|
359
|
-
if not apply:
|
|
360
|
-
console.print("[bold]Google Antigravity CLI[/bold]")
|
|
361
|
-
console.print(f"Project MCP config file: [dim]{path}[/dim]")
|
|
362
|
-
console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
|
|
363
|
-
console.print(
|
|
364
|
-
"Direct executor command: "
|
|
365
|
-
"[dim]agy --print --print-timeout 30m "
|
|
366
|
-
'"Read and execute the DevCouncil task prompt at .devcouncil/TASK-001-antigravity-task.md."[/dim]'
|
|
367
|
-
)
|
|
368
|
-
return True
|
|
369
|
-
|
|
370
|
-
if not shutil.which("agy"):
|
|
371
|
-
console.print("[yellow]Antigravity CLI (`agy`) not found on PATH. Install it before using `dev run --executor antigravity`.[/yellow]")
|
|
372
|
-
try:
|
|
373
|
-
written = _write_antigravity_mcp_config(project_root)
|
|
374
|
-
except ValueError as exc:
|
|
375
|
-
console.print(f"[red]{exc}[/red]")
|
|
376
|
-
return False
|
|
377
|
-
_record_antigravity_config(project_root)
|
|
378
|
-
console.print(f"[green]Antigravity MCP config written:[/green] {written}")
|
|
379
|
-
return True
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def _configure_warp(project_root: Path, apply: bool) -> bool:
|
|
383
|
-
path = _warp_mcp_path(project_root)
|
|
384
|
-
config = _warp_mcp_config(project_root)
|
|
385
|
-
if not apply:
|
|
386
|
-
console.print("[bold]Warp / Oz[/bold]")
|
|
387
|
-
console.print(f"MCP config file: [dim]{path}[/dim]")
|
|
388
|
-
console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
|
|
389
|
-
console.print(f"Direct executor command: [dim]oz agent run --cwd {project_root} --mcp {path} --prompt <task prompt>[/dim]")
|
|
390
|
-
return True
|
|
391
|
-
|
|
392
|
-
written = _write_warp_mcp_config(project_root)
|
|
393
|
-
_record_warp_config(project_root)
|
|
394
|
-
console.print(f"[green]Warp MCP config written:[/green] {written}")
|
|
395
|
-
if not shutil.which("oz"):
|
|
396
|
-
console.print("[yellow]oz CLI not found on PATH. Install Warp/Oz before using `dev run --executor warp`.[/yellow]")
|
|
397
|
-
return True
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
def _format_command(command: list[str]) -> str:
|
|
401
|
-
if sys.platform == "win32":
|
|
402
|
-
return " ".join(_quote_powershell_arg(arg) for arg in command)
|
|
403
|
-
return shlex.join(command)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
def _quote_powershell_arg(arg: str) -> str:
|
|
407
|
-
if arg == "":
|
|
408
|
-
return "''"
|
|
409
|
-
special_chars = set(" \t\r\n'\"{}[](),;|&<>")
|
|
410
|
-
if not any(char in special_chars for char in arg):
|
|
411
|
-
return arg
|
|
412
|
-
return "'" + arg.replace("'", "''") + "'"
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
def _opencode_plugin_source() -> Path:
|
|
416
|
-
return Path(__file__).resolve().parents[2] / "integrations" / OPENCODE_HOOK_PLUGIN_NAME
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def _opencode_plugin_path(project_root: Path) -> Path:
|
|
420
|
-
return project_root / ".devcouncil" / "integrations" / OPENCODE_HOOK_PLUGIN_NAME
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
def _hook_command(project_root: Path, client: str, event: str) -> str:
|
|
424
|
-
return _format_command([
|
|
425
|
-
"devcouncil",
|
|
426
|
-
"hook",
|
|
427
|
-
event,
|
|
428
|
-
"--client",
|
|
429
|
-
client,
|
|
430
|
-
"--project-root",
|
|
431
|
-
str(project_root),
|
|
432
|
-
])
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
def _probe_mcp_tools(root: Path, *, timeout_seconds: float = 30.0) -> list[str]:
|
|
436
|
-
from mcp import ClientSession, StdioServerParameters
|
|
437
|
-
from mcp.client.stdio import stdio_client
|
|
438
|
-
import asyncio
|
|
439
|
-
import os
|
|
440
|
-
|
|
441
|
-
async def _list_tools() -> list[str]:
|
|
442
|
-
env = os.environ.copy()
|
|
443
|
-
env["DEVCOUNCIL_PROJECT_ROOT"] = str(root)
|
|
444
|
-
params = StdioServerParameters(
|
|
445
|
-
command=sys.executable,
|
|
446
|
-
args=["-m", "devcouncil", "mcp-server"],
|
|
447
|
-
cwd=str(root),
|
|
448
|
-
env=env,
|
|
449
|
-
)
|
|
450
|
-
async with stdio_client(params) as (read, write):
|
|
451
|
-
async with ClientSession(read, write) as session:
|
|
452
|
-
await session.initialize()
|
|
453
|
-
tools = await session.list_tools()
|
|
454
|
-
return [tool.name for tool in tools.tools]
|
|
455
|
-
|
|
456
|
-
async def _list_tools_with_deadline() -> list[str]:
|
|
457
|
-
# A wedged server process would otherwise block `dev integrate check`
|
|
458
|
-
# indefinitely; the caller treats TimeoutError as a failed probe.
|
|
459
|
-
return await asyncio.wait_for(_list_tools(), timeout=timeout_seconds)
|
|
460
|
-
|
|
461
|
-
return asyncio.run(_list_tools_with_deadline())
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
def _run(command: list[str]) -> int:
|
|
465
|
-
executable = shutil.which(command[0])
|
|
466
|
-
if not executable:
|
|
467
|
-
return 127
|
|
468
|
-
resolved = [executable, *command[1:]]
|
|
469
|
-
use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
|
|
470
|
-
invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
|
|
471
|
-
try:
|
|
472
|
-
result = subprocess.run(invocation, text=True, shell=use_shell)
|
|
473
|
-
except (FileNotFoundError, OSError):
|
|
474
|
-
return 127
|
|
475
|
-
return result.returncode
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
def _run_capture(command: list[str], timeout: int = 10) -> tuple[int, str]:
|
|
479
|
-
executable = shutil.which(command[0])
|
|
480
|
-
if not executable:
|
|
481
|
-
return 127, f"{command[0]} not found on PATH"
|
|
482
|
-
|
|
483
|
-
resolved = [executable, *command[1:]]
|
|
484
|
-
use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
|
|
485
|
-
invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
|
|
486
|
-
try:
|
|
487
|
-
result = subprocess.run(
|
|
488
|
-
invocation,
|
|
489
|
-
capture_output=True,
|
|
490
|
-
text=True,
|
|
491
|
-
encoding="utf-8",
|
|
492
|
-
errors="replace",
|
|
493
|
-
shell=use_shell,
|
|
494
|
-
timeout=timeout,
|
|
495
|
-
env=clean_subprocess_env(),
|
|
496
|
-
)
|
|
497
|
-
except subprocess.TimeoutExpired:
|
|
498
|
-
return 124, "timed out"
|
|
499
|
-
except (FileNotFoundError, OSError) as exc:
|
|
500
|
-
return 127, f"{command[0]} could not be executed: {exc}"
|
|
501
|
-
return result.returncode, (result.stdout + result.stderr).strip()
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
def _config_path(project_root: Path) -> Path:
|
|
505
|
-
return project_root / ".devcouncil" / "config.yaml"
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
def _load_raw_config(project_root: Path) -> dict:
|
|
509
|
-
path = _config_path(project_root)
|
|
510
|
-
if not path.exists():
|
|
511
|
-
return {}
|
|
512
|
-
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
def _save_raw_config(project_root: Path, config: dict) -> None:
|
|
516
|
-
path = _config_path(project_root)
|
|
517
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
518
|
-
path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8")
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
def _load_json(path: Path) -> dict:
|
|
522
|
-
if not path.exists():
|
|
523
|
-
return {}
|
|
524
|
-
try:
|
|
525
|
-
return json.loads(path.read_text(encoding="utf-8")) or {}
|
|
526
|
-
except json.JSONDecodeError:
|
|
527
|
-
return {}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
def _save_json(path: Path, data: dict) -> None:
|
|
531
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
532
|
-
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def _upsert_hook(settings: dict, event: str, matcher: str, command: str, name: str) -> None:
|
|
536
|
-
hooks = settings.setdefault("hooks", {})
|
|
537
|
-
groups = hooks.setdefault(event, [])
|
|
538
|
-
for group in groups:
|
|
539
|
-
if group.get("matcher") == matcher:
|
|
540
|
-
group_hooks = group.setdefault("hooks", [])
|
|
541
|
-
if not any(hook.get("command") == command for hook in group_hooks):
|
|
542
|
-
group_hooks.append({
|
|
543
|
-
"type": "command",
|
|
544
|
-
"name": name,
|
|
545
|
-
"command": command,
|
|
546
|
-
"timeout": 10000,
|
|
547
|
-
})
|
|
548
|
-
return
|
|
549
|
-
groups.append({
|
|
550
|
-
"matcher": matcher,
|
|
551
|
-
"hooks": [{
|
|
552
|
-
"type": "command",
|
|
553
|
-
"name": name,
|
|
554
|
-
"command": command,
|
|
555
|
-
"timeout": 10000,
|
|
556
|
-
}],
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
def _ensure_codex_hooks_enabled(project_root: Path) -> Path:
|
|
561
|
-
config_path = project_root / ".codex" / "config.toml"
|
|
562
|
-
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
563
|
-
existing = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
|
|
564
|
-
if "codex_hooks" not in existing:
|
|
565
|
-
if "[features]" in existing:
|
|
566
|
-
lines = existing.splitlines()
|
|
567
|
-
updated: list[str] = []
|
|
568
|
-
in_features = False
|
|
569
|
-
inserted = False
|
|
570
|
-
for line in lines:
|
|
571
|
-
stripped = line.strip()
|
|
572
|
-
if stripped == "[features]":
|
|
573
|
-
in_features = True
|
|
574
|
-
updated.append(line)
|
|
575
|
-
continue
|
|
576
|
-
if in_features and stripped.startswith("[") and stripped.endswith("]"):
|
|
577
|
-
updated.append("codex_hooks = true")
|
|
578
|
-
inserted = True
|
|
579
|
-
in_features = False
|
|
580
|
-
updated.append(line)
|
|
581
|
-
if in_features and not inserted:
|
|
582
|
-
updated.append("codex_hooks = true")
|
|
583
|
-
config_path.write_text("\n".join(updated) + "\n", encoding="utf-8")
|
|
584
|
-
else:
|
|
585
|
-
separator = "\n" if existing and not existing.endswith("\n") else ""
|
|
586
|
-
config_path.write_text(f"{existing}{separator}\n[features]\ncodex_hooks = true\n", encoding="utf-8")
|
|
587
|
-
return config_path
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
def _install_codex_hooks(project_root: Path) -> list[Path]:
|
|
591
|
-
path = project_root / ".codex" / "hooks.json"
|
|
592
|
-
settings = _load_json(path)
|
|
593
|
-
matcher = "Bash|shell_command|exec_command|local_shell|Write|Edit|MultiEdit|write_file|edit_file|apply_patch"
|
|
594
|
-
_upsert_hook(
|
|
595
|
-
settings,
|
|
596
|
-
"PreToolUse",
|
|
597
|
-
matcher,
|
|
598
|
-
_hook_command(project_root, "codex", "pre-tool-use"),
|
|
599
|
-
"devcouncil-pre-tool-use",
|
|
600
|
-
)
|
|
601
|
-
_upsert_hook(
|
|
602
|
-
settings,
|
|
603
|
-
"PostToolUse",
|
|
604
|
-
matcher,
|
|
605
|
-
_hook_command(project_root, "codex", "post-tool-use"),
|
|
606
|
-
"devcouncil-post-tool-use",
|
|
607
|
-
)
|
|
608
|
-
_save_json(path, settings)
|
|
609
|
-
return [path, _ensure_codex_hooks_enabled(project_root)]
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
def _install_gemini_hooks(project_root: Path) -> list[Path]:
|
|
613
|
-
path = project_root / ".gemini" / "settings.json"
|
|
614
|
-
settings = _load_json(path)
|
|
615
|
-
matcher = "run_shell_command|shell_command|write_file|edit_file|replace|apply_patch"
|
|
616
|
-
_upsert_hook(
|
|
617
|
-
settings,
|
|
618
|
-
"BeforeTool",
|
|
619
|
-
matcher,
|
|
620
|
-
_hook_command(project_root, "gemini", "pre-tool-use"),
|
|
621
|
-
"devcouncil-pre-tool-use",
|
|
622
|
-
)
|
|
623
|
-
_upsert_hook(
|
|
624
|
-
settings,
|
|
625
|
-
"AfterTool",
|
|
626
|
-
matcher,
|
|
627
|
-
_hook_command(project_root, "gemini", "post-tool-use"),
|
|
628
|
-
"devcouncil-post-tool-use",
|
|
629
|
-
)
|
|
630
|
-
_save_json(path, settings)
|
|
631
|
-
return [path]
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
def _upsert_cursor_hook(settings: dict, event: str, matcher: str, command: str) -> None:
|
|
635
|
-
hooks = settings.setdefault("hooks", {})
|
|
636
|
-
entries = hooks.setdefault(event, [])
|
|
637
|
-
for entry in entries:
|
|
638
|
-
if entry.get("command") == command:
|
|
639
|
-
return
|
|
640
|
-
payload: dict = {"command": command}
|
|
641
|
-
if matcher:
|
|
642
|
-
payload["matcher"] = matcher
|
|
643
|
-
entries.append(payload)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
def _install_cursor_hooks(project_root: Path) -> list[Path]:
|
|
647
|
-
path = project_root / ".cursor" / "hooks.json"
|
|
648
|
-
settings = _load_json(path)
|
|
649
|
-
settings.setdefault("version", 1)
|
|
650
|
-
matcher = "Shell|Write|Edit|MultiEdit|Read|Task"
|
|
651
|
-
_upsert_cursor_hook(
|
|
652
|
-
settings,
|
|
653
|
-
"preToolUse",
|
|
654
|
-
matcher,
|
|
655
|
-
_hook_command(project_root, "cursor", "pre-tool-use"),
|
|
656
|
-
)
|
|
657
|
-
_upsert_cursor_hook(
|
|
658
|
-
settings,
|
|
659
|
-
"postToolUse",
|
|
660
|
-
matcher,
|
|
661
|
-
_hook_command(project_root, "cursor", "post-tool-use"),
|
|
662
|
-
)
|
|
663
|
-
_save_json(path, settings)
|
|
664
|
-
|
|
665
|
-
def mutate(config: dict) -> None:
|
|
666
|
-
cursor = config.setdefault("integrations", {}).setdefault("cursor", {})
|
|
667
|
-
cursor.update({
|
|
668
|
-
"hooks_path": str(path.relative_to(project_root)),
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
_mutate_raw_config(project_root, mutate)
|
|
672
|
-
return [path]
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
def _install_opencode_hooks(project_root: Path) -> list[Path]:
|
|
676
|
-
source = _opencode_plugin_source()
|
|
677
|
-
if not source.exists():
|
|
678
|
-
raise FileNotFoundError(f"Missing bundled OpenCode hook plugin: {source}")
|
|
679
|
-
destination = _opencode_plugin_path(project_root)
|
|
680
|
-
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
681
|
-
destination.write_text(source.read_text(encoding="utf-8"), encoding="utf-8")
|
|
682
|
-
|
|
683
|
-
path = _opencode_config_path(project_root)
|
|
684
|
-
data = _load_json_strict(path, "OpenCode") if path.exists() else {"$schema": "https://opencode.ai/config.json"}
|
|
685
|
-
data.setdefault("$schema", "https://opencode.ai/config.json")
|
|
686
|
-
plugins_raw = data.setdefault("plugin", [])
|
|
687
|
-
if not isinstance(plugins_raw, list):
|
|
688
|
-
plugins_raw = []
|
|
689
|
-
data["plugin"] = plugins_raw
|
|
690
|
-
plugins: list[str] = [str(item) for item in plugins_raw]
|
|
691
|
-
data["plugin"] = plugins
|
|
692
|
-
plugin_ref = f"./.devcouncil/integrations/{OPENCODE_HOOK_PLUGIN_NAME}"
|
|
693
|
-
if plugin_ref not in plugins:
|
|
694
|
-
plugins.append(plugin_ref)
|
|
695
|
-
_save_json(path, data)
|
|
696
|
-
_record_opencode_config(project_root)
|
|
697
|
-
return [destination, path]
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
def _install_claude_hooks(project_root: Path, *, write_gate: bool = False) -> list[Path]:
|
|
701
|
-
"""Install DevCouncil's Claude Code hooks into .claude/settings.local.json.
|
|
702
|
-
|
|
703
|
-
By default this installs only the *assistive* lifecycle hooks (status injection on
|
|
704
|
-
SessionStart/UserPromptSubmit, the live-review Stop signal, and the SessionEnd/
|
|
705
|
-
PreCompact/SubagentStop/Notification trace hooks). These never block a tool call.
|
|
706
|
-
|
|
707
|
-
The blocking pre-action **write-gate** (PreToolUse/PostToolUse, which denies any
|
|
708
|
-
Bash/Write/Edit not authorized by an active task lease) is installed ONLY when
|
|
709
|
-
``write_gate`` is True. It is meant for autonomous executor runs, not interactive
|
|
710
|
-
human sessions — in an interactive session there is no task lease, so the gate would
|
|
711
|
-
fail-closed and deny every command. (``dev run --executor claude`` does its own
|
|
712
|
-
post-hoc scope enforcement and does not depend on this hook, so leaving it off by
|
|
713
|
-
default loses no containment.)"""
|
|
714
|
-
path = project_root / ".claude" / "settings.local.json"
|
|
715
|
-
settings = _load_json(path)
|
|
716
|
-
matcher = "Bash|Write|Edit|MultiEdit"
|
|
717
|
-
if write_gate:
|
|
718
|
-
_upsert_hook(
|
|
719
|
-
settings,
|
|
720
|
-
"PreToolUse",
|
|
721
|
-
matcher,
|
|
722
|
-
_hook_command(project_root, "claude", "pre-tool-use"),
|
|
723
|
-
"devcouncil-pre-tool-use",
|
|
724
|
-
)
|
|
725
|
-
_upsert_hook(
|
|
726
|
-
settings,
|
|
727
|
-
"PostToolUse",
|
|
728
|
-
matcher,
|
|
729
|
-
_hook_command(project_root, "claude", "post-tool-use"),
|
|
730
|
-
"devcouncil-post-tool-use",
|
|
731
|
-
)
|
|
732
|
-
_upsert_hook(
|
|
733
|
-
settings,
|
|
734
|
-
"Stop",
|
|
735
|
-
"",
|
|
736
|
-
_hook_command(project_root, "claude", "agent-response"),
|
|
737
|
-
"devcouncil-agent-response-ready",
|
|
738
|
-
)
|
|
739
|
-
# Lifecycle events: status-on-start/prompt, teardown, compaction, subagent finish,
|
|
740
|
-
# and notifications. These complete DevCouncil's coverage of the documented Claude
|
|
741
|
-
# Code hook surface beyond the pre/post/stop gate.
|
|
742
|
-
_upsert_hook(
|
|
743
|
-
settings,
|
|
744
|
-
"SessionStart",
|
|
745
|
-
"startup|resume",
|
|
746
|
-
_hook_command(project_root, "claude", "session-start"),
|
|
747
|
-
"devcouncil-session-start",
|
|
748
|
-
)
|
|
749
|
-
_upsert_hook(
|
|
750
|
-
settings,
|
|
751
|
-
"UserPromptSubmit",
|
|
752
|
-
"",
|
|
753
|
-
_hook_command(project_root, "claude", "user-prompt-submit"),
|
|
754
|
-
"devcouncil-user-prompt-submit",
|
|
755
|
-
)
|
|
756
|
-
_upsert_hook(
|
|
757
|
-
settings,
|
|
758
|
-
"SessionEnd",
|
|
759
|
-
"",
|
|
760
|
-
_hook_command(project_root, "claude", "session-end"),
|
|
761
|
-
"devcouncil-session-end",
|
|
762
|
-
)
|
|
763
|
-
_upsert_hook(
|
|
764
|
-
settings,
|
|
765
|
-
"PreCompact",
|
|
766
|
-
"",
|
|
767
|
-
_hook_command(project_root, "claude", "pre-compact"),
|
|
768
|
-
"devcouncil-pre-compact",
|
|
769
|
-
)
|
|
770
|
-
_upsert_hook(
|
|
771
|
-
settings,
|
|
772
|
-
"SubagentStop",
|
|
773
|
-
"",
|
|
774
|
-
_hook_command(project_root, "claude", "subagent-stop"),
|
|
775
|
-
"devcouncil-subagent-stop",
|
|
776
|
-
)
|
|
777
|
-
_upsert_hook(
|
|
778
|
-
settings,
|
|
779
|
-
"Notification",
|
|
780
|
-
"",
|
|
781
|
-
_hook_command(project_root, "claude", "notification"),
|
|
782
|
-
"devcouncil-notification",
|
|
783
|
-
)
|
|
784
|
-
_save_json(path, settings)
|
|
785
|
-
return [path]
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
def _devcouncil_version() -> str:
|
|
789
|
-
"""Package version for plugin manifests, or a stable placeholder when uninstalled."""
|
|
790
|
-
import importlib.metadata
|
|
791
|
-
|
|
792
|
-
try:
|
|
793
|
-
return importlib.metadata.version("devcouncil")
|
|
794
|
-
except importlib.metadata.PackageNotFoundError:
|
|
795
|
-
return "0.0.0"
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
# Read-only DevCouncil commands the generated slash commands / hooks shell out to. Adding
|
|
799
|
-
# them to the Claude permissions allow-list keeps the integration from prompting on every
|
|
800
|
-
# `dev status`/`dev report` the slash commands run.
|
|
801
|
-
_CLAUDE_PERMISSION_ALLOW = [
|
|
802
|
-
"Bash(dev status:*)",
|
|
803
|
-
"Bash(dev report:*)",
|
|
804
|
-
"Bash(dev tasks:*)",
|
|
805
|
-
"Bash(dev verify:*)",
|
|
806
|
-
"Bash(dev repair:*)",
|
|
807
|
-
"Bash(dev plan:*)",
|
|
808
|
-
"Bash(dev watch:*)",
|
|
809
|
-
"Bash(devcouncil mcp-server)",
|
|
810
|
-
]
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
def _install_claude_settings(project_root: Path) -> tuple[Path, bool]:
|
|
814
|
-
"""Write the statusLine, MCP enablement, and permission allow-list into Claude settings.
|
|
815
|
-
|
|
816
|
-
Merges into .claude/settings.local.json without clobbering existing user entries.
|
|
817
|
-
Returns (path, changed); only rewrites the file when the merge changes something so
|
|
818
|
-
re-running integration is a true no-op."""
|
|
819
|
-
path = project_root / ".claude" / "settings.local.json"
|
|
820
|
-
settings = _load_json(path)
|
|
821
|
-
before = json.dumps(settings, sort_keys=True)
|
|
822
|
-
|
|
823
|
-
settings["statusLine"] = {
|
|
824
|
-
"type": "command",
|
|
825
|
-
"command": "devcouncil hook claude-statusline",
|
|
826
|
-
}
|
|
827
|
-
# Auto-enable the project-scoped DevCouncil MCP server so a teammate cloning the repo
|
|
828
|
-
# doesn't have to approve it interactively.
|
|
829
|
-
enabled = settings.setdefault("enabledMcpjsonServers", [])
|
|
830
|
-
if isinstance(enabled, list) and "devcouncil" not in enabled:
|
|
831
|
-
enabled.append("devcouncil")
|
|
832
|
-
|
|
833
|
-
permissions = settings.setdefault("permissions", {})
|
|
834
|
-
if isinstance(permissions, dict):
|
|
835
|
-
allow = permissions.setdefault("allow", [])
|
|
836
|
-
if isinstance(allow, list):
|
|
837
|
-
for rule in _CLAUDE_PERMISSION_ALLOW:
|
|
838
|
-
if rule not in allow:
|
|
839
|
-
allow.append(rule)
|
|
840
|
-
|
|
841
|
-
changed = json.dumps(settings, sort_keys=True) != before
|
|
842
|
-
if changed:
|
|
843
|
-
_save_json(path, settings)
|
|
844
|
-
return path, changed
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
def _selected_skill_assets(project_root: Path):
|
|
848
|
-
"""Scaffold the applicable skills and return them as GeneratedAsset-like records.
|
|
849
|
-
|
|
850
|
-
Returns (written_paths, skill_assets) where skill_assets carry (path, content) for the
|
|
851
|
-
plugin bundler so the plugin ships the same skill bodies that land in .claude/skills/."""
|
|
852
|
-
from devcouncil.integrations.claude_assets import GeneratedAsset
|
|
853
|
-
from devcouncil.skills.registry import scaffold_skills, select_skills
|
|
854
|
-
|
|
855
|
-
skills = select_skills("", project_root)
|
|
856
|
-
written = scaffold_skills(project_root, skills)
|
|
857
|
-
assets: list[GeneratedAsset] = []
|
|
858
|
-
skills_root = project_root / ".claude" / "skills"
|
|
859
|
-
for skill in skills:
|
|
860
|
-
target = skills_root / skill.name / "SKILL.md"
|
|
861
|
-
if target.exists():
|
|
862
|
-
assets.append(GeneratedAsset(target, target.read_text(encoding="utf-8")))
|
|
863
|
-
return written, assets
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
def _install_claude_assets(project_root: Path) -> list[Path]:
|
|
867
|
-
"""Generate the static Claude Code asset surface (commands, agents, output style,
|
|
868
|
-
statusline, permissions) and scaffold the applicable skills. Idempotent."""
|
|
869
|
-
from devcouncil.integrations import claude_assets
|
|
870
|
-
|
|
871
|
-
written: list[Path] = []
|
|
872
|
-
assets: list[claude_assets.GeneratedAsset] = []
|
|
873
|
-
assets += claude_assets.build_slash_commands(project_root)
|
|
874
|
-
assets += claude_assets.build_subagents(project_root)
|
|
875
|
-
assets += claude_assets.build_output_style(project_root)
|
|
876
|
-
for asset in assets:
|
|
877
|
-
if asset.write_if_changed():
|
|
878
|
-
written.append(asset.path)
|
|
879
|
-
|
|
880
|
-
skills_written, _ = _selected_skill_assets(project_root)
|
|
881
|
-
written.extend(skills_written)
|
|
882
|
-
settings_path, settings_changed = _install_claude_settings(project_root)
|
|
883
|
-
if settings_changed:
|
|
884
|
-
written.append(settings_path)
|
|
885
|
-
return written
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
def _install_claude_plugin(project_root: Path, *, write_gate: bool = False) -> list[Path]:
|
|
889
|
-
"""Build the self-contained Claude Code plugin + single-repo marketplace bundle.
|
|
890
|
-
|
|
891
|
-
Bundles the commands, agents, applicable skills, hooks, and MCP config so the entire
|
|
892
|
-
DevCouncil integration installs with one `/plugin install`. Assist-mode hooks by
|
|
893
|
-
default; pass write_gate=True to bundle the blocking containment gate."""
|
|
894
|
-
from devcouncil.integrations import claude_assets
|
|
895
|
-
|
|
896
|
-
_, skill_assets = _selected_skill_assets(project_root)
|
|
897
|
-
bundle = claude_assets.build_plugin_bundle(
|
|
898
|
-
project_root, version=_devcouncil_version(), skill_assets=skill_assets, write_gate=write_gate
|
|
899
|
-
)
|
|
900
|
-
return [asset.path for asset in bundle if asset.write_if_changed()]
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
def _uninstall_claude(project_root: Path) -> list[str]:
|
|
904
|
-
"""Remove everything DevCouncil installed into a Claude Code project. Idempotent.
|
|
905
|
-
|
|
906
|
-
Strips DevCouncil's hooks (every event), the DevCouncil statusLine, the MCP enablement
|
|
907
|
-
and permission rules from .claude/settings.local.json (leaving any user-authored
|
|
908
|
-
entries untouched), deletes the generated commands/subagents/output-style files, and
|
|
909
|
-
best-effort de-registers the MCP server via `claude mcp remove`. Returns a list of the
|
|
910
|
-
changes made. The recoverable, in-band counterpart to a fail-closed write-gate."""
|
|
911
|
-
removed: list[str] = []
|
|
912
|
-
path = project_root / ".claude" / "settings.local.json"
|
|
913
|
-
settings = _load_json(path)
|
|
914
|
-
before = json.dumps(settings, sort_keys=True)
|
|
915
|
-
|
|
916
|
-
# Hooks: drop any entry whose command invokes `devcouncil hook`, then prune empties.
|
|
917
|
-
hooks = settings.get("hooks")
|
|
918
|
-
if isinstance(hooks, dict):
|
|
919
|
-
for event in list(hooks):
|
|
920
|
-
groups = hooks.get(event)
|
|
921
|
-
if not isinstance(groups, list):
|
|
922
|
-
continue
|
|
923
|
-
kept_groups = []
|
|
924
|
-
for group in groups:
|
|
925
|
-
inner = group.get("hooks", []) if isinstance(group, dict) else []
|
|
926
|
-
inner_kept = [
|
|
927
|
-
h for h in inner
|
|
928
|
-
if "devcouncil hook" not in str(h.get("command", ""))
|
|
929
|
-
]
|
|
930
|
-
if inner_kept:
|
|
931
|
-
group["hooks"] = inner_kept
|
|
932
|
-
kept_groups.append(group)
|
|
933
|
-
if kept_groups:
|
|
934
|
-
hooks[event] = kept_groups
|
|
935
|
-
else:
|
|
936
|
-
hooks.pop(event)
|
|
937
|
-
if not hooks:
|
|
938
|
-
settings.pop("hooks")
|
|
939
|
-
removed.append(f"hooks in {path.name}")
|
|
940
|
-
|
|
941
|
-
# statusLine: only remove ours.
|
|
942
|
-
status = settings.get("statusLine")
|
|
943
|
-
if isinstance(status, dict) and "devcouncil" in str(status.get("command", "")):
|
|
944
|
-
settings.pop("statusLine")
|
|
945
|
-
removed.append("statusLine")
|
|
946
|
-
|
|
947
|
-
enabled = settings.get("enabledMcpjsonServers")
|
|
948
|
-
if isinstance(enabled, list) and "devcouncil" in enabled:
|
|
949
|
-
enabled.remove("devcouncil")
|
|
950
|
-
if not enabled:
|
|
951
|
-
settings.pop("enabledMcpjsonServers")
|
|
952
|
-
removed.append("enabledMcpjsonServers entry")
|
|
953
|
-
|
|
954
|
-
permissions = settings.get("permissions")
|
|
955
|
-
if isinstance(permissions, dict) and isinstance(permissions.get("allow"), list):
|
|
956
|
-
kept = [r for r in permissions["allow"] if r not in _CLAUDE_PERMISSION_ALLOW]
|
|
957
|
-
if len(kept) != len(permissions["allow"]):
|
|
958
|
-
permissions["allow"] = kept
|
|
959
|
-
removed.append("permission allow-rules")
|
|
960
|
-
if not permissions.get("allow"):
|
|
961
|
-
permissions.pop("allow", None)
|
|
962
|
-
if not permissions:
|
|
963
|
-
settings.pop("permissions")
|
|
964
|
-
|
|
965
|
-
if json.dumps(settings, sort_keys=True) != before:
|
|
966
|
-
if settings:
|
|
967
|
-
_save_json(path, settings)
|
|
968
|
-
elif path.exists():
|
|
969
|
-
path.unlink()
|
|
970
|
-
removed.append(f"deleted empty {path.name}")
|
|
971
|
-
|
|
972
|
-
# Generated asset files.
|
|
973
|
-
targets = [
|
|
974
|
-
project_root / ".claude" / "commands" / "devcouncil",
|
|
975
|
-
project_root / ".claude" / "output-styles" / "devcouncil.md",
|
|
976
|
-
]
|
|
977
|
-
targets += [
|
|
978
|
-
project_root / ".claude" / "agents" / f"{name}.md"
|
|
979
|
-
for name in ("devcouncil-implementer", "devcouncil-verifier", "devcouncil-reviewer")
|
|
980
|
-
]
|
|
981
|
-
for target in targets:
|
|
982
|
-
if target.is_dir():
|
|
983
|
-
shutil.rmtree(target)
|
|
984
|
-
removed.append(str(target.relative_to(project_root)))
|
|
985
|
-
elif target.exists():
|
|
986
|
-
target.unlink()
|
|
987
|
-
removed.append(str(target.relative_to(project_root)))
|
|
988
|
-
|
|
989
|
-
# De-register the MCP server (best-effort; only if the claude CLI is present).
|
|
990
|
-
if shutil.which("claude"):
|
|
991
|
-
code = _run(["claude", "mcp", "remove", "devcouncil"])
|
|
992
|
-
if code == 0:
|
|
993
|
-
removed.append("claude mcp server registration")
|
|
994
|
-
|
|
995
|
-
return removed
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
def _preview_hook_paths(project_root: Path, tool: str) -> list[tuple[str, Path]]:
|
|
999
|
-
paths = {
|
|
1000
|
-
"codex": [project_root / ".codex" / "hooks.json", project_root / ".codex" / "config.toml"],
|
|
1001
|
-
"gemini": [project_root / ".gemini" / "settings.json"],
|
|
1002
|
-
"claude": [project_root / ".claude" / "settings.local.json"],
|
|
1003
|
-
"cursor": [project_root / ".cursor" / "hooks.json"],
|
|
1004
|
-
"opencode": [_opencode_plugin_path(project_root), _opencode_config_path(project_root)],
|
|
1005
|
-
}
|
|
1006
|
-
selected: tuple[str, ...]
|
|
1007
|
-
if tool == "all":
|
|
1008
|
-
selected = (*SUPPORTED_HOOK_TOOLS, "opencode")
|
|
1009
|
-
elif tool == "opencode":
|
|
1010
|
-
selected = ("opencode",)
|
|
1011
|
-
else:
|
|
1012
|
-
selected = (tool,)
|
|
1013
|
-
return [(client, path) for client in selected for path in paths.get(client, [])]
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
def _configure_native_hooks(
|
|
1017
|
-
project_root: Path, tool: str = "all", apply: bool = False, *, claude_write_gate: bool = False
|
|
1018
|
-
) -> None:
|
|
1019
|
-
allowed = {"all", *SUPPORTED_HOOK_TOOLS, "opencode"}
|
|
1020
|
-
if tool not in allowed:
|
|
1021
|
-
console.print("[red]--tool must be one of: all, codex, gemini, claude, cursor, opencode.[/red]")
|
|
1022
|
-
raise typer.Exit(code=2)
|
|
1023
|
-
|
|
1024
|
-
if not apply:
|
|
1025
|
-
console.print("[bold]Native hook config preview[/bold]")
|
|
1026
|
-
for client, path in _preview_hook_paths(project_root, tool):
|
|
1027
|
-
console.print(f"{client}: {path}", soft_wrap=True)
|
|
1028
|
-
console.print("[yellow]Preview only. Rerun with --apply to write hook config files.[/yellow]")
|
|
1029
|
-
return
|
|
1030
|
-
|
|
1031
|
-
selected: tuple[str, ...]
|
|
1032
|
-
if tool == "all":
|
|
1033
|
-
selected = (*SUPPORTED_HOOK_TOOLS, "opencode")
|
|
1034
|
-
elif tool == "opencode":
|
|
1035
|
-
selected = ("opencode",)
|
|
1036
|
-
else:
|
|
1037
|
-
selected = (tool,)
|
|
1038
|
-
installers = {
|
|
1039
|
-
"codex": _install_codex_hooks,
|
|
1040
|
-
"gemini": _install_gemini_hooks,
|
|
1041
|
-
# Claude's blocking write-gate is opt-in (assist-mode default); the other clients
|
|
1042
|
-
# install their native pre/post hooks unconditionally as before.
|
|
1043
|
-
"claude": lambda root: _install_claude_hooks(root, write_gate=claude_write_gate),
|
|
1044
|
-
"cursor": _install_cursor_hooks,
|
|
1045
|
-
"opencode": _install_opencode_hooks,
|
|
1046
|
-
}
|
|
1047
|
-
# Batch the per-installer config.yaml record updates (cursor/opencode)
|
|
1048
|
-
# into one load/save instead of re-parsing YAML per tool.
|
|
1049
|
-
with _batched_raw_config(project_root):
|
|
1050
|
-
for client in selected:
|
|
1051
|
-
try:
|
|
1052
|
-
written = installers[client](project_root)
|
|
1053
|
-
except (ValueError, FileNotFoundError) as exc:
|
|
1054
|
-
console.print(f"[red]{client} hook setup failed: {exc}[/red]")
|
|
1055
|
-
raise typer.Exit(code=1) from exc
|
|
1056
|
-
console.print(f"[green]{client} native hooks configured:[/green] {', '.join(str(path) for path in written)}")
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
def _print_command(tool: str, command: list[str], apply: bool):
|
|
1060
|
-
if apply:
|
|
1061
|
-
console.print(f"[cyan]Configuring {tool} MCP integration...[/cyan]")
|
|
1062
|
-
else:
|
|
1063
|
-
console.print(f"[bold]{tool}[/bold]")
|
|
1064
|
-
console.print(_format_command(command), soft_wrap=True)
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
def _configure(tool: str, command: list[str], apply: bool) -> bool:
|
|
1068
|
-
executable = command[0]
|
|
1069
|
-
if not shutil.which(executable):
|
|
1070
|
-
console.print(f"[yellow]{tool} CLI not found on PATH. Install it first, then rerun this command.[/yellow]")
|
|
1071
|
-
console.print(_format_command(command), soft_wrap=True)
|
|
1072
|
-
return False
|
|
1073
|
-
|
|
1074
|
-
_print_command(tool, command, apply)
|
|
1075
|
-
if not apply:
|
|
1076
|
-
return True
|
|
1077
|
-
|
|
1078
|
-
code = _run(command)
|
|
1079
|
-
if code == 0:
|
|
1080
|
-
console.print(f"[green]{tool} integration configured.[/green]")
|
|
1081
|
-
return True
|
|
1082
|
-
|
|
1083
|
-
console.print(f"[red]{tool} integration command failed with exit code {code}.[/red]")
|
|
1084
|
-
console.print("You can rerun it manually:")
|
|
1085
|
-
console.print(_format_command(command), soft_wrap=True)
|
|
1086
|
-
return False
|
|
57
|
+
# Back-compat re-exports for tests and apply_integration_target
|
|
58
|
+
_project_root = common._project_root
|
|
59
|
+
_warn_if_verify_only = common._warn_if_verify_only
|
|
60
|
+
_server_args = common._server_args
|
|
61
|
+
_codex_command = codex_client._codex_command
|
|
62
|
+
_gemini_command = gemini_client._gemini_command
|
|
63
|
+
_claude_command = claude_client._claude_command
|
|
64
|
+
_cursor_config_path = cursor_client._cursor_config_path
|
|
65
|
+
_configure_cursor = cursor_client._configure_cursor
|
|
66
|
+
_configure_grok = grok_client._configure_grok
|
|
67
|
+
_configure_opencode = opencode_client._configure_opencode
|
|
68
|
+
_configure_antigravity = antigravity_client._configure_antigravity
|
|
69
|
+
_configure_warp = warp_client._configure_warp
|
|
70
|
+
_configure_aider = aider_client._configure_aider
|
|
71
|
+
_write_cursor_config = cursor_client._write_cursor_config
|
|
72
|
+
_grok_config_path = grok_client._grok_config_path
|
|
73
|
+
_write_opencode_config = opencode_client._write_opencode_config
|
|
74
|
+
_write_antigravity_mcp_config = antigravity_client._write_antigravity_mcp_config
|
|
75
|
+
_write_warp_mcp_config = warp_client._write_warp_mcp_config
|
|
76
|
+
_record_cursor_config = cursor_client._record_cursor_config
|
|
77
|
+
_record_grok_config = grok_client._record_grok_config
|
|
78
|
+
_record_claude_config = claude_client._record_claude_config
|
|
79
|
+
_record_opencode_config = opencode_client._record_opencode_config
|
|
80
|
+
_record_antigravity_config = antigravity_client._record_antigravity_config
|
|
81
|
+
_record_warp_config = warp_client._record_warp_config
|
|
82
|
+
_record_aider_config = aider_client._record_aider_config
|
|
83
|
+
_batched_raw_config = common._batched_raw_config
|
|
84
|
+
_mutate_raw_config = common._mutate_raw_config
|
|
85
|
+
_load_raw_config = common._load_raw_config
|
|
86
|
+
_save_raw_config = common._save_raw_config
|
|
87
|
+
_config_path = common._config_path
|
|
88
|
+
_load_json = common._load_json
|
|
89
|
+
_save_json = common._save_json
|
|
90
|
+
_format_command = common._format_command
|
|
91
|
+
_configure = common._configure
|
|
92
|
+
_run = common._run
|
|
93
|
+
_run_capture = common._run_capture
|
|
94
|
+
_probe_mcp_tools = common._probe_mcp_tools
|
|
95
|
+
_cursor_mcp_config = cursor_client._cursor_mcp_config
|
|
96
|
+
_opencode_plugin_source = opencode_client._opencode_plugin_source
|
|
97
|
+
_install_claude_hooks = hooks_client._install_claude_hooks
|
|
98
|
+
_install_codex_hooks = hooks_client._install_codex_hooks
|
|
99
|
+
_install_claude_assets = claude_client._install_claude_assets
|
|
100
|
+
_install_claude_plugin = claude_client._install_claude_plugin
|
|
101
|
+
_uninstall_claude = claude_client._uninstall_claude
|
|
102
|
+
_configure_native_hooks = hooks_client._configure_native_hooks
|
|
103
|
+
_opencode_config_path = opencode_client._opencode_config_path
|
|
104
|
+
_opencode_plugin_path = opencode_client._opencode_plugin_path
|
|
1087
105
|
|
|
1088
106
|
|
|
1089
107
|
@app.callback(invoke_without_command=True)
|
|
@@ -1094,31 +112,40 @@ def overview(ctx: typer.Context):
|
|
|
1094
112
|
if ctx.invoked_subcommand is not None:
|
|
1095
113
|
return
|
|
1096
114
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
115
|
+
logger.info("dev integrate: overview")
|
|
116
|
+
with log_stage("integrate", subcommand="overview"):
|
|
117
|
+
log_step("integrate/1: listing integration options", trace=True)
|
|
118
|
+
table = Table(title="DevCouncil Coding CLI Integrations")
|
|
119
|
+
table.add_column("Tool", style="cyan")
|
|
120
|
+
table.add_column("Setup command", style="green")
|
|
121
|
+
table.add_column("Notes")
|
|
122
|
+
table.add_row("Claude Code", f"{PREFERRED_COMMAND} claude --apply", "MCP + assistive hooks + slash commands, subagents, output style, skills, statusline. Add --write-gate for blocking containment.")
|
|
123
|
+
table.add_row("Codex CLI", f"{PREFERRED_COMMAND} codex --apply", "MCP + native lifecycle hooks. Review project hooks with /hooks after setup.")
|
|
124
|
+
table.add_row("Gemini CLI (deprecated)", f"{PREFERRED_COMMAND} gemini --apply", "Deprecated — use dev integrate antigravity --apply instead.")
|
|
125
|
+
table.add_row("Claude assets", f"{PREFERRED_COMMAND} claude-assets --apply", "Slash commands, subagents, output style, statusline, permissions, skills (no MCP/hooks).")
|
|
126
|
+
table.add_row("Claude plugin", f"{PREFERRED_COMMAND} claude-plugin --apply", "Self-contained Claude Code plugin + marketplace bundling everything for /plugin install.")
|
|
127
|
+
table.add_row("Claude uninstall", f"{PREFERRED_COMMAND} claude --uninstall", "Remove DevCouncil hooks, statusline, MCP enablement, and generated assets from .claude/.")
|
|
128
|
+
table.add_row("Cursor", f"{PREFERRED_COMMAND} cursor --apply", "Writes project .cursor/mcp.json for Cursor editor and agent/cursor-agent.")
|
|
129
|
+
table.add_row("Grok Build", f"{PREFERRED_COMMAND} grok --apply", "Registers DevCouncil MCP via grok mcp add or .grok/config.toml fallback.")
|
|
130
|
+
table.add_row("OpenCode", f"{PREFERRED_COMMAND} opencode --apply", "Adds DevCouncil as a project-scoped OpenCode MCP server and executor.")
|
|
131
|
+
table.add_row("Google Antigravity CLI", f"{PREFERRED_COMMAND} antigravity --apply", "Writes project .agents/mcp_config.json and enables the agy executor.")
|
|
132
|
+
table.add_row("Warp / Oz", f"{PREFERRED_COMMAND} warp --apply", "Writes a Warp-compatible MCP JSON file for local agents and Oz CLI.")
|
|
133
|
+
table.add_row("Aider", f"{PREFERRED_COMMAND} aider --apply", "Enables the built-in Aider headless executor (no MCP).")
|
|
134
|
+
table.add_row("Bring your own CLI", f"{PREFERRED_COMMAND} cli-agent NAME --command TOOL --apply", "Registers any prompt-taking CLI as a DevCouncil executor.")
|
|
135
|
+
table.add_row("All", f"{PREFERRED_COMMAND} all --apply", "Runs MCP setup and installs native hooks.")
|
|
136
|
+
table.add_row("Native hooks", f"{PREFERRED_COMMAND} hooks --apply", "Installs Codex, Claude, Cursor, Grok, and OpenCode hook files.")
|
|
137
|
+
table.add_row("Recommend", f"{PREFERRED_COMMAND} recommend", "Show the best executor for this machine and project.")
|
|
138
|
+
table.add_row("Status", f"{PREFERRED_COMMAND} status", "Compact PATH + config summary (no MCP probe).")
|
|
139
|
+
table.add_row("Matrix", f"{PREFERRED_COMMAND} matrix", "Print built-in coding CLI integration tiers.")
|
|
140
|
+
table.add_row(
|
|
141
|
+
"Check",
|
|
142
|
+
f"{PREFERRED_COMMAND} check",
|
|
143
|
+
"Verify MCP, hooks, and integrations (--strict fails on real defects; optional coding CLIs stay warnings).",
|
|
144
|
+
)
|
|
145
|
+
console.print(table)
|
|
146
|
+
console.print(f"\nIf your install exposes only the setup flow, use: {LEGACY_COMMAND} --apply")
|
|
147
|
+
console.print("\nRun without [bold]--apply[/bold] to preview the exact commands first.")
|
|
148
|
+
log_step("integrate/complete", trace=True)
|
|
1122
149
|
|
|
1123
150
|
|
|
1124
151
|
@app.command("doctor")
|
|
@@ -1127,51 +154,7 @@ def integrations_doctor(
|
|
|
1127
154
|
):
|
|
1128
155
|
"""Check optional integration tools and local client wiring prerequisites."""
|
|
1129
156
|
root = _project_root(project_root)
|
|
1130
|
-
|
|
1131
|
-
table.add_column("Integration", style="cyan", no_wrap=True)
|
|
1132
|
-
table.add_column("Status")
|
|
1133
|
-
table.add_column("Notes", overflow="fold")
|
|
1134
|
-
|
|
1135
|
-
checks = [
|
|
1136
|
-
("Agent Flow", "agent-flow-app", "Optional live/replay visualizer for trace JSONL."),
|
|
1137
|
-
("code-review-graph", "code-review-graph", "Optional structural graph context adapter."),
|
|
1138
|
-
("Claude Code", "claude", "Optional MCP client and native hook runtime for pre-tool-use enforcement."),
|
|
1139
|
-
("Codex CLI", "codex", "Optional MCP client, headless executor companion, and native hook runtime."),
|
|
1140
|
-
("Gemini CLI", "gemini", "Optional MCP client companion and native hook runtime."),
|
|
1141
|
-
("Cursor", "cursor-agent", "Optional MCP client, cursor-agent executor, and native hooks."),
|
|
1142
|
-
("OpenCode", "opencode", "Optional MCP client and headless coding-agent executor."),
|
|
1143
|
-
("Google Antigravity CLI", "agy", "Optional Antigravity CLI companion and headless coding-agent executor."),
|
|
1144
|
-
("Warp / Oz", "oz", "Optional Warp/Oz CLI companion and agent executor."),
|
|
1145
|
-
("Aider", "aider", "Optional headless executor via `dev run --executor aider` (no MCP)."),
|
|
1146
|
-
]
|
|
1147
|
-
for label, executable, notes in checks:
|
|
1148
|
-
found = shutil.which(executable)
|
|
1149
|
-
table.add_row(label, "[green]OK[/green]" if found else "[yellow]Missing[/yellow]", found or notes)
|
|
1150
|
-
|
|
1151
|
-
profiles = load_agent_profiles(root)
|
|
1152
|
-
for name, spec in load_cli_agent_specs(root).items():
|
|
1153
|
-
if spec.built_in:
|
|
1154
|
-
continue
|
|
1155
|
-
found = shutil.which(spec.executable)
|
|
1156
|
-
mode_ok = spec.input_mode in VALID_INPUT_MODES
|
|
1157
|
-
profile_ok = spec.default_profile in profiles
|
|
1158
|
-
status = "[green]OK[/green]" if found and mode_ok and profile_ok else "[red]Invalid[/red]"
|
|
1159
|
-
if not found:
|
|
1160
|
-
status = "[yellow]Missing[/yellow]"
|
|
1161
|
-
details = found or f"{spec.executable} not found on PATH"
|
|
1162
|
-
if not mode_ok:
|
|
1163
|
-
details = f"invalid input_mode={spec.input_mode}"
|
|
1164
|
-
if not profile_ok:
|
|
1165
|
-
details = f"{details}; missing profile={spec.default_profile}"
|
|
1166
|
-
table.add_row(f"CLI agent: {name}", status, details)
|
|
1167
|
-
|
|
1168
|
-
config = _config_path(root)
|
|
1169
|
-
table.add_row(
|
|
1170
|
-
"DevCouncil config",
|
|
1171
|
-
"[green]OK[/green]" if config.exists() else "[red]Missing[/red]",
|
|
1172
|
-
str(config) if config.exists() else "Run dev init first.",
|
|
1173
|
-
)
|
|
1174
|
-
console.print(table)
|
|
157
|
+
console.print(build_integrations_doctor_table(root))
|
|
1175
158
|
|
|
1176
159
|
|
|
1177
160
|
@app.command("codex")
|
|
@@ -1180,11 +163,22 @@ def codex(
|
|
|
1180
163
|
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
1181
164
|
):
|
|
1182
165
|
"""
|
|
1183
|
-
Set up DevCouncil MCP tools for Codex CLI.
|
|
166
|
+
Set up DevCouncil MCP tools and native lifecycle hooks for Codex CLI.
|
|
1184
167
|
"""
|
|
1185
168
|
root = _project_root(project_root)
|
|
1186
169
|
command = _codex_command(root)
|
|
1187
170
|
ok = _configure("Codex CLI", command, apply)
|
|
171
|
+
if apply and ok:
|
|
172
|
+
try:
|
|
173
|
+
written = _install_codex_hooks(root)
|
|
174
|
+
common.record_hook_dev_executable(root)
|
|
175
|
+
except (ValueError, FileNotFoundError, OSError) as exc:
|
|
176
|
+
console.print(f"[red]Codex hook setup failed: {exc}[/red]")
|
|
177
|
+
raise typer.Exit(code=1) from exc
|
|
178
|
+
console.print(
|
|
179
|
+
f"[green]Codex integration installed[/green] ({len(written)} hook/config file(s)): MCP + native hooks."
|
|
180
|
+
)
|
|
181
|
+
console.print("[yellow]Open Codex in this project and run /hooks to review and trust the generated hooks.[/yellow]")
|
|
1188
182
|
if not ok and apply:
|
|
1189
183
|
raise typer.Exit(code=1)
|
|
1190
184
|
|
|
@@ -1203,8 +197,9 @@ def gemini(
|
|
|
1203
197
|
raise typer.Exit(code=2)
|
|
1204
198
|
|
|
1205
199
|
root = _project_root(project_root)
|
|
200
|
+
console.print(f"[yellow]{GEMINI_DEPRECATION_MESSAGE}[/yellow]")
|
|
1206
201
|
command = _gemini_command(root, scope)
|
|
1207
|
-
ok = _configure("Gemini CLI", command, apply)
|
|
202
|
+
ok = _configure("Gemini CLI (deprecated)", command, apply)
|
|
1208
203
|
if not ok and apply:
|
|
1209
204
|
raise typer.Exit(code=1)
|
|
1210
205
|
|
|
@@ -1256,6 +251,7 @@ def claude(
|
|
|
1256
251
|
try:
|
|
1257
252
|
written = _install_claude_hooks(root, write_gate=write_gate)
|
|
1258
253
|
written += _install_claude_assets(root)
|
|
254
|
+
_record_claude_config(root, scope=scope, write_gate=write_gate)
|
|
1259
255
|
except (ValueError, FileNotFoundError, OSError) as exc:
|
|
1260
256
|
console.print(f"[red]Claude asset setup failed: {exc}[/red]")
|
|
1261
257
|
raise typer.Exit(code=1) from exc
|
|
@@ -1356,6 +352,37 @@ def claude_plugin_cmd(
|
|
|
1356
352
|
console.print(" [dim]/plugin install devcouncil@devcouncil-local[/dim]")
|
|
1357
353
|
|
|
1358
354
|
|
|
355
|
+
@app.command("claude-github")
|
|
356
|
+
def claude_github(
|
|
357
|
+
apply: bool = typer.Option(False, "--apply", help="Write the workflow file instead of previewing it."),
|
|
358
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
359
|
+
):
|
|
360
|
+
"""
|
|
361
|
+
Generate a GitHub Actions workflow that runs DevCouncil + Claude Code on repo events.
|
|
362
|
+
|
|
363
|
+
Read-only DevCouncil verification on pull requests, and gated autonomous task pickup via
|
|
364
|
+
headless Claude Code on manual dispatch or a nightly schedule. This is the stable-primitive
|
|
365
|
+
alternative to Claude Code's experimental cloud Routines; the autonomous job needs an
|
|
366
|
+
ANTHROPIC_API_KEY repository secret.
|
|
367
|
+
"""
|
|
368
|
+
from devcouncil.integrations.claude_assets import build_github_workflow
|
|
369
|
+
|
|
370
|
+
root = _project_root(project_root)
|
|
371
|
+
asset = build_github_workflow(root)
|
|
372
|
+
if not apply:
|
|
373
|
+
console.print("[bold]DevCouncil GitHub Actions workflow (preview)[/bold]")
|
|
374
|
+
console.print(f"Would write: [dim]{asset.path}[/dim]")
|
|
375
|
+
console.print("[yellow]Preview only. Rerun with --apply to write the workflow.[/yellow]")
|
|
376
|
+
return
|
|
377
|
+
changed = asset.write_if_changed()
|
|
378
|
+
rel = asset.path.relative_to(root)
|
|
379
|
+
if changed:
|
|
380
|
+
console.print(f"[green]Wrote GitHub Actions workflow[/green] at {rel}")
|
|
381
|
+
else:
|
|
382
|
+
console.print(f"[dim]GitHub Actions workflow already up to date at {rel}[/dim]")
|
|
383
|
+
console.print("Add an [bold]ANTHROPIC_API_KEY[/bold] repository secret for the autonomous runs.")
|
|
384
|
+
|
|
385
|
+
|
|
1359
386
|
@app.command("cursor")
|
|
1360
387
|
def cursor(
|
|
1361
388
|
apply: bool = typer.Option(False, "--apply", help="Write project Cursor MCP config instead of printing it."),
|
|
@@ -1377,6 +404,27 @@ def cursor(
|
|
|
1377
404
|
raise typer.Exit(code=1)
|
|
1378
405
|
|
|
1379
406
|
|
|
407
|
+
@app.command("grok")
|
|
408
|
+
def grok(
|
|
409
|
+
apply: bool = typer.Option(False, "--apply", help="Register Grok MCP config instead of printing it."),
|
|
410
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
411
|
+
):
|
|
412
|
+
"""
|
|
413
|
+
Set up DevCouncil MCP tools for Grok Build.
|
|
414
|
+
"""
|
|
415
|
+
root = _project_root(project_root)
|
|
416
|
+
if apply:
|
|
417
|
+
report = apply_integration_target(root, "grok")
|
|
418
|
+
if not report.ok:
|
|
419
|
+
console.print(report.to_json())
|
|
420
|
+
raise typer.Exit(code=1)
|
|
421
|
+
console.print("[green]Grok integration configured.[/green]")
|
|
422
|
+
return
|
|
423
|
+
ok = _configure_grok(root, apply)
|
|
424
|
+
if not ok and apply:
|
|
425
|
+
raise typer.Exit(code=1)
|
|
426
|
+
|
|
427
|
+
|
|
1380
428
|
@app.command("opencode")
|
|
1381
429
|
def opencode(
|
|
1382
430
|
apply: bool = typer.Option(False, "--apply", help="Write project OpenCode config instead of printing it."),
|
|
@@ -1441,29 +489,6 @@ def warp(
|
|
|
1441
489
|
_configure_warp(root, apply)
|
|
1442
490
|
|
|
1443
491
|
|
|
1444
|
-
def _record_aider_config(project_root: Path) -> None:
|
|
1445
|
-
def mutate(config: dict) -> None:
|
|
1446
|
-
config.setdefault("integrations", {}).setdefault("aider", {}).update({"enabled": True})
|
|
1447
|
-
|
|
1448
|
-
_mutate_raw_config(project_root, mutate)
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
def _configure_aider(project_root: Path, apply: bool) -> bool:
|
|
1452
|
-
command = ["aider", "--yes", "--no-show-model-warnings", "--message", "<task prompt>"]
|
|
1453
|
-
if not apply:
|
|
1454
|
-
console.print("[bold]Aider[/bold]")
|
|
1455
|
-
console.print("Built-in executor: [dim]dev run TASK-001 --executor aider[/dim]")
|
|
1456
|
-
console.print("Launch command: [dim]" + _format_command(command) + "[/dim]")
|
|
1457
|
-
console.print("Aider does not expose a first-party DevCouncil MCP server.")
|
|
1458
|
-
return True
|
|
1459
|
-
|
|
1460
|
-
if not shutil.which("aider"):
|
|
1461
|
-
console.print("[yellow]Aider CLI not found on PATH. Install it before using `dev run --executor aider`.[/yellow]")
|
|
1462
|
-
_record_aider_config(project_root)
|
|
1463
|
-
console.print("[green]Aider executor enabled in .devcouncil/config.yaml.[/green]")
|
|
1464
|
-
return True
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
492
|
@app.command("aider")
|
|
1468
493
|
def aider(
|
|
1469
494
|
apply: bool = typer.Option(False, "--apply", help="Record the built-in Aider executor in DevCouncil config."),
|
|
@@ -1542,7 +567,7 @@ def cli_agent(
|
|
|
1542
567
|
if not apply:
|
|
1543
568
|
console.print("[bold]Bring your own CLI executor preview[/bold]")
|
|
1544
569
|
console.print(f"Executor: [cyan]{normalized}[/cyan]")
|
|
1545
|
-
console.print(
|
|
570
|
+
console.print(dump_json(entry, indent=2), soft_wrap=True)
|
|
1546
571
|
console.print(f"Run with: [dim]dev run TASK-001 --executor {normalized}[/dim]")
|
|
1547
572
|
console.print("[yellow]Preview only. Rerun with --apply to update .devcouncil/config.yaml.[/yellow]")
|
|
1548
573
|
return
|
|
@@ -1570,7 +595,7 @@ def all_tools(
|
|
|
1570
595
|
strict: bool = typer.Option(
|
|
1571
596
|
False,
|
|
1572
597
|
"--strict",
|
|
1573
|
-
help="After --apply, run dev integrate check --strict
|
|
598
|
+
help="After --apply, run dev integrate check --strict (fails on real integration defects; missing optional coding CLIs stay warnings).",
|
|
1574
599
|
),
|
|
1575
600
|
):
|
|
1576
601
|
"""
|
|
@@ -1601,13 +626,13 @@ def all_tools(
|
|
|
1601
626
|
return
|
|
1602
627
|
|
|
1603
628
|
commands = [
|
|
1604
|
-
("Codex CLI", _codex_command(root)),
|
|
1605
|
-
("Gemini CLI", _gemini_command(root, gemini_scope)),
|
|
1606
629
|
("Claude Code", _claude_command(root, claude_scope)),
|
|
630
|
+
("Codex CLI", _codex_command(root)),
|
|
1607
631
|
]
|
|
1608
632
|
for tool, command in commands:
|
|
1609
633
|
_configure(tool, command, apply)
|
|
1610
634
|
_configure_cursor(root, apply)
|
|
635
|
+
_configure_grok(root, apply)
|
|
1611
636
|
_configure_opencode(root, apply)
|
|
1612
637
|
_configure_antigravity(root, apply)
|
|
1613
638
|
_configure_warp(root, apply)
|
|
@@ -1621,47 +646,7 @@ def recommend(
|
|
|
1621
646
|
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
1622
647
|
):
|
|
1623
648
|
"""Recommend a coding CLI executor for this machine and project."""
|
|
1624
|
-
|
|
1625
|
-
probe_order = resolve_coding_cli_probe_order(root)
|
|
1626
|
-
detected = detect_available_coding_cli(root, probe_order=probe_order)
|
|
1627
|
-
resolved = resolve_automated_executor(root, None)
|
|
1628
|
-
|
|
1629
|
-
table = Table(title="DevCouncil Integration Recommendations")
|
|
1630
|
-
table.add_column("Client", style="cyan")
|
|
1631
|
-
table.add_column("PATH")
|
|
1632
|
-
table.add_column("Tier")
|
|
1633
|
-
table.add_column("MCP")
|
|
1634
|
-
table.add_column("Hooks")
|
|
1635
|
-
|
|
1636
|
-
for client in probe_order:
|
|
1637
|
-
info = CODING_CLI_INTEGRATION_INFO.get(client)
|
|
1638
|
-
on_path = resolve_coding_cli_executable(root, client)
|
|
1639
|
-
table.add_row(
|
|
1640
|
-
client,
|
|
1641
|
-
"[green]yes[/green]" if on_path else "[dim]no[/dim]",
|
|
1642
|
-
integration_tier_label(client),
|
|
1643
|
-
"yes" if info and info.mcp else "no",
|
|
1644
|
-
"yes" if info and info.hooks else "no",
|
|
1645
|
-
)
|
|
1646
|
-
|
|
1647
|
-
console.print(table)
|
|
1648
|
-
if summary := integration_status_summary(root):
|
|
1649
|
-
if summary.get("custom_probe_order"):
|
|
1650
|
-
console.print(
|
|
1651
|
-
f"\n[dim]Probe order:[/dim] {', '.join(summary['probe_order'])} "
|
|
1652
|
-
f"(from execution.coding_cli_probe_order)"
|
|
1653
|
-
)
|
|
1654
|
-
else:
|
|
1655
|
-
console.print(f"\n[dim]Probe order:[/dim] {', '.join(summary['probe_order'])} (default)")
|
|
1656
|
-
if detected:
|
|
1657
|
-
console.print(f"\n[bold]Recommended executor:[/bold] [cyan]{resolved}[/cyan]")
|
|
1658
|
-
console.print(f"Run: [dim]dev run TASK-001 --executor {resolved}[/dim]")
|
|
1659
|
-
console.print(f"Or: [dim]dev go \"Your goal\" --executor {resolved}[/dim]")
|
|
1660
|
-
console.print(f"Setup: [dim]{PREFERRED_COMMAND} {resolved} --apply[/dim]")
|
|
1661
|
-
else:
|
|
1662
|
-
console.print("\n[yellow]No built-in coding CLI was found on PATH.[/yellow]")
|
|
1663
|
-
console.print("Install Codex, Gemini, Claude Code, Cursor Agent, OpenCode, or register a custom CLI:")
|
|
1664
|
-
console.print(f"[dim]{PREFERRED_COMMAND} cli-agent NAME --command TOOL --apply[/dim]")
|
|
649
|
+
print_recommendations(_project_root(project_root), console)
|
|
1665
650
|
|
|
1666
651
|
|
|
1667
652
|
@app.command("status")
|
|
@@ -1670,46 +655,7 @@ def status(
|
|
|
1670
655
|
as_json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
|
|
1671
656
|
):
|
|
1672
657
|
"""Show a compact integration summary without running the MCP server probe."""
|
|
1673
|
-
|
|
1674
|
-
summary = integration_status_summary(root)
|
|
1675
|
-
raw_config = _load_raw_config(root) if (root / ".devcouncil").exists() else {}
|
|
1676
|
-
integrations = raw_config.get("integrations", {})
|
|
1677
|
-
|
|
1678
|
-
if as_json:
|
|
1679
|
-
payload = {
|
|
1680
|
-
**summary,
|
|
1681
|
-
"integrations_enabled": {
|
|
1682
|
-
name: bool(integrations.get(name, {}).get("enabled"))
|
|
1683
|
-
for name in ("cursor", "opencode", "antigravity", "warp", "aider")
|
|
1684
|
-
},
|
|
1685
|
-
}
|
|
1686
|
-
typer.echo(json.dumps(payload, indent=2))
|
|
1687
|
-
return
|
|
1688
|
-
|
|
1689
|
-
table = Table(title="DevCouncil Integration Status")
|
|
1690
|
-
table.add_column("Setting", style="cyan")
|
|
1691
|
-
table.add_column("Value")
|
|
1692
|
-
|
|
1693
|
-
table.add_row("Project", "[green]initialized[/green]" if summary["project_initialized"] else "[yellow]not initialized[/yellow]")
|
|
1694
|
-
table.add_row("Default executor", summary["default_executor"])
|
|
1695
|
-
table.add_row("Resolved executor", summary["resolved_executor"])
|
|
1696
|
-
table.add_row("CLIs on PATH", ", ".join(summary["coding_clis_on_path"]) or "[dim]none[/dim]")
|
|
1697
|
-
table.add_row("Probe order", ", ".join(summary["probe_order"]))
|
|
1698
|
-
table.add_row("Stream CLI output", "yes" if summary["stream_cli_output"] else "no")
|
|
1699
|
-
table.add_row("Cursor resume mode", summary["cursor_resume_mode"])
|
|
1700
|
-
|
|
1701
|
-
for name in ("cursor", "opencode", "antigravity", "warp", "aider"):
|
|
1702
|
-
enabled = bool(integrations.get(name, {}).get("enabled"))
|
|
1703
|
-
table.add_row(f"{name} integration", "[green]enabled[/green]" if enabled else "[dim]off[/dim]")
|
|
1704
|
-
|
|
1705
|
-
console.print(table)
|
|
1706
|
-
if summary["resolved_executor"] not in {"", "manual"}:
|
|
1707
|
-
console.print(
|
|
1708
|
-
f"\n[dim]Next:[/dim] dev run TASK-001 --executor {summary['resolved_executor']} "
|
|
1709
|
-
f"| {PREFERRED_COMMAND} check for full readiness"
|
|
1710
|
-
)
|
|
1711
|
-
else:
|
|
1712
|
-
console.print(f"\n[dim]Next:[/dim] {PREFERRED_COMMAND} recommend | {PREFERRED_COMMAND} check")
|
|
658
|
+
print_integration_status(_project_root(project_root), console, as_json=as_json)
|
|
1713
659
|
|
|
1714
660
|
|
|
1715
661
|
@app.command("matrix")
|
|
@@ -1717,36 +663,8 @@ def matrix(
|
|
|
1717
663
|
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
1718
664
|
):
|
|
1719
665
|
"""Print built-in coding CLI integration tiers and capabilities."""
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
table = Table(title="DevCouncil Coding CLI Integration Matrix")
|
|
1723
|
-
table.add_column("Client", style="cyan")
|
|
1724
|
-
table.add_column("Tier")
|
|
1725
|
-
table.add_column("Headless")
|
|
1726
|
-
table.add_column("MCP setup")
|
|
1727
|
-
table.add_column("Native hooks")
|
|
1728
|
-
table.add_column("Enforcement")
|
|
1729
|
-
table.add_column("Notes")
|
|
1730
|
-
|
|
1731
|
-
for client in sorted(BUILTIN_CODING_EXECUTOR_NAMES):
|
|
1732
|
-
info = CODING_CLI_INTEGRATION_INFO.get(client)
|
|
1733
|
-
posture = info.enforcement if info else "verify-only"
|
|
1734
|
-
posture_render = "[green]pre-action[/green]" if posture == "pre-action" else "[yellow]verify-only[/yellow]"
|
|
1735
|
-
table.add_row(
|
|
1736
|
-
client,
|
|
1737
|
-
integration_tier_label(client),
|
|
1738
|
-
"yes" if info and info.tier == 1 else "no",
|
|
1739
|
-
"yes" if info and info.mcp else "no",
|
|
1740
|
-
"yes" if info and info.hooks else "verify only",
|
|
1741
|
-
posture_render,
|
|
1742
|
-
info.notes if info else "",
|
|
1743
|
-
)
|
|
1744
|
-
console.print(table)
|
|
1745
|
-
console.print(
|
|
1746
|
-
"\n[dim]Enforcement:[/dim] [green]pre-action[/green] blocks forbidden writes/commands "
|
|
1747
|
-
"before they happen; [yellow]verify-only[/yellow] catches them only at verify time."
|
|
1748
|
-
)
|
|
1749
|
-
console.print("\nSee [dim]docs/integration-tiers.md[/dim] for workflow guidance.")
|
|
666
|
+
_ = _project_root(project_root)
|
|
667
|
+
print_integration_matrix(console)
|
|
1750
668
|
|
|
1751
669
|
|
|
1752
670
|
@app.command("hooks")
|
|
@@ -1758,27 +676,117 @@ def hooks(
|
|
|
1758
676
|
False,
|
|
1759
677
|
"--write-gate/--no-write-gate",
|
|
1760
678
|
"--contain/--no-contain",
|
|
1761
|
-
help="Install Claude's blocking PreToolUse
|
|
679
|
+
help="Install Claude's blocking PreToolUse write-gate too (off by default; "
|
|
1762
680
|
"fail-closes an interactive session without a task lease).",
|
|
1763
681
|
),
|
|
682
|
+
git: bool = typer.Option(
|
|
683
|
+
True,
|
|
684
|
+
"--git/--no-git",
|
|
685
|
+
help="Install git post-commit/post-merge/post-checkout hooks that run "
|
|
686
|
+
"`dev map --if-stale` (on by default with --apply).",
|
|
687
|
+
),
|
|
688
|
+
check: bool = typer.Option(
|
|
689
|
+
False,
|
|
690
|
+
"--check",
|
|
691
|
+
help="Report whether installed hook commands still point at the resolved project `dev` executable.",
|
|
692
|
+
),
|
|
1764
693
|
):
|
|
1765
694
|
"""
|
|
1766
695
|
Install DevCouncil hook configuration for Codex, Gemini, Claude, Cursor, and OpenCode.
|
|
1767
696
|
|
|
1768
|
-
Claude installs
|
|
1769
|
-
containment (
|
|
697
|
+
Claude installs assistive hooks plus refresh-only PostToolUse by default; add
|
|
698
|
+
--write-gate for pre-action PreToolUse containment (autonomous executor runs).
|
|
699
|
+
Git map-refresh hooks install by default with --apply (use --no-git to skip).
|
|
1770
700
|
"""
|
|
1771
701
|
root = _project_root(project_root)
|
|
702
|
+
if check:
|
|
703
|
+
ok, details = common.check_hook_dev_executable(root)
|
|
704
|
+
current = common.resolve_dev_executable(root)
|
|
705
|
+
recorded = common.recorded_hook_dev_executable(root)
|
|
706
|
+
console.print(f"Resolved `dev`: [cyan]{current}[/cyan]")
|
|
707
|
+
console.print(f"Recorded for hooks: [cyan]{recorded or '(none)'}[/cyan]")
|
|
708
|
+
if ok:
|
|
709
|
+
console.print(f"[green]OK[/green] — {details}")
|
|
710
|
+
raise typer.Exit(code=0)
|
|
711
|
+
console.print(f"[red]MISMATCH[/red] — {details}")
|
|
712
|
+
console.print(
|
|
713
|
+
f"[dim]Re-run `{PREFERRED_COMMAND} hooks --apply` to retarget hook commands.[/dim]"
|
|
714
|
+
)
|
|
715
|
+
raise typer.Exit(code=1)
|
|
1772
716
|
if apply and tool == "all":
|
|
1773
717
|
report = apply_integration_target(root, "hooks", claude_write_gate=write_gate)
|
|
1774
718
|
if not report.ok:
|
|
1775
719
|
console.print(report.to_json())
|
|
1776
720
|
raise typer.Exit(code=1)
|
|
1777
721
|
console.print("[green]Native hooks configured.[/green]")
|
|
1778
|
-
|
|
1779
|
-
|
|
722
|
+
else:
|
|
723
|
+
_configure_native_hooks(root, tool, apply, claude_write_gate=write_gate)
|
|
724
|
+
if git:
|
|
725
|
+
written = _install_git_map_hooks(root, apply=apply)
|
|
726
|
+
for path in written:
|
|
727
|
+
console.print(f"[green]{'Wrote' if apply else 'Would write'} git hook:[/green] {path}")
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _install_git_map_hooks(root: Path, *, apply: bool) -> list[str]:
|
|
731
|
+
"""Install post-commit/post-merge/post-checkout hooks that refresh the map when stale."""
|
|
732
|
+
hooks_dir = root / ".git" / "hooks"
|
|
733
|
+
if not (root / ".git").exists():
|
|
734
|
+
console.print("[yellow]No .git directory; skipping git map hooks.[/yellow]")
|
|
735
|
+
return []
|
|
736
|
+
executable = common.resolve_dev_executable(root)
|
|
737
|
+
# Quote for POSIX shell; absolute path avoids PATH shadowing by a stale global install.
|
|
738
|
+
quoted = common._format_command([executable])
|
|
739
|
+
script = f"""#!/bin/sh
|
|
740
|
+
# DevCouncil: refresh repo map after git operations (best-effort).
|
|
741
|
+
{quoted} map --if-stale --no-wiki --project-root "$(git rev-parse --show-toplevel)" >/dev/null 2>&1 || true
|
|
742
|
+
"""
|
|
743
|
+
written: list[str] = []
|
|
744
|
+
for name in ("post-commit", "post-merge", "post-checkout"):
|
|
745
|
+
path = hooks_dir / name
|
|
746
|
+
rel = str(path.relative_to(root)) if path.is_relative_to(root) else str(path)
|
|
747
|
+
written.append(rel)
|
|
748
|
+
if not apply:
|
|
749
|
+
continue
|
|
750
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
751
|
+
existing = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
752
|
+
marker = "# DevCouncil: refresh repo map"
|
|
753
|
+
if marker in existing:
|
|
754
|
+
# Retarget the executable if a previous install used a different path.
|
|
755
|
+
if quoted not in existing:
|
|
756
|
+
updated = _retarget_git_hook_script(existing, quoted)
|
|
757
|
+
path.write_text(updated, encoding="utf-8")
|
|
758
|
+
continue
|
|
759
|
+
if existing and not existing.endswith("\n"):
|
|
760
|
+
existing += "\n"
|
|
761
|
+
path.write_text(existing + script if existing else script, encoding="utf-8")
|
|
762
|
+
path.chmod(path.stat().st_mode | 0o111)
|
|
763
|
+
if apply:
|
|
764
|
+
common.record_hook_dev_executable(root, executable)
|
|
765
|
+
return written
|
|
1780
766
|
|
|
1781
767
|
|
|
768
|
+
def _retarget_git_hook_script(existing: str, quoted_executable: str) -> str:
|
|
769
|
+
"""Replace the DevCouncil map-refresh command line with the current executable."""
|
|
770
|
+
import re
|
|
771
|
+
|
|
772
|
+
pattern = re.compile(
|
|
773
|
+
r"(?m)^.*\bmap --if-stale\b.*$|^.*\bmap --no-wiki\b.*$",
|
|
774
|
+
)
|
|
775
|
+
replacement = (
|
|
776
|
+
f'{quoted_executable} map --if-stale --no-wiki '
|
|
777
|
+
f'--project-root "$(git rev-parse --show-toplevel)" >/dev/null 2>&1 || true'
|
|
778
|
+
)
|
|
779
|
+
if pattern.search(existing):
|
|
780
|
+
return pattern.sub(replacement, existing, count=1)
|
|
781
|
+
# Marker present but no recognizable command line — append a fresh one.
|
|
782
|
+
suffix = existing if existing.endswith("\n") else existing + "\n"
|
|
783
|
+
return (
|
|
784
|
+
suffix
|
|
785
|
+
+ "# DevCouncil: refresh repo map after git operations (best-effort).\n"
|
|
786
|
+
+ replacement
|
|
787
|
+
+ "\n"
|
|
788
|
+
)
|
|
789
|
+
|
|
1782
790
|
@app.command("uninstall")
|
|
1783
791
|
def uninstall(
|
|
1784
792
|
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
@@ -1807,7 +815,7 @@ def check(
|
|
|
1807
815
|
strict: bool = typer.Option(
|
|
1808
816
|
False,
|
|
1809
817
|
"--strict",
|
|
1810
|
-
help="
|
|
818
|
+
help="Fail on real project/integration defects (broken state, hooks, MCP, configured-client auth). Missing optional coding CLIs stay warnings.",
|
|
1811
819
|
),
|
|
1812
820
|
as_json: bool = typer.Option(False, "--json", help="Emit machine-readable JSON for CI."),
|
|
1813
821
|
report_file: Path | None = typer.Option(
|
|
@@ -1821,48 +829,14 @@ def check(
|
|
|
1821
829
|
"""
|
|
1822
830
|
Check whether DevCouncil is ready to integrate with coding CLIs.
|
|
1823
831
|
"""
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
if row.status == "ok":
|
|
1833
|
-
rendered = "[green]OK[/green]"
|
|
1834
|
-
elif row.status == "skip":
|
|
1835
|
-
rendered = "[dim]SKIP[/dim]"
|
|
1836
|
-
elif row.status == "missing":
|
|
1837
|
-
rendered = "[yellow]Missing[/yellow]"
|
|
1838
|
-
else:
|
|
1839
|
-
rendered = "[red]FAIL[/red]"
|
|
1840
|
-
table.add_row(row.name, rendered, row.details)
|
|
1841
|
-
|
|
1842
|
-
write_json = as_json or report_file is not None
|
|
1843
|
-
if write_json:
|
|
1844
|
-
json_text = report.to_json()
|
|
1845
|
-
if report_file is not None:
|
|
1846
|
-
report_path = Path(report_file).expanduser().resolve()
|
|
1847
|
-
report_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1848
|
-
report_path.write_text(json_text + "\n", encoding="utf-8")
|
|
1849
|
-
if not as_json:
|
|
1850
|
-
console.print(f"[dim]Wrote integration report to[/dim] {report_path}")
|
|
1851
|
-
if as_json:
|
|
1852
|
-
typer.echo(json_text)
|
|
1853
|
-
if not write_json or not as_json:
|
|
1854
|
-
console.print(table)
|
|
1855
|
-
|
|
1856
|
-
if report.failures:
|
|
1857
|
-
if not as_json:
|
|
1858
|
-
console.print(
|
|
1859
|
-
f"\n[yellow]Fix failed checks, then run:[/yellow] {PREFERRED_COMMAND} all --apply "
|
|
1860
|
-
f"(or {LEGACY_COMMAND} --apply)."
|
|
1861
|
-
)
|
|
1862
|
-
raise typer.Exit(code=1)
|
|
1863
|
-
|
|
1864
|
-
if not as_json:
|
|
1865
|
-
console.print(f"\n[green]Ready.[/green] Run: {PREFERRED_COMMAND} all --apply (or {LEGACY_COMMAND} --apply).")
|
|
832
|
+
run_integration_check(
|
|
833
|
+
_project_root(project_root),
|
|
834
|
+
console,
|
|
835
|
+
strict=strict,
|
|
836
|
+
as_json=as_json,
|
|
837
|
+
report_file=report_file,
|
|
838
|
+
legacy_command=LEGACY_COMMAND,
|
|
839
|
+
)
|
|
1866
840
|
|
|
1867
841
|
|
|
1868
842
|
@setup_app.command("agent-flow")
|
|
@@ -1882,33 +856,7 @@ def setup_agent_flow(
|
|
|
1882
856
|
console.print("[yellow]Preview only. Rerun with --apply to record this integration in config.[/yellow]")
|
|
1883
857
|
return
|
|
1884
858
|
|
|
1885
|
-
|
|
1886
|
-
integrations = config.setdefault("integrations", {})
|
|
1887
|
-
integrations["agent_flow"] = {
|
|
1888
|
-
"enabled": True,
|
|
1889
|
-
"trace_path": str(trace_path),
|
|
1890
|
-
"mode": "jsonl",
|
|
1891
|
-
}
|
|
1892
|
-
_save_raw_config(root, config)
|
|
1893
|
-
docs_dir = root / ".devcouncil" / "integrations"
|
|
1894
|
-
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
1895
|
-
(docs_dir / "agent-flow.md").write_text(
|
|
1896
|
-
"\n".join([
|
|
1897
|
-
"# Agent Flow",
|
|
1898
|
-
"",
|
|
1899
|
-
f"DevCouncil writes trace events to `{trace_path}`.",
|
|
1900
|
-
"",
|
|
1901
|
-
"Local replay:",
|
|
1902
|
-
"",
|
|
1903
|
-
"```bash",
|
|
1904
|
-
"dev trace tail --follow",
|
|
1905
|
-
"```",
|
|
1906
|
-
"",
|
|
1907
|
-
"External visualizers can watch the JSONL file directly. DevCouncil does not modify global editor or Claude Code settings from this setup command.",
|
|
1908
|
-
"",
|
|
1909
|
-
]),
|
|
1910
|
-
encoding="utf-8",
|
|
1911
|
-
)
|
|
859
|
+
trace_path = apply_agent_flow_setup(root)
|
|
1912
860
|
console.print("[green]Agent Flow trace integration recorded in .devcouncil/config.yaml.[/green]")
|
|
1913
861
|
|
|
1914
862
|
|
|
@@ -1930,44 +878,7 @@ def setup_code_review_graph(
|
|
|
1930
878
|
console.print("[yellow]Preview only. Rerun with --apply to record this integration.[/yellow]")
|
|
1931
879
|
return
|
|
1932
880
|
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
"\n".join([
|
|
1936
|
-
".devcouncil/**",
|
|
1937
|
-
".git/**",
|
|
1938
|
-
".venv/**",
|
|
1939
|
-
"dist/**",
|
|
1940
|
-
"node_modules/**",
|
|
1941
|
-
"",
|
|
1942
|
-
]),
|
|
1943
|
-
encoding="utf-8",
|
|
1944
|
-
)
|
|
881
|
+
_executable, ignore_path, created = apply_code_review_graph_setup(root)
|
|
882
|
+
if created:
|
|
1945
883
|
console.print(f"[green]Created {ignore_path}.[/green]")
|
|
1946
|
-
|
|
1947
|
-
config = _load_raw_config(root)
|
|
1948
|
-
integrations = config.setdefault("integrations", {})
|
|
1949
|
-
integrations["code_review_graph"] = {
|
|
1950
|
-
"enabled": True,
|
|
1951
|
-
"command": "code-review-graph",
|
|
1952
|
-
"optional": True,
|
|
1953
|
-
}
|
|
1954
|
-
_save_raw_config(root, config)
|
|
1955
|
-
docs_dir = root / ".devcouncil" / "integrations"
|
|
1956
|
-
docs_dir.mkdir(parents=True, exist_ok=True)
|
|
1957
|
-
(docs_dir / "code-review-graph.md").write_text(
|
|
1958
|
-
"\n".join([
|
|
1959
|
-
"# code-review-graph",
|
|
1960
|
-
"",
|
|
1961
|
-
"Install and build the graph outside DevCouncil:",
|
|
1962
|
-
"",
|
|
1963
|
-
"```bash",
|
|
1964
|
-
"pipx install code-review-graph",
|
|
1965
|
-
"code-review-graph build",
|
|
1966
|
-
"```",
|
|
1967
|
-
"",
|
|
1968
|
-
"DevCouncil uses this as an optional context adapter for mapping, prompts, verification traces, and MCP graph context.",
|
|
1969
|
-
"",
|
|
1970
|
-
]),
|
|
1971
|
-
encoding="utf-8",
|
|
1972
|
-
)
|
|
1973
884
|
console.print("[green]code-review-graph adapter recorded in .devcouncil/config.yaml.[/green]")
|