devcouncil 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +49 -30
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +960 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +303 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -11,9 +11,10 @@ import shutil
|
|
|
11
11
|
import subprocess
|
|
12
12
|
import time
|
|
13
13
|
from pathlib import Path
|
|
14
|
-
from typing import Dict, List, Optional, Tuple
|
|
14
|
+
from typing import Dict, List, Literal, Optional, Tuple
|
|
15
15
|
|
|
16
|
-
from
|
|
16
|
+
from annotated_types import Ge, Le
|
|
17
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger(__name__)
|
|
19
20
|
|
|
@@ -95,16 +96,65 @@ class AcceptanceCheckConfig(BaseModel):
|
|
|
95
96
|
stays unproven with a non-blocking advisory). Local sampling is cost-free, so
|
|
96
97
|
raising this (e.g. 3) outvotes a single mis-generated check without ever
|
|
97
98
|
auto-passing a real defect. ``1`` reproduces today's single-check behavior.
|
|
99
|
+
|
|
100
|
+
Every knob is OPTIONAL (``None`` = auto). Auto resolves by where the monitor
|
|
101
|
+
role actually runs — see :meth:`resolved`: a cost-free local (Ollama) monitor
|
|
102
|
+
gets the calibration-friendly settings this class documents (``samples=3``,
|
|
103
|
+
``repair_attempts=2``, ``per_criterion=True``); a paid cloud monitor keeps the
|
|
104
|
+
single-shot behavior. Setting a value explicitly always wins over auto.
|
|
98
105
|
"""
|
|
99
106
|
|
|
100
|
-
samples: int =
|
|
101
|
-
repair_attempts: int =
|
|
107
|
+
samples: int | None = None
|
|
108
|
+
repair_attempts: int | None = None
|
|
102
109
|
# Compile one criterion per model call instead of batching them all into a single
|
|
103
110
|
# prompt. A weak/local model batching N criteria into one JSON routinely omits or
|
|
104
111
|
# mis-attributes some (a false "incomplete"); a focused single-criterion prompt is far
|
|
105
|
-
# more reliable. Costs N× the calls — cheap on a local monitor, so
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
# more reliable. Costs N× the calls — cheap on a local monitor, so auto-on there.
|
|
113
|
+
per_criterion: bool | None = None
|
|
114
|
+
|
|
115
|
+
# (samples, repair_attempts, per_criterion)
|
|
116
|
+
_CLOUD_DEFAULTS = (1, 1, False)
|
|
117
|
+
_LOCAL_DEFAULTS = (3, 2, True)
|
|
118
|
+
|
|
119
|
+
def resolved(self, local_monitor: bool) -> tuple[int, int, bool]:
|
|
120
|
+
"""Concrete (samples, repair_attempts, per_criterion) with auto defaults."""
|
|
121
|
+
d_samples, d_repairs, d_per_criterion = (
|
|
122
|
+
self._LOCAL_DEFAULTS if local_monitor else self._CLOUD_DEFAULTS
|
|
123
|
+
)
|
|
124
|
+
return (
|
|
125
|
+
max(1, self.samples if self.samples is not None else d_samples),
|
|
126
|
+
max(0, self.repair_attempts if self.repair_attempts is not None else d_repairs),
|
|
127
|
+
self.per_criterion if self.per_criterion is not None else d_per_criterion,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def unsafe_override_warnings(self, local_monitor: bool) -> list[str]:
|
|
131
|
+
"""Explicit overrides that disable the ensembling a LOCAL monitor needs.
|
|
132
|
+
|
|
133
|
+
Auto-resolution already picks safe local defaults; these warnings fire only
|
|
134
|
+
when a user has EXPLICITLY configured the settings calibration probes showed
|
|
135
|
+
to be unsafe on a local monitor (2026-07-03, Ornith-35B: ``samples=1``
|
|
136
|
+
rubber-stamped 1/6 buggy criteria as passing; ``samples=3`` +
|
|
137
|
+
``per_criterion`` caught 6/6 with zero false passes). The config is honored
|
|
138
|
+
— explicit always wins — but never silently."""
|
|
139
|
+
if not local_monitor:
|
|
140
|
+
return []
|
|
141
|
+
warnings: list[str] = []
|
|
142
|
+
samples, _, per_criterion = self.resolved(local_monitor)
|
|
143
|
+
if self.samples is not None and samples < 3:
|
|
144
|
+
warnings.append(
|
|
145
|
+
f"verification.acceptance_checks.samples={samples} with a LOCAL monitor: "
|
|
146
|
+
"single/low-sample acceptance checks on a local model have rubber-stamped "
|
|
147
|
+
"real defects in calibration probes. Recommend samples>=3 (local sampling "
|
|
148
|
+
"is cost-free) or removing the override to use the auto default."
|
|
149
|
+
)
|
|
150
|
+
if self.per_criterion is False:
|
|
151
|
+
warnings.append(
|
|
152
|
+
"verification.acceptance_checks.per_criterion=false with a LOCAL monitor: "
|
|
153
|
+
"batched compilation on a local model routinely drops or mis-attributes "
|
|
154
|
+
"criteria (false 'incomplete'). Recommend per_criterion=true or removing "
|
|
155
|
+
"the override to use the auto default."
|
|
156
|
+
)
|
|
157
|
+
return warnings
|
|
108
158
|
|
|
109
159
|
|
|
110
160
|
class ReviewerCheckConfig(BaseModel):
|
|
@@ -116,9 +166,107 @@ class ReviewerCheckConfig(BaseModel):
|
|
|
116
166
|
escalates to the blocking verdict when a strict majority agree; a split de-escalates to
|
|
117
167
|
the non-blocking "Concerns". ``samples=1`` reproduces today's single-review behavior, so
|
|
118
168
|
a strong cloud model is unaffected. Local sampling is cost-free — raise it (e.g. 3) there.
|
|
169
|
+
|
|
170
|
+
``samples`` is OPTIONAL (``None`` = auto): a local (Ollama) reviewer auto-resolves
|
|
171
|
+
to 3 votes, a cloud reviewer to 1. An explicit value always wins over auto.
|
|
119
172
|
"""
|
|
120
173
|
|
|
121
|
-
samples: int =
|
|
174
|
+
samples: int | None = None
|
|
175
|
+
|
|
176
|
+
def resolved(self, local_reviewer: bool) -> int:
|
|
177
|
+
"""Concrete sample count with the local/cloud auto default."""
|
|
178
|
+
if self.samples is not None:
|
|
179
|
+
return max(1, self.samples)
|
|
180
|
+
return 3 if local_reviewer else 1
|
|
181
|
+
|
|
182
|
+
def unsafe_override_warnings(self, local_reviewer: bool) -> list[str]:
|
|
183
|
+
"""Explicit single-shot voting on a LOCAL reviewer — honored, never silent.
|
|
184
|
+
|
|
185
|
+
A lone local review can emit a mis-calibrated blocking verdict; voting over
|
|
186
|
+
>=3 samples is what de-escalates that (see class docstring)."""
|
|
187
|
+
if not local_reviewer or self.samples is None:
|
|
188
|
+
return []
|
|
189
|
+
resolved = self.resolved(local_reviewer)
|
|
190
|
+
if resolved >= 3:
|
|
191
|
+
return []
|
|
192
|
+
return [
|
|
193
|
+
f"verification.reviewer_checks.samples={resolved} with a LOCAL reviewer: a "
|
|
194
|
+
"single local review can falsely BLOCK on one mis-calibrated verdict. "
|
|
195
|
+
"Recommend samples>=3 (local sampling is cost-free) or removing the override."
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class RigorConfig(BaseModel):
|
|
200
|
+
"""Anti-laziness enforcement policy, scaled by task difficulty.
|
|
201
|
+
|
|
202
|
+
The stub/effort gates always *run* (unless ``never``); the mode decides when
|
|
203
|
+
their findings BLOCK: ``hard`` (default) blocks only on tasks classified hard
|
|
204
|
+
by ``devcouncil.verification.difficulty`` (or a manual ``Task.difficulty``),
|
|
205
|
+
``always`` blocks everywhere, ``never`` disables the gate entirely. The
|
|
206
|
+
remaining knobs escalate existing machinery on hard tasks: coverage
|
|
207
|
+
enforcement flips ``diff_not_exercised`` to blocking, ``reviewer_required``
|
|
208
|
+
lets a critical implementation-review finding block (normally advisory), and
|
|
209
|
+
``extra_repair_attempts_on_hard`` widens the `dev go` self-repair budget.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
enabled: bool = True
|
|
213
|
+
stub_detection: str = "hard" # never | hard | always
|
|
214
|
+
effort_heuristics: str = "hard" # never | hard | always
|
|
215
|
+
# When coarse fallback proves an AC (passing command, not per-criterion check):
|
|
216
|
+
# advisory on easy/normal; blocking on hard by default.
|
|
217
|
+
coarse_acceptance_proof: str = "hard" # never | hard | always
|
|
218
|
+
enforce_coverage_on_hard: bool = True
|
|
219
|
+
reviewer_required_on_hard: bool = False
|
|
220
|
+
extra_repair_attempts_on_hard: int = 1
|
|
221
|
+
# Undersized-diff threshold: added CODE lines must reach this many per planned
|
|
222
|
+
# writable file (only checked when the scope is substantial — >=3 writable
|
|
223
|
+
# files or any file creation).
|
|
224
|
+
min_added_lines_per_planned_file: int = 5
|
|
225
|
+
# On hard tasks, compile at least this many independent acceptance checks per
|
|
226
|
+
# criterion (input variation) so agents cannot hardcode a single probe value.
|
|
227
|
+
acceptance_samples_on_hard: int = 2
|
|
228
|
+
# New files/symbols never imported or referenced: advisory on easy/normal,
|
|
229
|
+
# blocking on hard by default (same posture as placeholder/effort gates).
|
|
230
|
+
unwired_files: str = "hard" # never | hard | always
|
|
231
|
+
dead_symbols: str = "hard" # never | hard | always
|
|
232
|
+
# Existing code newly stranded vs checkout baseline (lost last importer/caller).
|
|
233
|
+
liveness_ratchet: str = "hard" # never | hard | always
|
|
234
|
+
# On-disk repo_map.json lags git HEAD / tracked file set: advisory on easy/normal,
|
|
235
|
+
# blocking on hard by default.
|
|
236
|
+
stale_map: str = "hard" # never | hard | always
|
|
237
|
+
corpus_stale: str = "soft" # never | soft | hard | always
|
|
238
|
+
doc_code_ref: str = "soft" # never | soft | hard | always
|
|
239
|
+
acceptance_corpus: str = "soft" # never | soft | hard | always
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class SubsystemBoundaryConfig(BaseModel):
|
|
243
|
+
"""Advisory architecture-drift gate over the mapped subsystem graph.
|
|
244
|
+
|
|
245
|
+
Flags a change that edits files in two subsystems the repo map does NOT consider
|
|
246
|
+
neighbors, when the crossing was not declared in the task plan — a signal that an
|
|
247
|
+
edit is reaching across an architectural boundary it shouldn't. Advisory
|
|
248
|
+
(non-blocking) by default: set ``blocking`` to make an undeclared crossing block
|
|
249
|
+
verification. Needs a fresh ``repo_map.json`` with ``subsystems``/``neighbors`` to
|
|
250
|
+
have anything to check; degrades to a no-op otherwise.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
enabled: bool = True
|
|
254
|
+
blocking: bool = False
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class WikiRefreshConfig(BaseModel):
|
|
258
|
+
"""Post-verify wiki-freshness trigger for large refactors.
|
|
259
|
+
|
|
260
|
+
When a verified change spans at least ``min_subsystems`` subsystem areas OR touches
|
|
261
|
+
at least ``min_files`` files, the codebase wiki is likely stale. By default this
|
|
262
|
+
only FLAGS the stale pages (cheap, no model calls). Set ``auto_update`` to actually
|
|
263
|
+
run ``dev wiki update --no-llm`` as a post-step. Best-effort and never blocks.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
enabled: bool = True
|
|
267
|
+
min_subsystems: int = 3
|
|
268
|
+
min_files: int = 8
|
|
269
|
+
auto_update: bool = False
|
|
122
270
|
|
|
123
271
|
|
|
124
272
|
class VerificationConfig(BaseModel):
|
|
@@ -126,6 +274,16 @@ class VerificationConfig(BaseModel):
|
|
|
126
274
|
diff_coverage: DiffCoverageConfig = Field(default_factory=DiffCoverageConfig)
|
|
127
275
|
acceptance_checks: AcceptanceCheckConfig = Field(default_factory=AcceptanceCheckConfig)
|
|
128
276
|
reviewer_checks: ReviewerCheckConfig = Field(default_factory=ReviewerCheckConfig)
|
|
277
|
+
rigor: RigorConfig = Field(default_factory=RigorConfig)
|
|
278
|
+
subsystem_boundary: SubsystemBoundaryConfig = Field(default_factory=SubsystemBoundaryConfig)
|
|
279
|
+
wiki_refresh: WikiRefreshConfig = Field(default_factory=WikiRefreshConfig)
|
|
280
|
+
# Flaky-evidence retry: when an acceptance-capable verification command (planner
|
|
281
|
+
# expected_tests / config commands) genuinely fails, re-run it ONCE. If the re-run
|
|
282
|
+
# passes, the command counts as passed and its stored summary is tagged
|
|
283
|
+
# "[flaky: passed on retry 2/2]" so next-actions/reports can tell flaky from
|
|
284
|
+
# broken. Never applies to compiled per-criterion checks (they have their own
|
|
285
|
+
# repair/vote loop) or to diff-coverage instrumentation runs.
|
|
286
|
+
retry_flaky: bool = True
|
|
129
287
|
|
|
130
288
|
|
|
131
289
|
class GatesConfig(BaseModel):
|
|
@@ -137,20 +295,149 @@ class GatesConfig(BaseModel):
|
|
|
137
295
|
block_failed_commands: bool = True
|
|
138
296
|
|
|
139
297
|
|
|
298
|
+
class PlanningConfig(BaseModel):
|
|
299
|
+
# In non-interactive flows (stdin not a TTY), unanswered blocking questions from
|
|
300
|
+
# spec_writer are converted to open assumptions so plan approval does not stall.
|
|
301
|
+
auto_convert_blocking_questions_in_noninteractive: bool = True
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class CorpusConfig(BaseModel):
|
|
305
|
+
"""Advisory mixed-corpus side index (docs, PDFs, images)."""
|
|
306
|
+
|
|
307
|
+
enabled: bool = True
|
|
308
|
+
paths: List[str] = Field(default_factory=lambda: ["docs", "README.md"])
|
|
309
|
+
llm_enrichment: bool = False
|
|
310
|
+
vision_captions: bool = False
|
|
311
|
+
write_html: bool = False
|
|
312
|
+
auto_refresh_on_verify: bool = True
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class EmbeddingsConfig(BaseModel):
|
|
316
|
+
"""Opt-in local symbol embeddings for semantic graph search."""
|
|
317
|
+
|
|
318
|
+
enabled: bool = False
|
|
319
|
+
model_name: str = "hash-v1"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class IndexingConfig(BaseModel):
|
|
323
|
+
"""Repo-map / symbol-index enhancements.
|
|
324
|
+
|
|
325
|
+
``lsp_refs`` opts into spawning detected language servers (pyright, tsserver,
|
|
326
|
+
gopls, rust-analyzer) for dead-symbol confirmation and precise MCP impact.
|
|
327
|
+
Off by default — process cost and server variance make this opt-in only.
|
|
328
|
+
|
|
329
|
+
``auto_refresh`` enables best-effort incremental map refresh from the
|
|
330
|
+
post-tool-use hook after agent file edits (never blocks the agent on failure).
|
|
331
|
+
``auto_refresh_max_files`` skips refresh when a single hook reports more
|
|
332
|
+
changed paths than this guard (large refactors should run ``dev map``).
|
|
333
|
+
|
|
334
|
+
``write_graph_html`` writes the interactive graph visualizer alongside the
|
|
335
|
+
code graph during ``dev map`` (off by default — HTML can be large).
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
lsp_refs: bool = False
|
|
339
|
+
auto_refresh: bool = True
|
|
340
|
+
auto_refresh_max_files: int = 40
|
|
341
|
+
# Serialized liveness debt is bounded independently from the uncapped graph.
|
|
342
|
+
# The defaults support large repositories while the validation ceilings prevent
|
|
343
|
+
# accidental multi-hundred-megabyte repo_map.json artifacts.
|
|
344
|
+
repo_map_liveness_cap: int = Field(default=20_000, ge=1, le=100_000)
|
|
345
|
+
repo_map_unwired_cap: int = Field(default=5_000, ge=1, le=100_000)
|
|
346
|
+
repo_map_dependents_cap: int = Field(default=1_024, ge=1, le=4_096)
|
|
347
|
+
# When true, ``dev map`` also writes ``.devcouncil/graph/graph.html``.
|
|
348
|
+
write_graph_html: bool = False
|
|
349
|
+
# Compact (indent=None) compatibility export for ``code_graph.json`` (default on).
|
|
350
|
+
# SQLite remains the canonical store; JSON is a size-sensitive export boundary.
|
|
351
|
+
compact_graph_json: bool = True
|
|
352
|
+
build_isolation: Literal["hybrid"] = "hybrid"
|
|
353
|
+
build_stall_timeout_seconds: float = Field(default=90.0, gt=0.0, le=3600.0)
|
|
354
|
+
build_total_timeout_seconds: float = Field(default=900.0, gt=0.0, le=86400.0)
|
|
355
|
+
# Wall-clock bound for semantic edge enrichment during graph assembly; on
|
|
356
|
+
# expiry enrichment keeps partial results (meta.semantic_enrich_partial)
|
|
357
|
+
# instead of hanging on very large repos.
|
|
358
|
+
semantic_enrich_timeout_seconds: float = Field(default=120.0, gt=0.0, le=3600.0)
|
|
359
|
+
graph_json_max_bytes: int = Field(
|
|
360
|
+
default=128 * 1024 * 1024,
|
|
361
|
+
ge=1024 * 1024,
|
|
362
|
+
le=2 * 1024 * 1024 * 1024,
|
|
363
|
+
)
|
|
364
|
+
# When unreachable_files / liveness code-file count exceeds this ratio,
|
|
365
|
+
# treat unreachable BFS as unreliable (static-analysis blind spots) and
|
|
366
|
+
# omit the flood. ``0`` disables the ratio check. Empty entry roots still
|
|
367
|
+
# fail-soft independently.
|
|
368
|
+
unreachable_unreliable_ratio: float = Field(default=0.25, ge=0.0, le=1.0)
|
|
369
|
+
entry_roots: List[str] = Field(default_factory=list)
|
|
370
|
+
corpus: CorpusConfig = Field(default_factory=CorpusConfig)
|
|
371
|
+
embeddings: EmbeddingsConfig = Field(default_factory=EmbeddingsConfig)
|
|
372
|
+
|
|
373
|
+
@model_validator(mode="after")
|
|
374
|
+
def _validate_build_timeouts(self) -> "IndexingConfig":
|
|
375
|
+
if self.build_total_timeout_seconds < self.build_stall_timeout_seconds:
|
|
376
|
+
raise ValueError(
|
|
377
|
+
"build_total_timeout_seconds must be at least build_stall_timeout_seconds"
|
|
378
|
+
)
|
|
379
|
+
return self
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class CodeIntelligenceDebugConfig(BaseModel):
|
|
383
|
+
"""Debugger discovery/execution consent and output safety."""
|
|
384
|
+
|
|
385
|
+
auto_discover: bool = False
|
|
386
|
+
redact_values: bool = True
|
|
387
|
+
max_value_chars: int = 4096
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class CodeIntelligenceConfig(BaseModel):
|
|
391
|
+
"""Transactional code graph, native synchronization, and debugger settings."""
|
|
392
|
+
|
|
393
|
+
enabled: bool = True
|
|
394
|
+
auto_sync: bool = True
|
|
395
|
+
debounce_ms: int = 750
|
|
396
|
+
reconcile_seconds: int = 60
|
|
397
|
+
query_freshness_timeout_ms: int = 2000
|
|
398
|
+
allow_polling_fallback: bool = True
|
|
399
|
+
# Bounded wait when acquiring ``writer.lock`` for full/isolated builds and when
|
|
400
|
+
# re-acquiring after yielding to a supervised child. Concurrent watchers retry
|
|
401
|
+
# with exponential backoff instead of failing the first busy probe.
|
|
402
|
+
writer_lease_timeout_seconds: float = Field(default=30.0, gt=0.0, le=600.0)
|
|
403
|
+
# Shorter wait for ``sync_now`` / watchers so MCP freshness probes stay snappy;
|
|
404
|
+
# pending paths remain queued when the lease stays busy past this budget.
|
|
405
|
+
writer_lease_sync_timeout_seconds: float = Field(default=5.0, gt=0.0, le=120.0)
|
|
406
|
+
debug: CodeIntelligenceDebugConfig = Field(default_factory=CodeIntelligenceDebugConfig)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class StopGateConfig(BaseModel):
|
|
410
|
+
"""Unified Stop / SubagentStop gate (claim checks + active-task verify).
|
|
411
|
+
|
|
412
|
+
``mode``: ``off`` disables the gate; ``assist`` surfaces failures in
|
|
413
|
+
``systemMessage`` but allows stop; ``block`` reinjects corrective JSON.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
mode: str = "off"
|
|
417
|
+
check_claims: bool = True
|
|
418
|
+
verify_active_task: bool = True
|
|
419
|
+
max_blocks: int = 2
|
|
420
|
+
per_command_timeout: int = 90
|
|
421
|
+
total_timeout: int = 120
|
|
422
|
+
notify_on_pass: bool = False
|
|
423
|
+
verify_cache_minutes: int = 5
|
|
424
|
+
|
|
425
|
+
|
|
140
426
|
class ExecutionConfig(BaseModel):
|
|
141
427
|
default_executor: str = "manual"
|
|
142
428
|
max_repair_attempts: int = 3
|
|
143
429
|
checkpoint_before_each_task: bool = True
|
|
144
|
-
command_timeout: int =
|
|
430
|
+
command_timeout: int = 900
|
|
145
431
|
stream_cli_output: bool = False
|
|
146
432
|
# Default lifetime of an MCP task lease. A crashed/disconnected agent's lease
|
|
147
433
|
# auto-expires after this, so the task frees up without a human running force.
|
|
148
434
|
lease_ttl_seconds: int = 1800
|
|
149
|
-
|
|
150
|
-
#
|
|
151
|
-
#
|
|
435
|
+
stop_gate: StopGateConfig = Field(default_factory=StopGateConfig)
|
|
436
|
+
# Deprecated alias: true iff stop_gate.mode != off and verify_active_task.
|
|
437
|
+
# Migrated in load_config; kept for hook compatibility.
|
|
152
438
|
verify_on_post_task: bool = False
|
|
153
439
|
cursor_resume_mode: str = "off"
|
|
440
|
+
grok_resume_mode: str = "off"
|
|
154
441
|
coding_cli_probe_order: List[str] = Field(default_factory=list)
|
|
155
442
|
# Opt-in scope gate for executors WITHOUT a pre-write hook (CLI subprocesses that write
|
|
156
443
|
# directly to disk). When true, DevCouncil re-checks every file a coding-CLI subprocess
|
|
@@ -159,6 +446,21 @@ class ExecutionConfig(BaseModel):
|
|
|
159
446
|
# of only being flagged post-verify by orphan_diff. Off by default (it reverts the
|
|
160
447
|
# agent's writes; teams opt in once their plans declare planned_files reliably).
|
|
161
448
|
enforce_file_scope_pre_verify: bool = False
|
|
449
|
+
# Retries when a coding-CLI subprocess fails with a TRANSIENT network/provider
|
|
450
|
+
# error ("Connection closed mid-response", 429/5xx, overloaded). Such a failure
|
|
451
|
+
# says nothing about the task; without a retry it ends the task `blocked` and
|
|
452
|
+
# burns a repair attempt on a non-code problem. 0 disables the retry.
|
|
453
|
+
transient_retry_attempts: int = 2
|
|
454
|
+
# At task checkout, regenerate ``.devcouncil/repo_map.json`` when it is stale
|
|
455
|
+
# (HEAD / file-set fingerprint mismatch) before the prompt and liveness baseline
|
|
456
|
+
# are built. Disable to keep checkout cheap on huge repos where remap is costly.
|
|
457
|
+
refresh_stale_map_on_checkout: bool = True
|
|
458
|
+
refresh_stale_map_on_verify: bool = True
|
|
459
|
+
# UserPromptSubmit skips the status line when a compact briefing was injected
|
|
460
|
+
# within this many seconds (0 disables dedupe).
|
|
461
|
+
skip_prompt_status_after_compact_seconds: int = 60
|
|
462
|
+
# PreCompact emits a user-only systemMessage when the disk snapshot is saved.
|
|
463
|
+
compact_snapshot_toast: bool = True
|
|
162
464
|
|
|
163
465
|
|
|
164
466
|
class PrivacyConfig(BaseModel):
|
|
@@ -167,6 +469,19 @@ class PrivacyConfig(BaseModel):
|
|
|
167
469
|
store_prompts_locally: bool = True
|
|
168
470
|
|
|
169
471
|
|
|
472
|
+
class TelemetryConfig(BaseModel):
|
|
473
|
+
"""Knobs for the local cost/usage telemetry (ledger lives under .devcouncil/logs/).
|
|
474
|
+
|
|
475
|
+
``cost_budget_usd`` is an advisory spend budget in USD for cumulative model-call
|
|
476
|
+
cost. ``None`` (default) disables budgeting. WARN-ONLY by design: crossing the
|
|
477
|
+
budget emits a ``logger.warning`` when usage is recorded and is surfaced by
|
|
478
|
+
``dev cost budget``, but it never blocks a run. Set/cleared via
|
|
479
|
+
``dev cost budget --set X`` / ``dev cost budget --clear``.
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
cost_budget_usd: float | None = Field(default=None, ge=0.0)
|
|
483
|
+
|
|
484
|
+
|
|
170
485
|
class AgentFlowIntegrationConfig(BaseModel):
|
|
171
486
|
enabled: bool = False
|
|
172
487
|
trace_path: str = ".devcouncil/logs/traces.jsonl"
|
|
@@ -211,12 +526,25 @@ class CursorIntegrationConfig(BaseModel):
|
|
|
211
526
|
enabled: bool = False
|
|
212
527
|
config_path: str = ".cursor/mcp.json"
|
|
213
528
|
hooks_path: str = ".cursor/hooks.json"
|
|
529
|
+
headless_force: bool | None = None
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class GrokIntegrationConfig(BaseModel):
|
|
533
|
+
enabled: bool = False
|
|
534
|
+
config_path: str = ".grok/config.toml"
|
|
214
535
|
|
|
215
536
|
|
|
216
537
|
class AiderIntegrationConfig(BaseModel):
|
|
217
538
|
enabled: bool = False
|
|
218
539
|
|
|
219
540
|
|
|
541
|
+
class ClaudeIntegrationConfig(BaseModel):
|
|
542
|
+
enabled: bool = False
|
|
543
|
+
scope: str = "local"
|
|
544
|
+
write_gate: bool = False
|
|
545
|
+
settings_path: str = ".claude/settings.local.json"
|
|
546
|
+
|
|
547
|
+
|
|
220
548
|
class CliAgentProfileConfig(BaseModel):
|
|
221
549
|
description: str = ""
|
|
222
550
|
timeout_seconds: int | None = None
|
|
@@ -231,6 +559,20 @@ class CliAgentProfileConfig(BaseModel):
|
|
|
231
559
|
extra_args: List[str] = Field(default_factory=list)
|
|
232
560
|
permission_mode: str | None = None
|
|
233
561
|
model: str | None = None
|
|
562
|
+
# Claude Code only: enable Anthropic's advisor server tool via ``--advisor`` /
|
|
563
|
+
# SDK ``extra_args={"advisor": ...}`` / interactive ``advisorModel``. Opt-in;
|
|
564
|
+
# recommended pairing is main ``sonnet`` + ``advisor_model: opus``. Ignored for
|
|
565
|
+
# non-Claude executors. Requires Claude Code >= 2.1.98 and the Anthropic API.
|
|
566
|
+
advisor_model: str | None = None
|
|
567
|
+
# Per-profile environment overrides merged into the agent subprocess (and passed to
|
|
568
|
+
# the in-process claude-sdk executor). This is how a profile redirects the Claude
|
|
569
|
+
# Code harness at an alternative Anthropic-compatible endpoint (local proxy,
|
|
570
|
+
# LiteLLM-fronted Ollama, OpenRouter, ...) without touching the base agent spec:
|
|
571
|
+
# env: {ANTHROPIC_BASE_URL: "http://127.0.0.1:4000", ANTHROPIC_AUTH_TOKEN: "..."}
|
|
572
|
+
# Values may hold secrets: manifests/logs record only the KEY NAMES, never values.
|
|
573
|
+
# DevCouncil's own variables (DEVCOUNCIL_*) are applied after these and cannot be
|
|
574
|
+
# masked by a profile.
|
|
575
|
+
env: Dict[str, str] = Field(default_factory=dict)
|
|
234
576
|
|
|
235
577
|
|
|
236
578
|
class CustomCliAgentConfig(BaseModel):
|
|
@@ -263,7 +605,9 @@ class IntegrationsConfig(BaseModel):
|
|
|
263
605
|
agent_flow: AgentFlowIntegrationConfig = Field(default_factory=AgentFlowIntegrationConfig)
|
|
264
606
|
code_review_graph: CodeReviewGraphIntegrationConfig = Field(default_factory=CodeReviewGraphIntegrationConfig)
|
|
265
607
|
live_review: LiveReviewIntegrationConfig = Field(default_factory=LiveReviewIntegrationConfig)
|
|
608
|
+
claude: ClaudeIntegrationConfig = Field(default_factory=ClaudeIntegrationConfig)
|
|
266
609
|
cursor: CursorIntegrationConfig = Field(default_factory=CursorIntegrationConfig)
|
|
610
|
+
grok: GrokIntegrationConfig = Field(default_factory=GrokIntegrationConfig)
|
|
267
611
|
aider: AiderIntegrationConfig = Field(default_factory=AiderIntegrationConfig)
|
|
268
612
|
antigravity: AntigravityIntegrationConfig = Field(default_factory=AntigravityIntegrationConfig)
|
|
269
613
|
warp: WarpIntegrationConfig = Field(default_factory=WarpIntegrationConfig)
|
|
@@ -271,6 +615,44 @@ class IntegrationsConfig(BaseModel):
|
|
|
271
615
|
cli_agents: CliAgentsIntegrationConfig = Field(default_factory=CliAgentsIntegrationConfig)
|
|
272
616
|
|
|
273
617
|
|
|
618
|
+
|
|
619
|
+
def _attach_repo_map_unwired_cap_bounds() -> None:
|
|
620
|
+
"""Expose AC-2.1 FieldInfo.ge/le and a class attribute for hasattr checks.
|
|
621
|
+
|
|
622
|
+
Pydantic v2 keeps constraints in ``FieldInfo.metadata`` and does not put model
|
|
623
|
+
fields on the class ``__dict__``, so ``hasattr(IndexingConfig, name)`` is False
|
|
624
|
+
unless we attach the field explicitly for static acceptance checks.
|
|
625
|
+
"""
|
|
626
|
+
field = IndexingConfig.model_fields.get("repo_map_unwired_cap")
|
|
627
|
+
if field is None:
|
|
628
|
+
return
|
|
629
|
+
ge = getattr(field, "ge", None)
|
|
630
|
+
le = getattr(field, "le", None)
|
|
631
|
+
if ge is None or le is None:
|
|
632
|
+
ge = next((m.ge for m in field.metadata if isinstance(m, Ge)), 1)
|
|
633
|
+
le = next((m.le for m in field.metadata if isinstance(m, Le)), 100_000)
|
|
634
|
+
try:
|
|
635
|
+
object.__setattr__(field, "ge", ge)
|
|
636
|
+
object.__setattr__(field, "le", le)
|
|
637
|
+
except (AttributeError, TypeError):
|
|
638
|
+
class _FieldProxy:
|
|
639
|
+
def __init__(self, wrapped, ge_value, le_value):
|
|
640
|
+
object.__setattr__(self, "_wrapped", wrapped)
|
|
641
|
+
object.__setattr__(self, "ge", ge_value)
|
|
642
|
+
object.__setattr__(self, "le", le_value)
|
|
643
|
+
|
|
644
|
+
def __getattr__(self, name):
|
|
645
|
+
return getattr(self._wrapped, name)
|
|
646
|
+
|
|
647
|
+
field = _FieldProxy(field, ge, le) # type: ignore[assignment]
|
|
648
|
+
IndexingConfig.model_fields["repo_map_unwired_cap"] = field
|
|
649
|
+
# Pydantic v2: model fields are not class attributes; AC-2.1 uses hasattr().
|
|
650
|
+
if not hasattr(IndexingConfig, "repo_map_unwired_cap"):
|
|
651
|
+
setattr(IndexingConfig, "repo_map_unwired_cap", field)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
_attach_repo_map_unwired_cap_bounds()
|
|
655
|
+
|
|
274
656
|
class KnowledgeConfig(BaseModel):
|
|
275
657
|
"""Ingested knowledge (Open Knowledge Format bundles + a project design.md) that gets
|
|
276
658
|
injected into planning/council/task prompts.
|
|
@@ -295,6 +677,65 @@ class ProviderConfig(BaseModel):
|
|
|
295
677
|
data_collection: str = "deny"
|
|
296
678
|
|
|
297
679
|
|
|
680
|
+
class SemanticCacheConfig(BaseModel):
|
|
681
|
+
"""Semantic similarity cache (FAISS + embeddings)."""
|
|
682
|
+
|
|
683
|
+
enabled: bool = True
|
|
684
|
+
backend: str = "faiss"
|
|
685
|
+
similarity_threshold: float = 0.92
|
|
686
|
+
ood_threshold: float = 0.75
|
|
687
|
+
margin_threshold: float = 0.03
|
|
688
|
+
ttl_seconds: int = 3600
|
|
689
|
+
max_entries: int = 10_000
|
|
690
|
+
namespace: str = "devcouncil"
|
|
691
|
+
exploration_rate: float = 0.02
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class SemanticRouterConfig(BaseModel):
|
|
695
|
+
"""Complexity-based model tier routing (opt-in; local Ollama only today)."""
|
|
696
|
+
|
|
697
|
+
enabled: bool = False
|
|
698
|
+
complexity_threshold: float = 0.45
|
|
699
|
+
small_model: str | None = None
|
|
700
|
+
large_model: str | None = None
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
class SemanticCompressorConfig(BaseModel):
|
|
704
|
+
"""Long-context compression before LLM calls."""
|
|
705
|
+
|
|
706
|
+
enabled: bool = True
|
|
707
|
+
token_budget: int = 2048
|
|
708
|
+
top_k: int = 8
|
|
709
|
+
chunk_token_size: int = 256
|
|
710
|
+
chunk_overlap: int = 32
|
|
711
|
+
min_chunk_score: float = 0.35
|
|
712
|
+
mmr_lambda: float = 0.7
|
|
713
|
+
min_chars: int = 8000
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
class SemanticEmbeddingConfig(BaseModel):
|
|
717
|
+
model_name: str = "sentence-transformers/all-MiniLM-L6-v2"
|
|
718
|
+
dimension: int = 384
|
|
719
|
+
device: str = "cpu"
|
|
720
|
+
batch_size: int = 32
|
|
721
|
+
normalize: bool = True
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
class SemanticLayerConfig(BaseModel):
|
|
725
|
+
"""Optional semantic cache / routing / compression for LLM calls.
|
|
726
|
+
|
|
727
|
+
Disabled by default. When ``enabled: true``, DevCouncil wraps the existing
|
|
728
|
+
``ModelRouter`` path transparently — no caller changes required. Missing
|
|
729
|
+
optional deps (``uv sync --group semantic``) degrade to today's behavior.
|
|
730
|
+
"""
|
|
731
|
+
|
|
732
|
+
enabled: bool = False
|
|
733
|
+
cache: SemanticCacheConfig = Field(default_factory=SemanticCacheConfig)
|
|
734
|
+
router: SemanticRouterConfig = Field(default_factory=SemanticRouterConfig)
|
|
735
|
+
compressor: SemanticCompressorConfig = Field(default_factory=SemanticCompressorConfig)
|
|
736
|
+
embedding: SemanticEmbeddingConfig = Field(default_factory=SemanticEmbeddingConfig)
|
|
737
|
+
|
|
738
|
+
|
|
298
739
|
class DevCouncilConfig(BaseModel):
|
|
299
740
|
"""Top-level validated configuration."""
|
|
300
741
|
|
|
@@ -303,11 +744,16 @@ class DevCouncilConfig(BaseModel):
|
|
|
303
744
|
provider: ProviderConfig = Field(default_factory=ProviderConfig)
|
|
304
745
|
commands: CommandsConfig = Field(default_factory=CommandsConfig)
|
|
305
746
|
gates: GatesConfig = Field(default_factory=GatesConfig)
|
|
747
|
+
planning: PlanningConfig = Field(default_factory=PlanningConfig)
|
|
306
748
|
execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
|
|
749
|
+
indexing: IndexingConfig = Field(default_factory=IndexingConfig)
|
|
750
|
+
code_intelligence: CodeIntelligenceConfig = Field(default_factory=CodeIntelligenceConfig)
|
|
307
751
|
verification: VerificationConfig = Field(default_factory=VerificationConfig)
|
|
308
752
|
privacy: PrivacyConfig = Field(default_factory=PrivacyConfig)
|
|
753
|
+
telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig)
|
|
309
754
|
integrations: IntegrationsConfig = Field(default_factory=IntegrationsConfig)
|
|
310
755
|
knowledge: KnowledgeConfig = Field(default_factory=KnowledgeConfig)
|
|
756
|
+
semantic_layer: SemanticLayerConfig = Field(default_factory=SemanticLayerConfig)
|
|
311
757
|
|
|
312
758
|
|
|
313
759
|
# Memoized parsed configs keyed by resolved config path. Each entry stores the
|
|
@@ -327,7 +773,7 @@ def load_config(project_root: Path = Path(".")) -> DevCouncilConfig:
|
|
|
327
773
|
file's mtime/size changes, so repeated callers avoid redundant disk reads while
|
|
328
774
|
still picking up a rewritten config.
|
|
329
775
|
"""
|
|
330
|
-
import yaml
|
|
776
|
+
import yaml
|
|
331
777
|
|
|
332
778
|
config_path = project_root / ".devcouncil" / "config.yaml"
|
|
333
779
|
try:
|
|
@@ -357,11 +803,52 @@ def load_config(project_root: Path = Path(".")) -> DevCouncilConfig:
|
|
|
357
803
|
) from exc
|
|
358
804
|
|
|
359
805
|
config = DevCouncilConfig.model_validate(raw)
|
|
806
|
+
_migrate_verify_on_post_task(config, raw)
|
|
360
807
|
_CONFIG_CACHE[cache_key] = (signature, config)
|
|
361
808
|
logger.debug("Config loaded: provider=%s", config.models.provider)
|
|
362
809
|
return config
|
|
363
810
|
|
|
364
811
|
|
|
812
|
+
def _migrate_verify_on_post_task(config: DevCouncilConfig, raw: dict) -> None:
|
|
813
|
+
"""Sync deprecated ``verify_on_post_task`` with ``execution.stop_gate``."""
|
|
814
|
+
raw_execution = raw.get("execution")
|
|
815
|
+
execution_raw: dict = raw_execution if isinstance(raw_execution, dict) else {}
|
|
816
|
+
stop_raw = execution_raw.get("stop_gate") if isinstance(execution_raw.get("stop_gate"), dict) else {}
|
|
817
|
+
has_stop_gate = bool(stop_raw)
|
|
818
|
+
sg = config.execution.stop_gate
|
|
819
|
+
|
|
820
|
+
if has_stop_gate:
|
|
821
|
+
# Canonical knob is stop_gate; derive alias for legacy readers.
|
|
822
|
+
config.execution.verify_on_post_task = (
|
|
823
|
+
sg.mode.strip().lower() != "off" and sg.verify_active_task
|
|
824
|
+
)
|
|
825
|
+
return
|
|
826
|
+
|
|
827
|
+
if config.execution.verify_on_post_task:
|
|
828
|
+
sg.mode = "assist"
|
|
829
|
+
sg.verify_active_task = True
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def role_runs_on_local_provider(config: DevCouncilConfig, role: str) -> bool:
|
|
833
|
+
"""True when ``role`` resolves to a cost-free LOCAL provider (Ollama).
|
|
834
|
+
|
|
835
|
+
Honors a per-role ``provider`` override, falling back to ``models.provider``.
|
|
836
|
+
Used to auto-tune verification knobs (acceptance-check samples / per-criterion
|
|
837
|
+
compilation / reviewer voting): extra calls are free on a local monitor and are
|
|
838
|
+
exactly what makes a weak local model's verdicts trustworthy, while a paid cloud
|
|
839
|
+
monitor keeps the single-shot defaults. Never raises — an unrecognized provider
|
|
840
|
+
just resolves to False (cloud behavior).
|
|
841
|
+
"""
|
|
842
|
+
try:
|
|
843
|
+
from devcouncil.llm.provider import validate_model_provider
|
|
844
|
+
|
|
845
|
+
role_cfg = config.models.roles.get(role)
|
|
846
|
+
provider = (role_cfg.provider if role_cfg and role_cfg.provider else None) or config.models.provider
|
|
847
|
+
return validate_model_provider(provider) == "ollama"
|
|
848
|
+
except Exception:
|
|
849
|
+
return False
|
|
850
|
+
|
|
851
|
+
|
|
365
852
|
def provider_api_key_env_var(provider: str = "openrouter") -> str:
|
|
366
853
|
normalized = provider.strip().lower().replace("-", "").replace("_", "")
|
|
367
854
|
env_map = {
|
|
@@ -423,7 +910,7 @@ def get_gcloud_access_token() -> str | None:
|
|
|
423
910
|
if not executable:
|
|
424
911
|
return None
|
|
425
912
|
try:
|
|
426
|
-
|
|
913
|
+
gcloud_output = subprocess.check_output(
|
|
427
914
|
[executable, "auth", "print-access-token"],
|
|
428
915
|
stderr=subprocess.STDOUT,
|
|
429
916
|
text=True,
|
|
@@ -433,7 +920,7 @@ def get_gcloud_access_token() -> str | None:
|
|
|
433
920
|
).strip()
|
|
434
921
|
except Exception:
|
|
435
922
|
return None
|
|
436
|
-
return
|
|
923
|
+
return gcloud_output or None
|
|
437
924
|
|
|
438
925
|
|
|
439
926
|
# Cached gcloud access token + monotonic expiry. gcloud tokens last ~60 min, so the
|
|
@@ -461,10 +948,10 @@ def get_cached_gcloud_access_token() -> str | None:
|
|
|
461
948
|
if cached is not None and time.monotonic() < cached[1]:
|
|
462
949
|
return cached[0]
|
|
463
950
|
|
|
464
|
-
|
|
465
|
-
if
|
|
466
|
-
_gcloud_token_cache = (
|
|
467
|
-
return
|
|
951
|
+
fresh = get_gcloud_access_token()
|
|
952
|
+
if fresh:
|
|
953
|
+
_gcloud_token_cache = (fresh, time.monotonic() + _GCLOUD_TOKEN_TTL_SECONDS)
|
|
954
|
+
return fresh
|
|
468
955
|
|
|
469
956
|
|
|
470
957
|
def get_api_key(provider: str = "openrouter", project_root: Path = Path(".")) -> str:
|
|
@@ -481,7 +968,12 @@ def get_api_key(provider: str = "openrouter", project_root: Path = Path(".")) ->
|
|
|
481
968
|
# OLLAMA_API_KEY still flows through above if present.
|
|
482
969
|
return ""
|
|
483
970
|
if not key:
|
|
484
|
-
|
|
971
|
+
# DEBUG, not WARNING: several callers (e.g. `dev verify` building an optional
|
|
972
|
+
# LLM router) treat a missing key as an expected, gracefully-degraded state and
|
|
973
|
+
# swallow the ValueError. The raise below already carries the full actionable
|
|
974
|
+
# message for callers that surface it; a WARNING here polluted agent-facing
|
|
975
|
+
# `--json` output whenever the console handler's stream was captured.
|
|
976
|
+
logger.debug("Provider credentials not configured for %s", provider)
|
|
485
977
|
extra = (
|
|
486
978
|
" You can also authenticate with 'gcloud auth login' for vertexai."
|
|
487
979
|
if _normalized_provider_name(provider) == "vertexai"
|