Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

AutoSkillit is a Claude Code plugin that runs YAML recipes through a
multi-level orchestrator. The bundled recipes implement issue → plan → worktree
→ tests → PR → merge pipelines using 58 MCP tools and 142 bundled skills.
→ tests → PR → merge pipelines using 59 MCP tools and 142 bundled skills.

## Start here

Expand Down
4 changes: 2 additions & 2 deletions docs/execution/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ How AutoSkillit runs a recipe end to end: orchestrator, kitchen gating, clone an

## Overview

AutoSkillit is a Claude Code plugin that orchestrates automated workflows using headless sessions. It provides 58 MCP tools and 142 bundled skills, organized into a gated visibility system.
AutoSkillit is a Claude Code plugin that orchestrates automated workflows using headless sessions. It provides 59 MCP tools and 142 bundled skills, organized into a gated visibility system.

## Core Concepts

Expand Down Expand Up @@ -68,7 +68,7 @@ AutoSkillit supports four session modes with different tool and skill visibility
`$ claude`); `/open-kitchen` reveals kitchen tools.

- **`$ autoskillit order`**: Pipeline orchestrator session. Kitchen is pre-opened at startup —
all 58 MCP tools are available immediately. All skill tiers are accessible. The orchestrator
all 59 MCP tools are available immediately. All skill tiers are accessible. The orchestrator
delegates work through `run_skill` (headless sessions) and `run_cmd` (shell commands).

- **`run_skill` (headless)**: Worker sessions launched by the orchestrator. Sees 4 Free Range
Expand Down
4 changes: 2 additions & 2 deletions docs/execution/tool-access.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MCP Tool Access Control

AutoSkillit provides 58 MCP tools organized into three access levels that control which
AutoSkillit provides 59 MCP tools organized into three access levels that control which
session types can see each tool.

## Three Access Levels
Expand Down Expand Up @@ -86,7 +86,7 @@ missing kitchen visibility.

## Complete MCP Tool Access Control Map

All 58 tools with their access level, tags, source file, and functional category.
All 59 tools with their access level, tags, source file, and functional category.

**Tag abbreviations**: AS = `autoskillit`, K = `kitchen`, HL = `headless`,
GH = `github`, CI = `ci`, CL = `clone`, TL = `telemetry`, FL = `fleet`
Expand Down
2 changes: 1 addition & 1 deletion docs/skills/visibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ and `/autoskillit:close-kitchen`. Skills in `skills_extended/` are never seen.

Order is similar to cook: AutoSkillit launches Claude Code with access to all tiers.
The key difference is the orchestrator (`sous-chef` skill) is injected and the kitchen
is pre-opened so all 58 MCP tools are available from the start.
is pre-opened so all 59 MCP tools are available from the start.

### Headless session (launched by `run_skill`)

Expand Down
4 changes: 4 additions & 0 deletions src/autoskillit/cli/_prompts_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ def _build_orchestrator_prompt(
via CLI). Partial progress may exist on disk. Follow on_context_limit.
- If the progress signal is false OR the step has no on_context_limit: fall through
to on_failure — no recoverable progress evidence.
- When run_skill returns "needs_retry: true" AND "retry_reason: outcome_invariant":
- The skill's contract-declared outcome invariant was violated (e.g. accept_count > 0
but fix_failures > 0). This is a logical failure — the skill ran but its core function
failed. Always fall through to on_failure. Do NOT route to on_context_limit.
- When run_skill returns "needs_retry: true" AND "retry_reason: contract_recovery":
- The model ran to completion and wrote artifacts but the structured output tokens
failed pattern validation. Infrastructure nudge was attempted but could not recover.
Expand Down
1 change: 1 addition & 0 deletions src/autoskillit/config/ingredient_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"create_and_publish_branch",
"check_pr_mergeable",
"set_commit_status",
"commit_files",
),
),
("Recipes", ("migrate_recipe", "list_recipes", "load_recipe", "validate_recipe")),
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 @@ -362,6 +362,7 @@ from .types import SessionTelemetry as SessionTelemetry
from .types import SessionType as SessionType
from .types import Severity as Severity
from .types import SkillCapabilityDef as SkillCapabilityDef
from .types import SkillContractResolver as SkillContractResolver
from .types import SkillFamilyDef as SkillFamilyDef
from .types import SkillLister as SkillLister
from .types import SkillResolver as SkillResolver
Expand Down
7 changes: 6 additions & 1 deletion src/autoskillit/core/types/_type_constants_registries.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
}
)

HEADLESS_TOOLS: frozenset[str] = frozenset({"test_check", "unlock_agent_pack"})
HEADLESS_TOOLS: frozenset[str] = frozenset({"test_check", "unlock_agent_pack", "commit_files"})

FLEET_TOOLS: frozenset[str] = frozenset(
{
Expand Down Expand Up @@ -323,6 +323,7 @@ class AgentPackDef(NamedTuple):
"reset_test_dir": frozenset({"kitchen-core"}),
"reset_workspace": frozenset({"kitchen-core"}),
"classify_fix": frozenset({"kitchen-core"}),
"commit_files": frozenset({"kitchen-core"}),
"list_recipes": frozenset({"kitchen-core", "fleet-dispatch"}),
"load_recipe": frozenset({"kitchen-core", "fleet-dispatch"}),
"validate_recipe": frozenset({"kitchen-core"}),
Expand Down Expand Up @@ -433,6 +434,10 @@ class HardCapabilityMismatch(NamedTuple):
codex_status="not-applicable",
worker_routable=True,
),
"commit_files": SkillCapabilityDef(
description="commit_files MCP tool — server-side git stage/commit",
codex_status="works-as-is",
),
"git_metadata_write": SkillCapabilityDef(
description=(
"Requires .git/ metadata write access (git commit, git rebase, "
Expand Down
3 changes: 3 additions & 0 deletions src/autoskillit/core/types/_type_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class RetryReason(StrEnum):
IDLE_STALL = "idle_stall" # stdout idle watchdog kill — session may have partial progress
RATE_LIMITED = "rate_limited" # transient HTTP 429 or rate-limit pattern — wait-and-retry
CANCELLED = "cancelled"
OUTCOME_INVARIANT = (
"outcome_invariant" # skill-emitted outcome fields violated a contract invariant
)


class InfraExitCategory(StrEnum):
Expand Down
3 changes: 2 additions & 1 deletion src/autoskillit/core/types/_type_protocols_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from collections.abc import Callable, Mapping, Sequence
from pathlib import Path
from typing import Protocol, runtime_checkable
from typing import Any, Protocol, runtime_checkable

from ._type_checkpoint import SessionCheckpoint # noqa: F401, TC001
from ._type_results import (
Expand Down Expand Up @@ -84,6 +84,7 @@ async def run(
network_access: bool = False,
closure_spec: ClosureAuthoritySpec | None = None,
closure_report_root: Path | None = None,
skill_contract: Any | None = None,
) -> SkillResult: ...

async def dispatch_food_truck(
Expand Down
9 changes: 9 additions & 0 deletions src/autoskillit/core/types/_type_protocols_recipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable

if TYPE_CHECKING:
from autoskillit.recipe._contracts_types import SkillContract
from autoskillit.recipe.schema import Recipe, RecipeInfo

from ._type_backend import BackendCapabilities
Expand All @@ -24,6 +25,7 @@
"MigrationService",
"DatabaseReader",
"ReadOnlyResolver",
"SkillContractResolver",
"ServeOverridesSnapshot",
]

Expand Down Expand Up @@ -131,3 +133,10 @@ class ReadOnlyResolver(Protocol):
"""Protocol for resolving whether a skill is read-only from skill contracts."""

def __call__(self, skill_command: str) -> bool: ...


@runtime_checkable
class SkillContractResolver(Protocol):
"""Protocol for resolving a skill's full contract from skill contracts."""

def __call__(self, skill_command: str) -> SkillContract | None: ...
21 changes: 21 additions & 0 deletions src/autoskillit/core/types/_type_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"ValidatedAddDir",
"ValidatedWorktreePath",
"VALID_INPUT_SPEC_TYPES",
"OutcomeInvariantSpec",
"WriteBehaviorSpec",
"WriteEvidence",
"FailureRecord",
Expand Down Expand Up @@ -146,6 +147,20 @@ def is_dir(self) -> bool:
return Path(self.path).is_dir()


@dataclass(frozen=True, slots=True)
class OutcomeInvariantSpec:
"""Outcome invariant evaluated against skill-emitted output fields.

when: predicate expression, e.g. "accept_count > 0"
require: requirement expression, e.g. "fix_failures == 0"
Both use grammar: <declared_int_field> <op> <int_literal>
with ops: >, >=, ==, !=, <=, <
"""

when: str
require: str


@dataclass(frozen=True, slots=True)
class WriteBehaviorSpec:
"""Write-expectation metadata resolved from skill contracts.
Expand Down Expand Up @@ -550,6 +565,9 @@ class SkillResult:
ndjson_drift: NdjsonDriftOutcome = field(default_factory=NdjsonDriftOutcome)
"""NDJSON parser vocabulary drift counters — populated by Codex sessions."""
completion_required: bool = False
outcome_fields: dict[str, int | str] | None = None
outcome_invariant_violated: bool = False
outcome_qualifier: str | None = None

def to_json(self) -> str:
data: dict[str, Any] = {
Expand Down Expand Up @@ -828,4 +846,7 @@ class SessionIndexEntry(TypedDict):
api_retry_last_status: int | None
ndjson_unknown_event_count: int
ndjson_unknown_item_count: int
outcome_fields: dict[str, int | str] | None
outcome_invariant_violated: bool
outcome_qualifier: str | None
schema_version: int
1 change: 1 addition & 0 deletions src/autoskillit/execution/headless/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Headless Claude session orchestration — command prep, subprocess invocation, r
| `_headless_recovery.py` | Session recovery: `_recover_from_separate_marker`, `_synthesize_from_write_artifacts` |
| `_headless_result.py` | `SkillResult` construction: `_build_skill_result` (evidence/telemetry moved to `_headless_evidence.py`) |
| `_headless_evidence.py` | Evidence computation (`_compute_write_evidence`, `_adapt_agent_result`), audit recording (`_capture_failure`, `_apply_budget_guard`), telemetry builders (`_build_session_telemetry`, `_build_error_path_telemetry`) |
| `_headless_outcome.py` | Contract-field parser and outcome invariant evaluator for post-session adjudication |
| `_headless_scan.py` | `_scan_jsonl_write_paths()` — scans stdout JSONL for Write/Edit/Bash tool calls; uses `core/bash_write_targets` for precise write-target extraction |

## Architecture Notes
Expand Down
5 changes: 5 additions & 0 deletions src/autoskillit/execution/headless/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@

if TYPE_CHECKING:
from autoskillit.pipeline.context import ToolContext
from autoskillit.recipe._contracts_types import SkillContract

__all__ = [
"DefaultHeadlessExecutor",
Expand Down Expand Up @@ -148,6 +149,7 @@ async def run_headless_core(
network_access: bool = False,
closure_spec: ClosureAuthoritySpec | None = None,
closure_report_root: Path | None = None,
skill_contract: SkillContract | None = None,
) -> SkillResult:
"""Shared headless runner used by run_skill.

Expand Down Expand Up @@ -263,6 +265,7 @@ async def run_headless_core(
backend_override_source=backend_override_source,
closure_spec=closure_spec,
closure_report_root=closure_report_root,
skill_contract=skill_contract,
)


Expand Down Expand Up @@ -314,6 +317,7 @@ async def run(
network_access: bool = False,
closure_spec: ClosureAuthoritySpec | None = None,
closure_report_root: Path | None = None,
skill_contract: SkillContract | None = None,
) -> SkillResult:
cfg = self._ctx.config.run_skill
effective_timeout = timeout if timeout is not None else cfg.timeout
Expand Down Expand Up @@ -359,6 +363,7 @@ async def run(
network_access=network_access,
closure_spec=closure_spec,
closure_report_root=closure_report_root,
skill_contract=skill_contract,
)

async def dispatch_food_truck(
Expand Down
6 changes: 6 additions & 0 deletions src/autoskillit/execution/headless/_headless_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
if TYPE_CHECKING:
from autoskillit.core import SubprocessResult
from autoskillit.pipeline.context import ToolContext
from autoskillit.recipe._contracts_types import SkillContract

logger = get_logger(__name__)

Expand Down Expand Up @@ -116,6 +117,7 @@ async def _execute_claude_headless(
on_session_id_resolved: Callable[[str], None] | None = None,
closure_spec: ClosureAuthoritySpec | None = None,
closure_report_root: Path | None = None,
skill_contract: SkillContract | None = None,
) -> SkillResult:
"""Shared subprocess execution for headless Claude sessions.

Expand Down Expand Up @@ -444,6 +446,7 @@ async def _execute_claude_headless(
readonly_skill=_readonly_skill,
closure_spec=closure_spec,
closure_report_root=closure_report_root,
skill_contract=skill_contract,
)

if (
Expand Down Expand Up @@ -588,6 +591,9 @@ async def _execute_claude_headless(
backend=cast(Literal["claude-code", "codex"], _step_backend.name),
channel_b_capable=_step_backend.capabilities.channel_b_capable,
backend_override_source=backend_override_source,
outcome_fields=skill_result.outcome_fields,
outcome_invariant_violated=skill_result.outcome_invariant_violated,
outcome_qualifier=skill_result.outcome_qualifier,
)
except Exception:
logger.debug("session_log_flush_failed", exc_info=True)
Expand Down
Loading
Loading