Skip to content

Commit 5e7cc96

Browse files
Trecekclaude
andauthored
Implementation Plan: T5-P5-A10-WP1 — CLI Config-Acceptance Probe (Check 34) (#4140)
## Summary Add `_check_cli_conformance_probes` to `src/autoskillit/cli/doctor/_doctor_runtime.py` as a new doctor check (Check 34) that writes a minimal TOML config to a temp directory, invokes the backend CLI with a config override flag, and reports whether the CLI accepted the config. Follows the exact gating pattern from `_check_codex_version`: skip when backend is `None` or `version_check_command` is empty, skip when CLI is unavailable or times out. **Check numbering note:** The issue references "Check 33" but the current codebase already assigns Check 33 to `_check_codex_graduation` (line 217–218 of `__init__.py`). This plan assigns the new check as **Check 34**, appended after Check 33. ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/impl-20260628-044315-253111/.autoskillit/temp/make-plan/T5-P5-A10-WP1_cli_conformance_probes_plan_2026-06-28_050100.md` Closes #4028 🤖 Generated with [Claude Code](https://claude.com/claude-code) via AutoSkillit <!-- autoskillit:pipeline-signature steps=prepare_pr,run_arch_lenses,compose_pr,annotate_pr_diff,review_pr --> ## Token Usage Summary | Step | Model | count | uncached | output | cache_read | peak_ctx | turns | cache_write | time | |------|-------|-------|----------|--------|------------|----------|-------|-------------|------| | plan* | opus[1m] | 1 | 61 | 19.5k | 1.8M | 98.9k | 58 | 81.4k | 12m 42s | | verify* | sonnet | 1 | 76 | 8.1k | 387.0k | 54.1k | 24 | 35.6k | 4m 10s | | implement* | MiniMax-M3 | 1 | 75.4k | 10.2k | 1.8M | 0 | 76 | 0 | 6m 51s | | fix* | sonnet | 1 | 126 | 3.9k | 663.6k | 52.6k | 35 | 34.1k | 3m 29s | | audit_impl* | sonnet | 1 | 54 | 8.8k | 214.2k | 48.4k | 17 | 40.2k | 3m 17s | | prepare_pr* | MiniMax-M3 | 1 | 43.4k | 1.8k | 153.9k | 0 | 12 | 0 | 1m 10s | | compose_pr* | MiniMax-M3 | 1 | 37.1k | 1.5k | 213.4k | 0 | 15 | 0 | 54s | | **Total** | | | 156.2k | 53.9k | 5.2M | 98.9k | | 191.3k | 32m 35s | \* *Step used a non-Anthropic provider; caching behavior may differ.* ## Token Efficiency | Step | LoC Changed | cache_read/LoC | cache_write/LoC | output/LoC | |------|-------------|----------------|-----------------|------------| | plan | 0 | — | — | — | | verify | 0 | — | — | — | | implement | 173 | 10138.6 | 0.0 | 58.7 | | fix | 2 | 331794.0 | 17058.5 | 1942.5 | | audit_impl | 0 | — | — | — | | prepare_pr | 0 | — | — | — | | compose_pr | 0 | — | — | — | | **Total** | **175** | 29773.9 | 1093.1 | 307.8 | ## Model Usage Breakdown | Model | steps | uncached | output | cache_read | cache_write | time | |-------|-------|----------|--------|------------|-------------|------| | opus[1m] | 1 | 61 | 19.5k | 1.8M | 81.4k | 12m 42s | | sonnet | 3 | 256 | 20.8k | 1.3M | 109.9k | 10m 57s | | MiniMax-M3 | 3 | 155.9k | 13.5k | 2.1M | 0 | 8m 55s | --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1a6564e commit 5e7cc96

6 files changed

Lines changed: 181 additions & 11 deletions

File tree

docs/installation.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ projects need.
6767

6868
autoskillit doctor
6969

70-
Doctor runs 28 checks (23 numbered + 5 lettered sub-checks: `2b`, `2c`, `2d`, `4b`, `7b`); up to 33 with the fleet feature enabled.
71-
Enumerated by `run_doctor` in `src/autoskillit/cli/_doctor.py`:
70+
Doctor runs 33 checks (23 numbered + 5 lettered sub-checks: `2b`, `2c`, `2d`, `4b`, `7b` + 5 backend runtime probes, checks 30–34); up to 39 with the fleet feature enabled.
71+
Enumerated by `run_doctor` in `src/autoskillit/cli/doctor/__init__.py`:
7272

7373
| # | Check | What it verifies |
7474
|---|-------|------------------|
@@ -101,6 +101,12 @@ Enumerated by `run_doctor` in `src/autoskillit/cli/_doctor.py`:
101101
| 22 | Feature dependency consistency | Enabled features satisfy their declared dependencies |
102102
| 23 | Feature registry import consistency | All feature gate modules import without errors |
103103
| 24–28 | Fleet infrastructure | Sous-chef skill, dispatch guard, stale state, onboarding, clone collisions (fleet feature only) |
104+
| 29 | Fleet state schema | Fleet state schema version drift (fleet feature only) |
105+
| 30 | Codex version | Codex CLI version meets minimum requirement |
106+
| 31 | script(1) binary | PTY binary availability with -qefc support |
107+
| 32 | MCP timeouts | Codex MCP tool_timeout_sec coherence |
108+
| 33 | Codex graduation | Multi-criteria graduation readiness (version, probe, matrix, smoke) |
109+
| 34 | CLI conformance | Backend CLI accepts minimal TOML config probe |
104110

105111
See **[Hooks](safety/hooks.md)** for what each PreToolUse / PostToolUse /
106112
SessionStart hook actually enforces.

src/autoskillit/cli/doctor/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# doctor/
22

3-
Diagnostic health checks for the autoskillit installation (33 checks).
3+
Diagnostic health checks for the autoskillit installation (34 checks).
44

55
## Files
66

src/autoskillit/cli/doctor/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
)
6262
from ._doctor_runtime import (
6363
_check_claude_process_state_breakdown,
64+
_check_cli_conformance_probes,
6465
_check_codex_graduation,
6566
_check_codex_version,
6667
_check_quota_cache_schema,
@@ -217,6 +218,9 @@ def run_doctor(*, output_json: bool = False) -> None:
217218
# Check 33: Codex graduation criteria
218219
results.append(_check_codex_graduation(backend=_backend))
219220

221+
# Check 34: CLI config-acceptance probe
222+
results.append(_check_cli_conformance_probes(backend=_backend))
223+
220224
# Output
221225
if output_json:
222226
print(

src/autoskillit/cli/doctor/_doctor_runtime.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
from __future__ import annotations
44

55
import json
6+
import shlex
67
import subprocess
8+
import tempfile
79
from pathlib import Path
810

911
import regex as re
1012

1113
from autoskillit.core import (
1214
CodingAgentBackend,
1315
Severity,
16+
atomic_write,
1417
default_log_dir,
1518
get_logger,
1619
)
@@ -270,3 +273,47 @@ def _check_codex_graduation(
270273
summary += f" — EXPERIMENTAL hold: {pending} of 4 criteria pending"
271274

272275
return DoctorResult(Severity.INFO, check_name, summary)
276+
277+
278+
def _check_cli_conformance_probes(*, backend: CodingAgentBackend | None = None) -> DoctorResult:
279+
check_name = "cli_conformance_probes"
280+
281+
if backend is None or not backend.capabilities.version_check_command:
282+
return DoctorResult(Severity.OK, check_name, "Skipped (no version check command)")
283+
284+
cli_argv = shlex.split(backend.capabilities.version_check_command)
285+
try:
286+
subprocess.run(cli_argv, capture_output=True, text=True, timeout=5)
287+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
288+
return DoctorResult(Severity.OK, check_name, "CLI unavailable; skipping config probe")
289+
290+
cli_binary = cli_argv[0]
291+
result = None
292+
with tempfile.TemporaryDirectory() as tmpdir:
293+
probe_path = Path(tmpdir) / "probe.toml"
294+
atomic_write(probe_path, 'model = "test-model"\n')
295+
try:
296+
result = subprocess.run(
297+
[cli_binary, "-c", str(probe_path), "--version"],
298+
capture_output=True,
299+
text=True,
300+
timeout=10,
301+
)
302+
except subprocess.TimeoutExpired:
303+
return DoctorResult(
304+
Severity.OK,
305+
check_name,
306+
f"{cli_binary} config probe timed out; skipping",
307+
)
308+
except (FileNotFoundError, OSError):
309+
return DoctorResult(Severity.OK, check_name, "CLI unavailable; skipping config probe")
310+
311+
if result is not None and result.returncode == 0:
312+
return DoctorResult(Severity.OK, check_name, f"{cli_binary} accepted minimal config probe")
313+
if result is None:
314+
return DoctorResult(Severity.OK, check_name, "CLI unavailable; skipping config probe")
315+
return DoctorResult(
316+
Severity.WARNING,
317+
check_name,
318+
f"{cli_binary} rejected config probe (exit {result.returncode})",
319+
)

tests/cli/test_doctor_backend_guards.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,3 +691,117 @@ def test_absent_files_yield_not_yet_run(
691691
assert "probe=not-yet-run" in result.message
692692
assert "matrix=not-yet-run" in result.message
693693
assert "smoke=not-found" in result.message
694+
695+
696+
class TestCheckCliConformanceProbes:
697+
"""Tests for _check_cli_conformance_probes doctor check (Check 34)."""
698+
699+
def test_skip_for_none_backend(self) -> None:
700+
from autoskillit.cli.doctor._doctor_runtime import _check_cli_conformance_probes
701+
from autoskillit.core import Severity
702+
703+
result = _check_cli_conformance_probes(backend=None)
704+
assert result.severity == Severity.OK
705+
assert result.check == "cli_conformance_probes"
706+
assert "skipped" in result.message.lower()
707+
708+
def test_skip_for_backend_without_version_check_command(self) -> None:
709+
from types import SimpleNamespace
710+
711+
from autoskillit.cli.doctor._doctor_runtime import _check_cli_conformance_probes
712+
from autoskillit.core import Severity
713+
714+
stub = SimpleNamespace(capabilities=SimpleNamespace(version_check_command=""))
715+
result = _check_cli_conformance_probes(backend=stub)
716+
assert result.severity == Severity.OK
717+
assert "skipped" in result.message.lower()
718+
719+
def test_skip_when_cli_unavailable(self, monkeypatch: pytest.MonkeyPatch) -> None:
720+
from autoskillit.cli.doctor._doctor_runtime import _check_cli_conformance_probes
721+
from autoskillit.core import Severity
722+
from autoskillit.execution.backends.codex import CodexBackend
723+
724+
def _raise(*a, **kw):
725+
raise FileNotFoundError("codex")
726+
727+
monkeypatch.setattr("autoskillit.cli.doctor._doctor_runtime.subprocess.run", _raise)
728+
result = _check_cli_conformance_probes(backend=CodexBackend())
729+
assert result.severity == Severity.OK
730+
assert "unavailable" in result.message.lower()
731+
732+
def test_skip_when_cli_times_out(self, monkeypatch: pytest.MonkeyPatch) -> None:
733+
import subprocess
734+
735+
from autoskillit.cli.doctor._doctor_runtime import _check_cli_conformance_probes
736+
from autoskillit.core import Severity
737+
from autoskillit.execution.backends.codex import CodexBackend
738+
739+
call_count = 0
740+
741+
def _stateful_fake(*a, **kw):
742+
nonlocal call_count
743+
call_count += 1
744+
if call_count == 1:
745+
return type(
746+
"CompletedProcess", (), {"returncode": 0, "stdout": "", "stderr": ""}
747+
)()
748+
raise subprocess.TimeoutExpired(cmd="codex", timeout=10)
749+
750+
monkeypatch.setattr(
751+
"autoskillit.cli.doctor._doctor_runtime.subprocess.run", _stateful_fake
752+
)
753+
result = _check_cli_conformance_probes(backend=CodexBackend())
754+
assert result.severity == Severity.OK
755+
assert "timed out" in result.message.lower()
756+
757+
def test_ok_when_config_accepted(self, monkeypatch: pytest.MonkeyPatch) -> None:
758+
import subprocess
759+
760+
from autoskillit.cli.doctor._doctor_runtime import _check_cli_conformance_probes
761+
from autoskillit.core import Severity
762+
from autoskillit.execution.backends.codex import CodexBackend
763+
764+
calls = []
765+
766+
def _fake_run(*a, **kw):
767+
calls.append(a)
768+
return type(
769+
"CompletedProcess",
770+
(),
771+
{"returncode": 0, "stdout": "codex 0.130.0\n", "stderr": ""},
772+
)()
773+
774+
monkeypatch.setattr(subprocess, "run", _fake_run)
775+
result = _check_cli_conformance_probes(backend=CodexBackend())
776+
assert result.severity == Severity.OK
777+
assert "accepted" in result.message.lower()
778+
assert len(calls) == 2
779+
780+
def test_warning_when_config_rejected(self, monkeypatch: pytest.MonkeyPatch) -> None:
781+
import subprocess
782+
783+
from autoskillit.cli.doctor._doctor_runtime import _check_cli_conformance_probes
784+
from autoskillit.core import Severity
785+
from autoskillit.execution.backends.codex import CodexBackend
786+
787+
call_count = 0
788+
789+
def _fake_run(*a, **kw):
790+
nonlocal call_count
791+
call_count += 1
792+
if call_count == 1:
793+
return type(
794+
"CompletedProcess",
795+
(),
796+
{"returncode": 0, "stdout": "codex 0.130.0\n", "stderr": ""},
797+
)()
798+
return type(
799+
"CompletedProcess",
800+
(),
801+
{"returncode": 1, "stdout": "", "stderr": "unknown flag"},
802+
)()
803+
804+
monkeypatch.setattr(subprocess, "run", _fake_run)
805+
result = _check_cli_conformance_probes(backend=CodexBackend())
806+
assert result.severity == Severity.WARNING
807+
assert "rejected" in result.message.lower() or "exit 1" in result.message

tests/docs/test_doc_counts.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,16 +237,15 @@ def test_quota_thresholds_defaults() -> None:
237237
assert long_ == pytest.approx(95.0)
238238

239239

240-
def test_doctor_check_count_is_40() -> None:
241-
# 40 total = 17 numbered base + 7 lettered sub-checks (2b–2e, 4b, 7b, 7c)
242-
# + 4 ambient env (18–21) + 2 feature (22–23) + 6 gated franchise (24–29)
243-
# + 1 codex version (30) + 1 script binary (31) + 1 MCP timeouts (32)
244-
# + 1 codex graduation (33).
245-
# The docs claim 17 user-visible checks; the gap is intentional (Check 2/4/7
246-
# split into sub-markers here but appear as single entries in docs).
240+
def test_doctor_check_count_is_41() -> None:
241+
# 41 total = 23 numbered (1–17 base + 18–21 ambient env + 22–23 feature)
242+
# + 7 lettered sub-checks (2b–2e, 4b, 7b, 7c)
243+
# + 6 gated fleet (24–29) + 5 backend runtime (30–34).
244+
# Docs list 33 user-visible checks (23 numbered + 5 documented lettered +
245+
# 5 backend runtime); 2e and 7c are internal-only sub-checks not shown in docs.
247246
# Update both tests whenever a new doctor check is added.
248247
count = _count_doctor_checks()
249-
assert count == 40, f"Expected 40 doctor checks; found {count}"
248+
assert count == 41, f"Expected 41 doctor checks; found {count}"
250249

251250

252251
def test_bundled_recipe_count_is_15() -> None:

0 commit comments

Comments
 (0)