Skip to content

Commit b82ed64

Browse files
authored
Implementation Plan: T5-P4-A2-WP2 Make build_inspector_cmd a Proper Capability-Gated Method (#4093)
## Summary Replace the unchecked `RuntimeError` stubs in both `ClaudeCodeBackend.build_inspector_cmd` and `CodexBackend.build_inspector_cmd` with a proper `CapabilityNotSupportedError` that is raised only when `inspector_capable=False`. This involves: 1. Defining `CapabilityNotSupportedError(Exception)` in `core/types/_type_exceptions.py` with structured `capability` and `backend_name` fields 2. Replacing `RuntimeError` raises in both backends with `CapabilityNotSupportedError` 3. Removing `inspector_capable` from `_FORWARD_DECLARED` (it now has a production consumer) 4. Updating conformance tests to assert the new exception type 5. Adding a parametrized arch test in `test_backend_behavioral_contract.py` ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/impl-20260612-102320-415368/.autoskillit/temp/make-plan/t5-p4-a2-wp2-capability-gated-build-inspector-cmd_plan_2026-06-12_103500.md` Closes #4017 🤖 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 | 3.5k | 11.9k | 1.1M | 94.3k | 40 | 73.3k | 10m 33s | | verify* | sonnet | 1 | 172 | 13.7k | 1.1M | 65.3k | 72 | 44.4k | 4m 17s | | implement* | MiniMax-M3 | 1 | 2.0M | 8.4k | 0 | 0 | 74 | 0 | 4m 16s | | fix* | sonnet | 1 | 126 | 5.2k | 692.4k | 59.5k | 42 | 38.6k | 5m 34s | | audit_impl* | sonnet | 1 | 46 | 16.1k | 168.1k | 43.3k | 17 | 38.9k | 5m 34s | | prepare_pr* | MiniMax-M3 | 1 | 196.1k | 2.3k | 0 | 0 | 13 | 0 | 55s | | compose_pr* | MiniMax-M3 | 1 | 176.6k | 1.2k | 0 | 0 | 11 | 0 | 46s | | **Total** | | | 2.4M | 58.9k | 3.1M | 94.3k | | 195.2k | 31m 56s | \* *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 | 50 | 0.0 | 0.0 | 167.2 | | fix | 7 | 98910.0 | 5514.4 | 745.6 | | audit_impl | 0 | — | — | — | | prepare_pr | 0 | — | — | — | | compose_pr | 0 | — | — | — | | **Total** | **57** | 53800.9 | 3425.0 | 1033.6 | ## Model Usage Breakdown | Model | steps | uncached | output | cache_read | cache_write | time | |-------|-------|----------|--------|------------|-------------|------| | opus[1m] | 1 | 3.5k | 11.9k | 1.1M | 73.3k | 10m 33s | | sonnet | 3 | 344 | 35.1k | 1.9M | 121.9k | 15m 26s | | MiniMax-M3 | 3 | 2.4M | 11.9k | 0 | 0 | 5m 56s |
1 parent 8840c6d commit b82ed64

10 files changed

Lines changed: 44 additions & 20 deletions

src/autoskillit/core/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ from .types import BackgroundSupervisor as BackgroundSupervisor
223223
from .types import BareResume as BareResume
224224
from .types import CampaignProtector as CampaignProtector
225225
from .types import CanonicalTokenUsage as CanonicalTokenUsage
226+
from .types import CapabilityNotSupportedError as CapabilityNotSupportedError
226227
from .types import CaptureEntrySpec as CaptureEntrySpec
227228
from .types import CaptureValueType as CaptureValueType
228229
from .types import CaptureValueTypeError as CaptureValueTypeError

src/autoskillit/core/types/_type_exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
__all__ = [
6+
"CapabilityNotSupportedError",
67
"RecipeLoadError",
78
"ProcessStaleError",
89
"RecipeNotFoundError",
@@ -19,3 +20,12 @@ class ProcessStaleError(RecipeLoadError):
1920

2021
class RecipeNotFoundError(RecipeLoadError):
2122
"""Named recipe could not be found in any scan directory."""
23+
24+
25+
class CapabilityNotSupportedError(Exception):
26+
"""Backend does not support the requested capability."""
27+
28+
def __init__(self, capability: str, backend_name: str) -> None:
29+
self.capability = capability
30+
self.backend_name = backend_name
31+
super().__init__(f"{backend_name!r} does not support capability {capability!r}")

src/autoskillit/execution/backends/claude.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
BackendConventions,
2828
BackendEventKind,
2929
BareResume,
30+
CapabilityNotSupportedError,
3031
ClaudeDirectoryConventions,
3132
ClaudeEventData,
3233
ClaudeFlags,
@@ -865,4 +866,7 @@ def ensure_pre_launch(self) -> list[str]:
865866
return []
866867

867868
def build_inspector_cmd(self, prompt: str, *, model: str = "") -> CmdSpec:
868-
raise RuntimeError("build_inspector_cmd not yet implemented — lands in #3534")
869+
if not self.capabilities.inspector_capable:
870+
raise CapabilityNotSupportedError("inspector_capable", self.name)
871+
msg = "inspector_capable is True but build_inspector_cmd has no implementation"
872+
raise AssertionError(msg)

src/autoskillit/execution/backends/codex.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
BackendCapabilities,
3636
BackendConventions,
3737
BareResume,
38+
CapabilityNotSupportedError,
3839
ClaudeDirectoryConventions,
3940
CmdSpec,
4041
CodexEventType,
@@ -1054,4 +1055,7 @@ def ensure_pre_launch(self) -> list[str]:
10541055
return errors
10551056

10561057
def build_inspector_cmd(self, prompt: str, *, model: str = "") -> CmdSpec:
1057-
raise RuntimeError("build_inspector_cmd not yet implemented — lands in #3534")
1058+
if not self.capabilities.inspector_capable:
1059+
raise CapabilityNotSupportedError("inspector_capable", self.name)
1060+
msg = "inspector_capable is True but build_inspector_cmd has no implementation"
1061+
raise AssertionError(msg)

tests/_test_filter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ class ImportContext(enum.StrEnum):
248248
"_type_constants_registries": frozenset(
249249
{"cli", "config", "core", "pipeline", "recipe", "server", "workspace"}
250250
),
251-
"_type_exceptions": frozenset({"core", "fleet", "recipe", "server"}),
251+
"_type_exceptions": frozenset({"core", "execution", "fleet", "recipe", "server"}),
252252
"_type_phoropter": frozenset({"core"}),
253253
"_type_tradition_manifest": frozenset({"core"}),
254254
"_step_context": frozenset({"core", "execution", "pipeline", "server"}),

tests/arch/test_capability_consumption.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,6 @@ class ForwardDeclaredField:
4444
rationale="MCP env forwarding — enforcement arch test exists, awaiting src/ consumer",
4545
added_date=date(2026, 5, 31),
4646
),
47-
"inspector_capable": ForwardDeclaredField(
48-
issue=3533,
49-
rationale="Health Inspector capability gating — production consumer in #3574",
50-
added_date=date(2026, 6, 1),
51-
),
5247
"required_session_files": ForwardDeclaredField(
5348
issue=3134,
5449
rationale=(

tests/arch/test_subpackage_isolation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -982,7 +982,7 @@ def test_data_directories_are_not_python_packages() -> None:
982982
"dispatch gate add defense-in-depth checks",
983983
),
984984
"execution/backends/codex.py": (
985-
1060,
985+
1062,
986986
"REQ-CNST-010-E9: Codex backend — skill_sigil capability threading adds multi-line "
987987
"keyword args to _ensure_skill_prefix call sites and _has_prefix guard; "
988988
"write_guard_tool_names env injection adds 7 lines to _codex_exec_extras; "
@@ -994,7 +994,8 @@ def test_data_directories_are_not_python_packages() -> None:
994994
"; project_log_dir method added to CodexSessionLocator (+3 net lines)"
995995
"; session_log_path method added to CodexSessionLocator (+5 net lines)"
996996
"; process_idle_timeout_ms field wired through build_skill_session_cmd and "
997-
"build_food_truck_cmd CmdSpec constructors (+8 net lines)",
997+
"build_food_truck_cmd CmdSpec constructors (+8 net lines)"
998+
"; CapabilityNotSupportedError capability-gate in build_inspector_cmd (+1 net line)",
998999
),
9991000
}
10001001

tests/execution/backends/test_backend_behavioral_contract.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import pytest
88

9-
from autoskillit.core import DirectInstall
9+
from autoskillit.core import CapabilityNotSupportedError, DirectInstall
1010
from autoskillit.execution.backends import BACKEND_REGISTRY
1111
from autoskillit.execution.backends.codex import CodexBackend
1212

@@ -22,6 +22,10 @@
2222
"completion_marker": "%%X%%",
2323
},
2424
),
25+
"inspector_capable": (
26+
"build_inspector_cmd",
27+
{"prompt": "test prompt"},
28+
),
2529
"session_resume_capable": (
2630
"build_resume_cmd",
2731
{"resume_session_id": "x", "prompt": "x"},
@@ -44,9 +48,14 @@ def test_true_bool_capabilities_have_non_raising_implementations(
4448
except (RuntimeError, NotImplementedError) as exc:
4549
pytest.fail(f"{backend_name}.{method_name}() raised {type(exc).__name__}: {exc}")
4650

47-
def test_inspector_capable_is_false(self, backend_name: str) -> None:
51+
def test_incapable_backends_raise_capability_not_supported(self, backend_name: str) -> None:
4852
backend = BACKEND_REGISTRY[backend_name]()
49-
assert backend.capabilities.inspector_capable is False
53+
if backend.capabilities.inspector_capable:
54+
pytest.skip("backend is inspector_capable")
55+
with pytest.raises(CapabilityNotSupportedError) as exc_info:
56+
backend.build_inspector_cmd("test prompt")
57+
assert exc_info.value.capability == "inspector_capable"
58+
assert exc_info.value.backend_name == backend_name
5059

5160

5261
class TestWriteDetectionStrategyRouting:

tests/execution/backends/test_coding_agent_backend_conformance.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from autoskillit.core import (
1212
BackendCapabilities,
1313
BackendConventions,
14+
CapabilityNotSupportedError,
1415
CmdSpec,
1516
CodingAgentBackend,
1617
DirectInstall,
@@ -28,7 +29,6 @@
2829

2930
NOT_YET_LIVE: frozenset[str] = frozenset(
3031
{
31-
"inspector_capable",
3232
"min_version",
3333
"patch_format",
3434
"required_session_files",
@@ -276,9 +276,10 @@ def test_build_inspector_cmd_raises_when_not_capable(self) -> None:
276276
"""BackendCapabilities.inspector_capable — raises when not capable."""
277277
if self.backend.capabilities.inspector_capable:
278278
pytest.skip("backend is inspector_capable — not testing the not-capable path")
279-
with pytest.raises(RuntimeError) as exc_info:
279+
with pytest.raises(CapabilityNotSupportedError) as exc_info:
280280
self.backend.build_inspector_cmd("test prompt")
281-
assert "not yet implemented" in str(exc_info.value)
281+
assert exc_info.value.capability == "inspector_capable"
282+
assert exc_info.value.backend_name == self.backend.name
282283

283284
# --- Group 6: Behavioral Contracts ---
284285

tests/test_test_filter_core_cascade.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def test_type_constants_registries_cascade(self) -> None:
211211

212212
def test_type_exceptions_cascade(self) -> None:
213213
assert MODULE_CASCADE_CORE["_type_exceptions"] == frozenset(
214-
{"core", "fleet", "recipe", "server"}
214+
{"core", "execution", "fleet", "recipe", "server"}
215215
)
216216

217217
def test_step_context_cascade(self) -> None:
@@ -670,7 +670,7 @@ def test_type_constants_registries_narrow_cascade(self, tmp_path: Path) -> None:
670670
)
671671

672672
def test_type_exceptions_narrow_cascade(self, tmp_path: Path) -> None:
673-
"""_type_exceptions → narrow cascade of 4 dirs."""
673+
"""_type_exceptions → narrow cascade of 5 dirs."""
674674
tests_root = self._make_tests_root(tmp_path, self.ALL_DIRS)
675675
result = build_test_scope(
676676
changed_files={"src/autoskillit/core/types/_type_exceptions.py"},
@@ -679,12 +679,11 @@ def test_type_exceptions_narrow_cascade(self, tmp_path: Path) -> None:
679679
)
680680
assert result is not None
681681
dir_names = {p.name for p in result}
682-
for pkg in ["core", "fleet", "recipe", "server"]:
682+
for pkg in ["core", "execution", "fleet", "recipe", "server"]:
683683
assert pkg in dir_names, f"_type_exceptions cascade should include {pkg}"
684684
for excluded in [
685685
"cli",
686686
"config",
687-
"execution",
688687
"pipeline",
689688
"migration",
690689
"workspace",

0 commit comments

Comments
 (0)