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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Git ref names for per-task checkpoints.
|
|
2
|
+
|
|
3
|
+
Owned by the domain layer so the writer (execution.checkpoints.CheckpointService)
|
|
4
|
+
and the reader (verification.verifier.Verifier) share one definition of the ref
|
|
5
|
+
layout without importing each other — checkpoints.py imports Verifier, so the
|
|
6
|
+
verifier must not import checkpoints back. This module must stay dependency-free
|
|
7
|
+
(no imports from execution/ or verification/).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
TASK_REF_PREFIX = "refs/devcouncil/tasks"
|
|
11
|
+
|
|
12
|
+
REF_BEFORE_TEMPLATE = TASK_REF_PREFIX + "/{task_id}/before"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def task_before_ref(task_id: str) -> str:
|
|
16
|
+
"""Ref pointing at the snapshot taken before ``task_id`` started."""
|
|
17
|
+
return REF_BEFORE_TEMPLATE.format(task_id=task_id)
|
|
@@ -22,6 +22,16 @@ class Gap(BaseModel):
|
|
|
22
22
|
"quality_gate_failed",
|
|
23
23
|
"skipped_verification_command",
|
|
24
24
|
"coarse_acceptance_proof",
|
|
25
|
+
"stub_detected",
|
|
26
|
+
"stub_declared",
|
|
27
|
+
"suspicious_effort",
|
|
28
|
+
"unwired_file",
|
|
29
|
+
"dead_symbol",
|
|
30
|
+
"stranded_code",
|
|
31
|
+
"stale_map",
|
|
32
|
+
"corpus_stale",
|
|
33
|
+
"doc_code_ref",
|
|
34
|
+
"acceptance_corpus",
|
|
25
35
|
]
|
|
26
36
|
requirement_id: Optional[str] = None
|
|
27
37
|
task_id: Optional[str] = None
|
|
@@ -18,5 +18,9 @@ class Requirement(BaseModel):
|
|
|
18
18
|
title: str
|
|
19
19
|
description: str
|
|
20
20
|
priority: Literal["low", "medium", "high", "critical"]
|
|
21
|
-
|
|
21
|
+
# Defaulted: requirements come back through LLM round-trips (arbiter/critic
|
|
22
|
+
# rewrites) where weaker models sometimes drop provenance metadata; a missing
|
|
23
|
+
# source shouldn't invalidate an otherwise-sound requirement. "planner" is the
|
|
24
|
+
# most common origin for machine-generated requirements.
|
|
25
|
+
source: Literal["user", "planner", "critic", "arbiter"] = "planner"
|
|
22
26
|
acceptance_criteria: List[AcceptanceCriterion] = Field(default_factory=list)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from pydantic import BaseModel, Field
|
|
2
|
-
from typing import Literal, List
|
|
2
|
+
from typing import Literal, List, Optional
|
|
3
3
|
|
|
4
4
|
class PlannedFile(BaseModel):
|
|
5
5
|
path: str
|
|
@@ -27,6 +27,14 @@ class Task(BaseModel):
|
|
|
27
27
|
"repo is already configured for them."
|
|
28
28
|
),
|
|
29
29
|
)
|
|
30
|
+
agent_appended_expected_tests: List[str] = Field(
|
|
31
|
+
default_factory=list,
|
|
32
|
+
description=(
|
|
33
|
+
"Subset of expected_tests appended at runtime by a leased agent via "
|
|
34
|
+
"devcouncil_update_task_scope. These may run during verification but "
|
|
35
|
+
"cannot coarse-prove acceptance criteria."
|
|
36
|
+
),
|
|
37
|
+
)
|
|
30
38
|
allowed_commands: List[str] = Field(
|
|
31
39
|
default_factory=list,
|
|
32
40
|
description=(
|
|
@@ -36,7 +44,39 @@ class Task(BaseModel):
|
|
|
36
44
|
"only when the task needs no commands beyond those in expected_tests."
|
|
37
45
|
),
|
|
38
46
|
)
|
|
47
|
+
agent_appended_allowed_commands: List[str] = Field(
|
|
48
|
+
default_factory=list,
|
|
49
|
+
description=(
|
|
50
|
+
"Subset of allowed_commands appended at runtime by a leased agent via "
|
|
51
|
+
"devcouncil_update_task_scope. These may run during execution/verification "
|
|
52
|
+
"but cannot coarse-prove acceptance criteria (self-certification guard, "
|
|
53
|
+
"mirroring agent_appended_expected_tests)."
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
agent_appended_planned_files: List[str] = Field(
|
|
57
|
+
default_factory=list,
|
|
58
|
+
description=(
|
|
59
|
+
"Paths appended to planned_files at runtime by a leased agent via "
|
|
60
|
+
"devcouncil_update_task_scope / `dev scope update --planned-file`. "
|
|
61
|
+
"Always added as modify-op only; secret/restricted paths are rejected. "
|
|
62
|
+
"Lets an agent wire an unwired file into its intended caller without "
|
|
63
|
+
"hitting orphan_diff / write-policy deadlock."
|
|
64
|
+
),
|
|
65
|
+
)
|
|
39
66
|
forbidden_changes: List[str] = Field(default_factory=list)
|
|
67
|
+
difficulty: Optional[Literal["easy", "normal", "hard"]] = Field(
|
|
68
|
+
default=None,
|
|
69
|
+
description=(
|
|
70
|
+
"Manual difficulty override. When set (by a planner or a human) it wins over "
|
|
71
|
+
"the deterministic estimator in devcouncil.verification.difficulty; hard tasks "
|
|
72
|
+
"get stricter verification (stub/effort gates block, coverage enforced) per "
|
|
73
|
+
"the verification.rigor config."
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
priority: Optional[Literal["high", "medium", "low"]] = Field(
|
|
77
|
+
default=None,
|
|
78
|
+
description="Optional human/planner priority hint (high/medium/low). Default None.",
|
|
79
|
+
)
|
|
40
80
|
depends_on: List[str] = Field(
|
|
41
81
|
default_factory=list,
|
|
42
82
|
description=(
|
|
@@ -51,5 +91,6 @@ class Task(BaseModel):
|
|
|
51
91
|
"running",
|
|
52
92
|
"blocked",
|
|
53
93
|
"verified",
|
|
54
|
-
"done"
|
|
94
|
+
"done",
|
|
95
|
+
"cancelled",
|
|
55
96
|
] = "planned"
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import json
|
|
6
5
|
import logging
|
|
7
6
|
import os
|
|
8
7
|
import subprocess
|
|
@@ -12,6 +11,10 @@ from pathlib import Path
|
|
|
12
11
|
|
|
13
12
|
from pydantic import BaseModel
|
|
14
13
|
|
|
14
|
+
from devcouncil.domain.checkpoint_refs import REF_BEFORE_TEMPLATE
|
|
15
|
+
from devcouncil.utils.fsio import atomic_write_text
|
|
16
|
+
from devcouncil.utils.proc import GIT_TIMEOUT
|
|
17
|
+
from devcouncil.utils.json_persist import write_json
|
|
15
18
|
from devcouncil.verification.verifier import Verifier
|
|
16
19
|
|
|
17
20
|
logger = logging.getLogger(__name__)
|
|
@@ -27,9 +30,10 @@ class CheckpointResult(BaseModel):
|
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
class CheckpointService:
|
|
30
|
-
|
|
33
|
+
# The before-ref layout lives in devcouncil.domain.checkpoint_refs because the
|
|
34
|
+
# Verifier reads it too; kept as a class attr for compat with .format() callers.
|
|
35
|
+
REF_BEFORE = REF_BEFORE_TEMPLATE
|
|
31
36
|
REF_AFTER = "refs/devcouncil/tasks/{task_id}/after"
|
|
32
|
-
REF_ATTEMPT = "refs/devcouncil/tasks/{task_id}/attempts/{attempt}"
|
|
33
37
|
|
|
34
38
|
def __init__(self, project_root: Path):
|
|
35
39
|
self.project_root = project_root.resolve()
|
|
@@ -42,10 +46,6 @@ class CheckpointService:
|
|
|
42
46
|
def create_after(self, task_id: str) -> CheckpointResult:
|
|
43
47
|
return self._create(task_id, stage="after")
|
|
44
48
|
|
|
45
|
-
def create_attempt(self, task_id: str, attempt: int) -> CheckpointResult:
|
|
46
|
-
ref_template = self.REF_ATTEMPT.format(task_id=task_id, attempt=attempt)
|
|
47
|
-
return self._create(task_id, stage="attempt", ref_name=ref_template)
|
|
48
|
-
|
|
49
49
|
def rollback(self, task_id: str) -> CheckpointResult:
|
|
50
50
|
logger.info("Rollback requested for %s", task_id)
|
|
51
51
|
before_ref = self.REF_BEFORE.format(task_id=task_id)
|
|
@@ -60,6 +60,7 @@ class CheckpointService:
|
|
|
60
60
|
text=True,
|
|
61
61
|
encoding="utf-8",
|
|
62
62
|
errors="replace",
|
|
63
|
+
timeout=GIT_TIMEOUT,
|
|
63
64
|
)
|
|
64
65
|
if diff.strip():
|
|
65
66
|
subprocess.run(
|
|
@@ -68,6 +69,7 @@ class CheckpointService:
|
|
|
68
69
|
input=diff,
|
|
69
70
|
text=True,
|
|
70
71
|
check=True,
|
|
72
|
+
timeout=GIT_TIMEOUT,
|
|
71
73
|
)
|
|
72
74
|
return CheckpointResult(
|
|
73
75
|
task_id=task_id,
|
|
@@ -75,7 +77,7 @@ class CheckpointService:
|
|
|
75
77
|
git_ref_created=True,
|
|
76
78
|
message="Rolled back using git refs.",
|
|
77
79
|
)
|
|
78
|
-
except subprocess.CalledProcessError as exc:
|
|
80
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
|
|
79
81
|
return CheckpointResult(
|
|
80
82
|
task_id=task_id,
|
|
81
83
|
message=f"Git ref rollback failed: {exc}",
|
|
@@ -86,13 +88,14 @@ class CheckpointService:
|
|
|
86
88
|
subprocess.check_call(
|
|
87
89
|
["git", "apply", "-R", str(after_patch)],
|
|
88
90
|
cwd=self.project_root,
|
|
91
|
+
timeout=GIT_TIMEOUT,
|
|
89
92
|
)
|
|
90
93
|
return CheckpointResult(
|
|
91
94
|
task_id=task_id,
|
|
92
95
|
patch_path=str(after_patch),
|
|
93
96
|
message="Rolled back using after patch.",
|
|
94
97
|
)
|
|
95
|
-
except subprocess.CalledProcessError as exc:
|
|
98
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
|
|
96
99
|
return CheckpointResult(
|
|
97
100
|
task_id=task_id,
|
|
98
101
|
patch_path=str(after_patch),
|
|
@@ -104,23 +107,6 @@ class CheckpointService:
|
|
|
104
107
|
message="No checkpoint refs or after patch found.",
|
|
105
108
|
)
|
|
106
109
|
|
|
107
|
-
def import_legacy_patch(self, task_id: str) -> CheckpointResult:
|
|
108
|
-
before_patch = self.checkpoint_dir / f"{task_id}-before.patch"
|
|
109
|
-
if not before_patch.exists():
|
|
110
|
-
return CheckpointResult(
|
|
111
|
-
task_id=task_id,
|
|
112
|
-
message="No legacy before patch to import.",
|
|
113
|
-
)
|
|
114
|
-
ref = self.REF_BEFORE.format(task_id=task_id)
|
|
115
|
-
created = self._update_ref(ref)
|
|
116
|
-
return CheckpointResult(
|
|
117
|
-
task_id=task_id,
|
|
118
|
-
ref=ref if created else None,
|
|
119
|
-
patch_path=str(before_patch),
|
|
120
|
-
git_ref_created=created,
|
|
121
|
-
message="Imported legacy before patch ref when possible.",
|
|
122
|
-
)
|
|
123
|
-
|
|
124
110
|
def _create(
|
|
125
111
|
self,
|
|
126
112
|
task_id: str,
|
|
@@ -145,7 +131,7 @@ class CheckpointService:
|
|
|
145
131
|
try:
|
|
146
132
|
diff = verifier.get_diff()
|
|
147
133
|
if diff:
|
|
148
|
-
patch_path
|
|
134
|
+
atomic_write_text(patch_path, diff)
|
|
149
135
|
except Exception as exc:
|
|
150
136
|
# Without a patch (and if the ref also failed) rollback is impossible —
|
|
151
137
|
# never let this fail silently.
|
|
@@ -157,7 +143,7 @@ class CheckpointService:
|
|
|
157
143
|
"changed_files": verifier.get_changed_files(),
|
|
158
144
|
}
|
|
159
145
|
snapshot_path = self.checkpoint_dir / f"{task_id}-before.json"
|
|
160
|
-
snapshot_path
|
|
146
|
+
write_json(snapshot_path, snapshot)
|
|
161
147
|
json_path = str(snapshot_path)
|
|
162
148
|
|
|
163
149
|
# Routine bookkeeping (fires before+after every task and repair attempt) — DEBUG
|
|
@@ -188,15 +174,17 @@ class CheckpointService:
|
|
|
188
174
|
text=True,
|
|
189
175
|
encoding="utf-8",
|
|
190
176
|
errors="replace",
|
|
177
|
+
timeout=GIT_TIMEOUT,
|
|
191
178
|
).strip()
|
|
192
179
|
if not target:
|
|
193
180
|
return False
|
|
194
181
|
subprocess.check_call(
|
|
195
182
|
["git", "update-ref", ref, target],
|
|
196
183
|
cwd=self.project_root,
|
|
184
|
+
timeout=GIT_TIMEOUT,
|
|
197
185
|
)
|
|
198
186
|
return True
|
|
199
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
187
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
200
188
|
return False
|
|
201
189
|
|
|
202
190
|
def _snapshot_commit(self) -> str | None:
|
|
@@ -218,14 +206,18 @@ class CheckpointService:
|
|
|
218
206
|
subprocess.run(
|
|
219
207
|
["git", "read-tree", "HEAD"],
|
|
220
208
|
cwd=self.project_root, env=env, check=True, capture_output=True, text=True,
|
|
209
|
+
timeout=GIT_TIMEOUT,
|
|
221
210
|
)
|
|
211
|
+
# Staging the full tree scales with repo size — give it extra room.
|
|
222
212
|
subprocess.run(
|
|
223
213
|
["git", "add", "-A", "--", ".", ":(exclude).devcouncil"],
|
|
224
214
|
cwd=self.project_root, env=env, check=True, capture_output=True, text=True,
|
|
215
|
+
timeout=GIT_TIMEOUT * 2,
|
|
225
216
|
)
|
|
226
217
|
tree = subprocess.check_output(
|
|
227
218
|
["git", "write-tree"],
|
|
228
219
|
cwd=self.project_root, env=env, text=True, encoding="utf-8", errors="replace",
|
|
220
|
+
timeout=GIT_TIMEOUT,
|
|
229
221
|
).strip()
|
|
230
222
|
if not tree:
|
|
231
223
|
return None
|
|
@@ -233,9 +225,10 @@ class CheckpointService:
|
|
|
233
225
|
["git", "-c", "user.name=DevCouncil", "-c", "user.email=devcouncil@local",
|
|
234
226
|
"commit-tree", tree, "-p", "HEAD", "-m", "devcouncil checkpoint"],
|
|
235
227
|
cwd=self.project_root, text=True, encoding="utf-8", errors="replace",
|
|
228
|
+
timeout=GIT_TIMEOUT,
|
|
236
229
|
).strip()
|
|
237
230
|
return commit or None
|
|
238
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
231
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
239
232
|
return None
|
|
240
233
|
finally:
|
|
241
234
|
try:
|
|
@@ -250,7 +243,8 @@ class CheckpointService:
|
|
|
250
243
|
cwd=self.project_root,
|
|
251
244
|
stderr=subprocess.DEVNULL,
|
|
252
245
|
text=True,
|
|
246
|
+
timeout=GIT_TIMEOUT,
|
|
253
247
|
)
|
|
254
248
|
return True
|
|
255
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
249
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
|
256
250
|
return False
|
|
@@ -1,60 +1,31 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from typing import List
|
|
3
|
+
|
|
3
4
|
from devcouncil.domain.task import Task
|
|
4
|
-
|
|
5
|
-
from devcouncil.utils.redaction import redact_string
|
|
6
|
-
import json
|
|
5
|
+
|
|
7
6
|
|
|
8
7
|
class ContextBuilder:
|
|
9
|
-
"""Gathers repo-level
|
|
10
|
-
|
|
8
|
+
"""Gathers repo-level context for agent prompts."""
|
|
9
|
+
|
|
11
10
|
def __init__(self, project_root: Path):
|
|
12
11
|
self.project_root = project_root
|
|
13
12
|
|
|
14
|
-
def build_task_context(self, task: Task, requirements: List[Requirement]) -> str:
|
|
15
|
-
"""Collects all relevant information for implementing a task."""
|
|
16
|
-
|
|
17
|
-
# 1. Map requirements relevant to this task
|
|
18
|
-
req_map = {r.id: r for r in requirements}
|
|
19
|
-
task_reqs = [req_map[rid] for rid in task.requirement_ids if rid in req_map]
|
|
20
|
-
|
|
21
|
-
# 2. Gather content of planned files (if they exist)
|
|
22
|
-
file_contents = {}
|
|
23
|
-
for pf in task.planned_files:
|
|
24
|
-
file_path = self.project_root / pf.path
|
|
25
|
-
if file_path.exists() and file_path.is_file():
|
|
26
|
-
try:
|
|
27
|
-
raw = file_path.read_text(encoding="utf-8")
|
|
28
|
-
file_contents[pf.path] = redact_string(raw)
|
|
29
|
-
except Exception:
|
|
30
|
-
file_contents[pf.path] = "[Error reading file]"
|
|
31
|
-
else:
|
|
32
|
-
file_contents[pf.path] = "[New file - does not exist yet]"
|
|
33
|
-
|
|
34
|
-
# 3. Assemble the context string
|
|
35
|
-
context = {
|
|
36
|
-
"task": task.model_dump(),
|
|
37
|
-
"relevant_requirements": [r.model_dump() for r in task_reqs],
|
|
38
|
-
"file_contents": file_contents,
|
|
39
|
-
"project_structure": self.get_structure_summary(task)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return json.dumps(context, indent=2)
|
|
43
|
-
|
|
44
13
|
def get_structure_summary(self, task: Task | None = None) -> List[str]:
|
|
45
14
|
"""Simple list of files in the project for context."""
|
|
46
15
|
try:
|
|
47
|
-
import
|
|
48
|
-
|
|
49
|
-
|
|
16
|
+
from devcouncil.utils.proc import git_output
|
|
17
|
+
|
|
18
|
+
output = git_output(
|
|
19
|
+
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
50
20
|
cwd=self.project_root,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
21
|
+
).splitlines()
|
|
22
|
+
|
|
54
23
|
if task and task.planned_files:
|
|
55
24
|
planned_paths = {pf.path for pf in task.planned_files}
|
|
56
|
-
output = [p for p in output if p in planned_paths] + [
|
|
57
|
-
|
|
58
|
-
|
|
25
|
+
output = [p for p in output if p in planned_paths] + [
|
|
26
|
+
p for p in output if p not in planned_paths
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
return output[:100] # Limit to avoid context overflow
|
|
59
30
|
except Exception:
|
|
60
31
|
return []
|
|
@@ -14,6 +14,7 @@ from devcouncil.execution.policy_engine import TaskPolicyEngine
|
|
|
14
14
|
from devcouncil.storage.db import get_db
|
|
15
15
|
from devcouncil.storage.native import FileChangeRepository, TaskLeaseRepository
|
|
16
16
|
from devcouncil.storage.repositories import GapRepository, TaskRepository
|
|
17
|
+
from devcouncil.verification.stub_detector import detect_stubs, task_allows_scaffolding
|
|
17
18
|
from devcouncil.verification.verifier import Verifier
|
|
18
19
|
|
|
19
20
|
_IGNORED_PREFIXES = (
|
|
@@ -37,6 +38,7 @@ _EVENT_IGNORED_PREFIXES = _IGNORED_PREFIXES + (".devcouncil/", ".gitignore")
|
|
|
37
38
|
|
|
38
39
|
_TASK_CACHE_TTL_SECONDS = 10.0
|
|
39
40
|
_EVENT_DEBOUNCE_SECONDS = 0.5
|
|
41
|
+
_STUB_SCAN_INTERVAL_SECONDS = 3.0
|
|
40
42
|
|
|
41
43
|
logger = logging.getLogger(__name__)
|
|
42
44
|
|
|
@@ -58,6 +60,8 @@ class FilesystemWatcher:
|
|
|
58
60
|
self._seen: dict[str, float] = {}
|
|
59
61
|
self._seen_last_cleanup: float = 0.0
|
|
60
62
|
self._task_cache: tuple[float, Task | None] | None = None
|
|
63
|
+
self._last_stub_scan: float = 0.0
|
|
64
|
+
self._reported_stub_keys: set[tuple[str, int, str]] = set()
|
|
61
65
|
# Reuse one Verifier across polls (avoids rebuilding its scanners each tick) but
|
|
62
66
|
# still run get_changed_files() fresh every poll — caching the git result would
|
|
63
67
|
# make the watcher miss changes for the cache TTL, defeating the poll interval.
|
|
@@ -75,6 +79,7 @@ class FilesystemWatcher:
|
|
|
75
79
|
if self.should_ignore(path):
|
|
76
80
|
continue
|
|
77
81
|
events.append(self._record_path(path, task, operation="modify"))
|
|
82
|
+
self._scan_stubs_live(task)
|
|
78
83
|
return events
|
|
79
84
|
|
|
80
85
|
def watch(self) -> None:
|
|
@@ -129,6 +134,7 @@ class FilesystemWatcher:
|
|
|
129
134
|
return None
|
|
130
135
|
event = self._record_path(rel, self._task_cached(), operation=operation)
|
|
131
136
|
self._notify(event)
|
|
137
|
+
self._scan_stubs_live(self._task_cached())
|
|
132
138
|
return event
|
|
133
139
|
|
|
134
140
|
def _notify(self, event: dict) -> None:
|
|
@@ -203,3 +209,61 @@ class FilesystemWatcher:
|
|
|
203
209
|
)
|
|
204
210
|
)
|
|
205
211
|
return {"path": path, "operation": operation, "allowed": allowed, "reason": decision.reason}
|
|
212
|
+
|
|
213
|
+
def _scan_stubs_live(self, task: Task | None) -> None:
|
|
214
|
+
"""Run stub detection on the current working-tree diff during execution.
|
|
215
|
+
|
|
216
|
+
Surfaces non-blocking ``stub_detected`` gaps mid-session — cheaper to fix
|
|
217
|
+
than waiting for ``dev verify``. Throttled so rapid saves do not spam gaps.
|
|
218
|
+
"""
|
|
219
|
+
now = time.monotonic()
|
|
220
|
+
if now - self._last_stub_scan < _STUB_SCAN_INTERVAL_SECONDS:
|
|
221
|
+
return
|
|
222
|
+
self._last_stub_scan = now
|
|
223
|
+
if task is None:
|
|
224
|
+
return
|
|
225
|
+
if self._verifier is None:
|
|
226
|
+
self._verifier = Verifier(self.project_root)
|
|
227
|
+
try:
|
|
228
|
+
diff = self._verifier.get_diff()
|
|
229
|
+
except Exception:
|
|
230
|
+
logger.debug("Live stub scan skipped: could not load diff", exc_info=True)
|
|
231
|
+
return
|
|
232
|
+
if not diff.strip():
|
|
233
|
+
return
|
|
234
|
+
scaffolding_ok = task_allows_scaffolding(task)
|
|
235
|
+
findings = detect_stubs(self.project_root, diff, honor_allow_stub=scaffolding_ok)
|
|
236
|
+
if not findings:
|
|
237
|
+
return
|
|
238
|
+
db = get_db(self.project_root)
|
|
239
|
+
if not db:
|
|
240
|
+
return
|
|
241
|
+
with db.get_session() as session:
|
|
242
|
+
gap_repo = GapRepository(session)
|
|
243
|
+
for finding in findings:
|
|
244
|
+
key = (finding.file, finding.line, finding.reason)
|
|
245
|
+
if key in self._reported_stub_keys:
|
|
246
|
+
continue
|
|
247
|
+
self._reported_stub_keys.add(key)
|
|
248
|
+
gap_repo.save(
|
|
249
|
+
Gap(
|
|
250
|
+
id=f"GAP-{self.task_id}-LIVESTUB-{uuid.uuid4().hex[:10]}",
|
|
251
|
+
severity="medium",
|
|
252
|
+
gap_type="stub_detected",
|
|
253
|
+
task_id=self.task_id,
|
|
254
|
+
description=(
|
|
255
|
+
f"Live stub/placeholder at {finding.file}:{finding.line}: {finding.reason}."
|
|
256
|
+
),
|
|
257
|
+
evidence=[f"{finding.file}:{finding.line}", finding.snippet],
|
|
258
|
+
recommended_fix=(
|
|
259
|
+
f"Replace the placeholder at {finding.file}:{finding.line} before continuing."
|
|
260
|
+
),
|
|
261
|
+
blocking=False,
|
|
262
|
+
file=finding.file,
|
|
263
|
+
line=finding.line,
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
logger.info(
|
|
267
|
+
"Live stub detected for %s at %s:%s (%s)",
|
|
268
|
+
self.task_id, finding.file, finding.line, finding.reason,
|
|
269
|
+
)
|