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
|
@@ -10,9 +10,11 @@ from the stated intent and runs them as evidence.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
-
import
|
|
13
|
+
from devcouncil.utils.json_persist import dump_json
|
|
14
14
|
import logging
|
|
15
|
-
import
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
from datetime import datetime
|
|
16
18
|
from pathlib import Path
|
|
17
19
|
|
|
18
20
|
import typer
|
|
@@ -27,8 +29,14 @@ from devcouncil.integrations.github_intent import resolve_goal_intent
|
|
|
27
29
|
from devcouncil.llm.provider import ProviderRequestError, create_provider, validate_model_provider
|
|
28
30
|
from devcouncil.llm.router import ModelRouter, StructuredOutputError
|
|
29
31
|
from devcouncil.verification.ad_hoc_check import AdHocCheckResult, run_working_tree_check
|
|
32
|
+
from devcouncil.verification.gate_cache import GateResultCache
|
|
33
|
+
from devcouncil.verification.incremental_check import (
|
|
34
|
+
run_incremental_gates,
|
|
35
|
+
selected_gate_specs,
|
|
36
|
+
)
|
|
30
37
|
from devcouncil.verification.implementation_reviewer import ImplementationReviewer
|
|
31
38
|
from devcouncil.verification.verifier import Verifier
|
|
39
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
32
40
|
|
|
33
41
|
console = Console()
|
|
34
42
|
logger = logging.getLogger(__name__)
|
|
@@ -37,12 +45,9 @@ logger = logging.getLogger(__name__)
|
|
|
37
45
|
def _diff(root: Path, base: str | None) -> str:
|
|
38
46
|
if not base:
|
|
39
47
|
return Verifier(root).get_diff()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
except Exception:
|
|
45
|
-
return ""
|
|
48
|
+
from devcouncil.utils.proc import git_output
|
|
49
|
+
|
|
50
|
+
return git_output(["diff", base, "--"], cwd=root, default="")
|
|
46
51
|
|
|
47
52
|
|
|
48
53
|
def check(
|
|
@@ -50,8 +55,11 @@ def check(
|
|
|
50
55
|
base: str | None = typer.Option(None, "--base", help="Diff against this git ref instead of the uncommitted working tree."),
|
|
51
56
|
test_commands: list[str] | None = typer.Option(None, "--test", "-t", help="A verification command proving the change works (repeatable). Switches to the deterministic evidence gate."),
|
|
52
57
|
verify: bool = typer.Option(False, "--verify", help="Run the deterministic evidence gate (orphan-diff, acceptance evidence, diff↔coverage, next actions) instead of the LLM audit. No provider keys needed."),
|
|
58
|
+
watch: bool = typer.Option(False, "--watch", help="Keep watching the project tree and re-run the deterministic evidence gate on every change (implies --verify). Prints one compact verdict line per run; Ctrl-C to stop."),
|
|
59
|
+
list_gates: bool = typer.Option(False, "--list-gates", help="Preview which incremental stack gates would run for the current working-tree changes (no execution)."),
|
|
53
60
|
enforce_coverage: bool = typer.Option(False, "--enforce-coverage", help="Evidence gate: block when the tests do not exercise the changed lines."),
|
|
54
61
|
min_coverage: float = typer.Option(0.0, "--min-coverage", help="Evidence gate: minimum fraction of changed lines that must be exercised (implies --enforce-coverage)."),
|
|
62
|
+
persist: bool = typer.Option(False, "--persist", help="Evidence gate: persist gaps/evidence for dev report (typical in CI)."),
|
|
55
63
|
json_format: bool = typer.Option(False, "--json", help="Machine-readable output."),
|
|
56
64
|
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
|
|
57
65
|
):
|
|
@@ -59,8 +67,66 @@ def check(
|
|
|
59
67
|
root = project_root.expanduser().resolve()
|
|
60
68
|
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
61
69
|
set_log_dir(root)
|
|
70
|
+
logger.info(
|
|
71
|
+
"dev check: verify=%s goal=%s base=%s watch=%s list_gates=%s",
|
|
72
|
+
verify, bool(goal), base or "working-tree", watch, list_gates,
|
|
73
|
+
)
|
|
62
74
|
initialize_project(root, quiet=True)
|
|
63
75
|
|
|
76
|
+
with log_stage("check", project_root=root, verify=verify):
|
|
77
|
+
if list_gates:
|
|
78
|
+
_list_gates(root, test_commands, json_format)
|
|
79
|
+
return
|
|
80
|
+
_run_check_body(
|
|
81
|
+
root, goal, base, test_commands, verify, enforce_coverage,
|
|
82
|
+
min_coverage, json_format, watch, persist,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _list_gates(root: Path, test_commands: list[str] | None, json_format: bool) -> None:
|
|
87
|
+
"""Dry-run: show incremental gates for the current working-tree diff."""
|
|
88
|
+
from devcouncil.verification.incremental_check import (
|
|
89
|
+
_default_changed_files,
|
|
90
|
+
_default_commands,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
changed = _default_changed_files(root)
|
|
94
|
+
cmds = _default_commands(root)
|
|
95
|
+
if test_commands:
|
|
96
|
+
cmds = {**cmds, "test": list(cmds.get("test") or []) + list(test_commands)}
|
|
97
|
+
gates = selected_gate_specs(root, changed, commands=cmds, narrow=True)
|
|
98
|
+
payload = {
|
|
99
|
+
"changed_files": changed,
|
|
100
|
+
"gates": [
|
|
101
|
+
{
|
|
102
|
+
"name": g.name,
|
|
103
|
+
"kind": g.kind,
|
|
104
|
+
"command": list(g.command),
|
|
105
|
+
"narrowed": g.narrowed,
|
|
106
|
+
}
|
|
107
|
+
for g in gates
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
if json_format:
|
|
111
|
+
typer.echo(dump_json(payload, indent=2))
|
|
112
|
+
return
|
|
113
|
+
if not changed:
|
|
114
|
+
console.print("[dim]No working-tree changes — no gates selected.[/dim]")
|
|
115
|
+
return
|
|
116
|
+
if not gates:
|
|
117
|
+
console.print(f"[dim]{len(changed)} changed file(s), no matching stack gates.[/dim]")
|
|
118
|
+
return
|
|
119
|
+
console.print(f"[bold]{len(gates)}[/bold] gate(s) for {len(changed)} changed file(s):")
|
|
120
|
+
for g in gates:
|
|
121
|
+
narrow = " [narrowed]" if g.narrowed else ""
|
|
122
|
+
console.print(f" • {g.kind}/{g.name}{narrow}: {' '.join(g.command)}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _run_check_body(
|
|
126
|
+
root, goal, base, test_commands, verify, enforce_coverage, min_coverage, json_format, watch, persist,
|
|
127
|
+
):
|
|
128
|
+
log_step("check/1: resolving diff scope", project_root=root, trace=True)
|
|
129
|
+
|
|
64
130
|
# A --goal of "#142" or a GitHub issue/PR URL is a reference, not a spec —
|
|
65
131
|
# expand it into the issue/PR title + body so acceptance checks and the review
|
|
66
132
|
# are grounded in the real intent (same behavior as `dev go`).
|
|
@@ -70,6 +136,20 @@ def check(
|
|
|
70
136
|
console.print(f"[dim]{intent_note}[/dim]")
|
|
71
137
|
goal = expanded_goal
|
|
72
138
|
|
|
139
|
+
# Watch mode: run the deterministic gate, then re-run it whenever the project tree
|
|
140
|
+
# changes, printing one compact verdict line per run. Implies --verify — looping the
|
|
141
|
+
# LLM audit would burn provider tokens on every save.
|
|
142
|
+
if watch:
|
|
143
|
+
if json_format:
|
|
144
|
+
raise typer.BadParameter("--watch is interactive; it cannot be combined with --json.")
|
|
145
|
+
_watch_gate(
|
|
146
|
+
root, goal,
|
|
147
|
+
test_commands=list(test_commands or []),
|
|
148
|
+
enforce_coverage=enforce_coverage,
|
|
149
|
+
min_coverage=min_coverage,
|
|
150
|
+
)
|
|
151
|
+
return
|
|
152
|
+
|
|
73
153
|
# Evidence-gate mode: deterministic verification of the working-tree diff against an
|
|
74
154
|
# inline requirement (--goal), with the diff↔coverage gate and the typed next-actions
|
|
75
155
|
# contract. Provider-key-free — this is the lite path that lets you taste the gate.
|
|
@@ -77,16 +157,19 @@ def check(
|
|
|
77
157
|
result = run_working_tree_check(
|
|
78
158
|
root,
|
|
79
159
|
goal,
|
|
160
|
+
base=base,
|
|
80
161
|
test_commands=list(test_commands or []),
|
|
81
162
|
enforce_coverage=enforce_coverage,
|
|
82
163
|
min_ratio=min_coverage,
|
|
164
|
+
persist=persist,
|
|
83
165
|
)
|
|
84
166
|
if json_format:
|
|
85
|
-
typer.echo(
|
|
167
|
+
typer.echo(dump_json(result.to_dict(), indent=2))
|
|
86
168
|
else:
|
|
87
169
|
_render_gate(result)
|
|
88
170
|
raise typer.Exit(code=0 if result.passed else 1)
|
|
89
171
|
|
|
172
|
+
log_step("check/2: running LLM audit", project_root=root)
|
|
90
173
|
verifier = Verifier(root)
|
|
91
174
|
|
|
92
175
|
logger.info("dev check (LLM audit): base=%s goal=%s", base or "working-tree", "set" if goal else "none")
|
|
@@ -94,7 +177,7 @@ def check(
|
|
|
94
177
|
if not diff.strip():
|
|
95
178
|
logger.info("dev check: clean working tree; nothing to audit")
|
|
96
179
|
msg = "No changes to check (clean working tree)."
|
|
97
|
-
typer.echo(
|
|
180
|
+
typer.echo(dump_json({"ok": True, "message": msg}) if json_format else msg)
|
|
98
181
|
return
|
|
99
182
|
|
|
100
183
|
changed_files = verifier.get_changed_files()
|
|
@@ -136,7 +219,7 @@ def check(
|
|
|
136
219
|
logger.info("dev check audit complete: %d secret finding(s), %d review finding(s)", len(secret_gaps), len(findings))
|
|
137
220
|
|
|
138
221
|
if json_format:
|
|
139
|
-
typer.echo(
|
|
222
|
+
typer.echo(dump_json({
|
|
140
223
|
"ok": not secret_gaps,
|
|
141
224
|
"changed_files": changed_files,
|
|
142
225
|
"secret_findings": [g.model_dump() for g in secret_gaps],
|
|
@@ -180,12 +263,15 @@ def check(
|
|
|
180
263
|
"[dim]Tip: `dev check --verify --test \"<cmd>\"` runs the deterministic evidence gate "
|
|
181
264
|
"(no provider keys).[/dim]"
|
|
182
265
|
)
|
|
266
|
+
log_step("check/complete", project_root=root, trace=True)
|
|
183
267
|
|
|
184
268
|
|
|
185
269
|
def _render_gate(result: AdHocCheckResult) -> None:
|
|
186
270
|
"""Render the deterministic evidence-gate result for humans."""
|
|
187
271
|
if result.reason == "no_changes":
|
|
188
|
-
console.print(
|
|
272
|
+
console.print(
|
|
273
|
+
"[yellow]No changes to verify. Make a change or pass --base for PR-scoped diff.[/yellow]"
|
|
274
|
+
)
|
|
189
275
|
return
|
|
190
276
|
|
|
191
277
|
console.print(f"[bold]Checking:[/bold] {result.requirement}")
|
|
@@ -218,3 +304,130 @@ def _render_gate(result: AdHocCheckResult) -> None:
|
|
|
218
304
|
console.print("\n[green]Verified with non-blocking signals only.[/green]")
|
|
219
305
|
else:
|
|
220
306
|
console.print("\n[red]Not verified: blocking gaps must be resolved.[/red]")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# --watch plumbing. execution/fs_watcher.FilesystemWatcher was considered and rejected:
|
|
310
|
+
# it exists to ATTRIBUTE changes to a leased task (needs a task_id; records file-change
|
|
311
|
+
# events and orphan-diff gaps in the DB through the policy engine), not to signal
|
|
312
|
+
# "something changed". A 2s mtime scan plus a short settle debounce is the genuinely
|
|
313
|
+
# simpler fit here — no watchdog dependency, identical behavior on every platform.
|
|
314
|
+
_WATCH_IGNORED_DIRS = frozenset({
|
|
315
|
+
".git", ".hg", ".svn", ".devcouncil", "__pycache__", ".pytest_cache", ".mypy_cache",
|
|
316
|
+
".ruff_cache", "node_modules", "dist", "build", "target", ".venv", "venv", ".tox",
|
|
317
|
+
".idea", ".vscode",
|
|
318
|
+
})
|
|
319
|
+
_WATCH_POLL_SECONDS = 2.0
|
|
320
|
+
_WATCH_DEBOUNCE_SECONDS = 1.5
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _watch_snapshot(root: Path) -> dict:
|
|
324
|
+
"""One cheap pass over the tree: ``{path: mtime_ns}`` for non-ignored files."""
|
|
325
|
+
snapshot: dict = {}
|
|
326
|
+
stack = [str(root)]
|
|
327
|
+
while stack:
|
|
328
|
+
directory = stack.pop()
|
|
329
|
+
try:
|
|
330
|
+
with os.scandir(directory) as entries:
|
|
331
|
+
for entry in entries:
|
|
332
|
+
try:
|
|
333
|
+
if entry.is_dir(follow_symlinks=False):
|
|
334
|
+
if entry.name not in _WATCH_IGNORED_DIRS:
|
|
335
|
+
stack.append(entry.path)
|
|
336
|
+
elif entry.is_file(follow_symlinks=False):
|
|
337
|
+
snapshot[entry.path] = entry.stat(follow_symlinks=False).st_mtime_ns
|
|
338
|
+
except OSError:
|
|
339
|
+
continue
|
|
340
|
+
except OSError:
|
|
341
|
+
continue
|
|
342
|
+
return snapshot
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _watch_evidence_once(root: Path, goal, test_commands, enforce_coverage, min_coverage, stamp) -> None:
|
|
346
|
+
"""Full deterministic evidence gate — the fallback when no stack gate applies."""
|
|
347
|
+
try:
|
|
348
|
+
result = run_working_tree_check(
|
|
349
|
+
root, goal,
|
|
350
|
+
test_commands=test_commands,
|
|
351
|
+
enforce_coverage=enforce_coverage,
|
|
352
|
+
min_ratio=min_coverage,
|
|
353
|
+
)
|
|
354
|
+
except Exception as exc: # keep watching through transient failures (git hiccups, ...)
|
|
355
|
+
logger.warning("dev check --watch: run failed: %s", exc)
|
|
356
|
+
console.print(f"[dim]{stamp}[/dim] [red]ERROR[/red] — {str(exc)[:120]}")
|
|
357
|
+
return
|
|
358
|
+
verdict = "[green]PASS[/green]" if result.passed else "[red]FAIL[/red]"
|
|
359
|
+
blocking = len([g for g in result.gaps if g.blocking])
|
|
360
|
+
detail = f"{len(result.gaps)} gap(s)" + (f", {blocking} blocking" if blocking else "")
|
|
361
|
+
if result.reason == "no_changes":
|
|
362
|
+
detail = "no working-tree changes"
|
|
363
|
+
console.print(f"[dim]{stamp}[/dim] {verdict} — {detail} [dim](evidence gate)[/dim]")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _watch_gate_once(root: Path, goal, test_commands, enforce_coverage, min_coverage, cache) -> None:
|
|
367
|
+
"""Run the incremental gate once and print a compact one-line verdict.
|
|
368
|
+
|
|
369
|
+
Only the gates whose inputs actually changed are executed; the rest are served from
|
|
370
|
+
the content-hash cache, so iterative edits stay sub-second. When no stack-relevant
|
|
371
|
+
gate is configured for the change, fall back to the full evidence gate so the user
|
|
372
|
+
still gets orphan/secret/coverage feedback."""
|
|
373
|
+
stamp = datetime.now().strftime("%H:%M:%S")
|
|
374
|
+
try:
|
|
375
|
+
result = run_incremental_gates(
|
|
376
|
+
root, extra_test_commands=test_commands, cache=cache,
|
|
377
|
+
)
|
|
378
|
+
except Exception as exc: # keep watching through transient failures (git hiccups, ...)
|
|
379
|
+
logger.warning("dev check --watch: incremental run failed: %s", exc)
|
|
380
|
+
console.print(f"[dim]{stamp}[/dim] [red]ERROR[/red] — {str(exc)[:120]}")
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
if result.no_changes:
|
|
384
|
+
console.print(f"[dim]{stamp}[/dim] [dim]— no working-tree changes[/dim]")
|
|
385
|
+
return
|
|
386
|
+
if result.no_gates:
|
|
387
|
+
# No configured command targets the changed stack(s); give the full picture.
|
|
388
|
+
_watch_evidence_once(root, goal, test_commands, enforce_coverage, min_coverage, stamp)
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
verdict = "[green]PASS[/green]" if result.passed else "[red]FAIL[/red]"
|
|
392
|
+
ran, cached = len(result.ran), len(result.cached)
|
|
393
|
+
detail = f"{ran} gate(s) run, {cached} cached"
|
|
394
|
+
failing = [o for o in result.outcomes if not o.passed]
|
|
395
|
+
if failing:
|
|
396
|
+
detail += " — failed: " + ", ".join(o.kind for o in failing[:4])
|
|
397
|
+
ms = result.duration_s * 1000
|
|
398
|
+
console.print(f"[dim]{stamp}[/dim] {verdict} — {detail} [dim]({ms:.0f}ms)[/dim]")
|
|
399
|
+
if result.narrowed:
|
|
400
|
+
logger.info("dev check --watch: narrowed — full verify recommended before commit")
|
|
401
|
+
console.print(
|
|
402
|
+
"[dim]narrowed — full verify recommended before commit[/dim]"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _watch_gate(root: Path, goal, *, test_commands, enforce_coverage, min_coverage) -> None:
|
|
407
|
+
"""Initial gate run, then a poll/debounce loop re-running it until Ctrl-C.
|
|
408
|
+
|
|
409
|
+
A single :class:`GateResultCache` is shared across iterations so a gate whose inputs
|
|
410
|
+
did not change between saves is skipped rather than re-run."""
|
|
411
|
+
console.print(
|
|
412
|
+
f"[dim]Watching {root} — re-running only the gates affected by each change "
|
|
413
|
+
f"(poll {_WATCH_POLL_SECONDS:.0f}s, debounce {_WATCH_DEBOUNCE_SECONDS:.1f}s). "
|
|
414
|
+
"Ctrl-C to stop. Run without --watch for full findings.[/dim]"
|
|
415
|
+
)
|
|
416
|
+
cache = GateResultCache(root)
|
|
417
|
+
try:
|
|
418
|
+
_watch_gate_once(root, goal, test_commands, enforce_coverage, min_coverage, cache)
|
|
419
|
+
baseline = _watch_snapshot(root)
|
|
420
|
+
while True:
|
|
421
|
+
time.sleep(_WATCH_POLL_SECONDS)
|
|
422
|
+
current = _watch_snapshot(root)
|
|
423
|
+
if current == baseline:
|
|
424
|
+
continue
|
|
425
|
+
# Debounce: let an editor save-storm settle before burning a run.
|
|
426
|
+
time.sleep(_WATCH_DEBOUNCE_SECONDS)
|
|
427
|
+
_watch_gate_once(root, goal, test_commands, enforce_coverage, min_coverage, cache)
|
|
428
|
+
# Re-baseline AFTER the run so artifacts the gate itself writes (coverage
|
|
429
|
+
# data, logs outside .devcouncil/) can never re-trigger it. Trade-off:
|
|
430
|
+
# edits made while the gate was running are absorbed — save again.
|
|
431
|
+
baseline = _watch_snapshot(root)
|
|
432
|
+
except KeyboardInterrupt:
|
|
433
|
+
console.print("\n[dim]Watch stopped.[/dim]")
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import typer
|
|
3
|
+
from typing import cast
|
|
2
4
|
import yaml
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from rich.console import Console
|
|
@@ -10,9 +12,143 @@ from devcouncil.llm.provider import (
|
|
|
10
12
|
build_role_model_config,
|
|
11
13
|
validate_model_provider,
|
|
12
14
|
)
|
|
15
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
13
16
|
|
|
14
17
|
app = typer.Typer(help="Manage DevCouncil configuration")
|
|
15
18
|
console = Console()
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
_CONFIG_SETTABLE_KEYS = {
|
|
22
|
+
"execution.default_executor": ("execution", "default_executor", str),
|
|
23
|
+
"execution.max_repair_attempts": ("execution", "max_repair_attempts", int),
|
|
24
|
+
"execution.command_timeout": ("execution", "command_timeout", int),
|
|
25
|
+
"execution.verify_on_post_task": ("execution", "verify_on_post_task", bool),
|
|
26
|
+
"execution.stop_gate.mode": ("execution", "stop_gate", "mode", str),
|
|
27
|
+
"execution.stop_gate.check_claims": ("execution", "stop_gate", "check_claims", bool),
|
|
28
|
+
"execution.stop_gate.verify_active_task": ("execution", "stop_gate", "verify_active_task", bool),
|
|
29
|
+
"execution.stop_gate.max_blocks": ("execution", "stop_gate", "max_blocks", int),
|
|
30
|
+
"verification.diff_coverage.enforce": ("verification", "diff_coverage", "enforce", bool),
|
|
31
|
+
"semantic_layer.enabled": ("semantic_layer", "enabled", bool),
|
|
32
|
+
"semantic_layer.cache.enabled": ("semantic_layer", "cache", "enabled", bool),
|
|
33
|
+
"semantic_layer.router.enabled": ("semantic_layer", "router", "enabled", bool),
|
|
34
|
+
"semantic_layer.compressor.enabled": ("semantic_layer", "compressor", "enabled", bool),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_config_value(raw: str, typ: type) -> object:
|
|
39
|
+
if typ is bool:
|
|
40
|
+
normalized = raw.strip().lower()
|
|
41
|
+
if normalized in {"true", "1", "yes", "on"}:
|
|
42
|
+
return True
|
|
43
|
+
if normalized in {"false", "0", "no", "off"}:
|
|
44
|
+
return False
|
|
45
|
+
raise ValueError(f"Expected boolean (true/false), got {raw!r}")
|
|
46
|
+
if typ is int:
|
|
47
|
+
return int(raw)
|
|
48
|
+
return raw
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _nested_get(raw: dict, path: tuple[str, ...]) -> object | None:
|
|
52
|
+
current: object = raw
|
|
53
|
+
for key in path:
|
|
54
|
+
if not isinstance(current, dict):
|
|
55
|
+
return None
|
|
56
|
+
current = current.get(key)
|
|
57
|
+
return current
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _nested_set(raw: dict, path: tuple[str, ...], value: object) -> None:
|
|
61
|
+
current = raw
|
|
62
|
+
for key in path[:-1]:
|
|
63
|
+
if key not in current or not isinstance(current[key], dict):
|
|
64
|
+
current[key] = {}
|
|
65
|
+
current = current[key]
|
|
66
|
+
current[path[-1]] = value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command("show")
|
|
70
|
+
def show_config(
|
|
71
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
72
|
+
):
|
|
73
|
+
"""Display key DevCouncil settings."""
|
|
74
|
+
root = project_root.expanduser().resolve()
|
|
75
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
76
|
+
set_log_dir(root)
|
|
77
|
+
|
|
78
|
+
with log_stage("config", project_root=root, subcommand="show"):
|
|
79
|
+
try:
|
|
80
|
+
cfg = load_config(root)
|
|
81
|
+
except FileNotFoundError as e:
|
|
82
|
+
console.print(f"[red]{e}[/red]")
|
|
83
|
+
raise typer.Exit(code=1) from e
|
|
84
|
+
|
|
85
|
+
console.print("[bold]DevCouncil Settings[/bold]")
|
|
86
|
+
console.print(f" [cyan]execution.default_executor[/cyan]: {cfg.execution.default_executor}")
|
|
87
|
+
console.print(f" [cyan]execution.max_repair_attempts[/cyan]: {cfg.execution.max_repair_attempts}")
|
|
88
|
+
console.print(f" [cyan]execution.command_timeout[/cyan]: {cfg.execution.command_timeout}")
|
|
89
|
+
console.print(f" [cyan]execution.verify_on_post_task[/cyan]: {cfg.execution.verify_on_post_task} [dim](deprecated alias)[/dim]")
|
|
90
|
+
sg = cfg.execution.stop_gate
|
|
91
|
+
console.print(f" [cyan]execution.stop_gate.mode[/cyan]: {sg.mode}")
|
|
92
|
+
console.print(f" [cyan]execution.stop_gate.check_claims[/cyan]: {sg.check_claims}")
|
|
93
|
+
console.print(f" [cyan]execution.stop_gate.verify_active_task[/cyan]: {sg.verify_active_task}")
|
|
94
|
+
console.print(f" [cyan]execution.stop_gate.max_blocks[/cyan]: {sg.max_blocks}")
|
|
95
|
+
console.print(f" [cyan]verification.diff_coverage.enforce[/cyan]: {cfg.verification.diff_coverage.enforce}")
|
|
96
|
+
console.print(f" [cyan]verification.rigor.enabled[/cyan]: {cfg.verification.rigor.enabled}")
|
|
97
|
+
console.print(f" [cyan]verification.rigor.stub_detection[/cyan]: {cfg.verification.rigor.stub_detection}")
|
|
98
|
+
console.print(f" [cyan]gates.block_orphan_diffs[/cyan]: {cfg.gates.block_orphan_diffs}")
|
|
99
|
+
console.print(f" [cyan]semantic_layer.enabled[/cyan]: {cfg.semantic_layer.enabled}")
|
|
100
|
+
if cfg.semantic_layer.enabled:
|
|
101
|
+
console.print(f" [cyan]semantic_layer.cache.enabled[/cyan]: {cfg.semantic_layer.cache.enabled}")
|
|
102
|
+
console.print(f" [cyan]semantic_layer.router.enabled[/cyan]: {cfg.semantic_layer.router.enabled}")
|
|
103
|
+
console.print(f" [cyan]semantic_layer.compressor.enabled[/cyan]: {cfg.semantic_layer.compressor.enabled}")
|
|
104
|
+
console.print(
|
|
105
|
+
f" [cyan]semantic_layer.embedding.model_name[/cyan]: {cfg.semantic_layer.embedding.model_name}"
|
|
106
|
+
)
|
|
107
|
+
console.print(
|
|
108
|
+
" [dim]Enable: dev config set semantic_layer.enabled true "
|
|
109
|
+
"(requires uv sync --group semantic). "
|
|
110
|
+
"Check: dev doctor.[/dim]"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command("set")
|
|
115
|
+
def set_config(
|
|
116
|
+
key: str = typer.Argument(..., help="Dotted config key (e.g. execution.command_timeout)."),
|
|
117
|
+
value: str = typer.Argument(..., help="New value."),
|
|
118
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
119
|
+
):
|
|
120
|
+
"""Set a common DevCouncil config key."""
|
|
121
|
+
root = project_root.expanduser().resolve()
|
|
122
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
123
|
+
set_log_dir(root)
|
|
124
|
+
|
|
125
|
+
mapping = _CONFIG_SETTABLE_KEYS.get(key)
|
|
126
|
+
if mapping is None:
|
|
127
|
+
supported = ", ".join(sorted(_CONFIG_SETTABLE_KEYS))
|
|
128
|
+
console.print(f"[red]Unsupported key {key!r}. Supported: {supported}[/red]")
|
|
129
|
+
raise typer.Exit(code=2)
|
|
130
|
+
|
|
131
|
+
config_path = root / ".devcouncil" / "config.yaml"
|
|
132
|
+
if not config_path.exists():
|
|
133
|
+
console.print(f"[red]Config not found at {config_path}[/red]")
|
|
134
|
+
raise typer.Exit(code=1)
|
|
135
|
+
|
|
136
|
+
typ = cast(type, mapping[-1])
|
|
137
|
+
path = cast(tuple[str, ...], mapping[:-1])
|
|
138
|
+
try:
|
|
139
|
+
parsed = _parse_config_value(value, typ)
|
|
140
|
+
except ValueError as exc:
|
|
141
|
+
console.print(f"[red]{exc}[/red]")
|
|
142
|
+
raise typer.Exit(code=2) from exc
|
|
143
|
+
|
|
144
|
+
with log_stage("config", project_root=root, subcommand="set"):
|
|
145
|
+
with open(config_path) as f:
|
|
146
|
+
raw_config = yaml.safe_load(f) or {}
|
|
147
|
+
_nested_set(raw_config, path, parsed)
|
|
148
|
+
with open(config_path, "w") as f:
|
|
149
|
+
yaml.dump(raw_config, f, default_flow_style=False)
|
|
150
|
+
console.print(f"[green]Updated {key} = {parsed!r}[/green]")
|
|
151
|
+
|
|
16
152
|
|
|
17
153
|
@app.command("models")
|
|
18
154
|
def models(
|
|
@@ -28,88 +164,99 @@ def models(
|
|
|
28
164
|
):
|
|
29
165
|
"""View or edit model role configuration."""
|
|
30
166
|
root = project_root.expanduser().resolve()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
console.print(f"[red]{e}[/red]")
|
|
35
|
-
return
|
|
167
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
168
|
+
set_log_dir(root)
|
|
169
|
+
logger.info("dev config models: role=%s provider=%s", role, provider)
|
|
36
170
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
171
|
+
with log_stage("config", project_root=root, subcommand="models"):
|
|
172
|
+
log_step("config/1: loading configuration", project_root=root, trace=True)
|
|
173
|
+
try:
|
|
174
|
+
load_config(root)
|
|
175
|
+
except FileNotFoundError as e:
|
|
176
|
+
console.print(f"[red]{e}[/red]")
|
|
177
|
+
return
|
|
41
178
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
raise typer.Exit(code=2) from e
|
|
179
|
+
config_path = root / ".devcouncil" / "config.yaml"
|
|
180
|
+
|
|
181
|
+
with open(config_path) as f:
|
|
182
|
+
raw_config = yaml.safe_load(f) or {}
|
|
47
183
|
|
|
48
|
-
if provider:
|
|
49
184
|
try:
|
|
50
|
-
|
|
185
|
+
role_models = parse_role_model_overrides(role_model)
|
|
51
186
|
except ValueError as e:
|
|
52
187
|
console.print(f"[red]{e}[/red]")
|
|
53
188
|
raise typer.Exit(code=2) from e
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
189
|
+
|
|
190
|
+
if provider:
|
|
191
|
+
try:
|
|
192
|
+
normalized_provider = validate_model_provider(provider)
|
|
193
|
+
except ValueError as e:
|
|
194
|
+
console.print(f"[red]{e}[/red]")
|
|
195
|
+
raise typer.Exit(code=2) from e
|
|
196
|
+
raw_config.setdefault("models", {})
|
|
197
|
+
previous = raw_config["models"].get("provider", "openrouter")
|
|
198
|
+
raw_config["models"]["provider"] = normalized_provider
|
|
199
|
+
updated_role_defaults = apply_provider_default_role_models(raw_config, previous, normalized_provider)
|
|
200
|
+
with open(config_path, "w") as f:
|
|
201
|
+
yaml.dump(raw_config, f, default_flow_style=False)
|
|
202
|
+
if previous == normalized_provider:
|
|
203
|
+
console.print(f"[green]Model provider remains '{normalized_provider}'.[/green]")
|
|
204
|
+
else:
|
|
205
|
+
console.print(f"[green]Updated model provider from '{previous}' to '{normalized_provider}'.[/green]")
|
|
206
|
+
if updated_role_defaults:
|
|
207
|
+
console.print(f"[green]Updated default role models for '{normalized_provider}'.[/green]")
|
|
208
|
+
if not model and not role_models:
|
|
209
|
+
log_step("config/complete", project_root=root, trace=True)
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
if not role and (model or role_models):
|
|
213
|
+
raw_config.setdefault("models", {})
|
|
214
|
+
configured_provider = validate_model_provider(raw_config["models"].get("provider", "openrouter"))
|
|
215
|
+
raw_config["models"]["roles"] = build_role_model_config(
|
|
216
|
+
configured_provider,
|
|
217
|
+
model=model,
|
|
218
|
+
role_models=role_models,
|
|
219
|
+
)
|
|
220
|
+
with open(config_path, "w") as f:
|
|
221
|
+
yaml.dump(raw_config, f, default_flow_style=False)
|
|
222
|
+
if model:
|
|
223
|
+
console.print(f"[green]Updated all model roles to use '{model}'.[/green]")
|
|
224
|
+
for selected_role, selected_model in role_models.items():
|
|
225
|
+
console.print(f"[green]Updated '{selected_role}' to use model '{selected_model}'.[/green]")
|
|
226
|
+
log_step("config/complete", project_root=root, trace=True)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
if not role:
|
|
230
|
+
console.print("[bold]Model Configuration[/bold]")
|
|
231
|
+
configured_provider = raw_config.get("models", {}).get("provider", "openrouter")
|
|
232
|
+
supported = ", ".join(SUPPORTED_MODEL_PROVIDERS)
|
|
233
|
+
console.print(f" [cyan]provider[/cyan]: {configured_provider} (supported: {supported})")
|
|
234
|
+
for r, m in raw_config.get("models", {}).get("roles", {}).items():
|
|
235
|
+
console.print(f" [cyan]{r}[/cyan]: {m.get('model')}")
|
|
236
|
+
log_step("config/complete", project_root=root, trace=True)
|
|
67
237
|
return
|
|
68
238
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
239
|
+
if not model:
|
|
240
|
+
m = raw_config.get("models", {}).get("roles", {}).get(role)
|
|
241
|
+
if m:
|
|
242
|
+
console.print(f"[cyan]{role}[/cyan]: {m.get('model')}")
|
|
243
|
+
else:
|
|
244
|
+
console.print(f"[red]Role '{role}' not found.[/red]")
|
|
245
|
+
log_step("config/complete", project_root=root, trace=True)
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
if "models" not in raw_config:
|
|
249
|
+
raw_config["models"] = {"roles": {}}
|
|
250
|
+
if "roles" not in raw_config["models"]:
|
|
251
|
+
raw_config["models"]["roles"] = {}
|
|
252
|
+
|
|
253
|
+
if role not in raw_config["models"]["roles"]:
|
|
254
|
+
raw_config["models"]["roles"][role] = {}
|
|
255
|
+
|
|
256
|
+
raw_config["models"]["roles"][role]["model"] = model
|
|
257
|
+
|
|
77
258
|
with open(config_path, "w") as f:
|
|
78
259
|
yaml.dump(raw_config, f, default_flow_style=False)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
console.print(f"[green]Updated '{selected_role}' to use model '{selected_model}'.[/green]")
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
if not role:
|
|
86
|
-
console.print("[bold]Model Configuration[/bold]")
|
|
87
|
-
configured_provider = raw_config.get("models", {}).get("provider", "openrouter")
|
|
88
|
-
supported = ", ".join(SUPPORTED_MODEL_PROVIDERS)
|
|
89
|
-
console.print(f" [cyan]provider[/cyan]: {configured_provider} (supported: {supported})")
|
|
90
|
-
for r, m in raw_config.get("models", {}).get("roles", {}).items():
|
|
91
|
-
console.print(f" [cyan]{r}[/cyan]: {m.get('model')}")
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
if not model:
|
|
95
|
-
m = raw_config.get("models", {}).get("roles", {}).get(role)
|
|
96
|
-
if m:
|
|
97
|
-
console.print(f"[cyan]{role}[/cyan]: {m.get('model')}")
|
|
98
|
-
else:
|
|
99
|
-
console.print(f"[red]Role '{role}' not found.[/red]")
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
if "models" not in raw_config:
|
|
103
|
-
raw_config["models"] = {"roles": {}}
|
|
104
|
-
if "roles" not in raw_config["models"]:
|
|
105
|
-
raw_config["models"]["roles"] = {}
|
|
106
|
-
|
|
107
|
-
if role not in raw_config["models"]["roles"]:
|
|
108
|
-
raw_config["models"]["roles"][role] = {}
|
|
109
|
-
|
|
110
|
-
raw_config["models"]["roles"][role]["model"] = model
|
|
111
|
-
|
|
112
|
-
with open(config_path, "w") as f:
|
|
113
|
-
yaml.dump(raw_config, f, default_flow_style=False)
|
|
114
|
-
|
|
115
|
-
console.print(f"[green]Updated '{role}' to use model '{model}'[/green]")
|
|
260
|
+
|
|
261
|
+
console.print(f"[green]Updated '{role}' to use model '{model}'[/green]")
|
|
262
|
+
log_step("config/complete", project_root=root, trace=True)
|