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`) | diff --git a/src/autoskillit/core/__init__.pyi b/src/autoskillit/core/__init__.pyi index 84fa451cfa..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 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 a715ec060b..763ff4f61c 100644 --- a/src/autoskillit/core/types/constants/_type_constants_registries.py +++ b/src/autoskillit/core/types/constants/_type_constants_registries.py @@ -617,6 +617,7 @@ class AgentPackDef(NamedTuple): GATED_TOOLS - FLEET_TOOLS - EXPLORATION_TOOLS - EVIDENCE_READER_TOOLS ) + 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 cf9bd608a1..3141f717a9 100644 --- a/src/autoskillit/recipe/rules/rules_packs.py +++ b/src/autoskillit/recipe/rules/rules_packs.py @@ -2,7 +2,16 @@ 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, + food_truck_visibility_tags, +) 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 +42,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*. + + Uses the same ``food_truck_visibility_tags`` decision as the runtime dispatcher. + """ + 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) + + @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 +84,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) 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 ' diff --git a/src/autoskillit/server/lifecycle/_session_type.py b/src/autoskillit/server/lifecycle/_session_type.py index 47afee97ca..fc345c5cf0 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,12 +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: list[str] | None = None if tool_tags: - mcp.enable(tags={"kitchen-core"}) - 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" @@ -113,9 +112,9 @@ def _apply_session_type_visibility() -> None: ", ".join(sorted(CATEGORY_TAGS)), ) continue - mcp.enable(tags={pack}) - else: - mcp.enable(tags={"kitchen"}) + 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 96f56e1e53..98d9e22495 100644 --- a/tests/core/test_type_constants_tool_classification.py +++ b/tests/core/test_type_constants_tool_classification.py @@ -7,6 +7,27 @@ pytestmark = [pytest.mark.layer("core"), pytest.mark.small] +@pytest.mark.parametrize( + ("packs", "expected"), + [ + (None, ("kitchen",)), + ([], ("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 + + 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_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/recipe/test_rules_packs.py b/tests/recipe/test_rules_packs.py index fef4bd6c8c..ed2b05a505 100644 --- a/tests/recipe/test_rules_packs.py +++ b/tests/recipe/test_rules_packs.py @@ -6,7 +6,8 @@ import pytest -from autoskillit.core import Severity +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 from autoskillit.recipe.schema import Recipe, RecipeStep @@ -43,6 +44,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 # ---------------------------------------------------------------------- @@ -50,7 +63,6 @@ def _make_recipe_with_run_skill( 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"] @@ -61,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"] @@ -70,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"] @@ -80,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"] @@ -89,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())) @@ -118,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 @@ -205,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"]) @@ -216,3 +222,75 @@ 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: + 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): + 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) + + def test_empty_requires_packs_kitchen_gated_tool_passes(self): + 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) 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", } diff --git a/tests/server/test_session_type_visibility_orchestrator.py b/tests/server/test_session_type_visibility_orchestrator.py index e071ff4d9c..fb3f90d551 100644 --- a/tests/server/test_session_type_visibility_orchestrator.py +++ b/tests/server/test_session_type_visibility_orchestrator.py @@ -177,6 +177,8 @@ 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 assert tool_names.isdisjoint(EVIDENCE_READER_TOOLS) 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"