Skip to content

Commit 45e5b63

Browse files
Merge pull request #437 from math-inc/codex/public-managed-doctor-20260328
Add managed workflow checks to gauss doctor
2 parents ef5298c + bad46f1 commit 45e5b63

3 files changed

Lines changed: 315 additions & 1 deletion

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ gauss update
106106

107107
```
108108
gauss # Launch the CLI
109-
/project create ~/my-project # Create a new Lean project
109+
/project create ~/my-project --template-source <template-or-git-url>
110110
/prove 1+1=2 # Spawn a proving agent
111111
/swarm # See running agents
112112
```
@@ -138,6 +138,10 @@ Gauss treats Lean work as project-scoped by default. Before launching managed wo
138138
- `/project use [path]` pins the current session to an existing Gauss project
139139
- `/project clear` removes the session override and falls back to ambient project discovery
140140

141+
If you use `/project create` often, set a default template once with
142+
`gauss.project.template_source` in `~/.gauss/config.yaml` or the
143+
`GAUSS_BLUEPRINT_TEMPLATE_SOURCE` environment variable.
144+
141145
Gauss discovers `.gauss/project.yaml` upward from the current working directory, but managed workflow child agents launch from the active project root so the forwarded Lean workflow command always runs in the right project context.
142146

143147
## Workflow commands
@@ -169,6 +173,9 @@ Gauss discovers `.gauss/project.yaml` upward from the current working directory,
169173
- An active Gauss project selected via `.gauss/project.yaml`
170174

171175
Gauss checks these before launch and tells you exactly what is missing.
176+
`gauss doctor` also reports the managed-workflow backend, auth mode, `uv` / `lake`
177+
availability, and whether the current working directory resolves to an active
178+
Gauss project.
172179

173180
---
174181

gauss_cli/doctor.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import subprocess
1010
import shutil
1111
from pathlib import Path
12+
from typing import Any, Mapping
1213

1314
from gauss_cli.branding import get_cli_command_name, get_product_name, rewrite_cli_references
1415
from gauss_cli.config import get_project_root, get_gauss_home, get_env_path
@@ -106,6 +107,189 @@ def _check_gateway_service_linger(issues: list[str]) -> None:
106107
check_warn("Could not verify systemd linger", f"({linger_detail})")
107108

108109

110+
def _check_managed_workflow_requirements(
111+
issues: list[str],
112+
*,
113+
config: Mapping[str, Any] | None = None,
114+
env: Mapping[str, str] | None = None,
115+
active_cwd: str | Path | None = None,
116+
cli_name: str | None = None,
117+
) -> None:
118+
"""Report Lean workflow prerequisites that are specific to Open Gauss."""
119+
try:
120+
from gauss_cli.autoformalize import (
121+
CODEX_AUTOFORMALIZE_BACKEND,
122+
DEFAULT_AUTOFORMALIZE_BACKEND,
123+
_codex_auth_payload_has_api_key,
124+
_codex_auth_payload_is_valid,
125+
_has_local_claude_api_key,
126+
_has_local_claude_login,
127+
_load_local_codex_auth_payload,
128+
_resolve_auth_mode,
129+
_resolve_backend_name,
130+
_resolve_claude_auth_env,
131+
_resolve_codex_api_key,
132+
_resolve_uv_runner,
133+
)
134+
from gauss_cli.project import (
135+
ProjectManifestError,
136+
ProjectNotFoundError,
137+
discover_gauss_project,
138+
resolve_template_source,
139+
)
140+
except Exception as exc:
141+
print()
142+
print(color("◆ Managed Lean Workflows", Colors.CYAN, Colors.BOLD))
143+
check_warn("Managed workflow diagnostics unavailable", f"({exc})")
144+
return
145+
146+
from gauss_cli.config import load_config
147+
148+
cli_name = cli_name or get_cli_command_name()
149+
environment = dict(env or os.environ)
150+
resolved_config = config if isinstance(config, Mapping) else load_config()
151+
152+
print()
153+
print(color("◆ Managed Lean Workflows", Colors.CYAN, Colors.BOLD))
154+
155+
try:
156+
backend_name = _resolve_backend_name(resolved_config, environment)
157+
auth_mode = _resolve_auth_mode(resolved_config, environment)
158+
except Exception as exc:
159+
check_fail("Managed workflow config", f"({exc})")
160+
issues.append(f"Fix gauss.autoformalize config: {exc}")
161+
return
162+
163+
check_ok("Managed backend", f"({backend_name})")
164+
check_ok("Managed auth mode", f"({auth_mode})")
165+
166+
template_source = resolve_template_source(resolved_config, environment)
167+
if template_source:
168+
check_ok("Project template source", f"({template_source})")
169+
else:
170+
check_warn(
171+
"Project template source",
172+
"(`/project create` needs `--template-source` or `gauss.project.template_source`)",
173+
)
174+
175+
try:
176+
uv_runner = _resolve_uv_runner(environment)
177+
check_ok("uv / uvx", f"({Path(uv_runner[0]).name})")
178+
except Exception as exc:
179+
check_fail("uv / uvx", f"({exc})")
180+
issues.append("Install uv so Gauss can launch the managed Lean MCP server")
181+
182+
lake_executable = shutil.which("lake", path=environment.get("PATH"))
183+
if lake_executable:
184+
check_ok("Lean toolchain (lake)", f"({lake_executable})")
185+
else:
186+
check_fail("Lean toolchain (lake)", "(required for managed Lean workflows)")
187+
issues.append("Install elan / Lean so `lake` is available on PATH")
188+
189+
active_dir = Path(active_cwd or environment.get("TERMINAL_CWD") or os.getcwd()).expanduser().resolve()
190+
if active_dir.exists():
191+
check_ok("Active working directory", f"({active_dir})")
192+
try:
193+
project = discover_gauss_project(active_dir)
194+
check_ok("Active Gauss project", f"({project.label})")
195+
except ProjectNotFoundError:
196+
check_warn("Active Gauss project", f"(none under {active_dir})")
197+
issues.append(
198+
"Create or select a Gauss project with `/project init`, `/project convert`, or `/project use <path>`"
199+
)
200+
except ProjectManifestError as exc:
201+
check_fail("Gauss project manifest", f"({exc})")
202+
issues.append(f"Repair the active Gauss project manifest: {exc}")
203+
else:
204+
check_fail("Active working directory", f"({active_dir} does not exist)")
205+
issues.append(f"Fix the managed workflow working directory: {active_dir}")
206+
207+
real_home = Path(environment.get("HOME", str(Path.home()))).expanduser().resolve()
208+
209+
if backend_name == DEFAULT_AUTOFORMALIZE_BACKEND:
210+
claude_executable = shutil.which("claude", path=environment.get("PATH"))
211+
if claude_executable:
212+
check_ok("Claude Code CLI", f"({claude_executable})")
213+
else:
214+
check_fail("Claude Code CLI", "(install with `npm install -g @anthropic-ai/claude-code`)")
215+
issues.append("Install the Claude Code CLI for managed Lean workflows")
216+
217+
has_local_login = _has_local_claude_login(real_home)
218+
has_local_api_key = _has_local_claude_api_key(real_home)
219+
staged_auth_env = _resolve_claude_auth_env(environment, include_persisted_env=True)
220+
staged_keys = ", ".join(sorted(staged_auth_env))
221+
222+
if auth_mode == "auto":
223+
if has_local_login:
224+
check_ok("Claude auth", "(local Claude login)")
225+
elif has_local_api_key:
226+
check_ok("Claude auth", "(local Claude API key)")
227+
elif staged_auth_env:
228+
check_ok("Claude auth", f"(staged via {staged_keys})")
229+
else:
230+
check_fail("Claude auth", "(not found)")
231+
issues.append(
232+
"Run `claude auth login`, save `ANTHROPIC_API_KEY`, or set `gauss.autoformalize.auth_mode: login`"
233+
)
234+
elif auth_mode == "login":
235+
if has_local_login:
236+
check_ok("Claude auth", "(local Claude login)")
237+
elif has_local_api_key:
238+
check_warn("Claude auth", "(login mode ignores local Claude API keys; first launch will prompt)")
239+
else:
240+
check_warn("Claude auth", "(login mode will prompt on first launch)")
241+
else:
242+
if staged_auth_env:
243+
check_ok("Claude API-key auth", f"(staged via {staged_keys})")
244+
elif has_local_api_key:
245+
check_ok("Claude API-key auth", "(local Claude API key)")
246+
else:
247+
check_fail("Claude API-key auth", "(not found)")
248+
issues.append(
249+
"Save `ANTHROPIC_API_KEY` / `ANTHROPIC_TOKEN` / `CLAUDE_CODE_OAUTH_TOKEN`, or switch `gauss.autoformalize.auth_mode` back to `auto` or `login`"
250+
)
251+
return
252+
253+
if backend_name == CODEX_AUTOFORMALIZE_BACKEND:
254+
codex_executable = shutil.which("codex", path=environment.get("PATH"))
255+
if codex_executable:
256+
check_ok("Codex CLI", f"({codex_executable})")
257+
else:
258+
check_fail("Codex CLI", "(install the OpenAI Codex CLI)")
259+
issues.append("Install the OpenAI Codex CLI for managed Lean workflows")
260+
261+
local_auth_payload = _load_local_codex_auth_payload(real_home, environment)
262+
has_local_auth = _codex_auth_payload_is_valid(local_auth_payload)
263+
has_local_api_key = _codex_auth_payload_has_api_key(local_auth_payload)
264+
staged_api_key = _resolve_codex_api_key(environment, include_persisted_env=True)
265+
266+
if auth_mode == "auto":
267+
if has_local_auth:
268+
check_ok("Codex auth", "(local Codex auth)")
269+
elif staged_api_key:
270+
check_ok("Codex auth", "(staged OPENAI_API_KEY)")
271+
else:
272+
check_fail("Codex auth", "(not found)")
273+
issues.append(
274+
"Run `codex login`, save `OPENAI_API_KEY`, or set `gauss.autoformalize.auth_mode: login`"
275+
)
276+
elif auth_mode == "login":
277+
if has_local_auth:
278+
check_ok("Codex auth", "(local Codex auth)")
279+
else:
280+
check_warn("Codex auth", "(login mode will prompt on first launch)")
281+
else:
282+
if staged_api_key:
283+
check_ok("Codex API-key auth", "(staged OPENAI_API_KEY)")
284+
elif has_local_api_key:
285+
check_ok("Codex API-key auth", "(local Codex API key)")
286+
else:
287+
check_fail("Codex API-key auth", "(not found)")
288+
issues.append(
289+
"Save `OPENAI_API_KEY`, or switch `gauss.autoformalize.auth_mode` back to `auto` or `login`"
290+
)
291+
292+
109293
def run_doctor(args):
110294
"""Run diagnostic checks."""
111295
should_fix = getattr(args, 'fix', False)
@@ -485,6 +669,8 @@ def run_doctor(args):
485669
except Exception:
486670
pass
487671

672+
_check_managed_workflow_requirements(issues, cli_name=cli_name)
673+
488674
# =========================================================================
489675
# Check: API connectivity
490676
# =========================================================================

tests/gauss_cli/test_doctor.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
"""Tests for gauss_cli.doctor."""
22

3+
import json
34
import os
45
import sys
56
import types
67
from argparse import Namespace
8+
from pathlib import Path
79

810
import pytest
911

12+
import gauss_cli.autoformalize as autoformalize_mod
1013
import gauss_cli.doctor as doctor
1114
import gauss_cli.gateway as gateway_cli
1215
from gauss_cli import doctor as doctor_mod
1316
from gauss_cli.doctor import _has_provider_env_config
17+
from gauss_cli.project import initialize_gauss_project
1418

1519

1620
class TestProviderEnvDetection:
@@ -101,3 +105,120 @@ def test_check_gateway_service_linger_skips_when_service_not_installed(monkeypat
101105
out = capsys.readouterr().out
102106
assert out == ""
103107
assert issues == []
108+
109+
110+
def _write_executable(path: Path) -> None:
111+
path.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8")
112+
path.chmod(0o755)
113+
114+
115+
def test_check_managed_workflow_requirements_reports_healthy_claude_setup(tmp_path, capsys):
116+
bin_dir = tmp_path / "bin"
117+
home_dir = tmp_path / "home"
118+
project_root = tmp_path / "project"
119+
bin_dir.mkdir()
120+
(home_dir / ".claude").mkdir(parents=True)
121+
for name in ("claude", "uvx", "lake"):
122+
_write_executable(bin_dir / name)
123+
124+
(home_dir / ".claude" / ".credentials.json").write_text(
125+
json.dumps({"claudeAiOauth": {"accessToken": "token"}}),
126+
encoding="utf-8",
127+
)
128+
project_root.mkdir()
129+
(project_root / "lakefile.lean").write_text("-- lean project\n", encoding="utf-8")
130+
initialize_gauss_project(project_root, name="Demo Project")
131+
132+
issues: list[str] = []
133+
doctor._check_managed_workflow_requirements(
134+
issues,
135+
config={
136+
"gauss": {
137+
"autoformalize": {"backend": "claude-code", "auth_mode": "auto"},
138+
"project": {"template_source": "https://example.com/template.git"},
139+
}
140+
},
141+
env={"PATH": str(bin_dir), "HOME": str(home_dir)},
142+
active_cwd=project_root,
143+
cli_name="gauss",
144+
)
145+
146+
output = capsys.readouterr().out
147+
assert "Managed Lean Workflows" in output
148+
assert "Managed backend" in output
149+
assert "claude-code" in output
150+
assert "Active Gauss project" in output
151+
assert "Demo Project" in output
152+
assert "Claude auth" in output
153+
assert "local Claude login" in output
154+
assert issues == []
155+
156+
157+
def test_check_managed_workflow_requirements_reports_missing_codex_prereqs(monkeypatch, tmp_path, capsys):
158+
missing_project_dir = tmp_path / "outside"
159+
home_dir = tmp_path / "home"
160+
missing_project_dir.mkdir()
161+
home_dir.mkdir()
162+
monkeypatch.setattr(autoformalize_mod, "get_env_value", lambda _key: None)
163+
164+
issues: list[str] = []
165+
doctor._check_managed_workflow_requirements(
166+
issues,
167+
config={
168+
"gauss": {
169+
"autoformalize": {"backend": "codex", "auth_mode": "api-key"},
170+
}
171+
},
172+
env={"PATH": "", "HOME": str(home_dir)},
173+
active_cwd=missing_project_dir,
174+
cli_name="gauss",
175+
)
176+
177+
output = capsys.readouterr().out
178+
assert "Codex CLI" in output
179+
assert "Codex API-key auth" in output
180+
assert "Active Gauss project" in output
181+
assert "uv / uvx" in output
182+
assert "Lean toolchain (lake)" in output
183+
assert any("Codex CLI" in issue or "OpenAI Codex CLI" in issue for issue in issues)
184+
assert any("OPENAI_API_KEY" in issue for issue in issues)
185+
assert any("/project init" in issue for issue in issues)
186+
assert any("uv" in issue for issue in issues)
187+
assert any("lake" in issue for issue in issues)
188+
189+
190+
def test_check_managed_workflow_requirements_warns_when_claude_login_mode_only_has_api_key(tmp_path, capsys):
191+
bin_dir = tmp_path / "bin"
192+
home_dir = tmp_path / "home"
193+
project_root = tmp_path / "project"
194+
bin_dir.mkdir()
195+
(home_dir / ".claude").mkdir(parents=True)
196+
for name in ("claude", "uvx", "lake"):
197+
_write_executable(bin_dir / name)
198+
199+
(home_dir / ".claude.json").write_text(
200+
json.dumps({"primaryApiKey": "sk-ant-api-key"}),
201+
encoding="utf-8",
202+
)
203+
project_root.mkdir()
204+
(project_root / "lakefile.lean").write_text("-- lean project\n", encoding="utf-8")
205+
initialize_gauss_project(project_root, name="Login Mode Project")
206+
207+
issues: list[str] = []
208+
doctor._check_managed_workflow_requirements(
209+
issues,
210+
config={
211+
"gauss": {
212+
"autoformalize": {"backend": "claude-code", "auth_mode": "login"},
213+
"project": {"template_source": "https://example.com/template.git"},
214+
}
215+
},
216+
env={"PATH": str(bin_dir), "HOME": str(home_dir)},
217+
active_cwd=project_root,
218+
cli_name="gauss",
219+
)
220+
221+
output = capsys.readouterr().out
222+
assert "Claude auth" in output
223+
assert "login mode ignores local Claude API keys" in output
224+
assert issues == []

0 commit comments

Comments
 (0)