devcouncil 0.1.0 → 0.2.0

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 (190) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +197 -494
  3. package/package.json +9 -2
  4. package/pyproject.toml +62 -27
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +297 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +163 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/assets/__init__.py +1 -0
  22. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  23. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  24. package/src/devcouncil/cli/commands/agents.py +292 -0
  25. package/src/devcouncil/cli/commands/artifacts.py +54 -48
  26. package/src/devcouncil/cli/commands/ast.py +22 -0
  27. package/src/devcouncil/cli/commands/baseline.py +35 -32
  28. package/src/devcouncil/cli/commands/check.py +209 -0
  29. package/src/devcouncil/cli/commands/config.py +115 -54
  30. package/src/devcouncil/cli/commands/cost.py +57 -0
  31. package/src/devcouncil/cli/commands/dashboard.py +31 -0
  32. package/src/devcouncil/cli/commands/doctor.py +291 -47
  33. package/src/devcouncil/cli/commands/evidence.py +48 -0
  34. package/src/devcouncil/cli/commands/go.py +656 -0
  35. package/src/devcouncil/cli/commands/handoff.py +69 -0
  36. package/src/devcouncil/cli/commands/hook.py +209 -33
  37. package/src/devcouncil/cli/commands/init.py +204 -57
  38. package/src/devcouncil/cli/commands/integrate.py +1171 -76
  39. package/src/devcouncil/cli/commands/lsp.py +20 -0
  40. package/src/devcouncil/cli/commands/map.py +96 -22
  41. package/src/devcouncil/cli/commands/plan.py +422 -210
  42. package/src/devcouncil/cli/commands/prompt.py +48 -34
  43. package/src/devcouncil/cli/commands/repair.py +89 -69
  44. package/src/devcouncil/cli/commands/report.py +120 -54
  45. package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
  46. package/src/devcouncil/cli/commands/rollback.py +55 -54
  47. package/src/devcouncil/cli/commands/run.py +285 -220
  48. package/src/devcouncil/cli/commands/runs.py +223 -0
  49. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  50. package/src/devcouncil/cli/commands/semantic.py +47 -0
  51. package/src/devcouncil/cli/commands/setup.py +300 -20
  52. package/src/devcouncil/cli/commands/shell.py +73 -0
  53. package/src/devcouncil/cli/commands/show.py +76 -57
  54. package/src/devcouncil/cli/commands/skills.py +88 -0
  55. package/src/devcouncil/cli/commands/status.py +141 -105
  56. package/src/devcouncil/cli/commands/tasks.py +55 -41
  57. package/src/devcouncil/cli/commands/trace.py +49 -4
  58. package/src/devcouncil/cli/commands/verify.py +293 -128
  59. package/src/devcouncil/cli/commands/version.py +20 -20
  60. package/src/devcouncil/cli/commands/watch.py +574 -0
  61. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  62. package/src/devcouncil/cli/main.py +92 -25
  63. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  64. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  65. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  66. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  67. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  68. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  69. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  70. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  71. package/src/devcouncil/domain/assumption.py +17 -17
  72. package/src/devcouncil/domain/critique.py +32 -32
  73. package/src/devcouncil/domain/evidence.py +47 -27
  74. package/src/devcouncil/domain/gap.py +52 -26
  75. package/src/devcouncil/domain/requirement.py +22 -22
  76. package/src/devcouncil/domain/task.py +55 -26
  77. package/src/devcouncil/execution/__init__.py +1 -1
  78. package/src/devcouncil/execution/checkpoints.py +246 -0
  79. package/src/devcouncil/execution/context_builder.py +54 -54
  80. package/src/devcouncil/execution/executor.py +15 -15
  81. package/src/devcouncil/execution/fs_watcher.py +180 -0
  82. package/src/devcouncil/execution/handoff.py +102 -0
  83. package/src/devcouncil/execution/hook_policy.py +186 -77
  84. package/src/devcouncil/execution/patch.py +77 -28
  85. package/src/devcouncil/execution/permissions.py +52 -59
  86. package/src/devcouncil/execution/policy_engine.py +343 -0
  87. package/src/devcouncil/execution/prompt_builder.py +650 -38
  88. package/src/devcouncil/execution/shell_session.py +225 -0
  89. package/src/devcouncil/execution/task_runner.py +68 -64
  90. package/src/devcouncil/executors/__init__.py +1 -1
  91. package/src/devcouncil/executors/agent_registry.py +575 -0
  92. package/src/devcouncil/executors/coding_cli.py +736 -0
  93. package/src/devcouncil/executors/mini_swe.py +63 -63
  94. package/src/devcouncil/executors/native/agent.py +186 -85
  95. package/src/devcouncil/executors/openhands.py +56 -56
  96. package/src/devcouncil/gating/__init__.py +1 -1
  97. package/src/devcouncil/gating/checks/clean_git.py +52 -45
  98. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  99. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  100. package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
  101. package/src/devcouncil/gating/policy.py +315 -167
  102. package/src/devcouncil/hardware.py +184 -0
  103. package/src/devcouncil/indexing/__init__.py +1 -1
  104. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  105. package/src/devcouncil/indexing/graph_index.py +48 -48
  106. package/src/devcouncil/indexing/lsp.py +161 -0
  107. package/src/devcouncil/indexing/repo_mapper.py +1455 -204
  108. package/src/devcouncil/indexing/semantic_index.py +205 -0
  109. package/src/devcouncil/integrations/actions.py +146 -0
  110. package/src/devcouncil/integrations/check.py +423 -0
  111. package/src/devcouncil/integrations/github.py +35 -35
  112. package/src/devcouncil/integrations/github_intent.py +142 -0
  113. package/src/devcouncil/integrations/gitnexus.py +62 -27
  114. package/src/devcouncil/integrations/graphify.py +34 -34
  115. package/src/devcouncil/integrations/mcp/server.py +2072 -96
  116. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  117. package/src/devcouncil/integrations/pr_comments.py +62 -0
  118. package/src/devcouncil/live/__init__.py +2 -0
  119. package/src/devcouncil/live/cards.py +349 -0
  120. package/src/devcouncil/live/models.py +63 -0
  121. package/src/devcouncil/live/repair_prompt.py +83 -0
  122. package/src/devcouncil/live/reviewer.py +70 -0
  123. package/src/devcouncil/live/signals.py +135 -0
  124. package/src/devcouncil/live/summary.py +34 -0
  125. package/src/devcouncil/live/tasks.py +18 -0
  126. package/src/devcouncil/live/transcripts.py +141 -0
  127. package/src/devcouncil/llm/__init__.py +1 -1
  128. package/src/devcouncil/llm/cache.py +42 -38
  129. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  130. package/src/devcouncil/llm/provider.py +627 -125
  131. package/src/devcouncil/llm/router.py +303 -118
  132. package/src/devcouncil/optimization/__init__.py +1 -0
  133. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  134. package/src/devcouncil/planning/__init__.py +1 -1
  135. package/src/devcouncil/planning/arbiter_service.py +57 -57
  136. package/src/devcouncil/planning/correction_manifest.py +303 -0
  137. package/src/devcouncil/planning/critique_service.py +71 -66
  138. package/src/devcouncil/planning/plan_service.py +60 -46
  139. package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
  140. package/src/devcouncil/planning/repair_service.py +39 -39
  141. package/src/devcouncil/planning/spec_service.py +70 -44
  142. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  143. package/src/devcouncil/repo/gitignore.py +123 -0
  144. package/src/devcouncil/repo/sca.py +374 -0
  145. package/src/devcouncil/reporting/github_check.py +32 -32
  146. package/src/devcouncil/reporting/json_report.py +30 -17
  147. package/src/devcouncil/reporting/markdown_report.py +83 -46
  148. package/src/devcouncil/reporting/report_builder.py +14 -14
  149. package/src/devcouncil/skills/__init__.py +19 -0
  150. package/src/devcouncil/skills/library/README.md +46 -0
  151. package/src/devcouncil/skills/library/ai-training.md +50 -0
  152. package/src/devcouncil/skills/library/android.md +50 -0
  153. package/src/devcouncil/skills/library/backend.md +52 -0
  154. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  155. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  156. package/src/devcouncil/skills/library/desktop.md +46 -0
  157. package/src/devcouncil/skills/library/devops.md +48 -0
  158. package/src/devcouncil/skills/library/game-dev.md +46 -0
  159. package/src/devcouncil/skills/library/ios.md +48 -0
  160. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  161. package/src/devcouncil/skills/library/security.md +48 -0
  162. package/src/devcouncil/skills/library/systems.md +48 -0
  163. package/src/devcouncil/skills/library/web.md +47 -0
  164. package/src/devcouncil/skills/library/windows.md +47 -0
  165. package/src/devcouncil/skills/registry.py +330 -0
  166. package/src/devcouncil/storage/db.py +147 -66
  167. package/src/devcouncil/storage/models.py +204 -83
  168. package/src/devcouncil/storage/native.py +557 -0
  169. package/src/devcouncil/storage/repositories.py +388 -249
  170. package/src/devcouncil/telemetry/cost.py +140 -34
  171. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  172. package/src/devcouncil/telemetry/pricing.py +28 -0
  173. package/src/devcouncil/telemetry/traces.py +62 -7
  174. package/src/devcouncil/telemetry/tracker.py +52 -49
  175. package/src/devcouncil/ui/__init__.py +1 -0
  176. package/src/devcouncil/ui/dashboard.py +423 -0
  177. package/src/devcouncil/utils/__init__.py +1 -1
  178. package/src/devcouncil/utils/redaction.py +147 -141
  179. package/src/devcouncil/utils/subprocess_env.py +69 -0
  180. package/src/devcouncil/verification/__init__.py +1 -1
  181. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  182. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  183. package/src/devcouncil/verification/diff_coverage.py +353 -0
  184. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  185. package/src/devcouncil/verification/next_actions.py +189 -0
  186. package/src/devcouncil/verification/sandbox.py +178 -0
  187. package/src/devcouncil/verification/test_resolver.py +91 -0
  188. package/src/devcouncil/verification/verifier.py +1342 -307
  189. package/uv.lock +205 -64
  190. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -0,0 +1,209 @@
1
+ """`dev check` — a one-shot audit of the current uncommitted changes.
2
+
3
+ The lowest-friction entry point: no planning, no task graph. You let a coding
4
+ agent change your repo, then `dev check` tells you what is out of scope, what
5
+ edge cases look unhandled, what's risky, and whether any secrets leaked —
6
+ grounded in the real diff. With ``--goal`` it also compiles acceptance checks
7
+ from the stated intent and runs them as evidence.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ import subprocess
15
+ from pathlib import Path
16
+
17
+ import typer
18
+ from rich.console import Console
19
+ from rich.table import Table
20
+
21
+ from devcouncil.app.config import get_api_key, load_config
22
+ from devcouncil.cli.commands.init import initialize_project
23
+ from devcouncil.domain.task import Task
24
+ from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
25
+ from devcouncil.integrations.github_intent import resolve_goal_intent
26
+ from devcouncil.llm.provider import ProviderRequestError, create_provider, validate_model_provider
27
+ from devcouncil.llm.router import ModelRouter, StructuredOutputError
28
+ from devcouncil.verification.ad_hoc_check import AdHocCheckResult, run_working_tree_check
29
+ from devcouncil.verification.implementation_reviewer import ImplementationReviewer
30
+ from devcouncil.verification.verifier import Verifier
31
+
32
+ console = Console()
33
+
34
+
35
+ def _diff(root: Path, base: str | None) -> str:
36
+ if not base:
37
+ return Verifier(root).get_diff()
38
+ try:
39
+ return subprocess.check_output(
40
+ ["git", "diff", base, "--"], cwd=root, text=True, encoding="utf-8", errors="replace"
41
+ )
42
+ except Exception:
43
+ return ""
44
+
45
+
46
+ def check(
47
+ goal: str | None = typer.Option(None, "--goal", "-g", help="What the change was meant to do — sharpens the review and enables acceptance checks. Also accepts a GitHub issue/PR reference (#142, owner/repo#142, or a github.com URL)."),
48
+ base: str | None = typer.Option(None, "--base", help="Diff against this git ref instead of the uncommitted working tree."),
49
+ test_commands: list[str] | None = typer.Option(None, "--test", "-t", help="A verification command proving the change works (repeatable). Switches to the deterministic evidence gate."),
50
+ verify: bool = typer.Option(False, "--verify", help="Run the deterministic evidence gate (orphan-diff, acceptance evidence, diff↔coverage, next actions) instead of the LLM audit. No provider keys needed."),
51
+ enforce_coverage: bool = typer.Option(False, "--enforce-coverage", help="Evidence gate: block when the tests do not exercise the changed lines."),
52
+ min_coverage: float = typer.Option(0.0, "--min-coverage", help="Evidence gate: minimum fraction of changed lines that must be exercised (implies --enforce-coverage)."),
53
+ json_format: bool = typer.Option(False, "--json", help="Machine-readable output."),
54
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
55
+ ):
56
+ """Audit the current changes — scope, risks, missing edge cases, secrets — no planning required."""
57
+ root = project_root.expanduser().resolve()
58
+ initialize_project(root, quiet=True)
59
+
60
+ # A --goal of "#142" or a GitHub issue/PR URL is a reference, not a spec —
61
+ # expand it into the issue/PR title + body so acceptance checks and the review
62
+ # are grounded in the real intent (same behavior as `dev go`).
63
+ if goal:
64
+ expanded_goal, intent_note = resolve_goal_intent(goal, root)
65
+ if intent_note and not json_format:
66
+ console.print(f"[dim]{intent_note}[/dim]")
67
+ goal = expanded_goal
68
+
69
+ # Evidence-gate mode: deterministic verification of the working-tree diff against an
70
+ # inline requirement (--goal), with the diff↔coverage gate and the typed next-actions
71
+ # contract. Provider-key-free — this is the lite path that lets you taste the gate.
72
+ if verify or test_commands:
73
+ result = run_working_tree_check(
74
+ root,
75
+ goal,
76
+ test_commands=list(test_commands or []),
77
+ enforce_coverage=enforce_coverage,
78
+ min_ratio=min_coverage,
79
+ )
80
+ if json_format:
81
+ typer.echo(json.dumps(result.to_dict(), indent=2))
82
+ else:
83
+ _render_gate(result)
84
+ raise typer.Exit(code=0 if result.passed else 1)
85
+
86
+ verifier = Verifier(root)
87
+
88
+ diff = _diff(root, base)
89
+ if not diff.strip():
90
+ msg = "No changes to check (clean working tree)."
91
+ typer.echo(json.dumps({"ok": True, "message": msg}) if json_format else msg)
92
+ return
93
+
94
+ changed_files = verifier.get_changed_files()
95
+ secret_gaps = verifier.secret_scanner.scan_diff(diff, "check")
96
+
97
+ # Blast radius: what do the changed files ripple into? Surfaced from the
98
+ # structural graph when the code-review-graph integration is enabled, so a
99
+ # reviewer sees the impact (and which tests to run) before approving.
100
+ graph_context = CodeReviewGraphAdapter(root).get_context(changed_files)
101
+
102
+ findings = []
103
+ review_note = None
104
+ try:
105
+ config = load_config(root)
106
+ validate_model_provider(config.models.provider)
107
+ api_key = get_api_key(config.models.provider, root)
108
+ provider = create_provider(config.models.provider, api_key, project_root=root)
109
+ role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
110
+ router = ModelRouter(provider, role_config, project_root=root)
111
+ synthetic = Task(
112
+ id="CHECK",
113
+ title="Ad-hoc change review",
114
+ description=(
115
+ goal
116
+ or "Review these changes for correctness, missing edge cases, error handling, "
117
+ "risky shortcuts, and scope creep."
118
+ ),
119
+ )
120
+ review = asyncio.run(ImplementationReviewer(router).review_changes(synthetic, [], diff))
121
+ findings = review.findings
122
+ except (ProviderRequestError, StructuredOutputError) as exc:
123
+ review_note = f"LLM review unavailable: {exc}"
124
+ except Exception as exc: # pragma: no cover - best effort
125
+ review_note = f"LLM review unavailable: {exc}"
126
+
127
+ if json_format:
128
+ typer.echo(json.dumps({
129
+ "ok": not secret_gaps,
130
+ "changed_files": changed_files,
131
+ "secret_findings": [g.model_dump() for g in secret_gaps],
132
+ "review_findings": [g.model_dump() for g in findings],
133
+ "review_note": review_note,
134
+ "blast_radius": {
135
+ "available": graph_context.available,
136
+ "impacted_files": graph_context.impacted_files,
137
+ "related_tests": graph_context.related_tests,
138
+ },
139
+ }, indent=2))
140
+ return
141
+
142
+ console.print(f"[bold]Changed files ({len(changed_files)}):[/bold] " + ", ".join(changed_files[:20]) or "(none)")
143
+ if secret_gaps:
144
+ console.print(f"\n[red bold]⚠ Possible secrets in the diff ({len(secret_gaps)}):[/red bold]")
145
+ for g in secret_gaps[:10]:
146
+ console.print(f" - {g.description[:100]}")
147
+ if findings:
148
+ console.print(f"\n[bold]Review findings ({len(findings)}):[/bold]")
149
+ for f in findings[:15]:
150
+ sev = getattr(f, "severity", "info")
151
+ colour = {"critical": "red", "high": "red", "medium": "yellow"}.get(sev, "white")
152
+ console.print(f" - [{colour}]{sev}[/{colour}]: {f.description[:140]}")
153
+ if graph_context.available and (graph_context.impacted_files or graph_context.related_tests):
154
+ console.print("\n[bold]Blast radius[/bold] [dim](from the structural graph)[/dim]:")
155
+ if graph_context.impacted_files:
156
+ console.print(f" [cyan]Impacted files ({len(graph_context.impacted_files)}):[/cyan] "
157
+ + ", ".join(graph_context.impacted_files[:15]))
158
+ if graph_context.related_tests:
159
+ console.print(f" [cyan]Related tests ({len(graph_context.related_tests)}):[/cyan] "
160
+ + ", ".join(graph_context.related_tests[:15]))
161
+ if not secret_gaps and not findings:
162
+ console.print("\n[green]No secrets or review concerns found in the diff.[/green]")
163
+ if review_note:
164
+ console.print(f"\n[dim]{review_note}[/dim]")
165
+ console.print(
166
+ "\n[dim]Tip: `dev check --goal \"what this change should do\"` sharpens the review.[/dim]"
167
+ )
168
+ console.print(
169
+ "[dim]Tip: `dev check --verify --test \"<cmd>\"` runs the deterministic evidence gate "
170
+ "(no provider keys).[/dim]"
171
+ )
172
+
173
+
174
+ def _render_gate(result: AdHocCheckResult) -> None:
175
+ """Render the deterministic evidence-gate result for humans."""
176
+ if result.reason == "no_changes":
177
+ console.print("[yellow]No working-tree changes to verify. Make a change first, then re-run.[/yellow]")
178
+ return
179
+
180
+ console.print(f"[bold]Checking:[/bold] {result.requirement}")
181
+ console.print(f"[dim]{len(result.changed_files)} changed file(s) in scope.[/dim]")
182
+ if result.diff_coverage and result.diff_coverage.measured:
183
+ console.print(f"[dim]Diff coverage: {result.diff_coverage.summary}[/dim]")
184
+
185
+ if not result.gaps:
186
+ console.print("\n[green]Verified: the change is backed by passing evidence.[/green]")
187
+ return
188
+
189
+ table = Table(title="Findings")
190
+ table.add_column("Type", style="cyan")
191
+ table.add_column("Severity", style="magenta")
192
+ table.add_column("Finding", style="white")
193
+ table.add_column("Blocking", style="red")
194
+ for gap in result.gaps[:20]:
195
+ table.add_row(gap.gap_type, gap.severity, gap.description, "YES" if gap.blocking else "no")
196
+ console.print(table)
197
+
198
+ if result.next_actions:
199
+ console.print("\n[bold]Next actions:[/bold]")
200
+ for action in result.next_actions:
201
+ location = ""
202
+ if action.file:
203
+ location = f" [dim]({action.file}{':' + str(action.line) if action.line else ''})[/dim]"
204
+ console.print(f" • [[cyan]{action.category}[/cyan]] {action.action}{location}")
205
+
206
+ if result.passed:
207
+ console.print("\n[green]Verified with non-blocking signals only.[/green]")
208
+ else:
209
+ console.print("\n[red]Not verified: blocking gaps must be resolved.[/red]")
@@ -1,54 +1,115 @@
1
- import typer
2
- import yaml
3
- from pathlib import Path
4
- from rich.console import Console
5
- from devcouncil.app.config import load_config
6
-
7
- app = typer.Typer(help="Manage DevCouncil configuration")
8
- console = Console()
9
-
10
- @app.command("models")
11
- def models(
12
- role: str = typer.Option(None, "--role", "-r", help="Specific role to show/edit"),
13
- model: str = typer.Option(None, "--model", "-m", help="New model string to set for the role")
14
- ):
15
- """View or edit model role configuration."""
16
- try:
17
- load_config(Path("."))
18
- except FileNotFoundError as e:
19
- console.print(f"[red]{e}[/red]")
20
- return
21
-
22
- config_path = Path(".devcouncil/config.yaml")
23
-
24
- with open(config_path) as f:
25
- raw_config = yaml.safe_load(f) or {}
26
-
27
- if not role:
28
- console.print("[bold]Model Configuration[/bold]")
29
- for r, m in raw_config.get("models", {}).get("roles", {}).items():
30
- console.print(f" [cyan]{r}[/cyan]: {m.get('model')}")
31
- return
32
-
33
- if not model:
34
- m = raw_config.get("models", {}).get("roles", {}).get(role)
35
- if m:
36
- console.print(f"[cyan]{role}[/cyan]: {m.get('model')}")
37
- else:
38
- console.print(f"[red]Role '{role}' not found.[/red]")
39
- return
40
-
41
- if "models" not in raw_config:
42
- raw_config["models"] = {"roles": {}}
43
- if "roles" not in raw_config["models"]:
44
- raw_config["models"]["roles"] = {}
45
-
46
- if role not in raw_config["models"]["roles"]:
47
- raw_config["models"]["roles"][role] = {}
48
-
49
- raw_config["models"]["roles"][role]["model"] = model
50
-
51
- with open(config_path, "w") as f:
52
- yaml.dump(raw_config, f, default_flow_style=False)
53
-
54
- console.print(f"[green]Updated '{role}' to use model '{model}'[/green]")
1
+ import typer
2
+ import yaml
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+ from devcouncil.cli.commands.init import parse_role_model_overrides
6
+ from devcouncil.app.config import load_config
7
+ from devcouncil.llm.provider import (
8
+ SUPPORTED_MODEL_PROVIDERS,
9
+ apply_provider_default_role_models,
10
+ build_role_model_config,
11
+ validate_model_provider,
12
+ )
13
+
14
+ app = typer.Typer(help="Manage DevCouncil configuration")
15
+ console = Console()
16
+
17
+ @app.command("models")
18
+ def models(
19
+ role: str = typer.Option(None, "--role", "-r", help="Specific role to show/edit"),
20
+ model: str = typer.Option(None, "--model", "-m", help="New model string to set for the role, or every role when --role is omitted."),
21
+ provider: str = typer.Option(None, "--provider", help="Set the model provider."),
22
+ role_model: list[str] | None = typer.Option(
23
+ None,
24
+ "--role-model",
25
+ help="Per-role model override in ROLE=MODEL form. Can be repeated.",
26
+ ),
27
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
28
+ ):
29
+ """View or edit model role configuration."""
30
+ root = project_root.expanduser().resolve()
31
+ try:
32
+ load_config(root)
33
+ except FileNotFoundError as e:
34
+ console.print(f"[red]{e}[/red]")
35
+ return
36
+
37
+ config_path = root / ".devcouncil" / "config.yaml"
38
+
39
+ with open(config_path) as f:
40
+ raw_config = yaml.safe_load(f) or {}
41
+
42
+ try:
43
+ role_models = parse_role_model_overrides(role_model)
44
+ except ValueError as e:
45
+ console.print(f"[red]{e}[/red]")
46
+ raise typer.Exit(code=2) from e
47
+
48
+ if provider:
49
+ try:
50
+ normalized_provider = validate_model_provider(provider)
51
+ except ValueError as e:
52
+ console.print(f"[red]{e}[/red]")
53
+ raise typer.Exit(code=2) from e
54
+ raw_config.setdefault("models", {})
55
+ previous = raw_config["models"].get("provider", "openrouter")
56
+ raw_config["models"]["provider"] = normalized_provider
57
+ updated_role_defaults = apply_provider_default_role_models(raw_config, previous, normalized_provider)
58
+ with open(config_path, "w") as f:
59
+ yaml.dump(raw_config, f, default_flow_style=False)
60
+ if previous == normalized_provider:
61
+ console.print(f"[green]Model provider remains '{normalized_provider}'.[/green]")
62
+ else:
63
+ console.print(f"[green]Updated model provider from '{previous}' to '{normalized_provider}'.[/green]")
64
+ if updated_role_defaults:
65
+ console.print(f"[green]Updated default role models for '{normalized_provider}'.[/green]")
66
+ if not model and not role_models:
67
+ return
68
+
69
+ if not role and (model or role_models):
70
+ raw_config.setdefault("models", {})
71
+ configured_provider = validate_model_provider(raw_config["models"].get("provider", "openrouter"))
72
+ raw_config["models"]["roles"] = build_role_model_config(
73
+ configured_provider,
74
+ model=model,
75
+ role_models=role_models,
76
+ )
77
+ with open(config_path, "w") as f:
78
+ yaml.dump(raw_config, f, default_flow_style=False)
79
+ if model:
80
+ console.print(f"[green]Updated all model roles to use '{model}'.[/green]")
81
+ for selected_role, selected_model in role_models.items():
82
+ console.print(f"[green]Updated '{selected_role}' to use model '{selected_model}'.[/green]")
83
+ return
84
+
85
+ if not role:
86
+ console.print("[bold]Model Configuration[/bold]")
87
+ configured_provider = raw_config.get("models", {}).get("provider", "openrouter")
88
+ supported = ", ".join(SUPPORTED_MODEL_PROVIDERS)
89
+ console.print(f" [cyan]provider[/cyan]: {configured_provider} (supported: {supported})")
90
+ for r, m in raw_config.get("models", {}).get("roles", {}).items():
91
+ console.print(f" [cyan]{r}[/cyan]: {m.get('model')}")
92
+ return
93
+
94
+ if not model:
95
+ m = raw_config.get("models", {}).get("roles", {}).get(role)
96
+ if m:
97
+ console.print(f"[cyan]{role}[/cyan]: {m.get('model')}")
98
+ else:
99
+ console.print(f"[red]Role '{role}' not found.[/red]")
100
+ return
101
+
102
+ if "models" not in raw_config:
103
+ raw_config["models"] = {"roles": {}}
104
+ if "roles" not in raw_config["models"]:
105
+ raw_config["models"]["roles"] = {}
106
+
107
+ if role not in raw_config["models"]["roles"]:
108
+ raw_config["models"]["roles"][role] = {}
109
+
110
+ raw_config["models"]["roles"][role]["model"] = model
111
+
112
+ with open(config_path, "w") as f:
113
+ yaml.dump(raw_config, f, default_flow_style=False)
114
+
115
+ console.print(f"[green]Updated '{role}' to use model '{model}'[/green]")
@@ -0,0 +1,57 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+
8
+ from devcouncil.telemetry.cost import group_cost
9
+
10
+ app = typer.Typer(help="Inspect DevCouncil model-call cost, grouped by task and run.")
11
+ console = Console()
12
+
13
+
14
+ @app.command("show")
15
+ def show(
16
+ json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
17
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
18
+ ):
19
+ """Report estimated model-call cost grouped by task_id and run_id.
20
+
21
+ Reads the local ``model_calls.jsonl`` ledger only — fully offline. Records
22
+ written before per-task attribution (or made without a task/run context) are
23
+ grouped under ``(unattributed)``.
24
+ """
25
+ root = project_root.expanduser().resolve()
26
+ summary = group_cost(root)
27
+
28
+ if json_format:
29
+ typer.echo(json.dumps(summary, indent=2))
30
+ return
31
+
32
+ console.print(
33
+ f"[bold]Total Cost:[/bold] ${summary['total_cost']:.4f} "
34
+ f"across {summary['total_calls']} model call(s)"
35
+ )
36
+
37
+ def _render(title: str, groups: dict) -> None:
38
+ if not groups:
39
+ return
40
+ table = Table(title=title)
41
+ table.add_column("Group", style="cyan")
42
+ table.add_column("Cost ($)", justify="right")
43
+ table.add_column("Calls", justify="right")
44
+ table.add_column("Prompt", justify="right")
45
+ table.add_column("Completion", justify="right")
46
+ for name, stats in sorted(groups.items(), key=lambda kv: kv[1]["cost"], reverse=True):
47
+ table.add_row(
48
+ name,
49
+ f"{stats['cost']:.4f}",
50
+ str(stats["calls"]),
51
+ str(stats["prompt_tokens"]),
52
+ str(stats["completion_tokens"]),
53
+ )
54
+ console.print(table)
55
+
56
+ _render("Cost by Task", summary["by_task"])
57
+ _render("Cost by Run", summary["by_run"])
@@ -0,0 +1,31 @@
1
+ from pathlib import Path
2
+ import webbrowser
3
+
4
+ import typer
5
+ from rich.console import Console
6
+
7
+ from devcouncil.cli.commands.init import initialize_project
8
+ from devcouncil.ui.dashboard import run_dashboard
9
+
10
+ app = typer.Typer(help="Serve the live DevCouncil dashboard.")
11
+ console = Console()
12
+
13
+
14
+ @app.callback(invoke_without_command=True)
15
+ def dashboard(
16
+ ctx: typer.Context,
17
+ host: str = typer.Option("127.0.0.1", "--host", help="Dashboard bind host."),
18
+ port: int = typer.Option(8765, "--port", help="Dashboard bind port."),
19
+ open_browser: bool = typer.Option(False, "--open", help="Open the dashboard URL in the default browser before serving."),
20
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
21
+ ):
22
+ """Serve a local live dashboard with project status, tasks, coverage, and traces."""
23
+ if ctx.invoked_subcommand is not None:
24
+ return
25
+ root = project_root.expanduser().resolve()
26
+ initialize_project(root, quiet=True)
27
+ url = f"http://{host}:{port}"
28
+ console.print(f"Serving DevCouncil dashboard at {url}")
29
+ if open_browser:
30
+ webbrowser.open(url)
31
+ run_dashboard(root, host=host, port=port)