From 7dacd8c256c09e95dc5939c4436fe673ae11bae2 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 12:02:02 -0700 Subject: [PATCH 01/13] test: cover direct-tool recipe pack requirements --- tests/recipe/test_rules_packs.py | 89 +++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/tests/recipe/test_rules_packs.py b/tests/recipe/test_rules_packs.py index fef4bd6c8c..e8e9cc1260 100644 --- a/tests/recipe/test_rules_packs.py +++ b/tests/recipe/test_rules_packs.py @@ -6,7 +6,7 @@ import pytest -from autoskillit.core import Severity +from autoskillit.core import KITCHEN_GATED_TOOLS, Severity from autoskillit.recipe.io import builtin_recipes_dir, load_recipe from autoskillit.recipe.registry import run_semantic_rules from autoskillit.recipe.schema import Recipe, RecipeStep @@ -43,6 +43,18 @@ def _make_recipe_with_run_skill( ) +def _make_recipe_with_tool( + requires_packs: list[str], tool: str, step_name: str = "call_it" +) -> Recipe: + return Recipe( + name="test", + description="test recipe", + version="0.7.2", + requires_packs=requires_packs, + steps={step_name: RecipeStep(tool=tool)}, + ) + + # ---------------------------------------------------------------------- # unknown-required-pack tests # ---------------------------------------------------------------------- @@ -216,3 +228,78 @@ def test_research_design_yaml_triggers_error_without_vis_lens(self): vis_lens_findings = [f for f in findings if "vis-lens" in f.message] assert len(vis_lens_findings) == 2 assert all(f.severity == Severity.ERROR for f in vis_lens_findings) + + +class TestUndeclaredPackRequirementToolSteps: + def _run_rule(self, recipe: Recipe) -> list: + import autoskillit.recipe # noqa: F401 + + return [f for f in run_semantic_rules(recipe) if f.rule == "undeclared-pack-requirement"] + + def test_tool_needing_undeclared_enabled_pack_is_error(self): + recipe = _make_recipe_with_tool(["research"], "verify_review_receipt") + findings = self._run_rule(recipe) + assert len(findings) == 1 + assert findings[0].severity == Severity.ERROR + assert findings[0].step_name == "call_it" + assert "github" in findings[0].message + assert "verify_review_receipt" in findings[0].message + + def test_tool_with_declared_pack_passes(self): + recipe = _make_recipe_with_tool(["research", "github"], "verify_review_receipt") + assert not self._run_rule(recipe) + + def test_kitchen_core_tool_needs_no_declaration(self): + recipe = _make_recipe_with_tool(["research"], "run_cmd") + assert not self._run_rule(recipe) + + def test_kitchen_core_plus_category_tool_needs_no_declaration(self): + recipe = _make_recipe_with_tool(["research"], "write_telemetry_files") + assert not self._run_rule(recipe) + + def test_empty_requires_packs_kitchen_gated_tool_passes(self): + assert "verify_review_receipt" in KITCHEN_GATED_TOOLS + assert "post_pr_review" not in KITCHEN_GATED_TOOLS + recipe = _make_recipe_with_tool([], "verify_review_receipt") + assert not self._run_rule(recipe) + + def test_empty_requires_packs_exploration_tool_passes(self): + recipe = _make_recipe_with_tool([], "submit_exploration_query") + assert not self._run_rule(recipe) + + def test_empty_requires_packs_non_kitchen_tool_is_error(self): + recipe = _make_recipe_with_tool([], "post_pr_review") + findings = self._run_rule(recipe) + assert len(findings) == 1 + assert findings[0].severity == Severity.ERROR + assert "github" in findings[0].message + assert "post_pr_review" in findings[0].message + + def test_tool_less_and_unknown_tool_steps_are_skipped(self): + recipe = replace( + _make_recipe(["research"]), + steps={ + "py": RecipeStep(python="m.f"), + "bogus": RecipeStep(tool="not_a_real_tool"), + "stop": RecipeStep(action="stop"), + }, + ) + assert not self._run_rule(recipe) + + @pytest.mark.parametrize("name", ["research.yaml", "research-review.yaml"]) + def test_research_recipes_without_github_trigger_error(self, name): + recipe = replace( + load_recipe(builtin_recipes_dir() / name), + requires_packs=["research", "exp-lens", "vis-lens"], + ) + findings = self._run_rule(recipe) + assert {f.step_name for f in findings} == { + "check_review_posted", + "check_audit_review_posted", + } + assert all(f.severity == Severity.ERROR for f in findings) + + @pytest.mark.parametrize("name", ["research.yaml", "research-review.yaml"]) + def test_bundled_research_recipes_have_no_undeclared_packs(self, name): + recipe = load_recipe(builtin_recipes_dir() / name) + assert not self._run_rule(recipe) From 7fef78607147eeda2bc48cccf2daa3697a9f4416 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 12:03:19 -0700 Subject: [PATCH 02/13] fix: validate direct-tool pack visibility in recipes --- src/autoskillit/recipe/rules/rules_packs.py | 56 +++++++++++++++++---- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/src/autoskillit/recipe/rules/rules_packs.py b/src/autoskillit/recipe/rules/rules_packs.py index cf9bd608a1..ded2f64667 100644 --- a/src/autoskillit/recipe/rules/rules_packs.py +++ b/src/autoskillit/recipe/rules/rules_packs.py @@ -2,7 +2,15 @@ from __future__ import annotations -from autoskillit.core import PACK_REGISTRY, SKILL_TOOLS, Severity +from autoskillit.core import ( + CATEGORY_TAGS, + EXPLORATION_TOOLS, + KITCHEN_GATED_TOOLS, + PACK_REGISTRY, + SKILL_TOOLS, + TOOL_SUBSET_TAGS, + Severity, +) from autoskillit.recipe._analysis import ValidationContext from autoskillit.recipe._skill_helpers import _get_skill_category_map from autoskillit.recipe.contracts import resolve_skill_name @@ -33,22 +41,37 @@ def _check_unknown_required_pack(ctx: ValidationContext) -> list[RuleFinding]: return findings +def _food_truck_can_call(tool: str, declared_packs: frozenset[str]) -> bool: + """Whether a food truck dispatched with *declared_packs* exposes *tool*. + + Non-empty packs enable ``kitchen-core`` plus each declared pack. Empty packs + enable the ``kitchen`` tag, including exploration tools. + """ + if not declared_packs: + return tool in KITCHEN_GATED_TOOLS | EXPLORATION_TOOLS + return bool(TOOL_SUBSET_TAGS[tool] & (declared_packs | {"kitchen-core"})) + + @semantic_rule( name="undeclared-pack-requirement", description=( - "Recipes dispatching skills in default-disabled pack categories " - "must declare those packs in requires_packs" + "Recipes must declare in requires_packs every pack gating a dispatched " + "skill category or a direct tool step's food-truck visibility" ), severity=Severity.ERROR, ) def _check_undeclared_pack_requirement(ctx: ValidationContext) -> list[RuleFinding]: - """Flag recipes that dispatch skills in default-disabled pack categories without - declaring the corresponding requires_packs entry. + """Flag dispatched skills in undeclared default-disabled pack categories. + + Direct tools must be visible under the exact non-empty ``requires_packs`` + allowlist, with ``kitchen-core`` implicit. Empty packs expose kitchen-tagged + tools, including exploration tools. ``_food_truck_can_call`` mirrors + ``_apply_session_type_visibility``: each declared pack is enabled separately, + so any matching tag suffices. - Mirrors the pattern of check_requires_features_declared in rules_features.py. - PACK_REGISTRY keys directly equal CATEGORY_TAGS, so no intermediate mapping - (like FeatureDef.skill_categories) is needed — a category that appears in - PACK_REGISTRY with default_enabled=False is exactly a pack that must be declared. + Only the static pack axis is checked. Config-disabled subsets and feature + tags suppressed by ``_suppress_disabled_feature_tags`` are covered separately + by ``_check_subset_disabled_tool`` and ``check_requires_features_declared``. """ category_map = ( ctx.skill_category_map if ctx.skill_category_map is not None else _get_skill_category_map() @@ -60,7 +83,22 @@ def _check_undeclared_pack_requirement(ctx: ValidationContext) -> list[RuleFindi findings: list[RuleFinding] = [] for step_name, step in ctx.recipe.steps.items(): + if step.tool is None: + continue if step.tool not in SKILL_TOOLS: + tool_packs = TOOL_SUBSET_TAGS.get(step.tool, frozenset()) & CATEGORY_TAGS + if tool_packs and not _food_truck_can_call(step.tool, declared_packs): + findings.append( + make_finding( + rule_name="undeclared-pack-requirement", + step_name=step_name, + message=( + f"step '{step_name}': tool '{step.tool}' is not callable in this " + f"recipe's food-truck session. Declare one of its packs " + f"{sorted(tool_packs)} in requires_packs." + ), + ) + ) continue skill_cmd = (step.with_args or {}).get("skill_command") or "" skill_name = resolve_skill_name(skill_cmd) From f9e349a71ad53fe62614945cc28fcbf0fff22c1f Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 12:04:50 -0700 Subject: [PATCH 03/13] fix: declare github pack in research recipes --- src/autoskillit/recipes/diagrams/research.md | 2 +- src/autoskillit/recipes/research-review.json | 3 ++- src/autoskillit/recipes/research-review.yaml | 1 + src/autoskillit/recipes/research.json | 3 ++- src/autoskillit/recipes/research.yaml | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/autoskillit/recipes/diagrams/research.md b/src/autoskillit/recipes/diagrams/research.md index 50d42f6f39..d9710a884e 100644 --- a/src/autoskillit/recipes/diagrams/research.md +++ b/src/autoskillit/recipes/diagrams/research.md @@ -1,4 +1,4 @@ - + ## research diff --git a/src/autoskillit/recipes/research-review.json b/src/autoskillit/recipes/research-review.json index c6c0930fa9..c849d394c9 100644 --- a/src/autoskillit/recipes/research-review.json +++ b/src/autoskillit/recipes/research-review.json @@ -8,7 +8,8 @@ "requires_packs": [ "research", "exp-lens", - "vis-lens" + "vis-lens", + "github" ], "description": "PR creation, review, claim audit, and finalization worktree and completed experiment outputs from a campaign dispatch, then runs the 22-step PR/review pipeline: prepare PR, run experiment lenses, stage bundle, compose PR, review, audit claims, resolve feedback, re-run if needed, finalize, and export. Terminates with sentinel emission of output paths.\n", "dispatch_only": true, diff --git a/src/autoskillit/recipes/research-review.yaml b/src/autoskillit/recipes/research-review.yaml index 788a990804..b1a9429133 100644 --- a/src/autoskillit/recipes/research-review.yaml +++ b/src/autoskillit/recipes/research-review.yaml @@ -7,6 +7,7 @@ requires_packs: - research - exp-lens - vis-lens +- github description: 'PR creation, review, claim audit, and finalization worktree and completed experiment outputs from a campaign dispatch, then runs the 22-step PR/review pipeline: prepare PR, run experiment lenses, stage bundle, compose PR, review, audit claims, resolve feedback, re-run if needed, finalize, and export. Terminates with sentinel emission of output paths. diff --git a/src/autoskillit/recipes/research.json b/src/autoskillit/recipes/research.json index 87b404bd6f..2f02ca3ba8 100644 --- a/src/autoskillit/recipes/research.json +++ b/src/autoskillit/recipes/research.json @@ -4,7 +4,8 @@ "requires_packs": [ "research", "exp-lens", - "vis-lens" + "vis-lens", + "github" ], "description": "End-to-end technical research: scope, implement, report\n", "summary": "scope > plan > [review?] > worktree > env-setup > decompose > phases > experiment > report > open-pr > [review? > resolve > [re-run?] > push] > archive", diff --git a/src/autoskillit/recipes/research.yaml b/src/autoskillit/recipes/research.yaml index f76e43a507..989cf476b6 100644 --- a/src/autoskillit/recipes/research.yaml +++ b/src/autoskillit/recipes/research.yaml @@ -4,6 +4,7 @@ requires_packs: - research - exp-lens - vis-lens +- github description: 'End-to-end technical research: scope, implement, report ' From 2dc4c73165c2fe4a6ac425beab6d2f72ee54cf19 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 12:05:46 -0700 Subject: [PATCH 04/13] test: retire research tool callability exemptions --- tests/recipe/test_io_discovery.py | 2 +- tests/recipe/test_research_review_recipe.py | 2 +- .../server/test_consumer_tool_callability.py | 20 ------------------- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/tests/recipe/test_io_discovery.py b/tests/recipe/test_io_discovery.py index 6b3836d29f..68b03a77d8 100644 --- a/tests/recipe/test_io_discovery.py +++ b/tests/recipe/test_io_discovery.py @@ -403,7 +403,7 @@ def test_research_recipe_declares_requires_packs(): path = pkg_root() / "recipes" / "research.yaml" recipe = load_recipe(path) - assert recipe.requires_packs == ["research", "exp-lens", "vis-lens"] + assert recipe.requires_packs == ["research", "exp-lens", "vis-lens", "github"] # --------------------------------------------------------------------------- diff --git a/tests/recipe/test_research_review_recipe.py b/tests/recipe/test_research_review_recipe.py index 620cfe2c16..3b59876ed4 100644 --- a/tests/recipe/test_research_review_recipe.py +++ b/tests/recipe/test_research_review_recipe.py @@ -31,7 +31,7 @@ def test_categories(self, recipe) -> None: assert recipe.categories == ["research-family"] def test_requires_packs(self, recipe) -> None: - assert set(recipe.requires_packs) == {"research", "exp-lens", "vis-lens"} + assert set(recipe.requires_packs) == {"research", "exp-lens", "vis-lens", "github"} def test_no_autoskillit_version(self) -> None: path = builtin_recipes_dir() / "research-review.yaml" diff --git a/tests/server/test_consumer_tool_callability.py b/tests/server/test_consumer_tool_callability.py index e7d81714d1..311367d8a7 100644 --- a/tests/server/test_consumer_tool_callability.py +++ b/tests/server/test_consumer_tool_callability.py @@ -104,26 +104,6 @@ "batch_cleanup_clones", CatalogContext.INTERACTIVE_ORCHESTRATOR, ): "#5179: fleet mutation tool is hidden from interactive orchestrators", - ( - "recipe:research:check_review_posted", - "verify_review_receipt", - CatalogContext.FOOD_TRUCK, - ): "#5178: recipe does not declare the github pack", - ( - "recipe:research:check_audit_review_posted", - "verify_review_receipt", - CatalogContext.FOOD_TRUCK, - ): "#5178: recipe does not declare the github pack", - ( - "recipe:research-review:check_review_posted", - "verify_review_receipt", - CatalogContext.FOOD_TRUCK, - ): "#5178: recipe does not declare the github pack", - ( - "recipe:research-review:check_audit_review_posted", - "verify_review_receipt", - CatalogContext.FOOD_TRUCK, - ): "#5178: recipe does not declare the github pack", } From 796d0d1e4e76057532b7df77f908e55cd188ae0e Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 12:06:38 -0700 Subject: [PATCH 05/13] docs: describe direct-tool recipe pack validation --- docs/recipes/authoring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/recipes/authoring.md b/docs/recipes/authoring.md index 52a3c97395..fb96573212 100644 --- a/docs/recipes/authoring.md +++ b/docs/recipes/authoring.md @@ -85,7 +85,7 @@ The rule families live in `src/autoskillit/recipe/rules_*.py` (28 files): | `rules_inputs.py` | Ingredient/version validation — required ingredients are present, types are correct | | `rules_isolation.py` | Workspace isolation breaches: `source-isolation-violation` and `git-mutation-on-source` | | `rules_merge.py` | `merge_worktree` routing completeness — every `MergeFailedStep` value must have a route | -| `rules_packs.py` | Unknown pack names referenced in `requires_packs` | +| `rules_packs.py` | Unknown pack names in `requires_packs`; packs required by dispatched skill categories or by direct tool steps' food-truck visibility missing from `requires_packs` | | `rules_reachability.py` | Symbolic reachability: `capture-inversion-detection`, `event-scope-requires-upstream-capture` | | `rules_recipe.py` | Unknown sub-recipe references inside a `requires_packs` entry | | `rules_skill_content.py` | SKILL.md bash-block placeholder validation (`undefined-bash-placeholder`) | From 4760d96eaeec35475b635f4a6ab2b3462b7311e8 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 12:14:29 -0700 Subject: [PATCH 06/13] test: declare kitchen-core for headless validation fixture --- tests/server/test_tools_recipe.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/server/test_tools_recipe.py b/tests/server/test_tools_recipe.py index 61b56206a0..70f2cec715 100644 --- a/tests/server/test_tools_recipe.py +++ b/tests/server/test_tools_recipe.py @@ -236,6 +236,7 @@ async def test_validate_recipe_includes_semantic_findings(self, tmp_path): script.write_text( "name: semantic-test\n" "description: Has model on non-skill step\n" + "requires_packs: [kitchen-core]\n" "kitchen_rules:\n" " - test\n" "steps:\n" From 2f543aebd0475d95415a756e538a9845c0d12091 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 13:44:03 -0700 Subject: [PATCH 07/13] test: prove empty-pack visibility against the runtime catalog --- src/autoskillit/recipe/rules/rules_packs.py | 5 +++-- tests/server/test_session_type_visibility_orchestrator.py | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/autoskillit/recipe/rules/rules_packs.py b/src/autoskillit/recipe/rules/rules_packs.py index ded2f64667..ccba056029 100644 --- a/src/autoskillit/recipe/rules/rules_packs.py +++ b/src/autoskillit/recipe/rules/rules_packs.py @@ -44,8 +44,9 @@ def _check_unknown_required_pack(ctx: ValidationContext) -> list[RuleFinding]: def _food_truck_can_call(tool: str, declared_packs: frozenset[str]) -> bool: """Whether a food truck dispatched with *declared_packs* exposes *tool*. - Non-empty packs enable ``kitchen-core`` plus each declared pack. Empty packs - enable the ``kitchen`` tag, including exploration tools. + Mirrors the headless-orchestrator branch of ``_apply_session_type_visibility``: + non-empty ``requires_packs`` enables ``kitchen-core`` plus each declared pack; + empty ``requires_packs`` falls back to the ``kitchen`` tag surface. """ if not declared_packs: return tool in KITCHEN_GATED_TOOLS | EXPLORATION_TOOLS diff --git a/tests/server/test_session_type_visibility_orchestrator.py b/tests/server/test_session_type_visibility_orchestrator.py index e071ff4d9c..0c1fd7cd84 100644 --- a/tests/server/test_session_type_visibility_orchestrator.py +++ b/tests/server/test_session_type_visibility_orchestrator.py @@ -165,6 +165,7 @@ async def test_food_truck_with_multiple_packs(monkeypatch): async def test_food_truck_without_tool_tags_sees_full_kitchen(monkeypatch): """ORCHESTRATOR+HEADLESS without FOOD_TRUCK_TOOL_TAGS falls back to full kitchen.""" from autoskillit.core import EVIDENCE_READER_TOOLS, KITCHEN_GATED_TOOLS + from autoskillit.recipe.rules.rules_packs import _food_truck_can_call from autoskillit.server import _apply_session_type_visibility, mcp monkeypatch.setenv("AUTOSKILLIT_SESSION_TYPE", "orchestrator") @@ -177,6 +178,10 @@ async def test_food_truck_without_tool_tags_sees_full_kitchen(monkeypatch): for name in KITCHEN_GATED_TOOLS: assert name in tool_names + assert "submit_exploration_query" in tool_names + assert "post_pr_review" not in tool_names + for name in ("submit_exploration_query", "post_pr_review"): + assert _food_truck_can_call(name, frozenset()) == (name in tool_names) assert tool_names.isdisjoint(EVIDENCE_READER_TOOLS) From 2dc180731c7c9a2f0b9fda775dfc2ded80afd512 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 13:53:42 -0700 Subject: [PATCH 08/13] test: keep runtime visibility coverage within the server layer --- tests/server/test_session_type_visibility_orchestrator.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/server/test_session_type_visibility_orchestrator.py b/tests/server/test_session_type_visibility_orchestrator.py index 0c1fd7cd84..fb3f90d551 100644 --- a/tests/server/test_session_type_visibility_orchestrator.py +++ b/tests/server/test_session_type_visibility_orchestrator.py @@ -165,7 +165,6 @@ async def test_food_truck_with_multiple_packs(monkeypatch): async def test_food_truck_without_tool_tags_sees_full_kitchen(monkeypatch): """ORCHESTRATOR+HEADLESS without FOOD_TRUCK_TOOL_TAGS falls back to full kitchen.""" from autoskillit.core import EVIDENCE_READER_TOOLS, KITCHEN_GATED_TOOLS - from autoskillit.recipe.rules.rules_packs import _food_truck_can_call from autoskillit.server import _apply_session_type_visibility, mcp monkeypatch.setenv("AUTOSKILLIT_SESSION_TYPE", "orchestrator") @@ -180,8 +179,6 @@ async def test_food_truck_without_tool_tags_sees_full_kitchen(monkeypatch): assert name in tool_names assert "submit_exploration_query" in tool_names assert "post_pr_review" not in tool_names - for name in ("submit_exploration_query", "post_pr_review"): - assert _food_truck_can_call(name, frozenset()) == (name in tool_names) assert tool_names.isdisjoint(EVIDENCE_READER_TOOLS) From 233cb4aa9cc0b1b793989630f46d07d211e75579 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 17:38:54 -0700 Subject: [PATCH 09/13] fix(review): keep review tool classification assertions in core tests --- tests/core/test_type_constants_tool_classification.py | 7 +++++++ tests/recipe/test_rules_packs.py | 4 +--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/core/test_type_constants_tool_classification.py b/tests/core/test_type_constants_tool_classification.py index 96f56e1e53..e70f9df33a 100644 --- a/tests/core/test_type_constants_tool_classification.py +++ b/tests/core/test_type_constants_tool_classification.py @@ -7,6 +7,13 @@ pytestmark = [pytest.mark.layer("core"), pytest.mark.small] +def test_review_tool_kitchen_classification() -> None: + from autoskillit.core import KITCHEN_GATED_TOOLS + + assert "verify_review_receipt" in KITCHEN_GATED_TOOLS + assert "post_pr_review" not in KITCHEN_GATED_TOOLS + + def test_headless_tools_contains_expected_names() -> None: from autoskillit.core.types import HEADLESS_TOOLS diff --git a/tests/recipe/test_rules_packs.py b/tests/recipe/test_rules_packs.py index e8e9cc1260..8918207d4d 100644 --- a/tests/recipe/test_rules_packs.py +++ b/tests/recipe/test_rules_packs.py @@ -6,7 +6,7 @@ import pytest -from autoskillit.core import KITCHEN_GATED_TOOLS, Severity +from autoskillit.core import Severity from autoskillit.recipe.io import builtin_recipes_dir, load_recipe from autoskillit.recipe.registry import run_semantic_rules from autoskillit.recipe.schema import Recipe, RecipeStep @@ -258,8 +258,6 @@ def test_kitchen_core_plus_category_tool_needs_no_declaration(self): assert not self._run_rule(recipe) def test_empty_requires_packs_kitchen_gated_tool_passes(self): - assert "verify_review_receipt" in KITCHEN_GATED_TOOLS - assert "post_pr_review" not in KITCHEN_GATED_TOOLS recipe = _make_recipe_with_tool([], "verify_review_receipt") assert not self._run_rule(recipe) From abe51d2c9dd9164fe346ab0cc64615faacbef65b Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 17:39:30 -0700 Subject: [PATCH 10/13] fix(review): ensure kitchen-core rule test cannot pass via research tags --- tests/recipe/test_rules_packs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/recipe/test_rules_packs.py b/tests/recipe/test_rules_packs.py index 8918207d4d..f86ceb9f7a 100644 --- a/tests/recipe/test_rules_packs.py +++ b/tests/recipe/test_rules_packs.py @@ -6,7 +6,7 @@ import pytest -from autoskillit.core import Severity +from autoskillit.core import TOOL_SUBSET_TAGS, Severity from autoskillit.recipe.io import builtin_recipes_dir, load_recipe from autoskillit.recipe.registry import run_semantic_rules from autoskillit.recipe.schema import Recipe, RecipeStep @@ -254,6 +254,7 @@ def test_kitchen_core_tool_needs_no_declaration(self): assert not self._run_rule(recipe) def test_kitchen_core_plus_category_tool_needs_no_declaration(self): + assert "research" not in TOOL_SUBSET_TAGS["write_telemetry_files"] recipe = _make_recipe_with_tool(["research"], "write_telemetry_files") assert not self._run_rule(recipe) From 5fb74613a7bdd54b63295a3b4940a6c0adfe1441 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 17:40:09 -0700 Subject: [PATCH 11/13] fix(review): declare recipe rule registration import at module scope --- tests/recipe/test_rules_packs.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/recipe/test_rules_packs.py b/tests/recipe/test_rules_packs.py index f86ceb9f7a..ed2b05a505 100644 --- a/tests/recipe/test_rules_packs.py +++ b/tests/recipe/test_rules_packs.py @@ -6,6 +6,7 @@ import pytest +import autoskillit.recipe # noqa: F401 -- triggers rule registration from autoskillit.core import TOOL_SUBSET_TAGS, Severity from autoskillit.recipe.io import builtin_recipes_dir, load_recipe from autoskillit.recipe.registry import run_semantic_rules @@ -62,7 +63,6 @@ def _make_recipe_with_tool( def test_unknown_pack_produces_error(): """Pack name not in PACK_REGISTRY produces an ERROR finding.""" - import autoskillit.recipe # noqa: F401 -- triggers rule registration recipe = _make_recipe(["nonexistent-pack"]) findings = [f for f in run_semantic_rules(recipe) if f.rule == "unknown-required-pack"] @@ -73,7 +73,6 @@ def test_unknown_pack_produces_error(): def test_known_pack_produces_no_finding(): """Known pack name (in PACK_REGISTRY) produces no finding.""" - import autoskillit.recipe # noqa: F401 -- triggers rule registration recipe = _make_recipe(["research"]) findings = [f for f in run_semantic_rules(recipe) if f.rule == "unknown-required-pack"] @@ -82,7 +81,6 @@ def test_known_pack_produces_no_finding(): def test_mixed_packs_flags_only_unknown(): """Only unknown packs are flagged; known packs pass silently.""" - import autoskillit.recipe # noqa: F401 -- triggers rule registration recipe = _make_recipe(["research", "bogus-pack"]) findings = [f for f in run_semantic_rules(recipe) if f.rule == "unknown-required-pack"] @@ -92,7 +90,6 @@ def test_mixed_packs_flags_only_unknown(): def test_empty_requires_packs_produces_no_finding(): """Recipes without requires_packs produce no finding.""" - import autoskillit.recipe # noqa: F401 -- triggers rule registration recipe = _make_recipe([]) findings = [f for f in run_semantic_rules(recipe) if f.rule == "unknown-required-pack"] @@ -101,7 +98,6 @@ def test_empty_requires_packs_produces_no_finding(): def test_all_builtin_packs_pass(): """Every pack in PACK_REGISTRY is a valid name (no self-flagging).""" - import autoskillit.recipe # noqa: F401 -- triggers rule registration from autoskillit.core import PACK_REGISTRY recipe = _make_recipe(list(PACK_REGISTRY.keys())) @@ -130,7 +126,6 @@ def _run_rule( recipe: Recipe, skill_category_map: dict[str, frozenset[str]] | None = None, ) -> list: - import autoskillit.recipe # noqa: F401 -- triggers rule registration if skill_category_map is not None: from autoskillit.recipe._analysis import make_validation_context @@ -217,7 +212,6 @@ def test_unknown_skill_name_is_skipped(self): def test_research_design_yaml_triggers_error_without_vis_lens(self): """research-design.yaml with only [research] triggers vis-lens ERROR.""" - import autoskillit.recipe # noqa: F401 -- triggers rule registration base_recipe = load_recipe(builtin_recipes_dir() / "research-design.yaml") recipe = replace(base_recipe, requires_packs=["research"]) @@ -232,8 +226,6 @@ def test_research_design_yaml_triggers_error_without_vis_lens(self): class TestUndeclaredPackRequirementToolSteps: def _run_rule(self, recipe: Recipe) -> list: - import autoskillit.recipe # noqa: F401 - return [f for f in run_semantic_rules(recipe) if f.rule == "undeclared-pack-requirement"] def test_tool_needing_undeclared_enabled_pack_is_error(self): From bf5d4cc1c4d7f5e3c68834007ae153c7a47c8ac0 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 17:42:10 -0700 Subject: [PATCH 12/13] fix(review): share food-truck pack visibility across validation and runtime --- src/autoskillit/core/__init__.pyi | 1 + .../types/constants/_type_constants_registries.py | 15 ++++++++++++++- src/autoskillit/recipe/rules/rules_packs.py | 10 +++++----- src/autoskillit/server/lifecycle/_session_type.py | 9 +++++---- .../test_type_constants_tool_classification.py | 14 ++++++++++++++ 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/autoskillit/core/__init__.pyi b/src/autoskillit/core/__init__.pyi index 84fa451cfa..ddb1d779e7 100644 --- a/src/autoskillit/core/__init__.pyi +++ b/src/autoskillit/core/__init__.pyi @@ -1399,6 +1399,7 @@ from .types import extract_path_arg as extract_path_arg from .types import extract_positional_args as extract_positional_args from .types import extract_skill_name as extract_skill_name from .types import fleet_error as fleet_error +from .types import food_truck_visibility_tags as food_truck_visibility_tags from .types import group_by_pair as group_by_pair from .types import hookdef_session_scope as hookdef_session_scope from .types import is_canonical_plugin_artifact_digest as is_canonical_plugin_artifact_digest diff --git a/src/autoskillit/core/types/constants/_type_constants_registries.py b/src/autoskillit/core/types/constants/_type_constants_registries.py index a715ec060b..aeb39520f7 100644 --- a/src/autoskillit/core/types/constants/_type_constants_registries.py +++ b/src/autoskillit/core/types/constants/_type_constants_registries.py @@ -7,7 +7,7 @@ import hashlib import json -from collections.abc import Mapping +from collections.abc import Collection, Mapping from dataclasses import dataclass from types import MappingProxyType from typing import Literal, NamedTuple @@ -71,6 +71,7 @@ "FLEET_MENU_TOOLS", "EXPLORATION_TOOLS", "KITCHEN_GATED_TOOLS", + "food_truck_visibility_tags", "FLEET_ERROR_CODES", "EXPLORATION_FAILURE_CODES", "BROKER_AUTHORITY_STATUSES", @@ -617,6 +618,18 @@ class AgentPackDef(NamedTuple): GATED_TOOLS - FLEET_TOOLS - EXPLORATION_TOOLS - EVIDENCE_READER_TOOLS ) + +def food_truck_visibility_tags(packs: Collection[str] | None) -> frozenset[str]: + """Return tags enabled for a food-truck session's validated pack request. + + None means no request and exposes the kitchen surface. An explicit request, + even when no valid packs remain, exposes kitchen-core plus the supplied packs. + """ + if packs is None: + return frozenset({"kitchen"}) + return frozenset({"kitchen-core", *packs}) + + ALL_VISIBILITY_TAGS: frozenset[str] = frozenset( { "kitchen", diff --git a/src/autoskillit/recipe/rules/rules_packs.py b/src/autoskillit/recipe/rules/rules_packs.py index ccba056029..a820438e2e 100644 --- a/src/autoskillit/recipe/rules/rules_packs.py +++ b/src/autoskillit/recipe/rules/rules_packs.py @@ -10,6 +10,7 @@ SKILL_TOOLS, TOOL_SUBSET_TAGS, Severity, + food_truck_visibility_tags, ) from autoskillit.recipe._analysis import ValidationContext from autoskillit.recipe._skill_helpers import _get_skill_category_map @@ -44,13 +45,12 @@ def _check_unknown_required_pack(ctx: ValidationContext) -> list[RuleFinding]: def _food_truck_can_call(tool: str, declared_packs: frozenset[str]) -> bool: """Whether a food truck dispatched with *declared_packs* exposes *tool*. - Mirrors the headless-orchestrator branch of ``_apply_session_type_visibility``: - non-empty ``requires_packs`` enables ``kitchen-core`` plus each declared pack; - empty ``requires_packs`` falls back to the ``kitchen`` tag surface. + Uses the same ``food_truck_visibility_tags`` decision as the runtime dispatcher. """ - if not declared_packs: + visible_tags = food_truck_visibility_tags(declared_packs or None) + if "kitchen" in visible_tags: return tool in KITCHEN_GATED_TOOLS | EXPLORATION_TOOLS - return bool(TOOL_SUBSET_TAGS[tool] & (declared_packs | {"kitchen-core"})) + return bool(TOOL_SUBSET_TAGS[tool] & visible_tags) @semantic_rule( diff --git a/src/autoskillit/server/lifecycle/_session_type.py b/src/autoskillit/server/lifecycle/_session_type.py index 47afee97ca..e53f0006c5 100644 --- a/src/autoskillit/server/lifecycle/_session_type.py +++ b/src/autoskillit/server/lifecycle/_session_type.py @@ -18,6 +18,7 @@ FOOD_TRUCK_TOOL_TAGS_ENV_VAR, HEADLESS_AUTO_GATE_ENV_VAR, SessionType, + food_truck_visibility_tags, get_logger, session_shape, ) @@ -99,8 +100,9 @@ def _apply_session_type_visibility() -> None: mcp.enable(tags={"fleet-dispatch"}) case SessionType.ORCHESTRATOR if _headless: tool_tags = os.environ.get(FOOD_TRUCK_TOOL_TAGS_ENV_VAR, "") + packs: set[str] | None = None if tool_tags: - mcp.enable(tags={"kitchen-core"}) + packs = set() for pack in tool_tags.split(","): pack = pack.strip() if not pack: @@ -113,9 +115,8 @@ def _apply_session_type_visibility() -> None: ", ".join(sorted(CATEGORY_TAGS)), ) continue - mcp.enable(tags={pack}) - else: - mcp.enable(tags={"kitchen"}) + packs.add(pack) + mcp.enable(tags=set(food_truck_visibility_tags(packs))) case SessionType.SKILL if _headless: mcp.enable(tags={"headless"}) if os.environ.get(HEADLESS_AUTO_GATE_ENV_VAR) == "1": diff --git a/tests/core/test_type_constants_tool_classification.py b/tests/core/test_type_constants_tool_classification.py index e70f9df33a..a5a505a8b5 100644 --- a/tests/core/test_type_constants_tool_classification.py +++ b/tests/core/test_type_constants_tool_classification.py @@ -7,6 +7,20 @@ pytestmark = [pytest.mark.layer("core"), pytest.mark.small] +@pytest.mark.parametrize( + ("packs", "expected"), + [ + (None, {"kitchen"}), + (set(), {"kitchen-core"}), + ({"github"}, {"kitchen-core", "github"}), + ], +) +def test_food_truck_visibility_tags(packs, expected) -> None: + from autoskillit.core import food_truck_visibility_tags + + assert food_truck_visibility_tags(packs) == expected + + def test_review_tool_kitchen_classification() -> None: from autoskillit.core import KITCHEN_GATED_TOOLS From 855cc980d06aac9f645c8704f10241a1a766c249 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sun, 4 Oct 2026 18:06:14 -0700 Subject: [PATCH 13/13] fix(review): preserve ordered visibility enables and contract coordinates --- src/autoskillit/core/__init__.pyi | 2 +- src/autoskillit/core/tool_registry.py | 11 ++++++++++- .../types/constants/_type_constants_registries.py | 14 +------------- src/autoskillit/recipe/rules/rules_packs.py | 2 +- src/autoskillit/server/lifecycle/_session_type.py | 14 ++++++-------- tests/_ambient_env_surface.py | 6 +++--- .../test_type_constants_tool_classification.py | 6 +++--- 7 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/autoskillit/core/__init__.pyi b/src/autoskillit/core/__init__.pyi index ddb1d779e7..74b1dbfbdd 100644 --- a/src/autoskillit/core/__init__.pyi +++ b/src/autoskillit/core/__init__.pyi @@ -418,6 +418,7 @@ from .tool_registry import TOOL_REGISTRY as TOOL_REGISTRY from .tool_registry import all_tool_names as all_tool_names from .tool_registry import build_parameter_forwarding_rules as build_parameter_forwarding_rules from .tool_registry import compute_tool_contract_identity as compute_tool_contract_identity +from .tool_registry import food_truck_visibility_tags as food_truck_visibility_tags from .tool_registry import get_tool_def as get_tool_def from .tool_registry import runtime_exempt_param_names as runtime_exempt_param_names from .tool_registry import unsupported_tool_params as unsupported_tool_params @@ -1399,7 +1400,6 @@ from .types import extract_path_arg as extract_path_arg from .types import extract_positional_args as extract_positional_args from .types import extract_skill_name as extract_skill_name from .types import fleet_error as fleet_error -from .types import food_truck_visibility_tags as food_truck_visibility_tags from .types import group_by_pair as group_by_pair from .types import hookdef_session_scope as hookdef_session_scope from .types import is_canonical_plugin_artifact_digest as is_canonical_plugin_artifact_digest diff --git a/src/autoskillit/core/tool_registry.py b/src/autoskillit/core/tool_registry.py index a268119750..6321752329 100644 --- a/src/autoskillit/core/tool_registry.py +++ b/src/autoskillit/core/tool_registry.py @@ -7,7 +7,7 @@ from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Collection, Mapping from types import MappingProxyType from ._tool_registry_builders import ( @@ -34,12 +34,21 @@ "build_parameter_forwarding_rules", "compute_tool_contract_identity", "get_tool_def", + "food_truck_visibility_tags", "runtime_exempt_param_names", "unsupported_tool_params", ] _TOOL_CONTRACT_IDENTITY_DOMAIN = "autoskillit:tool-contract:v1:sha256" + +def food_truck_visibility_tags(packs: Collection[str] | None) -> tuple[str, ...]: + """Return kitchen for None; otherwise enable kitchen-core before the supplied packs.""" + if packs is None: + return ("kitchen",) + return ("kitchen-core", *packs) + + # RecipeStep fallbacks for execution-tuning parameters left at their vacancy sentinel. # `_run_skill_prepare.py` keeps explicit branches because the sentinels differ by type. EXECUTION_TUNING_STEP_FIELDS: Mapping[str, str] = MappingProxyType( diff --git a/src/autoskillit/core/types/constants/_type_constants_registries.py b/src/autoskillit/core/types/constants/_type_constants_registries.py index aeb39520f7..763ff4f61c 100644 --- a/src/autoskillit/core/types/constants/_type_constants_registries.py +++ b/src/autoskillit/core/types/constants/_type_constants_registries.py @@ -7,7 +7,7 @@ import hashlib import json -from collections.abc import Collection, Mapping +from collections.abc import Mapping from dataclasses import dataclass from types import MappingProxyType from typing import Literal, NamedTuple @@ -71,7 +71,6 @@ "FLEET_MENU_TOOLS", "EXPLORATION_TOOLS", "KITCHEN_GATED_TOOLS", - "food_truck_visibility_tags", "FLEET_ERROR_CODES", "EXPLORATION_FAILURE_CODES", "BROKER_AUTHORITY_STATUSES", @@ -619,17 +618,6 @@ class AgentPackDef(NamedTuple): ) -def food_truck_visibility_tags(packs: Collection[str] | None) -> frozenset[str]: - """Return tags enabled for a food-truck session's validated pack request. - - None means no request and exposes the kitchen surface. An explicit request, - even when no valid packs remain, exposes kitchen-core plus the supplied packs. - """ - if packs is None: - return frozenset({"kitchen"}) - return frozenset({"kitchen-core", *packs}) - - ALL_VISIBILITY_TAGS: frozenset[str] = frozenset( { "kitchen", diff --git a/src/autoskillit/recipe/rules/rules_packs.py b/src/autoskillit/recipe/rules/rules_packs.py index a820438e2e..3141f717a9 100644 --- a/src/autoskillit/recipe/rules/rules_packs.py +++ b/src/autoskillit/recipe/rules/rules_packs.py @@ -47,7 +47,7 @@ def _food_truck_can_call(tool: str, declared_packs: frozenset[str]) -> bool: Uses the same ``food_truck_visibility_tags`` decision as the runtime dispatcher. """ - visible_tags = food_truck_visibility_tags(declared_packs or None) + visible_tags = frozenset(food_truck_visibility_tags(declared_packs or None)) if "kitchen" in visible_tags: return tool in KITCHEN_GATED_TOOLS | EXPLORATION_TOOLS return bool(TOOL_SUBSET_TAGS[tool] & visible_tags) diff --git a/src/autoskillit/server/lifecycle/_session_type.py b/src/autoskillit/server/lifecycle/_session_type.py index e53f0006c5..fc345c5cf0 100644 --- a/src/autoskillit/server/lifecycle/_session_type.py +++ b/src/autoskillit/server/lifecycle/_session_type.py @@ -100,13 +100,10 @@ def _apply_session_type_visibility() -> None: mcp.enable(tags={"fleet-dispatch"}) case SessionType.ORCHESTRATOR if _headless: tool_tags = os.environ.get(FOOD_TRUCK_TOOL_TAGS_ENV_VAR, "") - packs: set[str] | None = None + packs: list[str] | None = None if tool_tags: - packs = set() - for pack in tool_tags.split(","): - pack = pack.strip() - if not pack: - continue + packs = [] + for pack in filter(None, map(str.strip, tool_tags.split(","))): if pack not in CATEGORY_TAGS: logger.warning( "Unknown pack %r in AUTOSKILLIT_FOOD_TRUCK_TOOL_TAGS" @@ -115,8 +112,9 @@ def _apply_session_type_visibility() -> None: ", ".join(sorted(CATEGORY_TAGS)), ) continue - packs.add(pack) - mcp.enable(tags=set(food_truck_visibility_tags(packs))) + packs.append(pack) + for tag in food_truck_visibility_tags(packs): + mcp.enable(tags={tag}) case SessionType.SKILL if _headless: mcp.enable(tags={"headless"}) if os.environ.get(HEADLESS_AUTO_GATE_ENV_VAR) == "1": diff --git a/tests/_ambient_env_surface.py b/tests/_ambient_env_surface.py index 1be1b652b6..423d8aa6f9 100644 --- a/tests/_ambient_env_surface.py +++ b/tests/_ambient_env_surface.py @@ -965,15 +965,15 @@ class AmbientEnvDisposition: "Dict-comprehension key bound by `for name in EVIDENCE_READER_ENV_FORWARD_VARS`; the three" "forwarded names are already captured directly via that collection's own R4 scan." ), - "server/lifecycle/_session_type.py:48": ( + "server/lifecycle/_session_type.py:49": ( "Set-comprehension key bound by `for key in EVIDENCE_READER_ENV_FORWARD_VARS`; the" "forwarded names are already captured directly via that collection's own R4 scan." ), - "server/lifecycle/_session_type.py:53": ( + "server/lifecycle/_session_type.py:54": ( "Generator-expression key bound by `for key in EVIDENCE_READER_ENV_FORWARD_VARS`; the" "forwarded names are already captured directly via that collection's own R4 scan." ), - "server/lifecycle/_session_type.py:60": ( + "server/lifecycle/_session_type.py:61": ( "Dict-comprehension key bound by `for key in _EXPLORER_BINDING_ENV_KEYS`; the four" "explorer-binding names are already captured directly via that collection's own R4 scan." ), diff --git a/tests/core/test_type_constants_tool_classification.py b/tests/core/test_type_constants_tool_classification.py index a5a505a8b5..98d9e22495 100644 --- a/tests/core/test_type_constants_tool_classification.py +++ b/tests/core/test_type_constants_tool_classification.py @@ -10,9 +10,9 @@ @pytest.mark.parametrize( ("packs", "expected"), [ - (None, {"kitchen"}), - (set(), {"kitchen-core"}), - ({"github"}, {"kitchen-core", "github"}), + (None, ("kitchen",)), + ([], ("kitchen-core",)), + (["github"], ("kitchen-core", "github")), ], ) def test_food_truck_visibility_tags(packs, expected) -> None: