devcouncil 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -33
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +56 -40
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +964 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +325 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/summary.py +21 -3
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
|
|
2
|
+
"""Shared integration utilities."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import shlex
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from devcouncil.executors.agent_registry import (
|
|
19
|
+
CODING_CLI_INTEGRATION_INFO,
|
|
20
|
+
normalize_agent_name,
|
|
21
|
+
)
|
|
22
|
+
from devcouncil.utils.subprocess_env import clean_subprocess_env
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
OPENCODE_HOOK_PLUGIN_NAME = "opencode_devcouncil_plugin.mjs"
|
|
28
|
+
SUPPORTED_HOOK_TOOLS = ("claude", "codex", "cursor", "grok")
|
|
29
|
+
# Clients whose native hook installers wire Stop/SubagentStop handlers.
|
|
30
|
+
STOP_HOOK_TOOLS = frozenset({"claude", "codex"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def seed_stop_gate_assist_if_unset(config: dict) -> None:
|
|
34
|
+
"""Default ``execution.stop_gate.mode`` to ``assist`` when hooks are installed.
|
|
35
|
+
|
|
36
|
+
Code default remains ``off`` (``StopGateConfig.mode``); integrate/first-run setup
|
|
37
|
+
for Stop-hook clients opts into assistive warnings without blocking completion.
|
|
38
|
+
"""
|
|
39
|
+
execution = config.setdefault("execution", {})
|
|
40
|
+
stop_gate = execution.setdefault("stop_gate", {})
|
|
41
|
+
if not isinstance(stop_gate, dict):
|
|
42
|
+
stop_gate = {}
|
|
43
|
+
execution["stop_gate"] = stop_gate
|
|
44
|
+
if not stop_gate.get("mode"):
|
|
45
|
+
stop_gate["mode"] = "assist"
|
|
46
|
+
stop_gate.setdefault("check_claims", True)
|
|
47
|
+
stop_gate.setdefault("verify_active_task", True)
|
|
48
|
+
|
|
49
|
+
# Recorded when hooks are installed so `dev integrate hooks --check` can detect drift
|
|
50
|
+
# between a stale global CLI and the project venv.
|
|
51
|
+
HOOK_DEV_EXECUTABLE_REL = Path(".devcouncil") / "cache" / "hook_dev_executable"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_dev_executable(project_root: Path) -> str:
|
|
55
|
+
"""Resolve the ``dev`` CLI to invoke from hooks (project venv first, then PATH).
|
|
56
|
+
|
|
57
|
+
Preferring the project ``.venv`` avoids a globally installed stale ``dev`` silently
|
|
58
|
+
fighting the working tree over cache versions and map schema.
|
|
59
|
+
"""
|
|
60
|
+
root = project_root.expanduser().resolve()
|
|
61
|
+
candidates: list[Path] = []
|
|
62
|
+
if sys.platform == "win32":
|
|
63
|
+
candidates.extend(
|
|
64
|
+
[
|
|
65
|
+
root / ".venv" / "Scripts" / "dev.exe",
|
|
66
|
+
root / ".venv" / "Scripts" / "devcouncil.exe",
|
|
67
|
+
root / "venv" / "Scripts" / "dev.exe",
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
candidates.extend(
|
|
72
|
+
[
|
|
73
|
+
root / ".venv" / "bin" / "dev",
|
|
74
|
+
root / ".venv" / "bin" / "devcouncil",
|
|
75
|
+
root / "venv" / "bin" / "dev",
|
|
76
|
+
root / "venv" / "bin" / "devcouncil",
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
for candidate in candidates:
|
|
80
|
+
if candidate.is_file() and os.access(candidate, os.X_OK):
|
|
81
|
+
return str(candidate.resolve())
|
|
82
|
+
for name in ("dev", "devcouncil"):
|
|
83
|
+
found = shutil.which(name)
|
|
84
|
+
if found:
|
|
85
|
+
return found
|
|
86
|
+
return "dev"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def record_hook_dev_executable(project_root: Path, executable: str | None = None) -> Path:
|
|
90
|
+
"""Persist the resolved ``dev`` path used in installed hook commands."""
|
|
91
|
+
root = project_root.expanduser().resolve()
|
|
92
|
+
path = root / HOOK_DEV_EXECUTABLE_REL
|
|
93
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
resolved = executable or resolve_dev_executable(root)
|
|
95
|
+
path.write_text(resolved + "\n", encoding="utf-8")
|
|
96
|
+
return path
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def recorded_hook_dev_executable(project_root: Path) -> str | None:
|
|
100
|
+
path = project_root.expanduser().resolve() / HOOK_DEV_EXECUTABLE_REL
|
|
101
|
+
if not path.is_file():
|
|
102
|
+
return None
|
|
103
|
+
text = path.read_text(encoding="utf-8").strip()
|
|
104
|
+
return text or None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def check_hook_dev_executable(project_root: Path) -> tuple[bool, str]:
|
|
108
|
+
"""Compare recorded hook executable against the currently resolved one."""
|
|
109
|
+
root = project_root.expanduser().resolve()
|
|
110
|
+
current = resolve_dev_executable(root)
|
|
111
|
+
recorded = recorded_hook_dev_executable(root)
|
|
112
|
+
if recorded is None:
|
|
113
|
+
return True, f"No recorded hook executable (current: {current})"
|
|
114
|
+
if Path(recorded).resolve() == Path(current).resolve() or recorded == current:
|
|
115
|
+
return True, f"Hook executable matches: {current}"
|
|
116
|
+
return False, f"Hook executable mismatch: recorded={recorded} current={current}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _project_root(path: str | Path | None) -> Path:
|
|
120
|
+
root = Path(path or ".").expanduser().resolve()
|
|
121
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
122
|
+
|
|
123
|
+
set_log_dir(root)
|
|
124
|
+
return root
|
|
125
|
+
|
|
126
|
+
def _warn_if_verify_only(client: str) -> None:
|
|
127
|
+
"""Print a prominent containment warning when wiring a verify-only client.
|
|
128
|
+
|
|
129
|
+
Verify-only clients have no native pre-tool-use hook, so DevCouncil cannot block a
|
|
130
|
+
forbidden write or command before it happens — it is only caught post-hoc at verify
|
|
131
|
+
time. Surface this loudly so users don't assume hard containment."""
|
|
132
|
+
info = CODING_CLI_INTEGRATION_INFO.get(normalize_agent_name(client))
|
|
133
|
+
if info is not None and not info.hooks:
|
|
134
|
+
console.print(
|
|
135
|
+
f"[bold yellow]Warning ({info.label}): No pre-action containment — "
|
|
136
|
+
"forbidden writes/commands are caught only at verify time.[/bold yellow]"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _server_args(project_root: Path) -> list[str]:
|
|
140
|
+
"""Return the exact MCP server command clients should persist.
|
|
141
|
+
|
|
142
|
+
Project integrations must execute the checkout they were generated from. A
|
|
143
|
+
bare ``devcouncil`` command can resolve to an older global installation and
|
|
144
|
+
silently serve a different schema than the working tree.
|
|
145
|
+
"""
|
|
146
|
+
return [resolve_dev_executable(project_root), "mcp-server"]
|
|
147
|
+
|
|
148
|
+
_PENDING_RAW_CONFIG: dict | None = None
|
|
149
|
+
|
|
150
|
+
@contextmanager
|
|
151
|
+
def _batched_raw_config(project_root: Path):
|
|
152
|
+
global _PENDING_RAW_CONFIG
|
|
153
|
+
# Re-entrant: if a batch is already active, participate in it instead of
|
|
154
|
+
# starting a nested load/save (which would otherwise reset the shared
|
|
155
|
+
# buffer to None on inner exit and drop the outer batch's mutations).
|
|
156
|
+
if _PENDING_RAW_CONFIG is not None:
|
|
157
|
+
yield
|
|
158
|
+
return
|
|
159
|
+
_PENDING_RAW_CONFIG = _load_raw_config(project_root)
|
|
160
|
+
try:
|
|
161
|
+
yield
|
|
162
|
+
finally:
|
|
163
|
+
# Persist whatever mutations accumulated, even if an inner installer raised
|
|
164
|
+
# partway through — matching the old per-installer save, which committed each
|
|
165
|
+
# installer's change immediately rather than dropping the whole batch on a
|
|
166
|
+
# mid-loop failure.
|
|
167
|
+
pending = _PENDING_RAW_CONFIG
|
|
168
|
+
_PENDING_RAW_CONFIG = None
|
|
169
|
+
if pending is not None:
|
|
170
|
+
_save_raw_config(project_root, pending)
|
|
171
|
+
|
|
172
|
+
def _mutate_raw_config(project_root: Path, mutate) -> None:
|
|
173
|
+
if _PENDING_RAW_CONFIG is not None:
|
|
174
|
+
mutate(_PENDING_RAW_CONFIG)
|
|
175
|
+
return
|
|
176
|
+
config = _load_raw_config(project_root)
|
|
177
|
+
mutate(config)
|
|
178
|
+
_save_raw_config(project_root, config)
|
|
179
|
+
|
|
180
|
+
def _load_json_strict(path: Path, label: str = "JSON") -> dict:
|
|
181
|
+
if not path.exists():
|
|
182
|
+
return {}
|
|
183
|
+
try:
|
|
184
|
+
return json.loads(path.read_text(encoding="utf-8")) or {}
|
|
185
|
+
except json.JSONDecodeError as exc:
|
|
186
|
+
raise ValueError(f"{path} is not valid JSON. Fix the {label} config before rerunning integration setup.") from exc
|
|
187
|
+
|
|
188
|
+
def _format_command(command: list[str]) -> str:
|
|
189
|
+
if sys.platform == "win32":
|
|
190
|
+
return " ".join(_quote_powershell_arg(arg) for arg in command)
|
|
191
|
+
return shlex.join(command)
|
|
192
|
+
|
|
193
|
+
def _quote_powershell_arg(arg: str) -> str:
|
|
194
|
+
if arg == "":
|
|
195
|
+
return "''"
|
|
196
|
+
special_chars = set(" \t\r\n'\"{}[](),;|&<>")
|
|
197
|
+
if not any(char in special_chars for char in arg):
|
|
198
|
+
return arg
|
|
199
|
+
return "'" + arg.replace("'", "''") + "'"
|
|
200
|
+
|
|
201
|
+
def _probe_mcp_tools(root: Path, *, timeout_seconds: float = 30.0) -> list[str]:
|
|
202
|
+
from mcp import ClientSession, StdioServerParameters
|
|
203
|
+
from mcp.client.stdio import stdio_client
|
|
204
|
+
import asyncio
|
|
205
|
+
import os
|
|
206
|
+
|
|
207
|
+
async def _list_tools() -> list[str]:
|
|
208
|
+
env = os.environ.copy()
|
|
209
|
+
env["DEVCOUNCIL_PROJECT_ROOT"] = str(root)
|
|
210
|
+
command = _server_args(root)
|
|
211
|
+
params = StdioServerParameters(
|
|
212
|
+
command=command[0],
|
|
213
|
+
args=command[1:],
|
|
214
|
+
cwd=str(root),
|
|
215
|
+
env=env,
|
|
216
|
+
)
|
|
217
|
+
async with stdio_client(params) as (read, write):
|
|
218
|
+
async with ClientSession(read, write) as session:
|
|
219
|
+
await session.initialize()
|
|
220
|
+
tools = await session.list_tools()
|
|
221
|
+
return [tool.name for tool in tools.tools]
|
|
222
|
+
|
|
223
|
+
async def _list_tools_with_deadline() -> list[str]:
|
|
224
|
+
# A wedged server process would otherwise block `dev integrate check`
|
|
225
|
+
# indefinitely; the caller treats TimeoutError as a failed probe.
|
|
226
|
+
return await asyncio.wait_for(_list_tools(), timeout=timeout_seconds)
|
|
227
|
+
|
|
228
|
+
return asyncio.run(_list_tools_with_deadline())
|
|
229
|
+
|
|
230
|
+
_CLI_RUN_TIMEOUT_SECONDS = 120
|
|
231
|
+
|
|
232
|
+
def _run(command: list[str]) -> int:
|
|
233
|
+
executable = shutil.which(command[0])
|
|
234
|
+
if not executable:
|
|
235
|
+
return 127
|
|
236
|
+
resolved = [executable, *command[1:]]
|
|
237
|
+
use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
|
|
238
|
+
invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
|
|
239
|
+
try:
|
|
240
|
+
result = subprocess.run(
|
|
241
|
+
invocation,
|
|
242
|
+
text=True,
|
|
243
|
+
shell=use_shell,
|
|
244
|
+
timeout=_CLI_RUN_TIMEOUT_SECONDS,
|
|
245
|
+
env=clean_subprocess_env(),
|
|
246
|
+
)
|
|
247
|
+
except subprocess.TimeoutExpired:
|
|
248
|
+
return 124
|
|
249
|
+
except (FileNotFoundError, OSError):
|
|
250
|
+
return 127
|
|
251
|
+
return result.returncode
|
|
252
|
+
|
|
253
|
+
def _run_capture(command: list[str], timeout: int = 10) -> tuple[int, str]:
|
|
254
|
+
executable = shutil.which(command[0])
|
|
255
|
+
if not executable:
|
|
256
|
+
return 127, f"{command[0]} not found on PATH"
|
|
257
|
+
|
|
258
|
+
resolved = [executable, *command[1:]]
|
|
259
|
+
use_shell = sys.platform == "win32" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
|
|
260
|
+
invocation = subprocess.list2cmdline(resolved) if use_shell else resolved
|
|
261
|
+
try:
|
|
262
|
+
result = subprocess.run(
|
|
263
|
+
invocation,
|
|
264
|
+
capture_output=True,
|
|
265
|
+
text=True,
|
|
266
|
+
encoding="utf-8",
|
|
267
|
+
errors="replace",
|
|
268
|
+
shell=use_shell,
|
|
269
|
+
timeout=timeout,
|
|
270
|
+
env=clean_subprocess_env(),
|
|
271
|
+
)
|
|
272
|
+
except subprocess.TimeoutExpired:
|
|
273
|
+
return 124, "timed out"
|
|
274
|
+
except (FileNotFoundError, OSError) as exc:
|
|
275
|
+
return 127, f"{command[0]} could not be executed: {exc}"
|
|
276
|
+
return result.returncode, (result.stdout + result.stderr).strip()
|
|
277
|
+
|
|
278
|
+
def _config_path(project_root: Path) -> Path:
|
|
279
|
+
return project_root / ".devcouncil" / "config.yaml"
|
|
280
|
+
|
|
281
|
+
def _load_raw_config(project_root: Path) -> dict:
|
|
282
|
+
path = _config_path(project_root)
|
|
283
|
+
if not path.exists():
|
|
284
|
+
return {}
|
|
285
|
+
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
286
|
+
|
|
287
|
+
def _save_raw_config(project_root: Path, config: dict) -> None:
|
|
288
|
+
path = _config_path(project_root)
|
|
289
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
path.write_text(yaml.safe_dump(config, sort_keys=False), encoding="utf-8")
|
|
291
|
+
|
|
292
|
+
def _load_json(path: Path) -> dict:
|
|
293
|
+
if not path.exists():
|
|
294
|
+
return {}
|
|
295
|
+
try:
|
|
296
|
+
return json.loads(path.read_text(encoding="utf-8")) or {}
|
|
297
|
+
except json.JSONDecodeError:
|
|
298
|
+
return {}
|
|
299
|
+
|
|
300
|
+
def _save_json(path: Path, data: dict) -> None:
|
|
301
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
302
|
+
from devcouncil.utils.json_persist import write_json
|
|
303
|
+
|
|
304
|
+
write_json(path, data)
|
|
305
|
+
|
|
306
|
+
def _print_command(tool: str, command: list[str], apply: bool):
|
|
307
|
+
if apply:
|
|
308
|
+
console.print(f"[cyan]Configuring {tool} MCP integration...[/cyan]")
|
|
309
|
+
else:
|
|
310
|
+
console.print(f"[bold]{tool}[/bold]")
|
|
311
|
+
console.print(_format_command(command), soft_wrap=True)
|
|
312
|
+
|
|
313
|
+
def _configure(tool: str, command: list[str], apply: bool) -> bool:
|
|
314
|
+
executable = command[0]
|
|
315
|
+
if not shutil.which(executable):
|
|
316
|
+
console.print(f"[yellow]{tool} CLI not found on PATH. Install it first, then rerun this command.[/yellow]")
|
|
317
|
+
console.print(_format_command(command), soft_wrap=True)
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
_print_command(tool, command, apply)
|
|
321
|
+
if not apply:
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
code = _run(command)
|
|
325
|
+
if code == 0:
|
|
326
|
+
console.print(f"[green]{tool} integration configured.[/green]")
|
|
327
|
+
return True
|
|
328
|
+
|
|
329
|
+
console.print(f"[red]{tool} integration command failed with exit code {code}.[/red]")
|
|
330
|
+
console.print("You can rerun it manually:")
|
|
331
|
+
console.print(_format_command(command), soft_wrap=True)
|
|
332
|
+
return False
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Cursor integration adapter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from devcouncil.executors.agent_registry import resolve_cursor_agent_executable
|
|
10
|
+
from devcouncil.integrations.clients import common as _common
|
|
11
|
+
|
|
12
|
+
_project_root = _common._project_root
|
|
13
|
+
_warn_if_verify_only = _common._warn_if_verify_only
|
|
14
|
+
_server_args = _common._server_args
|
|
15
|
+
_format_command = _common._format_command
|
|
16
|
+
_quote_powershell_arg = _common._quote_powershell_arg
|
|
17
|
+
_run = _common._run
|
|
18
|
+
_run_capture = _common._run_capture
|
|
19
|
+
_config_path = _common._config_path
|
|
20
|
+
_load_raw_config = _common._load_raw_config
|
|
21
|
+
_save_raw_config = _common._save_raw_config
|
|
22
|
+
_load_json = _common._load_json
|
|
23
|
+
_save_json = _common._save_json
|
|
24
|
+
_load_json_strict = _common._load_json_strict
|
|
25
|
+
_mutate_raw_config = _common._mutate_raw_config
|
|
26
|
+
_batched_raw_config = _common._batched_raw_config
|
|
27
|
+
_probe_mcp_tools = _common._probe_mcp_tools
|
|
28
|
+
_print_command = _common._print_command
|
|
29
|
+
_configure = _common._configure
|
|
30
|
+
|
|
31
|
+
console = _common.console
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _cursor_executable() -> str | None:
|
|
35
|
+
return resolve_cursor_agent_executable()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _cursor_config_path(project_root: Path) -> Path:
|
|
39
|
+
return project_root / ".cursor" / "mcp.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _cursor_mcp_command(project_root: Path) -> str:
|
|
43
|
+
"""Prefer the project venv ``devcouncil`` binary for Cursor MCP stdio.
|
|
44
|
+
|
|
45
|
+
Absolute paths survive Cursor sessions that lack the project venv on PATH;
|
|
46
|
+
bare ``devcouncil`` still works when PATH is set. Status checks accept both.
|
|
47
|
+
"""
|
|
48
|
+
from devcouncil.integrations.clients.common import resolve_dev_executable
|
|
49
|
+
|
|
50
|
+
resolved = Path(resolve_dev_executable(project_root))
|
|
51
|
+
name = resolved.name.lower()
|
|
52
|
+
if name in {"dev", "dev.exe"}:
|
|
53
|
+
sibling = resolved.with_name("devcouncil.exe" if name.endswith(".exe") else "devcouncil")
|
|
54
|
+
if sibling.is_file():
|
|
55
|
+
return str(sibling)
|
|
56
|
+
if name in {"devcouncil", "devcouncil.exe"}:
|
|
57
|
+
return str(resolved)
|
|
58
|
+
# Fall back to PATH name when no local binary exists.
|
|
59
|
+
return "devcouncil"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _cursor_mcp_config(project_root: Path) -> dict:
|
|
63
|
+
root = project_root.expanduser().resolve()
|
|
64
|
+
if os.name == "nt":
|
|
65
|
+
venv_bin = root / ".venv" / "Scripts"
|
|
66
|
+
else:
|
|
67
|
+
venv_bin = root / ".venv" / "bin"
|
|
68
|
+
path_prefix = str(venv_bin) if venv_bin.is_dir() else ""
|
|
69
|
+
existing_path = os.environ.get("PATH", "/usr/bin:/bin")
|
|
70
|
+
env_path = f"{path_prefix}{os.pathsep}{existing_path}" if path_prefix else existing_path
|
|
71
|
+
return {
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"devcouncil": {
|
|
74
|
+
"type": "stdio",
|
|
75
|
+
"command": _cursor_mcp_command(root),
|
|
76
|
+
"args": ["mcp-server"],
|
|
77
|
+
"env": {
|
|
78
|
+
"DEVCOUNCIL_PROJECT_ROOT": str(root),
|
|
79
|
+
"PATH": env_path,
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _write_cursor_config(project_root: Path) -> Path:
|
|
87
|
+
path = _cursor_config_path(project_root)
|
|
88
|
+
data = _load_json_strict(path, "Cursor")
|
|
89
|
+
mcp_servers = data.setdefault("mcpServers", {})
|
|
90
|
+
mcp_servers["devcouncil"] = _cursor_mcp_config(project_root)["mcpServers"]["devcouncil"]
|
|
91
|
+
_save_json(path, data)
|
|
92
|
+
return path
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _record_cursor_config(project_root: Path) -> None:
|
|
96
|
+
def mutate(config: dict) -> None:
|
|
97
|
+
cursor = config.setdefault("integrations", {}).setdefault("cursor", {})
|
|
98
|
+
cursor.update({
|
|
99
|
+
"enabled": True,
|
|
100
|
+
"config_path": str(_cursor_config_path(project_root).relative_to(project_root)),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
_mutate_raw_config(project_root, mutate)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def probe_cursor_auth() -> tuple[bool, str]:
|
|
107
|
+
"""Probe Cursor CLI auth via agent status/about."""
|
|
108
|
+
executable = _cursor_executable()
|
|
109
|
+
if not executable:
|
|
110
|
+
return False, "agent/cursor-agent not found on PATH"
|
|
111
|
+
if os.environ.get("CURSOR_API_KEY"):
|
|
112
|
+
return True, "CURSOR_API_KEY set (CI headless auth)"
|
|
113
|
+
for subcommand in ("status", "about"):
|
|
114
|
+
code, output = _run_capture([executable, subcommand])
|
|
115
|
+
if code == 0 and output.strip():
|
|
116
|
+
first = output.strip().splitlines()[0]
|
|
117
|
+
return True, first
|
|
118
|
+
return False, "Run agent login or set CURSOR_API_KEY for CI headless auth"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def probe_cursor_mcp_list() -> tuple[bool, str]:
|
|
122
|
+
executable = _cursor_executable()
|
|
123
|
+
if not executable:
|
|
124
|
+
return False, "agent/cursor-agent not found on PATH"
|
|
125
|
+
code, output = _run_capture([executable, "mcp", "list"])
|
|
126
|
+
if code != 0:
|
|
127
|
+
return False, output.strip() or "agent mcp list failed"
|
|
128
|
+
return True, output.strip().splitlines()[0] if output.strip() else "MCP servers listed"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _configure_cursor(project_root: Path, apply: bool) -> bool:
|
|
132
|
+
path = _cursor_config_path(project_root)
|
|
133
|
+
config = _cursor_mcp_config(project_root)
|
|
134
|
+
executable = _cursor_executable() or "agent"
|
|
135
|
+
if not apply:
|
|
136
|
+
console.print("[bold]Cursor[/bold]")
|
|
137
|
+
console.print(f"Project MCP config file: [dim]{path}[/dim]")
|
|
138
|
+
console.print(json.dumps(config, separators=(",", ":")), soft_wrap=True)
|
|
139
|
+
console.print(f"Verify in Cursor CLI with: [dim]{executable} mcp list[/dim]")
|
|
140
|
+
auth_ok, auth_details = probe_cursor_auth()
|
|
141
|
+
if auth_ok:
|
|
142
|
+
console.print(f"Auth: [green]{auth_details}[/green]")
|
|
143
|
+
else:
|
|
144
|
+
console.print(f"Auth: [yellow]{auth_details}[/yellow]")
|
|
145
|
+
if not os.environ.get("CURSOR_API_KEY"):
|
|
146
|
+
console.print("CI headless: export [dim]CURSOR_API_KEY[/dim] (see Cursor headless docs)")
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
if not shutil.which("cursor") and not _cursor_executable():
|
|
150
|
+
console.print("[yellow]Cursor CLI not found on PATH. Project MCP config will still be available to Cursor.[/yellow]")
|
|
151
|
+
try:
|
|
152
|
+
written = _write_cursor_config(project_root)
|
|
153
|
+
except ValueError as exc:
|
|
154
|
+
console.print(f"[red]{exc}[/red]")
|
|
155
|
+
return False
|
|
156
|
+
_record_cursor_config(project_root)
|
|
157
|
+
console.print(f"[green]Cursor MCP config written:[/green] {written}")
|
|
158
|
+
mcp_ok, mcp_details = probe_cursor_mcp_list()
|
|
159
|
+
if mcp_ok:
|
|
160
|
+
console.print(f"[dim]MCP probe: {mcp_details}[/dim]")
|
|
161
|
+
auth_ok, auth_details = probe_cursor_auth()
|
|
162
|
+
if not auth_ok:
|
|
163
|
+
console.print(f"[yellow]Auth check: {auth_details}[/yellow]")
|
|
164
|
+
return True
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Gemini integration adapter (deprecated).
|
|
2
|
+
|
|
3
|
+
Google replaced Gemini CLI with Antigravity CLI. Prefer ``dev integrate antigravity --apply``.
|
|
4
|
+
Explicit ``dev integrate gemini`` remains for compatibility only.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import warnings
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from devcouncil.executors.agent_registry import GEMINI_DEPRECATION_MESSAGE
|
|
12
|
+
from devcouncil.integrations.clients import common as _common
|
|
13
|
+
|
|
14
|
+
_project_root = _common._project_root
|
|
15
|
+
_warn_if_verify_only = _common._warn_if_verify_only
|
|
16
|
+
_server_args = _common._server_args
|
|
17
|
+
_format_command = _common._format_command
|
|
18
|
+
_quote_powershell_arg = _common._quote_powershell_arg
|
|
19
|
+
_run = _common._run
|
|
20
|
+
_run_capture = _common._run_capture
|
|
21
|
+
_config_path = _common._config_path
|
|
22
|
+
_load_raw_config = _common._load_raw_config
|
|
23
|
+
_save_raw_config = _common._save_raw_config
|
|
24
|
+
_load_json = _common._load_json
|
|
25
|
+
_save_json = _common._save_json
|
|
26
|
+
_load_json_strict = _common._load_json_strict
|
|
27
|
+
_mutate_raw_config = _common._mutate_raw_config
|
|
28
|
+
_batched_raw_config = _common._batched_raw_config
|
|
29
|
+
_probe_mcp_tools = _common._probe_mcp_tools
|
|
30
|
+
_print_command = _common._print_command
|
|
31
|
+
_configure = _common._configure
|
|
32
|
+
|
|
33
|
+
console = _common.console
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _gemini_command(project_root: Path, scope: str) -> list[str]:
|
|
37
|
+
warnings.warn(GEMINI_DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=2)
|
|
38
|
+
console.print(f"[yellow]{GEMINI_DEPRECATION_MESSAGE}[/yellow]")
|
|
39
|
+
return [
|
|
40
|
+
"gemini",
|
|
41
|
+
"mcp",
|
|
42
|
+
"add",
|
|
43
|
+
"--scope",
|
|
44
|
+
scope,
|
|
45
|
+
"--env",
|
|
46
|
+
f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
|
|
47
|
+
"devcouncil",
|
|
48
|
+
*_server_args(project_root),
|
|
49
|
+
]
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Grok Build integration adapter."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from devcouncil.integrations.clients import common as _common
|
|
8
|
+
|
|
9
|
+
_project_root = _common._project_root
|
|
10
|
+
_format_command = _common._format_command
|
|
11
|
+
_run = _common._run
|
|
12
|
+
_mutate_raw_config = _common._mutate_raw_config
|
|
13
|
+
_configure = _common._configure
|
|
14
|
+
|
|
15
|
+
console = _common.console
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _grok_config_path(project_root: Path) -> Path:
|
|
19
|
+
return project_root / ".grok" / "config.toml"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _grok_mcp_toml_snippet(project_root: Path) -> str:
|
|
23
|
+
root = str(project_root)
|
|
24
|
+
return (
|
|
25
|
+
"[mcp_servers.devcouncil]\n"
|
|
26
|
+
'command = "devcouncil"\n'
|
|
27
|
+
'args = ["mcp-server"]\n'
|
|
28
|
+
f'env = {{ DEVCOUNCIL_PROJECT_ROOT = "{root}" }}\n'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _grok_mcp_command(project_root: Path) -> list[str]:
|
|
33
|
+
return [
|
|
34
|
+
"grok",
|
|
35
|
+
"mcp",
|
|
36
|
+
"add",
|
|
37
|
+
"devcouncil",
|
|
38
|
+
"--scope",
|
|
39
|
+
"project",
|
|
40
|
+
"-e",
|
|
41
|
+
f"DEVCOUNCIL_PROJECT_ROOT={project_root}",
|
|
42
|
+
"--",
|
|
43
|
+
"devcouncil",
|
|
44
|
+
"mcp-server",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _merge_grok_config_toml(project_root: Path) -> Path:
|
|
49
|
+
path = _grok_config_path(project_root)
|
|
50
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
snippet = _grok_mcp_toml_snippet(project_root)
|
|
52
|
+
existing = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
53
|
+
if "mcp_servers.devcouncil" in existing or "[mcp_servers.devcouncil]" in existing:
|
|
54
|
+
return path
|
|
55
|
+
separator = "\n" if existing and not existing.endswith("\n") else ""
|
|
56
|
+
path.write_text(f"{existing}{separator}\n{snippet}", encoding="utf-8")
|
|
57
|
+
return path
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _record_grok_config(project_root: Path) -> None:
|
|
61
|
+
def mutate(config: dict) -> None:
|
|
62
|
+
grok = config.setdefault("integrations", {}).setdefault("grok", {})
|
|
63
|
+
grok.update({
|
|
64
|
+
"enabled": True,
|
|
65
|
+
"config_path": str(_grok_config_path(project_root).relative_to(project_root)),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
_mutate_raw_config(project_root, mutate)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _configure_grok(project_root: Path, apply: bool) -> bool:
|
|
72
|
+
path = _grok_config_path(project_root)
|
|
73
|
+
cli_command = _grok_mcp_command(project_root)
|
|
74
|
+
if not apply:
|
|
75
|
+
console.print("[bold]Grok Build[/bold]")
|
|
76
|
+
console.print(f"Preferred MCP registration: [dim]{_format_command(cli_command)}[/dim]")
|
|
77
|
+
console.print(f"Fallback project MCP config: [dim]{path}[/dim]")
|
|
78
|
+
console.print(_grok_mcp_toml_snippet(project_root), soft_wrap=True)
|
|
79
|
+
console.print(
|
|
80
|
+
"Project hooks require trust — run [dim]/hooks-trust[/dim] in Grok after install."
|
|
81
|
+
)
|
|
82
|
+
console.print("Verify with: [dim]grok mcp list --json[/dim]")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
if shutil.which("grok"):
|
|
86
|
+
code = _run(cli_command)
|
|
87
|
+
if code == 0:
|
|
88
|
+
_record_grok_config(project_root)
|
|
89
|
+
console.print("[green]Grok MCP server registered via grok mcp add.[/green]")
|
|
90
|
+
return True
|
|
91
|
+
console.print(
|
|
92
|
+
"[yellow]grok mcp add failed; falling back to .grok/config.toml merge.[/yellow]"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
written = _merge_grok_config_toml(project_root)
|
|
97
|
+
except OSError as exc:
|
|
98
|
+
console.print(f"[red]Failed to write Grok MCP config: {exc}[/red]")
|
|
99
|
+
return False
|
|
100
|
+
_record_grok_config(project_root)
|
|
101
|
+
console.print(f"[green]Grok MCP config written:[/green] {written}")
|
|
102
|
+
console.print(
|
|
103
|
+
"[dim]Trust project hooks with /hooks-trust (or grok --trust) before pre-action gates run.[/dim]"
|
|
104
|
+
)
|
|
105
|
+
return True
|