devcouncil 0.1.1 → 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 (129) hide show
  1. package/README.md +190 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +167 -5
  5. package/src/devcouncil/artifacts/graph.py +23 -3
  6. package/src/devcouncil/assets/__init__.py +1 -0
  7. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  8. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  9. package/src/devcouncil/cli/commands/agents.py +292 -0
  10. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  11. package/src/devcouncil/cli/commands/check.py +209 -0
  12. package/src/devcouncil/cli/commands/config.py +43 -4
  13. package/src/devcouncil/cli/commands/cost.py +57 -0
  14. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  15. package/src/devcouncil/cli/commands/doctor.py +221 -21
  16. package/src/devcouncil/cli/commands/evidence.py +48 -0
  17. package/src/devcouncil/cli/commands/go.py +452 -33
  18. package/src/devcouncil/cli/commands/handoff.py +69 -0
  19. package/src/devcouncil/cli/commands/hook.py +124 -15
  20. package/src/devcouncil/cli/commands/init.py +154 -18
  21. package/src/devcouncil/cli/commands/integrate.py +894 -105
  22. package/src/devcouncil/cli/commands/map.py +80 -10
  23. package/src/devcouncil/cli/commands/plan.py +212 -51
  24. package/src/devcouncil/cli/commands/prompt.py +18 -7
  25. package/src/devcouncil/cli/commands/repair.py +40 -23
  26. package/src/devcouncil/cli/commands/report.py +8 -0
  27. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  28. package/src/devcouncil/cli/commands/rollback.py +27 -28
  29. package/src/devcouncil/cli/commands/run.py +69 -49
  30. package/src/devcouncil/cli/commands/runs.py +223 -0
  31. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  32. package/src/devcouncil/cli/commands/semantic.py +47 -0
  33. package/src/devcouncil/cli/commands/setup.py +145 -6
  34. package/src/devcouncil/cli/commands/shell.py +73 -0
  35. package/src/devcouncil/cli/commands/skills.py +88 -0
  36. package/src/devcouncil/cli/commands/status.py +25 -1
  37. package/src/devcouncil/cli/commands/trace.py +47 -3
  38. package/src/devcouncil/cli/commands/verify.py +138 -3
  39. package/src/devcouncil/cli/commands/watch.py +9 -9
  40. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  41. package/src/devcouncil/cli/main.py +56 -7
  42. package/src/devcouncil/domain/evidence.py +22 -2
  43. package/src/devcouncil/domain/gap.py +27 -1
  44. package/src/devcouncil/domain/task.py +31 -2
  45. package/src/devcouncil/execution/checkpoints.py +246 -0
  46. package/src/devcouncil/execution/context_builder.py +1 -1
  47. package/src/devcouncil/execution/fs_watcher.py +180 -0
  48. package/src/devcouncil/execution/handoff.py +102 -0
  49. package/src/devcouncil/execution/hook_policy.py +162 -74
  50. package/src/devcouncil/execution/patch.py +59 -10
  51. package/src/devcouncil/execution/permissions.py +17 -24
  52. package/src/devcouncil/execution/policy_engine.py +343 -0
  53. package/src/devcouncil/execution/prompt_builder.py +633 -21
  54. package/src/devcouncil/execution/shell_session.py +225 -0
  55. package/src/devcouncil/execution/task_runner.py +6 -2
  56. package/src/devcouncil/executors/agent_registry.py +575 -0
  57. package/src/devcouncil/executors/coding_cli.py +663 -39
  58. package/src/devcouncil/executors/native/agent.py +121 -20
  59. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  60. package/src/devcouncil/gating/checks/secret_scan_check.py +40 -21
  61. package/src/devcouncil/gating/policy.py +158 -10
  62. package/src/devcouncil/hardware.py +184 -0
  63. package/src/devcouncil/indexing/ast_matcher.py +1 -1
  64. package/src/devcouncil/indexing/lsp.py +45 -4
  65. package/src/devcouncil/indexing/repo_mapper.py +1256 -9
  66. package/src/devcouncil/indexing/semantic_index.py +205 -0
  67. package/src/devcouncil/integrations/actions.py +146 -0
  68. package/src/devcouncil/integrations/check.py +423 -0
  69. package/src/devcouncil/integrations/github_intent.py +142 -0
  70. package/src/devcouncil/integrations/gitnexus.py +35 -0
  71. package/src/devcouncil/integrations/mcp/server.py +1552 -29
  72. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  73. package/src/devcouncil/live/cards.py +161 -19
  74. package/src/devcouncil/live/signals.py +2 -2
  75. package/src/devcouncil/live/transcripts.py +9 -6
  76. package/src/devcouncil/llm/cache.py +10 -6
  77. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  78. package/src/devcouncil/llm/provider.py +515 -34
  79. package/src/devcouncil/llm/router.py +231 -46
  80. package/src/devcouncil/optimization/__init__.py +1 -0
  81. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  82. package/src/devcouncil/planning/correction_manifest.py +303 -0
  83. package/src/devcouncil/planning/critique_service.py +7 -2
  84. package/src/devcouncil/planning/plan_service.py +17 -3
  85. package/src/devcouncil/planning/prompt_enhancer_service.py +82 -1
  86. package/src/devcouncil/planning/spec_service.py +27 -1
  87. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  88. package/src/devcouncil/repo/gitignore.py +123 -0
  89. package/src/devcouncil/repo/sca.py +374 -0
  90. package/src/devcouncil/reporting/json_report.py +11 -1
  91. package/src/devcouncil/reporting/markdown_report.py +15 -0
  92. package/src/devcouncil/skills/__init__.py +19 -0
  93. package/src/devcouncil/skills/library/README.md +46 -0
  94. package/src/devcouncil/skills/library/ai-training.md +50 -0
  95. package/src/devcouncil/skills/library/android.md +50 -0
  96. package/src/devcouncil/skills/library/backend.md +52 -0
  97. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  98. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  99. package/src/devcouncil/skills/library/desktop.md +46 -0
  100. package/src/devcouncil/skills/library/devops.md +48 -0
  101. package/src/devcouncil/skills/library/game-dev.md +46 -0
  102. package/src/devcouncil/skills/library/ios.md +48 -0
  103. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  104. package/src/devcouncil/skills/library/security.md +48 -0
  105. package/src/devcouncil/skills/library/systems.md +48 -0
  106. package/src/devcouncil/skills/library/web.md +47 -0
  107. package/src/devcouncil/skills/library/windows.md +47 -0
  108. package/src/devcouncil/skills/registry.py +330 -0
  109. package/src/devcouncil/storage/db.py +83 -2
  110. package/src/devcouncil/storage/models.py +121 -0
  111. package/src/devcouncil/storage/native.py +557 -0
  112. package/src/devcouncil/storage/repositories.py +137 -75
  113. package/src/devcouncil/telemetry/cost.py +123 -17
  114. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  115. package/src/devcouncil/telemetry/pricing.py +28 -0
  116. package/src/devcouncil/telemetry/traces.py +62 -7
  117. package/src/devcouncil/telemetry/tracker.py +12 -9
  118. package/src/devcouncil/ui/dashboard.py +324 -23
  119. package/src/devcouncil/utils/redaction.py +9 -3
  120. package/src/devcouncil/utils/subprocess_env.py +69 -0
  121. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  122. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  123. package/src/devcouncil/verification/diff_coverage.py +353 -0
  124. package/src/devcouncil/verification/next_actions.py +189 -0
  125. package/src/devcouncil/verification/sandbox.py +178 -0
  126. package/src/devcouncil/verification/test_resolver.py +91 -0
  127. package/src/devcouncil/verification/verifier.py +1065 -47
  128. package/uv.lock +205 -64
  129. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -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"])
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ import webbrowser
2
3
 
3
4
  import typer
4
5
  from rich.console import Console
@@ -15,6 +16,7 @@ def dashboard(
15
16
  ctx: typer.Context,
16
17
  host: str = typer.Option("127.0.0.1", "--host", help="Dashboard bind host."),
17
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."),
18
20
  project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
19
21
  ):
20
22
  """Serve a local live dashboard with project status, tasks, coverage, and traces."""
@@ -22,5 +24,8 @@ def dashboard(
22
24
  return
23
25
  root = project_root.expanduser().resolve()
24
26
  initialize_project(root, quiet=True)
25
- console.print(f"Serving DevCouncil dashboard at http://{host}:{port}")
27
+ url = f"http://{host}:{port}"
28
+ console.print(f"Serving DevCouncil dashboard at {url}")
29
+ if open_browser:
30
+ webbrowser.open(url)
26
31
  run_dashboard(root, host=host, port=port)
@@ -6,12 +6,80 @@ from pathlib import Path
6
6
  from rich.console import Console
7
7
  from rich.table import Table
8
8
 
9
- from devcouncil.app.config import load_config, load_local_secrets, provider_api_key_env_var
9
+ from devcouncil.app.config import get_gcloud_access_token, load_config, load_local_secrets, provider_api_key_env_var
10
+ from devcouncil.executors.agent_registry import (
11
+ CODING_CLI_INTEGRATION_INFO,
12
+ CODING_CLI_PROBE_ORDER,
13
+ CODING_CLI_VERSION_COMMANDS,
14
+ detect_available_coding_cli,
15
+ resolve_automated_executor,
16
+ )
10
17
  from devcouncil.llm.provider import SUPPORTED_MODEL_PROVIDERS, validate_model_provider
11
18
 
12
19
  app = typer.Typer()
13
20
  console = Console()
14
21
 
22
+
23
+ def _probe_ollama(base_url: str) -> tuple[bool, str]:
24
+ """Best-effort reachability check for a local Ollama server.
25
+
26
+ ``base_url`` carries the OpenAI-compatible ``/v1`` suffix; the native
27
+ ``/api/version`` endpoint lives one level up. Returns (reachable, detail);
28
+ any failure is reported, never raised.
29
+ """
30
+ root = base_url.rstrip("/")
31
+ if root.endswith("/v1"):
32
+ root = root[: -len("/v1")].rstrip("/")
33
+ try:
34
+ import httpx
35
+
36
+ resp = httpx.get(f"{root}/api/version", timeout=3.0)
37
+ if resp.status_code < 400:
38
+ version = ""
39
+ try:
40
+ version = (resp.json() or {}).get("version", "")
41
+ except Exception:
42
+ version = ""
43
+ return True, f"Reachable at {root}" + (f" (v{version})." if version else ".")
44
+ return False, f"Server at {root} returned HTTP {resp.status_code}."
45
+ except Exception:
46
+ return False, f"No Ollama server reachable at {root}."
47
+
48
+
49
+ def _probe_ollama_models(base_url: str) -> tuple[bool, set[str]]:
50
+ """Best-effort list of locally-pulled Ollama model tags via native ``/api/tags``.
51
+
52
+ Returns (queried_ok, names). ``names`` holds the reported tags (e.g.
53
+ ``qwen2.5-coder:7b``). Any failure returns (False, set()) and is never raised — a
54
+ green liveness probe with no pulled model is the most common "configured but doesn't
55
+ work" trap, so this turns it into an actionable row."""
56
+ root = base_url.rstrip("/")
57
+ if root.endswith("/v1"):
58
+ root = root[: -len("/v1")].rstrip("/")
59
+ try:
60
+ import httpx
61
+
62
+ resp = httpx.get(f"{root}/api/tags", timeout=3.0)
63
+ if resp.status_code >= 400:
64
+ return False, set()
65
+ models = (resp.json() or {}).get("models", []) or []
66
+ names = {str(m.get("name", "")).strip() for m in models if m.get("name")}
67
+ return True, {n for n in names if n}
68
+ except Exception:
69
+ return False, set()
70
+
71
+
72
+ def _ollama_model_present(model: str, pulled: set[str]) -> bool:
73
+ """Whether ``model`` is among the pulled tags, tolerant of the implicit ``:latest``
74
+ tag Ollama adds to untagged models."""
75
+ if model in pulled:
76
+ return True
77
+ base = model.split(":", 1)[0]
78
+ # configured "qwen2.5-coder" matches a pulled "qwen2.5-coder:latest", and vice versa.
79
+ candidates = {model, f"{model}:latest", base, f"{base}:latest"}
80
+ return any(tag in candidates or tag.split(":", 1)[0] == base and ":" not in model for tag in pulled)
81
+
82
+
15
83
  def render_doctor_check(project_root: Path = Path(".")):
16
84
  def _command_version(command: list[str]) -> str | None:
17
85
  executable = shutil.which(command[0])
@@ -66,33 +134,39 @@ def render_doctor_check(project_root: Path = Path(".")):
66
134
  else:
67
135
  table.add_row("ripgrep (rg)", "[yellow]Missing[/yellow]", "ripgrep is highly recommended for fast repo mapping.")
68
136
 
69
- # Check supported coding CLIs
70
- codex_ver = _command_version(["codex", "--version"])
71
- if codex_ver:
72
- table.add_row(
73
- "Codex CLI",
74
- "[green]OK[/green]",
75
- f"{codex_ver}. Setup: dev integrate codex --apply (or dev setup --integrate --apply).",
76
- )
77
- else:
78
- table.add_row(
79
- "Codex CLI",
80
- "[yellow]Missing[/yellow]",
81
- "Optional. Install Codex, then run 'dev integrate codex --apply' (or 'dev setup --integrate --apply').",
82
- )
137
+ # Check supported coding CLIs (driven by the agent registry, so new
138
+ # built-in agents show up here without doctor edits).
139
+ for name in CODING_CLI_PROBE_ORDER:
140
+ info = CODING_CLI_INTEGRATION_INFO.get(name)
141
+ if info is None:
142
+ continue
143
+ version = None
144
+ for probe in CODING_CLI_VERSION_COMMANDS.get(name, ()):
145
+ version = _command_version(list(probe))
146
+ if version:
147
+ break
148
+ if version:
149
+ table.add_row(info.label, "[green]OK[/green]", f"{version}. Setup: {info.notes}.")
150
+ else:
151
+ table.add_row(
152
+ info.label,
153
+ "[yellow]Missing[/yellow]",
154
+ f"Optional. Install {info.label}, then use: {info.notes}.",
155
+ )
83
156
 
84
- gemini_ver = _command_version(["gemini", "--version"])
85
- if gemini_ver:
157
+ detected = detect_available_coding_cli(project_root)
158
+ resolved = resolve_automated_executor(project_root, None)
159
+ if detected:
86
160
  table.add_row(
87
- "Gemini CLI",
161
+ "Recommended coding CLI",
88
162
  "[green]OK[/green]",
89
- f"{gemini_ver}. Setup: dev integrate gemini --apply (or dev setup --integrate --apply).",
163
+ f"Use --executor {resolved} for dev go / dev run (detected on PATH).",
90
164
  )
91
165
  else:
92
166
  table.add_row(
93
- "Gemini CLI",
167
+ "Recommended coding CLI",
94
168
  "[yellow]Missing[/yellow]",
95
- "Optional. Install Gemini CLI, then run 'dev integrate gemini --apply' (or 'dev setup --integrate --apply').",
169
+ "No built-in coding CLI on PATH. Run dev integrate recommend after installing one.",
96
170
  )
97
171
 
98
172
  try:
@@ -108,6 +182,109 @@ def render_doctor_check(project_root: Path = Path(".")):
108
182
  "[red]Unsupported[/red]",
109
183
  f"{provider} is configured, but this runtime supports: {supported}.",
110
184
  )
185
+ console.print(table)
186
+ return
187
+ if provider == "ollama":
188
+ # Use the provider's own resolver so the displayed URL reflects OLLAMA_HOST
189
+ # (with scheme/-/v1 normalization), not just OLLAMA_BASE_URL.
190
+ from devcouncil.execution.prompt_builder import MAX_PROMPT_CHARS
191
+ from devcouncil.llm.provider import OllamaProvider
192
+
193
+ base_url = OllamaProvider._resolve_base_url()
194
+ num_ctx = OllamaProvider._resolve_num_ctx()
195
+ # DevCouncil's planning prompts reach ~MAX_PROMPT_CHARS chars (~4 chars/token);
196
+ # recommend a context window that covers the prompt plus headroom for output.
197
+ recommended_ctx = 16384
198
+ min_ctx = max(8192, (MAX_PROMPT_CHARS // 4))
199
+ table.add_row(
200
+ "OLLAMA",
201
+ "[green]OK[/green]",
202
+ f"Local provider; no API key required (server: {base_url}).",
203
+ )
204
+
205
+ # Is the local Ollama server actually up? A native /api/version probe is
206
+ # cheap and turns the most common failure ("provider configured but
207
+ # `ollama serve` not running") into an actionable row instead of a
208
+ # connection traceback on the first model call.
209
+ reachable, detail = _probe_ollama(base_url)
210
+ if reachable:
211
+ table.add_row("OLLAMA server", "[green]OK[/green]", detail)
212
+ else:
213
+ table.add_row(
214
+ "OLLAMA server",
215
+ "[yellow]WARN[/yellow]",
216
+ f"{detail} Start it with 'ollama serve' (or 'brew services start ollama').",
217
+ )
218
+
219
+ # A reachable server with the configured model NOT pulled is the most common
220
+ # "all-green doctor, 404 on first call" trap. Verify the role models exist locally.
221
+ if reachable:
222
+ try:
223
+ configured_models = sorted(
224
+ {role.model for role in load_config(project_root).models.roles.values() if role.model}
225
+ )
226
+ except Exception:
227
+ configured_models = []
228
+ queried_ok, pulled = _probe_ollama_models(base_url)
229
+ if not queried_ok:
230
+ table.add_row(
231
+ "OLLAMA models",
232
+ "[yellow]WARN[/yellow]",
233
+ "Could not list pulled models (/api/tags). Ensure each configured model is pulled.",
234
+ )
235
+ elif not configured_models:
236
+ table.add_row(
237
+ "OLLAMA models",
238
+ "[yellow]WARN[/yellow]",
239
+ "No role models configured; run 'dev setup --provider ollama --model <model>'.",
240
+ )
241
+ else:
242
+ missing = [m for m in configured_models if not _ollama_model_present(m, pulled)]
243
+ if missing:
244
+ pulls = "; ".join(f"ollama pull {m}" for m in missing)
245
+ table.add_row(
246
+ "OLLAMA models",
247
+ "[yellow]WARN[/yellow]",
248
+ f"Configured model(s) not pulled: {', '.join(missing)}. Pull first: {pulls}.",
249
+ )
250
+ else:
251
+ table.add_row(
252
+ "OLLAMA models",
253
+ "[green]OK[/green]",
254
+ f"All configured models present locally ({', '.join(configured_models)}).",
255
+ )
256
+
257
+ if num_ctx is None:
258
+ table.add_row(
259
+ "OLLAMA num_ctx",
260
+ "[yellow]WARN[/yellow]",
261
+ f"OLLAMA_NUM_CTX not set — Ollama's small default (~2048-4096) will "
262
+ f"truncate DevCouncil's large planning prompts. Set OLLAMA_NUM_CTX={recommended_ctx}.",
263
+ )
264
+ elif num_ctx < min_ctx:
265
+ table.add_row(
266
+ "OLLAMA num_ctx",
267
+ "[yellow]WARN[/yellow]",
268
+ f"OLLAMA_NUM_CTX={num_ctx} may be too small for planning prompts "
269
+ f"(~{MAX_PROMPT_CHARS // 4} tokens); recommend >= {recommended_ctx}.",
270
+ )
271
+ else:
272
+ table.add_row("OLLAMA num_ctx", "[green]OK[/green]", f"context window = {num_ctx} tokens.")
273
+
274
+ # Local model size is bounded by host memory — unified RAM on Apple Silicon,
275
+ # VRAM on a discrete-GPU box, system RAM otherwise. Surface a model that will
276
+ # actually fit on this host (any OS), not a one-size default.
277
+ from devcouncil import hardware
278
+
279
+ host = hardware.describe_host()
280
+ table.add_row(
281
+ host.platform_label,
282
+ "[green]OK[/green]",
283
+ f"{host.chip_label}, {host.memory_label}. "
284
+ f"Recommended local model: {host.recommended_ollama_model} "
285
+ f"(dev setup --provider ollama --model {host.recommended_ollama_model}).",
286
+ )
287
+
111
288
  console.print(table)
112
289
  return
113
290
  env_var = provider_api_key_env_var(provider)
@@ -116,9 +293,32 @@ def render_doctor_check(project_root: Path = Path(".")):
116
293
  table.add_row(env_var, "[green]OK[/green]", f"Found in environment for {provider}.")
117
294
  elif local_secrets.get(env_var):
118
295
  table.add_row(env_var, "[green]OK[/green]", f"Found in .devcouncil/secrets.env for {provider}.")
296
+ elif provider == "vertexai" and get_gcloud_access_token():
297
+ table.add_row(env_var, "[green]OK[/green]", "Resolvable via gcloud auth print-access-token.")
298
+ table.caption = "Resolvable via gcloud auth print-access-token."
119
299
  else:
120
300
  table.add_row(env_var, "[yellow]Missing[/yellow]", f"Required if using {provider} provider. Run 'dev setup'.")
121
301
 
302
+ if provider == "vertexai":
303
+ project = (
304
+ os.environ.get("VERTEXAI_PROJECT")
305
+ or os.environ.get("GOOGLE_CLOUD_PROJECT")
306
+ or local_secrets.get("VERTEXAI_PROJECT")
307
+ or local_secrets.get("GOOGLE_CLOUD_PROJECT")
308
+ )
309
+ if project:
310
+ source = "environment" if os.environ.get("VERTEXAI_PROJECT") or os.environ.get("GOOGLE_CLOUD_PROJECT") else ".devcouncil/secrets.env"
311
+ table.add_row("VERTEXAI_PROJECT", "[green]OK[/green]", f"Found in {source}.")
312
+ else:
313
+ table.add_row(
314
+ "VERTEXAI_PROJECT",
315
+ "[yellow]Missing[/yellow]",
316
+ "Required for vertexai. Run 'dev setup --provider vertexai --vertex-project PROJECT_ID'.",
317
+ )
318
+
319
+ location = os.environ.get("VERTEXAI_LOCATION") or local_secrets.get("VERTEXAI_LOCATION", "global")
320
+ table.add_row("VERTEXAI_LOCATION", "[green]OK[/green]", location)
321
+
122
322
  console.print(table)
123
323
 
124
324
 
@@ -0,0 +1,48 @@
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.storage.db import get_db
8
+ from devcouncil.storage.repositories import TaskRepository
9
+ from devcouncil.verification.test_resolver import TestResolver
10
+ from devcouncil.verification.verifier import Verifier
11
+
12
+ app = typer.Typer(help="Evidence suggestion utilities.")
13
+ console = Console()
14
+
15
+
16
+ @app.command("suggest")
17
+ def suggest(
18
+ task_id: str = typer.Argument(...),
19
+ apply: bool = typer.Option(False, "--apply"),
20
+ include_low_confidence: bool = typer.Option(False, "--include-low-confidence"),
21
+ project_root: Path = typer.Option(Path("."), "--project-root"),
22
+ ):
23
+ root = project_root.expanduser().resolve()
24
+ initialize_project(root, quiet=True)
25
+ db = get_db(root)
26
+ if not db:
27
+ console.print("[red]DevCouncil not initialized.[/red]")
28
+ raise typer.Exit(code=1)
29
+
30
+ with db.get_session() as session:
31
+ task = TaskRepository(session).get_by_id(task_id)
32
+ if not task:
33
+ console.print(f"[red]Task {task_id} not found.[/red]")
34
+ raise typer.Exit(code=1)
35
+ changed = Verifier(root).get_task_changed_files(task_id)
36
+ suggestions = TestResolver(root).suggest_for_task(task, changed)
37
+ if not include_low_confidence:
38
+ suggestions = [s for s in suggestions if s.confidence != "low"]
39
+ if apply:
40
+ for item in suggestions:
41
+ if item.confidence == "high" and item.command not in task.expected_tests:
42
+ task.expected_tests.append(item.command)
43
+ TaskRepository(session).save(task)
44
+ typer.echo(json.dumps({
45
+ "task_id": task_id,
46
+ "suggestions": [s.model_dump() for s in suggestions],
47
+ "expected_tests": task.expected_tests,
48
+ }, indent=2))