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,24 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
-
3
- const projectRoot = process.env.DEVCOUNCIL_PROJECT_ROOT || process.cwd();
4
-
5
- function runHook(event, payload) {
6
- const args = ["hook", event, "--client", "opencode", "--project-root", projectRoot];
7
- const result = spawnSync("devcouncil", args, {
8
- input: JSON.stringify(payload ?? {}),
9
- encoding: "utf-8",
10
- env: { ...process.env, DEVCOUNCIL_PROJECT_ROOT: projectRoot },
11
- });
12
- if (result.status === 2) {
13
- throw new Error(result.stderr || result.stdout || "DevCouncil blocked the tool call.");
14
- }
15
- }
16
-
17
- export const DevCouncilOpenCodeHook = async () => ({
18
- "tool.execute.before": async (input, output) => {
19
- runHook("pre-tool-use", { tool: input.tool, arguments: output.args });
20
- },
21
- "tool.execute.after": async (input, output) => {
22
- runHook("post-tool-use", { tool: input.tool, arguments: output.args });
23
- },
24
- });
@@ -1,62 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import httpx
4
- from urllib.parse import quote
5
-
6
- from devcouncil.artifacts.graph import ArtifactGraph
7
- from devcouncil.reporting.report_builder import ReportBuilder
8
-
9
-
10
- class PullRequestCommentError(RuntimeError):
11
- pass
12
-
13
-
14
- def build_pr_comment_body(graph: ArtifactGraph, live_review: dict | None = None) -> str:
15
- report = ReportBuilder.build_markdown(graph, live_review=live_review)
16
- return "\n".join([
17
- "## DevCouncil Verification",
18
- "",
19
- report,
20
- ])
21
-
22
-
23
- class GitHubPRCommenter:
24
- def __init__(self, token: str, repository: str, pull_number: int, *, base_url: str = "https://api.github.com"):
25
- self.token = token
26
- self.repository = repository
27
- self.pull_number = pull_number
28
- self.base_url = base_url.rstrip("/")
29
-
30
- async def post_comment(self, body: str) -> dict:
31
- url = f"{self.base_url}/repos/{self.repository}/issues/{self.pull_number}/comments"
32
- headers = {
33
- "Authorization": f"Bearer {self.token}",
34
- "Accept": "application/vnd.github+json",
35
- "Content-Type": "application/json",
36
- }
37
- async with httpx.AsyncClient() as client:
38
- response = await client.post(url, headers=headers, json={"body": body})
39
- if response.status_code >= 400:
40
- raise PullRequestCommentError(f"GitHub comment failed with HTTP {response.status_code}: {response.text}")
41
- return response.json() if response.content else {}
42
-
43
-
44
- class GitLabMRCommenter:
45
- def __init__(self, token: str, project_id: str, merge_request_iid: int, *, base_url: str = "https://gitlab.com/api/v4"):
46
- self.token = token
47
- self.project_id = project_id
48
- self.merge_request_iid = merge_request_iid
49
- self.base_url = base_url.rstrip("/")
50
-
51
- async def post_comment(self, body: str) -> dict:
52
- project = quote(self.project_id, safe="")
53
- url = f"{self.base_url}/projects/{project}/merge_requests/{self.merge_request_iid}/notes"
54
- headers = {
55
- "PRIVATE-TOKEN": self.token,
56
- "Content-Type": "application/json",
57
- }
58
- async with httpx.AsyncClient() as client:
59
- response = await client.post(url, headers=headers, json={"body": body})
60
- if response.status_code >= 400:
61
- raise PullRequestCommentError(f"GitLab comment failed with HTTP {response.status_code}: {response.text}")
62
- return response.json() if response.content else {}
@@ -1,2 +0,0 @@
1
- """Live coding-agent review primitives."""
2
-
@@ -1,349 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import hashlib
4
- import json
5
- import re
6
- from dataclasses import dataclass
7
- from pathlib import Path
8
-
9
- from devcouncil.live.models import AgentTurn, CardStatus, CritiqueCard, Verdict
10
-
11
- RISK_TERMS = (
12
- "skip tests",
13
- "no tests",
14
- "untested",
15
- "ignore failing",
16
- "disable",
17
- "workaround",
18
- "quick hack",
19
- "hardcode",
20
- "force push",
21
- "--no-verify",
22
- "reset --hard",
23
- )
24
-
25
- EVIDENCE_TERMS = (
26
- "test",
27
- "pytest",
28
- "vitest",
29
- "npm test",
30
- "go test",
31
- "cargo test",
32
- "verification",
33
- "verified",
34
- )
35
-
36
- # Word-boundary matchers so "done" matches "I'm done" but not "abandoned"/"undone".
37
- _COMPLETION_RE = re.compile(
38
- r"\b(done|complete|completed|finished|implemented|fixed|ready|all set|"
39
- r"ship it|good to go|works now|it works)\b"
40
- )
41
- # An agent asserting its verification actually passed (the claim we cross-check).
42
- _PASS_CLAIM_RE = re.compile(
43
- r"(tests?\s+(?:are\s+|now\s+)?pass(?:ing|ed|es)?"
44
- r"|all\s+(?:tests?|checks?|cases?)\s+pass"
45
- r"|passing\s+tests?"
46
- r"|\bverified\b|verification\s+(?:pass|succeed)"
47
- r"|tests?\s+green|green\s+tests?"
48
- r"|(?:ran|run)\s+[^.\n]{0,40}?\bpass)"
49
- )
50
- # Negations that flip a nearby claim ("not done", "tests do not pass", "still failing").
51
- _NEGATION_RE = re.compile(
52
- r"\b(not|isn'?t|aren'?t|won'?t|can'?t|cannot|haven'?t|hasn'?t|don'?t|"
53
- r"doesn'?t|didn'?t|no longer|never|yet to|still need|still failing|"
54
- r"not yet|unable|fail(?:s|ing|ed)?)\b"
55
- )
56
- _NEGATION_WINDOW = 30
57
-
58
-
59
- def _claim_present(pattern: re.Pattern[str], lower: str) -> bool:
60
- """True if `pattern` matches and is not negated by a word shortly before it."""
61
- for match in pattern.finditer(lower):
62
- prefix = lower[max(0, match.start() - _NEGATION_WINDOW):match.start()]
63
- if _NEGATION_RE.search(prefix):
64
- continue
65
- return True
66
- return False
67
-
68
-
69
- @dataclass
70
- class _TaskGrounding:
71
- """A snapshot of a task's real verification state from the artifact graph."""
72
-
73
- task_id: str
74
- status: str
75
- blocking_gaps: int
76
- failing_commands: int
77
- acs_total: int
78
- acs_passing: int
79
-
80
- @property
81
- def acs_unproven(self) -> int:
82
- return max(0, self.acs_total - self.acs_passing)
83
-
84
- @property
85
- def is_satisfied(self) -> bool:
86
- return (
87
- self.status in ("verified", "done")
88
- and self.blocking_gaps == 0
89
- and (self.acs_total == 0 or self.acs_passing >= self.acs_total)
90
- )
91
-
92
-
93
- def _load_task_grounding(project_root: Path, task_id: str | None) -> _TaskGrounding | None:
94
- """Load the scoped task's real verification state so claims can be checked
95
- against evidence instead of trusted on the agent's word. Best-effort: any
96
- failure (no DB, unknown task) returns None and the caller falls back to the
97
- pure-heuristic review."""
98
- if not task_id:
99
- return None
100
- try:
101
- from devcouncil.storage.db import get_db
102
- from devcouncil.storage.repositories import ArtifactGraphRepository
103
-
104
- db = get_db(project_root)
105
- if not db:
106
- return None
107
- with db.get_session() as session:
108
- graph = ArtifactGraphRepository(session).load_graph()
109
- except Exception:
110
- return None
111
-
112
- task = graph.tasks.get(task_id)
113
- if task is None:
114
- return None
115
-
116
- blocking = [g for g in graph.gaps.values() if g.task_id == task_id and g.blocking]
117
- failing = [g for g in blocking if g.gap_type == "test_failed"]
118
- ac_ids = set(task.acceptance_criterion_ids)
119
- passing_ac = {
120
- ev.acceptance_criterion_id
121
- for ev in graph.test_evidence
122
- if ev.acceptance_criterion_id in ac_ids and getattr(ev, "status", "") == "passed"
123
- }
124
- return _TaskGrounding(
125
- task_id=task_id,
126
- status=task.status,
127
- blocking_gaps=len(blocking),
128
- failing_commands=len(failing),
129
- acs_total=len(ac_ids),
130
- acs_passing=len(passing_ac),
131
- )
132
-
133
-
134
- def review_turn(
135
- turn: AgentTurn,
136
- project_root: Path,
137
- client: str | None = None,
138
- task_id: str | None = None,
139
- ) -> CritiqueCard:
140
- """Generate a deterministic critique card for an agent response.
141
-
142
- When ``task_id`` resolves to a known task, completion/verification claims are
143
- checked against the task's real artifact state (status, blocking gaps, passing
144
- acceptance-criterion evidence) instead of being trusted by keyword alone. With
145
- no task state available it falls back to the lightweight keyword heuristic.
146
- """
147
- content = turn.content.strip()
148
- lower = content.lower()
149
- concerns: list[str] = []
150
- alternatives: list[str] = []
151
- evidence_requests: list[str] = []
152
- verdict: Verdict = "Approved"
153
-
154
- grounding = _load_task_grounding(project_root, task_id)
155
-
156
- risky_terms = [term for term in RISK_TERMS if term in lower]
157
- if risky_terms:
158
- concerns.append(f"Response contains risky implementation language: {', '.join(risky_terms[:4])}.")
159
- alternatives.append("Replace risky shortcuts with a scoped implementation and explicit rollback or verification path.")
160
-
161
- claims_completion = _claim_present(_COMPLETION_RE, lower)
162
- claims_passing = _claim_present(_PASS_CLAIM_RE, lower)
163
-
164
- if grounding is not None:
165
- # Evidence-grounded review: cross-check the agent's claims against reality.
166
- if claims_passing and grounding.failing_commands > 0:
167
- concerns.append(
168
- f"Agent claims verification passes, but DevCouncil recorded "
169
- f"{grounding.failing_commands} failing verification command(s) for "
170
- f"task {grounding.task_id}."
171
- )
172
- evidence_requests.append(
173
- f"Re-run 'dev verify {grounding.task_id}' and fix the failing command(s) "
174
- "before claiming success."
175
- )
176
- verdict = "Critical Issues"
177
- elif (claims_completion or claims_passing) and not grounding.is_satisfied:
178
- details = [f"task {grounding.task_id} is '{grounding.status}'"]
179
- if grounding.blocking_gaps:
180
- details.append(f"{grounding.blocking_gaps} blocking gap(s)")
181
- if grounding.acs_unproven:
182
- details.append(
183
- f"{grounding.acs_unproven}/{grounding.acs_total} acceptance "
184
- "criteria still lack passing evidence"
185
- )
186
- concerns.append(
187
- "Completion claim is not yet backed by DevCouncil evidence: "
188
- + ", ".join(details) + "."
189
- )
190
- evidence_requests.append(
191
- f"Run 'dev verify {grounding.task_id}' and resolve the gaps so the "
192
- "claim is supported by passing evidence."
193
- )
194
- elif claims_completion and grounding.is_satisfied:
195
- alternatives.append(
196
- f"Completion is corroborated by passing evidence for task {grounding.task_id}; "
197
- "proceed."
198
- )
199
- elif claims_completion and not any(term in lower for term in EVIDENCE_TERMS):
200
- # No task state to ground against: best-effort keyword heuristic.
201
- concerns.append("The response appears to claim completion without naming verification evidence.")
202
- evidence_requests.append("State the exact commands, checks, or reviewed artifacts that prove the change.")
203
-
204
- if _mentions_broad_change(lower):
205
- concerns.append("The response suggests broad codebase changes; confirm they are authorized by the active DevCouncil task.")
206
- alternatives.append("Split broad work into smaller planned files and run DevCouncil gates before marking it done.")
207
-
208
- if "todo" in lower or "follow-up" in lower or "later" in lower:
209
- evidence_requests.append("List any remaining TODOs as DevCouncil gaps or repair tasks instead of burying them in chat.")
210
-
211
- if concerns and verdict == "Approved":
212
- verdict = "Concerns"
213
- if any(term in lower for term in ("--no-verify", "reset --hard", "force push", "ignore failing")):
214
- verdict = "Critical Issues"
215
-
216
- if not alternatives and verdict == "Approved":
217
- alternatives.append("Proceed, but keep the final answer tied to changed files and verification evidence.")
218
-
219
- summary = "No blocking critique found." if verdict == "Approved" else concerns[0]
220
- message_for_agent = _message_for_agent(verdict, concerns, evidence_requests)
221
- card_id = _card_id(turn)
222
- return CritiqueCard(
223
- schema="devcouncil.critique_card.v1",
224
- id=card_id,
225
- session_id=turn.session_id,
226
- turn_id=turn.turn_id,
227
- task_id=task_id,
228
- client=client or turn.source,
229
- verdict=verdict,
230
- summary=summary,
231
- concerns=concerns,
232
- alternatives=alternatives,
233
- evidence_requests=evidence_requests,
234
- message_for_agent=message_for_agent,
235
- )
236
-
237
-
238
- def save_card(project_root: Path, card: CritiqueCard) -> Path:
239
- cards_dir = project_root / ".devcouncil" / "live" / "cards"
240
- cards_dir.mkdir(parents=True, exist_ok=True)
241
- path = cards_dir / f"{card.id}.json"
242
- path.write_text(card.model_dump_json(indent=2) + "\n", encoding="utf-8")
243
- return path
244
-
245
-
246
- def card_path(project_root: Path, card_id: str) -> Path:
247
- return project_root / ".devcouncil" / "live" / "cards" / f"{card_id}.json"
248
-
249
-
250
- def load_cards(project_root: Path) -> list[CritiqueCard]:
251
- cards_dir = project_root / ".devcouncil" / "live" / "cards"
252
- if not cards_dir.exists():
253
- return []
254
- cards: list[CritiqueCard] = []
255
- for path in sorted(cards_dir.glob("*.json"), key=lambda item: item.stat().st_mtime, reverse=True):
256
- try:
257
- cards.append(CritiqueCard.model_validate(json.loads(path.read_text(encoding="utf-8"))))
258
- except Exception:
259
- continue
260
- return cards
261
-
262
-
263
- def filter_cards(
264
- cards: list[CritiqueCard],
265
- *,
266
- task_id: str | None = None,
267
- status: str | None = None,
268
- verdict: str | None = None,
269
- client: str | None = None,
270
- ) -> tuple[list[CritiqueCard], str | None, str | None]:
271
- normalized_status = status.lower() if status else None
272
- if normalized_status and normalized_status not in {"open", "resolved", "ignored"}:
273
- return [], "--status must be open, resolved, or ignored.", "status"
274
-
275
- verdict_map = {
276
- "approved": "Approved",
277
- "concerns": "Concerns",
278
- "critical": "Critical Issues",
279
- "critical issues": "Critical Issues",
280
- }
281
- normalized_verdict = None
282
- if verdict:
283
- normalized_verdict = verdict_map.get(verdict.lower())
284
- if not normalized_verdict:
285
- return [], "--verdict must be approved, concerns, or critical.", "verdict"
286
-
287
- normalized_client = client.lower() if client else None
288
- filtered = []
289
- for card in cards:
290
- if task_id and card.task_id != task_id:
291
- continue
292
- if normalized_status and card.status != normalized_status:
293
- continue
294
- if normalized_verdict and card.verdict != normalized_verdict:
295
- continue
296
- if normalized_client and card.client.lower() != normalized_client:
297
- continue
298
- filtered.append(card)
299
- return filtered, None, None
300
-
301
-
302
- def get_card(project_root: Path, card_id: str) -> CritiqueCard | None:
303
- for card in load_cards(project_root):
304
- if card.id == card_id:
305
- return card
306
- return None
307
-
308
-
309
- def update_card_status(project_root: Path, card_id: str, status: CardStatus) -> CritiqueCard | None:
310
- cards_dir = project_root / ".devcouncil" / "live" / "cards"
311
- path = cards_dir / f"{card_id}.json"
312
- if not path.exists():
313
- return None
314
- card = CritiqueCard.model_validate(json.loads(path.read_text(encoding="utf-8")))
315
- updated = card.model_copy(update={"status": status})
316
- path.write_text(updated.model_dump_json(indent=2) + "\n", encoding="utf-8")
317
- return updated
318
-
319
-
320
- def unresolved_blocking_cards(project_root: Path, task_id: str | None = None) -> list[CritiqueCard]:
321
- return [
322
- card for card in load_cards(project_root)
323
- if card.status == "open" and card.verdict == "Critical Issues"
324
- and (task_id is None or card.task_id in {None, task_id})
325
- ]
326
-
327
-
328
- def _card_id(turn: AgentTurn) -> str:
329
- digest = hashlib.sha256(f"{turn.session_id}:{turn.turn_id}:{turn.content}".encode("utf-8")).hexdigest()
330
- return f"CARD-{digest[:12]}"
331
-
332
-
333
- def _mentions_broad_change(lower: str) -> bool:
334
- return any(phrase in lower for phrase in (
335
- "refactor the entire",
336
- "rewrite",
337
- "all files",
338
- "every file",
339
- "across the codebase",
340
- ))
341
-
342
-
343
- def _message_for_agent(verdict: Verdict, concerns: list[str], evidence_requests: list[str]) -> str:
344
- if verdict == "Approved":
345
- return "Continue, but keep the next response grounded in changed files and verification evidence."
346
- pieces = ["Pause and address this review before proceeding."]
347
- pieces.extend(concerns)
348
- pieces.extend(evidence_requests)
349
- return " ".join(pieces)
@@ -1,63 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from datetime import datetime, timezone
4
- from pathlib import Path
5
- from typing import Any, Literal
6
-
7
- from pydantic import BaseModel, ConfigDict, Field
8
-
9
-
10
- Verdict = Literal["Approved", "Concerns", "Critical Issues"]
11
- CardStatus = Literal["open", "resolved", "ignored"]
12
-
13
-
14
- class AgentTurn(BaseModel):
15
- """A normalized coding-agent conversation turn."""
16
-
17
- session_id: str
18
- turn_id: str
19
- source: str = "generic"
20
- role: Literal["user", "assistant", "system", "tool", "unknown"] = "unknown"
21
- content: str = ""
22
- timestamp: str | None = None
23
- raw: dict[str, Any] = Field(default_factory=dict)
24
-
25
-
26
- class AgentSession(BaseModel):
27
- """A discovered coding-agent transcript."""
28
-
29
- id: str
30
- client: str
31
- transcript_path: str
32
- updated_at: str | None = None
33
- turns: int = 0
34
-
35
-
36
- class CritiqueCard(BaseModel):
37
- """Sage-style response review, shaped for DevCouncil gates."""
38
-
39
- model_config = ConfigDict(populate_by_name=True, serialize_by_alias=True)
40
-
41
- schema_version: str = Field("devcouncil.critique_card.v1", alias="schema")
42
- id: str
43
- session_id: str
44
- turn_id: str
45
- task_id: str | None = None
46
- client: str
47
- verdict: Verdict
48
- summary: str
49
- concerns: list[str] = Field(default_factory=list)
50
- alternatives: list[str] = Field(default_factory=list)
51
- message_for_agent: str = ""
52
- evidence_requests: list[str] = Field(default_factory=list)
53
- status: CardStatus = "open"
54
- created_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
55
- source_path: str | None = None
56
-
57
- @property
58
- def blocks_completion(self) -> bool:
59
- return self.verdict == "Critical Issues"
60
-
61
-
62
- def session_id_from_path(path: Path) -> str:
63
- return path.stem.replace(".", "-")
@@ -1,83 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
-
5
- from devcouncil.execution.prompt_builder import PromptBuilder
6
- from devcouncil.live.models import CritiqueCard
7
- from devcouncil.storage.db import get_db
8
- from devcouncil.storage.repositories import RequirementRepository, TaskRepository
9
-
10
-
11
- def build_live_repair_prompt(project_root: Path, card: CritiqueCard) -> str:
12
- """Build a ready-to-paste repair prompt for a live-review critique card."""
13
- prompt = [
14
- f"# Repair Live Review Card {card.id}",
15
- "",
16
- "A DevCouncil live-review card is blocking or warning on the current coding-agent session.",
17
- "Address the critique directly, then provide exact verification evidence.",
18
- "",
19
- "## Card",
20
- f"- Verdict: {card.verdict}",
21
- f"- Status: {card.status}",
22
- f"- Task: {card.task_id or '(unscoped)'}",
23
- f"- Summary: {card.summary}",
24
- ]
25
- if card.concerns:
26
- prompt.extend(["", "## Concerns"])
27
- prompt.extend(f"- {item}" for item in card.concerns)
28
- if card.alternatives:
29
- prompt.extend(["", "## Safer Alternatives"])
30
- prompt.extend(f"- {item}" for item in card.alternatives)
31
- if card.evidence_requests:
32
- prompt.extend(["", "## Required Evidence"])
33
- prompt.extend(f"- {item}" for item in card.evidence_requests)
34
- if card.message_for_agent:
35
- prompt.extend(["", "## Message For Agent", card.message_for_agent])
36
-
37
- task_prompt = _task_prompt(project_root, card.task_id)
38
- if task_prompt:
39
- prompt.extend(["", "## Original DevCouncil Task Contract", task_prompt])
40
-
41
- prompt.extend([
42
- "",
43
- "## Repair Instructions",
44
- "1. Do not bypass tests, hooks, or verification gates.",
45
- "2. Keep changes within the DevCouncil task contract when one is present.",
46
- "3. Address each concern above explicitly.",
47
- "4. Run the expected verification commands and report exact results.",
48
- f"5. After the repair is complete, ask the developer to run `dev watch resolve {card.id} --status resolved`.",
49
- ])
50
- return "\n".join(prompt).rstrip() + "\n"
51
-
52
-
53
- def build_bulk_live_repair_prompt(project_root: Path, cards: list[CritiqueCard]) -> str:
54
- """Build a combined repair prompt for multiple live-review critique cards."""
55
- if not cards:
56
- return "# Live Review Repair\n\nNo blocking live-review cards found for this scope.\n"
57
- sections = [
58
- "# Repair Blocking Live Review Cards",
59
- "",
60
- f"DevCouncil found {len(cards)} blocking live-review card(s). Address each card below.",
61
- ]
62
- for index, card in enumerate(cards, start=1):
63
- sections.extend([
64
- "",
65
- f"---\n\n## Card {index}: {card.id}",
66
- "",
67
- build_live_repair_prompt(project_root, card).strip(),
68
- ])
69
- return "\n".join(sections).rstrip() + "\n"
70
-
71
-
72
- def _task_prompt(project_root: Path, task_id: str | None) -> str | None:
73
- if not task_id:
74
- return None
75
- db = get_db(project_root)
76
- if not db:
77
- return None
78
- with db.get_session() as session:
79
- task = TaskRepository(session).get_by_id(task_id)
80
- if not task:
81
- return None
82
- requirements = RequirementRepository(session).get_all()
83
- return PromptBuilder(project_root).build_task_prompt(task, requirements)
@@ -1,70 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
-
5
- from devcouncil.live.cards import review_turn
6
- from devcouncil.live.models import AgentTurn, CritiqueCard
7
- from devcouncil.llm.router import ModelRouter
8
-
9
-
10
- class LiveReviewService:
11
- """Reviews coding-agent responses with deterministic or model-backed critique cards."""
12
-
13
- def __init__(self, router: ModelRouter | None = None, role: str = "live_reviewer"):
14
- self.router = router
15
- self.role = role
16
-
17
- async def review(
18
- self,
19
- turn: AgentTurn,
20
- project_root: Path,
21
- client: str = "generic",
22
- use_llm: bool = False,
23
- ) -> CritiqueCard:
24
- fallback = review_turn(turn, project_root, client=client)
25
- if not use_llm or self.router is None:
26
- return fallback
27
-
28
- prompt = f"""
29
- You are DevCouncil's live coding-agent reviewer.
30
- Review the latest assistant response before the developer follows it.
31
-
32
- Return a critique card with:
33
- - verdict: Approved, Concerns, or Critical Issues.
34
- - concerns: concrete risks in the response, reasoning, plan, or proof.
35
- - alternatives: safer approaches or architectures.
36
- - evidence_requests: exact proof the agent should provide before claiming done.
37
- - message_for_agent: a concise ready-to-paste instruction for the coding agent.
38
-
39
- Do not praise. Do not review formatting. Focus on correctness, missing requirements, architectural drift,
40
- unsafe commands, weak evidence, and premature completion claims.
41
-
42
- Client: {client}
43
- Session: {turn.session_id}
44
- Turn: {turn.turn_id}
45
-
46
- Assistant response:
47
- {turn.content}
48
- """
49
- try:
50
- reviewed = await self.router.complete_structured(
51
- role=self.role,
52
- messages=[{"role": "user", "content": prompt}],
53
- schema=CritiqueCard,
54
- )
55
- except ValueError:
56
- reviewed = await self.router.complete_structured(
57
- role="implementation_reviewer",
58
- messages=[{"role": "user", "content": prompt}],
59
- schema=CritiqueCard,
60
- )
61
- except Exception:
62
- return fallback
63
-
64
- return reviewed.model_copy(update={
65
- "id": fallback.id,
66
- "session_id": turn.session_id,
67
- "turn_id": turn.turn_id,
68
- "client": client,
69
- "source_path": fallback.source_path,
70
- })