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
@@ -1,15 +1,86 @@
1
- import typer
1
+ import typer
2
2
  import subprocess
3
3
  import os
4
4
  import shutil
5
5
  from pathlib import Path
6
- from rich.console import Console
7
- from rich.table import Table
8
-
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
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
+ )
17
+ from devcouncil.llm.provider import SUPPORTED_MODEL_PROVIDERS, validate_model_provider
18
+
9
19
  app = typer.Typer()
10
20
  console = Console()
11
21
 
12
- def render_doctor_check():
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
+
83
+ def render_doctor_check(project_root: Path = Path(".")):
13
84
  def _command_version(command: list[str]) -> str | None:
14
85
  executable = shutil.which(command[0])
15
86
  if not executable:
@@ -30,32 +101,32 @@ def render_doctor_check():
30
101
  ).splitlines()[0].strip()
31
102
  except Exception:
32
103
  return None
33
-
34
- table = Table(title="DevCouncil Doctor Check")
35
- table.add_column("Component", style="cyan")
36
- table.add_column("Status", style="magenta")
37
- table.add_column("Notes", style="green")
38
-
39
- # Check Git
40
- git_ver = _command_version(["git", "--version"])
41
- if git_ver:
42
- table.add_row("Git", "[green]OK[/green]", git_ver)
43
- else:
44
- table.add_row("Git", "[red]Missing[/red]", "Git is required for repo mapping and checkpoints.")
45
-
46
- # Check uv
47
- uv_ver = _command_version(["uv", "--version"])
48
- if uv_ver:
49
- table.add_row("uv", "[green]OK[/green]", uv_ver)
50
- else:
51
- table.add_row("uv", "[red]Missing[/red]", "Install uv to run or install DevCouncil.")
52
-
53
- # Check CLI shims
54
- if shutil.which("devcouncil"):
55
- table.add_row("devcouncil CLI", "[green]OK[/green]", "Found on PATH.")
56
- else:
57
- table.add_row("devcouncil CLI", "[yellow]Missing[/yellow]", "Run via 'uv run devcouncil' or install with 'uv tool install --force .'.")
58
-
104
+
105
+ table = Table(title="DevCouncil Doctor Check")
106
+ table.add_column("Component", style="cyan")
107
+ table.add_column("Status", style="magenta")
108
+ table.add_column("Notes", style="green")
109
+
110
+ # Check Git
111
+ git_ver = _command_version(["git", "--version"])
112
+ if git_ver:
113
+ table.add_row("Git", "[green]OK[/green]", git_ver)
114
+ else:
115
+ table.add_row("Git", "[red]Missing[/red]", "Git is required for repo mapping and checkpoints.")
116
+
117
+ # Check uv
118
+ uv_ver = _command_version(["uv", "--version"])
119
+ if uv_ver:
120
+ table.add_row("uv", "[green]OK[/green]", uv_ver)
121
+ else:
122
+ table.add_row("uv", "[red]Missing[/red]", "Install uv to run or install DevCouncil.")
123
+
124
+ # Check CLI shims
125
+ if shutil.which("devcouncil"):
126
+ table.add_row("devcouncil CLI", "[green]OK[/green]", "Found on PATH.")
127
+ else:
128
+ table.add_row("devcouncil CLI", "[yellow]Missing[/yellow]", "Run via 'uv run devcouncil' or install with 'uv tool install --force .'.")
129
+
59
130
  # Check ripgrep
60
131
  rg_ver = _command_version(["rg", "--version"])
61
132
  if rg_ver:
@@ -63,34 +134,207 @@ def render_doctor_check():
63
134
  else:
64
135
  table.add_row("ripgrep (rg)", "[yellow]Missing[/yellow]", "ripgrep is highly recommended for fast repo mapping.")
65
136
 
66
- # Check supported coding CLIs
67
- codex_ver = _command_version(["codex", "--version"])
68
- if codex_ver:
69
- table.add_row("Codex CLI", "[green]OK[/green]", f"{codex_ver}. Setup: dev integrate codex --apply")
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
+ )
156
+
157
+ detected = detect_available_coding_cli(project_root)
158
+ resolved = resolve_automated_executor(project_root, None)
159
+ if detected:
160
+ table.add_row(
161
+ "Recommended coding CLI",
162
+ "[green]OK[/green]",
163
+ f"Use --executor {resolved} for dev go / dev run (detected on PATH).",
164
+ )
70
165
  else:
71
- table.add_row("Codex CLI", "[yellow]Missing[/yellow]", "Optional. Install Codex, then run 'dev integrate codex --apply'.")
166
+ table.add_row(
167
+ "Recommended coding CLI",
168
+ "[yellow]Missing[/yellow]",
169
+ "No built-in coding CLI on PATH. Run dev integrate recommend after installing one.",
170
+ )
171
+
172
+ try:
173
+ provider = load_config(project_root).models.provider
174
+ except Exception:
175
+ provider = "openrouter"
176
+ try:
177
+ provider = validate_model_provider(provider)
178
+ except ValueError:
179
+ supported = ", ".join(SUPPORTED_MODEL_PROVIDERS)
180
+ table.add_row(
181
+ "models.provider",
182
+ "[red]Unsupported[/red]",
183
+ f"{provider} is configured, but this runtime supports: {supported}.",
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
72
192
 
73
- gemini_ver = _command_version(["gemini", "--version"])
74
- if gemini_ver:
75
- table.add_row("Gemini CLI", "[green]OK[/green]", f"{gemini_ver}. Setup: dev integrate gemini --apply")
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
+
288
+ console.print(table)
289
+ return
290
+ env_var = provider_api_key_env_var(provider)
291
+ local_secrets = load_local_secrets(project_root)
292
+ if os.environ.get(env_var):
293
+ table.add_row(env_var, "[green]OK[/green]", f"Found in environment for {provider}.")
294
+ elif local_secrets.get(env_var):
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."
76
299
  else:
77
- table.add_row("Gemini CLI", "[yellow]Missing[/yellow]", "Optional. Install Gemini CLI, then run 'dev integrate gemini --apply'.")
300
+ table.add_row(env_var, "[yellow]Missing[/yellow]", f"Required if using {provider} provider. Run 'dev setup'.")
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
+ )
78
318
 
79
- # Check OpenRouter API Key
80
- if os.environ.get("OPENROUTER_API_KEY"):
81
- table.add_row("OPENROUTER_API_KEY", "[green]OK[/green]", "Found in environment.")
82
- else:
83
- table.add_row("OPENROUTER_API_KEY", "[yellow]Missing[/yellow]", "Required if using OpenRouter provider.")
319
+ location = os.environ.get("VERTEXAI_LOCATION") or local_secrets.get("VERTEXAI_LOCATION", "global")
320
+ table.add_row("VERTEXAI_LOCATION", "[green]OK[/green]", location)
84
321
 
85
322
  console.print(table)
86
323
 
87
324
 
88
325
  @app.callback(invoke_without_command=True)
89
- def doctor(ctx: typer.Context):
326
+ def doctor(
327
+ ctx: typer.Context,
328
+ project_root: Path = typer.Option(
329
+ Path("."),
330
+ "--project-root",
331
+ help="Repository root containing .devcouncil/config.yaml.",
332
+ ),
333
+ ):
90
334
  """
91
335
  Check the environment for DevCouncil prerequisites.
92
336
  """
93
337
  if ctx.invoked_subcommand is not None:
94
338
  return
95
339
 
96
- render_doctor_check()
340
+ render_doctor_check(project_root.expanduser().resolve())
@@ -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))