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/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 61 MCP tools and 142 bundled skills.
→ tests → PR → merge pipelines using 62 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 61 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 62 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 61 MCP tools are available immediately. All skill tiers are accessible. The orchestrator
all 62 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 61 MCP tools organized into three access levels that control which
AutoSkillit provides 62 MCP tools organized into three access levels that control which
session types can see each tool.

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

## Complete MCP Tool Access Control Map

All 61 tools with their access level, tags, source file, and functional category.
All 62 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
1 change: 1 addition & 0 deletions src/autoskillit/config/ingredient_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"validate_recipe",
"get_recipe_section",
"complete_recipe_initialization",
"write_audit_cycle_artifact",
),
),
("Agents", ("unlock_agent_pack",)),
Expand Down
4 changes: 4 additions & 0 deletions src/autoskillit/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"_collect_disabled_feature_tags",
"_AUTOSKILLIT_GITIGNORE_ENTRIES",
"_COMMITTED_BY_DESIGN",
"_MAX_ASSOCIATION_FILES",
"_MAX_REFERENCED_ARTIFACTS_PER_CALL",
"_PLAN_ASSOCIATION_DOMAIN",
"_PLAN_ASSOCIATION_KEYS",
}
)
__all__ = [n for n in __all__ if n not in _PRIVATE_REEXPORTS]
Expand Down
4 changes: 4 additions & 0 deletions src/autoskillit/core/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ from .tool_sequence_analysis import (
from .tool_sequence_analysis import render_adjacency_table as render_adjacency_table
from .tool_sequence_analysis import render_dot as render_dot
from .tool_sequence_analysis import render_mermaid as render_mermaid
from .types import _MAX_ASSOCIATION_FILES as _MAX_ASSOCIATION_FILES
from .types import _MAX_REFERENCED_ARTIFACTS_PER_CALL as _MAX_REFERENCED_ARTIFACTS_PER_CALL
from .types import _PLAN_ASSOCIATION_DOMAIN as _PLAN_ASSOCIATION_DOMAIN
from .types import _PLAN_ASSOCIATION_KEYS as _PLAN_ASSOCIATION_KEYS
from .types import ABSENT_BOUND_VALUE as ABSENT_BOUND_VALUE
from .types import ADMIRAL_DISPATCH_SECTIONS as ADMIRAL_DISPATCH_SECTIONS
from .types import AGENT_BACKEND_CLAUDE_CODE as AGENT_BACKEND_CLAUDE_CODE
Expand Down
43 changes: 33 additions & 10 deletions src/autoskillit/core/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
Zero autoskillit imports. Provides atomic filesystem writes, project temp directory
management, and YAML load/dump helpers.

All NEW on-disk JSON artifacts SHOULD use ``write_versioned_json`` so schema drift
is detectable. Existing artifacts are tracked in
``tests/infra/test_schema_version_convention.py`` (landed in a later phase).
New on-disk JSON artifacts fall into two families. Default to ``write_versioned_json``
so schema drift is detectable; existing sites are tracked in
``tests/infra/test_schema_version_convention.py``. Use ``write_canonical_versioned_json``
instead when the artifact's reader will call
``decode_versioned_json_bytes(..., require_canonical=True)`` for tamper-evident,
hash-bound content addressing — every such producer/consumer pairing must be
registered in ``tests/infra/test_canonical_json_producer_convention.py``.
"""

from __future__ import annotations
Expand Down Expand Up @@ -199,27 +203,42 @@ def atomic_write(
content: str,
*,
strict_durability: bool = False,
exclusive: bool = False,
) -> None:
"""Crash-safe write: write to a temp file then os.replace.

Includes data fsync and directory fsync for durability on ext4/xfs.
The directory fsync is skipped on Windows (no O_RDONLY semantics).

When ``exclusive`` is True, atomically claims ``path`` via
``os.O_CREAT | os.O_EXCL`` before writing, raising ``FileExistsError``
with no bytes written if the destination already exists. Closes the
TOCTOU window between a separate existence check and the write.
"""
import sys as _sys

path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
if exclusive:
os.close(os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY))
tmp: str | None = None
try:
fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(content)
f.flush()
os.fsync(f.fileno()) # durable data write
os.replace(tmp, path)
except Exception:
try:
os.unlink(tmp)
except OSError:
pass
if tmp is not None:
try:
os.unlink(tmp)
except OSError:
pass
if exclusive:
try:
os.unlink(path)
except OSError:
pass
raise
# Durable rename: fsync the parent directory on POSIX.
# Default callers retain best-effort parent durability. Identity-bearing
Expand Down Expand Up @@ -390,15 +409,19 @@ def write_versioned_json(


def write_canonical_versioned_json(
path: Path, payload: dict[str, Any], schema_version: int
path: Path,
payload: dict[str, Any],
schema_version: int,
*,
exclusive: bool = False,
) -> None:
"""Atomically write versioned canonical JSON for hash-bound artifacts."""
from .closure_hashing import canonical_json_bytes

if not isinstance(payload, dict):
raise TypeError("write_canonical_versioned_json requires a dict payload")
enriched = {**payload, "schema_version": schema_version}
atomic_write(path, canonical_json_bytes(enriched).decode("utf-8"))
atomic_write(path, canonical_json_bytes(enriched).decode("utf-8"), exclusive=exclusive)


def decode_versioned_json_bytes(
Expand Down
7 changes: 7 additions & 0 deletions src/autoskillit/core/tool_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"unlock_agent_pack",
"wait_for_ci",
"wait_for_merge_queue",
"write_audit_cycle_artifact",
"write_telemetry_files",
}
)
Expand Down Expand Up @@ -393,6 +394,12 @@ def _run_skill() -> ToolDef:
("paths", "message", "cwd", "step_name"),
required=("paths", "message", "cwd"),
),
_tool(
"write_audit_cycle_artifact",
("kind", "path", "fields", "cwd", "step_name"),
required=("kind", "path", "fields", "cwd"),
wire_types={"fields": ToolWireType.OBJECT},
),
_tool(
"fetch_github_issue",
("issue_url", "include_comments"),
Expand Down
26 changes: 26 additions & 0 deletions src/autoskillit/core/types/_type_audit_cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
"PlanDispositionReport",
"PlanDispositionRow",
"compute_findings_digest",
"_MAX_ASSOCIATION_FILES",
"_MAX_REFERENCED_ARTIFACTS_PER_CALL",
"_PLAN_ASSOCIATION_DOMAIN",
"_PLAN_ASSOCIATION_KEYS",
]

AUDIT_CYCLE_SCHEMA_VERSION = 1
Expand All @@ -36,6 +40,28 @@
_SATISFIED_RE = re.compile(r"^satisfied-by-round-([1-9][0-9]*)$")
_T = TypeVar("_T")

# Shared by the write-side write_audit_cycle_artifact MCP tool (server/tools/
# tools_audit_cycle.py) and the read-side _resolve_plan_disposition
# (recipe/_cmd_rpc_guards.py) — a single IL-0 definition so the two sides of
# the plan-association contract cannot drift into DUAL-COPY CONSTANTS.
_PLAN_ASSOCIATION_DOMAIN = "autoskillit:audit-cycle:plan-association:v1:sha256"
_PLAN_ASSOCIATION_KEYS = frozenset(
{
"schema_version",
"plan_ref",
"disposition_ref",
"parent_authority_digest",
"association_digest",
}
)
_MAX_ASSOCIATION_FILES = 256

# Bounds the number of referenced-artifact entries (audited_plan_refs,
# assessments, dispositions, etc.) accepted in a single write_audit_cycle_artifact
# request payload. Distinct from _MAX_ASSOCIATION_FILES, which bounds the count
# of association files already on disk under an associations/ directory glob.
_MAX_REFERENCED_ARTIFACTS_PER_CALL = 256


def _immutable_typed_tuple(
name: str,
Expand Down
13 changes: 12 additions & 1 deletion src/autoskillit/core/types/_type_constants_registries.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@
}
)

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

FLEET_TOOLS: frozenset[str] = frozenset(
{
Expand Down Expand Up @@ -743,6 +745,7 @@ class AgentPackDef(NamedTuple):
"reset_workspace": frozenset({"kitchen-core"}),
"classify_fix": frozenset({"kitchen-core"}),
"commit_files": frozenset({"kitchen-core"}),
"write_audit_cycle_artifact": 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 @@ -870,6 +873,14 @@ class HardCapabilityMismatch(NamedTuple):
codex_status="works-as-is",
allowed_execution_roles=_ALL_SKILL_EXECUTION_ROLES,
),
"write_audit_cycle_artifact": SkillCapabilityDef(
description=(
"write_audit_cycle_artifact MCP tool — server-side construction, digest "
"computation, and canonical write for hash-bound audit-cycle artifacts"
),
codex_status="works-as-is",
allowed_execution_roles=_ALL_SKILL_EXECUTION_ROLES,
),
"git_metadata_write": SkillCapabilityDef(
description=(
"Requires .git/ metadata write access (git commit, git rebase, "
Expand Down
16 changes: 3 additions & 13 deletions src/autoskillit/recipe/_cmd_rpc_guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import regex as re

from autoskillit.core import (
_MAX_ASSOCIATION_FILES,
_PLAN_ASSOCIATION_DOMAIN,
_PLAN_ASSOCIATION_KEYS,
AUDIT_CYCLE_SCHEMA_VERSION,
ArtifactRef,
AuditCycleVerifier,
Expand Down Expand Up @@ -214,19 +217,6 @@ def _normalize_plan_parts(plan_parts: str) -> list[str] | None:
return items


_PLAN_ASSOCIATION_DOMAIN = "autoskillit:audit-cycle:plan-association:v1:sha256"
_PLAN_ASSOCIATION_KEYS = frozenset(
{
"schema_version",
"plan_ref",
"disposition_ref",
"parent_authority_digest",
"association_digest",
}
)
_MAX_ASSOCIATION_FILES = 256


def _log_plan_disposition_rejection(
reason: str,
*,
Expand Down
4 changes: 2 additions & 2 deletions src/autoskillit/server/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Controls whether the tool succeeds when called (independent of visibility):
| Standard kitchen | `kitchen` | Yes | Yes (`_require_enabled`) | `run_cmd`, `run_skill`, `report_bug` |
| Fleet tool | `fleet`, `kitchen-core` | Yes (via `ALL_VISIBILITY_TAGS` loop) | Yes (`_require_fleet` or `_require_enabled`) | `dispatch_food_truck`, `record_gate_dispatch` |
| Fleet-dispatch tool | `fleet-dispatch` (± `kitchen-core`) | Yes (via `ALL_VISIBILITY_TAGS` loop) | Yes (`_require_enabled`) | `fetch_github_issue`, `list_recipes` |
| Headless-exempt | `kitchen`, `headless` | Yes | No | `test_check` |
| Headless-exempt | `kitchen`, `headless` | Yes | No | `test_check`, `commit_files`, `unlock_agent_pack`, `write_audit_cycle_artifact` |
| Free-range | _(none of the above)_ | No | No | `open_kitchen`, `close_kitchen` |

### Registry Constants
Expand All @@ -93,7 +93,7 @@ The canonical tool sets are in `core/types/_type_constants_registries.py`:

- `GATED_TOOLS` — all tools that call `_require_enabled()` (validated by arch test)
- `UNGATED_TOOLS` = `FREE_RANGE_TOOLS` — tools with no gating at all
- `HEADLESS_TOOLS` — `{"test_check"}` — kitchen-tagged but not application-gated
- `HEADLESS_TOOLS` — `{"test_check", "unlock_agent_pack", "commit_files", "write_audit_cycle_artifact"}` — kitchen-tagged but not application-gated
- `FLEET_TOOLS` — fleet-session-only tools
- `FLEET_DISPATCH_TOOLS` — fleet-dispatch-mode tools (hidden at startup, application-gated)
- `ALL_VISIBILITY_TAGS` — `{"kitchen", "headless", "fleet", "fleet-dispatch", "kitchen-core", "plan-review"}`
3 changes: 3 additions & 0 deletions src/autoskillit/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
from autoskillit.server.tools import ( # noqa: E402, F401
tools_agents as _tools_agents,
)
from autoskillit.server.tools import ( # noqa: E402, F401
tools_audit_cycle as _tools_audit_cycle,
)
from autoskillit.server.tools import ( # noqa: E402, F401
tools_ci as _tools_ci,
)
Expand Down
5 changes: 3 additions & 2 deletions src/autoskillit/server/tools/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# tools/

MCP `@mcp.tool()` handlers registered on import (20 tool modules).
MCP `@mcp.tool()` handlers registered on import (21 tool modules).

## Files

Expand Down Expand Up @@ -28,7 +28,8 @@ MCP `@mcp.tool()` handlers registered on import (20 tool modules).
| `tools_execution.py` | `run_cmd`, `run_python`, `run_skill` |
| `tools_fleet_dispatch.py` | `dispatch_food_truck`, `record_gate_dispatch` |
| `tools_fleet_reset.py` | `reset_dispatch` (full dispatch artifact cleanup) |
| `tools_git.py` | `merge_worktree`, `classify_fix`, `create_unique_branch`, `create_and_publish_branch`, `check_pr_mergeable` |
| `tools_git.py` | `merge_worktree`, `classify_fix`, `create_unique_branch`, `create_and_publish_branch`, `check_pr_mergeable`, `commit_files` |
| `tools_audit_cycle.py` | `write_audit_cycle_artifact` — server-side construction, digest computation, and canonical write for hash-bound audit-cycle artifacts (`authority`, `inventory`, `disposition_report`, `plan_association`) |
| `tools_github.py` | `fetch_github_issue`, `get_issue_title`, `report_bug` |
| `tools_issue_headless.py` | `prepare_issue`, `enrich_issues` (headless session tools) |
| `tools_issue_labels.py` | `claim_issue`, `release_issue` (GitHub label management) |
Expand Down
Loading
Loading