feat: three-tier skill visibility, subset access control, and overrides#413
Conversation
- Rename 'automation' → 'autoskillit' on all 39 MCP tools
- Move 11 formerly-ungated tools into 'kitchen' tier (GATED_TOOLS)
- Add HEADLESS_TOOLS = frozenset({'test_check'}) constant
- Add FREE_RANGE_TOOLS = frozenset({'open_kitchen', 'close_kitchen'})
- Redefine UNGATED_TOOLS = FREE_RANGE_TOOLS (was 13, now 2)
- Remove WORKER_TOOLS and HEADLESS_BLOCKED_UNGATED_TOOLS constants
- Add CATEGORY_TAGS constant with functional category names
- Add 'headless' tag to test_check, remove _require_enabled() guard
- Change server/__init__.py headless enable: 'kitchen' → 'headless'
- Remove gate.enable() pre-open in _factory.py headless block
- Remove _require_not_headless() guards from kitchen-gated tools
- Expand _ALL_TOOLS in rules_tools.py to include HEADLESS_TOOLS
- Apply category tags (github/ci/clone/telemetry) to relevant tools
- Update all affected tests for new three-tier model
- Update CLAUDE.md and docs/architecture.md tool counts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…at closed gate Recipe tools (list_recipes, load_recipe, validate_recipe) had _require_enabled() correctly added in the implementation but test fixtures (_close_kitchen) were not updated — the tests still tested "ungated" behavior. Root cause: formerly-ungated recipe/status tools moved to kitchen tier require both the 'kitchen' FastMCP tag AND _require_enabled() gate checks. The test classes' _close_kitchen fixtures (and missing tool_ctx in some tests) caused gate error returns instead of real results. Fixes: - Remove _close_kitchen from TestRecipeTools, TestValidateRecipeTool, TestLoadRecipeDiagram — tools are gated, gate must be open to call them - Add _ensure_ctx autouse fixture to TestRecipeTools, TestValidateRecipeTool, TestMigrationSuggestions, TestLoadRecipeReadOnly to supply tool_ctx - Add tool_ctx param to TestLoadRecipeDiagram test methods - Replace null-ctx standalone tests with recipes=None variants - Remove _close_kitchen from TestGetTokenSummaryMcpResponses (bogus fixture) All 4297 tests pass.
Creates the filesystem foundation for the three-tier skill visibility system: - Add SkillSource.BUNDLED_EXTENDED enum member to core/types.py - Add bundled_skills_extended_dir() and update SkillResolver to scan both skills/ and skills_extended/ directories (workspace/skills.py) - Migrate 57 skills from skills/ to skills_extended/ (Tier 2: 41, Tier 3: 16); skills/ retains only open-kitchen, close-kitchen, sous-chef (Tier 1) - Add SkillsConfig dataclass with tier1/tier2/tier3 fields and duplication validation; wire into AutomationConfig (config/settings.py) - Add skills: section to defaults.yaml with canonical tier assignments - Update config/__init__.py to export SkillsConfig - Update check_doc_counts.py to count both skills/ and skills_extended/ - Update existing tests that break due to filesystem migration; add new tests for all Part A requirements (tests/workspace/test_skills.py, tests/config/test_config.py) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All test files that hardcoded paths to skills/ or used bundled_skills_dir() for skills that moved to skills_extended/ are updated. Also: - workspace/__init__.py exports bundled_skills_extended_dir - recipe/contracts.py check_contract_staleness uses SkillResolver when skills_dir=None so it finds skills in both directories - write-recipe SKILL.md removes internal sous-chef from bundled skills list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-plugin-dir, run_skill effective_add_dir - Remove TIER2_SKILLS constant from session_skills.py and workspace/__init__.py - Update DefaultSessionSkillManager.init_session() to accept config: AutomationConfig | None; loads config from load_config() if None, warns on unknown skill names, drives tier2 gating from config.skills.tier2 - Update SkillsDirectoryProvider.get_skill_content() to use tier2_gated flag directly (caller-determined, no TIER2_SKILLS lookup) - Add --plugin-dir pkg_root() and load_config() wiring to cli/_chefs_hat.py - Default run_skill add_dir to bundled_skills_extended_dir() when not explicitly set - Update tests: remove TIER2_SKILLS refs, update injection/cook tests to use mermaid with explicit config, add 4 new tests (constant removed, config param, unknown warn, disable injection), add CH-6 plugin-dir test, add run_skill add_dir default test, add bundled_skills_extended_dir to workspace export contract Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove deferred load_config() from workspace/session_skills.py; when
config=None, default tier2_skills to empty frozenset (no gating) so
workspace stays L0-only
- Replace tools_execution.py workspace.skills import with inline
pkg_root() / "skills_extended" to satisfy server/tools_*.py allowed-
import constraint ({core, pipeline, server, config})
- Fix test_provider_does_not_inject_for_cook_session to use mermaid
(no flag at rest) instead of open-kitchen (always has the flag)
- Fix test_init_session_unknown_skill_logs_warning to use
structlog.testing.capture_logs() instead of stdlib caplog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…o.categories Tests T1-T5 cover SubsetsConfig defaults, load_config integration, unknown-category warnings, and config package export. Tests T6 cover read_skill_categories(), SkillInfo.categories field, and per-skill category assertions for github/ci/arch-lens/audit groups. All tests fail against the current codebase and will pass after Part B implements SubsetsConfig, read_skill_categories(), and SKILL.md frontmatter additions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and SKILL.md categories
- Add SubsetsConfig dataclass (disabled: list[str], custom_tags: dict[str,list[str]])
to config/settings.py with stdlib logging warning for unknown categories
- Add subsets: section to config/defaults.yaml (disabled: [], custom_tags: {})
- Export SubsetsConfig from config/__init__.py
- Add read_skill_categories(Path) -> frozenset[str] to workspace/skills.py
(parses YAML frontmatter using core.load_yaml, uses open() to bypass monkeypatches)
- Add categories: frozenset[str] field to SkillInfo (default frozenset())
- Populate categories in SkillResolver.resolve() and _scan_directory()
- Add categories: frontmatter to 37 bundled SKILL.md files:
github (14): analyze-prs, collapse-issues, enrich-issues, issue-splitter, merge-pr,
open-integration-pr, open-pr, pipeline-summary, prepare-issue, report-bug,
resolve-merge-conflicts, resolve-review, review-pr, triage-issues
ci (1): diagnose-ci
arch-lens (15): all arch-lens-* + make-arch-diag + verify-diag
audit (7): audit-arch, audit-bugs, audit-cohesion, audit-defense-standards,
audit-friction, audit-impl, audit-tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enforcement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n, and skill init
- _initialize(): call mcp.disable(tags={subset}) for each config.subsets.disabled entry
- open_kitchen(): re-disable subsets after enable_components to handle FastMCP session override
- init_session(): add _is_skill_disabled() helper and skip disabled-subset skills when copying
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ST-008 @mcp.tool handlers must not contain for-loops (arch rule). Extract the subset re-disable iteration into _redisable_subsets() and call it from open_kitchen(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…al overrides Implements TDD test suite for issue #306 REQ-OVR-001..004: - T-OVR-001..006: detect_project_local_overrides() detection function - T-OVR-007..011: init_session() project_dir override filtering - T-OVR-012..014: run_headless_core multi-path add_dirs support - T-OVR-015..018: project-local-skill-override recipe validation rule All 15 substantive tests fail against the current codebase as expected for TDD. 3 negative-assertion tests (T-OVR-016..018) pass correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-001..004) - Add detect_project_local_overrides() to workspace/skills.py scanning .claude/skills/<name>/SKILL.md and .autoskillit/skills/<name>/SKILL.md - Add project_dir parameter to init_session() with override-skip logic and debug logging (init_session_override_skip event) - Rename add_dir → add_dirs: Sequence[str] in run_headless_core and DefaultHeadlessExecutor.run() for multi-path --add-dir emission loop - Update HeadlessExecutor and SessionSkillManager protocols in core/types.py - Update run_skill in tools_execution.py to pass [skills_extended/, cwd] as add_dirs so project-local skills resolve in headless sessions - Pass project_dir=Path.cwd() from chefs-hat to init_session() - Add project-local-skill-override WARNING recipe validation rule - Re-export detect_project_local_overrides from workspace/__init__.py - Update test fixtures for add_dir → add_dirs signature change All T-OVR-001..018 tests now pass. 4360 total tests green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Maps each MCP tool name to its functional category subset tag (github, ci, clone, telemetry). Exported from core/__init__.py for use by recipe validation rules (REQ-VAL-004). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds disabled_subsets: frozenset[str] field to ValidationContext dataclass and corresponding parameter to make_validation_context(). Defaults to empty frozenset for backwards compatibility. Enables subset-aware semantic rules to check whether a recipe references disabled tools/skills. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New WARNING rule that detects run_skill steps referencing a bundled skill whose functional category (e.g. 'github') is in ctx.disabled_subsets. Emits RuleFinding with rule='subset-disabled-skill' and the blocking subset name in the message. Skips dynamic skill names and non-skill steps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a recipe step uses a known MCP tool that belongs to a disabled subset, emit RuleFinding with rule='subset-disabled-tool' (WARNING) instead of silently passing. Truly unknown tools continue to emit 'unknown-tool' (ERROR). The @semantic_rule registration name stays 'unknown-tool' while the finding rule name differs per case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _launch_cook_session gains extra_env param to inject env overrides - _get_subsets_needed helper extracts needed subset names from findings - _enable_subsets_permanently helper removes subsets from config.yaml - cook() runs subset-disabled gate after structural validation: non-TTY exits with error; TTY offers enable-temporarily (AUTOSKILLIT_SUBSETS__DISABLED=@JSON []), enable-permanently (edits .autoskillit/config.yaml), or cancel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ook gate T-VAL-001..007: Recipe semantic rule tests for subset-disabled-tool and subset-disabled-skill (new file tests/recipe/test_rules_subset_disabled.py). T-VAL-008..010: Cook-time subset gate tests in TestCookSubsetGate class (non-interactive hard error, enable-temporarily env override, enable-permanently config update). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shorten long docstrings in test_rules_subset_disabled.py and test_cook.py to satisfy ruff's 99-char line limit. No logic changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- recipe/__init__.py: export make_validation_context via validator import (fulfills plan Step 2 note: confirm in __init__ __all__) - cli/app.py: replace submodule imports with public autoskillit.recipe surface to satisfy REQ-IMP-004 and cross-package submodule import rules Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rides docs - docs/skill-visibility.md: three-tier skill system (REQ-DOC-001/002/004) - docs/subset-categories.md: functional subset categories (REQ-DOC-003) - docs/mcp-tool-access.md: complete 39-tool access control map (REQ-DOC-005/008) - docs/project-local-overrides.md: project-local skill overrides (REQ-DOC-006) - CLAUDE.md: two-directory skills layout, gate.py/server/__init__.py/session_skills.py entries (REQ-DOC-007) - docs/architecture.md: 37 kitchen tools, session model expanded to 4 modes - docs/configuration.md: AUTOSKILLIT_HEADLESS accuracy fix, Skill Visibility and Subset Categories sections - scripts/check_doc_counts.py: fix tool counting to include HEADLESS_TOOLS + FREE_RANGE_TOOLS (total=39, gated=37) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit PR Review — Verdict: changes_requested (23 blocking issues found; 2 critical arch violations, 1 critical bug, 14 warnings, 6 decision items)
| cwd, | ||
| model=model, | ||
| add_dir=add_dir, | ||
| add_dirs=[str(pkg_root() / "skills_extended"), cwd], |
There was a problem hiding this comment.
[warning] arch: run_skill hardcodes add_dirs=[str(pkg_root() / 'skills_extended'), cwd], embedding session directory composition policy in the server layer. This policy belongs in the execution or workspace layer, not in the MCP tool handler.
| cwd, | ||
| model=model, | ||
| add_dir=add_dir, | ||
| add_dirs=[str(pkg_root() / "skills_extended"), cwd], |
There was a problem hiding this comment.
[warning] arch: run_skill hardcodes add_dirs=[str(pkg_root() / 'skills_extended'), cwd], embedding session directory composition policy in the server layer. This policy belongs in the execution or workspace layer, not in the MCP tool handler.
|
|
||
|
|
||
| @mcp.tool(tags={"automation", "kitchen"}, annotations={"readOnlyHint": True}) | ||
| @mcp.tool(tags={"autoskillit", "kitchen", "headless"}, annotations={"readOnlyHint": True}) |
There was a problem hiding this comment.
[warning] bugs: test_check carries both 'kitchen' (disabled at startup) and 'headless' tags. Correctness depends entirely on FastMCP using OR semantics for tag-based visibility (any enabled tag makes tool visible). If FastMCP uses AND semantics, test_check remains invisible in headless sessions since 'kitchen' stays disabled. This assumption is undocumented and untested.
…sertions, add mock guard - test_cook.py: simplify env extraction in T-VAL-009 to use kwargs['env'] directly instead of fragile ternary; remove disjunctive 'github' fallback in T-VAL-008 assertion to test only the specific error message - test_chefs_hat.py: add mock_mgr.init_session.assert_called_once() to confirm the mock was actually used during chefs_hat() - test_config.py: use caplog.at_level(..., logger='autoskillit.config.settings') to capture warnings regardless of logger propagate setting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ring The prior commit removed the notification-free sentence from get_quota_events to fix the stale ungated-tools justification. A contract test (P5-1) checks that this tool documents its notification-free behavior. Restore the fact without the stale justification. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add CH-7, TestBuildInteractiveCmdExtended, and T-OVR-012..014 tests that define expected behavior for the two architectural fixes: - CH-7: --dangerously-skip-permissions must appear in chefs-hat subprocess - TestBuildInteractiveCmdExtended: build_interactive_cmd must accept plugin_dir/add_dirs - T-OVR-012..014: cook_session=True must bypass all skill filters Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1: build_interactive_cmd() now accepts plugin_dir and add_dirs, making it the sole path to construct an interactive command. chefs-hat uses the builder so --dangerously-skip-permissions can never be omitted. Bug 2: _should_inject_skill() centralizes all skill-filtering logic. cook_session=True bypasses every restriction at the top of the helper, making it structurally impossible for future filters to suppress cook sessions. Subset-disable and project-local-override filters are unreachable when cook_session=True. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit PR Review — Verdict: changes_requested. Found 21 actionable findings (2 additional critical findings at tests/server/test_server_init.py L1265 and L1280 are outside diff hunks and included in this summary only). See inline comments.
|
|
||
| # Maps each MCP tool name to its functional category subset tags. | ||
| # Mirrors the FastMCP @mcp.tool(tags=...) category assignments in the server layer. | ||
| # Tools with no functional category are absent from this map (empty intersection = no finding). |
There was a problem hiding this comment.
[warning] arch: TOOL_SUBSET_TAGS in L0 (core/) encodes server-layer semantic knowledge: which MCP tools carry which functional-category FastMCP tags. The comment explicitly says it 'Mirrors the FastMCP @mcp.tool(tags=...) category assignments in the server layer.' This couples L0 to server-layer tagging decisions; any tag change in server/tools_*.py requires a corresponding update in L0. This data belongs in L1 (pipeline/) or L3 (server/) where it can reference both L0 constants and server-layer contracts.
| # session is a cook session (see init_session cook_session parameter). | ||
| TIER2_SKILLS: frozenset[str] = frozenset({"open-kitchen", "close-kitchen"}) | ||
| if TYPE_CHECKING: | ||
| from autoskillit.config.settings import AutomationConfig |
There was a problem hiding this comment.
[warning] arch: Cross-L1 import: workspace/ (L1) imports AutomationConfig from config/settings.py (also L1) under TYPE_CHECKING. Per the architecture, L1 packages should only import from L0 (core/). Accept the config data as a protocol/TypedDict defined in core/, or use Any/string-quoted forward reference.
|
|
||
| FREE_RANGE_TOOLS: frozenset[str] = frozenset({"open_kitchen", "close_kitchen"}) | ||
|
|
||
| UNGATED_TOOLS: frozenset[str] = FREE_RANGE_TOOLS |
There was a problem hiding this comment.
[warning] slop: UNGATED_TOOLS is defined as a pure alias for FREE_RANGE_TOOLS (UNGATED_TOOLS: frozenset[str] = FREE_RANGE_TOOLS). This is a backward-compat shim — callers should import FREE_RANGE_TOOLS directly. WORKER_TOOLS and HEADLESS_BLOCKED_UNGATED_TOOLS were removed entirely while UNGATED_TOOLS was kept; apply consistent policy (either alias all removed names or remove all).
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit review found 21 actionable blocking issues. See inline review comments for details. Verdict: changes_requested.
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit review found 21 actionable blocking issues (plus 2 critical xdist-unsafe tests at test_server_init.py L1265/L1280 outside diff hunks). See inline review comments for details. Verdict: changes_requested.
…cstrings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ory to Path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l_logs_warning The unknown-skill warning fires before cook_session is consulted; False is more accurate for the intent of the test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace assert guard with explicit RuntimeError in contracts.py staleness check - Upgrade subset-disable import failure from WARNING to ERROR in server/_state.py - Fix open_kitchen to resolve _get_ctx() before ctx.enable_components to prevent half-open kitchen on initialization error - Add debug log when a skill is skipped due to subset-disable in session_skills.py - Extract subset-disabled-tool check into its own @semantic_rule function in rules_tools.py - Rename get_skill_content tier2_gated param to gated (the flag applies to any tier) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ecipe tools - test_list_recipes_gate_closed_returns_gate_error: verifies list_recipes returns gate_error when kitchen is closed (covers deleted test_list_recipes_denied_when_headless) - test_gate_closed_returns_gate_error for get_pipeline_report, get_token_summary, and get_timing_summary: restore coverage lost when these tools moved to kitchen-gated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
The ephemeral skill directory bug (CC-001, fixed in v0.5.1) exposed a
structural gap: Claude Code's external interface conventions—exact
directory layouts that its runtime must discover—are not governed by any
registry or independent test. The project has a gold-standard governance
model for CLI flags (`ClaudeFlags` StrEnum + `test_flag_contracts.py`
with hardcoded string literal pinning), but no equivalent for directory
layout conventions. `_SKILLS_SUBDIR` in `session_skills.py` was a
private, unexported, untested constant. Tests verified "did the code
write to the path it says it wrote" — not "did the code write to the
path Claude Code requires."
Part A creates the `ClaudeFlags` equivalent for directory conventions: a
new `core/claude_conventions.py` L0 module containing
`ClaudeDirectoryConventions`, migrates `_SKILLS_SUBDIR` to be derived
from it, and creates
`tests/contracts/test_claude_code_interface_contracts.py` with constant
value-pinning tests (in the same form as `TestClaudeFlagValues`) plus
CC-001 and CC-002 behavioral guards.
Part B (separate task) covers unmocked chefs-hat integration tests and
the remaining CC-003 through CC-008 contracts.
## Architecture Impact
### Module Dependency Diagram
```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
graph TB
%% CLASS DEFINITIONS %%
classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
subgraph L0 ["L0 — core/ (stdlib imports only)"]
direction LR
FLAGS["core/types.py<br/>━━━━━━━━━━<br/>ClaudeFlags (StrEnum)<br/>ADD_DIR · PLUGIN_DIR<br/>fan-in: 12 modules"]
CONV["★ core/claude_conventions.py<br/>━━━━━━━━━━<br/>ClaudeDirectoryConventions<br/>ADD_DIR_SKILLS_SUBDIR<br/>PLUGIN_DIR_SKILLS_SUBDIR<br/>SKILL_FILENAME"]
PATHS["core/paths.py<br/>━━━━━━━━━━<br/>pkg_root()"]
CORE["● core/__init__.py<br/>━━━━━━━━━━<br/>Re-exports ClaudeDirectoryConventions<br/>+ full public surface"]
end
subgraph L1 ["L1 — workspace/"]
direction LR
SS["● workspace/session_skills.py<br/>━━━━━━━━━━<br/>_SKILLS_SUBDIR = ClaudeDirectoryConventions<br/>.ADD_DIR_SKILLS_SUBDIR<br/>(was: private Path literal)"]
SK["workspace/skills.py<br/>━━━━━━━━━━<br/>SkillResolver<br/>bundled_skills_dir()"]
end
subgraph L3 ["L3 — Consumers (unchanged)"]
direction LR
CH["cli/_chefs_hat.py<br/>━━━━━━━━━━<br/>chefs_hat()<br/>→ init_session()"]
RE["server/tools_execution.py<br/>━━━━━━━━━━<br/>run_skill()"]
end
subgraph TESTS ["tests/ — governance layer"]
direction TB
TCIIC["★ tests/contracts/<br/>test_claude_code_interface_contracts.py<br/>━━━━━━━━━━<br/>TestClaudeDirectoryConventions (value pins)<br/>TestAddDirLayoutContract (CC-001)<br/>TestPluginDirLayoutContract (CC-002)"]
TFC["tests/execution/test_flag_contracts.py<br/>━━━━━━━━━━<br/>TestClaudeFlagValues (existing)"]
GAP["[ELIMINATED] _SKILLS_SUBDIR<br/>━━━━━━━━━━<br/>was: private literal, no value pin<br/>→ replaced by registry + contract test"]
end
%% L0 INTERNAL %%
CONV -->|"re-exported by"| CORE
FLAGS -->|"re-exported by"| CORE
PATHS -->|"re-exported by"| CORE
%% L0 → L1 %%
CORE -->|"imports ClaudeDirectoryConventions"| SS
PATHS -->|"imports pkg_root()"| SK
%% L1 → L3 %%
SS -->|"imported by"| CH
SS -->|"imported by"| RE
SK -->|"imports bundled_skills_dir()"| RE
%% L3 uses flags %%
CORE -->|"ClaudeFlags.ADD_DIR"| CH
CORE -->|"ClaudeFlags.ADD_DIR"| RE
%% TEST GOVERNANCE %%
TCIIC -->|"pins string literals for"| CONV
TCIIC -->|"behavioral guard"| SS
TCIIC -->|"structural guard"| PATHS
TFC -->|"pins string literals for"| FLAGS
GAP -.->|"replaced by"| TCIIC
%% CLASS ASSIGNMENTS %%
class FLAGS stateNode;
class CONV newComponent;
class PATHS handler;
class CORE phase;
class SS phase;
class SK handler;
class CH,RE cli;
class TCIIC newComponent;
class TFC output;
class GAP gap;
```
**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Teal | High Fan-In | `ClaudeFlags` — core constant registry (12
dependents) |
| Green | New Component | `★ core/claude_conventions.py` + `★
test_claude_code_interface_contracts.py` |
| Purple | Modified/Control | `● core/__init__.py` (adds re-export), `●
session_skills.py` (derives from registry) |
| Orange | Utility | `paths.py`, `workspace/skills.py` — support modules
|
| Dark Blue | Consumers | CLI and server layer (unchanged; benefit from
registry indirectly) |
| Dark Teal | Existing Test | `test_flag_contracts.py` — existing
governance (parallel model) |
| Amber | Eliminated Gap | Private `_SKILLS_SUBDIR` literal — no longer
a gap |
### State Lifecycle Diagram
```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
flowchart TB
%% CLASS DEFINITIONS %%
classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
subgraph Registry ["★ REGISTRY: core/claude_conventions.py"]
direction TB
ADD_DIR["ADD_DIR_SKILLS_SUBDIR<br/>━━━━━━━━━━<br/>Path('.claude') / 'skills'<br/>INIT_ONLY — external spec"]
PLUGIN_DIR["PLUGIN_DIR_SKILLS_SUBDIR<br/>━━━━━━━━━━<br/>Path('skills')<br/>INIT_ONLY — external spec"]
SKILL_F["SKILL_FILENAME<br/>━━━━━━━━━━<br/>'SKILL.md'<br/>INIT_ONLY — external spec"]
end
subgraph Derived ["● DERIVED STATE: workspace/session_skills.py"]
direction LR
SUBDIR["● _SKILLS_SUBDIR<br/>━━━━━━━━━━<br/>= ClaudeDirectoryConventions<br/>.ADD_DIR_SKILLS_SUBDIR<br/>INIT_PRESERVE — registry-bound"]
end
subgraph Gate1 ["GATE 1 — Value Pinning (CI catches constant drift)"]
direction TB
PIN1["TestClaudeDirectoryConventions<br/>━━━━━━━━━━<br/>assert ADD_DIR_SKILLS_SUBDIR == '.claude/skills'<br/>assert PLUGIN_DIR_SKILLS_SUBDIR == 'skills'<br/>assert SKILL_FILENAME == 'SKILL.md'<br/>HARDCODED STRING LITERALS — never imports"]
end
subgraph Gate2 ["GATE 2 — Behavioral Guard (CC-001, CC-002)"]
direction TB
CC001["TestAddDirLayoutContract (CC-001)<br/>━━━━━━━━━━<br/>init_session writes '.claude/skills/*/SKILL.md'<br/>No flat layout at session root<br/>Independent of _SKILLS_SUBDIR constant"]
CC002["TestPluginDirLayoutContract (CC-002)<br/>━━━━━━━━━━<br/>pkg_root()/'skills'/ exists<br/>pkg_root()/'skills/*/SKILL.md' found<br/>'open-kitchen' skill at literal path"]
end
subgraph Before ["[ELIMINATED] Pre-PR Gap"]
direction TB
PRIV["_SKILLS_SUBDIR (private literal)<br/>━━━━━━━━━━<br/>Path('.claude') / 'skills'<br/>No export · No value pin · No registry<br/>CC-001 bug: drifted to flat layout<br/>Tests mirrored wrong value — silent"]
end
subgraph Runtime ["RUNTIME CONSUMERS (unchanged)"]
direction LR
INIT_S["init_session()<br/>━━━━━━━━━━<br/>uses _SKILLS_SUBDIR<br/>to build --add-dir path"]
ACT_T2["activate_tier2()<br/>━━━━━━━━━━<br/>uses _SKILLS_SUBDIR<br/>to build tier2 path"]
end
%% REGISTRY FEEDS DERIVED STATE %%
ADD_DIR -->|"derives"| SUBDIR
%% GATE 1 PINS REGISTRY %%
PIN1 -->|"pins string literal"| ADD_DIR
PIN1 -->|"pins string literal"| PLUGIN_DIR
PIN1 -->|"pins string literal"| SKILL_F
%% GATE 2 GUARDS BEHAVIOR %%
CC001 -->|"behavioral guard"| SUBDIR
CC002 -->|"structural guard"| PLUGIN_DIR
%% RUNTIME USES DERIVED %%
SUBDIR -->|"consumed by"| INIT_S
SUBDIR -->|"consumed by"| ACT_T2
%% ELIMINATED GAP %%
PRIV -.->|"replaced by"| Registry
%% CLASS ASSIGNMENTS %%
class ADD_DIR,PLUGIN_DIR,SKILL_F detector;
class SUBDIR phase;
class PIN1 newComponent;
class CC001,CC002 newComponent;
class PRIV gap;
class INIT_S,ACT_T2 cli;
```
**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Red | INIT_ONLY Constants | External spec values — must match Claude
Code runtime exactly |
| Purple | INIT_PRESERVE | Registry-derived constant — drifts with
registry (guarded by Gate 1) |
| Green | New Guards | New contract tests — value pins (Gate 1) and
behavioral guards (Gate 2) |
| Dark Blue | Consumers | Runtime functions that write discovery paths |
| Amber | Eliminated Gap | Pre-PR private literal — no governance
(CC-001 root cause) |
## Implementation Plan
Plan file:
`temp/rectify/rectify_claude-code-interface-contracts-registry_2026-03-16_000000_part_a.md`
## Token Usage Summary
No token data available for this pipeline run.
🤖 Generated with [Claude Code](https://claude.com/claude-code) via
AutoSkillit
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary
The dead-end guard in `_compute_outcome` (`session.py`) conflates two
fundamentally different failure categories under a single "channel
confirmed + failure = retriable" assumption:
1. **Drain-race artifacts** (transient) — `session.result` is empty or
missing the completion marker because stdout was not fully drained
before the guard ran. These are genuinely retriable: a resume will find
the content.
2. **Pattern-contract violations** (terminal) — `session.result` is
non-empty and contains the completion marker, but the skill's
`expected_output_patterns` are absent. The session completed normally;
the model simply did not produce the required output block. Retrying
will never produce different content.
The guard promotes **both** categories to `RETRIABLE + RESUME`, causing
infinite retry cycles until the budget guard terminates the run. This PR
introduces `ContentState`, a typed enum that makes the four content
evaluation outcomes first-class concepts (`ABSENT`,
`CONTRACT_VIOLATION`, `COMPLETE`, `SESSION_ERROR`). The dead-end guard
now checks `ContentState` rather than just `bool`, restricting
drain-race promotion to `ContentState.ABSENT` only. This makes the bug
class structurally impossible.
## Architecture Impact
### Process Flow Diagram
```mermaid
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, 'curve': 'basis'}}}%%
flowchart TB
classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
START([session exits<br/>channel confirmed])
subgraph Recovery ["Recovery Phase (● headless.py)"]
direction TB
R1["_recover_from_separate_marker<br/>━━━━━━━━━━<br/>Rebuild result if marker<br/>in standalone assistant msg"]
R2["● _recover_block_from_assistant_messages<br/>━━━━━━━━━━<br/>Rebuild result if patterns found<br/>in assistant_messages<br/>(now: CHANNEL_A + CHANNEL_B)"]
end
subgraph Evaluation ["★ Content Evaluation (● session.py)"]
direction TB
ECS["★ _evaluate_content_state<br/>━━━━━━━━━━<br/>is_error? → SESSION_ERROR<br/>empty result? → ABSENT<br/>marker absent? → ABSENT<br/>patterns fail? → CONTRACT_VIOLATION<br/>else → COMPLETE"]
end
subgraph Outcome ["● _compute_outcome (session.py)"]
direction TB
CS["● _compute_success<br/>━━━━━━━━━━<br/>CHANNEL_B: delegates to<br/>_evaluate_content_state<br/>CHANNEL_A: _check_session_content"]
CR["_compute_retry<br/>━━━━━━━━━━<br/>API signals / termination /<br/>channel confirmation dispatch"]
CONTRA{"contradiction guard<br/>━━━━━━━━━━<br/>success=True AND<br/>needs_retry=True?"}
DEG{"● dead-end guard<br/>━━━━━━━━━━<br/>not success AND<br/>not needs_retry AND<br/>channel confirmed?"}
DEGS{"★ ContentState check<br/>━━━━━━━━━━<br/>ABSENT?"}
end
subgraph Resolution ["Outcome Resolution"]
direction TB
PROMOTE["promote to RETRIABLE<br/>━━━━━━━━━━<br/>retry_reason = RESUME<br/>(drain-race rescue only)"]
TERMINAL["remain FAILED<br/>━━━━━━━━━━<br/>CONTRACT_VIOLATION or<br/>SESSION_ERROR → terminal"]
NS["_normalize_subtype<br/>━━━━━━━━━━<br/>adjudicated_failure /<br/>empty_result / missing_marker"]
end
RESULT(["SkillResult<br/>success / needs_retry / subtype"])
START --> R1 --> R2 --> ECS
ECS --> CS
ECS --> CR
CS --> CONTRA
CR --> CONTRA
CONTRA -->|"yes: demote success"| DEG
CONTRA -->|"no"| DEG
DEG -->|"yes"| DEGS
DEG -->|"no"| NS
DEGS -->|"ABSENT"| PROMOTE
DEGS -->|"CONTRACT_VIOLATION<br/>or SESSION_ERROR"| TERMINAL
PROMOTE --> NS
TERMINAL --> NS
NS --> RESULT
class START,RESULT terminal;
class R1,R2 handler;
class ECS,DEGS newComponent;
class CS,DEG detector;
class CR,CONTRA phase;
class PROMOTE stateNode;
class TERMINAL,NS output;
```
**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | Terminal | Session entry and SkillResult exit |
| Orange | Handler | Recovery phase functions (● modified) |
| Green | New Component | ★ _evaluate_content_state and ★ ContentState
gate |
| Red | Detector | ● _compute_success (modified), ● dead-end guard
(modified) |
| Purple | Phase | _compute_retry, contradiction guard |
| Teal | State | RETRIABLE promotion (drain-race rescue path) |
| Dark Teal | Output | Terminal outcome and _normalize_subtype |
### State Lifecycle Diagram
```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;
classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
subgraph Fields ["SKILLRESULT FIELD LIFECYCLE"]
direction LR
INIT["INIT_ONLY<br/>━━━━━━━━━━<br/>session_id<br/>start_ts<br/>completion_marker<br/>NEVER modify after set"]
APPEND["APPEND_ONLY<br/>━━━━━━━━━━<br/>assistant_messages<br/>only grows during recovery"]
MUTABLE["MUTABLE<br/>━━━━━━━━━━<br/>● needs_retry<br/>● success<br/>● retry_reason<br/>● subtype<br/>computed then may be promoted"]
DERIVED["DERIVED<br/>━━━━━━━━━━<br/>outcome<br/>mapped from success+needs_retry<br/>(SUCCEEDED / RETRIABLE / FAILED)"]
end
subgraph Recovery ["● RECOVERY MUTATIONS (headless.py)"]
direction TB
R1["_recover_from_separate_marker<br/>━━━━━━━━━━<br/>may overwrite session.result<br/>from assistant_messages"]
R2["● _recover_block_from_assistant_messages<br/>━━━━━━━━━━<br/>appends to session.result<br/>now: CHANNEL_A + CHANNEL_B"]
end
subgraph ContentGate ["★ CONTENTSTATE VALIDATION GATE (session.py)"]
direction TB
ECS["★ _evaluate_content_state<br/>━━━━━━━━━━<br/>reads: session.result, is_error,<br/>completion_marker, patterns<br/>writes: ContentState discriminant"]
CS_ENUM["★ ContentState enum<br/>━━━━━━━━━━<br/>COMPLETE — all checks pass<br/>ABSENT — empty/marker missing (retriable)<br/>CONTRACT_VIOLATION — result+marker+no patterns (terminal)<br/>SESSION_ERROR — is_error=True (terminal)"]
end
subgraph DeadEndGuard ["● DEAD-END GUARD CONTRACT (session.py)"]
direction TB
PRE["● _compute_outcome entry<br/>━━━━━━━━━━<br/>success=False AND<br/>needs_retry=False AND<br/>channel_confirmed?"]
GATE{"★ ContentState gate<br/>━━━━━━━━━━<br/>ContentState.ABSENT?"}
PROMOTE["PROMOTE<br/>━━━━━━━━━━<br/>needs_retry = True<br/>retry_reason = RESUME<br/>(drain-race rescue)"]
BLOCK["BLOCK promotion<br/>━━━━━━━━━━<br/>needs_retry stays False<br/>outcome = FAILED<br/>(contract violation guard)"]
end
subgraph SkillResult ["SKILLRESULT OUTPUT CONTRACT"]
direction TB
SR["SkillResult<br/>━━━━━━━━━━<br/>success / needs_retry / subtype<br/>guaranteed: CONTRACT_VIOLATION<br/>never becomes RETRIABLE"]
end
INIT --> Recovery
APPEND --> Recovery
MUTABLE --> Recovery
R1 --> R2
R2 --> ECS
ECS --> CS_ENUM
CS_ENUM --> PRE
PRE --> GATE
GATE -->|"ABSENT"| PROMOTE
GATE -->|"CONTRACT_VIOLATION<br/>or SESSION_ERROR"| BLOCK
PROMOTE --> MUTABLE
BLOCK --> MUTABLE
MUTABLE --> DERIVED
DERIVED --> SR
class INIT detector;
class APPEND handler;
class MUTABLE phase;
class DERIVED gap;
class R1,R2 handler;
class ECS,CS_ENUM,GATE newComponent;
class PRE detector;
class PROMOTE stateNode;
class BLOCK output;
class SR terminal;
```
**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Red | INIT_ONLY / Guard Entry | Fields that must never change;
dead-end guard check |
| Orange | APPEND_ONLY / Handlers | assistant_messages growth; recovery
functions (● modified) |
| Purple | MUTABLE | Fields computed and potentially promoted by guards
|
| Yellow/Amber | DERIVED | Outcome derived from field combination |
| Green | New Component | ★ ContentState enum, ★
_evaluate_content_state, ★ gate |
| Teal | Promotion | needs_retry promoted for drain-race (ABSENT only) |
| Dark Teal | Block | Promotion blocked for terminal failures |
| Dark Blue | Terminal | SkillResult output contract |
## Implementation Plan
Plan file:
`temp/rectify/rectify_adjudicated-failure-false-positive_2026-03-16_000001_part_a.md`
## Token Usage Summary
No token data available for this pipeline run.
🤖 Generated with [Claude Code](https://claude.com/claude-code) via
AutoSkillit
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…edAddDir Wire run_skill's headless path through ctx.session_skill_manager.init_session() instead of hardcoding pkg_root()/skills_extended as --add-dir. Add ValidatedAddDir opaque type, LayoutError, validate_add_dir(), and dead-with-param semantic rule. Remove dead add_dir parameter from remediation.yaml and write-recipe/SKILL.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ll path Add CC-HEADLESS-001 contract test, ValidatedAddDir unit tests, dead-with-param rule tests, and update existing routing tests (T-OVR-014, run_skill_passes_add_dirs) to verify ValidatedAddDir usage and ephemeral session dir layout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d-output - Add __truediv__ to ValidatedAddDir for path composition compatibility - Use relative import in claude_conventions.py for core/ isolation - Remove dead review_path capture from remediation.yaml review step - Scope bundled recipe test to check add_dir specifically Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Existing tests use Path methods on init_session() return value. Add thin delegations to avoid modifying many existing test files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…walkthrough The review step's review_path capture is needed to satisfy the implicit-handoff rule. Reference it as a context key in dry_walkthrough (replaces the removed add_dir parameter). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…am rule Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Establish compile-time and test-time guarantees that `TOOL_CATEGORIES`
and `TOOL_SUBSET_TAGS` cannot silently drift from the actual MCP tool
registrations. Add a completeness test for `TOOL_CATEGORIES` (modeled on
the existing `test_all_mcp_tools_are_registered` pattern for
`GATED_TOOLS`), a consistency test for `TOOL_SUBSET_TAGS` against
FastMCP tag introspection, and a docstring/CLAUDE.md count validation
test. For `plugin.json`, auto-generate it at build time via a
`sync_plugin_json.py` script invoked by a new `task sync-plugin-version`
Taskfile task so it can never drift from `pyproject.toml`.
## Architecture Impact
### Development Diagram
```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%%
flowchart TB
classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
subgraph Sources ["CONFIGURATION SOURCES"]
direction TB
PYPROJECT["pyproject.toml<br/>━━━━━━━━━━<br/>hatchling backend<br/>v0.5.1, Python 3.11+"]
PRECOMMIT[".pre-commit-config.yaml<br/>━━━━━━━━━━<br/>7 hooks registered"]
end
subgraph Tasks ["● TASK RUNNER (Taskfile.yml)"]
direction TB
TEST_ALL["test-all<br/>━━━━━━━━━━<br/>lint-imports + pytest -n 4"]
TEST_CHECK["test-check<br/>━━━━━━━━━━<br/>PASS/FAIL automation gate"]
SYNC["★ sync-plugin-version<br/>━━━━━━━━━━<br/>runs sync_plugin_json.py"]
INSTALL["install-worktree<br/>━━━━━━━━━━<br/>uv venv + install .[dev]"]
end
subgraph Scripts ["★ SCRIPTS"]
direction TB
SYNC_SCRIPT["★ scripts/sync_plugin_json.py<br/>━━━━━━━━━━<br/>tomllib reads pyproject.toml<br/>atomic write to plugin.json"]
end
subgraph QualityGates ["QUALITY GATES (pre-commit)"]
direction TB
RUFF_FMT["ruff-format<br/>━━━━━━━━━━<br/>auto-format (writes)"]
RUFF_LINT["ruff<br/>━━━━━━━━━━<br/>E/F/I/UP/TID251 (auto-fix)"]
MYPY["mypy<br/>━━━━━━━━━━<br/>type-check src/ (report)"]
NO_GEN["no-generated-configs<br/>━━━━━━━━━━<br/>block hooks.json commits"]
DOC_CNT["doc-counts<br/>━━━━━━━━━━<br/>scripts/check_doc_counts.py"]
GITLEAKS["gitleaks v8.30.0<br/>━━━━━━━━━━<br/>secret scanning"]
end
subgraph TestSuite ["● TEST SUITE (tests/arch/test_layer_enforcement.py)"]
direction TB
EXISTING["Existing tests<br/>━━━━━━━━━━<br/>MCP registry AST checks<br/>import layer enforcement<br/>cross-package constraints"]
T1["● test_tool_categories_covers_all_tools<br/>━━━━━━━━━━<br/>TOOL_CATEGORIES = GATED|HEADLESS|FREE_RANGE"]
T2["● test_tool_subset_tags_match_decorators<br/>━━━━━━━━━━<br/>TOOL_SUBSET_TAGS matches @mcp.tool tags"]
T3["● test_server_docstring_counts_accurate<br/>━━━━━━━━━━<br/>prose counts match frozenset sizes"]
end
subgraph CI ["CI WORKFLOWS (.github/workflows/)"]
direction TB
TESTS_YML["tests.yml<br/>━━━━━━━━━━<br/>uv + go-task + lint-imports<br/>ubuntu + macos (stable)"]
VER_BUMP["version-bump.yml<br/>━━━━━━━━━━<br/>patch bump on main merge"]
end
subgraph EntryPoints ["ENTRY POINTS"]
CLI_EP["autoskillit<br/>━━━━━━━━━━<br/>autoskillit.cli:main"]
PLUGIN_JSON["plugin.json<br/>━━━━━━━━━━<br/>Claude Code plugin loader"]
end
PYPROJECT -->|"version source"| SYNC_SCRIPT
SYNC_SCRIPT -->|"atomic write"| PLUGIN_JSON
SYNC -->|"runs"| SYNC_SCRIPT
PYPROJECT -->|"build config"| INSTALL
PRECOMMIT --> RUFF_FMT
PRECOMMIT --> RUFF_LINT
PRECOMMIT --> MYPY
PRECOMMIT --> NO_GEN
PRECOMMIT --> DOC_CNT
PRECOMMIT --> GITLEAKS
RUFF_FMT -->|"formatted code"| RUFF_LINT
RUFF_LINT -->|"linted code"| MYPY
MYPY -->|"type-checked"| TEST_ALL
TEST_ALL -->|"pytest -n 4"| EXISTING
TEST_ALL -->|"pytest -n 4"| T1
TEST_ALL -->|"pytest -n 4"| T2
TEST_ALL -->|"pytest -n 4"| T3
TEST_CHECK -->|"CI gate"| EXISTING
TEST_CHECK -->|"CI gate"| T1
PYPROJECT -->|"entry_points"| CLI_EP
TESTS_YML -->|"runs"| TEST_ALL
VER_BUMP -->|"calls"| SYNC_SCRIPT
class PYPROJECT,PRECOMMIT stateNode;
class TEST_ALL,TEST_CHECK,INSTALL phase;
class SYNC newComponent;
class SYNC_SCRIPT newComponent;
class RUFF_FMT,RUFF_LINT,MYPY,NO_GEN,DOC_CNT,GITLEAKS detector;
class EXISTING handler;
class T1,T2,T3 newComponent;
class TESTS_YML,VER_BUMP cli;
class CLI_EP,PLUGIN_JSON output;
```
**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Teal | Configuration | pyproject.toml, pre-commit config |
| Purple | Tasks | Taskfile task definitions |
| Green (★/●) | New/Modified | sync_plugin_json.py, new tests, sync task
|
| Red | Quality Gates | Pre-commit hooks: formatter, linter, type
checker |
| Orange | Existing Tests | Unchanged enforcement tests |
| Dark Blue | CI | GitHub Actions workflows |
| Dark Teal | Entry Points | CLI console script, plugin.json |
### Module Dependency Diagram
```mermaid
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%%
graph TB
classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff;
classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff;
classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff;
classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff;
classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff;
subgraph L3 ["L3 — APPLICATION (server/, cli/)"]
direction LR
SERVER_INIT["server/__init__.py<br/>━━━━━━━━━━<br/>FastMCP app, gate setup"]
TOOLS_KITCHEN["server/tools_kitchen.py<br/>━━━━━━━━━━<br/>open_kitchen: consumes<br/>TOOL_CATEGORIES"]
end
subgraph L1 ["L1 — SERVICES (pipeline/)"]
direction LR
PIPELINE_GATE["pipeline/gate.py<br/>━━━━━━━━━━<br/>GATED_TOOLS, UNGATED_TOOLS<br/>re-exports from core"]
end
subgraph L0 ["L0 — FOUNDATION (core/)"]
direction TB
CORE_INIT["core/__init__.py<br/>━━━━━━━━━━<br/>gateway re-exports<br/>full public surface"]
CORE_TYPES["● core/types.py<br/>━━━━━━━━━━<br/>GATED_TOOLS, HEADLESS_TOOLS<br/>FREE_RANGE_TOOLS, UNGATED_TOOLS<br/>● TOOL_CATEGORIES (468)<br/>● TOOL_SUBSET_TAGS (436)"]
end
subgraph Scripts ["SCRIPTS (no autoskillit coupling)"]
SYNC_SCRIPT["★ scripts/sync_plugin_json.py<br/>━━━━━━━━━━<br/>stdlib only: tomllib, pathlib<br/>json, sys, tempfile"]
end
subgraph Stdlib ["STDLIB / EXTERNAL"]
TOMLLIB["tomllib (3.11+)<br/>━━━━━━━━━━<br/>pyproject.toml parsing"]
FASTMCP["fastmcp ≥3.1.1<br/>━━━━━━━━━━<br/>MCP server, tag introspection"]
end
subgraph Tests ["TESTS (test-only, not shipped)"]
TEST_FILE["● tests/arch/test_layer_enforcement.py<br/>━━━━━━━━━━<br/>● test_tool_categories_covers_all_tools<br/>● test_tool_subset_tags_match_decorators<br/>● test_server_docstring_counts_accurate"]
end
CORE_INIT -->|"re-exports"| CORE_TYPES
TOOLS_KITCHEN -->|"imports TOOL_CATEGORIES"| CORE_INIT
SERVER_INIT -->|"imports get_logger"| CORE_INIT
PIPELINE_GATE -->|"imports GATED_TOOLS"| CORE_TYPES
TEST_FILE -->|"imports TOOL_CATEGORIES<br/>GATED/HEADLESS/FREE_RANGE"| CORE_INIT
TEST_FILE -->|"direct: TOOL_SUBSET_TAGS<br/>FREE_RANGE/GATED/HEADLESS"| CORE_TYPES
TEST_FILE -->|"imports GATED_TOOLS<br/>UNGATED_TOOLS (other tests)"| PIPELINE_GATE
SYNC_SCRIPT -->|"reads"| TOMLLIB
SERVER_INIT -->|"tool decorators"| FASTMCP
class SERVER_INIT,TOOLS_KITCHEN cli;
class PIPELINE_GATE phase;
class CORE_INIT stateNode;
class CORE_TYPES handler;
class SYNC_SCRIPT newComponent;
class TEST_FILE newComponent;
class TOMLLIB,FASTMCP integration;
```
**Color Legend:**
| Color | Category | Description |
|-------|----------|-------------|
| Dark Blue | L3 Application | server/ and cli/ modules |
| Purple | L1 Services | pipeline/gate.py re-export layer |
| Teal | L0 Gateway | core/__init__.py — high fan-in re-export hub |
| Orange | L0 Types | core/types.py — canonical constant definitions (●
modified) |
| Green (★/●) | New/Modified | sync_plugin_json.py (★ new), test file (●
modified) |
| Red | External | stdlib tomllib, fastmcp package |
## Implementation Plan
Plan file:
`/home/talon/projects/autoskillit-runs/impl-20260316-142645-718690/temp/make-plan/tool_metadata_single_source_of_truth_plan_2026-03-16_120000.md`
## Token Usage Summary
| Step | Input | Output | Total |
|------|-------|--------|-------|
| (token summary unavailable) | — | — | — |
🤖 Generated with [Claude Code](https://claude.com/claude-code) via
AutoSkillit
Summary
Introduces a three-tier skill visibility system backed by a new
skills_extended/directory and config-driven subset categories, with enforcement at MCP server startup, kitchen open, and ephemeral skill dir construction. Reclassifies all 39 MCP tool tags into kitchen/headless/free-range tiers with category tags, and wires disabled subsets to block both tools and skills from sessions. Adds project-local skill override detection, recipe validation rules for disabled subsets and overrides, a cook-time interactive gate, and comprehensive documentation across four new docs files.Individual Group Plans
Group A: MCP Tool Tag Reclassification
Reclassify the tag taxonomy for all 39 MCP tools: rename
"automation"→"autoskillit"on every tool, move 11 formerly-ungated tools into the"kitchen"tier, introduce a new"headless"tag fortest_check, remove the headless-session gate pre-open from_factory.pyandserver/__init__.py, and update theGATED_TOOLS/UNGATED_TOOLS/WORKER_TOOLS/HEADLESS_BLOCKED_UNGATED_TOOLSconstants incore/types.pyto match the new three-tier model. Apply functional category tags (github,ci,clone,telemetry) to all relevant tools.Group B (Part A): Three-Tier Skill Directory Layout — Foundation
Creates the filesystem foundation for the three-tier skill visibility system:
skills/retains onlyopen-kitchen,close-kitchen,sous-chef(3 dirs, Tier 1). Newskills_extended/holds the remaining 57 skills.SkillsConfigdataclass added toAutomationConfigwithtier1/tier2/tier3fields.defaults.yamlrecords canonical default tier assignments for all 60 skills.Group B (Part B): Three-Tier Skill Directory Layout — Wiring
Wires the foundations into session distribution: removes hardcoded
TIER2_SKILLSconstant fromsession_skills.py, makesinit_session()accept and use anAutomationConfigfor tier-driven gating and unknown-skill warnings, adds--plugin-dir <pkg_root()>tochefs-hat, defaultsrun_skill'sadd_dirtobundled_skills_extended_dir().Group C: Skill Category Metadata + Subset Config Schema
Add
categories:frontmatter to 37 applicable bundled SKILL.md files, introduce aSubsetsConfigdataclass that extendsAutomationConfigwithsubsets.disabledandsubsets.custom_tagsconfig keys, and add aread_skill_categories()helper toworkspace/skills.pythat exposes category membership onSkillInfoobjects.Group D: Subset Visibility Enforcement
Wire the config-driven subset category system into three runtime enforcement paths: (1) server startup
mcp.disable(tags={subset})in_initialize(), (2)open_kitchenre-disable loop afterctx.enable_components(tags={"kitchen"})to neutralize FastMCP session-rule-overrides-server-rule behavior, and (3) ephemeral skill directory subset filter ininit_session().Group E: Project-Local Skill Overrides
Implement project-local skill override detection: when
.claude/skills/<name>/SKILL.mdor.autoskillit/skills/<name>/SKILL.mdexists, the corresponding bundled skill is excluded from the ephemeral session directory.run_headless_coregains multi-path--add-dirsupport. A new WARNING recipe validation rule fires when a recipe references/autoskillit:<name>but a project-local override exists.Group F: Recipe Validation for Disabled Subsets + Cook Gate
Extend recipe validation with
subset-disabled-skillandsubset-disabled-toolWARNING rules, adddisabled_subsetsfield toValidationContext, addTOOL_SUBSET_TAGSstatic map tocore/types.py, and add a cook-time interactive prompt when disabled-subset findings are present.Group G: Documentation
Write all user-facing documentation: four new files (
docs/skill-visibility.md,docs/subset-categories.md,docs/mcp-tool-access.md,docs/project-local-overrides.md), and targeted updates toCLAUDE.md,docs/architecture.md, anddocs/configuration.md.Requirements
Tier System (TIER)
skills/(Tier 1) must be discoverable via--plugin-dirby Claude Code's native plugin scanner.skills_extended/(Tier 2+3) must NOT be discoverable via--plugin-dir.autoskillit chefs-hatmust copy skills from BOTHskills/andskills_extended/into the ephemeral session dir.run_headless_coremust pass--add-dirpointing toskills_extended/so headless sessions discover Tier 2+3 skills.SkillResolvermust scan bothskills/andskills_extended/directories for resolution and listing.skills.tier1,skills.tier2,skills.tier3keys for reclassification.init_session()time using dynaconf layered resolution.autoskillit chefs-hatmust pass--plugin-dir <pkg_root()>to ensure MCP server loads without requiringautoskillit init.MCP Tool Access (TOOL)
"headless"tag added; headless sessions pre-enable only headless-tagged tools (test_check), not full kitchen.open_kitchenreveals all kitchen tools, then re-disables subset-tagged tools per config.test_checkreclassified as headless-accessible:{"autoskillit", "kitchen", "headless"}.gate.enable()removed from_factory.pyfor headless sessions."kitchen"tag; all 39 tools renamed"automation"→"autoskillit".Category + Config + Visibility
CATEGORY_TAGSregistry, SKILL.mdcategories:frontmatter,subsets.custom_tagsconfig support.subsets.disabledkey, dynaconf resolution, graceful unknown-name handling.open_kitchenre-disable loop.Project-Local Overrides + Recipe Validation + Documentation
--add-dir; recipe validation WARNING.subset-disabled-skill/toolrules; cook-time gate (interactive prompt / hard error).Architecture Impact
Module Dependency Diagram
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%% graph TB %% CLASS DEFINITIONS %% classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff; classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff; classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff; classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff; classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff; subgraph L0 ["L0 — core/ (stdlib only)"] direction LR TYPES["● core/types.py<br/>━━━━━━━━━━<br/>★ HEADLESS_TOOLS<br/>★ FREE_RANGE_TOOLS<br/>★ CATEGORY_TAGS<br/>★ TOOL_SUBSET_TAGS<br/>● GATED_TOOLS, UNGATED_TOOLS"] end subgraph L1cfg ["L1 — config/"] direction LR SETTINGS["● config/settings.py<br/>━━━━━━━━━━<br/>★ SkillsConfig<br/>★ SubsetsConfig<br/>● AutomationConfig"] end subgraph L1pipe ["L1 — pipeline/"] direction LR GATE["● pipeline/gate.py<br/>━━━━━━━━━━<br/>● GATED_TOOLS<br/>● UNGATED_TOOLS<br/>(= FREE_RANGE_TOOLS)"] end subgraph L1ws ["L1 — workspace/"] direction LR SKILLS["● workspace/skills.py<br/>━━━━━━━━━━<br/>★ bundled_skills_extended_dir()<br/>★ read_skill_categories()<br/>★ detect_project_local_overrides()<br/>● SkillInfo.categories"] SESSK["● workspace/session_skills.py<br/>━━━━━━━━━━<br/>● init_session(config=, project_dir=)<br/>● subset filter via config.subsets<br/>● override detection"] end subgraph L1exec ["L1 — execution/"] direction LR HEADLESS["● execution/headless.py<br/>━━━━━━━━━━<br/>● add_dir → add_dirs: Sequence[str]<br/>● multi-path --add-dir support"] end subgraph L2 ["L2 — recipe/"] direction LR RULES_SK["● recipe/rules_skills.py<br/>━━━━━━━━━━<br/>★ subset-disabled-skill rule<br/>★ project-local-override rule"] RULES_T["● recipe/rules_tools.py<br/>━━━━━━━━━━<br/>★ subset-disabled-tool rule<br/>uses HEADLESS_TOOLS<br/>uses TOOL_SUBSET_TAGS"] ANALYSIS["● recipe/_analysis.py<br/>━━━━━━━━━━<br/>★ disabled_subsets field<br/>on ValidationContext"] end subgraph L3srv ["L3 — server/"] direction LR SRV_INIT["● server/__init__.py<br/>━━━━━━━━━━<br/>● mcp.disable(tags=subset)<br/>per config.subsets.disabled"] SRV_STATE["● server/_state.py<br/>━━━━━━━━━━<br/>calls _initialize(ctx)<br/>→ subset disable loop"] KITCHEN["● server/tools_kitchen.py<br/>━━━━━━━━━━<br/>★ re-disable subsets<br/>after open_kitchen<br/>session rule override fix"] FACTORY["● server/_factory.py<br/>━━━━━━━━━━<br/>● remove gate.enable()<br/>in headless sessions"] TOOLS_EXEC["● server/tools_execution.py<br/>━━━━━━━━━━<br/>● add_dir → add_dirs<br/>[skills_ext, cwd]"] end subgraph L3cli ["L3 — cli/"] direction LR CHEFSHAT["● cli/_chefs_hat.py<br/>━━━━━━━━━━<br/>★ --plugin-dir pkg_root()<br/>★ bundled_skills_extended_dir()<br/>● init_session(config=)"] APP["● cli/app.py<br/>━━━━━━━━━━<br/>★ cook-time subset gate<br/>★ disabled_subsets prompt<br/>interactive/non-interactive"] end subgraph L1ws_ext ["L1 — skills_extended/ (★ new dir)"] direction LR SKILLSEXT["★ skills_extended/<br/>━━━━━━━━━━<br/>57 SKILL.md files<br/>★ categories: frontmatter<br/>Tier 2+3 (NOT plugin-scanned)"] end %% VALID DEPENDENCIES — L0 exported to L1 %% SETTINGS -->|"imports CATEGORY_TAGS"| TYPES GATE -->|"imports GATED_TOOLS<br/>UNGATED_TOOLS"| TYPES SKILLS -->|"imports SkillSource<br/>load_yaml, pkg_root"| TYPES HEADLESS -->|"imports core.*"| TYPES %% L1 → L1 (intra-layer) %% SESSK -->|"imports SkillInfo<br/>SkillResolver<br/>detect_project_local_overrides"| SKILLS %% L2 → L1 %% RULES_T -->|"imports HEADLESS_TOOLS<br/>TOOL_SUBSET_TAGS"| TYPES RULES_SK -->|"imports workspace.*"| SKILLS ANALYSIS -->|"disabled_subsets field<br/>plumbed from config"| SETTINGS %% L3 → L2 %% SRV_STATE -->|"uses ToolContext"| GATE APP -->|"imports recipe.*<br/>make_validation_context"| ANALYSIS %% L3 → L1 %% SRV_STATE -->|"imports AutomationConfig"| SETTINGS CHEFSHAT -->|"imports bundled_skills_extended_dir<br/>SkillResolver"| SKILLS CHEFSHAT -->|"imports load_config"| SETTINGS TOOLS_EXEC -->|"passes add_dirs"| HEADLESS %% L3 → L3 (intra-layer) %% SRV_STATE -->|"_initialize(ctx)"| SRV_INIT KITCHEN -->|"calls ctx.disable_components<br/>after enable(kitchen)"| SRV_INIT FACTORY -->|"removes gate.enable()<br/>headless sessions"| GATE %% skills_extended consumed by %% SKILLSEXT -.->|"scanned by"| SKILLS SKILLS -->|"reads SKILL.md<br/>categories: frontmatter"| SKILLSEXT %% CLASS ASSIGNMENTS %% class TYPES stateNode; class SETTINGS,GATE handler; class SKILLS,SESSK,HEADLESS phase; class RULES_SK,RULES_T,ANALYSIS detector; class SRV_INIT,SRV_STATE,KITCHEN,FACTORY,TOOLS_EXEC cli; class CHEFSHAT,APP cli; class SKILLSEXT newComponent;Security Diagram
%%{init: {'flowchart': {'nodeSpacing': 44, 'rankSpacing': 56, 'curve': 'basis'}}}%% flowchart TB %% CLASS DEFINITIONS %% classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff; classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff; classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff; classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff; classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000; subgraph MCPBoundary ["MCP TOOL VISIBILITY BOUNDARY — server/__init__.py + _state.py"] direction TB FR["Free Range (2)<br/>━━━━━━━━━━<br/>open_kitchen<br/>close_kitchen<br/>tags: autoskillit only"] KIT_HIDE["● mcp.disable(tags='kitchen')<br/>━━━━━━━━━━<br/>36 kitchen + 1 headless<br/>hidden at module load"] SUB_HIDE["★ mcp.disable(tags=subset)<br/>━━━━━━━━━━<br/>per config.subsets.disabled<br/>e.g. github, ci, clone<br/>applied in _initialize()"] HL_REVEAL["● mcp.enable(tags='headless')<br/>━━━━━━━━━━<br/>AUTOSKILLIT_HEADLESS=1<br/>reveals test_check only<br/>★ HEADLESS_TOOLS constant"] end subgraph KitchenBoundary ["KITCHEN OPEN BOUNDARY — server/tools_kitchen.py"] direction TB OK_ENABLE["ctx.enable_components(tags='kitchen')<br/>━━━━━━━━━━<br/>Session rule: reveals 37 tools<br/>(overrides server-level!)"] REDISABLE["★ for subset in config.subsets.disabled:<br/>ctx.disable_components(tags=subset)<br/>━━━━━━━━━━<br/>Session rule after enable<br/>Later rule wins — re-hides subset tools<br/>Neutralizes FastMCP override behavior"] GUARD_HOOK["open_kitchen_guard.py<br/>━━━━━━━━━━<br/>PreToolUse hook (defense-in-depth)<br/>blocks open_kitchen from headless"] end subgraph HeadlessBoundary ["HEADLESS SESSION BOUNDARY — pipeline/gate.py + hooks/"] direction TB HL_GUARD["headless_orchestration_guard.py<br/>━━━━━━━━━━<br/>PreToolUse hook: blocks<br/>run_skill, run_cmd, run_python<br/>from headless sessions"] GATE_CHECK["● _require_not_headless()<br/>━━━━━━━━━━<br/>Code guard: orchestration triad<br/>defense-in-depth layer 2"] FACTORY_FIX["● server/_factory.py<br/>━━━━━━━━━━<br/>★ gate.enable() REMOVED<br/>headless sessions no longer<br/>pre-enable kitchen gate"] end subgraph SkillBoundary ["SKILL FILESYSTEM BOUNDARY — workspace/session_skills.py"] direction TB TIER1["skills/ — Tier 1<br/>━━━━━━━━━━<br/>Plugin-scanned by Claude Code<br/>open-kitchen, close-kitchen<br/>sous-chef (injected)"] T23["★ skills_extended/ — Tier 2+3<br/>━━━━━━━━━━<br/>NOT plugin-scanned<br/>57 skills with categories: frontmatter"] SUBSET_FILTER["★ init_session() subset filter<br/>━━━━━━━━━━<br/>Check SkillInfo.categories<br/>∩ config.subsets.disabled<br/>Skip if intersects → excluded"] OVR_FILTER["★ override detection<br/>━━━━━━━━━━<br/>detect_project_local_overrides()<br/>.claude/skills/ or<br/>.autoskillit/skills/ wins<br/>Bundled skill excluded"] EPH_DIR["Ephemeral skill dir<br/>━━━━━━━━━━<br/>subset-filtered<br/>override-aware copy<br/>passed via --add-dir"] TIER1 -->|"scanned at start"| SUBSET_FILTER T23 -->|"★ read categories:<br/>frontmatter"| SUBSET_FILTER SUBSET_FILTER -->|"disabled skills<br/>excluded"| OVR_FILTER OVR_FILTER -->|"bundled<br/>excluded if local exists"| EPH_DIR end subgraph RecipeBoundary ["RECIPE VALIDATION GATE — recipe/ + cli/app.py"] direction TB VAL_CTX["● make_validation_context()<br/>━━━━━━━━━━<br/>★ disabled_subsets=<br/>frozenset(cfg.subsets.disabled)"] SK_RULE["★ subset-disabled-skill rule<br/>━━━━━━━━━━<br/>recipe/rules_skills.py<br/>WARNING if skill.categories<br/>∩ disabled_subsets"] T_RULE["★ subset-disabled-tool rule<br/>━━━━━━━━━━<br/>recipe/rules_tools.py<br/>uses TOOL_SUBSET_TAGS<br/>WARNING if tool in disabled"] OVR_RULE["★ project-local-override rule<br/>━━━━━━━━━━<br/>recipe/rules_skills.py<br/>WARNING if /autoskillit:name<br/>has a project-local override"] COOK_GATE["★ cook-time subset gate<br/>━━━━━━━━━━<br/>cli/app.py cook()<br/>tty? → interactive prompt<br/>non-tty → hard error exit 1"] VAL_CTX --> SK_RULE VAL_CTX --> T_RULE VAL_CTX --> OVR_RULE SK_RULE --> COOK_GATE T_RULE --> COOK_GATE OVR_RULE --> COOK_GATE end %% TRUST FLOWS %% FR -->|"always visible"| KIT_HIDE KIT_HIDE -->|"37 kitchen tools<br/>hidden at startup"| SUB_HIDE SUB_HIDE -->|"subset tags additionally<br/>hidden per config"| HL_REVEAL OK_ENABLE -->|"session override attempt"| REDISABLE GUARD_HOOK -->|"blocks headless<br/>open_kitchen calls"| OK_ENABLE HL_GUARD -->|"hook layer"| GATE_CHECK GATE_CHECK -->|"code layer"| FACTORY_FIX %% CLASS ASSIGNMENTS %% class FR stateNode; class KIT_HIDE,SUB_HIDE,HL_REVEAL detector; class OK_ENABLE phase; class REDISABLE,GUARD_HOOK detector; class HL_GUARD,GATE_CHECK,FACTORY_FIX detector; class TIER1 output; class T23 newComponent; class SUBSET_FILTER,OVR_FILTER detector; class EPH_DIR output; class VAL_CTX handler; class SK_RULE,T_RULE,OVR_RULE detector; class COOK_GATE gap;State Lifecycle Diagram
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%% flowchart TB %% CLASS DEFINITIONS %% classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff; classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff; classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff; classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff; classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000; subgraph L0const ["L0 CONSTANTS — core/types.py (INIT_ONLY: module load)"] direction LR HTCONST["★ HEADLESS_TOOLS<br/>━━━━━━━━━━<br/>frozenset({'test_check'})<br/>INIT_ONLY: module-level"] FRCONST["★ FREE_RANGE_TOOLS<br/>━━━━━━━━━━<br/>frozenset({open_kitchen,<br/>close_kitchen})<br/>INIT_ONLY: module-level"] CATTAGS["★ CATEGORY_TAGS<br/>━━━━━━━━━━<br/>frozenset{github,ci,clone,<br/>telemetry,arch-lens,audit}<br/>INIT_ONLY: module-level"] TOOLMAP["★ TOOL_SUBSET_TAGS<br/>━━━━━━━━━━<br/>dict[tool → frozenset[tag]]<br/>INIT_ONLY: module-level"] end subgraph L1cfg ["L1 CONFIG — config/settings.py (INIT_ONLY: from_dynaconf())"] direction TB SUBSETS["★ SubsetsConfig<br/>━━━━━━━━━━<br/>disabled: list[str]<br/>custom_tags: dict[str, list[str]]<br/>INIT_ONLY after construction<br/>⚠ warns on unknown names"] SKILLSCFG["★ SkillsConfig<br/>━━━━━━━━━━<br/>tier1: list[str]<br/>tier2: list[str]<br/>tier3: list[str]<br/>INIT_ONLY after construction<br/>⚠ raises on duplicates"] AUTOCFG["● AutomationConfig<br/>━━━━━━━━━━<br/>subsets: SubsetsConfig<br/>skills: SkillsConfig<br/>INIT_ONLY: frozen dataclass<br/>__post_init__ validates"] SUBCFG_VAL["● AutomationConfig.__post_init__<br/>━━━━━━━━━━<br/>validates: no duplicate tiers<br/>validates: known category names<br/>GATE: ValueError on duplicate<br/>GATE: warning on unknown name"] SUBSETS --> AUTOCFG SKILLSCFG --> AUTOCFG AUTOCFG --> SUBCFG_VAL end subgraph L1ws ["L1 WORKSPACE — workspace/skills.py (INIT_ONLY: scan time)"] direction TB SKILLINFO["● SkillInfo<br/>━━━━━━━━━━<br/>★ categories: frozenset[str]<br/>INIT_ONLY: set by _scan_directory()<br/>source: read_skill_categories(SKILL.md)"] RDCAT["★ read_skill_categories()<br/>━━━━━━━━━━<br/>Path → frozenset[str]<br/>Reads categories: from SKILL.md<br/>INIT_ONLY: once per scan<br/>empty set if absent/malformed"] RDCAT --> SKILLINFO end subgraph L1session ["L1 SESSION INIT — workspace/session_skills.py (MUTABLE: rebuilt each call)"] direction TB INIT_S["● init_session(config, project_dir)<br/>━━━━━━━━━━<br/>Reads: config.subsets.disabled<br/>Reads: config.skills.tier2<br/>Reads: detect_project_local_overrides()<br/>Reads: skill_info.categories"] SUB_CHECK["★ subset intersection check<br/>━━━━━━━━━━<br/>skill.categories ∩ disabled_subsets<br/>→ skip if non-empty<br/>GATE: per-skill filter"] OVR_CHECK["★ override check<br/>━━━━━━━━━━<br/>skill.name in local_overrides<br/>→ skip bundled if local exists<br/>GATE: per-skill filter"] EPH["Ephemeral skill dir<br/>━━━━━━━━━━<br/>MUTABLE: rebuilt each session<br/>Filtered copy of skills_extended/"] INIT_S --> SUB_CHECK SUB_CHECK --> OVR_CHECK OVR_CHECK --> EPH end subgraph L2val ["L2 RECIPE VALIDATION — recipe/ (DERIVED: per validation run)"] direction TB VALCTX["★ ValidationContext.disabled_subsets<br/>━━━━━━━━━━<br/>frozenset: DERIVED from config<br/>INIT_ONLY for validation run<br/>plumbed via make_validation_context()"] SK_RULE_STATE["★ subset-disabled-skill rule<br/>━━━━━━━━━━<br/>Reads: skill.categories<br/>Reads: ctx.disabled_subsets<br/>Emits: WARNING finding<br/>DERIVED: stateless rule"] T_RULE_STATE["★ subset-disabled-tool rule<br/>━━━━━━━━━━<br/>Reads: TOOL_SUBSET_TAGS[tool]<br/>Reads: ctx.disabled_subsets<br/>Emits: WARNING finding<br/>DERIVED: stateless rule"] VALCTX --> SK_RULE_STATE VALCTX --> T_RULE_STATE end %% GATES AND VALIDATION FLOWS %% CATTAGS -->|"validates against"| SUBCFG_VAL HTCONST -->|"HEADLESS_TOOLS<br/>used by rules_tools"| T_RULE_STATE TOOLMAP -->|"TOOL_SUBSET_TAGS<br/>used by rules_tools"| T_RULE_STATE SUBCFG_VAL -->|"subsets.disabled<br/>plumbed to"| VALCTX SUBCFG_VAL -->|"subsets.disabled<br/>used by"| INIT_S SKILLINFO -->|"categories<br/>read by subset check"| SUB_CHECK SKILLINFO -->|"categories<br/>read by skill rule"| SK_RULE_STATE %% CLASS ASSIGNMENTS %% class HTCONST,FRCONST,CATTAGS,TOOLMAP detector; class SUBSETS,SKILLSCFG newComponent; class AUTOCFG handler; class SUBCFG_VAL stateNode; class SKILLINFO phase; class RDCAT newComponent; class INIT_S handler; class SUB_CHECK,OVR_CHECK stateNode; class EPH output; class VALCTX newComponent; class SK_RULE_STATE,T_RULE_STATE detector;Closes #306
Implementation Plan
Plan files:
/home/talon/projects/autoskillit-runs/impl-20260315-172637-050288/temp/make-plan/skill-tool-access-control-group-a_plan_2026-03-15_180000.md/home/talon/projects/autoskillit-runs/impl-20260315-172637-050288/temp/make-plan/groupB_skill_directory_layout_plan_2026-03-15_172700_part_a.md/home/talon/projects/autoskillit-runs/impl-20260315-172637-050288/temp/make-plan/groupB_skill_directory_layout_plan_2026-03-15_172700_part_b.md/home/talon/projects/autoskillit-runs/impl-20260315-172637-050288/temp/make-plan/groupC_skill_category_metadata_subset_config_plan_2026-03-15_202248_part_a.md/home/talon/projects/autoskillit-runs/impl-20260315-172637-050288/temp/make-plan/groupD_subset_visibility_enforcement_plan_2026-03-15_202800.md/home/talon/projects/autoskillit-runs/impl-20260315-172637-050288/temp/make-plan/groupE_project_local_overrides_plan_2026-03-15_172637_part_a.md/home/talon/projects/autoskillit-runs/impl-20260315-172637-050288/temp/make-plan/groupF_recipe_validation_subset_disabled_plan_2026-03-15_221419.md/home/talon/projects/autoskillit-runs/impl-20260315-172637-050288/temp/make-plan/groupG_documentation_plan_2026-03-15_172700.mdToken Usage Summary
No token usage data available for this pipeline run.
🤖 Generated with Claude Code via AutoSkillit