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.
Files changed (128) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +62 -543
  3. package/package.json +1 -1
  4. package/pyproject.toml +29 -26
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +135 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +143 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/cli/commands/artifacts.py +51 -48
  22. package/src/devcouncil/cli/commands/ast.py +22 -0
  23. package/src/devcouncil/cli/commands/baseline.py +35 -32
  24. package/src/devcouncil/cli/commands/config.py +76 -54
  25. package/src/devcouncil/cli/commands/dashboard.py +26 -0
  26. package/src/devcouncil/cli/commands/doctor.py +86 -42
  27. package/src/devcouncil/cli/commands/go.py +237 -0
  28. package/src/devcouncil/cli/commands/hook.py +96 -29
  29. package/src/devcouncil/cli/commands/init.py +67 -56
  30. package/src/devcouncil/cli/commands/integrate.py +320 -14
  31. package/src/devcouncil/cli/commands/lsp.py +20 -0
  32. package/src/devcouncil/cli/commands/map.py +25 -21
  33. package/src/devcouncil/cli/commands/plan.py +257 -206
  34. package/src/devcouncil/cli/commands/prompt.py +36 -33
  35. package/src/devcouncil/cli/commands/repair.py +72 -69
  36. package/src/devcouncil/cli/commands/report.py +112 -54
  37. package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
  38. package/src/devcouncil/cli/commands/rollback.py +49 -47
  39. package/src/devcouncil/cli/commands/run.py +252 -207
  40. package/src/devcouncil/cli/commands/setup.py +159 -18
  41. package/src/devcouncil/cli/commands/show.py +76 -57
  42. package/src/devcouncil/cli/commands/status.py +117 -105
  43. package/src/devcouncil/cli/commands/tasks.py +55 -41
  44. package/src/devcouncil/cli/commands/trace.py +2 -1
  45. package/src/devcouncil/cli/commands/verify.py +158 -128
  46. package/src/devcouncil/cli/commands/version.py +20 -20
  47. package/src/devcouncil/cli/commands/watch.py +574 -0
  48. package/src/devcouncil/cli/main.py +42 -24
  49. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  50. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  51. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  52. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  53. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  54. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  55. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  56. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  57. package/src/devcouncil/domain/assumption.py +17 -17
  58. package/src/devcouncil/domain/critique.py +32 -32
  59. package/src/devcouncil/domain/evidence.py +27 -27
  60. package/src/devcouncil/domain/gap.py +26 -26
  61. package/src/devcouncil/domain/requirement.py +22 -22
  62. package/src/devcouncil/domain/task.py +26 -26
  63. package/src/devcouncil/execution/__init__.py +1 -1
  64. package/src/devcouncil/execution/context_builder.py +54 -54
  65. package/src/devcouncil/execution/executor.py +15 -15
  66. package/src/devcouncil/execution/hook_policy.py +24 -3
  67. package/src/devcouncil/execution/patch.py +28 -28
  68. package/src/devcouncil/execution/permissions.py +44 -44
  69. package/src/devcouncil/execution/prompt_builder.py +23 -23
  70. package/src/devcouncil/execution/task_runner.py +63 -63
  71. package/src/devcouncil/executors/__init__.py +1 -1
  72. package/src/devcouncil/executors/coding_cli.py +112 -0
  73. package/src/devcouncil/executors/mini_swe.py +63 -63
  74. package/src/devcouncil/executors/native/agent.py +81 -81
  75. package/src/devcouncil/executors/openhands.py +56 -56
  76. package/src/devcouncil/gating/__init__.py +1 -1
  77. package/src/devcouncil/gating/checks/clean_git.py +50 -45
  78. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  79. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  80. package/src/devcouncil/gating/checks/secret_scan_check.py +34 -34
  81. package/src/devcouncil/gating/policy.py +157 -157
  82. package/src/devcouncil/indexing/__init__.py +1 -1
  83. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  84. package/src/devcouncil/indexing/graph_index.py +48 -48
  85. package/src/devcouncil/indexing/lsp.py +120 -0
  86. package/src/devcouncil/indexing/repo_mapper.py +208 -204
  87. package/src/devcouncil/integrations/github.py +35 -35
  88. package/src/devcouncil/integrations/gitnexus.py +27 -27
  89. package/src/devcouncil/integrations/graphify.py +34 -34
  90. package/src/devcouncil/integrations/mcp/server.py +549 -96
  91. package/src/devcouncil/integrations/pr_comments.py +62 -0
  92. package/src/devcouncil/live/__init__.py +2 -0
  93. package/src/devcouncil/live/cards.py +207 -0
  94. package/src/devcouncil/live/models.py +63 -0
  95. package/src/devcouncil/live/repair_prompt.py +83 -0
  96. package/src/devcouncil/live/reviewer.py +70 -0
  97. package/src/devcouncil/live/signals.py +135 -0
  98. package/src/devcouncil/live/summary.py +34 -0
  99. package/src/devcouncil/live/tasks.py +18 -0
  100. package/src/devcouncil/live/transcripts.py +138 -0
  101. package/src/devcouncil/llm/__init__.py +1 -1
  102. package/src/devcouncil/llm/cache.py +38 -38
  103. package/src/devcouncil/llm/provider.py +146 -125
  104. package/src/devcouncil/llm/router.py +111 -111
  105. package/src/devcouncil/planning/__init__.py +1 -1
  106. package/src/devcouncil/planning/arbiter_service.py +57 -57
  107. package/src/devcouncil/planning/critique_service.py +66 -66
  108. package/src/devcouncil/planning/plan_service.py +46 -46
  109. package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
  110. package/src/devcouncil/planning/repair_service.py +39 -39
  111. package/src/devcouncil/planning/spec_service.py +44 -44
  112. package/src/devcouncil/reporting/github_check.py +32 -32
  113. package/src/devcouncil/reporting/json_report.py +20 -17
  114. package/src/devcouncil/reporting/markdown_report.py +68 -46
  115. package/src/devcouncil/reporting/report_builder.py +14 -14
  116. package/src/devcouncil/storage/db.py +66 -66
  117. package/src/devcouncil/storage/models.py +83 -83
  118. package/src/devcouncil/storage/repositories.py +299 -222
  119. package/src/devcouncil/telemetry/cost.py +34 -34
  120. package/src/devcouncil/telemetry/tracker.py +49 -49
  121. package/src/devcouncil/ui/__init__.py +1 -0
  122. package/src/devcouncil/ui/dashboard.py +122 -0
  123. package/src/devcouncil/utils/__init__.py +1 -1
  124. package/src/devcouncil/utils/redaction.py +141 -141
  125. package/src/devcouncil/verification/__init__.py +1 -1
  126. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  127. package/src/devcouncil/verification/verifier.py +319 -302
  128. 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 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()
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
- 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)
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(Path(".")).get_context()
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. 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
- )
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
- # 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
-
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
- decision.final_tasks,
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
- @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))
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.storage.db import get_db
5
- from devcouncil.storage.repositories import TaskRepository, RequirementRepository
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
- 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("."))
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: