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,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
- resolved_cards = [card for card in cards if card is not None]
288
- prompt = build_bulk_live_repair_prompt(root, resolved_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 resolved_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
- # Pass task_id so the deterministic review can ground completion/evidence
561
- # claims against the task's real verification state.
562
- return review_turn(turn, root, client=client, task_id=task_id)
563
- try:
564
- config = load_config(root)
565
- validate_model_provider(config.models.provider)
566
- api_key = get_api_key(config.models.provider, root)
567
- provider = create_provider(config.models.provider, api_key, project_root=root)
568
- role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
569
- router = ModelRouter(provider, role_config, project_root=root)
570
- except Exception as exc:
571
- console.print(f"[yellow]Model-backed review unavailable; using deterministic card: {exc}[/yellow]")
572
- return review_turn(turn, root, client=client, task_id=task_id)
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,40 +0,0 @@
1
- import json
2
- import typer
3
- from pathlib import Path
4
- from rich.console import Console
5
-
6
- from devcouncil.cli.commands.init import initialize_project
7
- from devcouncil.execution.fs_watcher import FilesystemWatcher
8
-
9
- console = Console()
10
-
11
-
12
- def watch_fs(
13
- task_id: str = typer.Option(..., "--task", help="Task ID to attribute file changes to."),
14
- poll_interval: float = typer.Option(1.0, "--poll-interval"),
15
- once: bool = typer.Option(False, "--once", help="Scan once and exit."),
16
- project_root: Path = typer.Option(Path("."), "--project-root"),
17
- json_format: bool = typer.Option(False, "--json"),
18
- ):
19
- root = project_root.expanduser().resolve()
20
- initialize_project(root, quiet=True)
21
-
22
- def _print_event(event: dict) -> None:
23
- status = "allowed" if event["allowed"] else "denied"
24
- console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
25
-
26
- watcher = FilesystemWatcher(root, task_id, poll_interval=poll_interval, on_event=_print_event)
27
- if once:
28
- events = watcher.scan_once()
29
- if json_format:
30
- typer.echo(json.dumps({"events": events}, indent=2))
31
- else:
32
- for event in events:
33
- status = "allowed" if event["allowed"] else "denied"
34
- console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
35
- return
36
- console.print(f"[cyan]Watching filesystem for task {task_id}. Ctrl+C to stop.[/cyan]")
37
- try:
38
- watcher.watch()
39
- except KeyboardInterrupt:
40
- console.print("[yellow]Stopped filesystem watcher.[/yellow]")