devcouncil 0.1.1 → 0.3.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/README.md +201 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +348 -12
- package/src/devcouncil/app/orchestrator.py +10 -6
- package/src/devcouncil/app/state_machine.py +4 -0
- package/src/devcouncil/artifacts/graph.py +32 -5
- 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 +6 -3
- package/src/devcouncil/cli/commands/check.py +220 -0
- package/src/devcouncil/cli/commands/config.py +43 -4
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +6 -1
- package/src/devcouncil/cli/commands/design.py +186 -0
- package/src/devcouncil/cli/commands/doctor.py +379 -22
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +532 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +296 -15
- package/src/devcouncil/cli/commands/init.py +161 -20
- package/src/devcouncil/cli/commands/integrate.py +1371 -124
- package/src/devcouncil/cli/commands/logs.py +106 -0
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/okf.py +245 -0
- package/src/devcouncil/cli/commands/plan.py +256 -55
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +50 -24
- package/src/devcouncil/cli/commands/report.py +8 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
- package/src/devcouncil/cli/commands/rollback.py +27 -28
- package/src/devcouncil/cli/commands/run.py +195 -54
- 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 +145 -6
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/skills.py +267 -0
- package/src/devcouncil/cli/commands/status.py +30 -15
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +144 -3
- package/src/devcouncil/cli/commands/watch.py +32 -12
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +91 -7
- package/src/devcouncil/domain/evidence.py +29 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +256 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +205 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +65 -10
- package/src/devcouncil/execution/permissions.py +24 -24
- package/src/devcouncil/execution/policy_engine.py +350 -0
- package/src/devcouncil/execution/prompt_builder.py +751 -23
- package/src/devcouncil/execution/shell_session.py +231 -0
- package/src/devcouncil/execution/task_runner.py +24 -9
- package/src/devcouncil/executors/agent_registry.py +596 -0
- package/src/devcouncil/executors/coding_cli.py +791 -39
- package/src/devcouncil/executors/mini_swe.py +6 -0
- package/src/devcouncil/executors/native/agent.py +135 -19
- package/src/devcouncil/executors/openhands.py +6 -0
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
- package/src/devcouncil/gating/policy.py +190 -11
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +17 -7
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1284 -15
- package/src/devcouncil/indexing/semantic_index.py +221 -0
- package/src/devcouncil/integrations/actions.py +166 -0
- package/src/devcouncil/integrations/check.py +426 -0
- package/src/devcouncil/integrations/claude_assets.py +444 -0
- package/src/devcouncil/integrations/code_review_graph.py +13 -2
- package/src/devcouncil/integrations/github_intent.py +149 -0
- package/src/devcouncil/integrations/gitnexus.py +45 -2
- package/src/devcouncil/integrations/mcp/server.py +1944 -32
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +9 -0
- package/src/devcouncil/knowledge/__init__.py +23 -0
- package/src/devcouncil/knowledge/design.py +374 -0
- package/src/devcouncil/knowledge/design_conformance.py +317 -0
- package/src/devcouncil/knowledge/fetch.py +223 -0
- package/src/devcouncil/knowledge/frontmatter.py +51 -0
- package/src/devcouncil/knowledge/okf.py +202 -0
- package/src/devcouncil/knowledge/skill_bridge.py +96 -0
- package/src/devcouncil/knowledge/sources.py +239 -0
- package/src/devcouncil/live/cards.py +181 -25
- package/src/devcouncil/live/repair_prompt.py +29 -6
- package/src/devcouncil/live/reviewer.py +72 -13
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/summary.py +18 -8
- package/src/devcouncil/live/transcripts.py +47 -11
- package/src/devcouncil/llm/cache.py +20 -8
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +617 -49
- package/src/devcouncil/llm/router.py +337 -53
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/optimization/skillopt.py +673 -0
- package/src/devcouncil/planning/arbiter_service.py +10 -2
- package/src/devcouncil/planning/correction_manifest.py +346 -0
- package/src/devcouncil/planning/critique_service.py +16 -4
- package/src/devcouncil/planning/plan_service.py +86 -6
- package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
- package/src/devcouncil/planning/repair_service.py +8 -2
- package/src/devcouncil/planning/spec_service.py +37 -3
- package/src/devcouncil/repo/ci_scaffold.py +165 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +384 -0
- package/src/devcouncil/reporting/json_report.py +22 -1
- package/src/devcouncil/reporting/markdown_report.py +29 -1
- package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
- package/src/devcouncil/reporting/okf_html.py +323 -0
- package/src/devcouncil/reporting/report_builder.py +18 -1
- 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 +408 -0
- package/src/devcouncil/storage/db.py +140 -3
- package/src/devcouncil/storage/models.py +125 -0
- package/src/devcouncil/storage/native.py +559 -0
- package/src/devcouncil/storage/repositories.py +157 -78
- package/src/devcouncil/telemetry/cost.py +123 -17
- package/src/devcouncil/telemetry/logging_setup.py +244 -0
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/stages.py +141 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +24 -10
- package/src/devcouncil/ui/dashboard.py +393 -28
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/acceptance_compiler.py +253 -0
- package/src/devcouncil/verification/ad_hoc_check.py +135 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +11 -2
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +181 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1549 -143
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -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
|
-
|
|
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)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""`dev design` — lint, export, and inspect a project design.md design system.
|
|
2
|
+
|
|
3
|
+
Mirrors the upstream ``@google/design.md`` CLI's ``lint`` and ``export`` subcommands so a
|
|
4
|
+
DevCouncil project can validate its design tokens and convert them to CSS / Tailwind / W3C
|
|
5
|
+
Design Tokens. The same design.md is injected into coding-agent prompts (see
|
|
6
|
+
``.devcouncil/knowledge/design``) so agents honor the system while they build.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from devcouncil.knowledge.design import export as export_design
|
|
18
|
+
from devcouncil.knowledge.design import lint as lint_design
|
|
19
|
+
from devcouncil.knowledge.design import parse_design_md
|
|
20
|
+
from devcouncil.knowledge.design_conformance import (
|
|
21
|
+
STYLE_EXTENSIONS,
|
|
22
|
+
scan_files,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(help="Lint, export, and inspect a design.md design system.")
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
# Where a project's design system is looked for, in order.
|
|
29
|
+
_DEFAULT_PATHS = (
|
|
30
|
+
".devcouncil/knowledge/design/design.md",
|
|
31
|
+
"DESIGN.md",
|
|
32
|
+
"design.md",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Directories pruned while auto-discovering style files (heavy / generated / vendored).
|
|
36
|
+
_PRUNE_DIRS = frozenset({
|
|
37
|
+
".git", ".hg", ".svn", "node_modules", ".venv", "venv", "env",
|
|
38
|
+
"dist", "build", "out", ".next", ".nuxt", ".svelte-kit", "coverage",
|
|
39
|
+
"__pycache__", ".mypy_cache", ".pytest_cache", ".cache", "vendor",
|
|
40
|
+
})
|
|
41
|
+
# Cap on auto-discovered files so an enormous repo can't make `check` run unbounded.
|
|
42
|
+
_MAX_DISCOVERED_FILES = 5000
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _discover_style_files(root: Path) -> list[Path]:
|
|
46
|
+
"""Walk ``root`` for style-ish files, pruning heavy dirs and bounding the count."""
|
|
47
|
+
found: list[Path] = []
|
|
48
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
49
|
+
dirnames[:] = [d for d in dirnames if d not in _PRUNE_DIRS and not d.startswith(".")]
|
|
50
|
+
for name in filenames:
|
|
51
|
+
if Path(name).suffix.lower() in STYLE_EXTENSIONS:
|
|
52
|
+
found.append(Path(dirpath) / name)
|
|
53
|
+
if len(found) >= _MAX_DISCOVERED_FILES:
|
|
54
|
+
return found
|
|
55
|
+
return found
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _resolve_path(explicit: Path | None, project_root: Path) -> Path | None:
|
|
59
|
+
if explicit is not None:
|
|
60
|
+
candidate = explicit.expanduser()
|
|
61
|
+
return candidate if candidate.is_file() else None
|
|
62
|
+
for rel in _DEFAULT_PATHS:
|
|
63
|
+
candidate = project_root / rel
|
|
64
|
+
if candidate.is_file():
|
|
65
|
+
return candidate
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command("lint")
|
|
70
|
+
def lint(
|
|
71
|
+
path: Path = typer.Argument(None, help="Path to a design.md (defaults to the project's design system)."),
|
|
72
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
|
|
73
|
+
):
|
|
74
|
+
"""Validate a design system: broken token refs, contrast, ordering, orphans."""
|
|
75
|
+
root = project_root.expanduser().resolve()
|
|
76
|
+
target = _resolve_path(path, root)
|
|
77
|
+
if target is None:
|
|
78
|
+
console.print("[red]No design.md found.[/red] Looked for: " + ", ".join(_DEFAULT_PATHS))
|
|
79
|
+
raise typer.Exit(code=1)
|
|
80
|
+
|
|
81
|
+
findings = lint_design(parse_design_md(target))
|
|
82
|
+
if not findings:
|
|
83
|
+
console.print(f"[green]✓ {target} passed all design.md lint rules.[/green]")
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
errors = [f for f in findings if f.severity == "error"]
|
|
87
|
+
color = {"error": "red", "warning": "yellow", "info": "cyan"}
|
|
88
|
+
console.print(f"[bold]{len(findings)} finding(s) in {target}:[/bold]")
|
|
89
|
+
for f in findings:
|
|
90
|
+
console.print(f" [{color.get(f.severity, 'white')}]{f.format()}[/{color.get(f.severity, 'white')}]")
|
|
91
|
+
if errors:
|
|
92
|
+
raise typer.Exit(code=1)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@app.command("export")
|
|
96
|
+
def export(
|
|
97
|
+
path: Path = typer.Argument(None, help="Path to a design.md (defaults to the project's design system)."),
|
|
98
|
+
fmt: str = typer.Option("css", "--format", "-f", help="Output format: css | tailwind | w3c."),
|
|
99
|
+
output: Path = typer.Option(None, "--output", "-o", help="Write to this file instead of stdout."),
|
|
100
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
|
|
101
|
+
):
|
|
102
|
+
"""Export design tokens to CSS custom properties, a Tailwind config, or W3C tokens."""
|
|
103
|
+
root = project_root.expanduser().resolve()
|
|
104
|
+
if fmt not in ("css", "tailwind", "w3c"):
|
|
105
|
+
console.print(f"[red]Unknown format '{fmt}'.[/red] Use one of: css, tailwind, w3c.")
|
|
106
|
+
raise typer.Exit(code=2)
|
|
107
|
+
target = _resolve_path(path, root)
|
|
108
|
+
if target is None:
|
|
109
|
+
console.print("[red]No design.md found.[/red] Looked for: " + ", ".join(_DEFAULT_PATHS))
|
|
110
|
+
raise typer.Exit(code=1)
|
|
111
|
+
|
|
112
|
+
rendered = export_design(parse_design_md(target), fmt) # type: ignore[arg-type]
|
|
113
|
+
if output is not None:
|
|
114
|
+
out = output.expanduser().resolve()
|
|
115
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
out.write_text(rendered, encoding="utf-8")
|
|
117
|
+
console.print(f"[green]Wrote {fmt} tokens to[/green] {out}")
|
|
118
|
+
else:
|
|
119
|
+
typer.echo(rendered, nl=not rendered.endswith("\n"))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.command("check")
|
|
123
|
+
def check(
|
|
124
|
+
files: list[Path] = typer.Argument(
|
|
125
|
+
None, help="Files to check (defaults to the repo's style-ish files)."),
|
|
126
|
+
design: Path = typer.Option(
|
|
127
|
+
None, "--design", help="Path to a design.md (defaults to the project's design system)."),
|
|
128
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
|
|
129
|
+
):
|
|
130
|
+
"""Flag hardcoded style literals (hex colors, px sizes) that bypass design tokens.
|
|
131
|
+
|
|
132
|
+
Exits non-zero when any violation is found so it can gate CI / a pre-commit hook.
|
|
133
|
+
"""
|
|
134
|
+
root = project_root.expanduser().resolve()
|
|
135
|
+
target = _resolve_path(design, root)
|
|
136
|
+
if target is None:
|
|
137
|
+
console.print("[red]No design.md found.[/red] Looked for: " + ", ".join(_DEFAULT_PATHS))
|
|
138
|
+
raise typer.Exit(code=1)
|
|
139
|
+
|
|
140
|
+
ds = parse_design_md(target)
|
|
141
|
+
paths = [f.expanduser() for f in files] if files else _discover_style_files(root)
|
|
142
|
+
violations = scan_files(paths, ds)
|
|
143
|
+
|
|
144
|
+
if not violations:
|
|
145
|
+
scanned = len([p for p in paths if p.suffix.lower() in STYLE_EXTENSIONS])
|
|
146
|
+
console.print(
|
|
147
|
+
f"[green]✓ No design-token violations in {scanned} file(s) "
|
|
148
|
+
f"(tokens from {target}).[/green]")
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
by_file: dict[str, list] = {}
|
|
152
|
+
for v in violations:
|
|
153
|
+
by_file.setdefault(v.file or "<text>", []).append(v)
|
|
154
|
+
|
|
155
|
+
console.print(
|
|
156
|
+
f"[bold red]{len(violations)} design-token violation(s) "
|
|
157
|
+
f"in {len(by_file)} file(s):[/bold red]")
|
|
158
|
+
for fname in sorted(by_file):
|
|
159
|
+
console.print(f"[bold]{fname}[/bold]")
|
|
160
|
+
for v in sorted(by_file[fname], key=lambda x: (x.line, x.kind)):
|
|
161
|
+
console.print(
|
|
162
|
+
f" [yellow]{v.line}[/yellow] [{v.kind}] {v.message}\n"
|
|
163
|
+
f" [dim]{v.snippet}[/dim]")
|
|
164
|
+
raise typer.Exit(code=1)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command("show")
|
|
168
|
+
def show(
|
|
169
|
+
path: Path = typer.Argument(None, help="Path to a design.md (defaults to the project's design system)."),
|
|
170
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
|
|
171
|
+
):
|
|
172
|
+
"""Summarize a design system: token counts and document sections."""
|
|
173
|
+
root = project_root.expanduser().resolve()
|
|
174
|
+
target = _resolve_path(path, root)
|
|
175
|
+
if target is None:
|
|
176
|
+
console.print("[red]No design.md found.[/red] Looked for: " + ", ".join(_DEFAULT_PATHS))
|
|
177
|
+
raise typer.Exit(code=1)
|
|
178
|
+
|
|
179
|
+
ds = parse_design_md(target)
|
|
180
|
+
console.print(f"[bold]{ds.name or target.name}[/bold] ({target})")
|
|
181
|
+
console.print(
|
|
182
|
+
f" colors={len(ds.colors)} typography={len(ds.typography)} "
|
|
183
|
+
f"rounded={len(ds.rounded)} spacing={len(ds.spacing)} components={len(ds.components)}"
|
|
184
|
+
)
|
|
185
|
+
if ds.sections:
|
|
186
|
+
console.print(" sections: " + ", ".join(h for h, _ in ds.sections))
|
|
@@ -6,13 +6,230 @@ 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
|
+
|
|
83
|
+
def _knowledge_dir(project_root: Path, config=None) -> str:
|
|
84
|
+
"""Configured knowledge directory (honors ``knowledge.directory``), best-effort.
|
|
85
|
+
|
|
86
|
+
Mirrors ``cli.commands.okf._knowledge_okf_dir`` so doctor inspects the same location
|
|
87
|
+
ingest writes to. Any config failure falls back to the documented default rather than
|
|
88
|
+
raising — doctor must keep running even with a broken config.
|
|
89
|
+
|
|
90
|
+
``config`` is an optional pre-loaded config (default ``None`` loads as before), so a
|
|
91
|
+
single doctor invocation can reuse one ``load_config`` call across checks.
|
|
92
|
+
"""
|
|
93
|
+
directory = ".devcouncil/knowledge"
|
|
94
|
+
try:
|
|
95
|
+
cfg = config if config is not None else load_config(project_root)
|
|
96
|
+
directory = cfg.knowledge.directory
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
return directory
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def check_ingested_knowledge(project_root: Path, config=None) -> list[tuple[str, str, str]]:
|
|
103
|
+
"""Best-effort health rows for ingested knowledge under ``<knowledge dir>/{okf,design}``.
|
|
104
|
+
|
|
105
|
+
Returns ``(component, status_markup, notes)`` rows for the doctor table. This is the
|
|
106
|
+
most common "ingested but silently broken" surface: an OKF bundle with a dangling
|
|
107
|
+
cross-link, or a design.md with broken token references, both validate clean to the
|
|
108
|
+
eye but degrade the prompt context. We therefore read+validate every ingested bundle
|
|
109
|
+
and lint the design system, reporting counts and any problems.
|
|
110
|
+
|
|
111
|
+
Never raises: any knowledge-layer failure becomes a ``WARN`` row, and an empty
|
|
112
|
+
knowledge area yields a neutral ``INFO`` row — a project that never ingested knowledge
|
|
113
|
+
is not a misconfiguration.
|
|
114
|
+
"""
|
|
115
|
+
ok = "[green]OK[/green]"
|
|
116
|
+
warn = "[yellow]WARN[/yellow]"
|
|
117
|
+
info = "[cyan]INFO[/cyan]"
|
|
118
|
+
rows: list[tuple[str, str, str]] = []
|
|
119
|
+
|
|
120
|
+
directory = _knowledge_dir(project_root, config=config)
|
|
121
|
+
base = project_root / directory
|
|
122
|
+
okf_area = base / "okf"
|
|
123
|
+
design_md = base / "design" / "design.md"
|
|
124
|
+
|
|
125
|
+
# Identify ingested OKF bundles. `dev okf ingest` writes each bundle into its own
|
|
126
|
+
# subfolder under okf/; treat each such subdir as a bundle. If documents sit loose
|
|
127
|
+
# directly under okf/ (and there are no subfolder bundles), treat okf/ as one bundle.
|
|
128
|
+
# Choosing one or the other avoids double-counting documents via rglob.
|
|
129
|
+
bundle_dirs: list[Path] = []
|
|
130
|
+
if okf_area.is_dir():
|
|
131
|
+
try:
|
|
132
|
+
subdir_bundles = [
|
|
133
|
+
child
|
|
134
|
+
for child in sorted(okf_area.iterdir())
|
|
135
|
+
if child.is_dir() and any(child.rglob("*.md"))
|
|
136
|
+
]
|
|
137
|
+
except Exception:
|
|
138
|
+
subdir_bundles = []
|
|
139
|
+
loose_docs = any(p.is_file() for p in okf_area.glob("*.md"))
|
|
140
|
+
if subdir_bundles:
|
|
141
|
+
bundle_dirs = subdir_bundles
|
|
142
|
+
elif loose_docs:
|
|
143
|
+
bundle_dirs = [okf_area]
|
|
144
|
+
|
|
145
|
+
has_design = design_md.is_file()
|
|
146
|
+
|
|
147
|
+
# Nothing ingested at all → neutral info line, not a failure.
|
|
148
|
+
if not bundle_dirs and not has_design:
|
|
149
|
+
rows.append(
|
|
150
|
+
(
|
|
151
|
+
"Ingested knowledge",
|
|
152
|
+
info,
|
|
153
|
+
f"No ingested knowledge under {directory}/ (okf/, design/). "
|
|
154
|
+
"Add some with 'dev okf ingest <bundle>'.",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
return rows
|
|
158
|
+
|
|
159
|
+
# --- OKF bundles -----------------------------------------------------------------
|
|
160
|
+
if bundle_dirs:
|
|
161
|
+
from devcouncil.knowledge.okf import read_bundle, validate_bundle
|
|
162
|
+
|
|
163
|
+
total_docs = 0
|
|
164
|
+
problems: list[str] = []
|
|
165
|
+
for bdir in bundle_dirs:
|
|
166
|
+
try:
|
|
167
|
+
bundle = read_bundle(bdir)
|
|
168
|
+
total_docs += len(bundle.documents)
|
|
169
|
+
problems.extend(validate_bundle(bundle))
|
|
170
|
+
except Exception as exc: # never let a malformed bundle crash doctor
|
|
171
|
+
problems.append(f"{bdir.name}: failed to read bundle ({exc})")
|
|
172
|
+
summary = f"{len(bundle_dirs)} bundle(s), {total_docs} document(s)"
|
|
173
|
+
if problems:
|
|
174
|
+
preview = "; ".join(problems[:5])
|
|
175
|
+
extra = "" if len(problems) <= 5 else f" (+{len(problems) - 5} more)"
|
|
176
|
+
rows.append(
|
|
177
|
+
(
|
|
178
|
+
"Ingested OKF",
|
|
179
|
+
warn,
|
|
180
|
+
f"{summary}: {len(problems)} validation problem(s): {preview}{extra}.",
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
rows.append(("Ingested OKF", ok, f"{summary}; no validation problems."))
|
|
185
|
+
|
|
186
|
+
# --- Design system ----------------------------------------------------------------
|
|
187
|
+
if has_design:
|
|
188
|
+
from devcouncil.knowledge.design import lint, parse_design_md
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
findings = lint(parse_design_md(design_md))
|
|
192
|
+
except Exception as exc: # never let a malformed design.md crash doctor
|
|
193
|
+
rows.append(("Ingested design.md", warn, f"present, but lint failed: {exc}."))
|
|
194
|
+
else:
|
|
195
|
+
if findings:
|
|
196
|
+
preview = "; ".join(f.format() for f in findings[:5])
|
|
197
|
+
extra = "" if len(findings) <= 5 else f" (+{len(findings) - 5} more)"
|
|
198
|
+
rows.append(
|
|
199
|
+
(
|
|
200
|
+
"Ingested design.md",
|
|
201
|
+
warn,
|
|
202
|
+
f"present; {len(findings)} lint finding(s): {preview}{extra}.",
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
else:
|
|
206
|
+
rows.append(("Ingested design.md", ok, "present; 0 lint findings."))
|
|
207
|
+
|
|
208
|
+
return rows
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _add_logging_row(table, project_root: Path) -> None:
|
|
212
|
+
"""Append a logging-health row: where the durable run log lives and how big it
|
|
213
|
+
is, so a user chasing a recurring failure knows exactly where to look."""
|
|
214
|
+
from devcouncil.telemetry.logging_setup import LOG_RELATIVE_PATH
|
|
215
|
+
|
|
216
|
+
log_path = project_root / LOG_RELATIVE_PATH
|
|
217
|
+
if log_path.exists():
|
|
218
|
+
size_kb = log_path.stat().st_size / 1024
|
|
219
|
+
detail = f"{log_path} ({size_kb:.0f} KB). View: dev logs tail"
|
|
220
|
+
else:
|
|
221
|
+
detail = f"Will write to {log_path} on first command. View: dev logs tail"
|
|
222
|
+
table.add_row("logging", "[green]OK[/green]", detail)
|
|
223
|
+
|
|
224
|
+
|
|
15
225
|
def render_doctor_check(project_root: Path = Path(".")):
|
|
226
|
+
# Load config once for the whole invocation; the diagnostic checks below reuse
|
|
227
|
+
# this instead of re-reading config.yaml. None falls back to per-check loading.
|
|
228
|
+
try:
|
|
229
|
+
config = load_config(project_root)
|
|
230
|
+
except Exception:
|
|
231
|
+
config = None
|
|
232
|
+
|
|
16
233
|
def _command_version(command: list[str]) -> str | None:
|
|
17
234
|
executable = shutil.which(command[0])
|
|
18
235
|
if not executable:
|
|
@@ -66,37 +283,48 @@ def render_doctor_check(project_root: Path = Path(".")):
|
|
|
66
283
|
else:
|
|
67
284
|
table.add_row("ripgrep (rg)", "[yellow]Missing[/yellow]", "ripgrep is highly recommended for fast repo mapping.")
|
|
68
285
|
|
|
69
|
-
# Check supported coding CLIs
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
286
|
+
# Check supported coding CLIs (driven by the agent registry, so new
|
|
287
|
+
# built-in agents show up here without doctor edits).
|
|
288
|
+
for name in CODING_CLI_PROBE_ORDER:
|
|
289
|
+
info = CODING_CLI_INTEGRATION_INFO.get(name)
|
|
290
|
+
if info is None:
|
|
291
|
+
continue
|
|
292
|
+
version = None
|
|
293
|
+
for probe in CODING_CLI_VERSION_COMMANDS.get(name, ()):
|
|
294
|
+
version = _command_version(list(probe))
|
|
295
|
+
if version:
|
|
296
|
+
break
|
|
297
|
+
if version:
|
|
298
|
+
table.add_row(info.label, "[green]OK[/green]", f"{version}. Setup: {info.notes}.")
|
|
299
|
+
else:
|
|
300
|
+
table.add_row(
|
|
301
|
+
info.label,
|
|
302
|
+
"[yellow]Missing[/yellow]",
|
|
303
|
+
f"Optional. Install {info.label}, then use: {info.notes}.",
|
|
304
|
+
)
|
|
83
305
|
|
|
84
|
-
|
|
85
|
-
|
|
306
|
+
detected = detect_available_coding_cli(project_root)
|
|
307
|
+
resolved = resolve_automated_executor(project_root, None)
|
|
308
|
+
if detected:
|
|
86
309
|
table.add_row(
|
|
87
|
-
"
|
|
310
|
+
"Recommended coding CLI",
|
|
88
311
|
"[green]OK[/green]",
|
|
89
|
-
f"{
|
|
312
|
+
f"Use --executor {resolved} for dev go / dev run (detected on PATH).",
|
|
90
313
|
)
|
|
91
314
|
else:
|
|
92
315
|
table.add_row(
|
|
93
|
-
"
|
|
316
|
+
"Recommended coding CLI",
|
|
94
317
|
"[yellow]Missing[/yellow]",
|
|
95
|
-
"
|
|
318
|
+
"No built-in coding CLI on PATH. Run dev integrate recommend after installing one.",
|
|
96
319
|
)
|
|
97
320
|
|
|
321
|
+
# Ingested-knowledge health (added before the provider branch so it appears on every
|
|
322
|
+
# code path, including the early returns for ollama / unsupported providers).
|
|
323
|
+
for component, status, notes in check_ingested_knowledge(project_root, config=config):
|
|
324
|
+
table.add_row(component, status, notes)
|
|
325
|
+
|
|
98
326
|
try:
|
|
99
|
-
provider =
|
|
327
|
+
provider = config.models.provider if config is not None else "openrouter"
|
|
100
328
|
except Exception:
|
|
101
329
|
provider = "openrouter"
|
|
102
330
|
try:
|
|
@@ -108,6 +336,111 @@ def render_doctor_check(project_root: Path = Path(".")):
|
|
|
108
336
|
"[red]Unsupported[/red]",
|
|
109
337
|
f"{provider} is configured, but this runtime supports: {supported}.",
|
|
110
338
|
)
|
|
339
|
+
_add_logging_row(table, project_root)
|
|
340
|
+
console.print(table)
|
|
341
|
+
return
|
|
342
|
+
if provider == "ollama":
|
|
343
|
+
# Use the provider's own resolver so the displayed URL reflects OLLAMA_HOST
|
|
344
|
+
# (with scheme/-/v1 normalization), not just OLLAMA_BASE_URL.
|
|
345
|
+
from devcouncil.execution.prompt_builder import MAX_PROMPT_CHARS
|
|
346
|
+
from devcouncil.llm.provider import OllamaProvider
|
|
347
|
+
|
|
348
|
+
base_url = OllamaProvider._resolve_base_url()
|
|
349
|
+
num_ctx = OllamaProvider._resolve_num_ctx()
|
|
350
|
+
# DevCouncil's planning prompts reach ~MAX_PROMPT_CHARS chars (~4 chars/token);
|
|
351
|
+
# recommend a context window that covers the prompt plus headroom for output.
|
|
352
|
+
recommended_ctx = 16384
|
|
353
|
+
min_ctx = max(8192, (MAX_PROMPT_CHARS // 4))
|
|
354
|
+
table.add_row(
|
|
355
|
+
"OLLAMA",
|
|
356
|
+
"[green]OK[/green]",
|
|
357
|
+
f"Local provider; no API key required (server: {base_url}).",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Is the local Ollama server actually up? A native /api/version probe is
|
|
361
|
+
# cheap and turns the most common failure ("provider configured but
|
|
362
|
+
# `ollama serve` not running") into an actionable row instead of a
|
|
363
|
+
# connection traceback on the first model call.
|
|
364
|
+
reachable, detail = _probe_ollama(base_url)
|
|
365
|
+
if reachable:
|
|
366
|
+
table.add_row("OLLAMA server", "[green]OK[/green]", detail)
|
|
367
|
+
else:
|
|
368
|
+
table.add_row(
|
|
369
|
+
"OLLAMA server",
|
|
370
|
+
"[yellow]WARN[/yellow]",
|
|
371
|
+
f"{detail} Start it with 'ollama serve' (or 'brew services start ollama').",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# A reachable server with the configured model NOT pulled is the most common
|
|
375
|
+
# "all-green doctor, 404 on first call" trap. Verify the role models exist locally.
|
|
376
|
+
if reachable and config is not None:
|
|
377
|
+
try:
|
|
378
|
+
configured_models = sorted(
|
|
379
|
+
{role.model for role in config.models.roles.values() if role.model}
|
|
380
|
+
)
|
|
381
|
+
except Exception:
|
|
382
|
+
configured_models = []
|
|
383
|
+
queried_ok, pulled = _probe_ollama_models(base_url)
|
|
384
|
+
if not queried_ok:
|
|
385
|
+
table.add_row(
|
|
386
|
+
"OLLAMA models",
|
|
387
|
+
"[yellow]WARN[/yellow]",
|
|
388
|
+
"Could not list pulled models (/api/tags). Ensure each configured model is pulled.",
|
|
389
|
+
)
|
|
390
|
+
elif not configured_models:
|
|
391
|
+
table.add_row(
|
|
392
|
+
"OLLAMA models",
|
|
393
|
+
"[yellow]WARN[/yellow]",
|
|
394
|
+
"No role models configured; run 'dev setup --provider ollama --model <model>'.",
|
|
395
|
+
)
|
|
396
|
+
else:
|
|
397
|
+
missing = [m for m in configured_models if not _ollama_model_present(m, pulled)]
|
|
398
|
+
if missing:
|
|
399
|
+
pulls = "; ".join(f"ollama pull {m}" for m in missing)
|
|
400
|
+
table.add_row(
|
|
401
|
+
"OLLAMA models",
|
|
402
|
+
"[yellow]WARN[/yellow]",
|
|
403
|
+
f"Configured model(s) not pulled: {', '.join(missing)}. Pull first: {pulls}.",
|
|
404
|
+
)
|
|
405
|
+
else:
|
|
406
|
+
table.add_row(
|
|
407
|
+
"OLLAMA models",
|
|
408
|
+
"[green]OK[/green]",
|
|
409
|
+
f"All configured models present locally ({', '.join(configured_models)}).",
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if num_ctx is None:
|
|
413
|
+
table.add_row(
|
|
414
|
+
"OLLAMA num_ctx",
|
|
415
|
+
"[yellow]WARN[/yellow]",
|
|
416
|
+
f"OLLAMA_NUM_CTX not set — Ollama's small default (~2048-4096) will "
|
|
417
|
+
f"truncate DevCouncil's large planning prompts. Set OLLAMA_NUM_CTX={recommended_ctx}.",
|
|
418
|
+
)
|
|
419
|
+
elif num_ctx < min_ctx:
|
|
420
|
+
table.add_row(
|
|
421
|
+
"OLLAMA num_ctx",
|
|
422
|
+
"[yellow]WARN[/yellow]",
|
|
423
|
+
f"OLLAMA_NUM_CTX={num_ctx} may be too small for planning prompts "
|
|
424
|
+
f"(~{MAX_PROMPT_CHARS // 4} tokens); recommend >= {recommended_ctx}.",
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
table.add_row("OLLAMA num_ctx", "[green]OK[/green]", f"context window = {num_ctx} tokens.")
|
|
428
|
+
|
|
429
|
+
# Local model size is bounded by host memory — unified RAM on Apple Silicon,
|
|
430
|
+
# VRAM on a discrete-GPU box, system RAM otherwise. Surface a model that will
|
|
431
|
+
# actually fit on this host (any OS), not a one-size default.
|
|
432
|
+
from devcouncil import hardware
|
|
433
|
+
|
|
434
|
+
host = hardware.describe_host()
|
|
435
|
+
table.add_row(
|
|
436
|
+
host.platform_label,
|
|
437
|
+
"[green]OK[/green]",
|
|
438
|
+
f"{host.chip_label}, {host.memory_label}. "
|
|
439
|
+
f"Recommended local model: {host.recommended_ollama_model} "
|
|
440
|
+
f"(dev setup --provider ollama --model {host.recommended_ollama_model}).",
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
_add_logging_row(table, project_root)
|
|
111
444
|
console.print(table)
|
|
112
445
|
return
|
|
113
446
|
env_var = provider_api_key_env_var(provider)
|
|
@@ -116,9 +449,33 @@ def render_doctor_check(project_root: Path = Path(".")):
|
|
|
116
449
|
table.add_row(env_var, "[green]OK[/green]", f"Found in environment for {provider}.")
|
|
117
450
|
elif local_secrets.get(env_var):
|
|
118
451
|
table.add_row(env_var, "[green]OK[/green]", f"Found in .devcouncil/secrets.env for {provider}.")
|
|
452
|
+
elif provider == "vertexai" and get_gcloud_access_token():
|
|
453
|
+
table.add_row(env_var, "[green]OK[/green]", "Resolvable via gcloud auth print-access-token.")
|
|
454
|
+
table.caption = "Resolvable via gcloud auth print-access-token."
|
|
119
455
|
else:
|
|
120
456
|
table.add_row(env_var, "[yellow]Missing[/yellow]", f"Required if using {provider} provider. Run 'dev setup'.")
|
|
121
457
|
|
|
458
|
+
if provider == "vertexai":
|
|
459
|
+
project = (
|
|
460
|
+
os.environ.get("VERTEXAI_PROJECT")
|
|
461
|
+
or os.environ.get("GOOGLE_CLOUD_PROJECT")
|
|
462
|
+
or local_secrets.get("VERTEXAI_PROJECT")
|
|
463
|
+
or local_secrets.get("GOOGLE_CLOUD_PROJECT")
|
|
464
|
+
)
|
|
465
|
+
if project:
|
|
466
|
+
source = "environment" if os.environ.get("VERTEXAI_PROJECT") or os.environ.get("GOOGLE_CLOUD_PROJECT") else ".devcouncil/secrets.env"
|
|
467
|
+
table.add_row("VERTEXAI_PROJECT", "[green]OK[/green]", f"Found in {source}.")
|
|
468
|
+
else:
|
|
469
|
+
table.add_row(
|
|
470
|
+
"VERTEXAI_PROJECT",
|
|
471
|
+
"[yellow]Missing[/yellow]",
|
|
472
|
+
"Required for vertexai. Run 'dev setup --provider vertexai --vertex-project PROJECT_ID'.",
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
location = os.environ.get("VERTEXAI_LOCATION") or local_secrets.get("VERTEXAI_LOCATION", "global")
|
|
476
|
+
table.add_row("VERTEXAI_LOCATION", "[green]OK[/green]", location)
|
|
477
|
+
|
|
478
|
+
_add_logging_row(table, project_root)
|
|
122
479
|
console.print(table)
|
|
123
480
|
|
|
124
481
|
|