devcouncil 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +49 -30
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +960 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +303 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
"""Task gate operations shared by CLI and MCP (verify, scope, evidence, policy, run)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from devcouncil.domain.evidence import CommandResult, DiffCoverageEvidence, DiffEvidence, TestEvidence
|
|
13
|
+
from devcouncil.execution.hook_policy import HookPolicy
|
|
14
|
+
from devcouncil.execution.lease_validation import require_valid_lease
|
|
15
|
+
from devcouncil.integrations.mcp.util import allowed_next_tools, read_log_file, truncate_text
|
|
16
|
+
from devcouncil.storage.native import ShellCommandRepository, TaskLeaseRepository
|
|
17
|
+
from devcouncil.storage.repositories import (
|
|
18
|
+
EvidenceRepository,
|
|
19
|
+
GapRepository,
|
|
20
|
+
RequirementRepository,
|
|
21
|
+
TaskRepository,
|
|
22
|
+
)
|
|
23
|
+
from devcouncil.verification.command_evidence import (
|
|
24
|
+
command_has_acceptance_evidence,
|
|
25
|
+
command_is_trivial_evidence,
|
|
26
|
+
)
|
|
27
|
+
from devcouncil.verification.next_actions import split_next_actions
|
|
28
|
+
|
|
29
|
+
RECORD_COMMAND_STATUSES = frozenset({"started", "finished", "failed", "blocked"})
|
|
30
|
+
CLI_TIMEOUT_SECONDS = 120
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _db(project_root: Path):
|
|
34
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
35
|
+
from devcouncil.storage.db import get_db
|
|
36
|
+
|
|
37
|
+
if not (project_root / ".devcouncil" / "state.sqlite").is_file():
|
|
38
|
+
initialize_project(project_root, quiet=True)
|
|
39
|
+
return get_db(project_root)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _build_router(project_root: Path):
|
|
43
|
+
try:
|
|
44
|
+
from devcouncil.app.config import get_api_key, load_config
|
|
45
|
+
from devcouncil.llm.provider import create_provider, validate_model_provider
|
|
46
|
+
from devcouncil.llm.router import ModelRouter
|
|
47
|
+
|
|
48
|
+
config = load_config(project_root)
|
|
49
|
+
validate_model_provider(config.models.provider)
|
|
50
|
+
api_key = get_api_key(config.models.provider, project_root)
|
|
51
|
+
provider = create_provider(
|
|
52
|
+
config.models.provider, api_key, project_root=project_root, provider_prefs=config.provider,
|
|
53
|
+
)
|
|
54
|
+
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
55
|
+
return ModelRouter(provider, role_config, project_root=project_root)
|
|
56
|
+
except Exception:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def verify_task_payload(
|
|
61
|
+
project_root: Path,
|
|
62
|
+
*,
|
|
63
|
+
task_id: str,
|
|
64
|
+
lease_token: str,
|
|
65
|
+
sandbox: str = "local",
|
|
66
|
+
) -> dict[str, Any]:
|
|
67
|
+
if sandbox in {"docker", "nix"}:
|
|
68
|
+
return {
|
|
69
|
+
"ok": False,
|
|
70
|
+
"code": "unsupported_sandbox",
|
|
71
|
+
"reason": f"Sandbox {sandbox} is not available in this build.",
|
|
72
|
+
"sandbox": sandbox,
|
|
73
|
+
}
|
|
74
|
+
db = _db(project_root)
|
|
75
|
+
if not db:
|
|
76
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
77
|
+
|
|
78
|
+
with db.get_session() as session:
|
|
79
|
+
lease_error = require_valid_lease(session, task_id, lease_token)
|
|
80
|
+
if lease_error:
|
|
81
|
+
return lease_error
|
|
82
|
+
task_repo = TaskRepository(session)
|
|
83
|
+
task = task_repo.get_by_id(task_id)
|
|
84
|
+
if not task:
|
|
85
|
+
return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
|
|
86
|
+
from devcouncil.execution.stop_gate_verify_cache import record_verify_cache
|
|
87
|
+
from devcouncil.verification.verifier import Verifier
|
|
88
|
+
|
|
89
|
+
GapRepository(session).delete_for_task(task_id)
|
|
90
|
+
EvidenceRepository(session).delete_for_task(task_id)
|
|
91
|
+
verifier = Verifier(project_root, router=_build_router(project_root))
|
|
92
|
+
evidence_gaps, evidence = asyncio.run(
|
|
93
|
+
verifier.verify_task(task, RequirementRepository(session).get_all()),
|
|
94
|
+
)
|
|
95
|
+
gaps = evidence_gaps
|
|
96
|
+
for gap in gaps:
|
|
97
|
+
GapRepository(session).save(gap)
|
|
98
|
+
for ev in evidence:
|
|
99
|
+
if isinstance(ev, CommandResult):
|
|
100
|
+
EvidenceRepository(session).save_command_result(task_id, ev)
|
|
101
|
+
elif isinstance(ev, DiffCoverageEvidence):
|
|
102
|
+
EvidenceRepository(session).save_diff_coverage_evidence(ev)
|
|
103
|
+
elif isinstance(ev, DiffEvidence):
|
|
104
|
+
EvidenceRepository(session).save_diff_evidence(ev)
|
|
105
|
+
elif isinstance(ev, TestEvidence):
|
|
106
|
+
EvidenceRepository(session).save_test_evidence(ev, task_id)
|
|
107
|
+
task.status = "blocked" if any(g.blocking for g in gaps) else "verified"
|
|
108
|
+
task_repo.save(task)
|
|
109
|
+
blocking = [g.model_dump() for g in gaps if g.blocking]
|
|
110
|
+
blocking_actions, advisory_actions = split_next_actions(gaps)
|
|
111
|
+
record_verify_cache(
|
|
112
|
+
project_root,
|
|
113
|
+
task_id=task_id,
|
|
114
|
+
status=task.status,
|
|
115
|
+
blocking_gaps=len(blocking),
|
|
116
|
+
next_actions=[a.model_dump() for a in blocking_actions[:20]],
|
|
117
|
+
passed=len(blocking) == 0,
|
|
118
|
+
)
|
|
119
|
+
outcome = verifier.last_outcome
|
|
120
|
+
return {
|
|
121
|
+
"ok": True,
|
|
122
|
+
"task_id": task_id,
|
|
123
|
+
"status": task.status,
|
|
124
|
+
"sandbox": sandbox,
|
|
125
|
+
"blocking_gaps": blocking,
|
|
126
|
+
"next_actions": [a.model_dump() for a in blocking_actions],
|
|
127
|
+
"advisory_actions": [a.model_dump() for a in advisory_actions],
|
|
128
|
+
"allowed_next_tools": allowed_next_tools(task.status, len(blocking) > 0),
|
|
129
|
+
"passed": len(blocking) == 0,
|
|
130
|
+
"verification_mode": outcome.mode if outcome else "unknown",
|
|
131
|
+
"compiler_active": outcome.compiler_active if outcome else False,
|
|
132
|
+
"diff_empty": outcome.diff_empty if outcome else False,
|
|
133
|
+
"coverage_measured": outcome.coverage_measured if outcome else False,
|
|
134
|
+
"coverage_skipped_reason": outcome.coverage_skipped_reason if outcome else None,
|
|
135
|
+
"difficulty": outcome.difficulty if outcome else None,
|
|
136
|
+
"rigor_applied": list(outcome.rigor_applied) if outcome else [],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def attach_committed_range_payload(project_root: Path, *, task_id: str, lease_token: str, base: str, head: str = "HEAD") -> dict[str, Any]:
|
|
141
|
+
"""Attach an existing committed range to a leased task's checkpoint refs."""
|
|
142
|
+
db = _db(project_root)
|
|
143
|
+
if not db:
|
|
144
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
145
|
+
|
|
146
|
+
with db.get_session() as session:
|
|
147
|
+
lease_error = require_valid_lease(session, task_id, lease_token)
|
|
148
|
+
if lease_error:
|
|
149
|
+
return lease_error
|
|
150
|
+
if not TaskRepository(session).get_by_id(task_id):
|
|
151
|
+
return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
base_sha = subprocess.check_output(["git", "rev-parse", "--verify", f"{base}^{{commit}}"], cwd=project_root, text=True, timeout=CLI_TIMEOUT_SECONDS).strip()
|
|
155
|
+
head_sha = subprocess.check_output(["git", "rev-parse", "--verify", f"{head}^{{commit}}"], cwd=project_root, text=True, timeout=CLI_TIMEOUT_SECONDS).strip()
|
|
156
|
+
subprocess.run(["git", "merge-base", "--is-ancestor", base_sha, head_sha], cwd=project_root, check=True, timeout=CLI_TIMEOUT_SECONDS)
|
|
157
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
158
|
+
return {"ok": False, "code": "invalid_commit_range", "error": "--base and --head must resolve to an ancestor commit range.", "task_id": task_id}
|
|
159
|
+
if base_sha == head_sha:
|
|
160
|
+
return {"ok": False, "code": "invalid_commit_range", "error": "--base must be a strict ancestor of --head.", "task_id": task_id}
|
|
161
|
+
|
|
162
|
+
refs = {"before": f"refs/devcouncil/tasks/{task_id}/before", "after": f"refs/devcouncil/tasks/{task_id}/after"}
|
|
163
|
+
subprocess.run(["git", "update-ref", refs["before"], base_sha], cwd=project_root, check=True, timeout=CLI_TIMEOUT_SECONDS)
|
|
164
|
+
subprocess.run(["git", "update-ref", refs["after"], head_sha], cwd=project_root, check=True, timeout=CLI_TIMEOUT_SECONDS)
|
|
165
|
+
return {"ok": True, "task_id": task_id, "base": base_sha, "head": head_sha, "range": f"{base_sha}..{head_sha}", "refs": refs}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def update_task_scope_payload(
|
|
169
|
+
project_root: Path,
|
|
170
|
+
*,
|
|
171
|
+
task_id: str,
|
|
172
|
+
lease_token: str,
|
|
173
|
+
expected_tests: list[str] | None = None,
|
|
174
|
+
allowed_commands: list[str] | None = None,
|
|
175
|
+
planned_files: list[str] | None = None,
|
|
176
|
+
create_planned_files: list[str] | None = None,
|
|
177
|
+
) -> dict[str, Any]:
|
|
178
|
+
db = _db(project_root)
|
|
179
|
+
if not db:
|
|
180
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
181
|
+
|
|
182
|
+
expected_tests = expected_tests or []
|
|
183
|
+
allowed_commands = allowed_commands or []
|
|
184
|
+
planned_files = planned_files or []
|
|
185
|
+
create_planned_files = create_planned_files or []
|
|
186
|
+
with db.get_session() as session:
|
|
187
|
+
lease_error = require_valid_lease(session, task_id, lease_token)
|
|
188
|
+
if lease_error:
|
|
189
|
+
return lease_error
|
|
190
|
+
task_repo = TaskRepository(session)
|
|
191
|
+
task = task_repo.get_by_id(task_id)
|
|
192
|
+
if not task:
|
|
193
|
+
return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
|
|
194
|
+
rejected_commands: list[str] = []
|
|
195
|
+
for cmd in allowed_commands:
|
|
196
|
+
if cmd in task.allowed_commands:
|
|
197
|
+
continue
|
|
198
|
+
if command_is_trivial_evidence(cmd):
|
|
199
|
+
rejected_commands.append(cmd)
|
|
200
|
+
continue
|
|
201
|
+
task.allowed_commands.append(cmd)
|
|
202
|
+
if cmd not in task.agent_appended_allowed_commands:
|
|
203
|
+
task.agent_appended_allowed_commands.append(cmd)
|
|
204
|
+
rejected_tests: list[str] = []
|
|
205
|
+
for test in expected_tests:
|
|
206
|
+
if test in task.expected_tests:
|
|
207
|
+
continue
|
|
208
|
+
if not command_has_acceptance_evidence(test):
|
|
209
|
+
rejected_tests.append(test)
|
|
210
|
+
continue
|
|
211
|
+
task.expected_tests.append(test)
|
|
212
|
+
if test not in task.agent_appended_expected_tests:
|
|
213
|
+
task.agent_appended_expected_tests.append(test)
|
|
214
|
+
rejected_planned_files: list[str] = []
|
|
215
|
+
if planned_files:
|
|
216
|
+
from fnmatch import fnmatch
|
|
217
|
+
|
|
218
|
+
from devcouncil.domain.task import PlannedFile
|
|
219
|
+
from devcouncil.execution.policy_engine import (
|
|
220
|
+
SECRET_PATH_PATTERNS,
|
|
221
|
+
TaskPolicyEngine,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
engine = TaskPolicyEngine(project_root)
|
|
225
|
+
existing = {pf.path.replace("\\", "/") for pf in task.planned_files}
|
|
226
|
+
for raw in planned_files:
|
|
227
|
+
path = raw.replace("\\", "/")
|
|
228
|
+
if path.startswith("./"):
|
|
229
|
+
path = path[2:]
|
|
230
|
+
if not path or path in existing:
|
|
231
|
+
continue
|
|
232
|
+
normalized = engine._normalize_path(path)
|
|
233
|
+
if any(fnmatch(normalized, pat) for pat in SECRET_PATH_PATTERNS):
|
|
234
|
+
rejected_planned_files.append(raw)
|
|
235
|
+
continue
|
|
236
|
+
if engine._matches_restricted(normalized):
|
|
237
|
+
rejected_planned_files.append(raw)
|
|
238
|
+
continue
|
|
239
|
+
# Modify-op only: agents may append an existing caller to wire a new
|
|
240
|
+
# file, not authorize creates/deletes through this surface.
|
|
241
|
+
if not (project_root / normalized).is_file():
|
|
242
|
+
rejected_planned_files.append(raw)
|
|
243
|
+
continue
|
|
244
|
+
task.planned_files.append(PlannedFile(
|
|
245
|
+
path=normalized,
|
|
246
|
+
reason="agent-appended scope (wire caller)",
|
|
247
|
+
allowed_change="modify",
|
|
248
|
+
))
|
|
249
|
+
existing.add(normalized)
|
|
250
|
+
if normalized not in task.agent_appended_planned_files:
|
|
251
|
+
task.agent_appended_planned_files.append(normalized)
|
|
252
|
+
if create_planned_files:
|
|
253
|
+
from fnmatch import fnmatch
|
|
254
|
+
|
|
255
|
+
from devcouncil.domain.task import PlannedFile
|
|
256
|
+
from devcouncil.execution.policy_engine import (
|
|
257
|
+
SECRET_PATH_PATTERNS,
|
|
258
|
+
TaskPolicyEngine,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
engine = TaskPolicyEngine(project_root)
|
|
262
|
+
existing = {pf.path.replace("\\", "/") for pf in task.planned_files}
|
|
263
|
+
for raw in create_planned_files:
|
|
264
|
+
path = raw.replace("\\", "/")
|
|
265
|
+
if path.startswith("./"):
|
|
266
|
+
path = path[2:]
|
|
267
|
+
if not path or path in existing:
|
|
268
|
+
continue
|
|
269
|
+
normalized = engine._normalize_path(path)
|
|
270
|
+
if any(fnmatch(normalized, pat) for pat in SECRET_PATH_PATTERNS):
|
|
271
|
+
rejected_planned_files.append(raw)
|
|
272
|
+
continue
|
|
273
|
+
if engine._matches_restricted(normalized):
|
|
274
|
+
rejected_planned_files.append(raw)
|
|
275
|
+
continue
|
|
276
|
+
task.planned_files.append(PlannedFile(
|
|
277
|
+
path=normalized,
|
|
278
|
+
reason="agent-appended scope (create)",
|
|
279
|
+
allowed_change="create",
|
|
280
|
+
))
|
|
281
|
+
existing.add(normalized)
|
|
282
|
+
if normalized not in task.agent_appended_planned_files:
|
|
283
|
+
task.agent_appended_planned_files.append(normalized)
|
|
284
|
+
task_repo.save(task)
|
|
285
|
+
return {
|
|
286
|
+
"ok": True,
|
|
287
|
+
"task_id": task_id,
|
|
288
|
+
"allowed_commands": task.allowed_commands,
|
|
289
|
+
"expected_tests": task.expected_tests,
|
|
290
|
+
"planned_files": [pf.model_dump() for pf in task.planned_files],
|
|
291
|
+
"agent_appended_planned_files": list(task.agent_appended_planned_files),
|
|
292
|
+
"rejected_expected_tests": rejected_tests,
|
|
293
|
+
"rejected_allowed_commands": rejected_commands,
|
|
294
|
+
"rejected_planned_files": rejected_planned_files,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def append_evidence_payload(
|
|
299
|
+
project_root: Path,
|
|
300
|
+
*,
|
|
301
|
+
task_id: str,
|
|
302
|
+
lease_token: str,
|
|
303
|
+
command: str,
|
|
304
|
+
summary: str,
|
|
305
|
+
exit_code: int = 0,
|
|
306
|
+
) -> dict[str, Any]:
|
|
307
|
+
db = _db(project_root)
|
|
308
|
+
if not db:
|
|
309
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
310
|
+
|
|
311
|
+
with db.get_session() as session:
|
|
312
|
+
lease_error = require_valid_lease(session, task_id, lease_token)
|
|
313
|
+
if lease_error:
|
|
314
|
+
return lease_error
|
|
315
|
+
EvidenceRepository(session).save_command_result(
|
|
316
|
+
task_id,
|
|
317
|
+
CommandResult(
|
|
318
|
+
command=command,
|
|
319
|
+
exit_code=exit_code,
|
|
320
|
+
stdout_path="",
|
|
321
|
+
stderr_path="",
|
|
322
|
+
summary=summary,
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
return {"ok": True, "task_id": task_id, "recorded": True}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_evidence_payload(
|
|
329
|
+
project_root: Path,
|
|
330
|
+
*,
|
|
331
|
+
task_id: str,
|
|
332
|
+
command_filter: str | None = None,
|
|
333
|
+
limit: int = 20,
|
|
334
|
+
) -> dict[str, Any]:
|
|
335
|
+
db = _db(project_root)
|
|
336
|
+
if not db:
|
|
337
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
338
|
+
|
|
339
|
+
with db.get_session() as session:
|
|
340
|
+
results = EvidenceRepository(session).get_command_results_for_task(task_id)
|
|
341
|
+
evidence_rows: list[dict[str, object]] = []
|
|
342
|
+
for result in results:
|
|
343
|
+
if command_filter and command_filter not in result.command:
|
|
344
|
+
continue
|
|
345
|
+
stdout, stdout_truncated = truncate_text(read_log_file(result.stdout_path))
|
|
346
|
+
stderr, stderr_truncated = truncate_text(read_log_file(result.stderr_path))
|
|
347
|
+
evidence_rows.append({
|
|
348
|
+
"command": result.command,
|
|
349
|
+
"exit_code": result.exit_code,
|
|
350
|
+
"summary": result.summary,
|
|
351
|
+
"stdout": stdout,
|
|
352
|
+
"stderr": stderr,
|
|
353
|
+
"truncated": stdout_truncated or stderr_truncated,
|
|
354
|
+
})
|
|
355
|
+
if len(evidence_rows) >= limit:
|
|
356
|
+
break
|
|
357
|
+
return {"ok": True, "task_id": task_id, "evidence": evidence_rows}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def policy_check_write_payload(
|
|
361
|
+
project_root: Path,
|
|
362
|
+
*,
|
|
363
|
+
path: str,
|
|
364
|
+
task_id: str | None = None,
|
|
365
|
+
) -> dict[str, Any]:
|
|
366
|
+
db = _db(project_root)
|
|
367
|
+
if not db:
|
|
368
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
369
|
+
|
|
370
|
+
with db.get_session() as session:
|
|
371
|
+
task_repo = TaskRepository(session)
|
|
372
|
+
if task_id:
|
|
373
|
+
task = task_repo.get_by_id(task_id)
|
|
374
|
+
else:
|
|
375
|
+
running = [task for task in task_repo.get_all() if task.status == "running"]
|
|
376
|
+
task = running[0] if running else None
|
|
377
|
+
decision = HookPolicy(project_root=project_root).evaluate_file_write(path, task)
|
|
378
|
+
return {
|
|
379
|
+
"action": decision.action,
|
|
380
|
+
"allowed": decision.allowed,
|
|
381
|
+
"reason": decision.reason,
|
|
382
|
+
"target": decision.target,
|
|
383
|
+
"task_id": task.id if task else None,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def record_command_payload(
|
|
388
|
+
project_root: Path,
|
|
389
|
+
*,
|
|
390
|
+
task_id: str,
|
|
391
|
+
lease_token: str,
|
|
392
|
+
command: str,
|
|
393
|
+
status: str,
|
|
394
|
+
exit_code: int | None = None,
|
|
395
|
+
reason: str = "",
|
|
396
|
+
) -> dict[str, Any]:
|
|
397
|
+
if status not in RECORD_COMMAND_STATUSES:
|
|
398
|
+
return {
|
|
399
|
+
"ok": False,
|
|
400
|
+
"error": f"status must be one of {sorted(RECORD_COMMAND_STATUSES)}",
|
|
401
|
+
"code": "invalid_arguments",
|
|
402
|
+
"argument": "status",
|
|
403
|
+
}
|
|
404
|
+
db = _db(project_root)
|
|
405
|
+
if not db:
|
|
406
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
407
|
+
|
|
408
|
+
with db.get_session() as session:
|
|
409
|
+
lease_error = require_valid_lease(session, task_id, lease_token)
|
|
410
|
+
if lease_error:
|
|
411
|
+
return lease_error
|
|
412
|
+
ShellCommandRepository(session).record(
|
|
413
|
+
task_id,
|
|
414
|
+
command,
|
|
415
|
+
status,
|
|
416
|
+
exit_code=exit_code if isinstance(exit_code, int) else None,
|
|
417
|
+
reason=reason,
|
|
418
|
+
)
|
|
419
|
+
return {"ok": True, "task_id": task_id, "recorded": True}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def next_task_payload(
|
|
423
|
+
project_root: Path,
|
|
424
|
+
*,
|
|
425
|
+
status_filter: str | None = None,
|
|
426
|
+
client_id: str | None = None,
|
|
427
|
+
) -> dict[str, Any]:
|
|
428
|
+
del client_id # reserved for future fleet routing
|
|
429
|
+
db = _db(project_root)
|
|
430
|
+
if not db:
|
|
431
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
432
|
+
|
|
433
|
+
with db.get_session() as session:
|
|
434
|
+
tasks = TaskRepository(session).get_all()
|
|
435
|
+
leased_task_ids = {
|
|
436
|
+
lease.task_id
|
|
437
|
+
for lease, expired in TaskLeaseRepository(session).list_leases(active_only=True)
|
|
438
|
+
if not expired
|
|
439
|
+
}
|
|
440
|
+
blocking_by_task: dict[str, int] = {}
|
|
441
|
+
for gap in GapRepository(session).get_all():
|
|
442
|
+
if gap.blocking and gap.task_id:
|
|
443
|
+
blocking_by_task[gap.task_id] = blocking_by_task.get(gap.task_id, 0) + 1
|
|
444
|
+
done_ids = {t.id for t in tasks if t.status in {"verified", "done"}}
|
|
445
|
+
wanted_statuses = {status_filter} if status_filter else {"planned", "ready"}
|
|
446
|
+
candidates = []
|
|
447
|
+
for task in tasks:
|
|
448
|
+
if task.status not in wanted_statuses:
|
|
449
|
+
continue
|
|
450
|
+
if task.id in leased_task_ids:
|
|
451
|
+
continue
|
|
452
|
+
if any(dep not in done_ids for dep in task.depends_on):
|
|
453
|
+
continue
|
|
454
|
+
candidates.append(task)
|
|
455
|
+
if not candidates:
|
|
456
|
+
return {
|
|
457
|
+
"ok": True,
|
|
458
|
+
"task": None,
|
|
459
|
+
"reason": "No unblocked, unleased task is available.",
|
|
460
|
+
}
|
|
461
|
+
candidates.sort(key=lambda t: (len(t.depends_on), t.id))
|
|
462
|
+
chosen = candidates[0]
|
|
463
|
+
blocking_count = blocking_by_task.get(chosen.id, 0)
|
|
464
|
+
return {
|
|
465
|
+
"ok": True,
|
|
466
|
+
"task": chosen.model_dump(),
|
|
467
|
+
"blocking_gap_count": blocking_count,
|
|
468
|
+
"ready_to_checkout": blocking_count == 0,
|
|
469
|
+
"allowed_next_tools": allowed_next_tools(chosen.status, blocking_count > 0),
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def run_command_payload(
|
|
474
|
+
project_root: Path,
|
|
475
|
+
*,
|
|
476
|
+
task_id: str,
|
|
477
|
+
lease_token: str,
|
|
478
|
+
command: str,
|
|
479
|
+
) -> dict[str, Any]:
|
|
480
|
+
from devcouncil.utils.subprocess_env import clean_subprocess_env
|
|
481
|
+
|
|
482
|
+
normalized = " ".join(command.split())
|
|
483
|
+
db = _db(project_root)
|
|
484
|
+
if not db:
|
|
485
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
486
|
+
|
|
487
|
+
with db.get_session() as session:
|
|
488
|
+
lease_error = require_valid_lease(session, task_id, lease_token)
|
|
489
|
+
if lease_error:
|
|
490
|
+
return lease_error
|
|
491
|
+
task = TaskRepository(session).get_by_id(task_id)
|
|
492
|
+
if not task:
|
|
493
|
+
return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
|
|
494
|
+
from devcouncil.execution.policy_engine import TaskPolicyEngine
|
|
495
|
+
|
|
496
|
+
policy_decision = TaskPolicyEngine(project_root).evaluate_command(normalized, task)
|
|
497
|
+
if policy_decision.action == "deny":
|
|
498
|
+
ShellCommandRepository(session).record(
|
|
499
|
+
task_id, normalized, "blocked", reason=policy_decision.reason,
|
|
500
|
+
)
|
|
501
|
+
return {
|
|
502
|
+
"ok": False,
|
|
503
|
+
"error": policy_decision.reason or "Command is not in the task allowlist.",
|
|
504
|
+
"code": "command_not_allowed",
|
|
505
|
+
"task_id": task_id,
|
|
506
|
+
"command": normalized,
|
|
507
|
+
}
|
|
508
|
+
try:
|
|
509
|
+
args = shlex.split(normalized, posix=(os.name != "nt"))
|
|
510
|
+
completed = subprocess.run(
|
|
511
|
+
args,
|
|
512
|
+
cwd=project_root,
|
|
513
|
+
capture_output=True,
|
|
514
|
+
text=True,
|
|
515
|
+
encoding="utf-8",
|
|
516
|
+
errors="replace",
|
|
517
|
+
env=clean_subprocess_env(),
|
|
518
|
+
timeout=CLI_TIMEOUT_SECONDS,
|
|
519
|
+
)
|
|
520
|
+
exit_code = completed.returncode
|
|
521
|
+
stdout, stdout_truncated = truncate_text(completed.stdout)
|
|
522
|
+
stderr, stderr_truncated = truncate_text(completed.stderr)
|
|
523
|
+
timed_out = False
|
|
524
|
+
except subprocess.TimeoutExpired as exc:
|
|
525
|
+
exit_code = None
|
|
526
|
+
stdout, stdout_truncated = truncate_text(exc.output)
|
|
527
|
+
stderr, stderr_truncated = truncate_text(exc.stderr)
|
|
528
|
+
timed_out = True
|
|
529
|
+
except (FileNotFoundError, OSError, ValueError) as exc:
|
|
530
|
+
ShellCommandRepository(session).record(
|
|
531
|
+
task_id, normalized, "failed", reason=str(exc),
|
|
532
|
+
)
|
|
533
|
+
return {
|
|
534
|
+
"ok": False,
|
|
535
|
+
"error": f"Could not run command: {exc}",
|
|
536
|
+
"code": "run_failed",
|
|
537
|
+
"task_id": task_id,
|
|
538
|
+
}
|
|
539
|
+
ShellCommandRepository(session).record(
|
|
540
|
+
task_id,
|
|
541
|
+
normalized,
|
|
542
|
+
"finished" if exit_code == 0 else "failed",
|
|
543
|
+
exit_code=exit_code,
|
|
544
|
+
)
|
|
545
|
+
return {
|
|
546
|
+
"ok": exit_code == 0,
|
|
547
|
+
"task_id": task_id,
|
|
548
|
+
"exit_code": exit_code,
|
|
549
|
+
"stdout": stdout,
|
|
550
|
+
"stderr": stderr,
|
|
551
|
+
"truncated": stdout_truncated or stderr_truncated,
|
|
552
|
+
"timed_out": timed_out,
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def handoff_agent_payload(
|
|
557
|
+
project_root: Path,
|
|
558
|
+
*,
|
|
559
|
+
task_id: str,
|
|
560
|
+
lease_token: str,
|
|
561
|
+
from_agent: str,
|
|
562
|
+
to_agent: str,
|
|
563
|
+
instruction: str = "",
|
|
564
|
+
) -> dict[str, Any]:
|
|
565
|
+
db = _db(project_root)
|
|
566
|
+
if not db:
|
|
567
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
568
|
+
|
|
569
|
+
with db.get_session() as session:
|
|
570
|
+
lease_error = require_valid_lease(session, task_id, lease_token)
|
|
571
|
+
if lease_error:
|
|
572
|
+
return lease_error
|
|
573
|
+
try:
|
|
574
|
+
from devcouncil.execution.handoff import HandoffService
|
|
575
|
+
|
|
576
|
+
manifest, handoff_path, run_id = HandoffService(project_root).create(
|
|
577
|
+
task_id,
|
|
578
|
+
from_agent,
|
|
579
|
+
to_agent,
|
|
580
|
+
instruction=instruction,
|
|
581
|
+
)
|
|
582
|
+
return {
|
|
583
|
+
"ok": True,
|
|
584
|
+
"task_id": task_id,
|
|
585
|
+
"manifest_path": str(handoff_path),
|
|
586
|
+
"run_id": run_id,
|
|
587
|
+
"manifest": manifest.model_dump(),
|
|
588
|
+
}
|
|
589
|
+
except ValueError as exc:
|
|
590
|
+
return {"ok": False, "error": str(exc), "code": "handoff_failed", "task_id": task_id}
|
|
@@ -17,6 +17,7 @@ from devcouncil.app.errors import ExecutionError
|
|
|
17
17
|
from devcouncil.execution.patch import PatchEngine
|
|
18
18
|
from devcouncil.execution.paths import resolve_project_path
|
|
19
19
|
from devcouncil.telemetry.traces import TraceLogger
|
|
20
|
+
from devcouncil.telemetry.stages import log_step
|
|
20
21
|
from devcouncil.utils.redaction import redact_string
|
|
21
22
|
|
|
22
23
|
logger = logging.getLogger(__name__)
|
|
@@ -50,6 +51,12 @@ class TaskRunner:
|
|
|
50
51
|
self._validate_path_within_root(path)
|
|
51
52
|
self.permissions.validate_action("file_write", path, task, operation=operation)
|
|
52
53
|
applied = self.patch_engine.apply_patch(patch)
|
|
54
|
+
log_step(
|
|
55
|
+
f"task_runner/apply_patch: {'applied' if applied else 'failed'}",
|
|
56
|
+
project_root=self.project_root,
|
|
57
|
+
task_id=task.id,
|
|
58
|
+
paths=sorted(changes),
|
|
59
|
+
)
|
|
53
60
|
self.tracer.log_event(
|
|
54
61
|
"tool_patch_applied",
|
|
55
62
|
{"paths": sorted(changes), "success": applied},
|
|
@@ -144,6 +151,12 @@ class TaskRunner:
|
|
|
144
151
|
stderr_path = self._save_command_log(task.id, command, "stderr", stderr)
|
|
145
152
|
stdout_summary = redact_string(stdout[-500:])
|
|
146
153
|
stderr_summary = redact_string(stderr[-500:])
|
|
154
|
+
log_step(
|
|
155
|
+
f"task_runner/run_command: exit {result.returncode}",
|
|
156
|
+
project_root=self.project_root,
|
|
157
|
+
task_id=task.id,
|
|
158
|
+
command=command,
|
|
159
|
+
)
|
|
147
160
|
self.tracer.log_event(
|
|
148
161
|
"command_executed",
|
|
149
162
|
{"command": command, "exit_code": result.returncode},
|
|
@@ -171,6 +184,12 @@ class TaskRunner:
|
|
|
171
184
|
try:
|
|
172
185
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
173
186
|
full_path.write_text(content, encoding="utf-8")
|
|
187
|
+
log_step(
|
|
188
|
+
f"task_runner/write_file: {path}",
|
|
189
|
+
project_root=self.project_root,
|
|
190
|
+
task_id=task.id,
|
|
191
|
+
operation=operation,
|
|
192
|
+
)
|
|
174
193
|
self.tracer.log_event(
|
|
175
194
|
"file_written",
|
|
176
195
|
{"path": path},
|