Skip to content

Commit 774b83b

Browse files
Trecekclaude
andauthored
Implementation Plan: T5-P6-A7-WP1 Provide a Single Doctor Check Surfacing Four Codex Backend Graduation Criteria (#4127)
## Summary Add `_check_codex_graduation` to the doctor runtime module. The function evaluates four graduation criteria for the `codex_backend` feature (version check, probe-harness cache, matrix last-run, sessions.jsonl smoke run), assembles a pipe-delimited status summary, and returns `Severity.INFO` with an EXPERIMENTAL hold note when any criterion is non-green. Register as Check 33 in `__init__.py` and add 5 test scenarios to `test_doctor_backend_guards.py`. Note: The issue references "Check 34" but the current highest check number is 32 (Codex MCP tool_timeout_sec coherence). The next sequential number is 33. This plan uses 33 to maintain sequential ordering. Closes #4056 ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/impl-20260627-064010-500578/.autoskillit/temp/make-plan/t5_p6_a7_wp1_codex_graduation_doctor_check_plan_2026-06-27_070000.md` 🤖 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 | 1.4k | 18.6k | 1.2M | 98.3k | 44 | 84.2k | 13m 7s | | verify* | sonnet | 1 | 76 | 10.3k | 385.4k | 54.9k | 26 | 36.4k | 4m 50s | | implement* | MiniMax-M3 | 1 | 65.3k | 8.4k | 1.6M | 0 | 67 | 0 | 3m 39s | | fix* | sonnet | 1 | 360 | 16.5k | 2.9M | 86.7k | 108 | 68.2k | 8m 23s | | audit_impl* | sonnet | 1 | 44 | 11.5k | 164.7k | 43.5k | 14 | 36.5k | 5m 20s | | prepare_pr* | MiniMax-M3 | 1 | 41.2k | 2.8k | 156.3k | 0 | 13 | 0 | 42s | | compose_pr* | MiniMax-M3 | 1 | 36.1k | 1.3k | 176.4k | 0 | 12 | 0 | 29s | | **Total** | | | 144.6k | 69.3k | 6.6M | 98.3k | | 225.4k | 36m 34s | \* *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 | 192 | 8428.7 | 0.0 | 43.8 | | fix | 16 | 182832.3 | 4261.9 | 1032.5 | | audit_impl | 0 | — | — | — | | prepare_pr | 0 | — | — | — | | compose_pr | 0 | — | — | — | | **Total** | **208** | 31778.6 | 1083.4 | 333.3 | ## Model Usage Breakdown | Model | steps | uncached | output | cache_read | cache_write | time | |-------|-------|----------|--------|------------|-------------|------| | opus[1m] | 1 | 1.4k | 18.6k | 1.2M | 84.2k | 13m 7s | | sonnet | 3 | 480 | 38.3k | 3.5M | 141.2k | 18m 35s | | MiniMax-M3 | 3 | 142.7k | 12.5k | 2.0M | 0 | 4m 51s | --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 09fd4ed commit 774b83b

6 files changed

Lines changed: 203 additions & 8 deletions

File tree

src/autoskillit/cli/doctor/AGENTS.md

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

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

55
## Files
66

@@ -15,7 +15,7 @@ Diagnostic health checks for the autoskillit installation (28 checks).
1515
| `_doctor_hooks.py` | Hook registration, executability, and registry drift checks |
1616
| `_doctor_install.py` | Install path, entry points, version drift, update dismissal checks |
1717
| `_doctor_mcp.py` | MCP server registration, dual registration, plugin cache checks |
18-
| `_doctor_runtime.py` | Quota cache schema version, claude process state, and codex version checks |
18+
| `_doctor_runtime.py` | Quota cache schema version, claude process state, codex version, and codex graduation checks |
1919

2020
## Architecture Notes
2121

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_codex_graduation,
6465
_check_codex_version,
6566
_check_quota_cache_schema,
6667
_check_script_binary,
@@ -213,6 +214,9 @@ def run_doctor(*, output_json: bool = False) -> None:
213214
_check_codex_mcp_timeouts(backend=_backend, run_skill=cfg.run_skill, fleet=cfg.fleet)
214215
)
215216

217+
# Check 33: Codex graduation criteria
218+
results.append(_check_codex_graduation(backend=_backend))
219+
216220
# Output
217221
if output_json:
218222
print(

src/autoskillit/cli/doctor/_doctor_runtime.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88

99
import regex as re
1010

11-
from autoskillit.core import CodingAgentBackend, Severity, get_logger
11+
from autoskillit.core import (
12+
CodingAgentBackend,
13+
Severity,
14+
default_log_dir,
15+
get_logger,
16+
)
1217
from autoskillit.execution import QUOTA_CACHE_SCHEMA_VERSION
1318

1419
from ._doctor_types import DoctorResult
@@ -192,3 +197,76 @@ def _check_script_binary() -> DoctorResult:
192197
"script(1) present but -qefc flags unsupported — PTY wrapping may not work correctly",
193198
)
194199
return DoctorResult(Severity.OK, check_name, "script(1) available with -qefc support")
200+
201+
202+
def _check_codex_graduation(
203+
*,
204+
backend: CodingAgentBackend | None = None,
205+
log_dir: Path | None = None,
206+
) -> DoctorResult:
207+
check_name = "codex_graduation"
208+
209+
if backend is None or not backend.capabilities.version_check_command:
210+
return DoctorResult(Severity.OK, check_name, "Skipped (no codex backend)")
211+
212+
backend_name = backend.name
213+
log_root = log_dir or default_log_dir()
214+
215+
# Criterion 1: version check
216+
version_result = _check_codex_version(backend=backend)
217+
version_status = "pass" if version_result.severity == Severity.OK else "fail"
218+
219+
# Criterion 2: probe-harness cache
220+
probe_path = log_root / "codex-probe-cache.json"
221+
probe_status = "not-yet-run"
222+
try:
223+
raw = json.loads(probe_path.read_text())
224+
entries = raw.get("entries", {}) if isinstance(raw, dict) else {}
225+
if entries:
226+
probe_status = (
227+
"pass"
228+
if any(isinstance(e, dict) and e.get("passed") for e in entries.values())
229+
else "fail"
230+
)
231+
except (OSError, json.JSONDecodeError, ValueError):
232+
pass
233+
234+
# Criterion 3: matrix last-run result
235+
matrix_path = log_root / "codex-matrix-result.json"
236+
matrix_status = "not-yet-run"
237+
try:
238+
matrix_raw = json.loads(matrix_path.read_text())
239+
if isinstance(matrix_raw, dict) and "passed" in matrix_raw:
240+
matrix_status = "pass" if matrix_raw["passed"] else "fail"
241+
except (OSError, json.JSONDecodeError, ValueError):
242+
pass
243+
244+
# Criterion 4: sessions.jsonl smoke
245+
sessions_path = log_root / "sessions.jsonl"
246+
smoke_status = "not-found"
247+
try:
248+
for line in reversed(sessions_path.read_text().splitlines()):
249+
line = line.strip()
250+
if not line:
251+
continue
252+
try:
253+
entry = json.loads(line)
254+
except (json.JSONDecodeError, ValueError):
255+
continue
256+
if entry.get("backend") == backend_name:
257+
smoke_status = "pass" if entry.get("success") else "fail"
258+
break
259+
except OSError:
260+
pass
261+
262+
statuses = [version_status, probe_status, matrix_status, smoke_status]
263+
summary = (
264+
f"version={version_status} | probe={probe_status}"
265+
f" | matrix={matrix_status} | smoke={smoke_status}"
266+
)
267+
268+
if not all(s == "pass" for s in statuses):
269+
pending = sum(1 for s in statuses if s != "pass")
270+
summary += f" — EXPERIMENTAL hold: {pending} of 4 criteria pending"
271+
272+
return DoctorResult(Severity.INFO, check_name, summary)

tests/cli/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ CLI command, subcommand, and interactive workflow tests.
2828
| `test_cook_order_prompt.py` | Tests: cook CLI order command — system prompt content, MCP prefix selection, ownership |
2929
| `test_cook_workspace.py` | Tests: cook CLI workspace init and clean commands |
3030
| `test_doctor.py` | Tests for CLI doctor command and related utilities |
31-
| `test_doctor_backend_guards.py` | Tests for doctor backend guard checks (stale MCP, MCP registered, process state) and run_doctor backend wiring |
31+
| `test_doctor_backend_guards.py` | Tests for doctor backend guard checks (stale MCP, MCP registered, process state, codex graduation) and run_doctor backend wiring |
3232
| `test_doctor_codex_mcp.py` | Tests for _check_mcp_server_registered Codex branch — monkeypatch-based, no filesystem I/O |
3333
| `test_doctor_fleet_checks.py` | Tests for fleet doctor checks — Group M ambient env/infra/campaign, Group N feature gates and registry |
3434
| `test_doctor_migration.py` | Tests for doctor quota cache schema, install classification, version consistency, and drift |

tests/cli/test_doctor_backend_guards.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import json
56
from pathlib import Path
67
from types import SimpleNamespace
78

@@ -579,3 +580,114 @@ def test_version_above_minimum_returns_ok(self, monkeypatch: pytest.MonkeyPatch)
579580
result = _check_codex_version(backend=CodexBackend())
580581
assert result.severity == Severity.OK
581582
assert "0.131.0" in result.message
583+
584+
585+
class TestCheckCodexGraduation:
586+
"""Tests for _check_codex_graduation doctor check (Check 33)."""
587+
588+
def _codex_result(self, stdout: str, returncode: int = 0, stderr: str = ""):
589+
return type(
590+
"CompletedProcess",
591+
(),
592+
{"returncode": returncode, "stdout": stdout, "stderr": stderr},
593+
)()
594+
595+
def test_skip_for_none_backend(self) -> None:
596+
from autoskillit.cli.doctor._doctor_runtime import _check_codex_graduation
597+
from autoskillit.core import Severity
598+
599+
result = _check_codex_graduation(backend=None)
600+
assert result.severity == Severity.OK
601+
assert result.check == "codex_graduation"
602+
assert "skipped" in result.message.lower()
603+
604+
def test_skip_for_non_codex_backend(self) -> None:
605+
from types import SimpleNamespace
606+
607+
from autoskillit.cli.doctor._doctor_runtime import _check_codex_graduation
608+
from autoskillit.core import Severity
609+
610+
stub = SimpleNamespace(capabilities=SimpleNamespace(version_check_command=""))
611+
result = _check_codex_graduation(backend=stub)
612+
assert result.severity == Severity.OK
613+
assert "skipped" in result.message.lower()
614+
615+
def test_all_green_omits_hold_note(
616+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
617+
) -> None:
618+
import subprocess
619+
from datetime import UTC, datetime
620+
621+
from autoskillit.cli.doctor._doctor_runtime import _check_codex_graduation
622+
from autoskillit.core import Severity
623+
from autoskillit.execution.backends.codex import CodexBackend
624+
625+
monkeypatch.setattr(
626+
subprocess,
627+
"run",
628+
lambda *a, **kw: self._codex_result("Codex 0.130.0\n"),
629+
)
630+
ts = datetime.now(UTC).isoformat()
631+
(tmp_path / "codex-probe-cache.json").write_text(
632+
json.dumps(
633+
{
634+
"schema_version": 1,
635+
"entries": {"0.130.0": {"passed": True, "probe_timestamp": ts}},
636+
}
637+
)
638+
)
639+
(tmp_path / "codex-matrix-result.json").write_text(json.dumps({"passed": True}))
640+
(tmp_path / "sessions.jsonl").write_text(
641+
json.dumps({"backend": "codex", "success": True}) + "\n"
642+
)
643+
644+
result = _check_codex_graduation(backend=CodexBackend(), log_dir=tmp_path)
645+
assert result.severity == Severity.INFO
646+
assert "version=pass" in result.message
647+
assert "probe=pass" in result.message
648+
assert "matrix=pass" in result.message
649+
assert "smoke=pass" in result.message
650+
assert "EXPERIMENTAL" not in result.message
651+
652+
def test_non_green_includes_hold_note(
653+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
654+
) -> None:
655+
import subprocess
656+
657+
from autoskillit.cli.doctor._doctor_runtime import _check_codex_graduation
658+
from autoskillit.core import Severity
659+
from autoskillit.execution.backends.codex import CodexBackend
660+
661+
monkeypatch.setattr(
662+
subprocess,
663+
"run",
664+
lambda *a, **kw: self._codex_result("Codex 0.130.0\n"),
665+
)
666+
(tmp_path / "sessions.jsonl").write_text(
667+
json.dumps({"backend": "codex", "success": True}) + "\n"
668+
)
669+
670+
result = _check_codex_graduation(backend=CodexBackend(), log_dir=tmp_path)
671+
assert result.severity == Severity.INFO
672+
assert "not-yet-run" in result.message
673+
assert "EXPERIMENTAL" in result.message
674+
675+
def test_absent_files_yield_not_yet_run(
676+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
677+
) -> None:
678+
import subprocess
679+
680+
from autoskillit.cli.doctor._doctor_runtime import _check_codex_graduation
681+
from autoskillit.core import Severity
682+
from autoskillit.execution.backends.codex import CodexBackend
683+
684+
monkeypatch.setattr(
685+
subprocess,
686+
"run",
687+
lambda *a, **kw: self._codex_result("Codex 0.130.0\n"),
688+
)
689+
result = _check_codex_graduation(backend=CodexBackend(), log_dir=tmp_path)
690+
assert result.severity == Severity.INFO
691+
assert "probe=not-yet-run" in result.message
692+
assert "matrix=not-yet-run" in result.message
693+
assert "smoke=not-found" in result.message

tests/docs/test_doc_counts.py

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

239239

240-
def test_doctor_check_count_is_31() -> None:
241-
# 38 total = 17 numbered base + 7 lettered sub-checks (2b–2e, 4b, 7b, 7c)
240+
def test_doctor_check_count_is_40() -> None:
241+
# 40 total = 17 numbered base + 7 lettered sub-checks (2b–2e, 4b, 7b, 7c)
242242
# + 4 ambient env (18–21) + 2 feature (22–23) + 6 gated franchise (24–29)
243-
# + 1 codex version (30) + 1 script binary (31).
243+
# + 1 codex version (30) + 1 script binary (31) + 1 MCP timeouts (32)
244+
# + 1 codex graduation (33).
244245
# The docs claim 17 user-visible checks; the gap is intentional (Check 2/4/7
245246
# split into sub-markers here but appear as single entries in docs).
246247
# Update both tests whenever a new doctor check is added.
247248
count = _count_doctor_checks()
248-
assert count == 39, f"Expected 39 doctor checks; found {count}"
249+
assert count == 40, f"Expected 40 doctor checks; found {count}"
249250

250251

251252
def test_bundled_recipe_count_is_15() -> None:

0 commit comments

Comments
 (0)