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.
Files changed (125) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +643 -0
  3. package/bin/devcouncil.js +62 -0
  4. package/package.json +47 -0
  5. package/pyproject.toml +31 -0
  6. package/src/devcouncil/__init__.py +0 -0
  7. package/src/devcouncil/__main__.py +4 -0
  8. package/src/devcouncil/app/__init__.py +28 -0
  9. package/src/devcouncil/app/config.py +131 -0
  10. package/src/devcouncil/app/errors.py +23 -0
  11. package/src/devcouncil/app/events.py +44 -0
  12. package/src/devcouncil/app/orchestrator.py +92 -0
  13. package/src/devcouncil/app/run_context.py +39 -0
  14. package/src/devcouncil/app/state_machine.py +108 -0
  15. package/src/devcouncil/artifacts/__init__.py +1 -0
  16. package/src/devcouncil/artifacts/coverage.py +96 -0
  17. package/src/devcouncil/artifacts/graph.py +143 -0
  18. package/src/devcouncil/artifacts/migrations.py +20 -0
  19. package/src/devcouncil/artifacts/schemas.py +23 -0
  20. package/src/devcouncil/artifacts/serializer.py +21 -0
  21. package/src/devcouncil/artifacts/validators.py +27 -0
  22. package/src/devcouncil/cli/__init__.py +0 -0
  23. package/src/devcouncil/cli/commands/__init__.py +0 -0
  24. package/src/devcouncil/cli/commands/artifacts.py +48 -0
  25. package/src/devcouncil/cli/commands/baseline.py +32 -0
  26. package/src/devcouncil/cli/commands/config.py +54 -0
  27. package/src/devcouncil/cli/commands/doctor.py +96 -0
  28. package/src/devcouncil/cli/commands/hook.py +61 -0
  29. package/src/devcouncil/cli/commands/init.py +142 -0
  30. package/src/devcouncil/cli/commands/integrate.py +420 -0
  31. package/src/devcouncil/cli/commands/map.py +38 -0
  32. package/src/devcouncil/cli/commands/mcp_server.py +18 -0
  33. package/src/devcouncil/cli/commands/plan.py +276 -0
  34. package/src/devcouncil/cli/commands/prompt.py +47 -0
  35. package/src/devcouncil/cli/commands/repair.py +69 -0
  36. package/src/devcouncil/cli/commands/report.py +71 -0
  37. package/src/devcouncil/cli/commands/reset_demo_state.py +28 -0
  38. package/src/devcouncil/cli/commands/rollback.py +58 -0
  39. package/src/devcouncil/cli/commands/run.py +224 -0
  40. package/src/devcouncil/cli/commands/setup.py +82 -0
  41. package/src/devcouncil/cli/commands/show.py +57 -0
  42. package/src/devcouncil/cli/commands/status.py +105 -0
  43. package/src/devcouncil/cli/commands/tasks.py +41 -0
  44. package/src/devcouncil/cli/commands/trace.py +43 -0
  45. package/src/devcouncil/cli/commands/verify.py +163 -0
  46. package/src/devcouncil/cli/commands/version.py +20 -0
  47. package/src/devcouncil/cli/main.py +70 -0
  48. package/src/devcouncil/council/__init__.py +0 -0
  49. package/src/devcouncil/council/prompts/__init__.py +0 -0
  50. package/src/devcouncil/council/prompts/arbiter.md +19 -0
  51. package/src/devcouncil/council/prompts/critic_a.md +10 -0
  52. package/src/devcouncil/council/prompts/critic_b.md +10 -0
  53. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -0
  54. package/src/devcouncil/council/prompts/planner_a.md +16 -0
  55. package/src/devcouncil/council/prompts/planner_b.md +16 -0
  56. package/src/devcouncil/council/prompts/rebuttal.md +10 -0
  57. package/src/devcouncil/council/prompts/spec_writer.md +12 -0
  58. package/src/devcouncil/domain/__init__.py +0 -0
  59. package/src/devcouncil/domain/assumption.py +17 -0
  60. package/src/devcouncil/domain/critique.py +32 -0
  61. package/src/devcouncil/domain/evidence.py +27 -0
  62. package/src/devcouncil/domain/gap.py +26 -0
  63. package/src/devcouncil/domain/requirement.py +22 -0
  64. package/src/devcouncil/domain/task.py +26 -0
  65. package/src/devcouncil/execution/__init__.py +1 -0
  66. package/src/devcouncil/execution/context_builder.py +60 -0
  67. package/src/devcouncil/execution/executor.py +15 -0
  68. package/src/devcouncil/execution/hook_policy.py +144 -0
  69. package/src/devcouncil/execution/patch.py +28 -0
  70. package/src/devcouncil/execution/paths.py +14 -0
  71. package/src/devcouncil/execution/permissions.py +92 -0
  72. package/src/devcouncil/execution/prompt_builder.py +59 -0
  73. package/src/devcouncil/execution/task_runner.py +166 -0
  74. package/src/devcouncil/executors/__init__.py +1 -0
  75. package/src/devcouncil/executors/mini_swe.py +73 -0
  76. package/src/devcouncil/executors/native/__init__.py +0 -0
  77. package/src/devcouncil/executors/native/agent.py +107 -0
  78. package/src/devcouncil/executors/openhands.py +71 -0
  79. package/src/devcouncil/gating/__init__.py +1 -0
  80. package/src/devcouncil/gating/checks/__init__.py +0 -0
  81. package/src/devcouncil/gating/checks/clean_git.py +45 -0
  82. package/src/devcouncil/gating/checks/planned_files_check.py +32 -0
  83. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -0
  84. package/src/devcouncil/gating/checks/secret_scan_check.py +34 -0
  85. package/src/devcouncil/gating/policy.py +190 -0
  86. package/src/devcouncil/indexing/__init__.py +1 -0
  87. package/src/devcouncil/indexing/graph_index.py +48 -0
  88. package/src/devcouncil/indexing/repo_mapper.py +204 -0
  89. package/src/devcouncil/indexing/symbol_index.py +0 -0
  90. package/src/devcouncil/integrations/code_review_graph.py +163 -0
  91. package/src/devcouncil/integrations/github.py +39 -0
  92. package/src/devcouncil/integrations/gitnexus.py +27 -0
  93. package/src/devcouncil/integrations/graphify.py +34 -0
  94. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  95. package/src/devcouncil/integrations/mcp/server.py +146 -0
  96. package/src/devcouncil/llm/__init__.py +1 -0
  97. package/src/devcouncil/llm/cache.py +38 -0
  98. package/src/devcouncil/llm/provider.py +125 -0
  99. package/src/devcouncil/llm/router.py +125 -0
  100. package/src/devcouncil/planning/__init__.py +1 -0
  101. package/src/devcouncil/planning/arbiter_service.py +57 -0
  102. package/src/devcouncil/planning/critique_service.py +66 -0
  103. package/src/devcouncil/planning/plan_service.py +46 -0
  104. package/src/devcouncil/planning/repair_service.py +39 -0
  105. package/src/devcouncil/planning/spec_service.py +44 -0
  106. package/src/devcouncil/repo/__init__.py +0 -0
  107. package/src/devcouncil/reporting/__init__.py +0 -0
  108. package/src/devcouncil/reporting/github_check.py +32 -0
  109. package/src/devcouncil/reporting/json_report.py +17 -0
  110. package/src/devcouncil/reporting/markdown_report.py +46 -0
  111. package/src/devcouncil/reporting/report_builder.py +14 -0
  112. package/src/devcouncil/storage/__init__.py +0 -0
  113. package/src/devcouncil/storage/db.py +66 -0
  114. package/src/devcouncil/storage/models.py +83 -0
  115. package/src/devcouncil/storage/repositories.py +346 -0
  116. package/src/devcouncil/telemetry/__init__.py +0 -0
  117. package/src/devcouncil/telemetry/cost.py +34 -0
  118. package/src/devcouncil/telemetry/traces.py +91 -0
  119. package/src/devcouncil/telemetry/tracker.py +49 -0
  120. package/src/devcouncil/utils/__init__.py +1 -0
  121. package/src/devcouncil/utils/redaction.py +141 -0
  122. package/src/devcouncil/verification/__init__.py +1 -0
  123. package/src/devcouncil/verification/implementation_reviewer.py +55 -0
  124. package/src/devcouncil/verification/verifier.py +513 -0
  125. 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)