devcouncil 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -33
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +56 -40
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +964 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +325 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/summary.py +21 -3
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -2,11 +2,14 @@ import logging
|
|
|
2
2
|
import subprocess
|
|
3
3
|
import sys
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devcouncil.executors.transient_retry import run_subprocess_with_transient_retry
|
|
5
7
|
from rich.console import Console
|
|
6
8
|
from devcouncil.domain.task import Task
|
|
7
9
|
from devcouncil.domain.requirement import Requirement
|
|
8
10
|
from devcouncil.execution.executor import Executor, ExecutionResult
|
|
9
11
|
from devcouncil.execution.prompt_builder import PromptBuilder
|
|
12
|
+
from devcouncil.telemetry.stages import log_step
|
|
10
13
|
|
|
11
14
|
console = Console()
|
|
12
15
|
logger = logging.getLogger(__name__)
|
|
@@ -17,7 +20,12 @@ class MiniSWEExecutor(Executor):
|
|
|
17
20
|
|
|
18
21
|
def run_task(self, task: Task, requirements: list[Requirement]) -> ExecutionResult:
|
|
19
22
|
builder = PromptBuilder(self.project_root)
|
|
23
|
+
from devcouncil.planning.correction_manifest import repair_prompt_prefix
|
|
24
|
+
|
|
20
25
|
task_prompt = builder.build_task_prompt(task, requirements)
|
|
26
|
+
prefix = repair_prompt_prefix(self.project_root, task.id)
|
|
27
|
+
if prefix:
|
|
28
|
+
task_prompt = f"{prefix}{task_prompt}"
|
|
21
29
|
|
|
22
30
|
# Write temporary instruction file for mini-SWE-agent
|
|
23
31
|
instruction_file = self.project_root / ".devcouncil" / f"{task.id}-mini-swe-task.md"
|
|
@@ -25,6 +33,11 @@ class MiniSWEExecutor(Executor):
|
|
|
25
33
|
instruction_file.write_text(task_prompt, encoding="utf-8")
|
|
26
34
|
|
|
27
35
|
logger.info("mini-SWE-agent starting for %s", task.id)
|
|
36
|
+
log_step(
|
|
37
|
+
f"executor/mini-swe: starting task {task.id}",
|
|
38
|
+
project_root=self.project_root,
|
|
39
|
+
task_id=task.id,
|
|
40
|
+
)
|
|
28
41
|
console.print(f"Starting [bold]mini-SWE-agent[/bold] for task {task.id}...")
|
|
29
42
|
|
|
30
43
|
# In a real implementation, we'd invoke the agent CLI
|
|
@@ -42,24 +55,53 @@ class MiniSWEExecutor(Executor):
|
|
|
42
55
|
console.print("[yellow]Note: mini_swe_agent must be installed in the environment.[/yellow]")
|
|
43
56
|
|
|
44
57
|
try:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
def _run_once():
|
|
59
|
+
return subprocess.run(
|
|
60
|
+
cmd,
|
|
61
|
+
capture_output=True,
|
|
62
|
+
text=True,
|
|
63
|
+
encoding="utf-8",
|
|
64
|
+
errors="replace",
|
|
65
|
+
cwd=self.project_root,
|
|
66
|
+
timeout=1800,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
result = run_subprocess_with_transient_retry(
|
|
70
|
+
self.project_root,
|
|
71
|
+
label="mini-SWE-agent",
|
|
72
|
+
task_id=task.id,
|
|
73
|
+
run_once=_run_once,
|
|
53
74
|
)
|
|
54
75
|
self._write_log(task.id, result)
|
|
55
76
|
if result.returncode != 0:
|
|
56
77
|
logger.error("mini-SWE-agent exited %s for %s", result.returncode, task.id)
|
|
78
|
+
log_step(
|
|
79
|
+
f"executor/mini-swe: finished task {task.id}",
|
|
80
|
+
project_root=self.project_root,
|
|
81
|
+
task_id=task.id,
|
|
82
|
+
returncode=result.returncode,
|
|
83
|
+
success=False,
|
|
84
|
+
)
|
|
57
85
|
console.print(f"[red]mini-SWE-agent exited with {result.returncode}.[/red]")
|
|
58
86
|
return ExecutionResult(success=False, message='Execution failed')
|
|
59
87
|
logger.info("mini-SWE-agent finished for %s", task.id)
|
|
88
|
+
log_step(
|
|
89
|
+
f"executor/mini-swe: finished task {task.id}",
|
|
90
|
+
project_root=self.project_root,
|
|
91
|
+
task_id=task.id,
|
|
92
|
+
returncode=0,
|
|
93
|
+
success=True,
|
|
94
|
+
)
|
|
60
95
|
return ExecutionResult(success=True, message='Execution successful')
|
|
61
96
|
except Exception as e:
|
|
62
97
|
logger.exception("mini-SWE-agent error for %s: %s", task.id, e)
|
|
98
|
+
log_step(
|
|
99
|
+
f"executor/mini-swe: finished task {task.id}",
|
|
100
|
+
project_root=self.project_root,
|
|
101
|
+
task_id=task.id,
|
|
102
|
+
success=False,
|
|
103
|
+
error=str(e),
|
|
104
|
+
)
|
|
63
105
|
console.print(f"[red]Error running mini-SWE-agent: {e}[/red]")
|
|
64
106
|
return ExecutionResult(success=False, message='Execution failed')
|
|
65
107
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Any, Coroutine, Dict, List, Optional, Tuple, TypeVar
|
|
2
2
|
import asyncio
|
|
3
3
|
import logging
|
|
4
4
|
from rich.console import Console
|
|
@@ -12,6 +12,7 @@ from devcouncil.execution.context_builder import ContextBuilder
|
|
|
12
12
|
from devcouncil.execution.prompt_builder import PromptBuilder
|
|
13
13
|
from devcouncil.execution.paths import resolve_project_path
|
|
14
14
|
from devcouncil.app.errors import ExecutionError
|
|
15
|
+
from devcouncil.telemetry.stages import log_step
|
|
15
16
|
|
|
16
17
|
console = Console()
|
|
17
18
|
logger = logging.getLogger(__name__)
|
|
@@ -20,6 +21,12 @@ logger = logging.getLogger(__name__)
|
|
|
20
21
|
MAX_AGENT_STEPS = 10
|
|
21
22
|
MAX_STRUCTURED_FAILURES = 2 # model can't produce a valid action -> give up cleanly
|
|
22
23
|
MAX_CONSECUTIVE_PATCH_FAILURES = 3 # stop spinning on a patch the model can't fix
|
|
24
|
+
# How many times the agent may signal "finish", get BLOCKED by verification, and be
|
|
25
|
+
# handed the shared next-actions repair contract before we stop the closed loop. Mirrors
|
|
26
|
+
# the bounded self-repair budget the MCP/dev-go loops apply so native cannot spin forever.
|
|
27
|
+
MAX_VERIFY_ROUNDS = 3
|
|
28
|
+
|
|
29
|
+
_T = TypeVar("_T")
|
|
23
30
|
|
|
24
31
|
class ToolCall(BaseModel):
|
|
25
32
|
tool: str
|
|
@@ -31,33 +38,206 @@ class AgentAction(BaseModel):
|
|
|
31
38
|
finish: bool = False
|
|
32
39
|
|
|
33
40
|
class NativeAgent(Executor):
|
|
34
|
-
def __init__(self, router: ModelRouter, task_runner: TaskRunner):
|
|
41
|
+
def __init__(self, router: ModelRouter, task_runner: TaskRunner, *, sandbox: str = "local"):
|
|
35
42
|
self.router = router
|
|
36
43
|
self.task_runner = task_runner
|
|
44
|
+
# Writes and verification are now routed through the SAME lease-gated policy path
|
|
45
|
+
# the MCP surface uses (execution/gated_write.py + HookPolicy, verification via the
|
|
46
|
+
# shared verify payload), so the native executor no longer bypasses the lease/scope
|
|
47
|
+
# gate with direct TaskRunner writes. task_runner is retained for run_command (which
|
|
48
|
+
# already honors execution.command_timeout).
|
|
49
|
+
self.project_root = task_runner.project_root
|
|
50
|
+
self.sandbox = sandbox
|
|
37
51
|
# ContextBuilder is retained only for the cheap list_files file listing; the
|
|
38
52
|
# implementation context itself uses the budgeted PromptBuilder so the native
|
|
39
53
|
# executor gets the same repo-map orientation, symbol outlines, dependents and
|
|
40
54
|
# context-window budgeting as the CLI executors (rather than a flat JSON dump).
|
|
41
55
|
self.context_builder = ContextBuilder(task_runner.project_root)
|
|
42
56
|
self.prompt_builder = PromptBuilder(task_runner.project_root)
|
|
57
|
+
self._lease_token: Optional[str] = None
|
|
43
58
|
|
|
44
59
|
def run_task(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
|
|
45
60
|
"""Run the preview native executor behind the normal synchronous executor contract."""
|
|
46
|
-
|
|
61
|
+
root = self.task_runner.project_root
|
|
62
|
+
log_step(
|
|
63
|
+
f"executor/native: starting task {task.id}",
|
|
64
|
+
project_root=root,
|
|
65
|
+
task_id=task.id,
|
|
66
|
+
)
|
|
67
|
+
result = self._run_coroutine_from_sync(self._run_task_async(task, requirements))
|
|
68
|
+
log_step(
|
|
69
|
+
f"executor/native: finished task {task.id}",
|
|
70
|
+
project_root=root,
|
|
71
|
+
task_id=task.id,
|
|
72
|
+
success=result.success,
|
|
73
|
+
)
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _run_coroutine_from_sync(coro: Coroutine[Any, Any, _T]) -> _T:
|
|
78
|
+
"""Run async work from sync code without breaking nested event loops.
|
|
79
|
+
|
|
80
|
+
``asyncio.run()`` fails when a loop is already running (pytest-asyncio,
|
|
81
|
+
Jupyter). With no running loop we use ``asyncio.run()``; otherwise we run
|
|
82
|
+
the coroutine on a fresh loop in a worker thread.
|
|
83
|
+
"""
|
|
84
|
+
import concurrent.futures
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
asyncio.get_running_loop()
|
|
88
|
+
except RuntimeError:
|
|
89
|
+
return asyncio.run(coro)
|
|
90
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
91
|
+
return executor.submit(asyncio.run, coro).result()
|
|
92
|
+
|
|
93
|
+
# ------------------------------------------------------------------ lease-gated I/O
|
|
94
|
+
def _acquire_lease(self, task: Task) -> dict:
|
|
95
|
+
"""Acquire a task lease so writes/verify run through the shared gated path."""
|
|
96
|
+
from devcouncil.execution.lease_ops import checkout_task_payload
|
|
97
|
+
|
|
98
|
+
return checkout_task_payload(self.project_root, task_id=task.id, client_id="native")
|
|
99
|
+
|
|
100
|
+
def _release_lease(self, task: Task, lease_token: str) -> None:
|
|
101
|
+
from devcouncil.execution.lease_ops import release_task_payload
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
release_task_payload(self.project_root, task_id=task.id, lease_token=lease_token)
|
|
105
|
+
except Exception as exc: # a failed release only costs a TTL wait; never fail the run
|
|
106
|
+
logger.debug("Native lease release failed for %s: %s", task.id, exc)
|
|
107
|
+
|
|
108
|
+
def _gated_apply_patch(self, task: Task, patch: str) -> None:
|
|
109
|
+
"""Apply a unified diff through execution/gated_write.py (lease + scope + HookPolicy).
|
|
110
|
+
|
|
111
|
+
Raises ExecutionError on rejection so the loop's existing patch-failure handling
|
|
112
|
+
(retry guidance, consecutive-failure abort) applies unchanged.
|
|
113
|
+
"""
|
|
114
|
+
from devcouncil.execution.gated_write import apply_patch_payload
|
|
115
|
+
|
|
116
|
+
payload = apply_patch_payload(
|
|
117
|
+
self.project_root,
|
|
118
|
+
task_id=task.id,
|
|
119
|
+
lease_token=self._lease_token or "",
|
|
120
|
+
unified_diff=patch,
|
|
121
|
+
)
|
|
122
|
+
if not payload.get("ok"):
|
|
123
|
+
raise ExecutionError(self._write_rejection_reason(payload))
|
|
124
|
+
|
|
125
|
+
def _gated_write_file(self, task: Task, path: str, content: str) -> None:
|
|
126
|
+
"""Write a whole file through execution/gated_write.py (lease + scope + HookPolicy)."""
|
|
127
|
+
from devcouncil.execution.gated_write import write_file_payload
|
|
128
|
+
|
|
129
|
+
payload = write_file_payload(
|
|
130
|
+
self.project_root,
|
|
131
|
+
task_id=task.id,
|
|
132
|
+
lease_token=self._lease_token or "",
|
|
133
|
+
rel_path=path,
|
|
134
|
+
content=content,
|
|
135
|
+
)
|
|
136
|
+
if not payload.get("ok"):
|
|
137
|
+
raise ExecutionError(self._write_rejection_reason(payload))
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _write_rejection_reason(payload: dict) -> str:
|
|
141
|
+
rejected = payload.get("rejected_files") or []
|
|
142
|
+
if rejected:
|
|
143
|
+
first = rejected[0]
|
|
144
|
+
return f"Write to {first.get('path')} rejected: {first.get('reason')}"
|
|
145
|
+
return payload.get("error") or "Write rejected by the lease/scope gate."
|
|
146
|
+
|
|
147
|
+
def _verify_task(
|
|
148
|
+
self, task: Task, requirements: List[Requirement]
|
|
149
|
+
) -> Tuple[bool, List[dict], List[dict]]:
|
|
150
|
+
"""Verify through the shared surface and return (passed, blocking, advisory).
|
|
151
|
+
|
|
152
|
+
Local sandbox uses the same ``verify_task_payload`` the MCP verify loop uses, so the
|
|
153
|
+
``split_next_actions`` repair contract is byte-for-byte identical across surfaces.
|
|
154
|
+
docker/nix run through ``verification/sandbox.py`` for coding-CLI parity.
|
|
155
|
+
"""
|
|
156
|
+
if self.sandbox in {"docker", "nix"}:
|
|
157
|
+
return self._verify_via_sandbox(task, requirements)
|
|
158
|
+
from devcouncil.execution.task_gate_ops import verify_task_payload
|
|
159
|
+
|
|
160
|
+
payload = verify_task_payload(
|
|
161
|
+
self.project_root,
|
|
162
|
+
task_id=task.id,
|
|
163
|
+
lease_token=self._lease_token or "",
|
|
164
|
+
sandbox="local",
|
|
165
|
+
)
|
|
166
|
+
if not payload.get("ok"):
|
|
167
|
+
# A verification setup error (e.g. lease lost) is treated as "not passed" with the
|
|
168
|
+
# error surfaced as the single next action, so the loop can react rather than crash.
|
|
169
|
+
return False, [{"action": payload.get("error", "Verification failed to run.")}], []
|
|
170
|
+
return (
|
|
171
|
+
bool(payload.get("passed")),
|
|
172
|
+
list(payload.get("next_actions") or []),
|
|
173
|
+
list(payload.get("advisory_actions") or []),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _verify_via_sandbox(
|
|
177
|
+
self, task: Task, requirements: List[Requirement]
|
|
178
|
+
) -> Tuple[bool, List[dict], List[dict]]:
|
|
179
|
+
"""Run the task's expected commands in a docker/nix sandbox (command_timeout-bounded)."""
|
|
180
|
+
from devcouncil.verification.sandbox import get_sandbox
|
|
181
|
+
|
|
182
|
+
commands = task.expected_tests or task.allowed_commands
|
|
183
|
+
result = get_sandbox(self.sandbox, self.project_root).run(task, commands, requirements)
|
|
184
|
+
if result.status == "unsupported":
|
|
185
|
+
reason = f"Sandbox '{self.sandbox}' is unavailable in this environment."
|
|
186
|
+
return False, [{"action": reason}], []
|
|
187
|
+
return result.status == "passed", [], []
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _format_next_actions(blocking: List[dict], advisory: List[dict]) -> str:
|
|
191
|
+
"""Render the shared next-actions contract into the repair message fed back to the
|
|
192
|
+
model — the same guidance an MCP agent reads from devcouncil_verify_task."""
|
|
193
|
+
lines = ["[Verification BLOCKED] The change did not pass. Address these before finishing:"]
|
|
194
|
+
for i, action in enumerate(blocking, 1):
|
|
195
|
+
text = action.get("action") or action.get("gap_type") or "Resolve the blocking gap."
|
|
196
|
+
loc = action.get("file")
|
|
197
|
+
if loc and action.get("line"):
|
|
198
|
+
loc = f"{loc}:{action['line']}"
|
|
199
|
+
suffix = f" ({loc})" if loc else ""
|
|
200
|
+
cmd = action.get("suggested_command")
|
|
201
|
+
cmd_txt = f" Suggested command: {cmd}" if cmd else ""
|
|
202
|
+
lines.append(f"{i}. {text}{suffix}{cmd_txt}")
|
|
203
|
+
if advisory:
|
|
204
|
+
lines.append("Advisory (non-blocking) signals worth addressing:")
|
|
205
|
+
for action in advisory:
|
|
206
|
+
lines.append(f"- {action.get('action') or action.get('gap_type')}")
|
|
207
|
+
lines.append(
|
|
208
|
+
"Apply the fix through apply_patch, then set finish=true again to re-verify."
|
|
209
|
+
)
|
|
210
|
+
return "\n".join(lines)
|
|
47
211
|
|
|
48
212
|
async def _run_task_async(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
|
|
49
213
|
logger.info("Native agent starting for %s (max_steps=%d)", task.id, MAX_AGENT_STEPS)
|
|
50
214
|
console.print(f"Starting [bold]Native Executor[/bold] for task {task.id}...")
|
|
51
215
|
console.print("[yellow]Native executor is preview quality; DevCouncil verification remains the completion gate.[/yellow]")
|
|
52
|
-
|
|
216
|
+
|
|
217
|
+
# Acquire a lease so every write and verify runs through the same lease/scope gate
|
|
218
|
+
# as the MCP surface. Without a lease the gated write path refuses the write.
|
|
219
|
+
lease = self._acquire_lease(task)
|
|
220
|
+
if not lease.get("ok"):
|
|
221
|
+
message = lease.get("error", "Could not acquire a task lease.")
|
|
222
|
+
logger.error("Native agent could not lease %s: %s", task.id, message)
|
|
223
|
+
console.print(f"[red]Native agent could not acquire a lease for {task.id}: {message}[/red]")
|
|
224
|
+
return ExecutionResult(success=False, message=f"Lease unavailable: {message}")
|
|
225
|
+
self._lease_token = lease["lease_token"]
|
|
226
|
+
try:
|
|
227
|
+
return await self._run_agent_loop(task, requirements)
|
|
228
|
+
finally:
|
|
229
|
+
token = self._lease_token
|
|
230
|
+
self._lease_token = None
|
|
231
|
+
if token:
|
|
232
|
+
self._release_lease(task, token)
|
|
233
|
+
|
|
234
|
+
async def _run_agent_loop(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
|
|
53
235
|
# 1. Gather rich context (budgeted; includes repo-map orientation + symbol outlines)
|
|
54
236
|
context_block = self.prompt_builder.build_task_prompt(task, requirements)
|
|
55
|
-
from devcouncil.planning.correction_manifest import
|
|
237
|
+
from devcouncil.planning.correction_manifest import repair_prompt_prefix
|
|
56
238
|
|
|
57
|
-
|
|
58
|
-
correction_block = ""
|
|
59
|
-
if correction is not None:
|
|
60
|
-
correction_block = f"\nCorrection Manifest:\n{correction.model_dump_json(indent=2)}\n"
|
|
239
|
+
prefix = repair_prompt_prefix(self.task_runner.project_root, task.id)
|
|
240
|
+
correction_block = f"\n{prefix}" if prefix else ""
|
|
61
241
|
|
|
62
242
|
system_prompt = f"""
|
|
63
243
|
You are the DevCouncil Native Agent. Your goal is to implement the provided task.
|
|
@@ -78,7 +258,7 @@ Rules:
|
|
|
78
258
|
4. Set 'finish' to true when you believe the task is complete and verified.
|
|
79
259
|
"""
|
|
80
260
|
messages = [{"role": "system", "content": system_prompt}]
|
|
81
|
-
|
|
261
|
+
|
|
82
262
|
# Initial task prompt
|
|
83
263
|
messages.append({"role": "user", "content": f"Begin implementing task {task.id} based on the context provided."})
|
|
84
264
|
|
|
@@ -87,6 +267,7 @@ Rules:
|
|
|
87
267
|
# unfixable patch.
|
|
88
268
|
structured_failures = 0
|
|
89
269
|
consecutive_patch_failures = 0
|
|
270
|
+
verify_rounds = 0
|
|
90
271
|
for step in range(MAX_AGENT_STEPS):
|
|
91
272
|
try:
|
|
92
273
|
action = await self.router.complete_structured(
|
|
@@ -131,9 +312,33 @@ Rules:
|
|
|
131
312
|
messages.append({"role": "assistant", "content": action.model_dump_json()})
|
|
132
313
|
|
|
133
314
|
if action.finish:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
315
|
+
# Closed loop: instead of trusting the agent's self-report, verify through the
|
|
316
|
+
# same gate as MCP and, when BLOCKED, feed the shared next-actions repair
|
|
317
|
+
# contract back so the agent can self-repair and re-verify.
|
|
318
|
+
logger.info("Native agent signaled completion for %s at step %d; verifying", task.id, step + 1)
|
|
319
|
+
console.print("[green]Native agent signaled completion; verifying...[/green]")
|
|
320
|
+
# verify_task_payload runs the verifier via asyncio.run(); run it in a worker
|
|
321
|
+
# thread so it gets its own event loop instead of nesting inside this one.
|
|
322
|
+
passed, blocking, advisory = await asyncio.to_thread(
|
|
323
|
+
self._verify_task, task, requirements
|
|
324
|
+
)
|
|
325
|
+
if passed:
|
|
326
|
+
logger.info("Native agent %s verified after %d repair round(s)", task.id, verify_rounds)
|
|
327
|
+
console.print("[green]Verification passed.[/green]")
|
|
328
|
+
return ExecutionResult(success=True, message="Native agent completed and verification passed.")
|
|
329
|
+
verify_rounds += 1
|
|
330
|
+
if verify_rounds >= MAX_VERIFY_ROUNDS:
|
|
331
|
+
logger.error("Native agent %s still blocked after %d verify round(s)", task.id, verify_rounds)
|
|
332
|
+
return ExecutionResult(
|
|
333
|
+
success=False,
|
|
334
|
+
message=(
|
|
335
|
+
f"Verification still blocked after {verify_rounds} repair round(s): "
|
|
336
|
+
f"{len(blocking)} blocking gap(s)."
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
console.print(f"[yellow]Verification blocked ({len(blocking)} gap(s)); handing back repair guidance.[/yellow]")
|
|
340
|
+
messages.append({"role": "user", "content": self._format_next_actions(blocking, advisory)})
|
|
341
|
+
continue
|
|
137
342
|
|
|
138
343
|
if not action.tool_calls:
|
|
139
344
|
# No action and not finished — nudge instead of silently burning a step.
|
|
@@ -169,10 +374,10 @@ Rules:
|
|
|
169
374
|
elif tool_call.tool == "apply_patch":
|
|
170
375
|
if "path" in tool_call.args and "content" in tool_call.args:
|
|
171
376
|
# Fallback for when the model can't produce a valid unified
|
|
172
|
-
# diff. Routes through
|
|
173
|
-
# planned-files
|
|
174
|
-
self.
|
|
175
|
-
tool_call.args["path"], tool_call.args["content"]
|
|
377
|
+
# diff. Routes through the lease-gated write path, which enforces
|
|
378
|
+
# the same planned-files/scope policy — no widening of scope.
|
|
379
|
+
self._gated_write_file(
|
|
380
|
+
task, tool_call.args["path"], tool_call.args["content"]
|
|
176
381
|
)
|
|
177
382
|
consecutive_patch_failures = 0
|
|
178
383
|
result_summary = f"Wrote {tool_call.args['path']} via path+content fallback."
|
|
@@ -184,7 +389,7 @@ Rules:
|
|
|
184
389
|
"'diff --git a/<path> b/<path>', then '--- a/<path>' (or "
|
|
185
390
|
"'--- /dev/null' for a new file), '+++ b/<path>', and '@@' hunks."
|
|
186
391
|
)
|
|
187
|
-
self.
|
|
392
|
+
self._gated_apply_patch(task, patch)
|
|
188
393
|
consecutive_patch_failures = 0
|
|
189
394
|
result_summary = "Successfully applied patch."
|
|
190
395
|
elif tool_call.tool == "run_command":
|
|
@@ -192,7 +397,7 @@ Rules:
|
|
|
192
397
|
result_summary = f"Command finished with exit code {cmd_result.exit_code}."
|
|
193
398
|
else:
|
|
194
399
|
raise ValueError(f"Unknown tool: {tool_call.tool}")
|
|
195
|
-
|
|
400
|
+
|
|
196
401
|
messages.append({"role": "user", "content": f"[Tool Result] '{tool_call.tool}': {result_summary}"})
|
|
197
402
|
except Exception as e:
|
|
198
403
|
logger.warning("Native agent tool %s failed for %s: %s", tool_call.tool, task.id, e)
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import subprocess
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from devcouncil.executors.transient_retry import run_subprocess_with_transient_retry
|
|
4
6
|
from rich.console import Console
|
|
5
7
|
from devcouncil.domain.task import Task
|
|
6
8
|
from devcouncil.domain.requirement import Requirement
|
|
7
9
|
from devcouncil.execution.executor import Executor, ExecutionResult
|
|
8
10
|
from devcouncil.execution.prompt_builder import PromptBuilder
|
|
11
|
+
from devcouncil.telemetry.stages import log_step
|
|
9
12
|
|
|
10
13
|
console = Console()
|
|
11
14
|
logger = logging.getLogger(__name__)
|
|
@@ -16,9 +19,19 @@ class OpenHandsExecutor(Executor):
|
|
|
16
19
|
|
|
17
20
|
def run_task(self, task: Task, requirements: list[Requirement]) -> ExecutionResult:
|
|
18
21
|
builder = PromptBuilder(self.project_root)
|
|
22
|
+
from devcouncil.planning.correction_manifest import repair_prompt_prefix
|
|
23
|
+
|
|
19
24
|
task_prompt = builder.build_task_prompt(task, requirements)
|
|
25
|
+
prefix = repair_prompt_prefix(self.project_root, task.id)
|
|
26
|
+
if prefix:
|
|
27
|
+
task_prompt = f"{prefix}{task_prompt}"
|
|
20
28
|
|
|
21
29
|
logger.info("OpenHands starting for %s", task.id)
|
|
30
|
+
log_step(
|
|
31
|
+
"executor/openhands: starting task %s" % task.id,
|
|
32
|
+
project_root=self.project_root,
|
|
33
|
+
task_id=task.id,
|
|
34
|
+
)
|
|
22
35
|
console.print(f"Starting [bold]OpenHands[/bold] for task {task.id}...")
|
|
23
36
|
|
|
24
37
|
# OpenHands often expects a workspace mount and an instruction.
|
|
@@ -40,24 +53,53 @@ class OpenHandsExecutor(Executor):
|
|
|
40
53
|
console.print("[yellow]Note: OpenHands must be installed and configured in the environment.[/yellow]")
|
|
41
54
|
|
|
42
55
|
try:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
def _run_once():
|
|
57
|
+
return subprocess.run(
|
|
58
|
+
cmd,
|
|
59
|
+
capture_output=True,
|
|
60
|
+
text=True,
|
|
61
|
+
encoding="utf-8",
|
|
62
|
+
errors="replace",
|
|
63
|
+
cwd=self.project_root,
|
|
64
|
+
timeout=1800,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
result = run_subprocess_with_transient_retry(
|
|
68
|
+
self.project_root,
|
|
69
|
+
label="OpenHands",
|
|
70
|
+
task_id=task.id,
|
|
71
|
+
run_once=_run_once,
|
|
51
72
|
)
|
|
52
73
|
self._write_log(task.id, result)
|
|
53
74
|
if result.returncode != 0:
|
|
54
75
|
logger.error("OpenHands exited %s for %s", result.returncode, task.id)
|
|
76
|
+
log_step(
|
|
77
|
+
"executor/openhands: finished task %s" % task.id,
|
|
78
|
+
project_root=self.project_root,
|
|
79
|
+
task_id=task.id,
|
|
80
|
+
returncode=result.returncode,
|
|
81
|
+
success=False,
|
|
82
|
+
)
|
|
55
83
|
console.print(f"[red]OpenHands exited with {result.returncode}.[/red]")
|
|
56
84
|
return ExecutionResult(success=False, message=f"Exited with code {result.returncode}")
|
|
57
85
|
logger.info("OpenHands finished for %s", task.id)
|
|
86
|
+
log_step(
|
|
87
|
+
"executor/openhands: finished task %s" % task.id,
|
|
88
|
+
project_root=self.project_root,
|
|
89
|
+
task_id=task.id,
|
|
90
|
+
returncode=0,
|
|
91
|
+
success=True,
|
|
92
|
+
)
|
|
58
93
|
return ExecutionResult(success=True, message="Completed successfully")
|
|
59
94
|
except Exception as e:
|
|
60
95
|
logger.exception("OpenHands error for %s: %s", task.id, e)
|
|
96
|
+
log_step(
|
|
97
|
+
"executor/openhands: finished task %s" % task.id,
|
|
98
|
+
project_root=self.project_root,
|
|
99
|
+
task_id=task.id,
|
|
100
|
+
success=False,
|
|
101
|
+
error=str(e),
|
|
102
|
+
)
|
|
61
103
|
console.print(f"[red]Error running OpenHands: {e}[/red]")
|
|
62
104
|
return ExecutionResult(success=False, message=str(e))
|
|
63
105
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Shared transient-failure detection and retry for subprocess executors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Output signatures of failures caused by the NETWORK/PROVIDER, not the task.
|
|
14
|
+
# Matched case-insensitively against stderr plus the tail of stdout.
|
|
15
|
+
TRANSIENT_FAILURE_MARKERS = (
|
|
16
|
+
"connection closed",
|
|
17
|
+
"connection reset",
|
|
18
|
+
"connection refused",
|
|
19
|
+
"connection error",
|
|
20
|
+
"connection aborted",
|
|
21
|
+
"econnreset",
|
|
22
|
+
"econnrefused",
|
|
23
|
+
"etimedout",
|
|
24
|
+
"socket hang up",
|
|
25
|
+
"mid-response",
|
|
26
|
+
"network error",
|
|
27
|
+
"fetch failed",
|
|
28
|
+
"temporarily unavailable",
|
|
29
|
+
"service unavailable",
|
|
30
|
+
"internal server error",
|
|
31
|
+
"bad gateway",
|
|
32
|
+
"gateway timeout",
|
|
33
|
+
"overloaded",
|
|
34
|
+
"rate limit",
|
|
35
|
+
"too many requests",
|
|
36
|
+
"request timed out",
|
|
37
|
+
"timeout awaiting",
|
|
38
|
+
"tls handshake",
|
|
39
|
+
"dns",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def transient_failure_reason(result: subprocess.CompletedProcess[str]) -> str | None:
|
|
44
|
+
"""Return the matched transient marker when a failed run looks provider-caused."""
|
|
45
|
+
haystack = f"{result.stderr or ''}\n{(result.stdout or '')[-4000:]}".lower()
|
|
46
|
+
for marker in TRANSIENT_FAILURE_MARKERS:
|
|
47
|
+
if marker in haystack:
|
|
48
|
+
return marker
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def transient_error_in_text(text: str) -> str | None:
|
|
53
|
+
"""Return a transient marker found in an exception message, else None."""
|
|
54
|
+
haystack = (text or "").lower()
|
|
55
|
+
for marker in TRANSIENT_FAILURE_MARKERS:
|
|
56
|
+
if marker in haystack:
|
|
57
|
+
return marker
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def transient_retry_limit(project_root: Path, *, default: int = 2) -> int:
|
|
62
|
+
"""Max transient-failure retries from ``execution.transient_retry_attempts``."""
|
|
63
|
+
try:
|
|
64
|
+
from devcouncil.app.config import load_config
|
|
65
|
+
|
|
66
|
+
return max(0, int(load_config(project_root).execution.transient_retry_attempts))
|
|
67
|
+
except Exception:
|
|
68
|
+
return default
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_subprocess_with_transient_retry(
|
|
72
|
+
project_root: Path,
|
|
73
|
+
*,
|
|
74
|
+
label: str,
|
|
75
|
+
task_id: str,
|
|
76
|
+
run_once: Callable[[], subprocess.CompletedProcess[str]],
|
|
77
|
+
) -> subprocess.CompletedProcess[str]:
|
|
78
|
+
"""Run a subprocess callable, retrying on known transient failure signatures."""
|
|
79
|
+
result = run_once()
|
|
80
|
+
retry_limit = transient_retry_limit(project_root)
|
|
81
|
+
retries = 0
|
|
82
|
+
while result.returncode != 0 and retries < retry_limit:
|
|
83
|
+
reason = transient_failure_reason(result)
|
|
84
|
+
if reason is None:
|
|
85
|
+
break
|
|
86
|
+
retries += 1
|
|
87
|
+
delay = min(30.0, 5.0 * retries)
|
|
88
|
+
logger.warning(
|
|
89
|
+
"%s failed with a transient error for %s (%s); retrying %d/%d in %.0fs",
|
|
90
|
+
label,
|
|
91
|
+
task_id,
|
|
92
|
+
reason,
|
|
93
|
+
retries,
|
|
94
|
+
retry_limit,
|
|
95
|
+
delay,
|
|
96
|
+
)
|
|
97
|
+
time.sleep(delay)
|
|
98
|
+
result = run_once()
|
|
99
|
+
return result
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import subprocess
|
|
2
2
|
import logging
|
|
3
3
|
from devcouncil.domain.gap import Gap
|
|
4
|
+
from devcouncil.utils.proc import GIT_TIMEOUT
|
|
4
5
|
|
|
5
6
|
logger = logging.getLogger(__name__)
|
|
6
7
|
|
|
@@ -15,7 +16,9 @@ class CleanGitCheck:
|
|
|
15
16
|
|
|
16
17
|
def check(self, project_root, task_id: str) -> list[Gap]:
|
|
17
18
|
try:
|
|
18
|
-
status = subprocess.check_output(
|
|
19
|
+
status = subprocess.check_output(
|
|
20
|
+
["git", "status", "--porcelain"], cwd=project_root, timeout=GIT_TIMEOUT
|
|
21
|
+
).decode()
|
|
19
22
|
dirty_lines = [line for line in status.splitlines() if line.strip() and not self._is_runtime_state(line)]
|
|
20
23
|
if dirty_lines:
|
|
21
24
|
return [Gap(
|
|
@@ -38,7 +41,7 @@ class CleanGitCheck:
|
|
|
38
41
|
recommended_fix="Install git and ensure it is in your PATH.",
|
|
39
42
|
blocking=True
|
|
40
43
|
)]
|
|
41
|
-
except subprocess.CalledProcessError as e:
|
|
44
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
|
42
45
|
logger.warning("Git status check failed: %s", e)
|
|
43
46
|
return [Gap(
|
|
44
47
|
id=f"GAP-{task_id}-GIT-ERROR",
|