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.
- package/LICENSE +201 -201
- package/README.md +197 -494
- package/package.json +9 -2
- package/pyproject.toml +62 -27
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +297 -108
- package/src/devcouncil/app/errors.py +23 -23
- package/src/devcouncil/app/events.py +44 -44
- package/src/devcouncil/app/orchestrator.py +67 -67
- package/src/devcouncil/app/project_status.py +29 -0
- package/src/devcouncil/app/run_context.py +39 -39
- package/src/devcouncil/app/state_machine.py +108 -108
- package/src/devcouncil/artifacts/__init__.py +1 -1
- package/src/devcouncil/artifacts/coverage.py +96 -96
- package/src/devcouncil/artifacts/graph.py +163 -143
- package/src/devcouncil/artifacts/migrations.py +20 -20
- package/src/devcouncil/artifacts/schemas.py +23 -23
- package/src/devcouncil/artifacts/serializer.py +21 -21
- package/src/devcouncil/artifacts/validators.py +27 -27
- package/src/devcouncil/assets/__init__.py +1 -0
- package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/commands/agents.py +292 -0
- package/src/devcouncil/cli/commands/artifacts.py +54 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/check.py +209 -0
- package/src/devcouncil/cli/commands/config.py +115 -54
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +31 -0
- package/src/devcouncil/cli/commands/doctor.py +291 -47
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +656 -0
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +209 -33
- package/src/devcouncil/cli/commands/init.py +204 -57
- package/src/devcouncil/cli/commands/integrate.py +1171 -76
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +96 -22
- package/src/devcouncil/cli/commands/plan.py +422 -210
- package/src/devcouncil/cli/commands/prompt.py +48 -34
- package/src/devcouncil/cli/commands/repair.py +89 -69
- package/src/devcouncil/cli/commands/report.py +120 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
- package/src/devcouncil/cli/commands/rollback.py +55 -54
- package/src/devcouncil/cli/commands/run.py +285 -220
- package/src/devcouncil/cli/commands/runs.py +223 -0
- package/src/devcouncil/cli/commands/scaffold.py +32 -0
- package/src/devcouncil/cli/commands/semantic.py +47 -0
- package/src/devcouncil/cli/commands/setup.py +300 -20
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/skills.py +88 -0
- package/src/devcouncil/cli/commands/status.py +141 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +49 -4
- package/src/devcouncil/cli/commands/verify.py +293 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +92 -25
- package/src/devcouncil/council/prompts/arbiter.md +19 -19
- package/src/devcouncil/council/prompts/critic_a.md +10 -10
- package/src/devcouncil/council/prompts/critic_b.md +10 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
- package/src/devcouncil/council/prompts/planner_a.md +16 -16
- package/src/devcouncil/council/prompts/planner_b.md +16 -16
- package/src/devcouncil/council/prompts/rebuttal.md +10 -10
- package/src/devcouncil/council/prompts/spec_writer.md +12 -12
- package/src/devcouncil/domain/assumption.py +17 -17
- package/src/devcouncil/domain/critique.py +32 -32
- package/src/devcouncil/domain/evidence.py +47 -27
- package/src/devcouncil/domain/gap.py +52 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +55 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +186 -77
- package/src/devcouncil/execution/patch.py +77 -28
- package/src/devcouncil/execution/permissions.py +52 -59
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +650 -38
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +68 -64
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +736 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +186 -85
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +52 -45
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
- package/src/devcouncil/gating/policy.py +315 -167
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/__init__.py +1 -1
- package/src/devcouncil/indexing/ast_matcher.py +168 -0
- package/src/devcouncil/indexing/graph_index.py +48 -48
- package/src/devcouncil/indexing/lsp.py +161 -0
- package/src/devcouncil/indexing/repo_mapper.py +1455 -204
- package/src/devcouncil/indexing/semantic_index.py +205 -0
- package/src/devcouncil/integrations/actions.py +146 -0
- package/src/devcouncil/integrations/check.py +423 -0
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/github_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +62 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +2072 -96
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +349 -0
- package/src/devcouncil/live/models.py +63 -0
- package/src/devcouncil/live/repair_prompt.py +83 -0
- package/src/devcouncil/live/reviewer.py +70 -0
- package/src/devcouncil/live/signals.py +135 -0
- package/src/devcouncil/live/summary.py +34 -0
- package/src/devcouncil/live/tasks.py +18 -0
- package/src/devcouncil/live/transcripts.py +141 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +42 -38
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +627 -125
- package/src/devcouncil/llm/router.py +303 -118
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +71 -66
- package/src/devcouncil/planning/plan_service.py +60 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +70 -44
- package/src/devcouncil/repo/ci_scaffold.py +157 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +374 -0
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +30 -17
- package/src/devcouncil/reporting/markdown_report.py +83 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/skills/__init__.py +19 -0
- package/src/devcouncil/skills/library/README.md +46 -0
- package/src/devcouncil/skills/library/ai-training.md +50 -0
- package/src/devcouncil/skills/library/android.md +50 -0
- package/src/devcouncil/skills/library/backend.md +52 -0
- package/src/devcouncil/skills/library/core-engineering.md +95 -0
- package/src/devcouncil/skills/library/data-engineering.md +47 -0
- package/src/devcouncil/skills/library/desktop.md +46 -0
- package/src/devcouncil/skills/library/devops.md +48 -0
- package/src/devcouncil/skills/library/game-dev.md +46 -0
- package/src/devcouncil/skills/library/ios.md +48 -0
- package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
- package/src/devcouncil/skills/library/security.md +48 -0
- package/src/devcouncil/skills/library/systems.md +48 -0
- package/src/devcouncil/skills/library/web.md +47 -0
- package/src/devcouncil/skills/library/windows.md +47 -0
- package/src/devcouncil/skills/registry.py +330 -0
- package/src/devcouncil/storage/db.py +147 -66
- package/src/devcouncil/storage/models.py +204 -83
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +388 -249
- package/src/devcouncil/telemetry/cost.py +140 -34
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +52 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +423 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +147 -141
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/acceptance_compiler.py +125 -0
- package/src/devcouncil/verification/ad_hoc_check.py +129 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +178 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1342 -307
- package/uv.lock +205 -64
- 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.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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)
|