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