devcouncil 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +49 -30
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +960 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +303 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Git diff fallbacks when no batched snapshot is available.
|
|
2
|
+
|
|
3
|
+
Extracted from ``verifier.py`` so the verifier orchestrator stays focused on gate
|
|
4
|
+
logic while pre-head / untracked / walk-the-tree paths live in one place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import fnmatch
|
|
10
|
+
import logging
|
|
11
|
+
import subprocess
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
15
|
+
|
|
16
|
+
from devcouncil.domain.checkpoint_refs import task_before_ref
|
|
17
|
+
from devcouncil.utils.proc import GIT_TIMEOUT
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from devcouncil.utils.git_snapshot import GitWorktreeSnapshot
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
IGNORED_CHANGE_PATTERNS = (
|
|
25
|
+
"__pycache__/*",
|
|
26
|
+
"*/__pycache__/*",
|
|
27
|
+
"*.pyc",
|
|
28
|
+
"*.pyo",
|
|
29
|
+
".pytest_cache/*",
|
|
30
|
+
".mypy_cache/*",
|
|
31
|
+
".ruff_cache/*",
|
|
32
|
+
".devcouncil/*",
|
|
33
|
+
"AGENTS.md",
|
|
34
|
+
"CLAUDE.md",
|
|
35
|
+
".gitignore",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
MAX_UNTRACKED_DIFF_BYTES = 256_000
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class GitDiffFallback:
|
|
43
|
+
"""Pre-snapshot git diff helpers used by :class:`~devcouncil.verification.verifier.Verifier`."""
|
|
44
|
+
|
|
45
|
+
project_root: Path
|
|
46
|
+
git_snapshot: Optional["GitWorktreeSnapshot"] = None
|
|
47
|
+
untracked_cache: Optional[List[str]] = field(default=None, repr=False)
|
|
48
|
+
# When set, diff/changed-files use ``git diff <ref>`` (PR/base scope) instead of HEAD/worktree.
|
|
49
|
+
diff_base: Optional[str] = None
|
|
50
|
+
|
|
51
|
+
def has_head(self) -> bool:
|
|
52
|
+
if self.git_snapshot is not None:
|
|
53
|
+
return self.git_snapshot.has_head
|
|
54
|
+
try:
|
|
55
|
+
return subprocess.run(
|
|
56
|
+
["git", "rev-parse", "--verify", "HEAD"],
|
|
57
|
+
cwd=self.project_root,
|
|
58
|
+
stdout=subprocess.DEVNULL,
|
|
59
|
+
stderr=subprocess.DEVNULL,
|
|
60
|
+
timeout=GIT_TIMEOUT,
|
|
61
|
+
).returncode == 0
|
|
62
|
+
except (subprocess.TimeoutExpired, OSError) as exc:
|
|
63
|
+
logger.warning("git rev-parse HEAD failed: %s", exc)
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def get_initial_repo_diff(self) -> str:
|
|
67
|
+
parts: List[str] = []
|
|
68
|
+
for cmd in (["git", "diff", "--cached"], ["git", "diff"]):
|
|
69
|
+
result = subprocess.run(
|
|
70
|
+
cmd,
|
|
71
|
+
cwd=self.project_root,
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
encoding="utf-8",
|
|
75
|
+
errors="replace",
|
|
76
|
+
timeout=GIT_TIMEOUT,
|
|
77
|
+
)
|
|
78
|
+
if result.returncode == 0 and result.stdout:
|
|
79
|
+
parts.append(result.stdout)
|
|
80
|
+
untracked_diff = self.get_untracked_files_diff()
|
|
81
|
+
if untracked_diff:
|
|
82
|
+
parts.append(untracked_diff)
|
|
83
|
+
return "\n".join(parts)
|
|
84
|
+
|
|
85
|
+
def get_status_files(self) -> List[str]:
|
|
86
|
+
files: set[str] = set()
|
|
87
|
+
commands = (
|
|
88
|
+
["git", "diff", "--cached", "--name-only"],
|
|
89
|
+
["git", "diff", "--name-only"],
|
|
90
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
91
|
+
)
|
|
92
|
+
for cmd in commands:
|
|
93
|
+
try:
|
|
94
|
+
output = subprocess.check_output(
|
|
95
|
+
cmd,
|
|
96
|
+
cwd=self.project_root,
|
|
97
|
+
stderr=subprocess.DEVNULL,
|
|
98
|
+
timeout=GIT_TIMEOUT,
|
|
99
|
+
).decode("utf-8", errors="replace").splitlines()
|
|
100
|
+
files.update(path.replace("\\", "/") for path in output if path.strip())
|
|
101
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
102
|
+
continue
|
|
103
|
+
if not files:
|
|
104
|
+
files.update(self.walk_project_files())
|
|
105
|
+
return self.filter_change_paths(sorted(files))
|
|
106
|
+
|
|
107
|
+
def get_untracked_files(self) -> List[str]:
|
|
108
|
+
if self.git_snapshot is not None:
|
|
109
|
+
return self.filter_change_paths(self.git_snapshot.untracked_files)
|
|
110
|
+
if self.untracked_cache is not None:
|
|
111
|
+
return self.untracked_cache
|
|
112
|
+
try:
|
|
113
|
+
output = subprocess.check_output(
|
|
114
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
115
|
+
cwd=self.project_root,
|
|
116
|
+
stderr=subprocess.DEVNULL,
|
|
117
|
+
timeout=GIT_TIMEOUT,
|
|
118
|
+
).decode("utf-8", errors="replace").splitlines()
|
|
119
|
+
return self.filter_change_paths(output)
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
logger.debug("Failed to list untracked files: %s", exc)
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
def get_untracked_files_diff(self) -> str:
|
|
125
|
+
parts: List[str] = []
|
|
126
|
+
for rel_path in self.get_untracked_files():
|
|
127
|
+
full_path = self.project_root / rel_path
|
|
128
|
+
if not full_path.is_file():
|
|
129
|
+
continue
|
|
130
|
+
parts.append(self.format_new_file_diff(rel_path, full_path))
|
|
131
|
+
return "\n".join(part for part in parts if part)
|
|
132
|
+
|
|
133
|
+
def format_new_file_diff(self, rel_path: str, full_path: Path) -> str:
|
|
134
|
+
try:
|
|
135
|
+
raw = full_path.read_bytes()
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
logger.debug("Failed to read untracked file %s: %s", rel_path, exc)
|
|
138
|
+
return ""
|
|
139
|
+
|
|
140
|
+
header = [
|
|
141
|
+
f"diff --git a/{rel_path} b/{rel_path}",
|
|
142
|
+
"new file mode 100644",
|
|
143
|
+
"--- /dev/null",
|
|
144
|
+
f"+++ b/{rel_path}",
|
|
145
|
+
]
|
|
146
|
+
if b"\0" in raw[:8192]:
|
|
147
|
+
return "\n".join([*header, f"Binary files /dev/null and b/{rel_path} differ"])
|
|
148
|
+
|
|
149
|
+
truncated = len(raw) > MAX_UNTRACKED_DIFF_BYTES
|
|
150
|
+
if truncated:
|
|
151
|
+
raw = raw[:MAX_UNTRACKED_DIFF_BYTES]
|
|
152
|
+
text = raw.decode("utf-8", errors="replace")
|
|
153
|
+
lines = text.splitlines()
|
|
154
|
+
if text.endswith(("\n", "\r")):
|
|
155
|
+
line_count = len(lines)
|
|
156
|
+
else:
|
|
157
|
+
line_count = max(len(lines), 1 if text else 0)
|
|
158
|
+
|
|
159
|
+
diff_lines = [*header, f"@@ -0,0 +1,{line_count} @@"]
|
|
160
|
+
if not text:
|
|
161
|
+
return "\n".join(header) + "\n"
|
|
162
|
+
|
|
163
|
+
diff_lines.extend(f"+{line}" for line in lines)
|
|
164
|
+
if truncated:
|
|
165
|
+
diff_lines.append("+[devcouncil: untracked file diff truncated]")
|
|
166
|
+
return "\n".join(diff_lines)
|
|
167
|
+
|
|
168
|
+
def walk_project_files(self) -> List[str]:
|
|
169
|
+
files: List[str] = []
|
|
170
|
+
for path in self.project_root.rglob("*"):
|
|
171
|
+
if not path.is_file():
|
|
172
|
+
continue
|
|
173
|
+
rel = path.relative_to(self.project_root).as_posix()
|
|
174
|
+
if rel.startswith(".git/"):
|
|
175
|
+
continue
|
|
176
|
+
files.append(rel)
|
|
177
|
+
return files
|
|
178
|
+
|
|
179
|
+
def filter_change_paths(self, paths: List[str]) -> List[str]:
|
|
180
|
+
return [
|
|
181
|
+
path
|
|
182
|
+
for path in (p.strip().replace("\\", "/") for p in paths)
|
|
183
|
+
if path and not self.is_ignored_change(path)
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
def is_ignored_change(self, path: str) -> bool:
|
|
187
|
+
return any(fnmatch.fnmatch(path, pattern) for pattern in IGNORED_CHANGE_PATTERNS)
|
|
188
|
+
|
|
189
|
+
def get_diff(self) -> str:
|
|
190
|
+
if self.diff_base:
|
|
191
|
+
try:
|
|
192
|
+
return subprocess.check_output(
|
|
193
|
+
["git", "diff", self.diff_base],
|
|
194
|
+
cwd=self.project_root,
|
|
195
|
+
timeout=GIT_TIMEOUT,
|
|
196
|
+
).decode("utf-8", errors="replace")
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.warning("Failed to get git diff against %s: %s", self.diff_base, e)
|
|
199
|
+
return ""
|
|
200
|
+
snap = self.git_snapshot
|
|
201
|
+
try:
|
|
202
|
+
if snap is not None:
|
|
203
|
+
if not snap.has_head:
|
|
204
|
+
parts = [snap.cached_diff, snap.working_diff]
|
|
205
|
+
untracked_diff = self.get_untracked_files_diff()
|
|
206
|
+
return "\n".join(part for part in [*parts, untracked_diff] if part)
|
|
207
|
+
untracked_diff = self.get_untracked_files_diff()
|
|
208
|
+
return "\n".join(part for part in [snap.tracked_diff, untracked_diff] if part)
|
|
209
|
+
if not self.has_head():
|
|
210
|
+
return self.get_initial_repo_diff()
|
|
211
|
+
tracked_diff = subprocess.check_output(
|
|
212
|
+
["git", "diff", "HEAD"], cwd=self.project_root, timeout=GIT_TIMEOUT
|
|
213
|
+
).decode("utf-8", errors="replace")
|
|
214
|
+
untracked_diff = self.get_untracked_files_diff()
|
|
215
|
+
return "\n".join(part for part in [tracked_diff, untracked_diff] if part)
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.warning("Failed to get git diff: %s", e)
|
|
218
|
+
return ""
|
|
219
|
+
|
|
220
|
+
def get_changed_files(self) -> List[str]:
|
|
221
|
+
if self.diff_base:
|
|
222
|
+
try:
|
|
223
|
+
output = subprocess.check_output(
|
|
224
|
+
["git", "diff", self.diff_base, "--name-only"],
|
|
225
|
+
cwd=self.project_root,
|
|
226
|
+
timeout=GIT_TIMEOUT,
|
|
227
|
+
).decode("utf-8", errors="replace").splitlines()
|
|
228
|
+
return self.filter_change_paths(output)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.warning("Failed to get changed files against %s: %s", self.diff_base, e)
|
|
231
|
+
return []
|
|
232
|
+
snap = self.git_snapshot
|
|
233
|
+
try:
|
|
234
|
+
if snap is not None:
|
|
235
|
+
if not snap.has_head:
|
|
236
|
+
return self.filter_change_paths(snap.status_paths or snap.untracked_files)
|
|
237
|
+
files = set(snap.tracked_changed_files)
|
|
238
|
+
files.update(snap.untracked_files)
|
|
239
|
+
return self.filter_change_paths(sorted(files))
|
|
240
|
+
if not self.has_head():
|
|
241
|
+
return self.get_status_files()
|
|
242
|
+
output = subprocess.check_output(
|
|
243
|
+
["git", "diff", "HEAD", "--name-only"], cwd=self.project_root, timeout=GIT_TIMEOUT
|
|
244
|
+
).decode("utf-8", errors="replace").splitlines()
|
|
245
|
+
files = set(output)
|
|
246
|
+
files.update(self.get_untracked_files())
|
|
247
|
+
return self.filter_change_paths(sorted(files))
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.warning("Failed to get changed files: %s", e)
|
|
250
|
+
return []
|
|
251
|
+
|
|
252
|
+
def committed_task_diff(self, task_id: str) -> str:
|
|
253
|
+
"""Diff of work committed since the task's ``before`` checkpoint, or ""."""
|
|
254
|
+
before_ref = task_before_ref(task_id)
|
|
255
|
+
try:
|
|
256
|
+
has_ref = subprocess.run(
|
|
257
|
+
["git", "rev-parse", "--verify", before_ref],
|
|
258
|
+
cwd=self.project_root,
|
|
259
|
+
stdout=subprocess.DEVNULL,
|
|
260
|
+
stderr=subprocess.DEVNULL,
|
|
261
|
+
timeout=GIT_TIMEOUT,
|
|
262
|
+
).returncode == 0
|
|
263
|
+
if has_ref:
|
|
264
|
+
return subprocess.check_output(
|
|
265
|
+
["git", "diff", before_ref],
|
|
266
|
+
cwd=self.project_root,
|
|
267
|
+
stderr=subprocess.DEVNULL,
|
|
268
|
+
timeout=GIT_TIMEOUT,
|
|
269
|
+
).decode("utf-8", errors="replace")
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.debug("Failed to read committed task diff for %s: %s", task_id, e)
|
|
272
|
+
return ""
|
|
@@ -47,6 +47,19 @@ Your task is to identify if the implementation is complete, correct, and follows
|
|
|
47
47
|
- Identify architectural drift.
|
|
48
48
|
- Identify security risks not caught by static scans.
|
|
49
49
|
|
|
50
|
+
Review rules — follow them exactly:
|
|
51
|
+
- The diff above IS the complete, authoritative record of the change. Review the CODE in it.
|
|
52
|
+
Do not ask for proof that the change was made, and do not report "no evidence of
|
|
53
|
+
implementation" — you are looking at the implementation.
|
|
54
|
+
- Every finding must cite the specific file/hunk in the diff that demonstrates the problem
|
|
55
|
+
(in its 'evidence'). A concern you cannot tie to a concrete line in the diff is not a finding.
|
|
56
|
+
- Do not review prose, commit-message style, response formatting, or your estimate of effort.
|
|
57
|
+
- Severity rubric: 'critical' is reserved for a demonstrable correctness or security defect
|
|
58
|
+
visible in the diff (wrong behavior, data loss, injection, broken requirement). Missing
|
|
59
|
+
edge cases are 'high' at most; style and structure are 'low'.
|
|
60
|
+
- If the diff implements the requirements correctly, return is_satisfactory=true and an
|
|
61
|
+
empty findings list. An empty review of correct code is a correct review.
|
|
62
|
+
|
|
50
63
|
Return a JSON object with 'is_satisfactory' and a list of 'findings' (as Gap objects).
|
|
51
64
|
"""
|
|
52
65
|
messages = [{"role": "user", "content": prompt}]
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Incremental gate runner: the sub-second sidecar behind ``dev check --watch``.
|
|
2
|
+
|
|
3
|
+
Ties :mod:`devcouncil.verification.gate_selector` (path → relevant/narrowed gates) to
|
|
4
|
+
:mod:`devcouncil.verification.gate_cache` (content-hash skip of previously-green gates).
|
|
5
|
+
On an iterative edit only the gates whose inputs actually changed are executed; the rest
|
|
6
|
+
are served from cache. This is deliberately separate from the full ``verify_task`` path,
|
|
7
|
+
which release / ``dev go`` still run in full and which never consults this cache.
|
|
8
|
+
|
|
9
|
+
Given the working-tree changed files and the project's configured commands, it:
|
|
10
|
+
1. selects the gates relevant to the changed stacks (and narrows linters to the touched
|
|
11
|
+
files),
|
|
12
|
+
2. skips any gate that passed last time with byte-identical inputs,
|
|
13
|
+
3. runs the remainder and records their pass/fail in the cache,
|
|
14
|
+
4. returns a compact, render-friendly result.
|
|
15
|
+
|
|
16
|
+
Best-effort and side-effect-light: it shells out only to run the gates themselves and
|
|
17
|
+
persists a single small JSON cache file.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import time
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Callable, List, Mapping, Optional, Sequence
|
|
27
|
+
|
|
28
|
+
from devcouncil.utils.json_persist import read_json
|
|
29
|
+
from devcouncil.verification.gate_cache import GateResultCache
|
|
30
|
+
from devcouncil.verification.gate_selector import GateSpec, select_gates
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class GateOutcome:
|
|
37
|
+
name: str
|
|
38
|
+
kind: str
|
|
39
|
+
command: str
|
|
40
|
+
passed: bool
|
|
41
|
+
cached: bool
|
|
42
|
+
summary: str = ""
|
|
43
|
+
duration_s: float = 0.0
|
|
44
|
+
narrowed: bool = False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class IncrementalResult:
|
|
49
|
+
changed_files: List[str] = field(default_factory=list)
|
|
50
|
+
outcomes: List[GateOutcome] = field(default_factory=list)
|
|
51
|
+
skipped: List[tuple[str, str]] = field(default_factory=list)
|
|
52
|
+
duration_s: float = 0.0
|
|
53
|
+
no_changes: bool = False
|
|
54
|
+
no_gates: bool = False
|
|
55
|
+
narrowed: bool = False
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def ran(self) -> List[GateOutcome]:
|
|
59
|
+
return [o for o in self.outcomes if not o.cached]
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def cached(self) -> List[GateOutcome]:
|
|
63
|
+
return [o for o in self.outcomes if o.cached]
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def passed(self) -> bool:
|
|
67
|
+
return all(o.passed for o in self.outcomes)
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
return {
|
|
71
|
+
"passed": self.passed,
|
|
72
|
+
"no_changes": self.no_changes,
|
|
73
|
+
"no_gates": self.no_gates,
|
|
74
|
+
"narrowed": self.narrowed,
|
|
75
|
+
"changed_files": self.changed_files,
|
|
76
|
+
"duration_s": round(self.duration_s, 4),
|
|
77
|
+
"gates": [
|
|
78
|
+
{
|
|
79
|
+
"name": o.name,
|
|
80
|
+
"kind": o.kind,
|
|
81
|
+
"command": o.command,
|
|
82
|
+
"passed": o.passed,
|
|
83
|
+
"cached": o.cached,
|
|
84
|
+
"narrowed": o.narrowed,
|
|
85
|
+
"duration_s": round(o.duration_s, 4),
|
|
86
|
+
}
|
|
87
|
+
for o in self.outcomes
|
|
88
|
+
],
|
|
89
|
+
"ran": len(self.ran),
|
|
90
|
+
"cached_hits": len(self.cached),
|
|
91
|
+
"skipped": [{"command": c, "reason": r} for c, r in self.skipped],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _default_commands(project_root: Path) -> dict[str, list[str]]:
|
|
96
|
+
"""Configured test/lint/typecheck commands, or ``{}`` on any config failure."""
|
|
97
|
+
try:
|
|
98
|
+
from devcouncil.app.config import load_config
|
|
99
|
+
|
|
100
|
+
cfg = load_config(project_root)
|
|
101
|
+
return {
|
|
102
|
+
"test": list(cfg.commands.test),
|
|
103
|
+
"lint": list(cfg.commands.lint),
|
|
104
|
+
"typecheck": list(cfg.commands.typecheck),
|
|
105
|
+
}
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
logger.debug("incremental gates: config commands unavailable: %s", exc)
|
|
108
|
+
return {}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _load_repo_map(project_root: Path) -> dict | None:
|
|
112
|
+
"""Best-effort load of ``.devcouncil/repo_map.json`` for dependent expansion."""
|
|
113
|
+
map_path = project_root / ".devcouncil" / "repo_map.json"
|
|
114
|
+
if not map_path.is_file():
|
|
115
|
+
return None
|
|
116
|
+
try:
|
|
117
|
+
data = read_json(map_path)
|
|
118
|
+
return data if isinstance(data, dict) else None
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
logger.debug("incremental gates: repo map unavailable: %s", exc)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _default_changed_files(project_root: Path) -> list[str]:
|
|
125
|
+
from devcouncil.verification.verifier import Verifier
|
|
126
|
+
|
|
127
|
+
return Verifier(project_root).get_changed_files()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _default_runner(project_root: Path, timeout: int) -> Callable[[str], tuple[bool, str]]:
|
|
131
|
+
from devcouncil.verification.command_runner import run_verification_command
|
|
132
|
+
|
|
133
|
+
def _run(command: str) -> tuple[bool, str]:
|
|
134
|
+
result = run_verification_command(
|
|
135
|
+
project_root, command, task_id="check-watch", timeout=timeout
|
|
136
|
+
)
|
|
137
|
+
return result.exit_code == 0, result.summary
|
|
138
|
+
|
|
139
|
+
return _run
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def run_incremental_gates(
|
|
143
|
+
project_root: Path,
|
|
144
|
+
*,
|
|
145
|
+
commands: Optional[Mapping[str, Sequence[str]]] = None,
|
|
146
|
+
extra_test_commands: Optional[Sequence[str]] = None,
|
|
147
|
+
changed_files: Optional[Sequence[str]] = None,
|
|
148
|
+
cache: Optional[GateResultCache] = None,
|
|
149
|
+
use_cache: bool = True,
|
|
150
|
+
narrow: bool = True,
|
|
151
|
+
runner: Optional[Callable[[str], tuple[bool, str]]] = None,
|
|
152
|
+
timeout: int = 300,
|
|
153
|
+
) -> IncrementalResult:
|
|
154
|
+
"""Run only the gates affected by ``changed_files``, skipping cached-green ones.
|
|
155
|
+
|
|
156
|
+
``commands`` defaults to the project's configured test/lint/typecheck commands;
|
|
157
|
+
``extra_test_commands`` (e.g. ``dev check --test``) are appended to the test kind.
|
|
158
|
+
``runner`` is a ``command -> (passed, summary)`` callable (defaults to the real
|
|
159
|
+
verification command runner) — the seam that keeps this unit-testable without
|
|
160
|
+
subprocesses.
|
|
161
|
+
"""
|
|
162
|
+
started = time.perf_counter()
|
|
163
|
+
root = Path(project_root)
|
|
164
|
+
|
|
165
|
+
if changed_files is None:
|
|
166
|
+
changed_files = _default_changed_files(root)
|
|
167
|
+
changed = [p for p in changed_files if p and p.strip()]
|
|
168
|
+
|
|
169
|
+
resolved_commands: dict[str, list[str]] = {
|
|
170
|
+
k: list(v) for k, v in (commands or _default_commands(root)).items()
|
|
171
|
+
}
|
|
172
|
+
if extra_test_commands:
|
|
173
|
+
resolved_commands.setdefault("test", [])
|
|
174
|
+
resolved_commands["test"] = list(resolved_commands["test"]) + [
|
|
175
|
+
c for c in extra_test_commands if c and c.strip()
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
if not changed:
|
|
179
|
+
return IncrementalResult(no_changes=True, duration_s=time.perf_counter() - started)
|
|
180
|
+
|
|
181
|
+
repo_map = _load_repo_map(root)
|
|
182
|
+
selection = select_gates(changed, resolved_commands, narrow=narrow, repo_map=repo_map)
|
|
183
|
+
any_narrowed = any(g.narrowed for g in selection.gates)
|
|
184
|
+
if not selection.gates:
|
|
185
|
+
return IncrementalResult(
|
|
186
|
+
changed_files=changed,
|
|
187
|
+
skipped=selection.skipped,
|
|
188
|
+
no_gates=True,
|
|
189
|
+
narrowed=any_narrowed,
|
|
190
|
+
duration_s=time.perf_counter() - started,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if cache is None:
|
|
194
|
+
cache = GateResultCache(root)
|
|
195
|
+
cache.load()
|
|
196
|
+
run = runner or _default_runner(root, timeout)
|
|
197
|
+
|
|
198
|
+
outcomes: List[GateOutcome] = []
|
|
199
|
+
for gate in selection.gates:
|
|
200
|
+
if use_cache and cache.is_green(gate):
|
|
201
|
+
outcomes.append(GateOutcome(
|
|
202
|
+
name=gate.name, kind=gate.kind, command=gate.command,
|
|
203
|
+
passed=True, cached=True,
|
|
204
|
+
summary=cache.cached_summary(gate) or "", narrowed=gate.narrowed,
|
|
205
|
+
))
|
|
206
|
+
continue
|
|
207
|
+
gate_started = time.perf_counter()
|
|
208
|
+
try:
|
|
209
|
+
passed, summary = run(gate.command)
|
|
210
|
+
except Exception as exc: # a gate crash must not kill the sidecar loop
|
|
211
|
+
logger.debug("incremental gate %s crashed: %s", gate.name, exc)
|
|
212
|
+
passed, summary = False, f"gate crashed: {exc}"
|
|
213
|
+
gate_dt = time.perf_counter() - gate_started
|
|
214
|
+
cache.record(gate, passed=passed, summary=summary)
|
|
215
|
+
outcomes.append(GateOutcome(
|
|
216
|
+
name=gate.name, kind=gate.kind, command=gate.command,
|
|
217
|
+
passed=passed, cached=False, summary=summary,
|
|
218
|
+
duration_s=gate_dt, narrowed=gate.narrowed,
|
|
219
|
+
))
|
|
220
|
+
|
|
221
|
+
cache.save()
|
|
222
|
+
return IncrementalResult(
|
|
223
|
+
changed_files=changed,
|
|
224
|
+
outcomes=outcomes,
|
|
225
|
+
skipped=selection.skipped,
|
|
226
|
+
narrowed=any_narrowed,
|
|
227
|
+
duration_s=time.perf_counter() - started,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def selected_gate_specs(
|
|
232
|
+
project_root: Path,
|
|
233
|
+
changed_files: Sequence[str],
|
|
234
|
+
*,
|
|
235
|
+
commands: Optional[Mapping[str, Sequence[str]]] = None,
|
|
236
|
+
narrow: bool = True,
|
|
237
|
+
) -> list[GateSpec]:
|
|
238
|
+
"""Convenience: the gates that *would* run for these changed files (no execution)."""
|
|
239
|
+
resolved = commands or _default_commands(Path(project_root))
|
|
240
|
+
repo_map = _load_repo_map(Path(project_root))
|
|
241
|
+
return select_gates(changed_files, resolved, narrow=narrow, repo_map=repo_map).gates
|
|
@@ -40,6 +40,14 @@ _CATEGORY_BY_GAP_TYPE = {
|
|
|
40
40
|
"migration_gap": "fix_code",
|
|
41
41
|
"requirement_not_planned": "plan",
|
|
42
42
|
"task_not_implemented": "plan",
|
|
43
|
+
"stub_detected": "fix_code",
|
|
44
|
+
"stub_declared": "review",
|
|
45
|
+
"suspicious_effort": "review",
|
|
46
|
+
"coarse_acceptance_proof": "add_test",
|
|
47
|
+
"unwired_file": "fix_code",
|
|
48
|
+
"dead_symbol": "fix_code",
|
|
49
|
+
"stranded_code": "fix_code",
|
|
50
|
+
"stale_map": "refresh_map",
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
|
|
@@ -85,7 +93,10 @@ def _derive_file(gap: Gap) -> Optional[str]:
|
|
|
85
93
|
def _action_text(gap: Gap, file: Optional[str]) -> str:
|
|
86
94
|
target = file or "the affected file"
|
|
87
95
|
if gap.gap_type == "orphan_diff":
|
|
88
|
-
return
|
|
96
|
+
return (
|
|
97
|
+
f"Revert changes to {target} or append it with "
|
|
98
|
+
f"`dev scope update <task_id> --lease-token <token> --planned-file {target}`."
|
|
99
|
+
)
|
|
89
100
|
if gap.gap_type == "planned_file_not_changed":
|
|
90
101
|
return f"Modify {target} as planned, or remove it from the task's planned files."
|
|
91
102
|
if gap.gap_type == "dependency_risk":
|
|
@@ -104,6 +115,54 @@ def _action_text(gap: Gap, file: Optional[str]) -> str:
|
|
|
104
115
|
return "Remove the detected secret/finding from the diff and rotate any exposed credential."
|
|
105
116
|
if gap.gap_type == "architecture_drift":
|
|
106
117
|
return "Address the flagged change or resolve the open critique card, then re-verify."
|
|
118
|
+
if gap.gap_type == "stub_detected":
|
|
119
|
+
loc = f"{target}{':' + str(gap.line) if gap.line else ''}"
|
|
120
|
+
return (
|
|
121
|
+
f"Replace the stub/placeholder at {loc} with a real implementation, then re-verify. "
|
|
122
|
+
"Do not mark work complete while placeholders remain."
|
|
123
|
+
)
|
|
124
|
+
if gap.gap_type == "stub_declared":
|
|
125
|
+
loc = f"{target}{':' + str(gap.line) if gap.line else ''}"
|
|
126
|
+
return (
|
|
127
|
+
f"Review the intentional stub declared at {loc}; replace it before marking done."
|
|
128
|
+
)
|
|
129
|
+
if gap.gap_type == "suspicious_effort":
|
|
130
|
+
return (
|
|
131
|
+
"The diff looks too small or superficial for the planned scope. Complete the "
|
|
132
|
+
"planned work (or restore removed tests), then re-verify."
|
|
133
|
+
)
|
|
134
|
+
if gap.gap_type == "coarse_acceptance_proof":
|
|
135
|
+
return (
|
|
136
|
+
"Acceptance criteria were proven only by a coarse passing command, not a "
|
|
137
|
+
"per-criterion check. Add a verification command or test that exercises each "
|
|
138
|
+
"listed criterion specifically, then re-verify."
|
|
139
|
+
)
|
|
140
|
+
if gap.gap_type == "unwired_file":
|
|
141
|
+
return (
|
|
142
|
+
f"Import or register `{target}` from its intended non-test caller "
|
|
143
|
+
f"(append the caller with `dev scope update <task_id> --lease-token <token> "
|
|
144
|
+
f"--planned-file <caller>` if needed), or delete the unused file."
|
|
145
|
+
)
|
|
146
|
+
if gap.gap_type == "dead_symbol":
|
|
147
|
+
loc = f"{target}{':' + str(gap.line) if gap.line else ''}"
|
|
148
|
+
name = ""
|
|
149
|
+
for item in gap.evidence:
|
|
150
|
+
if isinstance(item, str) and item.startswith("symbol:"):
|
|
151
|
+
name = item.split(":", 1)[1].strip()
|
|
152
|
+
break
|
|
153
|
+
symbol = name or "the new symbol"
|
|
154
|
+
return (
|
|
155
|
+
f"Call or register `{symbol}` at {loc} from the code that needs it "
|
|
156
|
+
f"(use `dev scope update ... --planned-file <caller>` if the caller is "
|
|
157
|
+
f"out of scope), or remove it."
|
|
158
|
+
)
|
|
159
|
+
if gap.gap_type == "stranded_code":
|
|
160
|
+
return (
|
|
161
|
+
f"Restore the import/call that kept `{target}` live, or delete the "
|
|
162
|
+
"stranded module if it is intentionally unused."
|
|
163
|
+
)
|
|
164
|
+
if gap.gap_type == "stale_map":
|
|
165
|
+
return "Run `dev map` to regenerate the repository map, then re-verify."
|
|
107
166
|
# Fall back to the gap's own recommended fix for anything unmapped.
|
|
108
167
|
return gap.recommended_fix
|
|
109
168
|
|