devcouncil 0.2.0 → 0.2.2

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 +144 -308
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +22 -19
  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,656 +0,0 @@
1
- import asyncio
2
- import hashlib
3
- import subprocess
4
- from pathlib import Path
5
- from types import SimpleNamespace
6
-
7
- import typer
8
- from rich.console import Console
9
-
10
- from devcouncil.app.config import load_config
11
- from devcouncil.cli.commands import plan as plan_command
12
- from devcouncil.cli.commands import report as report_command
13
- from devcouncil.cli.commands import run as run_command
14
- from devcouncil.cli.commands import verify as verify_command
15
- from devcouncil.cli.commands.init import initialize_project
16
- from devcouncil.executors.agent_registry import (
17
- AGENT_ALIASES,
18
- BUILTIN_CODING_EXECUTOR_NAMES,
19
- load_cli_agent_specs,
20
- resolve_automated_executor,
21
- )
22
- from devcouncil.integrations.github_intent import resolve_goal_intent
23
- from devcouncil.llm.provider import ProviderRequestError
24
- from devcouncil.llm.router import StructuredOutputError
25
- from devcouncil.storage.db import get_db
26
- from devcouncil.storage.repositories import ArtifactGraphRepository, GapRepository, StateRepository, TaskRepository
27
- from devcouncil.app.state_machine import ProjectPhase
28
- from devcouncil.gating.policy import topological_order
29
- from devcouncil.live.summary import live_review_summary
30
- from devcouncil.reporting.report_builder import ReportBuilder
31
-
32
-
33
- console = Console()
34
-
35
- SUPPORTED_EXECUTORS = {
36
- *BUILTIN_CODING_EXECUTOR_NAMES,
37
- "native",
38
- "native-preview",
39
- "mini",
40
- "openhands",
41
- }
42
- SUPPORTED_EXECUTORS.update(AGENT_ALIASES)
43
-
44
- AGENT_REPORT_FILE = Path(".devcouncil/reports/latest.json")
45
-
46
-
47
- def _normalize_executor(executor: str) -> str:
48
- return executor.strip().lower().replace("_", "-")
49
-
50
-
51
- def _is_git_repo(root: Path) -> bool:
52
- """True when ``root`` is inside a git work tree.
53
-
54
- The reconciliation pass re-verifies tasks against the *committed integrated*
55
- state, which only exists when ``_commit_task_changes`` could actually commit —
56
- i.e. inside a git repo. Without git there is nothing to reconcile (each task was
57
- already verified in isolation during execution), and re-scanning a dirty,
58
- non-git tree would spuriously flag unrelated files as orphan diffs.
59
- """
60
- try:
61
- result = subprocess.run(
62
- ["git", "rev-parse", "--is-inside-work-tree"],
63
- cwd=root, capture_output=True, text=True,
64
- )
65
- except Exception:
66
- return False
67
- return result.returncode == 0 and result.stdout.strip() == "true"
68
-
69
-
70
- def _custom_cli_agents(root: Path) -> set[str]:
71
- specs = load_cli_agent_specs(root)
72
- return {name for name, spec in specs.items() if not spec.built_in}
73
-
74
-
75
- def _load_tasks(root: Path):
76
- db = get_db(root)
77
- if not db:
78
- return []
79
- with db.get_session() as session:
80
- return TaskRepository(session).get_all()
81
-
82
-
83
- def _task_status(root: Path, task_id: str) -> str:
84
- latest = {item.id: item for item in _load_tasks(root)}.get(task_id)
85
- return latest.status if latest else "missing"
86
-
87
-
88
- def _max_repair_attempts(root: Path) -> int:
89
- """How many self-repair attempts `dev go` may make per task (0 disables the loop)."""
90
- try:
91
- return max(0, int(load_config(root).execution.max_repair_attempts))
92
- except Exception:
93
- return 0
94
-
95
-
96
- def _blocking_gap_signature(root: Path, task_id: str) -> str:
97
- """Fingerprint of a task's current blocking gaps, for no-progress detection.
98
-
99
- If a repair attempt reproduces the exact same blocking gaps as the previous one,
100
- the agent is stuck — we abort rather than burn the rest of the budget repeating a
101
- fix that does not move the gate.
102
- """
103
- db = get_db(root)
104
- if not db:
105
- return ""
106
- with db.get_session() as session:
107
- gaps = [g for g in GapRepository(session).get_all() if g.task_id == task_id and g.blocking]
108
- key = "\n".join(sorted(f"{g.gap_type}:{g.description}" for g in gaps))
109
- return hashlib.sha1(key.encode("utf-8")).hexdigest() if key else ""
110
-
111
-
112
- def _build_repair_service(root: Path):
113
- """Best-effort LLM repair service used to sharpen the correction manifest's root
114
- cause. Returns None when no provider key is configured — the manifest still has a
115
- deterministic, task-scoped fallback (allowed files, commands, forbidden changes)."""
116
- try:
117
- from devcouncil.app.config import get_api_key
118
- from devcouncil.llm.provider import create_provider, validate_model_provider
119
- from devcouncil.llm.router import ModelRouter
120
- from devcouncil.planning.repair_service import RepairService
121
-
122
- config = load_config(root)
123
- validate_model_provider(config.models.provider)
124
- api_key = get_api_key(config.models.provider, root)
125
- provider = create_provider(config.models.provider, api_key, project_root=root)
126
- role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
127
- return RepairService(ModelRouter(provider, role_config, project_root=root))
128
- except Exception:
129
- return None
130
-
131
-
132
- def _execute_task_with_repair(
133
- root: Path,
134
- task,
135
- *,
136
- executor: str,
137
- profile: str | None,
138
- stream: bool,
139
- max_repairs: int,
140
- repair_service,
141
- ) -> tuple[str, int]:
142
- """Run a task, then self-repair in a bounded loop until it verifies or the budget
143
- is exhausted. Returns ``(final_status, repair_attempts_used)``.
144
-
145
- Each repair attempt writes a correction manifest (which the coding-CLI executor
146
- folds into its prompt) and re-runs the executor. Between attempts the prior work
147
- is committed so the readiness gate's clean-tree requirement holds and the next
148
- attempt builds on it; verification still recognises the committed work via the
149
- task's checkpoint. The loop stops early when an attempt makes no progress (the
150
- same blocking gaps reappear) so it never spins on an unfixable gate.
151
-
152
- When the task ultimately verifies, the intermediate ``[blocked]`` commits made
153
- between attempts are squashed into a single verified commit (see
154
- :func:`_squash_repair_commits`) so failed attempts don't pollute git history. The
155
- squash preserves the task's checkpoint refs, so the verifier's empty-diff guard
156
- and ``dev rollback`` keep working.
157
- """
158
- from devcouncil.planning.correction_manifest import write_correction_manifest
159
-
160
- def _run_once() -> None:
161
- # An executor that raises (e.g. an experimental native agent hitting a
162
- # StructuredOutputError, or a CLI crash) must not abort the whole `dev go`
163
- # run — record it and let the loop/report treat the task as blocked.
164
- try:
165
- run_command.run(task.id, executor=executor, profile=profile, stream=stream, project_root=root)
166
- except Exception as exc: # noqa: BLE001 - executor faults are non-fatal to the run
167
- console.print(f"[red]{task.id}: executor '{executor}' errored: {exc}[/red]")
168
-
169
- # HEAD before this task makes any commit, captured lazily right before the first
170
- # intermediate commit. On a successful repair we `git reset --soft` back to here
171
- # so only one verified commit remains (squashing the [blocked] attempts).
172
- squash_base: str | None = None
173
- intermediate_commits = 0
174
-
175
- _run_once()
176
- status = _task_status(root, task.id)
177
-
178
- attempt = 0
179
- last_signature: str | None = None
180
- while status not in {"verified", "done"} and attempt < max_repairs:
181
- signature = _blocking_gap_signature(root, task.id)
182
- if not signature:
183
- # Blocked without recorded blocking gaps (e.g. the executor failed to
184
- # start) — there is nothing concrete to repair against, so stop.
185
- break
186
- if signature == last_signature:
187
- console.print(
188
- f"[yellow]{task.id}: repair made no progress (identical blocking gaps); "
189
- "stopping the self-repair loop.[/yellow]"
190
- )
191
- break
192
- last_signature = signature
193
-
194
- # Record where history started before the first failed-attempt commit, so the
195
- # squash collapses exactly this task's intermediate commits and nothing earlier.
196
- if squash_base is None:
197
- squash_base = _current_head(root)
198
-
199
- # Commit the prior attempt so the next run starts from a clean tree (the
200
- # readiness gate requires it); the committed work stays visible to verify.
201
- # Marked [blocked] and squashed away later when the task verifies.
202
- if _commit_task_changes(root, task.id, status):
203
- intermediate_commits += 1
204
-
205
- manifest_path = write_correction_manifest(root, task.id, repair_service=repair_service)
206
- if manifest_path is None:
207
- break
208
- attempt += 1
209
- console.print(
210
- f"\n[bold]Self-repair attempt {attempt}/{max_repairs}[/bold] for "
211
- f"[bold]{task.id}[/bold] (was {status})..."
212
- )
213
- _run_once()
214
- status = _task_status(root, task.id)
215
-
216
- if status not in {"verified", "done"} and attempt >= max_repairs and max_repairs > 0:
217
- console.print(
218
- f"[yellow]{task.id}: gave up after {attempt} repair attempt(s); still {status}.[/yellow]"
219
- )
220
-
221
- # On success, squash the [blocked] attempt commits into one verified commit so the
222
- # user's history isn't littered with failed attempts. Only do this when the task
223
- # actually verified — a still-blocked task keeps its attempt commits so the work
224
- # isn't lost and the final reconciliation pass still sees committed changes.
225
- if status in {"verified", "done"} and squash_base and intermediate_commits:
226
- if _squash_repair_commits(root, task.id, squash_base, status):
227
- console.print(
228
- f"[dim]Squashed {intermediate_commits} blocked attempt commit(s) for "
229
- f"{task.id} into one verified commit.[/dim]"
230
- )
231
-
232
- return status, attempt
233
-
234
-
235
- def _current_head(root: Path) -> str | None:
236
- """Resolve the current HEAD commit, or None when there is no commit / no git."""
237
- try:
238
- result = subprocess.run(
239
- ["git", "rev-parse", "HEAD"],
240
- cwd=root, capture_output=True, text=True,
241
- )
242
- except Exception:
243
- return None
244
- if result.returncode != 0:
245
- return None
246
- head = result.stdout.strip()
247
- return head or None
248
-
249
-
250
- def _squash_repair_commits(root: Path, task_id: str, base: str, status: str) -> bool:
251
- """Collapse this task's intermediate ``[blocked]`` commits into one verified commit.
252
-
253
- Soft-resets HEAD back to ``base`` (the commit before the first failed-attempt
254
- commit) and re-commits the combined tree once. A soft reset moves only the branch
255
- pointer: the index and working tree are untouched and every prior commit object
256
- stays alive while a ref points at it. The task's checkpoint refs
257
- (``refs/devcouncil/tasks/<id>/before|after``) are independent named refs, so they
258
- still resolve after the squash — which keeps the verifier's empty-diff guard
259
- (``git diff <before_ref>``) and ``dev rollback`` (``git diff <before> <after>``)
260
- working against the same commit objects as before.
261
-
262
- Best-effort: any failure leaves the (already-committed) intermediate history in
263
- place rather than risking the tree, and returns False. Returns True on success.
264
- """
265
- try:
266
- # Guard: base must be a real ancestor we can reset to, and there must be
267
- # commits since it to squash. If base == HEAD there is nothing to do.
268
- head = _current_head(root)
269
- if not head or head == base:
270
- return False
271
- base_ok = subprocess.run(
272
- ["git", "rev-parse", "--verify", f"{base}^{{commit}}"],
273
- cwd=root, capture_output=True, text=True,
274
- )
275
- if base_ok.returncode != 0:
276
- return False
277
- # Soft reset keeps the working tree + index exactly as-is; only the branch
278
- # pointer moves back to base, so the next commit captures the whole task.
279
- reset = subprocess.run(
280
- ["git", "reset", "--soft", base],
281
- cwd=root, capture_output=True, text=True,
282
- )
283
- if reset.returncode != 0:
284
- return False
285
- # Stage the FINAL (verified) attempt's still-uncommitted changes too, so they land
286
- # in this single squash commit. Without this they'd be committed separately by the
287
- # caller afterward, producing two commits for what the message calls "one verified
288
- # commit" (and leaving the squash to capture only the [blocked] diffs).
289
- add = subprocess.run(
290
- ["git", "add", "-A"],
291
- cwd=root, capture_output=True, text=True,
292
- )
293
- if add.returncode != 0:
294
- return False
295
- # Re-commit the squashed tree. There may be nothing staged if every attempt's
296
- # changes cancelled out (unlikely for a verified task) — tolerate that.
297
- commit = subprocess.run(
298
- [
299
- "git",
300
- "-c", "user.name=DevCouncil",
301
- "-c", "user.email=devcouncil@local",
302
- "commit", "--no-verify", "--allow-empty",
303
- "-m", f"devcouncil(e2e): {task_id} [{status}]",
304
- ],
305
- cwd=root, capture_output=True, text=True,
306
- )
307
- return commit.returncode == 0
308
- except Exception:
309
- return False
310
-
311
-
312
- def _commit_task_changes(root: Path, task_id: str, status: str) -> bool:
313
- """Commit the working-tree changes a task produced.
314
-
315
- Sequential plans build on each other and the task-readiness gate requires a
316
- clean working tree, so without this each task after the first is blocked by
317
- the previous task's uncommitted changes. Commits are clearly attributed to
318
- DevCouncil (via ``-c`` so the user's git identity/config is never mutated)
319
- and can be squashed or reset afterwards. Returns True if a commit was made.
320
- """
321
- try:
322
- status_out = subprocess.run(
323
- ["git", "status", "--porcelain"],
324
- cwd=root, capture_output=True, text=True,
325
- )
326
- if status_out.returncode != 0 or not status_out.stdout.strip():
327
- return False
328
- subprocess.run(["git", "add", "-A"], cwd=root, check=True, capture_output=True)
329
- commit = subprocess.run(
330
- [
331
- "git",
332
- "-c", "user.name=DevCouncil",
333
- "-c", "user.email=devcouncil@local",
334
- "commit", "--no-verify",
335
- "-m", f"devcouncil(e2e): {task_id} [{status}]",
336
- ],
337
- cwd=root, capture_output=True, text=True,
338
- )
339
- return commit.returncode == 0
340
- except Exception:
341
- return False
342
-
343
-
344
- def _load_tasks_by_id(root: Path, task_ids: list[str]):
345
- db = get_db(root)
346
- if not db:
347
- return [], task_ids
348
- with db.get_session() as session:
349
- repo = TaskRepository(session)
350
- tasks = []
351
- missing = []
352
- for task_id in task_ids:
353
- task = repo.get_by_id(task_id)
354
- if task is None:
355
- missing.append(task_id)
356
- else:
357
- tasks.append(task)
358
- return tasks, missing
359
-
360
-
361
- def _unique_task_ids(task_ids: list[str]) -> list[str]:
362
- seen = set()
363
- unique = []
364
- for task_id in task_ids:
365
- if task_id in seen:
366
- continue
367
- seen.add(task_id)
368
- unique.append(task_id)
369
- return unique
370
-
371
-
372
- def _record_project_done(root: Path) -> None:
373
- db = get_db(root)
374
- if not db:
375
- return
376
- with db.get_session() as session:
377
- StateRepository(session).record_phase(ProjectPhase.PROJECT_DONE.value)
378
-
379
-
380
- def _record_project_blocked(root: Path) -> None:
381
- db = get_db(root)
382
- if not db:
383
- return
384
- with db.get_session() as session:
385
- StateRepository(session).record_phase(ProjectPhase.TASK_BLOCKED.value)
386
-
387
-
388
- def _render_final_report(root: Path, json_report: bool) -> str:
389
- db = get_db(root)
390
- if not db:
391
- raise RuntimeError("DevCouncil state is unavailable in this directory.")
392
- with db.get_session() as session:
393
- graph = ArtifactGraphRepository(session).load_graph()
394
- live_review = live_review_summary(root)
395
- if json_report:
396
- return ReportBuilder.build_json(graph, live_review=live_review)
397
- return ReportBuilder.build_markdown(graph, live_review=live_review)
398
-
399
-
400
- def _write_report_file(root: Path, report_file: Path, content: str) -> Path:
401
- path = report_file.expanduser()
402
- if not path.is_absolute():
403
- path = root / path
404
- path.parent.mkdir(parents=True, exist_ok=True)
405
- path.write_text(content, encoding="utf-8")
406
- return path
407
-
408
-
409
- def _command_label(ctx: typer.Context) -> str:
410
- command = ctx.info_name or "e2e"
411
- return f"dev {command}"
412
-
413
-
414
- def go(
415
- ctx: typer.Context,
416
- goal: str = typer.Argument(..., help="Implementation goal to plan, execute, verify, and report. Also accepts a GitHub issue/PR reference (#142, owner/repo#142, or a github.com URL), whose title+body becomes the goal."),
417
- executor: str | None = typer.Option(
418
- None,
419
- "--executor",
420
- "-e",
421
- help="Automated executor to use. Defaults to execution.default_executor in .devcouncil/config.yaml.",
422
- ),
423
- dry_run: bool = typer.Option(False, "--dry-run", help="Use mock planning responses for local smoke testing."),
424
- quick: bool = typer.Option(
425
- False,
426
- "--quick",
427
- help="Rigor dial: skip the planning council (A/B debate, critique, rebuttal, arbitration) "
428
- "for a single spec + plan. Faster and cheaper; verification still gates every diff.",
429
- ),
430
- force: bool = typer.Option(
431
- False,
432
- "--force",
433
- "--yes",
434
- "-y",
435
- help="Proceed past unresolved planning gaps (critique findings, blocking questions) "
436
- "without manual approval. Verification still gates each task's actual diff.",
437
- ),
438
- continue_on_blocked: bool = typer.Option(
439
- False,
440
- "--continue-on-blocked",
441
- help="Continue later tasks even if an earlier task is blocked by verification.",
442
- ),
443
- json_report: bool = typer.Option(False, "--json-report", "--json", help="Print the final report as JSON."),
444
- report_file: Path | None = typer.Option(
445
- None,
446
- "--report-file",
447
- help="Write the final report to a file. Relative paths resolve from --project-root.",
448
- ),
449
- agent: bool = typer.Option(
450
- False,
451
- "--agent",
452
- help="Use coding-agent defaults: JSON report plus .devcouncil/reports/latest.json.",
453
- ),
454
- profile: str | None = typer.Option(None, "--profile", help="CLI-agent execution profile to pass to dev run."),
455
- stream: bool = typer.Option(
456
- False,
457
- "--stream",
458
- help="Stream coding CLI stdout/stderr live during execution (also enabled by execution.stream_cli_output).",
459
- ),
460
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
461
- ):
462
- """
463
- Run the full DevCouncil loop in one command.
464
- """
465
- root = project_root.expanduser().resolve()
466
- initialize_project(root, quiet=True)
467
-
468
- # A goal like "#142" or a GitHub issue/PR URL is a reference, not a spec —
469
- # expand it into the issue/PR title + body (the real intent) via the gh CLI.
470
- expanded_goal, intent_note = resolve_goal_intent(goal, root)
471
- if intent_note:
472
- console.print(f"[dim]{intent_note}[/dim]")
473
- goal = expanded_goal
474
-
475
- if agent:
476
- json_report = True
477
- if report_file is None:
478
- report_file = AGENT_REPORT_FILE
479
-
480
- normalized_executor = resolve_automated_executor(root, executor)
481
- command_label = _command_label(ctx)
482
- if normalized_executor == "manual":
483
- console.print(
484
- f"[red]`{command_label}` requires an automated executor. "
485
- "Set execution.default_executor in .devcouncil/config.yaml or install a coding CLI on PATH.[/red]"
486
- )
487
- raise typer.Exit(code=2)
488
- if executor is None and normalized_executor != "manual":
489
- console.print(
490
- f"[dim]Using automated executor:[/dim] [bold]{normalized_executor}[/bold] "
491
- "(from config or first coding CLI found on PATH)."
492
- )
493
- supported = SUPPORTED_EXECUTORS | _custom_cli_agents(root)
494
- if normalized_executor not in supported:
495
- console.print(
496
- f"[red]Unsupported executor for `{command_label}`: "
497
- f"{normalized_executor}. Supported: {', '.join(sorted(supported))}.[/red]"
498
- )
499
- raise typer.Exit(code=2)
500
-
501
- console.print(f"[bold]Planning goal:[/bold] {goal}")
502
- try:
503
- planned_task_ids = asyncio.run(plan_command.run_plan_flow(goal, dry_run=dry_run, persist=True, project_root=root, quick=quick))
504
- except (ProviderRequestError, StructuredOutputError) as exc:
505
- plan_command.print_planning_error(exc)
506
- raise typer.Exit(code=1)
507
-
508
- task_ids = _unique_task_ids(planned_task_ids or [])
509
- # The planning council almost always raises advisory gaps (critique findings,
510
- # clarifying questions), so run_plan_flow returns no approved tasks and the
511
- # plan is left in AWAITING_USER_DECISIONS. For an automated one-command flow
512
- # that means there is nothing to run. With --force, approve the generated plan
513
- # anyway and proceed — verification still gates each task's actual diff.
514
- if not task_ids:
515
- if force:
516
- try:
517
- plan_command.approve(run_id=None, force=True, project_root=root)
518
- except SystemExit:
519
- pass
520
- tasks = _load_tasks(root)
521
- if tasks:
522
- console.print(
523
- "[yellow]Proceeding past planning gaps via --force; "
524
- "verification still gates each task.[/yellow]"
525
- )
526
- else:
527
- tasks = []
528
- if not tasks:
529
- console.print("[red]Planning did not produce any approved tasks.[/red]")
530
- console.print(
531
- "Review gaps with [bold]dev status[/bold], then run [bold]dev approve[/bold] "
532
- "to accept the plan — or re-run with [bold]--force[/bold] to proceed past "
533
- "advisory planning gaps automatically."
534
- )
535
- raise typer.Exit(code=1)
536
- else:
537
- tasks, missing_task_ids = _load_tasks_by_id(root, task_ids)
538
- if missing_task_ids:
539
- console.print(f"[red]Planning returned task IDs that were not persisted: {', '.join(missing_task_ids)}[/red]")
540
- raise typer.Exit(code=1)
541
- if not tasks:
542
- console.print("[red]Planning did not produce any approved tasks.[/red]")
543
- raise typer.Exit(code=1)
544
-
545
- failed: list[str] = []
546
- executed_task_ids: list[str] = []
547
- # Automated executors can self-repair; manual sidecar mode cannot (a human drives
548
- # the edits), so the repair loop only applies to automated runs.
549
- max_repairs = _max_repair_attempts(root) if normalized_executor != "manual" else 0
550
- repair_service = _build_repair_service(root) if max_repairs else None
551
- # Run tasks in dependency order so a task never executes before the tasks it needs.
552
- tasks = topological_order(tasks)
553
- completed_ids = {task.id for task in tasks if task.status in {"verified", "done"}}
554
- for task in tasks:
555
- if task.status in {"verified", "done"}:
556
- console.print(f"[green]Skipping {task.id}; already {task.status}.[/green]")
557
- completed_ids.add(task.id)
558
- continue
559
-
560
- # Don't run a task whose prerequisites didn't complete — it would fail for an
561
- # unrelated reason and (with the repair loop) burn its whole budget against an
562
- # unsatisfiable precondition. Skip it and surface why.
563
- unmet = [dep for dep in task.depends_on if dep not in completed_ids]
564
- if unmet:
565
- console.print(f"[yellow]Skipping {task.id}: upstream {', '.join(unmet)} not completed.[/yellow]")
566
- failed.append(f"{task.id} (skipped: upstream {', '.join(unmet)} unsatisfied)")
567
- continue
568
-
569
- console.print(f"\n[bold]Executing {task.id}[/bold] with [bold]{normalized_executor}[/bold]...")
570
- executed_task_ids.append(task.id)
571
- # Run, then self-repair in a bounded loop (closes the autonomous loop: the
572
- # one-shot executor no longer needs a human to run `dev repair` and re-run).
573
- latest_status, repairs_used = _execute_task_with_repair(
574
- root,
575
- task,
576
- executor=normalized_executor,
577
- profile=profile,
578
- stream=stream,
579
- max_repairs=max_repairs,
580
- repair_service=repair_service,
581
- )
582
-
583
- # Commit whatever this task produced so the next task in the plan starts
584
- # from a clean tree — otherwise its readiness gate blocks on the dirty
585
- # tree and the whole multi-task plan stalls after task one.
586
- if _commit_task_changes(root, task.id, latest_status):
587
- note = f" after {repairs_used} repair attempt(s)" if repairs_used else ""
588
- console.print(f"[dim]Committed {task.id} changes ({latest_status}){note}.[/dim]")
589
-
590
- if latest_status in {"verified", "done"}:
591
- completed_ids.add(task.id)
592
- else:
593
- failed.append(f"{task.id} ({latest_status})")
594
- # --continue-on-blocked is a "run the whole plan, best effort" switch:
595
- # don't let one task that blocked or could not start halt the rest. The
596
- # final reconciliation pass judges the integrated result fairly.
597
- if not continue_on_blocked:
598
- console.print(f"[red]Stopping because {task.id} ended as {latest_status}.[/red]")
599
- break
600
- console.print(f"[yellow]{task.id} ended as {latest_status}; continuing to the next task.[/yellow]")
601
-
602
- if not executed_task_ids:
603
- failed.append("all planned tasks were already completed before execution")
604
-
605
- # Final reconciliation: re-verify every task against the fully integrated,
606
- # committed state. Earlier tasks are verified before later tasks create shared
607
- # test files, so their gates can pass now even though they blocked mid-run.
608
- # The LLM review is diff-gated and the tree is clean here, so this costs no
609
- # model calls — it just refreshes statuses/gaps so the final report is honest.
610
- if executed_task_ids and _is_git_repo(root):
611
- console.print("\n[bold]Reconciling verification against the final integrated state...[/bold]")
612
- try:
613
- verify_command.verify(task_id=None, sandbox="local", json_format=True, project_root=root)
614
- except typer.Exit:
615
- # Expected signal: verify() raises Exit(code=1) when any task is blocked,
616
- # but it has already persisted every task status before raising. The
617
- # reconciliation pass therefore completed — fall through to the reload so
618
- # blocked statuses are refreshed honestly. (Only real errors should skip.)
619
- pass
620
- except Exception as exc: # pragma: no cover - reconciliation is best-effort
621
- console.print(f"[yellow]Reconciliation pass skipped: {exc}[/yellow]")
622
- reconciled = {item.id: item for item in _load_tasks(root)}
623
- # Rebuild from the FULL planned set, not just executed_task_ids: a task skipped
624
- # for an unmet dependency, or one reconciliation downgraded from done->blocked,
625
- # must still count as unfinished — otherwise `dev go` reports success while work
626
- # is incomplete.
627
- failed = []
628
- for planned in tasks:
629
- item = reconciled.get(planned.id)
630
- if not (item and item.status in {"verified", "done"}):
631
- status = item.status if item else "missing"
632
- failed.append(f"{planned.id} ({status})")
633
-
634
- if not failed:
635
- _record_project_done(root)
636
- else:
637
- _record_project_blocked(root)
638
-
639
- console.print("\n[bold]Final DevCouncil report[/bold]")
640
- report_command.report(
641
- SimpleNamespace(invoked_subcommand=None), # type: ignore[arg-type]
642
- planning_only=False,
643
- json_format=json_report,
644
- github=False,
645
- github_pr_comment=False,
646
- gitlab_pr_comment=False,
647
- project_root=root,
648
- )
649
- if report_file is not None:
650
- output = _render_final_report(root, json_report=json_report)
651
- written = _write_report_file(root, report_file, output)
652
- console.print(f"[green]Final report written to {written}[/green]")
653
-
654
- if failed:
655
- console.print(f"\n[red]Unfinished task(s): {', '.join(failed)}[/red]")
656
- raise typer.Exit(code=1)