Skip to content

feat: three-tier skill visibility, subset access control, and overrides#413

Merged
Trecek merged 48 commits intointegrationfrom
skill-and-tool-access-control-tiered-visibility-cross-tier-s/306
Mar 16, 2026
Merged

feat: three-tier skill visibility, subset access control, and overrides#413
Trecek merged 48 commits intointegrationfrom
skill-and-tool-access-control-tiered-visibility-cross-tier-s/306

Conversation

@Trecek
Copy link
Collaborator

@Trecek Trecek commented Mar 16, 2026

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 for test_check, remove the headless-session gate pre-open from _factory.py and server/__init__.py, and update the GATED_TOOLS / UNGATED_TOOLS / WORKER_TOOLS / HEADLESS_BLOCKED_UNGATED_TOOLS constants in core/types.py to 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 only open-kitchen, close-kitchen, sous-chef (3 dirs, Tier 1). New skills_extended/ holds the remaining 57 skills. SkillsConfig dataclass added to AutomationConfig with tier1/tier2/tier3 fields. defaults.yaml records 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_SKILLS constant from session_skills.py, makes init_session() accept and use an AutomationConfig for tier-driven gating and unknown-skill warnings, adds --plugin-dir <pkg_root()> to chefs-hat, defaults run_skill's add_dir to bundled_skills_extended_dir().

Group C: Skill Category Metadata + Subset Config Schema

Add categories: frontmatter to 37 applicable bundled SKILL.md files, introduce a SubsetsConfig dataclass that extends AutomationConfig with subsets.disabled and subsets.custom_tags config keys, and add a read_skill_categories() helper to workspace/skills.py that exposes category membership on SkillInfo objects.

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_kitchen re-disable loop after ctx.enable_components(tags={"kitchen"}) to neutralize FastMCP session-rule-overrides-server-rule behavior, and (3) ephemeral skill directory subset filter in init_session().

Group E: Project-Local Skill Overrides

Implement project-local skill override detection: when .claude/skills/<name>/SKILL.md or .autoskillit/skills/<name>/SKILL.md exists, the corresponding bundled skill is excluded from the ephemeral session directory. run_headless_core gains multi-path --add-dir support. 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-skill and subset-disabled-tool WARNING rules, add disabled_subsets field to ValidationContext, add TOOL_SUBSET_TAGS static map to core/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 to CLAUDE.md, docs/architecture.md, and docs/configuration.md.

Requirements

Tier System (TIER)

  • REQ-TIER-001: Skills in skills/ (Tier 1) must be discoverable via --plugin-dir by Claude Code's native plugin scanner.
  • REQ-TIER-002: Skills in skills_extended/ (Tier 2+3) must NOT be discoverable via --plugin-dir.
  • REQ-TIER-003: autoskillit chefs-hat must copy skills from BOTH skills/ and skills_extended/ into the ephemeral session dir.
  • REQ-TIER-004: run_headless_core must pass --add-dir pointing to skills_extended/ so headless sessions discover Tier 2+3 skills.
  • REQ-TIER-005: SkillResolver must scan both skills/ and skills_extended/ directories for resolution and listing.
  • REQ-TIER-006: Each skill must have a default tier assignment in package defaults.
  • REQ-TIER-007: The config schema must support skills.tier1, skills.tier2, skills.tier3 keys for reclassification.
  • REQ-TIER-008: Config-driven reclassification must be resolved at init_session() time using dynaconf layered resolution.
  • REQ-TIER-009: A skill in multiple tier lists must produce a validation error at startup.
  • REQ-TIER-010: Unrecognized skill names in tier config must produce a logged warning, not a crash.
  • REQ-TIER-011: autoskillit chefs-hat must pass --plugin-dir <pkg_root()> to ensure MCP server loads without requiring autoskillit init.

MCP Tool Access (TOOL)

  • REQ-TOOL-001 / REQ-TOOL-002: "headless" tag added; headless sessions pre-enable only headless-tagged tools (test_check), not full kitchen.
  • REQ-TOOL-003 / REQ-TOOL-004: open_kitchen reveals all kitchen tools, then re-disables subset-tagged tools per config.
  • REQ-TOOL-006: test_check reclassified as headless-accessible: {"autoskillit", "kitchen", "headless"}.
  • REQ-TOOL-008: gate.enable() removed from _factory.py for headless sessions.
  • REQ-TOOL-009 / REQ-TOOL-010: 11 formerly ungated tools gain "kitchen" tag; all 39 tools renamed "automation""autoskillit".

Category + Config + Visibility

  • REQ-CAT-001..005: CATEGORY_TAGS registry, SKILL.md categories: frontmatter, subsets.custom_tags config support.
  • REQ-CFG-001..005: subsets.disabled key, dynaconf resolution, graceful unknown-name handling.
  • REQ-VIS-001..008: Tool hiding at startup, skill exclusion from sessions, open_kitchen re-disable loop.

Project-Local Overrides + Recipe Validation + Documentation

  • REQ-OVR-001..004: Project-local overrides shadow bundled; multi-path --add-dir; recipe validation WARNING.
  • REQ-VAL-001..004: subset-disabled-skill/tool rules; cook-time gate (interactive prompt / hard error).
  • REQ-DOC-001..008: Four new docs files; CLAUDE.md, architecture.md, configuration.md updates.

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;
Loading

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;
Loading

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;
Loading

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.md

Token Usage Summary

No token usage data available for this pipeline run.

🤖 Generated with Claude Code via AutoSkillit

Trecek and others added 23 commits March 15, 2026 18:08
- 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>
Copy link
Collaborator Author

@Trecek Trecek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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],
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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],
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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})
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Trecek and others added 6 commits March 16, 2026 00:17
…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>
Copy link
Collaborator Author

@Trecek Trecek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Copy link
Collaborator Author

@Trecek Trecek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoSkillit review found 21 actionable blocking issues. See inline review comments for details. Verdict: changes_requested.

Copy link
Collaborator Author

@Trecek Trecek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Trecek and others added 15 commits March 16, 2026 09:41
…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
@Trecek Trecek added this pull request to the merge queue Mar 16, 2026
Merged via the queue into integration with commit 2f46e08 Mar 16, 2026
2 checks passed
@Trecek Trecek deleted the skill-and-tool-access-control-tiered-visibility-cross-tier-s/306 branch March 16, 2026 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant