devcouncil 0.1.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 -0
- package/README.md +643 -0
- package/bin/devcouncil.js +62 -0
- package/package.json +47 -0
- package/pyproject.toml +31 -0
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +4 -0
- package/src/devcouncil/app/__init__.py +28 -0
- package/src/devcouncil/app/config.py +131 -0
- package/src/devcouncil/app/errors.py +23 -0
- package/src/devcouncil/app/events.py +44 -0
- package/src/devcouncil/app/orchestrator.py +92 -0
- package/src/devcouncil/app/run_context.py +39 -0
- package/src/devcouncil/app/state_machine.py +108 -0
- package/src/devcouncil/artifacts/__init__.py +1 -0
- package/src/devcouncil/artifacts/coverage.py +96 -0
- package/src/devcouncil/artifacts/graph.py +143 -0
- package/src/devcouncil/artifacts/migrations.py +20 -0
- package/src/devcouncil/artifacts/schemas.py +23 -0
- package/src/devcouncil/artifacts/serializer.py +21 -0
- package/src/devcouncil/artifacts/validators.py +27 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/artifacts.py +48 -0
- package/src/devcouncil/cli/commands/baseline.py +32 -0
- package/src/devcouncil/cli/commands/config.py +54 -0
- package/src/devcouncil/cli/commands/doctor.py +96 -0
- package/src/devcouncil/cli/commands/hook.py +61 -0
- package/src/devcouncil/cli/commands/init.py +142 -0
- package/src/devcouncil/cli/commands/integrate.py +420 -0
- package/src/devcouncil/cli/commands/map.py +38 -0
- package/src/devcouncil/cli/commands/mcp_server.py +18 -0
- package/src/devcouncil/cli/commands/plan.py +276 -0
- package/src/devcouncil/cli/commands/prompt.py +47 -0
- package/src/devcouncil/cli/commands/repair.py +69 -0
- package/src/devcouncil/cli/commands/report.py +71 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +28 -0
- package/src/devcouncil/cli/commands/rollback.py +58 -0
- package/src/devcouncil/cli/commands/run.py +224 -0
- package/src/devcouncil/cli/commands/setup.py +82 -0
- package/src/devcouncil/cli/commands/show.py +57 -0
- package/src/devcouncil/cli/commands/status.py +105 -0
- package/src/devcouncil/cli/commands/tasks.py +41 -0
- package/src/devcouncil/cli/commands/trace.py +43 -0
- package/src/devcouncil/cli/commands/verify.py +163 -0
- package/src/devcouncil/cli/commands/version.py +20 -0
- package/src/devcouncil/cli/main.py +70 -0
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +19 -0
- package/src/devcouncil/council/prompts/critic_a.md +10 -0
- package/src/devcouncil/council/prompts/critic_b.md +10 -0
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -0
- package/src/devcouncil/council/prompts/planner_a.md +16 -0
- package/src/devcouncil/council/prompts/planner_b.md +16 -0
- package/src/devcouncil/council/prompts/rebuttal.md +10 -0
- package/src/devcouncil/council/prompts/spec_writer.md +12 -0
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +17 -0
- package/src/devcouncil/domain/critique.py +32 -0
- package/src/devcouncil/domain/evidence.py +27 -0
- package/src/devcouncil/domain/gap.py +26 -0
- package/src/devcouncil/domain/requirement.py +22 -0
- package/src/devcouncil/domain/task.py +26 -0
- package/src/devcouncil/execution/__init__.py +1 -0
- package/src/devcouncil/execution/context_builder.py +60 -0
- package/src/devcouncil/execution/executor.py +15 -0
- package/src/devcouncil/execution/hook_policy.py +144 -0
- package/src/devcouncil/execution/patch.py +28 -0
- package/src/devcouncil/execution/paths.py +14 -0
- package/src/devcouncil/execution/permissions.py +92 -0
- package/src/devcouncil/execution/prompt_builder.py +59 -0
- package/src/devcouncil/execution/task_runner.py +166 -0
- package/src/devcouncil/executors/__init__.py +1 -0
- package/src/devcouncil/executors/mini_swe.py +73 -0
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +107 -0
- package/src/devcouncil/executors/openhands.py +71 -0
- package/src/devcouncil/gating/__init__.py +1 -0
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +45 -0
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -0
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -0
- package/src/devcouncil/gating/checks/secret_scan_check.py +34 -0
- package/src/devcouncil/gating/policy.py +190 -0
- package/src/devcouncil/indexing/__init__.py +1 -0
- package/src/devcouncil/indexing/graph_index.py +48 -0
- package/src/devcouncil/indexing/repo_mapper.py +204 -0
- package/src/devcouncil/indexing/symbol_index.py +0 -0
- package/src/devcouncil/integrations/code_review_graph.py +163 -0
- package/src/devcouncil/integrations/github.py +39 -0
- package/src/devcouncil/integrations/gitnexus.py +27 -0
- package/src/devcouncil/integrations/graphify.py +34 -0
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +146 -0
- package/src/devcouncil/llm/__init__.py +1 -0
- package/src/devcouncil/llm/cache.py +38 -0
- package/src/devcouncil/llm/provider.py +125 -0
- package/src/devcouncil/llm/router.py +125 -0
- package/src/devcouncil/planning/__init__.py +1 -0
- package/src/devcouncil/planning/arbiter_service.py +57 -0
- package/src/devcouncil/planning/critique_service.py +66 -0
- package/src/devcouncil/planning/plan_service.py +46 -0
- package/src/devcouncil/planning/repair_service.py +39 -0
- package/src/devcouncil/planning/spec_service.py +44 -0
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +32 -0
- package/src/devcouncil/reporting/json_report.py +17 -0
- package/src/devcouncil/reporting/markdown_report.py +46 -0
- package/src/devcouncil/reporting/report_builder.py +14 -0
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +66 -0
- package/src/devcouncil/storage/models.py +83 -0
- package/src/devcouncil/storage/repositories.py +346 -0
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +34 -0
- package/src/devcouncil/telemetry/traces.py +91 -0
- package/src/devcouncil/telemetry/tracker.py +49 -0
- package/src/devcouncil/utils/__init__.py +1 -0
- package/src/devcouncil/utils/redaction.py +141 -0
- package/src/devcouncil/verification/__init__.py +1 -0
- package/src/devcouncil/verification/implementation_reviewer.py +55 -0
- package/src/devcouncil/verification/verifier.py +513 -0
- package/uv.lock +1085 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
import datetime
|
|
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
|
|
12
|
+
from devcouncil.storage.repositories import (
|
|
13
|
+
RequirementRepository,
|
|
14
|
+
AssumptionRepository,
|
|
15
|
+
TaskRepository,
|
|
16
|
+
CritiqueFindingRepository,
|
|
17
|
+
GapRepository,
|
|
18
|
+
)
|
|
19
|
+
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
20
|
+
from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
|
|
21
|
+
from devcouncil.llm.provider import OpenRouterProvider, MockProvider
|
|
22
|
+
from devcouncil.llm.router import ModelRouter
|
|
23
|
+
from devcouncil.planning.spec_service import SpecService
|
|
24
|
+
from devcouncil.planning.plan_service import PlanService
|
|
25
|
+
from devcouncil.planning.critique_service import CritiqueService
|
|
26
|
+
from devcouncil.planning.arbiter_service import ArbiterService
|
|
27
|
+
from devcouncil.gating.policy import GatePolicy
|
|
28
|
+
from devcouncil.app.orchestrator import Orchestrator
|
|
29
|
+
from devcouncil.app.state_machine import ProjectPhase
|
|
30
|
+
from devcouncil.app.config import load_config, get_api_key
|
|
31
|
+
|
|
32
|
+
app = typer.Typer()
|
|
33
|
+
console = Console()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _decision_ids(items: list[Any]) -> set[str]:
|
|
37
|
+
ids: set[str] = set()
|
|
38
|
+
for item in items:
|
|
39
|
+
if isinstance(item, str):
|
|
40
|
+
ids.add(item)
|
|
41
|
+
elif isinstance(item, dict):
|
|
42
|
+
value = item.get("id") or item.get("finding_id")
|
|
43
|
+
if value:
|
|
44
|
+
ids.add(str(value))
|
|
45
|
+
return ids
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _reconcile_findings(findings, decision):
|
|
49
|
+
accepted_ids = set(decision.accepted_finding_ids)
|
|
50
|
+
rejected_ids = _decision_ids(decision.rejected_finding_ids)
|
|
51
|
+
reconciled = []
|
|
52
|
+
for finding in findings:
|
|
53
|
+
if finding.id in accepted_ids:
|
|
54
|
+
reconciled.append(finding.model_copy(update={"status": "converted"}))
|
|
55
|
+
elif finding.id in rejected_ids:
|
|
56
|
+
reconciled.append(finding.model_copy(update={"status": "rejected"}))
|
|
57
|
+
else:
|
|
58
|
+
reconciled.append(finding)
|
|
59
|
+
return reconciled
|
|
60
|
+
|
|
61
|
+
async def run_plan_flow(
|
|
62
|
+
goal: str,
|
|
63
|
+
requirements_only: bool = False,
|
|
64
|
+
dry_run: bool = False,
|
|
65
|
+
persist: bool = True,
|
|
66
|
+
):
|
|
67
|
+
db = get_db()
|
|
68
|
+
if not db:
|
|
69
|
+
console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Load validated config
|
|
73
|
+
config = load_config(Path("."))
|
|
74
|
+
|
|
75
|
+
api_key = None
|
|
76
|
+
if not dry_run:
|
|
77
|
+
try:
|
|
78
|
+
api_key = get_api_key(config.models.provider)
|
|
79
|
+
except ValueError as e:
|
|
80
|
+
console.print(f"[red]{e}[/red]")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
orchestrator = Orchestrator(Path("."), persist_state=persist)
|
|
84
|
+
orchestrator.reset_state_machine(ProjectPhase.NEW)
|
|
85
|
+
run_id = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-plan"
|
|
86
|
+
await orchestrator.start_run(run_id, goal)
|
|
87
|
+
|
|
88
|
+
if dry_run:
|
|
89
|
+
# Override config models to be unique roles for mock mapping
|
|
90
|
+
config.models.roles["spec_writer"].model = "mock/spec_writer"
|
|
91
|
+
config.models.roles["planner_a"].model = "mock/planner_a"
|
|
92
|
+
config.models.roles["planner_b"].model = "mock/planner_b"
|
|
93
|
+
config.models.roles["critic_a"].model = "mock/critic_a"
|
|
94
|
+
config.models.roles["critic_b"].model = "mock/critic_b"
|
|
95
|
+
config.models.roles["arbiter"].model = "mock/arbiter"
|
|
96
|
+
|
|
97
|
+
provider = MockProvider()
|
|
98
|
+
provider.responses = {
|
|
99
|
+
"mock/spec_writer": json.dumps({
|
|
100
|
+
"requirements": [{"id": "REQ-001", "title": "Mock Req", "description": "Desc", "priority": "high", "source": "user", "acceptance_criteria": []}],
|
|
101
|
+
"assumptions": [],
|
|
102
|
+
"blocking_questions": []
|
|
103
|
+
}),
|
|
104
|
+
"mock/planner_a": [
|
|
105
|
+
json.dumps({
|
|
106
|
+
"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"}]
|
|
107
|
+
}),
|
|
108
|
+
json.dumps({"rebuttals": []})
|
|
109
|
+
],
|
|
110
|
+
"mock/planner_b": [
|
|
111
|
+
json.dumps({
|
|
112
|
+
"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"}]
|
|
113
|
+
}),
|
|
114
|
+
json.dumps({"rebuttals": []})
|
|
115
|
+
],
|
|
116
|
+
"mock/critic_a": '{"findings": []}',
|
|
117
|
+
"mock/critic_b": '{"findings": []}',
|
|
118
|
+
"mock/arbiter": json.dumps({
|
|
119
|
+
"accepted_finding_ids": [], "rejected_finding_ids": [],
|
|
120
|
+
"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"}]}],
|
|
121
|
+
"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"}]
|
|
122
|
+
}),
|
|
123
|
+
}
|
|
124
|
+
# Special case: PlanService calls use the same model names.
|
|
125
|
+
# I'll modify PlanService to use a slightly different role string if needed,
|
|
126
|
+
# but for Dry Run, let's just make the MockProvider return based on the schema requested.
|
|
127
|
+
else:
|
|
128
|
+
provider = OpenRouterProvider(api_key)
|
|
129
|
+
|
|
130
|
+
# Build role config after dry-run overrides so mocks are routed correctly.
|
|
131
|
+
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
132
|
+
router = ModelRouter(provider, role_config)
|
|
133
|
+
|
|
134
|
+
spec_service = SpecService(router)
|
|
135
|
+
plan_service = PlanService(router)
|
|
136
|
+
critique_service = CritiqueService(router)
|
|
137
|
+
arbiter_service = ArbiterService(router)
|
|
138
|
+
mapper = RepoMapper(Path("."))
|
|
139
|
+
|
|
140
|
+
with Progress(
|
|
141
|
+
SpinnerColumn(),
|
|
142
|
+
TextColumn("[progress.description]{task.description}"),
|
|
143
|
+
transient=True,
|
|
144
|
+
) as progress:
|
|
145
|
+
# 1. Repo Map
|
|
146
|
+
progress.add_task(description="Mapping repository...", total=None)
|
|
147
|
+
repo_map = mapper.map_repo(goal)
|
|
148
|
+
repo_map_json = repo_map.model_dump_json(indent=2)
|
|
149
|
+
orchestrator.save_run_artifact("repo_map.json", json.loads(repo_map_json))
|
|
150
|
+
graph_context = CodeReviewGraphAdapter(Path(".")).get_context()
|
|
151
|
+
if graph_context.available:
|
|
152
|
+
orchestrator.save_run_artifact("code_review_graph_context.json", graph_context.model_dump())
|
|
153
|
+
await orchestrator.transition_to(ProjectPhase.REPO_MAPPED)
|
|
154
|
+
|
|
155
|
+
# 2. Spec / Requirements
|
|
156
|
+
progress.add_task(description="Generating requirements...", total=None)
|
|
157
|
+
spec_output = await spec_service.generate_spec(goal, repo_map_json)
|
|
158
|
+
orchestrator.save_run_artifact("requirements.json", spec_output.model_dump())
|
|
159
|
+
await orchestrator.transition_to(ProjectPhase.REQUIREMENTS_DRAFTED)
|
|
160
|
+
|
|
161
|
+
if requirements_only:
|
|
162
|
+
console.print(Panel(f"Found {len(spec_output.requirements)} requirements.", title="Requirements Generated"))
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# 3. Independent Plans
|
|
166
|
+
progress.add_task(description="Generating Plan A (Pragmatic)...", total=None)
|
|
167
|
+
plan_a = await plan_service.generate_plan("planner_a", goal, json.dumps([r.model_dump() for r in spec_output.requirements]), repo_map_json)
|
|
168
|
+
orchestrator.save_run_artifact("plan_a.json", plan_a.model_dump())
|
|
169
|
+
|
|
170
|
+
progress.add_task(description="Generating Plan B (Robust)...", total=None)
|
|
171
|
+
plan_b = await plan_service.generate_plan("planner_b", goal, json.dumps([r.model_dump() for r in spec_output.requirements]), repo_map_json)
|
|
172
|
+
orchestrator.save_run_artifact("plan_b.json", plan_b.model_dump())
|
|
173
|
+
await orchestrator.transition_to(ProjectPhase.PLANS_GENERATED)
|
|
174
|
+
|
|
175
|
+
# 4. Cross-Critique
|
|
176
|
+
progress.add_task(description="Critiquing Plan B...", total=None)
|
|
177
|
+
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]))
|
|
178
|
+
orchestrator.save_run_artifact("critique_a.json", critique_a.model_dump())
|
|
179
|
+
|
|
180
|
+
progress.add_task(description="Critiquing Plan A...", total=None)
|
|
181
|
+
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]))
|
|
182
|
+
orchestrator.save_run_artifact("critique_b.json", critique_b.model_dump())
|
|
183
|
+
await orchestrator.transition_to(ProjectPhase.CRITIQUES_GENERATED)
|
|
184
|
+
|
|
185
|
+
# 5. Rebuttals
|
|
186
|
+
progress.add_task(description="Generating rebuttals...", total=None)
|
|
187
|
+
rebuttal_a = await critique_service.generate_rebuttal("planner_a", plan_a.model_dump_json(), critique_b.model_dump_json())
|
|
188
|
+
orchestrator.save_run_artifact("rebuttal_a.json", rebuttal_a.model_dump())
|
|
189
|
+
rebuttal_b = await critique_service.generate_rebuttal("planner_b", plan_b.model_dump_json(), critique_a.model_dump_json())
|
|
190
|
+
orchestrator.save_run_artifact("rebuttal_b.json", rebuttal_b.model_dump())
|
|
191
|
+
|
|
192
|
+
# 6. Arbitration
|
|
193
|
+
progress.add_task(description="Arbitrating final plan...", total=None)
|
|
194
|
+
decision = await arbiter_service.arbitrate(
|
|
195
|
+
goal,
|
|
196
|
+
json.dumps([r.model_dump() for r in spec_output.requirements]),
|
|
197
|
+
plan_a.model_dump_json(),
|
|
198
|
+
plan_b.model_dump_json(),
|
|
199
|
+
critique_a.model_dump_json(),
|
|
200
|
+
critique_b.model_dump_json(),
|
|
201
|
+
rebuttal_a.model_dump_json(),
|
|
202
|
+
rebuttal_b.model_dump_json()
|
|
203
|
+
)
|
|
204
|
+
orchestrator.save_run_artifact("decision.json", decision.model_dump())
|
|
205
|
+
await orchestrator.transition_to(ProjectPhase.ARBITRATED)
|
|
206
|
+
reconciled_findings = _reconcile_findings([*critique_a.findings, *critique_b.findings], decision)
|
|
207
|
+
|
|
208
|
+
# 7. Save to DB unless this is a non-persistent dry run.
|
|
209
|
+
if persist:
|
|
210
|
+
with db.get_session() as session:
|
|
211
|
+
req_repo = RequirementRepository(session)
|
|
212
|
+
assumption_repo = AssumptionRepository(session)
|
|
213
|
+
task_repo = TaskRepository(session)
|
|
214
|
+
finding_repo = CritiqueFindingRepository(session)
|
|
215
|
+
|
|
216
|
+
for req in decision.final_requirements:
|
|
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
|
+
|
|
236
|
+
# 8. Check Gates
|
|
237
|
+
policy = GatePolicy()
|
|
238
|
+
result = policy.check_plan_approval(
|
|
239
|
+
decision.final_requirements,
|
|
240
|
+
decision.final_tasks,
|
|
241
|
+
assumptions=spec_output.assumptions,
|
|
242
|
+
findings=reconciled_findings,
|
|
243
|
+
blocking_questions=spec_output.blocking_questions,
|
|
244
|
+
)
|
|
245
|
+
if persist:
|
|
246
|
+
with db.get_session() as session:
|
|
247
|
+
GapRepository(session).delete_plan_gaps()
|
|
248
|
+
|
|
249
|
+
if result.passed:
|
|
250
|
+
console.print("[green]Plan approved by gates.[/green]")
|
|
251
|
+
await orchestrator.transition_to(ProjectPhase.PLAN_APPROVED)
|
|
252
|
+
else:
|
|
253
|
+
if persist:
|
|
254
|
+
with db.get_session() as session:
|
|
255
|
+
gap_repo = GapRepository(session)
|
|
256
|
+
for gap in result.gaps:
|
|
257
|
+
gap_repo.save(gap)
|
|
258
|
+
console.print("[yellow]Plan generated but failed gates. See status for gaps.[/yellow]")
|
|
259
|
+
await orchestrator.transition_to(ProjectPhase.AWAITING_USER_DECISIONS)
|
|
260
|
+
|
|
261
|
+
@app.command()
|
|
262
|
+
def plan(
|
|
263
|
+
goal: str = typer.Argument(..., help="The goal of the implementation"),
|
|
264
|
+
requirements_only: bool = typer.Option(False, "--requirements-only", help="Only generate requirements"),
|
|
265
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Simulate planning without LLM calls"),
|
|
266
|
+
persist: bool = typer.Option(
|
|
267
|
+
False,
|
|
268
|
+
"--persist/--no-persist",
|
|
269
|
+
help="Persist dry-run planning artifacts into the main state database.",
|
|
270
|
+
),
|
|
271
|
+
):
|
|
272
|
+
"""
|
|
273
|
+
Run the full planning cycle (Repo map -> Spec -> Plan A/B -> Critique -> Arbiter).
|
|
274
|
+
"""
|
|
275
|
+
should_persist = persist or not dry_run
|
|
276
|
+
asyncio.run(run_plan_flow(goal, requirements_only, dry_run, should_persist))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.markdown import Markdown
|
|
4
|
+
from devcouncil.storage.db import get_db
|
|
5
|
+
from devcouncil.storage.repositories import TaskRepository, RequirementRepository
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from devcouncil.execution.prompt_builder import PromptBuilder
|
|
9
|
+
|
|
10
|
+
app = typer.Typer()
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
@app.callback(invoke_without_command=True)
|
|
14
|
+
def prompt(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
task_id: str = typer.Argument(..., help="ID of the task to generate a prompt for"),
|
|
17
|
+
pretty: bool = typer.Option(False, "--pretty", help="Render the prompt for terminal reading."),
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
Generate a constrained prompt for a specific task.
|
|
21
|
+
"""
|
|
22
|
+
if ctx.invoked_subcommand is not None:
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
db = get_db()
|
|
26
|
+
if not db:
|
|
27
|
+
console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
|
|
28
|
+
raise typer.Exit(code=1)
|
|
29
|
+
|
|
30
|
+
with db.get_session() as session:
|
|
31
|
+
task_repo = TaskRepository(session)
|
|
32
|
+
req_repo = RequirementRepository(session)
|
|
33
|
+
|
|
34
|
+
task = task_repo.get_by_id(task_id)
|
|
35
|
+
if not task:
|
|
36
|
+
console.print(f"[red]Task {task_id} not found.[/red]")
|
|
37
|
+
raise typer.Exit(code=1)
|
|
38
|
+
|
|
39
|
+
reqs = req_repo.get_all()
|
|
40
|
+
|
|
41
|
+
builder = PromptBuilder(Path("."))
|
|
42
|
+
task_prompt = builder.build_task_prompt(task, reqs)
|
|
43
|
+
|
|
44
|
+
if pretty:
|
|
45
|
+
console.print(Markdown(task_prompt))
|
|
46
|
+
else:
|
|
47
|
+
typer.echo(task_prompt, nl=not task_prompt.endswith("\n"))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from devcouncil.storage.db import get_db
|
|
4
|
+
from devcouncil.storage.repositories import TaskRepository, GapRepository
|
|
5
|
+
from devcouncil.planning.repair_service import RepairService
|
|
6
|
+
from devcouncil.execution.context_builder import ContextBuilder
|
|
7
|
+
from devcouncil.llm.provider import OpenRouterProvider
|
|
8
|
+
from devcouncil.llm.router import ModelRouter
|
|
9
|
+
from devcouncil.app.config import load_config, get_api_key
|
|
10
|
+
import asyncio
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
async def run_repair_flow():
|
|
17
|
+
db = get_db()
|
|
18
|
+
if not db:
|
|
19
|
+
console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
with db.get_session() as session:
|
|
23
|
+
gap_repo = GapRepository(session)
|
|
24
|
+
task_repo = TaskRepository(session)
|
|
25
|
+
|
|
26
|
+
all_gaps = gap_repo.get_all()
|
|
27
|
+
blocking_gaps = [g for g in all_gaps if g.blocking]
|
|
28
|
+
|
|
29
|
+
if not blocking_gaps:
|
|
30
|
+
console.print("[green]No blocking gaps found. Nothing to repair![/green]")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
console.print(f"Found [bold]{len(blocking_gaps)}[/bold] blocking gaps. Orchestrating repair plan...")
|
|
34
|
+
|
|
35
|
+
# Load router
|
|
36
|
+
try:
|
|
37
|
+
config = load_config(Path("."))
|
|
38
|
+
api_key = get_api_key(config.models.provider)
|
|
39
|
+
except (FileNotFoundError, ValueError) as e:
|
|
40
|
+
console.print(f"[red]{e}[/red]")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
provider = OpenRouterProvider(api_key)
|
|
44
|
+
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
45
|
+
router = ModelRouter(provider, role_config)
|
|
46
|
+
repair_service = RepairService(router)
|
|
47
|
+
context_builder = ContextBuilder(Path("."))
|
|
48
|
+
|
|
49
|
+
# Build minimal context for repair.
|
|
50
|
+
project_context = context_builder.get_structure_summary()
|
|
51
|
+
|
|
52
|
+
repair_output = await repair_service.generate_repair_plan(blocking_gaps, str(project_context))
|
|
53
|
+
|
|
54
|
+
for task in repair_output.suggested_tasks:
|
|
55
|
+
task.id = f"REPAIR-{task.id}"
|
|
56
|
+
task_repo.save(task)
|
|
57
|
+
console.print(f" - Created intelligent repair task [bold]{task.id}[/bold]: {task.title}")
|
|
58
|
+
|
|
59
|
+
console.print(f"\n[green]Successfully generated {len(repair_output.suggested_tasks)} repair tasks.[/green]")
|
|
60
|
+
|
|
61
|
+
@app.callback(invoke_without_command=True)
|
|
62
|
+
def repair(ctx: typer.Context):
|
|
63
|
+
"""
|
|
64
|
+
Convert blocking gaps into intelligent repair tasks using LLM inference.
|
|
65
|
+
"""
|
|
66
|
+
if ctx.invoked_subcommand is not None:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
asyncio.run(run_repair_flow())
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.markdown import Markdown
|
|
4
|
+
from devcouncil.storage.db import get_db
|
|
5
|
+
from devcouncil.storage.repositories import ArtifactGraphRepository
|
|
6
|
+
from devcouncil.reporting.report_builder import ReportBuilder
|
|
7
|
+
from devcouncil.integrations.github import GitHubIntegration
|
|
8
|
+
from devcouncil.artifacts.graph import ArtifactGraph
|
|
9
|
+
from devcouncil.telemetry.traces import TraceLogger
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
app = typer.Typer()
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
async def run_github_report(graph: ArtifactGraph):
|
|
19
|
+
token = os.environ.get("GITHUB_TOKEN")
|
|
20
|
+
repo = os.environ.get("GITHUB_REPOSITORY") # e.g. owner/repo
|
|
21
|
+
|
|
22
|
+
if not token or not repo:
|
|
23
|
+
console.print("[red]GITHUB_TOKEN and GITHUB_REPOSITORY must be set for GitHub reporting.[/red]")
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# Detect current SHA
|
|
28
|
+
sha = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
|
|
29
|
+
integration = GitHubIntegration(token, repo, sha)
|
|
30
|
+
await integration.report_verification(graph)
|
|
31
|
+
console.print(f"[green]Successfully reported to GitHub PR Checks for {repo} at {sha[:7]}[/green]")
|
|
32
|
+
except Exception as e:
|
|
33
|
+
console.print(f"[red]Failed to report to GitHub: {e}[/red]")
|
|
34
|
+
|
|
35
|
+
@app.callback(invoke_without_command=True)
|
|
36
|
+
def report(
|
|
37
|
+
ctx: typer.Context,
|
|
38
|
+
planning_only: bool = typer.Option(False, "--planning-only", help="Report only the planning phase status"),
|
|
39
|
+
json_format: bool = typer.Option(False, "--json", help="Output report in JSON format"),
|
|
40
|
+
github: bool = typer.Option(False, "--github", help="Post report to GitHub PR Checks"),
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Produce final evidence report.
|
|
44
|
+
"""
|
|
45
|
+
if ctx.invoked_subcommand is not None:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
db = get_db()
|
|
49
|
+
if not db:
|
|
50
|
+
console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
|
|
51
|
+
raise typer.Exit(code=1)
|
|
52
|
+
|
|
53
|
+
with db.get_session() as session:
|
|
54
|
+
graph_repo = ArtifactGraphRepository(session)
|
|
55
|
+
graph = graph_repo.load_graph()
|
|
56
|
+
TraceLogger(Path(".")).log_event(
|
|
57
|
+
"report_generated",
|
|
58
|
+
{"json": json_format, "github": github, "planning_only": planning_only},
|
|
59
|
+
summary="Generated DevCouncil report",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if github:
|
|
63
|
+
asyncio.run(run_github_report(graph))
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if json_format:
|
|
67
|
+
output = ReportBuilder.build_json(graph)
|
|
68
|
+
typer.echo(output)
|
|
69
|
+
else:
|
|
70
|
+
output = ReportBuilder.build_markdown(graph)
|
|
71
|
+
console.print(Markdown(output))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from sqlmodel import delete
|
|
4
|
+
|
|
5
|
+
from devcouncil.storage.db import get_db
|
|
6
|
+
from devcouncil.storage.models import EvidenceModel, GapModel, RequirementModel, TaskModel
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def reset_demo_state(
|
|
12
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm clearing planning/demo artifacts."),
|
|
13
|
+
):
|
|
14
|
+
"""Clear demo planning artifacts from the local DevCouncil state database."""
|
|
15
|
+
if not yes:
|
|
16
|
+
console.print("[red]Refusing to clear state without --yes.[/red]")
|
|
17
|
+
raise typer.Exit(code=1)
|
|
18
|
+
|
|
19
|
+
db = get_db()
|
|
20
|
+
if not db:
|
|
21
|
+
console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
|
|
22
|
+
raise typer.Exit(code=1)
|
|
23
|
+
|
|
24
|
+
with db.get_session() as session:
|
|
25
|
+
for model in (EvidenceModel, GapModel, TaskModel, RequirementModel):
|
|
26
|
+
session.exec(delete(model))
|
|
27
|
+
|
|
28
|
+
console.print("[green]Cleared requirements, tasks, gaps, and evidence from local state.[/green]")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
app = typer.Typer()
|
|
7
|
+
console = Console()
|
|
8
|
+
|
|
9
|
+
@app.callback(invoke_without_command=True)
|
|
10
|
+
def rollback(
|
|
11
|
+
ctx: typer.Context,
|
|
12
|
+
task_id: str = typer.Argument(..., help="ID of the task to rollback"),
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Revert changes using a task's git checkpoint.
|
|
16
|
+
"""
|
|
17
|
+
if ctx.invoked_subcommand is not None:
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
checkpoint_file = Path(".devcouncil/checkpoints") / f"{task_id}-before.patch"
|
|
21
|
+
after_patch = Path(".devcouncil/checkpoints") / f"{task_id}-after.patch"
|
|
22
|
+
|
|
23
|
+
if not checkpoint_file.exists() and not after_patch.exists():
|
|
24
|
+
console.print(
|
|
25
|
+
f"[red]No checkpoint found for task {task_id}. Expected {after_patch} "
|
|
26
|
+
f"or {checkpoint_file}.[/red]"
|
|
27
|
+
)
|
|
28
|
+
raise typer.Exit(code=1)
|
|
29
|
+
|
|
30
|
+
console.print(f"Rolling back task [bold]{task_id}[/bold]...")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
if after_patch.exists():
|
|
34
|
+
# Reverse-apply the task's changes only
|
|
35
|
+
console.print(f"Applying reverse patch from [bold]{after_patch}[/bold]...")
|
|
36
|
+
subprocess.check_call(
|
|
37
|
+
["git", "apply", "-R", str(after_patch)],
|
|
38
|
+
cwd=".",
|
|
39
|
+
)
|
|
40
|
+
console.print(f"[green]Successfully rolled back task {task_id} changes.[/green]")
|
|
41
|
+
else:
|
|
42
|
+
# No after-patch, but we have the before-patch — warn and offer manual reset
|
|
43
|
+
console.print(
|
|
44
|
+
f"[yellow]No after-patch found at {after_patch}.[/yellow]\n"
|
|
45
|
+
f"The before-patch at {checkpoint_file} captured the state before the task ran.\n"
|
|
46
|
+
f"To manually reset:\n"
|
|
47
|
+
f" 1. [bold]git stash[/bold] (if you want to keep current changes)\n"
|
|
48
|
+
f" 2. [bold]git checkout -- .[/bold] (discard working tree changes)\n"
|
|
49
|
+
f" 3. [bold]git apply {checkpoint_file}[/bold] (restore pre-task state)"
|
|
50
|
+
)
|
|
51
|
+
except subprocess.CalledProcessError as e:
|
|
52
|
+
console.print(f"[red]Failed to apply reverse patch: {e}[/red]")
|
|
53
|
+
console.print("[yellow]The patch may conflict with current changes. Try resolving manually:[/yellow]")
|
|
54
|
+
console.print(f" git apply -R --3way {after_patch}")
|
|
55
|
+
raise typer.Exit(code=1)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
console.print(f"[red]Failed to rollback: {e}[/red]")
|
|
58
|
+
raise typer.Exit(code=1)
|