devcouncil 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +62 -543
- package/package.json +1 -1
- package/pyproject.toml +29 -26
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +135 -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 +143 -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/cli/commands/artifacts.py +51 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/config.py +76 -54
- package/src/devcouncil/cli/commands/dashboard.py +26 -0
- package/src/devcouncil/cli/commands/doctor.py +86 -42
- package/src/devcouncil/cli/commands/go.py +237 -0
- package/src/devcouncil/cli/commands/hook.py +96 -29
- package/src/devcouncil/cli/commands/init.py +67 -56
- package/src/devcouncil/cli/commands/integrate.py +320 -14
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +25 -21
- package/src/devcouncil/cli/commands/plan.py +257 -206
- package/src/devcouncil/cli/commands/prompt.py +36 -33
- package/src/devcouncil/cli/commands/repair.py +72 -69
- package/src/devcouncil/cli/commands/report.py +112 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
- package/src/devcouncil/cli/commands/rollback.py +49 -47
- package/src/devcouncil/cli/commands/run.py +252 -207
- package/src/devcouncil/cli/commands/setup.py +159 -18
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/status.py +117 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +2 -1
- package/src/devcouncil/cli/commands/verify.py +158 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/main.py +42 -24
- 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 +27 -27
- package/src/devcouncil/domain/gap.py +26 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +26 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/hook_policy.py +24 -3
- package/src/devcouncil/execution/patch.py +28 -28
- package/src/devcouncil/execution/permissions.py +44 -44
- package/src/devcouncil/execution/prompt_builder.py +23 -23
- package/src/devcouncil/execution/task_runner.py +63 -63
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/coding_cli.py +112 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +81 -81
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +50 -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 +34 -34
- package/src/devcouncil/gating/policy.py +157 -157
- 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 +120 -0
- package/src/devcouncil/indexing/repo_mapper.py +208 -204
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/gitnexus.py +27 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +549 -96
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +207 -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 +138 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +38 -38
- package/src/devcouncil/llm/provider.py +146 -125
- package/src/devcouncil/llm/router.py +111 -111
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/critique_service.py +66 -66
- package/src/devcouncil/planning/plan_service.py +46 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +44 -44
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +20 -17
- package/src/devcouncil/reporting/markdown_report.py +68 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/storage/db.py +66 -66
- package/src/devcouncil/storage/models.py +83 -83
- package/src/devcouncil/storage/repositories.py +299 -222
- package/src/devcouncil/telemetry/cost.py +34 -34
- package/src/devcouncil/telemetry/tracker.py +49 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +122 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +141 -141
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/verifier.py +319 -302
- package/uv.lock +1 -1
|
@@ -3,35 +3,46 @@ 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 MockProvider, create_provider, validate_model_provider
|
|
19
|
+
from devcouncil.llm.router import ModelRouter
|
|
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 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
|
+
DEFAULT_PLANNING_MODEL = "anthropic/claude-3.5-sonnet"
|
|
36
|
+
REQUIRED_PLANNING_ROLES = (
|
|
37
|
+
"prompt_enhancer",
|
|
38
|
+
"spec_writer",
|
|
39
|
+
"planner_a",
|
|
40
|
+
"planner_b",
|
|
41
|
+
"critic_a",
|
|
42
|
+
"critic_b",
|
|
43
|
+
"arbiter",
|
|
44
|
+
)
|
|
45
|
+
|
|
35
46
|
|
|
36
47
|
def _decision_ids(items: list[Any]) -> set[str]:
|
|
37
48
|
ids: set[str] = set()
|
|
@@ -57,187 +68,215 @@ def _reconcile_findings(findings, decision):
|
|
|
57
68
|
else:
|
|
58
69
|
reconciled.append(finding)
|
|
59
70
|
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
|
-
|
|
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
|
-
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _ensure_planning_roles(config) -> None:
|
|
74
|
+
fallback = config.models.roles.get("spec_writer")
|
|
75
|
+
if fallback is None and config.models.roles:
|
|
76
|
+
fallback = next(iter(config.models.roles.values()))
|
|
77
|
+
if fallback is None:
|
|
78
|
+
fallback = ModelRoleConfig(model=DEFAULT_PLANNING_MODEL)
|
|
79
|
+
|
|
80
|
+
for role in REQUIRED_PLANNING_ROLES:
|
|
81
|
+
config.models.roles.setdefault(role, fallback.model_copy())
|
|
82
|
+
|
|
83
|
+
async def run_plan_flow(
|
|
84
|
+
goal: str,
|
|
85
|
+
requirements_only: bool = False,
|
|
86
|
+
dry_run: bool = False,
|
|
87
|
+
persist: bool = True,
|
|
88
|
+
project_root: Path = Path("."),
|
|
89
|
+
):
|
|
90
|
+
root = project_root.expanduser().resolve()
|
|
91
|
+
initialize_project(root, quiet=True)
|
|
92
|
+
db = get_db(root)
|
|
93
|
+
if not db:
|
|
94
|
+
console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
|
|
95
|
+
return []
|
|
96
|
+
|
|
97
|
+
# Load validated config
|
|
98
|
+
config = load_config(root)
|
|
99
|
+
_ensure_planning_roles(config)
|
|
100
|
+
|
|
101
|
+
api_key = None
|
|
102
|
+
if not dry_run:
|
|
103
|
+
try:
|
|
104
|
+
validate_model_provider(config.models.provider)
|
|
105
|
+
api_key = get_api_key(config.models.provider, root)
|
|
106
|
+
except ValueError as e:
|
|
107
|
+
console.print(f"[red]{e}[/red]")
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
orchestrator = Orchestrator(root, persist_state=persist)
|
|
111
|
+
orchestrator.reset_state_machine(ProjectPhase.NEW)
|
|
112
|
+
run_id = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-plan"
|
|
113
|
+
await orchestrator.start_run(run_id, goal)
|
|
114
|
+
|
|
115
|
+
if dry_run:
|
|
116
|
+
# Override config models to be unique roles for mock mapping
|
|
117
|
+
for role in REQUIRED_PLANNING_ROLES:
|
|
118
|
+
config.models.roles[role].model = f"mock/{role}"
|
|
119
|
+
|
|
120
|
+
provider = MockProvider()
|
|
121
|
+
provider.responses = {
|
|
122
|
+
"mock/prompt_enhancer": json.dumps({
|
|
123
|
+
"original_goal": goal,
|
|
124
|
+
"enhanced_goal": (
|
|
125
|
+
f"Plan and implement {goal} using the mapped repository's "
|
|
126
|
+
"existing patterns, tests, and verification gates."
|
|
127
|
+
),
|
|
128
|
+
"codebase_context": ["Use the repository map to target existing application and test structure."],
|
|
129
|
+
"debate_focus": ["Compare minimal implementation scope against production-readiness concerns."],
|
|
130
|
+
"constraints": [f"Do not broaden beyond: {goal}."]
|
|
131
|
+
}),
|
|
132
|
+
"mock/spec_writer": json.dumps({
|
|
133
|
+
"requirements": [{"id": "REQ-001", "title": "Mock Req", "description": "Desc", "priority": "high", "source": "user", "acceptance_criteria": []}],
|
|
134
|
+
"assumptions": [],
|
|
135
|
+
"blocking_questions": []
|
|
136
|
+
}),
|
|
137
|
+
"mock/planner_a": [
|
|
138
|
+
json.dumps({
|
|
139
|
+
"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"}]
|
|
140
|
+
}),
|
|
141
|
+
json.dumps({"rebuttals": []})
|
|
142
|
+
],
|
|
143
|
+
"mock/planner_b": [
|
|
144
|
+
json.dumps({
|
|
145
|
+
"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"}]
|
|
146
|
+
}),
|
|
147
|
+
json.dumps({"rebuttals": []})
|
|
148
|
+
],
|
|
149
|
+
"mock/critic_a": '{"findings": []}',
|
|
150
|
+
"mock/critic_b": '{"findings": []}',
|
|
151
|
+
"mock/arbiter": json.dumps({
|
|
152
|
+
"accepted_finding_ids": [], "rejected_finding_ids": [],
|
|
153
|
+
"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"}]}],
|
|
154
|
+
"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"}]
|
|
155
|
+
}),
|
|
156
|
+
}
|
|
157
|
+
# Special case: PlanService calls use the same model names.
|
|
158
|
+
# I'll modify PlanService to use a slightly different role string if needed,
|
|
159
|
+
# but for Dry Run, let's just make the MockProvider return based on the schema requested.
|
|
160
|
+
else:
|
|
161
|
+
provider = create_provider(config.models.provider, api_key)
|
|
162
|
+
|
|
163
|
+
# Build role config after dry-run overrides so mocks are routed correctly.
|
|
164
|
+
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
165
|
+
router = ModelRouter(provider, role_config)
|
|
166
|
+
|
|
167
|
+
prompt_enhancer = PromptEnhancerService(router)
|
|
168
|
+
spec_service = SpecService(router)
|
|
169
|
+
plan_service = PlanService(router)
|
|
170
|
+
critique_service = CritiqueService(router)
|
|
171
|
+
arbiter_service = ArbiterService(router)
|
|
172
|
+
mapper = RepoMapper(root)
|
|
173
|
+
|
|
174
|
+
with Progress(
|
|
175
|
+
SpinnerColumn(),
|
|
176
|
+
TextColumn("[progress.description]{task.description}"),
|
|
177
|
+
transient=True,
|
|
178
|
+
) as progress:
|
|
179
|
+
# 1. Repo Map
|
|
180
|
+
progress.add_task(description="Mapping repository...", total=None)
|
|
147
181
|
repo_map = mapper.map_repo(goal)
|
|
148
182
|
repo_map_json = repo_map.model_dump_json(indent=2)
|
|
149
183
|
orchestrator.save_run_artifact("repo_map.json", json.loads(repo_map_json))
|
|
150
|
-
graph_context = CodeReviewGraphAdapter(
|
|
184
|
+
graph_context = CodeReviewGraphAdapter(root).get_context()
|
|
151
185
|
if graph_context.available:
|
|
152
186
|
orchestrator.save_run_artifact("code_review_graph_context.json", graph_context.model_dump())
|
|
153
187
|
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
|
-
orchestrator.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
progress.add_task(description="
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
)
|
|
188
|
+
|
|
189
|
+
# 2. Codebase-specific prompt enhancement
|
|
190
|
+
progress.add_task(description="Enhancing prompt for codebase debate...", total=None)
|
|
191
|
+
prompt_enhancement = await prompt_enhancer.enhance_prompt(
|
|
192
|
+
goal,
|
|
193
|
+
repo_map_json,
|
|
194
|
+
graph_context.model_dump_json() if graph_context.available else None,
|
|
195
|
+
)
|
|
196
|
+
debate_goal = prompt_enhancement.debate_prompt()
|
|
197
|
+
orchestrator.save_run_artifact("prompt_enhancement.json", prompt_enhancement.model_dump())
|
|
198
|
+
TraceLogger(root).log_event(
|
|
199
|
+
"prompt_enhanced",
|
|
200
|
+
{
|
|
201
|
+
"original_goal": goal,
|
|
202
|
+
"enhanced_goal": prompt_enhancement.enhanced_goal,
|
|
203
|
+
"codebase_context_count": len(prompt_enhancement.codebase_context),
|
|
204
|
+
"constraint_count": len(prompt_enhancement.constraints),
|
|
205
|
+
"debate_focus_count": len(prompt_enhancement.debate_focus),
|
|
206
|
+
"artifact": f".devcouncil/runs/{run_id}/prompt_enhancement.json",
|
|
207
|
+
},
|
|
208
|
+
run_id=run_id,
|
|
209
|
+
summary="Prompt enhanced for codebase-specific debate.",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# 3. Spec / Requirements
|
|
213
|
+
progress.add_task(description="Generating requirements...", total=None)
|
|
214
|
+
spec_output = await spec_service.generate_spec(debate_goal, repo_map_json)
|
|
215
|
+
orchestrator.save_run_artifact("requirements.json", spec_output.model_dump())
|
|
216
|
+
await orchestrator.transition_to(ProjectPhase.REQUIREMENTS_DRAFTED)
|
|
217
|
+
|
|
218
|
+
if requirements_only:
|
|
219
|
+
console.print(Panel(f"Found {len(spec_output.requirements)} requirements.", title="Requirements Generated"))
|
|
220
|
+
return []
|
|
221
|
+
|
|
222
|
+
# 4. Independent Plans
|
|
223
|
+
progress.add_task(description="Generating Plan A (Pragmatic)...", total=None)
|
|
224
|
+
plan_a = await plan_service.generate_plan("planner_a", debate_goal, json.dumps([r.model_dump() for r in spec_output.requirements]), repo_map_json)
|
|
225
|
+
orchestrator.save_run_artifact("plan_a.json", plan_a.model_dump())
|
|
226
|
+
|
|
227
|
+
progress.add_task(description="Generating Plan B (Robust)...", total=None)
|
|
228
|
+
plan_b = await plan_service.generate_plan("planner_b", debate_goal, json.dumps([r.model_dump() for r in spec_output.requirements]), repo_map_json)
|
|
229
|
+
orchestrator.save_run_artifact("plan_b.json", plan_b.model_dump())
|
|
230
|
+
await orchestrator.transition_to(ProjectPhase.PLANS_GENERATED)
|
|
231
|
+
|
|
232
|
+
# 5. Cross-Critique
|
|
233
|
+
progress.add_task(description="Critiquing Plan B...", total=None)
|
|
234
|
+
critique_a = await critique_service.generate_critique("critic_a", plan_b.model_dump_json(), json.dumps([r.model_dump() for r in spec_output.requirements]))
|
|
235
|
+
orchestrator.save_run_artifact("critique_a.json", critique_a.model_dump())
|
|
236
|
+
|
|
237
|
+
progress.add_task(description="Critiquing Plan A...", total=None)
|
|
238
|
+
critique_b = await critique_service.generate_critique("critic_b", plan_a.model_dump_json(), json.dumps([r.model_dump() for r in spec_output.requirements]))
|
|
239
|
+
orchestrator.save_run_artifact("critique_b.json", critique_b.model_dump())
|
|
240
|
+
await orchestrator.transition_to(ProjectPhase.CRITIQUES_GENERATED)
|
|
241
|
+
|
|
242
|
+
# 6. Rebuttals
|
|
243
|
+
progress.add_task(description="Generating rebuttals...", total=None)
|
|
244
|
+
rebuttal_a = await critique_service.generate_rebuttal("planner_a", plan_a.model_dump_json(), critique_b.model_dump_json())
|
|
245
|
+
orchestrator.save_run_artifact("rebuttal_a.json", rebuttal_a.model_dump())
|
|
246
|
+
rebuttal_b = await critique_service.generate_rebuttal("planner_b", plan_b.model_dump_json(), critique_a.model_dump_json())
|
|
247
|
+
orchestrator.save_run_artifact("rebuttal_b.json", rebuttal_b.model_dump())
|
|
248
|
+
|
|
249
|
+
# 7. Arbitration
|
|
250
|
+
progress.add_task(description="Arbitrating final plan...", total=None)
|
|
251
|
+
decision = await arbiter_service.arbitrate(
|
|
252
|
+
debate_goal,
|
|
253
|
+
json.dumps([r.model_dump() for r in spec_output.requirements]),
|
|
254
|
+
plan_a.model_dump_json(),
|
|
255
|
+
plan_b.model_dump_json(),
|
|
256
|
+
critique_a.model_dump_json(),
|
|
257
|
+
critique_b.model_dump_json(),
|
|
258
|
+
rebuttal_a.model_dump_json(),
|
|
259
|
+
rebuttal_b.model_dump_json()
|
|
260
|
+
)
|
|
204
261
|
orchestrator.save_run_artifact("decision.json", decision.model_dump())
|
|
205
262
|
await orchestrator.transition_to(ProjectPhase.ARBITRATED)
|
|
206
263
|
reconciled_findings = _reconcile_findings([*critique_a.findings, *critique_b.findings], decision)
|
|
264
|
+
final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
|
|
207
265
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
req_repo.save(req)
|
|
218
|
-
|
|
219
|
-
for assumption in spec_output.assumptions:
|
|
220
|
-
assumption_repo.save(assumption)
|
|
221
|
-
|
|
222
|
-
for task in decision.final_tasks:
|
|
223
|
-
task_repo.save(task)
|
|
224
|
-
|
|
225
|
-
for finding in reconciled_findings:
|
|
226
|
-
finding_repo.save(finding)
|
|
227
|
-
|
|
228
|
-
console.print("[green]Planning complete![/green]")
|
|
229
|
-
if dry_run:
|
|
230
|
-
console.print("[blue](DRY RUN: No actual LLM calls were made)[/blue]")
|
|
231
|
-
if not persist:
|
|
232
|
-
console.print("[blue](DRY RUN: Final requirements/tasks were not persisted)[/blue]")
|
|
233
|
-
console.print(f"Final Requirements: [bold]{len(decision.final_requirements)}[/bold]")
|
|
234
|
-
console.print(f"Final Tasks: [bold]{len(decision.final_tasks)}[/bold]")
|
|
235
|
-
|
|
266
|
+
console.print("[green]Planning complete![/green]")
|
|
267
|
+
console.print(f"[blue]Prompt enhancement:[/blue] .devcouncil/runs/{run_id}/prompt_enhancement.json")
|
|
268
|
+
if dry_run:
|
|
269
|
+
console.print("[blue](DRY RUN: No actual LLM calls were made)[/blue]")
|
|
270
|
+
if not persist:
|
|
271
|
+
console.print("[blue](DRY RUN: Final requirements/tasks were not persisted)[/blue]")
|
|
272
|
+
console.print(f"Final Requirements: [bold]{len(decision.final_requirements)}[/bold]")
|
|
273
|
+
console.print(f"Final Tasks: [bold]{len(final_tasks)}[/bold]")
|
|
274
|
+
|
|
236
275
|
# 8. Check Gates
|
|
237
276
|
policy = GatePolicy()
|
|
238
277
|
result = policy.check_plan_approval(
|
|
239
278
|
decision.final_requirements,
|
|
240
|
-
|
|
279
|
+
final_tasks,
|
|
241
280
|
assumptions=spec_output.assumptions,
|
|
242
281
|
findings=reconciled_findings,
|
|
243
282
|
blocking_questions=spec_output.blocking_questions,
|
|
@@ -247,8 +286,18 @@ async def run_plan_flow(
|
|
|
247
286
|
GapRepository(session).delete_plan_gaps()
|
|
248
287
|
|
|
249
288
|
if result.passed:
|
|
289
|
+
if persist:
|
|
290
|
+
with db.get_session() as session:
|
|
291
|
+
PlanningStateRepository(session).replace_active_plan(
|
|
292
|
+
decision.final_requirements,
|
|
293
|
+
spec_output.assumptions,
|
|
294
|
+
final_tasks,
|
|
295
|
+
reconciled_findings,
|
|
296
|
+
)
|
|
297
|
+
|
|
250
298
|
console.print("[green]Plan approved by gates.[/green]")
|
|
251
299
|
await orchestrator.transition_to(ProjectPhase.PLAN_APPROVED)
|
|
300
|
+
return [task.id for task in final_tasks]
|
|
252
301
|
else:
|
|
253
302
|
if persist:
|
|
254
303
|
with db.get_session() as session:
|
|
@@ -257,20 +306,22 @@ async def run_plan_flow(
|
|
|
257
306
|
gap_repo.save(gap)
|
|
258
307
|
console.print("[yellow]Plan generated but failed gates. See status for gaps.[/yellow]")
|
|
259
308
|
await orchestrator.transition_to(ProjectPhase.AWAITING_USER_DECISIONS)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
"""
|
|
273
|
-
|
|
274
|
-
"""
|
|
275
|
-
|
|
276
|
-
|
|
309
|
+
return []
|
|
310
|
+
|
|
311
|
+
@app.command()
|
|
312
|
+
def plan(
|
|
313
|
+
goal: str = typer.Argument(..., help="The goal of the implementation"),
|
|
314
|
+
requirements_only: bool = typer.Option(False, "--requirements-only", help="Only generate requirements"),
|
|
315
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Simulate planning without LLM calls"),
|
|
316
|
+
persist: bool = typer.Option(
|
|
317
|
+
False,
|
|
318
|
+
"--persist/--no-persist",
|
|
319
|
+
help="Persist dry-run planning artifacts into the main state database.",
|
|
320
|
+
),
|
|
321
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
322
|
+
):
|
|
323
|
+
"""
|
|
324
|
+
Run the full planning cycle (Repo map -> Spec -> Plan A/B -> Critique -> Arbiter).
|
|
325
|
+
"""
|
|
326
|
+
should_persist = persist or not dry_run
|
|
327
|
+
asyncio.run(run_plan_flow(goal, requirements_only, dry_run, should_persist, project_root))
|
|
@@ -1,44 +1,47 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
|
-
from rich.markdown import Markdown
|
|
4
|
-
from devcouncil.
|
|
5
|
-
from devcouncil.storage.
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.markdown import Markdown
|
|
4
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
5
|
+
from devcouncil.storage.db import get_db
|
|
6
|
+
from devcouncil.storage.repositories import TaskRepository, RequirementRepository
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from devcouncil.execution.prompt_builder import PromptBuilder
|
|
9
|
-
|
|
10
|
-
app = typer.Typer()
|
|
11
|
-
console = Console()
|
|
12
|
-
|
|
13
|
-
@app.callback(invoke_without_command=True)
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
@app.callback(invoke_without_command=True)
|
|
14
15
|
def prompt(
|
|
15
16
|
ctx: typer.Context,
|
|
16
17
|
task_id: str = typer.Argument(..., help="ID of the task to generate a prompt for"),
|
|
17
18
|
pretty: bool = typer.Option(False, "--pretty", help="Render the prompt for terminal reading."),
|
|
19
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
18
20
|
):
|
|
19
|
-
"""
|
|
20
|
-
Generate a constrained prompt for a specific task.
|
|
21
|
-
"""
|
|
22
|
-
if ctx.invoked_subcommand is not None:
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
"""
|
|
22
|
+
Generate a constrained prompt for a specific task.
|
|
23
|
+
"""
|
|
24
|
+
if ctx.invoked_subcommand is not None:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
root = project_root.expanduser().resolve()
|
|
28
|
+
initialize_project(root, quiet=True)
|
|
29
|
+
db = get_db(root)
|
|
30
|
+
if not db:
|
|
31
|
+
raise typer.Exit(code=1)
|
|
32
|
+
|
|
33
|
+
with db.get_session() as session:
|
|
34
|
+
task_repo = TaskRepository(session)
|
|
35
|
+
req_repo = RequirementRepository(session)
|
|
36
|
+
|
|
37
|
+
task = task_repo.get_by_id(task_id)
|
|
38
|
+
if not task:
|
|
39
|
+
console.print(f"[red]Task {task_id} not found.[/red]")
|
|
40
|
+
raise typer.Exit(code=1)
|
|
41
|
+
|
|
42
|
+
reqs = req_repo.get_all()
|
|
43
|
+
|
|
44
|
+
builder = PromptBuilder(root)
|
|
42
45
|
task_prompt = builder.build_task_prompt(task, reqs)
|
|
43
46
|
|
|
44
47
|
if pretty:
|