devcouncil 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +49 -30
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +960 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +303 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Policy-gated file writes shared by CLI and MCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from devcouncil.execution.hook_policy import HookPolicy
|
|
11
|
+
from devcouncil.execution.lease_validation import require_valid_lease
|
|
12
|
+
from devcouncil.integrations.mcp.util import (
|
|
13
|
+
GIT_APPLY_TIMEOUT_SECONDS,
|
|
14
|
+
diff_target_paths,
|
|
15
|
+
is_git_repo,
|
|
16
|
+
within_root,
|
|
17
|
+
)
|
|
18
|
+
from devcouncil.storage.native import FileChangeRepository, TaskLeaseRepository
|
|
19
|
+
from devcouncil.storage.repositories import TaskRepository
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _explicitly_planned(path: str, task) -> bool: # noqa: ANN001
|
|
23
|
+
normalized = path.replace("\\", "/")
|
|
24
|
+
while normalized.startswith("./"):
|
|
25
|
+
normalized = normalized[2:]
|
|
26
|
+
return any(
|
|
27
|
+
normalized == planned.path.replace("\\", "/")
|
|
28
|
+
or fnmatch.fnmatch(normalized, planned.path.replace("\\", "/"))
|
|
29
|
+
for planned in task.planned_files
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def write_file_payload(
|
|
34
|
+
project_root: Path,
|
|
35
|
+
*,
|
|
36
|
+
task_id: str,
|
|
37
|
+
lease_token: str,
|
|
38
|
+
rel_path: str,
|
|
39
|
+
content: str,
|
|
40
|
+
) -> dict:
|
|
41
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
42
|
+
from devcouncil.storage.db import get_db
|
|
43
|
+
|
|
44
|
+
initialize_project(project_root, quiet=True)
|
|
45
|
+
db = get_db(project_root)
|
|
46
|
+
if not db:
|
|
47
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
48
|
+
|
|
49
|
+
with db.get_session() as session:
|
|
50
|
+
lease_error = require_valid_lease(session, task_id, lease_token)
|
|
51
|
+
if lease_error:
|
|
52
|
+
return lease_error
|
|
53
|
+
lease_record = TaskLeaseRepository(session).active_for_task(task_id)
|
|
54
|
+
assert lease_record is not None
|
|
55
|
+
task = TaskRepository(session).get_by_id(task_id)
|
|
56
|
+
if not task:
|
|
57
|
+
return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
|
|
58
|
+
|
|
59
|
+
target = within_root(project_root, rel_path)
|
|
60
|
+
if target is None:
|
|
61
|
+
reason = "path escapes the project root"
|
|
62
|
+
FileChangeRepository(session).record(
|
|
63
|
+
rel_path, "write", False, task_id=task_id, lease_id=lease_record.id, reason=reason,
|
|
64
|
+
)
|
|
65
|
+
return {
|
|
66
|
+
"ok": False, "task_id": task_id, "applied_files": [],
|
|
67
|
+
"rejected_files": [{"path": rel_path, "reason": reason}],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if not _explicitly_planned(rel_path, task):
|
|
71
|
+
reason = f"Task {task_id} does not explicitly authorize changes to {rel_path}."
|
|
72
|
+
FileChangeRepository(session).record(
|
|
73
|
+
rel_path, "write", False, task_id=task_id, lease_id=lease_record.id, reason=reason,
|
|
74
|
+
)
|
|
75
|
+
return {
|
|
76
|
+
"ok": False, "task_id": task_id, "applied_files": [],
|
|
77
|
+
"rejected_files": [{"path": rel_path, "reason": reason}],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
decision = HookPolicy(project_root=project_root).evaluate_file_write(rel_path, task, content=content)
|
|
81
|
+
if not decision.allowed:
|
|
82
|
+
FileChangeRepository(session).record(
|
|
83
|
+
rel_path, "write", False, task_id=task_id, lease_id=lease_record.id, reason=decision.reason,
|
|
84
|
+
)
|
|
85
|
+
return {
|
|
86
|
+
"ok": False, "task_id": task_id, "applied_files": [],
|
|
87
|
+
"rejected_files": [{"path": rel_path, "reason": decision.reason}],
|
|
88
|
+
}
|
|
89
|
+
try:
|
|
90
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
tmp = target.with_name(target.name + ".devcouncil-tmp")
|
|
92
|
+
tmp.write_text(content, encoding="utf-8")
|
|
93
|
+
os.replace(tmp, target)
|
|
94
|
+
except OSError as exc:
|
|
95
|
+
return {"ok": False, "error": f"Write failed: {exc}", "code": "write_failed", "task_id": task_id}
|
|
96
|
+
FileChangeRepository(session).record(
|
|
97
|
+
rel_path, "write", True, task_id=task_id, lease_id=lease_record.id, reason=decision.reason,
|
|
98
|
+
)
|
|
99
|
+
return {
|
|
100
|
+
"ok": True, "task_id": task_id, "applied_files": [rel_path], "rejected_files": [],
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def apply_patch_payload(
|
|
105
|
+
project_root: Path,
|
|
106
|
+
*,
|
|
107
|
+
task_id: str,
|
|
108
|
+
lease_token: str,
|
|
109
|
+
unified_diff: str,
|
|
110
|
+
) -> dict:
|
|
111
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
112
|
+
from devcouncil.storage.db import get_db
|
|
113
|
+
|
|
114
|
+
initialize_project(project_root, quiet=True)
|
|
115
|
+
db = get_db(project_root)
|
|
116
|
+
if not db:
|
|
117
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
118
|
+
|
|
119
|
+
if not is_git_repo(project_root):
|
|
120
|
+
return {
|
|
121
|
+
"ok": False,
|
|
122
|
+
"error": "apply_patch requires a git repository. Use write_file instead.",
|
|
123
|
+
"code": "not_a_git_repo",
|
|
124
|
+
"task_id": task_id,
|
|
125
|
+
}
|
|
126
|
+
targets = diff_target_paths(unified_diff)
|
|
127
|
+
if not targets:
|
|
128
|
+
return {"ok": False, "error": "No target files found in the diff.", "code": "empty_patch", "task_id": task_id}
|
|
129
|
+
|
|
130
|
+
with db.get_session() as session:
|
|
131
|
+
lease_error = require_valid_lease(session, task_id, lease_token)
|
|
132
|
+
if lease_error:
|
|
133
|
+
return lease_error
|
|
134
|
+
lease_record = TaskLeaseRepository(session).active_for_task(task_id)
|
|
135
|
+
assert lease_record is not None
|
|
136
|
+
task = TaskRepository(session).get_by_id(task_id)
|
|
137
|
+
if not task:
|
|
138
|
+
return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
|
|
139
|
+
|
|
140
|
+
policy = HookPolicy(project_root=project_root)
|
|
141
|
+
rejected: list[dict[str, str]] = []
|
|
142
|
+
for path in targets:
|
|
143
|
+
if within_root(project_root, path) is None:
|
|
144
|
+
rejected.append({"path": path, "reason": "path escapes the project root"})
|
|
145
|
+
continue
|
|
146
|
+
if not _explicitly_planned(path, task):
|
|
147
|
+
rejected.append({
|
|
148
|
+
"path": path,
|
|
149
|
+
"reason": f"Task {task_id} does not explicitly authorize changes to {path}.",
|
|
150
|
+
})
|
|
151
|
+
continue
|
|
152
|
+
decision = policy.evaluate_file_write(path, task)
|
|
153
|
+
if not decision.allowed:
|
|
154
|
+
rejected.append({"path": path, "reason": decision.reason})
|
|
155
|
+
if rejected:
|
|
156
|
+
for item in rejected:
|
|
157
|
+
FileChangeRepository(session).record(
|
|
158
|
+
item["path"], "apply_patch", False, task_id=task_id, lease_id=lease_record.id, reason=item["reason"],
|
|
159
|
+
)
|
|
160
|
+
return {
|
|
161
|
+
"ok": False, "task_id": task_id, "applied_files": [], "rejected_files": rejected,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
patch_path = project_root / ".devcouncil" / f"mcp-apply-{lease_record.id}.patch"
|
|
165
|
+
patch_path.parent.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
patch_path.write_text(unified_diff, encoding="utf-8")
|
|
167
|
+
try:
|
|
168
|
+
check = subprocess.run(
|
|
169
|
+
["git", "apply", "--check", "--ignore-whitespace", str(patch_path)],
|
|
170
|
+
cwd=project_root, capture_output=True, text=True,
|
|
171
|
+
timeout=GIT_APPLY_TIMEOUT_SECONDS,
|
|
172
|
+
)
|
|
173
|
+
if check.returncode != 0:
|
|
174
|
+
return {
|
|
175
|
+
"ok": False,
|
|
176
|
+
"error": f"Patch does not apply cleanly: {check.stderr.strip()}",
|
|
177
|
+
"code": "patch_rejected",
|
|
178
|
+
"task_id": task_id,
|
|
179
|
+
}
|
|
180
|
+
applied = subprocess.run(
|
|
181
|
+
["git", "apply", "--ignore-whitespace", str(patch_path)],
|
|
182
|
+
cwd=project_root, capture_output=True, text=True,
|
|
183
|
+
timeout=GIT_APPLY_TIMEOUT_SECONDS,
|
|
184
|
+
)
|
|
185
|
+
if applied.returncode != 0:
|
|
186
|
+
return {
|
|
187
|
+
"ok": False,
|
|
188
|
+
"error": f"Patch apply failed: {applied.stderr.strip()}",
|
|
189
|
+
"code": "patch_failed",
|
|
190
|
+
"task_id": task_id,
|
|
191
|
+
}
|
|
192
|
+
finally:
|
|
193
|
+
try:
|
|
194
|
+
patch_path.unlink()
|
|
195
|
+
except OSError:
|
|
196
|
+
pass
|
|
197
|
+
for path in targets:
|
|
198
|
+
FileChangeRepository(session).record(
|
|
199
|
+
path, "apply_patch", True, task_id=task_id, lease_id=lease_record.id, reason="policy allowed",
|
|
200
|
+
)
|
|
201
|
+
return {
|
|
202
|
+
"ok": True, "task_id": task_id, "applied_files": targets, "rejected_files": [],
|
|
203
|
+
}
|
|
@@ -12,6 +12,7 @@ from devcouncil.execution.checkpoints import CheckpointService
|
|
|
12
12
|
from devcouncil.storage.db import get_db
|
|
13
13
|
from devcouncil.storage.native import AgentHandoffRepository, SemanticDiffRepository, TaskLeaseRepository
|
|
14
14
|
from devcouncil.storage.repositories import EvidenceRepository, GapRepository, RequirementRepository, TaskRepository
|
|
15
|
+
from devcouncil.utils.json_persist import write_model_json
|
|
15
16
|
from devcouncil.verification.verifier import Verifier
|
|
16
17
|
|
|
17
18
|
|
|
@@ -78,7 +79,7 @@ class HandoffService:
|
|
|
78
79
|
instruction=instruction,
|
|
79
80
|
created_at=datetime.now(timezone.utc).isoformat(),
|
|
80
81
|
)
|
|
81
|
-
manifest_path
|
|
82
|
+
write_model_json(manifest_path, manifest)
|
|
82
83
|
|
|
83
84
|
status = "manifest_only"
|
|
84
85
|
with db.get_session() as session:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fnmatch
|
|
2
|
+
import logging
|
|
2
3
|
import re
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from pathlib import Path
|
|
@@ -13,6 +14,8 @@ from devcouncil.execution.policy_engine import (
|
|
|
13
14
|
)
|
|
14
15
|
from devcouncil.utils.redaction import SECRET_PATTERNS
|
|
15
16
|
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
16
19
|
# Splits a shell command into the segments a shell would execute independently, on
|
|
17
20
|
# the chaining/pipe/sequence operators. We deliberately do NOT try to parse quoting
|
|
18
21
|
# perfectly — any operator we miss only makes us evaluate a *larger* segment as a
|
|
@@ -56,12 +59,23 @@ class HookPolicy:
|
|
|
56
59
|
try:
|
|
57
60
|
from devcouncil.app.config import load_config
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
config = load_config(self.project_root)
|
|
63
|
+
# Same derivation as the run command's PermissionPolicy: the project's
|
|
64
|
+
# configured test/lint/typecheck commands are repo-wide allowed.
|
|
65
|
+
allowed = [
|
|
66
|
+
str(item)
|
|
67
|
+
for item in (config.commands.test + config.commands.lint + config.commands.typecheck)
|
|
68
|
+
]
|
|
69
|
+
# Forward-compat: honor an explicit execution.global_allowed_commands
|
|
70
|
+
# if the config schema ever grows one.
|
|
71
|
+
configured = getattr(config.execution, "global_allowed_commands", None)
|
|
61
72
|
if isinstance(configured, (list, tuple)):
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
allowed.extend(str(item) for item in configured)
|
|
74
|
+
return allowed
|
|
75
|
+
except Exception as e:
|
|
76
|
+
# Fail-closed is safe, but a silently dropped allowlist makes every
|
|
77
|
+
# configured command deny with no visible cause.
|
|
78
|
+
logger.warning("Failed to load global allowed commands from config, gate uses empty allowlist: %s", e)
|
|
65
79
|
return []
|
|
66
80
|
|
|
67
81
|
secret_path_patterns = SECRET_PATH_PATTERNS
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Task lease operations shared by CLI and MCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from devcouncil.execution.prompt_builder import PromptBuilder
|
|
9
|
+
from devcouncil.execution.lease_validation import diagnose_lease, lease_error_payload, LeaseCode
|
|
10
|
+
from devcouncil.integrations.mcp.util import allowed_next_tools, lease_ttl_seconds
|
|
11
|
+
from devcouncil.storage.native import TaskLeaseRepository
|
|
12
|
+
from devcouncil.storage.repositories import GapRepository, RequirementRepository, TaskRepository
|
|
13
|
+
from devcouncil.utils.json_persist import read_json
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _refresh_stale_map_if_needed(project_root: Path) -> bool:
|
|
19
|
+
"""Regenerate ``repo_map.json`` when stale, before prompt/baseline."""
|
|
20
|
+
from devcouncil.indexing.map_refresh import refresh_stale_map_if_needed
|
|
21
|
+
|
|
22
|
+
return refresh_stale_map_if_needed(project_root, on_checkout=True, on_verify=False)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def checkout_task_payload(
|
|
26
|
+
project_root: Path,
|
|
27
|
+
*,
|
|
28
|
+
task_id: str,
|
|
29
|
+
client_id: str,
|
|
30
|
+
agent: str | None = None,
|
|
31
|
+
force: bool = False,
|
|
32
|
+
) -> dict:
|
|
33
|
+
"""Acquire a task lease and return scope for gated write tools."""
|
|
34
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
35
|
+
from devcouncil.storage.db import get_db
|
|
36
|
+
|
|
37
|
+
initialize_project(project_root, quiet=True)
|
|
38
|
+
db = get_db(project_root)
|
|
39
|
+
if not db:
|
|
40
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
41
|
+
|
|
42
|
+
with db.get_session() as session:
|
|
43
|
+
task_repo = TaskRepository(session)
|
|
44
|
+
task = task_repo.get_by_id(task_id)
|
|
45
|
+
if not task:
|
|
46
|
+
return {"ok": False, "error": f"Task {task_id} not found.", "code": "not_found", "task_id": task_id}
|
|
47
|
+
lease_repo = TaskLeaseRepository(session)
|
|
48
|
+
try:
|
|
49
|
+
lease = lease_repo.acquire(
|
|
50
|
+
task_id,
|
|
51
|
+
owner=f"mcp:{client_id}",
|
|
52
|
+
agent=agent,
|
|
53
|
+
client_id=client_id,
|
|
54
|
+
ttl_seconds=lease_ttl_seconds(project_root),
|
|
55
|
+
force=force,
|
|
56
|
+
)
|
|
57
|
+
except ValueError as exc:
|
|
58
|
+
return {"ok": False, "error": str(exc), "code": "lease_conflict", "task_id": task_id}
|
|
59
|
+
# Refresh stale map BEFORE prompt + liveness baseline so both see fresh
|
|
60
|
+
# fingerprints. Write-once baseline semantics are unchanged (snapshot still
|
|
61
|
+
# skips when a complete baseline already exists).
|
|
62
|
+
_refresh_stale_map_if_needed(project_root)
|
|
63
|
+
prompt = PromptBuilder(project_root).build_task_prompt(task, RequirementRepository(session).get_all())
|
|
64
|
+
semantic = None
|
|
65
|
+
semantic_path = project_root / ".devcouncil" / "semantic" / task_id / "before.json"
|
|
66
|
+
if semantic_path.exists():
|
|
67
|
+
semantic = read_json(semantic_path)
|
|
68
|
+
# Snapshot liveness at checkout so verify can ratchet against stranded
|
|
69
|
+
# pre-existing code. Write-once (skip if complete baseline exists).
|
|
70
|
+
# Best-effort; never blocks checkout.
|
|
71
|
+
try:
|
|
72
|
+
from devcouncil.verification.checks.liveness_ratchet import (
|
|
73
|
+
snapshot_liveness_baseline,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
snapshot_liveness_baseline(project_root, task_id, reset=False)
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
return {
|
|
80
|
+
"ok": True,
|
|
81
|
+
"lease_token": lease.lease_token,
|
|
82
|
+
"task_id": task.id,
|
|
83
|
+
"status": task.status,
|
|
84
|
+
"expires_at": lease.expires_at,
|
|
85
|
+
"prompt": prompt,
|
|
86
|
+
"planned_files": [f.model_dump() for f in task.planned_files],
|
|
87
|
+
"allowed_commands": task.allowed_commands,
|
|
88
|
+
"expected_tests": task.expected_tests,
|
|
89
|
+
"semantic_context": semantic,
|
|
90
|
+
"allowed_next_tools": allowed_next_tools(
|
|
91
|
+
"running",
|
|
92
|
+
bool(GapRepository(session).get_blocking_for_task(task_id)),
|
|
93
|
+
),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def release_task_payload(project_root: Path, *, task_id: str, lease_token: str) -> dict:
|
|
98
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
99
|
+
from devcouncil.storage.db import get_db
|
|
100
|
+
|
|
101
|
+
initialize_project(project_root, quiet=True)
|
|
102
|
+
db = get_db(project_root)
|
|
103
|
+
if not db:
|
|
104
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
105
|
+
|
|
106
|
+
with db.get_session() as session:
|
|
107
|
+
released = TaskLeaseRepository(session).release(task_id, lease_token)
|
|
108
|
+
if not released:
|
|
109
|
+
code, message = diagnose_lease(session, task_id, lease_token)
|
|
110
|
+
if code is LeaseCode.VALID:
|
|
111
|
+
return {"ok": False, "error": "Lease already released.", "code": "invalid_lease", "task_id": task_id}
|
|
112
|
+
return lease_error_payload(code, message, task_id)
|
|
113
|
+
try:
|
|
114
|
+
from devcouncil.verification.checks.liveness_ratchet import (
|
|
115
|
+
delete_liveness_baseline,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
delete_liveness_baseline(project_root, task_id)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
return {"ok": True, "task_id": task_id, "released": True}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def renew_lease_payload(
|
|
125
|
+
project_root: Path,
|
|
126
|
+
*,
|
|
127
|
+
task_id: str,
|
|
128
|
+
lease_token: str,
|
|
129
|
+
ttl_seconds: int | None = None,
|
|
130
|
+
) -> dict:
|
|
131
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
132
|
+
from devcouncil.storage.db import get_db
|
|
133
|
+
|
|
134
|
+
initialize_project(project_root, quiet=True)
|
|
135
|
+
db = get_db(project_root)
|
|
136
|
+
if not db:
|
|
137
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
138
|
+
|
|
139
|
+
ttl = ttl_seconds if ttl_seconds is not None else lease_ttl_seconds(project_root)
|
|
140
|
+
with db.get_session() as session:
|
|
141
|
+
renewed_lease = TaskLeaseRepository(session).renew(task_id, lease_token, ttl)
|
|
142
|
+
if renewed_lease is None:
|
|
143
|
+
code, message = diagnose_lease(session, task_id, lease_token)
|
|
144
|
+
if code is LeaseCode.VALID:
|
|
145
|
+
return {"ok": False, "error": "Lease renew failed.", "code": "invalid_lease", "task_id": task_id}
|
|
146
|
+
return lease_error_payload(code, message, task_id)
|
|
147
|
+
return {
|
|
148
|
+
"ok": True,
|
|
149
|
+
"task_id": task_id,
|
|
150
|
+
"expires_at": renewed_lease.expires_at,
|
|
151
|
+
"ttl_seconds": ttl,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def list_leases_payload(project_root: Path, *, active_only: bool = True) -> dict:
|
|
156
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
157
|
+
from devcouncil.storage.db import get_db
|
|
158
|
+
|
|
159
|
+
initialize_project(project_root, quiet=True)
|
|
160
|
+
db = get_db(project_root)
|
|
161
|
+
if not db:
|
|
162
|
+
return {"ok": False, "error": "DevCouncil state is unavailable in this directory.", "code": "not_initialized"}
|
|
163
|
+
|
|
164
|
+
with db.get_session() as session:
|
|
165
|
+
pairs = TaskLeaseRepository(session).list_leases(active_only=active_only)
|
|
166
|
+
leases = [
|
|
167
|
+
{
|
|
168
|
+
"task_id": lease.task_id,
|
|
169
|
+
"owner": lease.owner,
|
|
170
|
+
"agent": lease.agent,
|
|
171
|
+
"status": lease.status,
|
|
172
|
+
"expires_at": lease.expires_at,
|
|
173
|
+
"expired": expired,
|
|
174
|
+
}
|
|
175
|
+
for lease, expired in pairs
|
|
176
|
+
]
|
|
177
|
+
return {"ok": True, "leases": leases, "count": len(leases)}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Lease diagnosis for MCP/CLI gates — distinguish expired vs invalid vs conflict."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sqlmodel import Session, col, select
|
|
9
|
+
|
|
10
|
+
from devcouncil.storage.models import TaskLeaseModel
|
|
11
|
+
from devcouncil.storage.native import TaskLeaseRepository
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LeaseCode(str, Enum):
|
|
15
|
+
VALID = "valid"
|
|
16
|
+
LEASE_EXPIRED = "lease_expired"
|
|
17
|
+
INVALID_LEASE = "invalid_lease"
|
|
18
|
+
LEASE_HELD_BY_OTHER = "lease_held_by_other"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def diagnose_lease(session: Session, task_id: str, lease_token: str) -> tuple[LeaseCode, str]:
|
|
22
|
+
"""Classify a lease token for actionable agent recovery."""
|
|
23
|
+
repo = TaskLeaseRepository(session)
|
|
24
|
+
active = repo.active_for_task(task_id)
|
|
25
|
+
if active is not None:
|
|
26
|
+
if active.lease_token == lease_token:
|
|
27
|
+
return LeaseCode.VALID, ""
|
|
28
|
+
holder = active.owner or active.client_id or "another agent"
|
|
29
|
+
return (
|
|
30
|
+
LeaseCode.LEASE_HELD_BY_OTHER,
|
|
31
|
+
f"Task {task_id} is held by {holder}. Pick another task or wait for release.",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
stmt = (
|
|
35
|
+
select(TaskLeaseModel)
|
|
36
|
+
.where(TaskLeaseModel.task_id == task_id)
|
|
37
|
+
.where(TaskLeaseModel.lease_token == lease_token)
|
|
38
|
+
.order_by(col(TaskLeaseModel.created_at).desc())
|
|
39
|
+
)
|
|
40
|
+
prior = session.exec(stmt).first()
|
|
41
|
+
if prior is not None and prior.status in {"stale", "released"}:
|
|
42
|
+
return (
|
|
43
|
+
LeaseCode.LEASE_EXPIRED,
|
|
44
|
+
"Lease TTL expired. Check out the task again with devcouncil_checkout_task.",
|
|
45
|
+
)
|
|
46
|
+
return LeaseCode.INVALID_LEASE, "Invalid lease token."
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def lease_error_payload(code: LeaseCode, message: str, task_id: str) -> dict[str, Any]:
|
|
50
|
+
payload: dict[str, Any] = {
|
|
51
|
+
"ok": False,
|
|
52
|
+
"error": message,
|
|
53
|
+
"code": code.value,
|
|
54
|
+
"task_id": task_id,
|
|
55
|
+
}
|
|
56
|
+
if code is LeaseCode.LEASE_EXPIRED:
|
|
57
|
+
payload["suggested_action"] = "checkout_again"
|
|
58
|
+
payload["suggested_tool"] = "devcouncil_checkout_task"
|
|
59
|
+
payload["hint"] = "Renew only works before TTL expiry; call checkout again after expiry."
|
|
60
|
+
elif code is LeaseCode.LEASE_HELD_BY_OTHER:
|
|
61
|
+
payload["suggested_action"] = "pick_other_task"
|
|
62
|
+
payload["suggested_tool"] = "devcouncil_next_task"
|
|
63
|
+
return payload
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def require_valid_lease(session: Session, task_id: str, lease_token: str) -> dict[str, Any] | None:
|
|
67
|
+
"""Return an error payload when the lease is not valid, else None."""
|
|
68
|
+
code, message = diagnose_lease(session, task_id, lease_token)
|
|
69
|
+
if code is LeaseCode.VALID:
|
|
70
|
+
return None
|
|
71
|
+
return lease_error_payload(code, message, task_id)
|
|
@@ -6,6 +6,8 @@ from devcouncil.app.errors import ExecutionError
|
|
|
6
6
|
|
|
7
7
|
logger = logging.getLogger(__name__)
|
|
8
8
|
|
|
9
|
+
_GIT_APPLY_TIMEOUT_SECONDS = 120
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
class PatchEngine:
|
|
11
13
|
"""Handles applying unified diff patches to the codebase."""
|
|
@@ -67,6 +69,7 @@ class PatchEngine:
|
|
|
67
69
|
text=True,
|
|
68
70
|
encoding="utf-8",
|
|
69
71
|
errors="replace",
|
|
72
|
+
timeout=_GIT_APPLY_TIMEOUT_SECONDS,
|
|
70
73
|
)
|
|
71
74
|
if proc.returncode == 0:
|
|
72
75
|
logger.info("Patch applied (git apply %s)", " ".join(extra_args) or "strict")
|
|
@@ -34,6 +34,7 @@ class PermissionManager:
|
|
|
34
34
|
lines = ignore_file.read_text().splitlines()
|
|
35
35
|
return [line.strip() for line in lines if line.strip() and not line.startswith("#")]
|
|
36
36
|
except Exception:
|
|
37
|
+
logger.debug("Failed to read .devcouncilignore", exc_info=True)
|
|
37
38
|
pass
|
|
38
39
|
return []
|
|
39
40
|
|