devcouncil 0.1.0 → 0.2.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/LICENSE +201 -201
- package/README.md +197 -494
- package/package.json +9 -2
- package/pyproject.toml +62 -27
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +297 -108
- package/src/devcouncil/app/errors.py +23 -23
- package/src/devcouncil/app/events.py +44 -44
- package/src/devcouncil/app/orchestrator.py +67 -67
- package/src/devcouncil/app/project_status.py +29 -0
- package/src/devcouncil/app/run_context.py +39 -39
- package/src/devcouncil/app/state_machine.py +108 -108
- package/src/devcouncil/artifacts/__init__.py +1 -1
- package/src/devcouncil/artifacts/coverage.py +96 -96
- package/src/devcouncil/artifacts/graph.py +163 -143
- package/src/devcouncil/artifacts/migrations.py +20 -20
- package/src/devcouncil/artifacts/schemas.py +23 -23
- package/src/devcouncil/artifacts/serializer.py +21 -21
- package/src/devcouncil/artifacts/validators.py +27 -27
- package/src/devcouncil/assets/__init__.py +1 -0
- package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/commands/agents.py +292 -0
- package/src/devcouncil/cli/commands/artifacts.py +54 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/check.py +209 -0
- package/src/devcouncil/cli/commands/config.py +115 -54
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +31 -0
- package/src/devcouncil/cli/commands/doctor.py +291 -47
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +656 -0
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +209 -33
- package/src/devcouncil/cli/commands/init.py +204 -57
- package/src/devcouncil/cli/commands/integrate.py +1171 -76
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +96 -22
- package/src/devcouncil/cli/commands/plan.py +422 -210
- package/src/devcouncil/cli/commands/prompt.py +48 -34
- package/src/devcouncil/cli/commands/repair.py +89 -69
- package/src/devcouncil/cli/commands/report.py +120 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
- package/src/devcouncil/cli/commands/rollback.py +55 -54
- package/src/devcouncil/cli/commands/run.py +285 -220
- package/src/devcouncil/cli/commands/runs.py +223 -0
- package/src/devcouncil/cli/commands/scaffold.py +32 -0
- package/src/devcouncil/cli/commands/semantic.py +47 -0
- package/src/devcouncil/cli/commands/setup.py +300 -20
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/skills.py +88 -0
- package/src/devcouncil/cli/commands/status.py +141 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +49 -4
- package/src/devcouncil/cli/commands/verify.py +293 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +92 -25
- package/src/devcouncil/council/prompts/arbiter.md +19 -19
- package/src/devcouncil/council/prompts/critic_a.md +10 -10
- package/src/devcouncil/council/prompts/critic_b.md +10 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
- package/src/devcouncil/council/prompts/planner_a.md +16 -16
- package/src/devcouncil/council/prompts/planner_b.md +16 -16
- package/src/devcouncil/council/prompts/rebuttal.md +10 -10
- package/src/devcouncil/council/prompts/spec_writer.md +12 -12
- package/src/devcouncil/domain/assumption.py +17 -17
- package/src/devcouncil/domain/critique.py +32 -32
- package/src/devcouncil/domain/evidence.py +47 -27
- package/src/devcouncil/domain/gap.py +52 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +55 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +186 -77
- package/src/devcouncil/execution/patch.py +77 -28
- package/src/devcouncil/execution/permissions.py +52 -59
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +650 -38
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +68 -64
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +736 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +186 -85
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +52 -45
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
- package/src/devcouncil/gating/policy.py +315 -167
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/__init__.py +1 -1
- package/src/devcouncil/indexing/ast_matcher.py +168 -0
- package/src/devcouncil/indexing/graph_index.py +48 -48
- package/src/devcouncil/indexing/lsp.py +161 -0
- package/src/devcouncil/indexing/repo_mapper.py +1455 -204
- package/src/devcouncil/indexing/semantic_index.py +205 -0
- package/src/devcouncil/integrations/actions.py +146 -0
- package/src/devcouncil/integrations/check.py +423 -0
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/github_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +62 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +2072 -96
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +349 -0
- package/src/devcouncil/live/models.py +63 -0
- package/src/devcouncil/live/repair_prompt.py +83 -0
- package/src/devcouncil/live/reviewer.py +70 -0
- package/src/devcouncil/live/signals.py +135 -0
- package/src/devcouncil/live/summary.py +34 -0
- package/src/devcouncil/live/tasks.py +18 -0
- package/src/devcouncil/live/transcripts.py +141 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +42 -38
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +627 -125
- package/src/devcouncil/llm/router.py +303 -118
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +71 -66
- package/src/devcouncil/planning/plan_service.py +60 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +70 -44
- package/src/devcouncil/repo/ci_scaffold.py +157 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +374 -0
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +30 -17
- package/src/devcouncil/reporting/markdown_report.py +83 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/skills/__init__.py +19 -0
- package/src/devcouncil/skills/library/README.md +46 -0
- package/src/devcouncil/skills/library/ai-training.md +50 -0
- package/src/devcouncil/skills/library/android.md +50 -0
- package/src/devcouncil/skills/library/backend.md +52 -0
- package/src/devcouncil/skills/library/core-engineering.md +95 -0
- package/src/devcouncil/skills/library/data-engineering.md +47 -0
- package/src/devcouncil/skills/library/desktop.md +46 -0
- package/src/devcouncil/skills/library/devops.md +48 -0
- package/src/devcouncil/skills/library/game-dev.md +46 -0
- package/src/devcouncil/skills/library/ios.md +48 -0
- package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
- package/src/devcouncil/skills/library/security.md +48 -0
- package/src/devcouncil/skills/library/systems.md +48 -0
- package/src/devcouncil/skills/library/web.md +47 -0
- package/src/devcouncil/skills/library/windows.md +47 -0
- package/src/devcouncil/skills/registry.py +330 -0
- package/src/devcouncil/storage/db.py +147 -66
- package/src/devcouncil/storage/models.py +204 -83
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +388 -249
- package/src/devcouncil/telemetry/cost.py +140 -34
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +52 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +423 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +147 -141
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/acceptance_compiler.py +125 -0
- package/src/devcouncil/verification/ad_hoc_check.py +129 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +178 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1342 -307
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -3,35 +3,45 @@ import asyncio
|
|
|
3
3
|
import json
|
|
4
4
|
import datetime
|
|
5
5
|
from typing import Any
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
from rich.panel import Panel
|
|
8
|
-
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from devcouncil.storage.db import get_db
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from devcouncil.storage.db import get_db
|
|
12
12
|
from devcouncil.storage.repositories import (
|
|
13
|
-
RequirementRepository,
|
|
14
|
-
AssumptionRepository,
|
|
15
|
-
TaskRepository,
|
|
16
|
-
CritiqueFindingRepository,
|
|
17
13
|
GapRepository,
|
|
14
|
+
PlanningStateRepository,
|
|
18
15
|
)
|
|
19
16
|
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
20
17
|
from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
|
|
21
|
-
from devcouncil.llm.provider import
|
|
22
|
-
from devcouncil.llm.router import ModelRouter
|
|
23
|
-
from devcouncil.planning.spec_service import SpecService
|
|
24
|
-
from devcouncil.planning.
|
|
25
|
-
from devcouncil.planning.
|
|
26
|
-
from devcouncil.planning.
|
|
27
|
-
from devcouncil.
|
|
28
|
-
from devcouncil.
|
|
29
|
-
from devcouncil.app.
|
|
30
|
-
from devcouncil.app.
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
from devcouncil.llm.provider import Provider, MockProvider, ProviderRequestError, build_role_model_config, create_provider, validate_model_provider
|
|
19
|
+
from devcouncil.llm.router import ModelRouter, StructuredOutputError
|
|
20
|
+
from devcouncil.planning.spec_service import SpecService
|
|
21
|
+
from devcouncil.planning.prompt_enhancer_service import PromptEnhancerService
|
|
22
|
+
from devcouncil.planning.plan_service import PlanService
|
|
23
|
+
from devcouncil.planning.critique_service import CritiqueService
|
|
24
|
+
from devcouncil.planning.arbiter_service import ArbiterDecision, ArbiterService
|
|
25
|
+
from devcouncil.gating.policy import GatePolicy
|
|
26
|
+
from devcouncil.app.orchestrator import Orchestrator
|
|
27
|
+
from devcouncil.app.state_machine import ProjectPhase
|
|
28
|
+
from devcouncil.app.config import ModelRoleConfig, load_config, get_api_key
|
|
29
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
30
|
+
from devcouncil.telemetry.traces import TraceLogger
|
|
31
|
+
|
|
32
|
+
app = typer.Typer()
|
|
33
33
|
console = Console()
|
|
34
34
|
|
|
35
|
+
REQUIRED_PLANNING_ROLES = (
|
|
36
|
+
"prompt_enhancer",
|
|
37
|
+
"spec_writer",
|
|
38
|
+
"planner_a",
|
|
39
|
+
"planner_b",
|
|
40
|
+
"critic_a",
|
|
41
|
+
"critic_b",
|
|
42
|
+
"arbiter",
|
|
43
|
+
)
|
|
44
|
+
|
|
35
45
|
|
|
36
46
|
def _decision_ids(items: list[Any]) -> set[str]:
|
|
37
47
|
ids: set[str] = set()
|
|
@@ -57,187 +67,264 @@ def _reconcile_findings(findings, decision):
|
|
|
57
67
|
else:
|
|
58
68
|
reconciled.append(finding)
|
|
59
69
|
return reconciled
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"mock/
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _ensure_planning_roles(config) -> None:
|
|
73
|
+
fallback = config.models.roles.get("spec_writer")
|
|
74
|
+
if fallback is None and config.models.roles:
|
|
75
|
+
fallback = next(iter(config.models.roles.values()))
|
|
76
|
+
if fallback is None:
|
|
77
|
+
try:
|
|
78
|
+
provider_roles = build_role_model_config(config.models.provider)
|
|
79
|
+
fallback = ModelRoleConfig(model=provider_roles["spec_writer"]["model"])
|
|
80
|
+
except ValueError:
|
|
81
|
+
fallback = ModelRoleConfig(model="unconfigured")
|
|
82
|
+
|
|
83
|
+
for role in REQUIRED_PLANNING_ROLES:
|
|
84
|
+
config.models.roles.setdefault(role, fallback.model_copy())
|
|
85
|
+
|
|
86
|
+
async def run_plan_flow(
|
|
87
|
+
goal: str,
|
|
88
|
+
requirements_only: bool = False,
|
|
89
|
+
dry_run: bool = False,
|
|
90
|
+
persist: bool = True,
|
|
91
|
+
project_root: Path = Path("."),
|
|
92
|
+
quick: bool = False,
|
|
93
|
+
):
|
|
94
|
+
root = project_root.expanduser().resolve()
|
|
95
|
+
initialize_project(root, quiet=True)
|
|
96
|
+
db = get_db(root)
|
|
97
|
+
if not db:
|
|
98
|
+
console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
# Load validated config
|
|
102
|
+
config = load_config(root)
|
|
103
|
+
_ensure_planning_roles(config)
|
|
104
|
+
|
|
105
|
+
api_key = None
|
|
106
|
+
if not dry_run:
|
|
107
|
+
try:
|
|
108
|
+
validate_model_provider(config.models.provider)
|
|
109
|
+
api_key = get_api_key(config.models.provider, root)
|
|
110
|
+
except ValueError as e:
|
|
111
|
+
console.print(f"[red]{e}[/red]")
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
orchestrator = Orchestrator(root, persist_state=persist)
|
|
115
|
+
orchestrator.reset_state_machine(ProjectPhase.NEW)
|
|
116
|
+
run_id = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-plan"
|
|
117
|
+
await orchestrator.start_run(run_id, goal)
|
|
118
|
+
|
|
119
|
+
provider: Provider
|
|
120
|
+
if dry_run:
|
|
121
|
+
# Override config models to be unique roles for mock mapping
|
|
122
|
+
for role in REQUIRED_PLANNING_ROLES:
|
|
123
|
+
config.models.roles[role].model = f"mock/{role}"
|
|
124
|
+
|
|
125
|
+
provider = MockProvider()
|
|
126
|
+
provider.responses = {
|
|
127
|
+
"mock/prompt_enhancer": json.dumps({
|
|
128
|
+
"original_goal": goal,
|
|
129
|
+
"enhanced_goal": (
|
|
130
|
+
f"Plan and implement {goal} using the mapped repository's "
|
|
131
|
+
"existing patterns, tests, and verification gates."
|
|
132
|
+
),
|
|
133
|
+
"codebase_context": ["Use the repository map to target existing application and test structure."],
|
|
134
|
+
"debate_focus": ["Compare minimal implementation scope against production-readiness concerns."],
|
|
135
|
+
"constraints": [f"Do not broaden beyond: {goal}."]
|
|
136
|
+
}),
|
|
137
|
+
"mock/spec_writer": json.dumps({
|
|
138
|
+
"requirements": [{"id": "REQ-001", "title": "Mock Req", "description": "Desc", "priority": "high", "source": "user", "acceptance_criteria": []}],
|
|
139
|
+
"assumptions": [],
|
|
140
|
+
"blocking_questions": []
|
|
141
|
+
}),
|
|
142
|
+
"mock/planner_a": [
|
|
143
|
+
json.dumps({
|
|
144
|
+
"id": "PLAN-A", "rationale": "Simple", "tasks": [{"id": "TASK-001", "title": "Mock Task", "description": "Desc", "requirement_ids": ["REQ-001"], "acceptance_criterion_ids": [], "planned_files": [], "expected_tests": [], "allowed_commands": [], "status": "planned"}]
|
|
145
|
+
}),
|
|
146
|
+
json.dumps({"rebuttals": []})
|
|
147
|
+
],
|
|
148
|
+
"mock/planner_b": [
|
|
149
|
+
json.dumps({
|
|
150
|
+
"id": "PLAN-B", "rationale": "Robust", "tasks": [{"id": "TASK-001", "title": "Mock Task", "description": "Desc", "requirement_ids": ["REQ-001"], "acceptance_criterion_ids": [], "planned_files": [], "expected_tests": [], "allowed_commands": [], "status": "planned"}]
|
|
151
|
+
}),
|
|
152
|
+
json.dumps({"rebuttals": []})
|
|
153
|
+
],
|
|
154
|
+
"mock/critic_a": '{"findings": []}',
|
|
155
|
+
"mock/critic_b": '{"findings": []}',
|
|
156
|
+
"mock/arbiter": json.dumps({
|
|
157
|
+
"accepted_finding_ids": [], "rejected_finding_ids": [],
|
|
158
|
+
"final_requirements": [{"id": "REQ-001", "title": "Mock Req", "description": "Desc", "priority": "high", "source": "user", "acceptance_criteria": [{"id": "AC-1", "description": "Test it", "verification_method": "unit_test"}]}],
|
|
159
|
+
"final_tasks": [{"id": "TASK-001", "title": "Mock Task", "description": "Desc", "requirement_ids": ["REQ-001"], "acceptance_criterion_ids": ["AC-1"], "planned_files": [{"path": "test.py", "reason": "logic", "allowed_change": "modify"}], "expected_tests": [], "allowed_commands": [], "status": "planned"}]
|
|
160
|
+
}),
|
|
161
|
+
}
|
|
162
|
+
# Special case: PlanService calls use the same model names.
|
|
163
|
+
# I'll modify PlanService to use a slightly different role string if needed,
|
|
164
|
+
# but for Dry Run, let's just make the MockProvider return based on the schema requested.
|
|
165
|
+
else:
|
|
166
|
+
if api_key is None:
|
|
167
|
+
console.print("[red]Missing API key for configured model provider.[/red]")
|
|
168
|
+
return []
|
|
169
|
+
provider = create_provider(config.models.provider, api_key, project_root=root)
|
|
170
|
+
|
|
171
|
+
# Build role config after dry-run overrides so mocks are routed correctly.
|
|
172
|
+
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
173
|
+
router = ModelRouter(provider, role_config, project_root=root)
|
|
174
|
+
|
|
175
|
+
prompt_enhancer = PromptEnhancerService(router)
|
|
176
|
+
spec_service = SpecService(router)
|
|
177
|
+
plan_service = PlanService(router)
|
|
178
|
+
critique_service = CritiqueService(router)
|
|
179
|
+
arbiter_service = ArbiterService(router)
|
|
180
|
+
mapper = RepoMapper(root)
|
|
181
|
+
|
|
182
|
+
with Progress(
|
|
183
|
+
SpinnerColumn(),
|
|
184
|
+
TextColumn("[progress.description]{task.description}"),
|
|
185
|
+
transient=True,
|
|
186
|
+
) as progress:
|
|
187
|
+
# 1. Repo Map
|
|
188
|
+
progress.add_task(description="Mapping repository...", total=None)
|
|
147
189
|
repo_map = mapper.map_repo(goal)
|
|
148
190
|
repo_map_json = repo_map.model_dump_json(indent=2)
|
|
149
191
|
orchestrator.save_run_artifact("repo_map.json", json.loads(repo_map_json))
|
|
150
|
-
graph_context = CodeReviewGraphAdapter(
|
|
192
|
+
graph_context = CodeReviewGraphAdapter(root).get_context()
|
|
151
193
|
if graph_context.available:
|
|
152
194
|
orchestrator.save_run_artifact("code_review_graph_context.json", graph_context.model_dump())
|
|
153
195
|
await orchestrator.transition_to(ProjectPhase.REPO_MAPPED)
|
|
154
|
-
|
|
155
|
-
# 2.
|
|
156
|
-
progress.add_task(description="
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
#
|
|
186
|
-
progress.add_task(description="Generating
|
|
187
|
-
|
|
188
|
-
orchestrator.save_run_artifact("
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
196
|
+
|
|
197
|
+
# 2. Codebase-specific prompt enhancement
|
|
198
|
+
progress.add_task(description="Enhancing prompt for codebase debate...", total=None)
|
|
199
|
+
prompt_enhancement = await prompt_enhancer.enhance_prompt(
|
|
200
|
+
goal,
|
|
201
|
+
repo_map_json,
|
|
202
|
+
graph_context.model_dump_json() if graph_context.available else None,
|
|
203
|
+
project_root=root,
|
|
204
|
+
)
|
|
205
|
+
debate_goal = prompt_enhancement.debate_prompt()
|
|
206
|
+
orchestrator.save_run_artifact("prompt_enhancement.json", prompt_enhancement.model_dump())
|
|
207
|
+
if prompt_enhancement.applied_skills:
|
|
208
|
+
console.print(
|
|
209
|
+
"[dim]Domain skills applied:[/dim] "
|
|
210
|
+
+ ", ".join(prompt_enhancement.applied_skills)
|
|
211
|
+
)
|
|
212
|
+
TraceLogger(root).log_event(
|
|
213
|
+
"prompt_enhanced",
|
|
214
|
+
{
|
|
215
|
+
"original_goal": goal,
|
|
216
|
+
"enhanced_goal": prompt_enhancement.enhanced_goal,
|
|
217
|
+
"codebase_context_count": len(prompt_enhancement.codebase_context),
|
|
218
|
+
"constraint_count": len(prompt_enhancement.constraints),
|
|
219
|
+
"debate_focus_count": len(prompt_enhancement.debate_focus),
|
|
220
|
+
"applied_skills": prompt_enhancement.applied_skills,
|
|
221
|
+
"artifact": f".devcouncil/runs/{run_id}/prompt_enhancement.json",
|
|
222
|
+
},
|
|
223
|
+
run_id=run_id,
|
|
224
|
+
summary="Prompt enhanced for codebase-specific debate.",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# 3. Spec / Requirements
|
|
228
|
+
progress.add_task(description="Generating requirements...", total=None)
|
|
229
|
+
spec_output = await spec_service.generate_spec(debate_goal, repo_map_json)
|
|
230
|
+
orchestrator.save_run_artifact("requirements.json", spec_output.model_dump())
|
|
231
|
+
await orchestrator.transition_to(ProjectPhase.REQUIREMENTS_DRAFTED)
|
|
232
|
+
|
|
233
|
+
if requirements_only:
|
|
234
|
+
console.print(Panel(f"Found {len(spec_output.requirements)} requirements.", title="Requirements Generated"))
|
|
235
|
+
return []
|
|
236
|
+
|
|
237
|
+
requirements_json = json.dumps([r.model_dump() for r in spec_output.requirements])
|
|
238
|
+
|
|
239
|
+
if quick:
|
|
240
|
+
# Rigor dial: single pragmatic plan, no A/B debate, critique, rebuttal,
|
|
241
|
+
# or arbitration. Spec requirements (with their acceptance criteria)
|
|
242
|
+
# become the final requirements verbatim. This trades the council's
|
|
243
|
+
# adversarial robustness for ~5 fewer model calls — the right setting
|
|
244
|
+
# for small, well-scoped changes where verification (which still gates
|
|
245
|
+
# every diff) is the real safety net, not planning debate.
|
|
246
|
+
progress.add_task(description="Generating single plan (quick mode)...", total=None)
|
|
247
|
+
plan_a = await plan_service.generate_plan(
|
|
248
|
+
"planner_a", debate_goal, requirements_json, repo_map_json
|
|
249
|
+
)
|
|
250
|
+
orchestrator.save_run_artifact("plan_a.json", plan_a.model_dump())
|
|
251
|
+
await orchestrator.transition_to(ProjectPhase.PLANS_GENERATED)
|
|
252
|
+
|
|
253
|
+
decision = ArbiterDecision(
|
|
254
|
+
accepted_finding_ids=[],
|
|
255
|
+
rejected_finding_ids=[],
|
|
256
|
+
final_requirements=spec_output.requirements,
|
|
257
|
+
final_tasks=plan_a.tasks,
|
|
258
|
+
)
|
|
259
|
+
orchestrator.save_run_artifact("decision.json", decision.model_dump())
|
|
260
|
+
# Walk through CRITIQUES_GENERATED (the only path to ARBITRATED) without
|
|
261
|
+
# actually critiquing, so the rest of the lifecycle (approval, gates,
|
|
262
|
+
# status, the report's phase) is identical to the full council flow.
|
|
263
|
+
await orchestrator.transition_to(ProjectPhase.CRITIQUES_GENERATED)
|
|
264
|
+
await orchestrator.transition_to(ProjectPhase.ARBITRATED)
|
|
265
|
+
reconciled_findings = []
|
|
266
|
+
final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
|
|
267
|
+
else:
|
|
268
|
+
# 4. Independent Plans (run concurrently — they don't depend on each other)
|
|
269
|
+
progress.add_task(description="Generating Plans A (Pragmatic) and B (Robust)...", total=None)
|
|
270
|
+
plan_a, plan_b = await asyncio.gather(
|
|
271
|
+
plan_service.generate_plan("planner_a", debate_goal, requirements_json, repo_map_json),
|
|
272
|
+
plan_service.generate_plan("planner_b", debate_goal, requirements_json, repo_map_json),
|
|
273
|
+
)
|
|
274
|
+
orchestrator.save_run_artifact("plan_a.json", plan_a.model_dump())
|
|
275
|
+
orchestrator.save_run_artifact("plan_b.json", plan_b.model_dump())
|
|
276
|
+
await orchestrator.transition_to(ProjectPhase.PLANS_GENERATED)
|
|
277
|
+
|
|
278
|
+
# 5. Cross-Critique (independent — run concurrently)
|
|
279
|
+
progress.add_task(description="Critiquing Plans A and B...", total=None)
|
|
280
|
+
critique_a, critique_b = await asyncio.gather(
|
|
281
|
+
critique_service.generate_critique("critic_a", plan_b.model_dump_json(), requirements_json),
|
|
282
|
+
critique_service.generate_critique("critic_b", plan_a.model_dump_json(), requirements_json),
|
|
283
|
+
)
|
|
284
|
+
orchestrator.save_run_artifact("critique_a.json", critique_a.model_dump())
|
|
285
|
+
orchestrator.save_run_artifact("critique_b.json", critique_b.model_dump())
|
|
286
|
+
await orchestrator.transition_to(ProjectPhase.CRITIQUES_GENERATED)
|
|
287
|
+
|
|
288
|
+
# 6. Rebuttals (independent — run concurrently)
|
|
289
|
+
progress.add_task(description="Generating rebuttals...", total=None)
|
|
290
|
+
rebuttal_a, rebuttal_b = await asyncio.gather(
|
|
291
|
+
critique_service.generate_rebuttal("planner_a", plan_a.model_dump_json(), critique_b.model_dump_json()),
|
|
292
|
+
critique_service.generate_rebuttal("planner_b", plan_b.model_dump_json(), critique_a.model_dump_json()),
|
|
293
|
+
)
|
|
294
|
+
orchestrator.save_run_artifact("rebuttal_a.json", rebuttal_a.model_dump())
|
|
295
|
+
orchestrator.save_run_artifact("rebuttal_b.json", rebuttal_b.model_dump())
|
|
296
|
+
|
|
297
|
+
# 7. Arbitration
|
|
298
|
+
progress.add_task(description="Arbitrating final plan...", total=None)
|
|
299
|
+
decision = await arbiter_service.arbitrate(
|
|
300
|
+
debate_goal,
|
|
301
|
+
json.dumps([r.model_dump() for r in spec_output.requirements]),
|
|
302
|
+
plan_a.model_dump_json(),
|
|
303
|
+
plan_b.model_dump_json(),
|
|
304
|
+
critique_a.model_dump_json(),
|
|
305
|
+
critique_b.model_dump_json(),
|
|
306
|
+
rebuttal_a.model_dump_json(),
|
|
307
|
+
rebuttal_b.model_dump_json()
|
|
308
|
+
)
|
|
309
|
+
orchestrator.save_run_artifact("decision.json", decision.model_dump())
|
|
310
|
+
await orchestrator.transition_to(ProjectPhase.ARBITRATED)
|
|
311
|
+
reconciled_findings = _reconcile_findings([*critique_a.findings, *critique_b.findings], decision)
|
|
312
|
+
final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
|
|
313
|
+
|
|
314
|
+
console.print("[green]Planning complete![/green]")
|
|
315
|
+
console.print(f"[blue]Prompt enhancement:[/blue] .devcouncil/runs/{run_id}/prompt_enhancement.json")
|
|
316
|
+
if dry_run:
|
|
317
|
+
console.print("[blue](DRY RUN: No actual LLM calls were made)[/blue]")
|
|
318
|
+
if not persist:
|
|
319
|
+
console.print("[blue](DRY RUN: Final requirements/tasks were not persisted)[/blue]")
|
|
320
|
+
console.print(f"Final Requirements: [bold]{len(decision.final_requirements)}[/bold]")
|
|
321
|
+
console.print(f"Final Tasks: [bold]{len(final_tasks)}[/bold]")
|
|
322
|
+
|
|
236
323
|
# 8. Check Gates
|
|
237
324
|
policy = GatePolicy()
|
|
238
325
|
result = policy.check_plan_approval(
|
|
239
326
|
decision.final_requirements,
|
|
240
|
-
|
|
327
|
+
final_tasks,
|
|
241
328
|
assumptions=spec_output.assumptions,
|
|
242
329
|
findings=reconciled_findings,
|
|
243
330
|
blocking_questions=spec_output.blocking_questions,
|
|
@@ -247,8 +334,18 @@ async def run_plan_flow(
|
|
|
247
334
|
GapRepository(session).delete_plan_gaps()
|
|
248
335
|
|
|
249
336
|
if result.passed:
|
|
337
|
+
if persist:
|
|
338
|
+
with db.get_session() as session:
|
|
339
|
+
PlanningStateRepository(session).replace_active_plan(
|
|
340
|
+
decision.final_requirements,
|
|
341
|
+
spec_output.assumptions,
|
|
342
|
+
final_tasks,
|
|
343
|
+
reconciled_findings,
|
|
344
|
+
)
|
|
345
|
+
|
|
250
346
|
console.print("[green]Plan approved by gates.[/green]")
|
|
251
347
|
await orchestrator.transition_to(ProjectPhase.PLAN_APPROVED)
|
|
348
|
+
return [task.id for task in final_tasks]
|
|
252
349
|
else:
|
|
253
350
|
if persist:
|
|
254
351
|
with db.get_session() as session:
|
|
@@ -257,20 +354,135 @@ async def run_plan_flow(
|
|
|
257
354
|
gap_repo.save(gap)
|
|
258
355
|
console.print("[yellow]Plan generated but failed gates. See status for gaps.[/yellow]")
|
|
259
356
|
await orchestrator.transition_to(ProjectPhase.AWAITING_USER_DECISIONS)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
""
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
357
|
+
return []
|
|
358
|
+
|
|
359
|
+
def _latest_run_with_decision(root: Path, run_id: str | None) -> Path | None:
|
|
360
|
+
runs_dir = root / ".devcouncil" / "runs"
|
|
361
|
+
if run_id:
|
|
362
|
+
candidate = runs_dir / run_id
|
|
363
|
+
return candidate if (candidate / "decision.json").exists() else None
|
|
364
|
+
if not runs_dir.exists():
|
|
365
|
+
return None
|
|
366
|
+
candidates = [d for d in runs_dir.iterdir() if (d / "decision.json").exists()]
|
|
367
|
+
if not candidates:
|
|
368
|
+
return None
|
|
369
|
+
return max(candidates, key=lambda d: (d / "decision.json").stat().st_mtime)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def approve(
|
|
373
|
+
run_id: str | None = typer.Option(None, "--run-id", help="Run whose generated plan to approve (defaults to the most recent run with a decision)."),
|
|
374
|
+
force: bool = typer.Option(False, "--force", help="Approve even if blocking gate gaps remain."),
|
|
375
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
376
|
+
):
|
|
377
|
+
"""
|
|
378
|
+
Approve a generated plan after reviewing gate gaps (AWAITING_USER_DECISIONS -> PLAN_APPROVED).
|
|
379
|
+
"""
|
|
380
|
+
from devcouncil.planning.arbiter_service import ArbiterDecision
|
|
381
|
+
from devcouncil.planning.critique_service import CritiqueOutput
|
|
382
|
+
from devcouncil.planning.spec_service import SpecOutput
|
|
383
|
+
|
|
384
|
+
root = project_root.expanduser().resolve()
|
|
385
|
+
db = get_db(root)
|
|
386
|
+
if not db:
|
|
387
|
+
console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
|
|
388
|
+
raise typer.Exit(code=1)
|
|
389
|
+
|
|
390
|
+
run_dir = _latest_run_with_decision(root, run_id)
|
|
391
|
+
if run_dir is None:
|
|
392
|
+
console.print("[red]No planning run with a decision was found. Run 'dev plan' first.[/red]")
|
|
393
|
+
raise typer.Exit(code=1)
|
|
394
|
+
|
|
395
|
+
decision = ArbiterDecision.model_validate_json((run_dir / "decision.json").read_text(encoding="utf-8"))
|
|
396
|
+
spec_path = run_dir / "requirements.json"
|
|
397
|
+
spec_output = (
|
|
398
|
+
SpecOutput.model_validate_json(spec_path.read_text(encoding="utf-8")) if spec_path.exists() else None
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
findings = []
|
|
402
|
+
for name in ("critique_a.json", "critique_b.json"):
|
|
403
|
+
critique_path = run_dir / name
|
|
404
|
+
if critique_path.exists():
|
|
405
|
+
findings.extend(CritiqueOutput.model_validate_json(critique_path.read_text(encoding="utf-8")).findings)
|
|
406
|
+
reconciled_findings = _reconcile_findings(findings, decision)
|
|
407
|
+
final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
|
|
408
|
+
assumptions = spec_output.assumptions if spec_output else []
|
|
409
|
+
|
|
410
|
+
policy = GatePolicy()
|
|
411
|
+
result = policy.check_plan_approval(
|
|
412
|
+
decision.final_requirements,
|
|
413
|
+
final_tasks,
|
|
414
|
+
assumptions=assumptions,
|
|
415
|
+
findings=reconciled_findings,
|
|
416
|
+
blocking_questions=spec_output.blocking_questions if spec_output else [],
|
|
417
|
+
)
|
|
418
|
+
if not result.passed and not force:
|
|
419
|
+
console.print("[yellow]Plan still fails approval gates:[/yellow]")
|
|
420
|
+
for gap in result.gaps:
|
|
421
|
+
marker = "[red][BLOCKING][/red] " if gap.blocking else ""
|
|
422
|
+
console.print(f" - {marker}{gap.description} (Fix: {gap.recommended_fix})")
|
|
423
|
+
console.print("Resolve the gaps and re-run 'dev plan', or use --force to approve anyway.")
|
|
424
|
+
raise typer.Exit(code=1)
|
|
425
|
+
|
|
426
|
+
with db.get_session() as session:
|
|
427
|
+
GapRepository(session).delete_plan_gaps()
|
|
428
|
+
PlanningStateRepository(session).replace_active_plan(
|
|
429
|
+
decision.final_requirements,
|
|
430
|
+
assumptions,
|
|
431
|
+
final_tasks,
|
|
432
|
+
reconciled_findings,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
orchestrator = Orchestrator(root)
|
|
436
|
+
try:
|
|
437
|
+
asyncio.run(orchestrator.transition_to(ProjectPhase.PLAN_APPROVED))
|
|
438
|
+
except ValueError as exc:
|
|
439
|
+
console.print(f"[red]Cannot approve from the current project phase: {exc}[/red]")
|
|
440
|
+
raise typer.Exit(code=1)
|
|
441
|
+
console.print(f"[green]Plan from run {run_dir.name} approved ({len(final_tasks)} tasks).[/green]")
|
|
442
|
+
console.print("Use 'dev tasks list' to see the planned tasks and 'dev run TASK-ID' to execute one.")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@app.command()
|
|
446
|
+
def plan(
|
|
447
|
+
goal: str = typer.Argument(..., help="The goal of the implementation"),
|
|
448
|
+
requirements_only: bool = typer.Option(False, "--requirements-only", help="Only generate requirements"),
|
|
449
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Simulate planning without LLM calls"),
|
|
450
|
+
quick: bool = typer.Option(
|
|
451
|
+
False,
|
|
452
|
+
"--quick",
|
|
453
|
+
help="Rigor dial: skip the A/B debate, critique, rebuttal, and arbitration. "
|
|
454
|
+
"One spec + one plan (~5 fewer model calls). Verification still gates every diff.",
|
|
455
|
+
),
|
|
456
|
+
persist: bool = typer.Option(
|
|
457
|
+
False,
|
|
458
|
+
"--persist/--no-persist",
|
|
459
|
+
help="Persist dry-run planning artifacts into the main state database.",
|
|
460
|
+
),
|
|
461
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
462
|
+
):
|
|
463
|
+
"""
|
|
464
|
+
Run the full planning cycle (Repo map -> Spec -> Plan A/B -> Critique -> Arbiter).
|
|
465
|
+
"""
|
|
466
|
+
should_persist = persist or not dry_run
|
|
467
|
+
try:
|
|
468
|
+
asyncio.run(run_plan_flow(goal, requirements_only, dry_run, should_persist, project_root, quick=quick))
|
|
469
|
+
except (ProviderRequestError, StructuredOutputError) as exc:
|
|
470
|
+
print_planning_error(exc)
|
|
471
|
+
raise typer.Exit(code=1)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def print_planning_error(exc: Exception) -> None:
|
|
475
|
+
"""Render a planning/model failure as an actionable message instead of a traceback."""
|
|
476
|
+
console.print(f"\n[red]Planning could not complete:[/red] {exc}")
|
|
477
|
+
if isinstance(exc, StructuredOutputError):
|
|
478
|
+
console.print(
|
|
479
|
+
"[yellow]Tip:[/yellow] this role's model could not return valid structured JSON. "
|
|
480
|
+
"Free/very small models often can't. Set a more capable model, e.g.\n"
|
|
481
|
+
f" [bold]dev config models --role {exc.role} --model anthropic/claude-sonnet-4.6[/bold]\n"
|
|
482
|
+
" (or set all roles: [bold]dev config models --model <model>[/bold])"
|
|
483
|
+
)
|
|
484
|
+
elif isinstance(exc, ProviderRequestError) and exc.status_code == 402:
|
|
485
|
+
console.print(
|
|
486
|
+
"[yellow]Tip:[/yellow] add credits at https://openrouter.ai/settings/credits, "
|
|
487
|
+
"or switch to a free/cheaper model with [bold]dev config models --model <model>[/bold]."
|
|
488
|
+
)
|