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,303 +0,0 @@
1
- """Correction manifest generation for repair loops."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import uuid
7
- from datetime import datetime, timezone
8
- from pathlib import Path
9
-
10
- from pydantic import BaseModel, Field
11
-
12
- from devcouncil.app.config import load_config
13
- from devcouncil.domain.gap import Gap
14
- from devcouncil.domain.task import Task
15
- from devcouncil.storage.db import get_db
16
- from devcouncil.storage.native import CorrectionManifestRepository
17
- from devcouncil.storage.repositories import EvidenceRepository, GapRepository, TaskRepository
18
- from devcouncil.utils.redaction import redact_text
19
-
20
- # Bounds for the prior-attempt context folded into the manifest. These reach the
21
- # next executor's prompt verbatim, so they must stay small enough not to crowd out
22
- # the task spec / blow the context window while still carrying the signal the agent
23
- # needs (what it changed last time, and why verification rejected it).
24
- _MAX_PRIOR_DIFF_CHARS = 8000
25
- _MAX_FAILING_OUTPUT_CHARS = 4000
26
- # Per failed command, how much of the captured stdout/stderr tail to keep. Test
27
- # runners put the actual assertion/traceback at the end, so we keep the tail.
28
- _MAX_PER_COMMAND_OUTPUT_CHARS = 1500
29
-
30
-
31
- class CorrectionManifest(BaseModel):
32
- task_id: str
33
- root_cause: str
34
- failed_evidence: list[str] = Field(default_factory=list)
35
- allowed_repair_files: list[str] = Field(default_factory=list)
36
- forbidden_changes: list[str] = Field(default_factory=list)
37
- commands_to_rerun: list[str] = Field(default_factory=list)
38
- prior_failed_attempts: int = 0
39
- retry_budget: int = 3
40
- executor_recommendation: str = "manual"
41
- created_at: str
42
- # Blocking gaps ordered most-actionable-first (severity, then gap-type priority),
43
- # so the repair loop is steered at the real defect (a failing test) rather than an
44
- # arbitrary first gap (e.g. an orphan_diff). The first entry is the root_cause.
45
- ordered_blocking_gaps: list[str] = Field(default_factory=list)
46
- # Prior-attempt context (optional, backward-compatible). Without these the repair
47
- # executor only sees the root_cause text and re-derives the same wrong approach
48
- # blind. ``prior_diff`` is what the previous attempt actually changed; it lets the
49
- # agent see (and stop re-applying) its rejected edit. ``failing_output`` is the
50
- # captured failing test / verification output that explains *why* it was rejected.
51
- # Both are redacted and size-bounded before being written.
52
- prior_diff: str = ""
53
- failing_output: str = ""
54
-
55
-
56
- # Severity ordering: most severe first.
57
- _SEVERITY_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3}
58
-
59
- # Gap-type priority within a severity band. Lower sorts first. Executable-evidence
60
- # failures (a failing test / unproven acceptance criterion) are the real defect signal
61
- # and must outrank scope (orphan/dependency) and advisory (review/secret) gaps so the
62
- # repair loop targets the failing test, not an orphan_diff.
63
- _GAP_TYPE_PRIORITY = {
64
- "test_failed": 0,
65
- "acceptance_criteria_unproven": 1,
66
- "diff_not_exercised": 1,
67
- "task_not_implemented": 2,
68
- "migration_gap": 2,
69
- "orphan_diff": 3,
70
- "planned_file_not_changed": 3,
71
- "dependency_risk": 3,
72
- "architecture_drift": 4,
73
- "assumption_violated": 4,
74
- "security_risk": 5,
75
- }
76
-
77
-
78
- def _ordered_blocking_gaps(blocking_gaps: list[Gap]) -> list[Gap]:
79
- """Stable-sort blocking gaps by (severity, gap-type priority).
80
-
81
- ``test_failed`` / ``acceptance_*`` gaps come before orphan/dependency before
82
- review/secret, so the picked root_cause is the failing behavior rather than an
83
- incidental scope finding. Unknown severities/types sort last (defensive)."""
84
- return sorted(
85
- blocking_gaps,
86
- key=lambda g: (
87
- _SEVERITY_RANK.get(g.severity, 9),
88
- _GAP_TYPE_PRIORITY.get(g.gap_type, 9),
89
- ),
90
- )
91
-
92
-
93
- def _latest_agent_run(project_root: Path, task_id: str) -> dict | None:
94
- runs_dir = project_root / ".devcouncil" / "runs"
95
- if not runs_dir.exists():
96
- return None
97
- candidates = sorted(runs_dir.glob("*/agent-run.json"), reverse=True)
98
- for path in candidates:
99
- try:
100
- payload = json.loads(path.read_text(encoding="utf-8"))
101
- except Exception:
102
- continue
103
- if payload.get("task_id") == task_id:
104
- return payload
105
- return None
106
-
107
-
108
- def _truncate_tail(text: str, limit: int) -> str:
109
- """Keep the last ``limit`` chars of ``text`` (the actionable tail of test output),
110
- prefixing a marker when truncated. Empty/whitespace input returns ""."""
111
- text = (text or "").strip()
112
- if len(text) <= limit:
113
- return text
114
- return "[devcouncil: output truncated, showing last "f"{limit} chars]\n" + text[-limit:]
115
-
116
-
117
- def _truncate_head(text: str, limit: int) -> str:
118
- """Keep the first ``limit`` chars of ``text`` (diffs read top-down), with a marker
119
- when truncated. Empty/whitespace input returns ""."""
120
- text = (text or "").strip()
121
- if len(text) <= limit:
122
- return text
123
- return text[:limit] + "\n[devcouncil: diff truncated, "f"{len(text) - limit} chars omitted]"
124
-
125
-
126
- def _read_text_tail(path: Path, limit: int) -> str:
127
- """Best-effort read of a captured stdout/stderr file, keeping its tail. Never raises."""
128
- try:
129
- if not path.is_file():
130
- return ""
131
- return _truncate_tail(path.read_text(encoding="utf-8", errors="replace"), limit)
132
- except Exception:
133
- return ""
134
-
135
-
136
- def _collect_prior_diff(project_root: Path, task_id: str) -> str:
137
- """The prior attempt's working-tree diff, from the task's ``after`` checkpoint patch.
138
-
139
- The checkpoint service writes ``<task_id>-after.patch`` after each executor run, so
140
- this is exactly what the previous attempt changed. Redacted and head-bounded so the
141
- repair executor can see (and avoid re-applying) its rejected edit without the diff
142
- swamping the prompt. Returns "" when no patch exists (e.g. first attempt)."""
143
- patch_path = project_root / ".devcouncil" / "checkpoints" / f"{task_id}-after.patch"
144
- try:
145
- if not patch_path.is_file():
146
- return ""
147
- raw = patch_path.read_text(encoding="utf-8", errors="replace")
148
- except Exception:
149
- return ""
150
- return _truncate_head(redact_text(raw), _MAX_PRIOR_DIFF_CHARS)
151
-
152
-
153
- def _collect_failing_output(project_root: Path, failed_results) -> str:
154
- """The captured stdout/stderr of the failing verification commands.
155
-
156
- Folds each failed command's summary plus the tail of its captured stdout/stderr so
157
- the repair executor sees *why* it was rejected (the actual assertion / traceback),
158
- not just that a command exited non-zero. Redacted and size-bounded. Returns ""
159
- when there is nothing useful to show."""
160
- blocks: list[str] = []
161
- for result in failed_results:
162
- parts = [f"$ {result.command} (exit {result.exit_code})"]
163
- if result.summary and result.summary.strip():
164
- parts.append(result.summary.strip())
165
- for label, rel in (("stdout", result.stdout_path), ("stderr", result.stderr_path)):
166
- if not rel:
167
- continue
168
- path = Path(rel)
169
- if not path.is_absolute():
170
- path = project_root / rel
171
- tail = _read_text_tail(path, _MAX_PER_COMMAND_OUTPUT_CHARS)
172
- if tail:
173
- parts.append(f"--- {label} ---\n{tail}")
174
- blocks.append("\n".join(parts))
175
- if not blocks:
176
- return ""
177
- return _truncate_tail(redact_text("\n\n".join(blocks)), _MAX_FAILING_OUTPUT_CHARS)
178
-
179
-
180
- def build_correction_manifest(
181
- project_root: Path,
182
- task: Task,
183
- blocking_gaps: list[Gap],
184
- *,
185
- repair_service=None,
186
- prior_attempts: int = 0,
187
- ) -> CorrectionManifest:
188
- config = load_config(project_root)
189
- failed: list[str] = []
190
- failed_results: list = []
191
- db = get_db(project_root)
192
- if db:
193
- with db.get_session() as session:
194
- # Scope failed evidence to THIS task. Scanning every evidence row made a
195
- # repair for one task chase unrelated failures from another, so the loop
196
- # never converged on the real defect.
197
- for result in EvidenceRepository(session).get_command_results_for_task(task.id):
198
- if result.exit_code != 0:
199
- failed.append(f"{result.command} (exit {result.exit_code})")
200
- failed_results.append(result)
201
-
202
- # Steer the repair at the most actionable failure (a failing test / unproven AC),
203
- # not an arbitrary first gap such as an orphan_diff.
204
- ordered_gaps = _ordered_blocking_gaps(blocking_gaps)
205
- root_cause = ordered_gaps[0].description if ordered_gaps else "Unknown failure"
206
- manifest = CorrectionManifest(
207
- task_id=task.id,
208
- root_cause=root_cause,
209
- ordered_blocking_gaps=[g.description for g in ordered_gaps],
210
- failed_evidence=failed,
211
- allowed_repair_files=[pf.path for pf in task.planned_files],
212
- forbidden_changes=list(task.forbidden_changes),
213
- commands_to_rerun=task.expected_tests or task.allowed_commands,
214
- # The number of repair attempts already made on this task — real, not a
215
- # hardcoded 0. The agent sees how much of its budget is spent so it knows
216
- # when to change approach rather than retry the same fix.
217
- prior_failed_attempts=prior_attempts,
218
- retry_budget=config.execution.max_repair_attempts,
219
- executor_recommendation=config.execution.default_executor,
220
- created_at=datetime.now(timezone.utc).isoformat(),
221
- # Prior-attempt context so the next executor repairs against what actually
222
- # happened (its rejected diff + the failing output) instead of re-deriving
223
- # the same wrong approach blind. Both are redacted and size-bounded.
224
- prior_diff=_collect_prior_diff(project_root, task.id),
225
- failing_output=_collect_failing_output(project_root, failed_results),
226
- )
227
-
228
- if repair_service is not None:
229
- try:
230
- import asyncio
231
-
232
- plan = asyncio.run(repair_service.generate_repair_plan(blocking_gaps, task.description))
233
- if plan.suggested_tasks:
234
- suggested = plan.suggested_tasks[0]
235
- manifest.root_cause = suggested.description or manifest.root_cause
236
- # Use the repair plan's concrete scope instead of throwing it away:
237
- # union its targeted files/tests with the task's so the re-implement
238
- # step focuses on what actually needs fixing without losing task scope.
239
- manifest.allowed_repair_files = _union(
240
- manifest.allowed_repair_files, [pf.path for pf in suggested.planned_files]
241
- )
242
- manifest.commands_to_rerun = _union(manifest.commands_to_rerun, suggested.expected_tests)
243
- except Exception:
244
- pass
245
- return manifest
246
-
247
-
248
- def _union(base: list[str], extra: list[str]) -> list[str]:
249
- """Append items from ``extra`` not already in ``base`` (order-preserving dedupe)."""
250
- merged = list(base)
251
- for item in extra:
252
- if item and item not in merged:
253
- merged.append(item)
254
- return merged
255
-
256
-
257
- def write_correction_manifest(project_root: Path, task_id: str, *, repair_service=None) -> Path | None:
258
- db = get_db(project_root)
259
- if not db:
260
- return None
261
- with db.get_session() as session:
262
- task = TaskRepository(session).get_by_id(task_id)
263
- if not task:
264
- return None
265
- gaps = [g for g in GapRepository(session).get_all() if g.task_id == task_id and g.blocking]
266
- if not gaps:
267
- return None
268
- prior_record = CorrectionManifestRepository(session).latest_for_task(task_id)
269
- prior_attempts = (prior_record.attempt + 1) if prior_record else 1
270
-
271
- manifest = build_correction_manifest(
272
- project_root, task, gaps, repair_service=repair_service, prior_attempts=prior_attempts
273
- )
274
- run_id = str(uuid.uuid4())
275
- run_dir = project_root / ".devcouncil" / "runs" / run_id
276
- run_dir.mkdir(parents=True, exist_ok=True)
277
- path = run_dir / "correction-manifest.json"
278
- path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
279
-
280
- with db.get_session() as session:
281
- CorrectionManifestRepository(session).save(
282
- task_id,
283
- str(path),
284
- "open",
285
- run_id=run_id,
286
- retry_budget=manifest.retry_budget,
287
- attempt=manifest.prior_failed_attempts,
288
- )
289
- return path
290
-
291
-
292
- def load_latest_correction_manifest(project_root: Path, task_id: str) -> CorrectionManifest | None:
293
- db = get_db(project_root)
294
- if not db:
295
- return None
296
- with db.get_session() as session:
297
- record = CorrectionManifestRepository(session).latest_for_task(task_id)
298
- if not record:
299
- return None
300
- path = Path(record.manifest_path)
301
- if not path.exists():
302
- return None
303
- return CorrectionManifest.model_validate(json.loads(path.read_text(encoding="utf-8")))
@@ -1,71 +0,0 @@
1
- from typing import List
2
- from pydantic import BaseModel
3
- from devcouncil.domain.critique import CritiqueFinding
4
- from devcouncil.llm.router import ModelRouter
5
-
6
- class CritiqueOutput(BaseModel):
7
- findings: List[CritiqueFinding]
8
-
9
- class RebuttalItem(BaseModel):
10
- finding_id: str
11
- decision: str # "accepted", "rejected"
12
- reason: str
13
- suggested_change: str | None = None
14
-
15
- class RebuttalOutput(BaseModel):
16
- rebuttals: List[RebuttalItem]
17
-
18
- class CritiqueService:
19
- def __init__(self, router: ModelRouter):
20
- self.router = router
21
-
22
- async def generate_critique(self, role: str, target_plan_json: str, requirements_json: str) -> CritiqueOutput:
23
- prompt = f"""
24
- Requirements:
25
- {requirements_json}
26
-
27
- Target Plan:
28
- {target_plan_json}
29
-
30
- You are a hostile staff engineer reviewing another team's implementation plan.
31
- Find missing requirements, bad assumptions, missing tests, security risks, migration risks, and unverifiable claims.
32
- Do not praise. Do not rewrite the plan.
33
- Every finding must include a falsifiable_check.
34
- """
35
- messages = [
36
- {"role": "user", "content": prompt}
37
- ]
38
-
39
- return await self.router.complete_structured(
40
- role=role,
41
- messages=messages,
42
- schema=CritiqueOutput,
43
- # Degrade gracefully on weaker models: an un-critiqued plan is still a
44
- # usable plan, far better than crashing the whole planning run.
45
- fallback=CritiqueOutput(findings=[]),
46
- )
47
-
48
- async def generate_rebuttal(self, role: str, original_plan_json: str, findings_json: str) -> RebuttalOutput:
49
- prompt = f"""
50
- Original Plan:
51
- {original_plan_json}
52
-
53
- Critique Findings:
54
- {findings_json}
55
-
56
- You are the planner who created the original plan. Review the critique findings.
57
- - A finding can be rejected only with artifact evidence or strong justification.
58
- - A finding can be accepted and converted into a requirement/task/test.
59
- - No hand-wavy rebuttals.
60
- """
61
- messages = [
62
- {"role": "user", "content": prompt}
63
- ]
64
-
65
- return await self.router.complete_structured(
66
- role=role,
67
- messages=messages,
68
- schema=RebuttalOutput,
69
- # No rebuttals means findings stand as-is — a safe, conservative default.
70
- fallback=RebuttalOutput(rebuttals=[]),
71
- )
@@ -1,60 +0,0 @@
1
- from typing import List
2
- from pydantic import BaseModel
3
- from devcouncil.domain.task import Task
4
- from devcouncil.llm.router import ModelRouter
5
-
6
- class PlanOutput(BaseModel):
7
- id: str
8
- rationale: str
9
- tasks: List[Task]
10
-
11
- class PlanService:
12
- def __init__(self, router: ModelRouter):
13
- self.router = router
14
-
15
- async def generate_plan(self, role: str, goal: str, requirements_json: str, repo_map_json: str) -> PlanOutput:
16
- prompt = f"""
17
- Goal: {goal}
18
-
19
- Requirements:
20
- {requirements_json}
21
-
22
- Repository Map:
23
- {repo_map_json}
24
-
25
- Your task is to create a detailed implementation plan.
26
- - Break down the requirements into atomic implementation tasks, but use the FEWEST
27
- tasks that cover them — do NOT over-decompose. A small goal (e.g. add one function
28
- plus its test) is typically one or two tasks, not four.
29
- - Each file's changes must be OWNED BY A SINGLE TASK. Never create two tasks that both
30
- create/modify the same file — that causes duplicate or conflicting edits. If work on
31
- a file spans concerns, keep it in one task or split by FILE, not by sub-edit.
32
- - For each task, specify which files will be created or modified. Every implementation
33
- task must declare at least one writable (create/modify) planned file — a task that
34
- only reads files cannot implement anything.
35
- - Fill expected_tests with RUNNABLE shell commands (not prose) that exit 0 iff the
36
- task's acceptance criteria hold and can run immediately after THIS task with no
37
- missing tools or files. Prove BEHAVIOR with self-contained inline assertions, e.g.
38
- python -c "import calc; assert calc.add(2,3)==5". Use pytest only on a whole test
39
- file this or an earlier task creates (python -m pytest tests/test_x.py -q), never a
40
- ::node id. Do NOT assert repository/git state (git status, changed-file sets,
41
- append-only contents) and do NOT invoke flake8/mypy/ruff/eslint/tsc/npm unless the
42
- repo is already configured for them.
43
- - Ensure each task maps back to at least one requirement.
44
-
45
- Role-specific instructions:
46
- """
47
- if role == "planner_a":
48
- prompt += "You are the pragmatic tech lead. Optimize for simplicity and minimal dependencies."
49
- else:
50
- prompt += "You are the production-readiness architect. Optimize for security, performance, and edge cases."
51
-
52
- messages = [
53
- {"role": "user", "content": prompt}
54
- ]
55
-
56
- return await self.router.complete_structured(
57
- role=role,
58
- messages=messages,
59
- schema=PlanOutput
60
- )
@@ -1,167 +0,0 @@
1
- from pathlib import Path
2
-
3
- from pydantic import BaseModel, Field
4
-
5
- from devcouncil.llm.router import ModelRouter
6
-
7
- # Cap how much skill text we feed the enhancer so a repo matching many skills
8
- # can't blow up the planning prompt. Domain skills are ~50 lines each.
9
- _MAX_SKILLS_FOR_INTAKE = 4
10
- _MAX_INTAKE_CHARS = 8000
11
-
12
-
13
- class PromptEnhancement(BaseModel):
14
- original_goal: str
15
- enhanced_goal: str
16
- codebase_context: list[str] = Field(default_factory=list)
17
- debate_focus: list[str] = Field(default_factory=list)
18
- constraints: list[str] = Field(default_factory=list)
19
- # Senior-level domain intake folded in from the skills library (android, ios,
20
- # web, ...). ``applied_skills`` are the matched skill names; ``skills_brief`` is
21
- # the compact title+description block the council debates with. Both are set
22
- # deterministically after the model call — the LLM does not populate them.
23
- applied_skills: list[str] = Field(default_factory=list)
24
- skills_brief: str = ""
25
-
26
- def normalized(self, original_goal: str) -> "PromptEnhancement":
27
- enhanced_goal = self.enhanced_goal.strip() or original_goal
28
- return self.model_copy(
29
- update={
30
- "original_goal": original_goal,
31
- "enhanced_goal": enhanced_goal,
32
- "codebase_context": _clean_items(self.codebase_context),
33
- "debate_focus": _clean_items(self.debate_focus),
34
- "constraints": _clean_items(self.constraints),
35
- }
36
- )
37
-
38
- def debate_prompt(self) -> str:
39
- sections = [
40
- "# Enhanced Planning Prompt",
41
- "",
42
- "## Original user goal",
43
- self.original_goal,
44
- "",
45
- "## Codebase-specific goal",
46
- self.enhanced_goal,
47
- ]
48
- if self.codebase_context:
49
- sections.extend(["", "## Relevant codebase context"])
50
- sections.extend(f"- {item}" for item in self.codebase_context)
51
- if self.constraints:
52
- sections.extend(["", "## Constraints to preserve"])
53
- sections.extend(f"- {item}" for item in self.constraints)
54
- if self.debate_focus:
55
- sections.extend(["", "## Debate focus"])
56
- sections.extend(f"- {item}" for item in self.debate_focus)
57
- if self.skills_brief:
58
- sections.extend([
59
- "",
60
- "## Domain engineering intake (apply current senior-level practices)",
61
- "Plan to the *current* state of these domains — recommended libraries, "
62
- "deprecations to avoid, and the right build/test CLI commands. The coding "
63
- "agent receives the full skill text; the plan must already assume it.",
64
- self.skills_brief,
65
- ])
66
- return "\n".join(sections)
67
-
68
-
69
- class PromptEnhancerService:
70
- def __init__(self, router: ModelRouter):
71
- self.router = router
72
-
73
- async def enhance_prompt(
74
- self,
75
- goal: str,
76
- repo_map_json: str,
77
- graph_context_json: str | None = None,
78
- project_root: Path | None = None,
79
- ) -> PromptEnhancement:
80
- skills = _select_skills(goal, project_root)
81
- skills_intake = _full_intake(skills)
82
- skills_brief = _compact_brief(skills)
83
-
84
- prompt = f"""
85
- Original user goal:
86
- {goal}
87
-
88
- Repository map:
89
- {repo_map_json}
90
-
91
- Code review graph context:
92
- {graph_context_json or "{}"}
93
-
94
- Applicable engineering skills (senior-level domain intake for this codebase/goal):
95
- {skills_intake or "(no domain skills matched; rely on general engineering judgment)"}
96
-
97
- You are DevCouncil's codebase-specific prompt enhancer.
98
- Rewrite the user goal into a better planning prompt before it is sent to the council debate.
99
-
100
- Requirements:
101
- - Preserve the user's intent exactly; do not add unrelated features.
102
- - Make the goal specific to the mapped repository architecture, languages, tests, and likely ownership boundaries.
103
- - Fold the relevant skill intake into the goal and constraints like a senior engineer who
104
- just briefed themselves: name the *current* recommended libraries/APIs, the deprecated
105
- ones to avoid, the platform/SDK/toolchain versions to target, and the exact build/test
106
- CLI commands that will prove the change. Only include skill points relevant to THIS goal.
107
- - Identify constraints the planners and critics must preserve.
108
- - Identify debate focus areas that should force useful disagreement between pragmatic and production-readiness plans.
109
- - Keep the enhanced_goal concise enough to be used as the goal for spec, planning, critique, and arbitration.
110
- """
111
- enhancement = await self.router.complete_structured(
112
- role="prompt_enhancer",
113
- messages=[{"role": "user", "content": prompt}],
114
- schema=PromptEnhancement,
115
- # If enhancement fails on a weak model, fall back to the raw goal —
116
- # planning proceeds with the user's original intent unchanged.
117
- fallback=PromptEnhancement(original_goal=goal, enhanced_goal=goal),
118
- )
119
- # Skill provenance is deterministic, not model-decided: stamp it after the call
120
- # so the artifact/report shows exactly which skills shaped this plan.
121
- return enhancement.normalized(goal).model_copy(
122
- update={
123
- "applied_skills": [skill.name for skill in skills],
124
- "skills_brief": skills_brief,
125
- }
126
- )
127
-
128
-
129
- def _select_skills(goal: str, project_root: Path | None):
130
- """Codebase-aware skill selection; never raises (skills are best-effort)."""
131
- try:
132
- from devcouncil.skills.registry import select_skills
133
-
134
- return select_skills(goal=goal, project_root=project_root)
135
- except Exception:
136
- return []
137
-
138
-
139
- def _full_intake(skills: list) -> str:
140
- """Full skill bodies (capped) for the one-shot enhancer call."""
141
- if not skills:
142
- return ""
143
- blocks: list[str] = []
144
- total = 0
145
- for skill in skills[:_MAX_SKILLS_FOR_INTAKE]:
146
- body = (getattr(skill, "body", "") or "").strip()
147
- if not body:
148
- continue
149
- block = f"### Skill: {skill.name}\n{body}"
150
- total += len(block)
151
- if total > _MAX_INTAKE_CHARS:
152
- break
153
- blocks.append(block)
154
- return "\n\n".join(blocks).strip()
155
-
156
-
157
- def _compact_brief(skills: list) -> str:
158
- """One line per skill (name + description) for the council debate prompt."""
159
- lines = []
160
- for skill in skills:
161
- description = (getattr(skill, "description", "") or "").strip()
162
- lines.append(f"- **{skill.name}** — {description}" if description else f"- **{skill.name}**")
163
- return "\n".join(lines).strip()
164
-
165
-
166
- def _clean_items(items: list[str]) -> list[str]:
167
- return [item.strip() for item in items if item.strip()]
@@ -1,39 +0,0 @@
1
- from typing import List
2
- import json
3
- from pydantic import BaseModel
4
- from devcouncil.domain.gap import Gap
5
- from devcouncil.domain.task import Task
6
- from devcouncil.llm.router import ModelRouter
7
-
8
- class RepairOutput(BaseModel):
9
- suggested_tasks: List[Task]
10
-
11
- class RepairService:
12
- """Uses LLM to infer focused repair tasks from blocking gaps."""
13
-
14
- def __init__(self, router: ModelRouter):
15
- self.router = router
16
-
17
- async def generate_repair_plan(self, gaps: List[Gap], project_context: str) -> RepairOutput:
18
- prompt = f"""
19
- The following blocking gaps were detected during verification.
20
- Gaps:
21
- {json.dumps([g.model_dump() for g in gaps], indent=2)}
22
-
23
- Project Context:
24
- {project_context}
25
-
26
- Your task is to generate focused implementation tasks to fix these gaps.
27
- - Each task must have a clear description and recommended fix.
28
- - Specify 'planned_files' that need modification (infer from gap evidence).
29
- - Link each task to the relevant 'requirement_id' mentioned in the gap.
30
-
31
- Return a JSON object with 'suggested_tasks'.
32
- """
33
- messages = [{"role": "user", "content": prompt}]
34
-
35
- return await self.router.complete_structured(
36
- role="planner_a", # Pragmatic tech lead is best suited for repair task generation
37
- messages=messages,
38
- schema=RepairOutput
39
- )