devcouncil 0.1.1 → 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 (146) hide show
  1. package/README.md +218 -96
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +7 -11
  4. package/pyproject.toml +0 -34
  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 -158
  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 -143
  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/cli/__init__.py +0 -0
  23. package/src/devcouncil/cli/commands/__init__.py +0 -0
  24. package/src/devcouncil/cli/commands/artifacts.py +0 -51
  25. package/src/devcouncil/cli/commands/ast.py +0 -22
  26. package/src/devcouncil/cli/commands/baseline.py +0 -35
  27. package/src/devcouncil/cli/commands/config.py +0 -76
  28. package/src/devcouncil/cli/commands/dashboard.py +0 -26
  29. package/src/devcouncil/cli/commands/doctor.py +0 -140
  30. package/src/devcouncil/cli/commands/go.py +0 -237
  31. package/src/devcouncil/cli/commands/hook.py +0 -128
  32. package/src/devcouncil/cli/commands/init.py +0 -153
  33. package/src/devcouncil/cli/commands/integrate.py +0 -726
  34. package/src/devcouncil/cli/commands/lsp.py +0 -20
  35. package/src/devcouncil/cli/commands/map.py +0 -42
  36. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  37. package/src/devcouncil/cli/commands/plan.py +0 -327
  38. package/src/devcouncil/cli/commands/prompt.py +0 -50
  39. package/src/devcouncil/cli/commands/repair.py +0 -72
  40. package/src/devcouncil/cli/commands/report.py +0 -129
  41. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
  42. package/src/devcouncil/cli/commands/rollback.py +0 -60
  43. package/src/devcouncil/cli/commands/run.py +0 -269
  44. package/src/devcouncil/cli/commands/setup.py +0 -223
  45. package/src/devcouncil/cli/commands/show.py +0 -76
  46. package/src/devcouncil/cli/commands/status.py +0 -117
  47. package/src/devcouncil/cli/commands/tasks.py +0 -55
  48. package/src/devcouncil/cli/commands/trace.py +0 -44
  49. package/src/devcouncil/cli/commands/verify.py +0 -193
  50. package/src/devcouncil/cli/commands/version.py +0 -20
  51. package/src/devcouncil/cli/commands/watch.py +0 -574
  52. package/src/devcouncil/cli/main.py +0 -88
  53. package/src/devcouncil/council/__init__.py +0 -0
  54. package/src/devcouncil/council/prompts/__init__.py +0 -0
  55. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  56. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  57. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  58. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  59. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  60. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  61. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  62. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  63. package/src/devcouncil/domain/__init__.py +0 -0
  64. package/src/devcouncil/domain/assumption.py +0 -17
  65. package/src/devcouncil/domain/critique.py +0 -32
  66. package/src/devcouncil/domain/evidence.py +0 -27
  67. package/src/devcouncil/domain/gap.py +0 -26
  68. package/src/devcouncil/domain/requirement.py +0 -22
  69. package/src/devcouncil/domain/task.py +0 -26
  70. package/src/devcouncil/execution/__init__.py +0 -1
  71. package/src/devcouncil/execution/context_builder.py +0 -60
  72. package/src/devcouncil/execution/executor.py +0 -15
  73. package/src/devcouncil/execution/hook_policy.py +0 -165
  74. package/src/devcouncil/execution/patch.py +0 -28
  75. package/src/devcouncil/execution/paths.py +0 -14
  76. package/src/devcouncil/execution/permissions.py +0 -92
  77. package/src/devcouncil/execution/prompt_builder.py +0 -59
  78. package/src/devcouncil/execution/task_runner.py +0 -166
  79. package/src/devcouncil/executors/__init__.py +0 -1
  80. package/src/devcouncil/executors/coding_cli.py +0 -112
  81. package/src/devcouncil/executors/mini_swe.py +0 -73
  82. package/src/devcouncil/executors/native/__init__.py +0 -0
  83. package/src/devcouncil/executors/native/agent.py +0 -107
  84. package/src/devcouncil/executors/openhands.py +0 -71
  85. package/src/devcouncil/gating/__init__.py +0 -1
  86. package/src/devcouncil/gating/checks/__init__.py +0 -0
  87. package/src/devcouncil/gating/checks/clean_git.py +0 -50
  88. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  89. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  90. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -34
  91. package/src/devcouncil/gating/policy.py +0 -190
  92. package/src/devcouncil/indexing/__init__.py +0 -1
  93. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  94. package/src/devcouncil/indexing/graph_index.py +0 -48
  95. package/src/devcouncil/indexing/lsp.py +0 -120
  96. package/src/devcouncil/indexing/repo_mapper.py +0 -208
  97. package/src/devcouncil/indexing/symbol_index.py +0 -0
  98. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  99. package/src/devcouncil/integrations/github.py +0 -39
  100. package/src/devcouncil/integrations/gitnexus.py +0 -27
  101. package/src/devcouncil/integrations/graphify.py +0 -34
  102. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  103. package/src/devcouncil/integrations/mcp/server.py +0 -599
  104. package/src/devcouncil/integrations/pr_comments.py +0 -62
  105. package/src/devcouncil/live/__init__.py +0 -2
  106. package/src/devcouncil/live/cards.py +0 -207
  107. package/src/devcouncil/live/models.py +0 -63
  108. package/src/devcouncil/live/repair_prompt.py +0 -83
  109. package/src/devcouncil/live/reviewer.py +0 -70
  110. package/src/devcouncil/live/signals.py +0 -135
  111. package/src/devcouncil/live/summary.py +0 -34
  112. package/src/devcouncil/live/tasks.py +0 -18
  113. package/src/devcouncil/live/transcripts.py +0 -138
  114. package/src/devcouncil/llm/__init__.py +0 -1
  115. package/src/devcouncil/llm/cache.py +0 -38
  116. package/src/devcouncil/llm/provider.py +0 -146
  117. package/src/devcouncil/llm/router.py +0 -125
  118. package/src/devcouncil/planning/__init__.py +0 -1
  119. package/src/devcouncil/planning/arbiter_service.py +0 -57
  120. package/src/devcouncil/planning/critique_service.py +0 -66
  121. package/src/devcouncil/planning/plan_service.py +0 -46
  122. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
  123. package/src/devcouncil/planning/repair_service.py +0 -39
  124. package/src/devcouncil/planning/spec_service.py +0 -44
  125. package/src/devcouncil/repo/__init__.py +0 -0
  126. package/src/devcouncil/reporting/__init__.py +0 -0
  127. package/src/devcouncil/reporting/github_check.py +0 -32
  128. package/src/devcouncil/reporting/json_report.py +0 -20
  129. package/src/devcouncil/reporting/markdown_report.py +0 -68
  130. package/src/devcouncil/reporting/report_builder.py +0 -14
  131. package/src/devcouncil/storage/__init__.py +0 -0
  132. package/src/devcouncil/storage/db.py +0 -66
  133. package/src/devcouncil/storage/models.py +0 -83
  134. package/src/devcouncil/storage/repositories.py +0 -423
  135. package/src/devcouncil/telemetry/__init__.py +0 -0
  136. package/src/devcouncil/telemetry/cost.py +0 -34
  137. package/src/devcouncil/telemetry/traces.py +0 -91
  138. package/src/devcouncil/telemetry/tracker.py +0 -49
  139. package/src/devcouncil/ui/__init__.py +0 -1
  140. package/src/devcouncil/ui/dashboard.py +0 -122
  141. package/src/devcouncil/utils/__init__.py +0 -1
  142. package/src/devcouncil/utils/redaction.py +0 -141
  143. package/src/devcouncil/verification/__init__.py +0 -1
  144. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  145. package/src/devcouncil/verification/verifier.py +0 -530
  146. package/uv.lock +0 -1085
@@ -1,574 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import asyncio
5
- import time
6
- from pathlib import Path
7
-
8
- import typer
9
- from rich.console import Console
10
- from rich.panel import Panel
11
- from rich.table import Table
12
-
13
- from devcouncil.live.cards import (
14
- card_path,
15
- filter_cards,
16
- get_card,
17
- load_cards,
18
- review_turn,
19
- save_card,
20
- update_card_status,
21
- )
22
- from devcouncil.live.repair_prompt import build_bulk_live_repair_prompt, build_live_repair_prompt
23
- from devcouncil.live.reviewer import LiveReviewService
24
- from devcouncil.live.signals import ReviewSignal, load_signals, mark_processed
25
- from devcouncil.live.summary import live_review_summary
26
- from devcouncil.live.tasks import active_task_id
27
- from devcouncil.live.transcripts import discover_sessions, latest_assistant_turn, load_turns
28
- from devcouncil.app.config import get_api_key, load_config
29
- from devcouncil.llm.provider import create_provider, validate_model_provider
30
- from devcouncil.llm.router import ModelRouter
31
- from devcouncil.telemetry.traces import TraceLogger
32
-
33
- app = typer.Typer(help="Review active coding-agent sessions and emit critique cards.")
34
- console = Console()
35
-
36
-
37
- @app.command("sessions")
38
- def sessions(
39
- client: str = typer.Option("claude", "--client", help="Agent client: claude or generic."),
40
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
41
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
42
- ):
43
- """List coding-agent transcripts DevCouncil can review."""
44
- root = project_root.expanduser().resolve()
45
- found = discover_sessions(root, client=client)
46
- if json_format:
47
- typer.echo(json.dumps({"sessions": [item.model_dump() for item in found]}, indent=2))
48
- return
49
-
50
- table = Table(title="DevCouncil Watch Sessions")
51
- table.add_column("Client", style="cyan")
52
- table.add_column("Session")
53
- table.add_column("Turns", justify="right")
54
- table.add_column("Transcript")
55
- for item in found:
56
- table.add_row(item.client, item.id, str(item.turns), item.transcript_path)
57
- console.print(table)
58
-
59
-
60
- @app.command("review")
61
- def review(
62
- transcript: Path | None = typer.Option(None, "--transcript", "-t", help="JSONL transcript to review."),
63
- session: str | None = typer.Option(None, "--session", help="Discovered session ID to review, or 'latest'."),
64
- latest: bool = typer.Option(False, "--latest", help="Review the most recently updated discovered session."),
65
- client: str = typer.Option("generic", "--client", help="Agent client: claude, codex, gemini, cursor, or generic."),
66
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
67
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
68
- persist: bool = typer.Option(True, "--persist/--no-persist", help="Save the critique card under .devcouncil/live/cards."),
69
- llm: bool = typer.Option(False, "--llm", help="Use the configured live_reviewer or implementation_reviewer model."),
70
- force: bool = typer.Option(False, "--force", help="Regenerate and overwrite an existing card for this turn."),
71
- task_id: str | None = typer.Option(None, "--task-id", help="Associate the critique card with a DevCouncil task."),
72
- ):
73
- """Review the latest assistant response in a coding-agent transcript."""
74
- root = project_root.expanduser().resolve()
75
- transcript_path = _resolve_transcript(root, client, transcript=transcript, session=session, latest=latest)
76
- if transcript_path is None:
77
- message = "No transcript selected. Use --transcript, --session, or --latest."
78
- if json_format:
79
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
80
- else:
81
- console.print(f"[red]{message}[/red]")
82
- raise typer.Exit(code=2)
83
- turn = latest_assistant_turn(transcript_path, client=client)
84
- if turn is None:
85
- message = f"No assistant turn found in {transcript_path}."
86
- if json_format:
87
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
88
- else:
89
- console.print(f"[yellow]{message}[/yellow]")
90
- raise typer.Exit(code=1)
91
-
92
- scoped_task_id = task_id or active_task_id(root)
93
- card = asyncio.run(_review_turn(turn, root, client, llm, task_id=scoped_task_id))
94
- saved_path, duplicate = _save_card_once(root, card, persist=persist, force=force)
95
- if saved_path:
96
- _log_card_reviewed(root, card, saved_path, duplicate=duplicate, source="review")
97
- payload = card.model_dump()
98
- if saved_path:
99
- payload["path"] = str(saved_path)
100
- payload["duplicate"] = duplicate
101
-
102
- if json_format:
103
- typer.echo(json.dumps(payload, indent=2))
104
- return
105
-
106
- _print_card(card)
107
- if saved_path:
108
- console.print(f"[green]Saved critique card:[/green] {saved_path}")
109
-
110
-
111
- @app.command("cards")
112
- def cards(
113
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
114
- limit: int = typer.Option(10, "--limit", "-n", help="Maximum cards to show."),
115
- task_id: str | None = typer.Option(None, "--task-id", help="Filter cards by task ID."),
116
- status: str | None = typer.Option(None, "--status", help="Filter by status: open, resolved, or ignored."),
117
- verdict: str | None = typer.Option(None, "--verdict", help="Filter by verdict: approved, concerns, or critical."),
118
- client: str | None = typer.Option(None, "--client", help="Filter by coding-agent client."),
119
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
120
- ):
121
- """Show previously emitted critique cards."""
122
- root = project_root.expanduser().resolve()
123
- if limit < 1:
124
- message = "--limit must be greater than 0."
125
- if json_format:
126
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
127
- else:
128
- console.print(f"[red]{message}[/red]")
129
- raise typer.Exit(code=2)
130
- found, error, _argument = filter_cards(
131
- load_cards(root),
132
- task_id=task_id,
133
- status=status,
134
- verdict=verdict,
135
- client=client,
136
- )
137
- if error:
138
- if json_format:
139
- typer.echo(json.dumps({"ok": False, "error": error}, indent=2))
140
- else:
141
- console.print(f"[red]{error}[/red]")
142
- raise typer.Exit(code=2)
143
- total = len(found)
144
- found = found[:limit]
145
- if json_format:
146
- typer.echo(json.dumps({
147
- "cards": [item.model_dump() for item in found],
148
- "filters": {
149
- "task_id": task_id,
150
- "status": status,
151
- "verdict": verdict,
152
- "client": client,
153
- },
154
- "limit": limit,
155
- "total": total,
156
- }, indent=2))
157
- return
158
- for card in found:
159
- _print_card(card)
160
-
161
-
162
- @app.command("status")
163
- def status(
164
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
165
- task_id: str | None = typer.Option(None, "--task-id", help="Task scope for blocking-card calculation."),
166
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
167
- ):
168
- """Show compact live-review status, blockers, pending signals, and recent cards."""
169
- root = project_root.expanduser().resolve()
170
- payload = live_review_summary(root, task_id=task_id)
171
- if json_format:
172
- typer.echo(json.dumps(payload, indent=2))
173
- return
174
- blockers = payload["blocking_cards"]
175
-
176
- summary = Table(title="DevCouncil Live Review Status")
177
- summary.add_column("Metric", style="cyan")
178
- summary.add_column("Value")
179
- summary.add_row("Active task", payload["active_task_id"] or "(none)")
180
- summary.add_row("Scope task", payload["scope_task_id"] or "(unscoped)")
181
- summary.add_row("Pending signals", str(payload["pending_signals"]))
182
- summary.add_row("Cards", str(payload["cards"]["total"]))
183
- summary.add_row("Open cards", str(payload["cards"]["open"]))
184
- summary.add_row("Open critical cards", str(payload["cards"]["critical_open"]))
185
- summary.add_row("Blocking cards in scope", str(len(blockers)))
186
- console.print(summary)
187
-
188
- if blockers:
189
- table = Table(title="Blocking Live-Review Cards")
190
- table.add_column("Card", style="red")
191
- table.add_column("Task")
192
- table.add_column("Summary")
193
- for card in blockers:
194
- table.add_row(card["id"], card.get("task_id") or "(unscoped)", card["summary"])
195
- console.print(table)
196
-
197
- if payload["pending_signal_items"]:
198
- table = Table(title="Pending Agent Responses")
199
- table.add_column("Client", style="cyan")
200
- table.add_column("Task")
201
- table.add_column("Transcript", overflow="fold")
202
- table.add_column("Review Command", overflow="fold")
203
- for signal in payload["pending_signal_items"]:
204
- table.add_row(
205
- signal.get("client") or "",
206
- signal.get("task_id") or "(unscoped)",
207
- signal.get("transcript_path") or "",
208
- signal.get("review_command") or "",
209
- )
210
- console.print(table)
211
- commands = [signal.get("review_command") for signal in payload["pending_signal_items"] if signal.get("review_command")]
212
- if commands:
213
- console.print("[cyan]Pending review commands:[/cyan]")
214
- for command in commands:
215
- console.print(command)
216
-
217
-
218
- @app.command("resolve")
219
- def resolve(
220
- card_id: str = typer.Argument(..., help="Critique card ID to update."),
221
- status: str = typer.Option("resolved", "--status", help="New status: resolved or ignored."),
222
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
223
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
224
- ):
225
- """Mark a critique card resolved or ignored after the concern has been handled."""
226
- if status not in {"resolved", "ignored"}:
227
- message = "--status must be resolved or ignored."
228
- if json_format:
229
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
230
- else:
231
- console.print(f"[red]{message}[/red]")
232
- raise typer.Exit(code=2)
233
- root = project_root.expanduser().resolve()
234
- card = update_card_status(root, card_id, status) # type: ignore[arg-type]
235
- if card is None:
236
- message = f"Critique card {card_id} not found."
237
- if json_format:
238
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
239
- else:
240
- console.print(f"[red]{message}[/red]")
241
- raise typer.Exit(code=1)
242
- if json_format:
243
- typer.echo(json.dumps({"ok": True, "card": card.model_dump()}, indent=2))
244
- _log_card_resolved(root, card)
245
- return
246
- _log_card_resolved(root, card)
247
- console.print(f"[green]Updated {card.id}:[/green] {card.status}")
248
-
249
-
250
- @app.command("repair")
251
- def repair(
252
- card_id: str = typer.Argument(..., help="Critique card ID to turn into a repair prompt."),
253
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
254
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
255
- ):
256
- """Generate a ready-to-paste repair prompt for a critique card."""
257
- root = project_root.expanduser().resolve()
258
- card = get_card(root, card_id)
259
- if card is None:
260
- message = f"Critique card {card_id} not found."
261
- if json_format:
262
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
263
- else:
264
- console.print(f"[red]{message}[/red]")
265
- raise typer.Exit(code=1)
266
- prompt = build_live_repair_prompt(root, card)
267
- if json_format:
268
- typer.echo(json.dumps({"ok": True, "card": card.model_dump(), "prompt": prompt}, indent=2))
269
- return
270
- typer.echo(prompt)
271
-
272
-
273
- @app.command("repair-all")
274
- def repair_all(
275
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
276
- task_id: str | None = typer.Option(None, "--task-id", help="Task scope for blocking-card repair prompts."),
277
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
278
- ):
279
- """Generate repair prompts for all blocking live-review cards in scope."""
280
- root = project_root.expanduser().resolve()
281
- summary = live_review_summary(root, task_id=task_id)
282
- cards = [
283
- get_card(root, item["id"])
284
- for item in summary["blocking_cards"]
285
- if isinstance(item.get("id"), str)
286
- ]
287
- cards = [card for card in cards if card is not None]
288
- prompt = build_bulk_live_repair_prompt(root, cards)
289
- if json_format:
290
- typer.echo(json.dumps({
291
- "ok": True,
292
- "scope_task_id": summary["scope_task_id"],
293
- "cards": [card.model_dump() for card in cards],
294
- "prompt": prompt,
295
- }, indent=2))
296
- return
297
- typer.echo(prompt)
298
-
299
-
300
- @app.command("signals")
301
- def signals(
302
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
303
- client: str | None = typer.Option(None, "--client", help="Filter by client."),
304
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
305
- ):
306
- """List response-ready hook signals waiting for review."""
307
- root = project_root.expanduser().resolve()
308
- found = _filtered_signals(root, client)
309
- if json_format:
310
- typer.echo(json.dumps({"signals": [item.model_dump() for item in found]}, indent=2))
311
- return
312
- table = Table(title="DevCouncil Watch Signals")
313
- table.add_column("Client", style="cyan")
314
- table.add_column("Transcript")
315
- table.add_column("Command")
316
- table.add_column("Signal")
317
- for item in found:
318
- table.add_row(item.client, item.transcript_path or "", item.review_command or "", item.path or "")
319
- console.print(table)
320
-
321
-
322
- @app.command("pending")
323
- def pending(
324
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
325
- client: str | None = typer.Option(None, "--client", help="Filter by client."),
326
- task_id: str | None = typer.Option(None, "--task-id", help="Associate reviewed pending signals with a DevCouncil task."),
327
- llm: bool = typer.Option(False, "--llm", help="Use the configured live_reviewer or implementation_reviewer model."),
328
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
329
- keep: bool = typer.Option(False, "--keep", help="Keep signals pending after successful review."),
330
- force: bool = typer.Option(False, "--force", help="Regenerate and overwrite existing cards for reviewed turns."),
331
- ):
332
- """Review every pending response-ready signal that includes a transcript path."""
333
- root = project_root.expanduser().resolve()
334
- reviewed = []
335
- skipped = []
336
- for signal in _filtered_signals(root, client):
337
- transcript_path = _resolve_signal_transcript(root, signal)
338
- if transcript_path is None:
339
- skipped.append({"signal": signal.model_dump(), "reason": "No transcript path in signal payload."})
340
- continue
341
- turn = latest_assistant_turn(transcript_path, client=signal.client)
342
- if turn is None:
343
- skipped.append({"signal": signal.model_dump(), "reason": f"No assistant turn found in {transcript_path}."})
344
- continue
345
- scoped_task_id = task_id or signal.task_id or active_task_id(root)
346
- card = asyncio.run(_review_turn(turn, root, signal.client, llm, task_id=scoped_task_id))
347
- saved_path, duplicate = _save_card_once(root, card, persist=True, force=force)
348
- if saved_path:
349
- _log_card_reviewed(root, card, saved_path, duplicate=duplicate, source="pending")
350
- if not keep:
351
- processed_path = mark_processed(signal, root)
352
- _log_signal_processed(root, signal, processed_path, card)
353
- reviewed.append({
354
- "card": card.model_dump(),
355
- "path": str(saved_path) if saved_path else None,
356
- "duplicate": duplicate,
357
- "signal": signal.model_dump(),
358
- })
359
- if not json_format:
360
- _print_card(card)
361
- if duplicate:
362
- console.print(f"[yellow]Already reviewed:[/yellow] {saved_path}")
363
- else:
364
- console.print(f"[green]Saved critique card:[/green] {saved_path}")
365
-
366
- if json_format:
367
- typer.echo(json.dumps({"reviewed": reviewed, "skipped": skipped}, indent=2))
368
- return
369
- for item in skipped:
370
- console.print(f"[yellow]Skipped signal:[/yellow] {item['reason']}")
371
-
372
-
373
- @app.command("follow")
374
- def follow(
375
- transcript: Path | None = typer.Option(None, "--transcript", "-t", help="JSONL transcript to poll."),
376
- session: str | None = typer.Option(None, "--session", help="Discovered session ID to follow, or 'latest'."),
377
- latest: bool = typer.Option(False, "--latest", help="Follow the most recently updated discovered session."),
378
- client: str = typer.Option("generic", "--client", help="Agent client: claude, codex, gemini, cursor, or generic."),
379
- project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
380
- interval: float = typer.Option(2.0, "--interval", help="Polling interval in seconds."),
381
- llm: bool = typer.Option(False, "--llm", help="Use the configured live_reviewer or implementation_reviewer model."),
382
- once: bool = typer.Option(False, "--once", help="Review once and exit after a new assistant turn is found."),
383
- force: bool = typer.Option(False, "--force", help="Regenerate and overwrite an existing card for this turn."),
384
- task_id: str | None = typer.Option(None, "--task-id", help="Associate critique cards with a DevCouncil task."),
385
- ):
386
- """Poll a transcript and emit a critique card whenever the latest assistant turn changes."""
387
- root = project_root.expanduser().resolve()
388
- transcript_path = _resolve_transcript(root, client, transcript=transcript, session=session, latest=latest)
389
- if transcript_path is None:
390
- console.print("[red]No transcript selected. Use --transcript, --session, or --latest.[/red]")
391
- raise typer.Exit(code=2)
392
- seen_turn_id: str | None = None
393
- console.print(f"[cyan]Watching transcript:[/cyan] {transcript_path}")
394
- while True:
395
- turn = latest_assistant_turn(transcript_path, client=client)
396
- if turn and turn.turn_id != seen_turn_id:
397
- seen_turn_id = turn.turn_id
398
- scoped_task_id = task_id or active_task_id(root)
399
- card = asyncio.run(_review_turn(turn, root, client, llm, task_id=scoped_task_id))
400
- saved_path, duplicate = _save_card_once(root, card, persist=True, force=force)
401
- if saved_path:
402
- _log_card_reviewed(root, card, saved_path, duplicate=duplicate, source="follow")
403
- _print_card(card)
404
- if duplicate:
405
- console.print(f"[yellow]Already reviewed:[/yellow] {saved_path}")
406
- else:
407
- console.print(f"[green]Saved critique card:[/green] {saved_path}")
408
- if once:
409
- return
410
- if once:
411
- console.print("[yellow]No new assistant turn found.[/yellow]")
412
- raise typer.Exit(code=1)
413
- time.sleep(interval)
414
-
415
-
416
- @app.command("import")
417
- def import_transcript(
418
- transcript: Path = typer.Argument(..., help="Transcript JSONL file to normalize."),
419
- client: str = typer.Option("generic", "--client", help="Agent client label."),
420
- json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
421
- ):
422
- """Normalize a transcript into DevCouncil turn records without reviewing it."""
423
- turns = load_turns(transcript.expanduser().resolve(), client=client)
424
- if json_format:
425
- typer.echo(json.dumps({"turns": [turn.model_dump() for turn in turns]}, indent=2))
426
- return
427
- console.print(f"Loaded {len(turns)} turns from {transcript}")
428
-
429
-
430
- def _print_card(card) -> None:
431
- color = "green" if card.verdict == "Approved" else "yellow"
432
- if card.verdict == "Critical Issues":
433
- color = "red"
434
- body = [
435
- f"[bold]Verdict:[/bold] [{color}]{card.verdict}[/{color}]",
436
- f"[bold]Status:[/bold] {card.status}",
437
- f"[bold]Task:[/bold] {card.task_id or '(unscoped)'}",
438
- card.summary,
439
- ]
440
- if card.concerns:
441
- body.append("\n[bold]Concerns[/bold]")
442
- body.extend(f"- {item}" for item in card.concerns)
443
- if card.alternatives:
444
- body.append("\n[bold]Alternatives[/bold]")
445
- body.extend(f"- {item}" for item in card.alternatives)
446
- if card.evidence_requests:
447
- body.append("\n[bold]Evidence Requests[/bold]")
448
- body.extend(f"- {item}" for item in card.evidence_requests)
449
- body.append(f"\n[bold]Message for agent:[/bold] {card.message_for_agent}")
450
- console.print(Panel("\n".join(body), title=f"Critique Card {card.id}"))
451
-
452
-
453
- def _filtered_signals(root: Path, client: str | None) -> list[ReviewSignal]:
454
- found = load_signals(root)
455
- if client is None:
456
- return found
457
- normalized = client.lower()
458
- return [item for item in found if item.client == normalized]
459
-
460
-
461
- def _resolve_signal_transcript(root: Path, signal: ReviewSignal) -> Path | None:
462
- if not signal.transcript_path:
463
- return None
464
- path = Path(signal.transcript_path).expanduser()
465
- if not path.is_absolute():
466
- path = root / path
467
- return path.resolve()
468
-
469
-
470
- def _resolve_transcript(
471
- root: Path,
472
- client: str,
473
- transcript: Path | None = None,
474
- session: str | None = None,
475
- latest: bool = False,
476
- ) -> Path | None:
477
- if transcript is not None:
478
- path = transcript.expanduser()
479
- if not path.is_absolute():
480
- path = root / path
481
- return path.resolve()
482
-
483
- selector = "latest" if latest else session
484
- if selector is None:
485
- return None
486
-
487
- discovered = discover_sessions(root, client=client)
488
- if not discovered:
489
- return None
490
- if selector == "latest":
491
- return Path(discovered[0].transcript_path).expanduser().resolve()
492
- for item in discovered:
493
- if item.id == selector:
494
- return Path(item.transcript_path).expanduser().resolve()
495
- return None
496
-
497
-
498
- def _save_card_once(root: Path, card, persist: bool, force: bool) -> tuple[Path | None, bool]:
499
- if not persist:
500
- return None, False
501
- path = card_path(root, card.id)
502
- if path.exists() and not force:
503
- return path, True
504
- if path.exists() and force:
505
- existing_cards = [item for item in load_cards(root) if item.id == card.id]
506
- if existing_cards:
507
- card = card.model_copy(update={"status": existing_cards[0].status})
508
- return save_card(root, card), False
509
-
510
-
511
- def _log_card_reviewed(root: Path, card, path: Path, *, duplicate: bool, source: str) -> None:
512
- event_type = "live_review_card_reused" if duplicate else "live_review_card_saved"
513
- TraceLogger(root).log_event(
514
- event_type,
515
- {
516
- "card_id": card.id,
517
- "client": card.client,
518
- "verdict": card.verdict,
519
- "status": card.status,
520
- "path": str(path),
521
- "source": source,
522
- "duplicate": duplicate,
523
- },
524
- task_id=card.task_id,
525
- summary=f"Live-review card {card.id} {'reused' if duplicate else 'saved'} from {source}.",
526
- )
527
-
528
-
529
- def _log_card_resolved(root: Path, card) -> None:
530
- TraceLogger(root).log_event(
531
- "live_review_card_status_updated",
532
- {
533
- "card_id": card.id,
534
- "client": card.client,
535
- "verdict": card.verdict,
536
- "status": card.status,
537
- },
538
- task_id=card.task_id,
539
- summary=f"Live-review card {card.id} marked {card.status}.",
540
- )
541
-
542
-
543
- def _log_signal_processed(root: Path, signal: ReviewSignal, processed_path: Path | None, card) -> None:
544
- TraceLogger(root).log_event(
545
- "live_review_signal_processed",
546
- {
547
- "client": signal.client,
548
- "signal_path": signal.path,
549
- "processed_path": str(processed_path) if processed_path else None,
550
- "transcript_path": signal.transcript_path,
551
- "card_id": card.id,
552
- },
553
- task_id=card.task_id or signal.task_id,
554
- summary=f"Live-review signal processed into card {card.id}.",
555
- )
556
-
557
-
558
- async def _review_turn(turn, root: Path, client: str, use_llm: bool, task_id: str | None = None):
559
- if not use_llm:
560
- card = review_turn(turn, root, client=client)
561
- return card.model_copy(update={"task_id": task_id}) if task_id else card
562
- try:
563
- config = load_config(root)
564
- validate_model_provider(config.models.provider)
565
- api_key = get_api_key(config.models.provider, root)
566
- provider = create_provider(config.models.provider, api_key)
567
- role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
568
- router = ModelRouter(provider, role_config)
569
- except Exception as exc:
570
- console.print(f"[yellow]Model-backed review unavailable; using deterministic card: {exc}[/yellow]")
571
- card = review_turn(turn, root, client=client)
572
- return card.model_copy(update={"task_id": task_id}) if task_id else card
573
- card = await LiveReviewService(router).review(turn, root, client=client, use_llm=True)
574
- return card.model_copy(update={"task_id": task_id}) if task_id else card
@@ -1,88 +0,0 @@
1
- import typer
2
- from pathlib import Path
3
-
4
- from devcouncil.cli.commands import (
5
- artifacts,
6
- baseline,
7
- config,
8
- ast,
9
- dashboard,
10
- doctor,
11
- go,
12
- hook,
13
- init,
14
- integrate,
15
- lsp,
16
- map,
17
- mcp_server,
18
- plan,
19
- prompt,
20
- repair,
21
- report,
22
- reset_demo_state,
23
- rollback,
24
- run,
25
- setup,
26
- show,
27
- status,
28
- tasks,
29
- trace,
30
- verify,
31
- version,
32
- watch,
33
- )
34
- from devcouncil.cli.commands.init import initialize_project
35
-
36
- app = typer.Typer(
37
- name="dev",
38
- help="DevCouncil: Gated orchestrator for AI-assisted software development.",
39
- add_completion=False,
40
- )
41
-
42
- # Typer subcommands (those using app = Typer())
43
- app.add_typer(init.app, name="init")
44
- app.add_typer(doctor.app, name="doctor")
45
- app.add_typer(tasks.app, name="tasks")
46
- app.add_typer(report.app, name="report")
47
- app.add_typer(rollback.app, name="rollback")
48
- app.add_typer(config.app, name="config")
49
- app.add_typer(artifacts.app, name="artifacts")
50
- app.add_typer(hook.app, name="hook")
51
- app.add_typer(version.app, name="version")
52
- app.add_typer(mcp_server.app, name="mcp-server")
53
- app.add_typer(integrate.app, name="integrate")
54
- app.add_typer(integrate.app, name="integrations")
55
- app.add_typer(trace.app, name="trace")
56
- app.add_typer(setup.app, name="setup")
57
- app.add_typer(lsp.app, name="lsp")
58
- app.add_typer(ast.app, name="ast")
59
- app.add_typer(dashboard.app, name="dashboard")
60
- app.add_typer(watch.app, name="watch")
61
-
62
- # Direct command registrations (those defined as def cmd())
63
- app.command(name="baseline")(baseline.baseline)
64
- app.command(name="e2e")(go.go)
65
- app.command(name="go")(go.go)
66
- app.command(name="map")(map.map_repo)
67
- app.command(name="plan")(plan.plan)
68
- app.command(name="prompt")(prompt.prompt)
69
- app.command(name="reset-demo-state")(reset_demo_state.reset_demo_state)
70
- app.command(name="run")(run.run)
71
- app.command(name="show")(show.show)
72
- app.command(name="verify")(verify.verify)
73
- app.command(name="repair")(repair.repair)
74
- app.command(name="status")(status.status)
75
-
76
- @app.callback()
77
- def main(ctx: typer.Context):
78
- """
79
- DevCouncil: Gated orchestrator for AI-assisted software development.
80
- """
81
- if ctx.invoked_subcommand in {"init", "setup"}:
82
- return
83
-
84
- initialize_project(Path("."), quiet=True)
85
- return
86
-
87
- if __name__ == "__main__":
88
- app()
File without changes
File without changes
@@ -1,19 +0,0 @@
1
- Goal: {goal}
2
-
3
- Initial Requirements:
4
- {requirements_json}
5
-
6
- Plan A: {plan_a_json}
7
- Plan B: {plan_b_json}
8
-
9
- Critique of Plan B by Critic A: {critique_a_json}
10
- Critique of Plan A by Critic B: {critique_b_json}
11
-
12
- Rebuttal of Critic B by Planner A: {rebuttal_a_json}
13
- Rebuttal of Critic A by Planner B: {rebuttal_b_json}
14
-
15
- You are the arbiter engineering manager. Your goal is to produce the final, definitive set of requirements and tasks.
16
- - You do not decide by vibes.
17
- - High-severity unrefuted findings from critics must be incorporated into the final requirements or tasks.
18
- - If a planner successfully rebutted a finding, you may skip it.
19
- - Produce a single, coherent task graph.