Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/recipes/authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) |
Expand Down
1 change: 1 addition & 0 deletions src/autoskillit/core/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion src/autoskillit/core/tool_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ class AgentPackDef(NamedTuple):
GATED_TOOLS - FLEET_TOOLS - EXPLORATION_TOOLS - EVIDENCE_READER_TOOLS
)


ALL_VISIBILITY_TAGS: frozenset[str] = frozenset(
{
"kitchen",
Expand Down
57 changes: 48 additions & 9 deletions src/autoskillit/recipe/rules/rules_packs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/autoskillit/recipes/diagrams/research.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!-- autoskillit-recipe-hash: sha256:ad4866c3b0af521bd5135a8f8e18608a7803fcbf2c6027a03d43362cf42f4d09 -->
<!-- autoskillit-recipe-hash: sha256:06ffb7df6927f014287376817b0f1e5061bb143b8d248a43a7eac981d3346450 -->
<!-- autoskillit-diagram-format: v7 -->

## research
Expand Down
3 changes: 2 additions & 1 deletion src/autoskillit/recipes/research-review.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/autoskillit/recipes/research-review.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion src/autoskillit/recipes/research.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/autoskillit/recipes/research.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ requires_packs:
- research
- exp-lens
- vis-lens
- github
description: 'End-to-end technical research: scope, implement, report

'
Expand Down
15 changes: 7 additions & 8 deletions src/autoskillit/server/lifecycle/_session_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
FOOD_TRUCK_TOOL_TAGS_ENV_VAR,
HEADLESS_AUTO_GATE_ENV_VAR,
SessionType,
food_truck_visibility_tags,
get_logger,
session_shape,
)
Expand Down Expand Up @@ -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"
Expand All @@ -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":
Expand Down
6 changes: 3 additions & 3 deletions tests/_ambient_env_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
),
Expand Down
21 changes: 21 additions & 0 deletions tests/core/test_type_constants_tool_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/recipe/test_io_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]


# ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion tests/recipe/test_research_review_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading