Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
e3b9b1b
fix: deliver complete recipe credentials and actionable attestation r…
Trecek Oct 14, 2026
df35a2f
fix: attach recipe checkpoint carriers from registry outcomes
Trecek Oct 14, 2026
a18501f
fix(review): centralize successful checkpoint outcome assertions
Trecek Oct 14, 2026
6fc6bdc
fix(review): share READY recipe overrides and implementation parameters
Trecek Oct 14, 2026
f4fc73a
fix(review): require validation of all three checkpoint carriers
Trecek Oct 14, 2026
de628d5
fix(review): identify unresolved skill inputs in plan-walk failures
Trecek Oct 14, 2026
dc3c94b
fix(review): share research fixture inputs outside conftest
Trecek Oct 14, 2026
3133ee0
fix(review): expose tracked delivery recipes through shared test support
Trecek Oct 14, 2026
347aac3
fix(review): verify recovery completion before reading its credential
Trecek Oct 14, 2026
0c901c1
fix(review): compare complete delivered skill-input shape values
Trecek Oct 14, 2026
e5eda7f
fix(review): cover every tracked recipe with attested invocations
Trecek Oct 14, 2026
d7f1031
docs(review): document completion response step identity
Trecek Oct 14, 2026
b7d382f
fix(review): assert the complete checkpoint carrier validation sequence
Trecek Oct 14, 2026
d0bae4b
fix(review): enable dependency packs in recipe credential test contexts
Trecek Oct 14, 2026
27ed20f
fix(review): require successful verification in the delivered plan walk
Trecek Oct 15, 2026
26671db
fix(review): exercise verification after review-approach plan binding
Trecek Oct 15, 2026
1c03fb5
refactor(review): share plan-set fixtures without importing a test mo…
Trecek Oct 15, 2026
d199795
refactor(review): centralize implementation review-approach test cases
Trecek Oct 15, 2026
47f31c5
test(review): generate a guaranteed different execution identifier
Trecek Oct 15, 2026
3a48d5f
test(review): require a successful recovered attested invocation
Trecek Oct 15, 2026
e25ed4b
test(review): require failure envelopes for attestation rejection
Trecek Oct 15, 2026
33d7144
test(review): cover successful responses from tools without recipe de…
Trecek Oct 15, 2026
5750a4b
test(review): verify success payload survives omitted checkpoint deli…
Trecek Oct 15, 2026
7096631
test(review): cover distinct registered checkpoint outcome values
Trecek Oct 15, 2026
0648444
test(review): apply the checkpoint conformance async marker once
Trecek Oct 15, 2026
c1a28f7
test(review): validate completion recovery routes and pull identity
Trecek Oct 15, 2026
791ccaf
test(review): validate recovery-only claim checkpoint carriers
Trecek Oct 15, 2026
a27fdb2
test(review): verify review receipt recovery retrieval metadata
Trecek Oct 15, 2026
33c9ca7
test(review): use one checkpoint delivery import in completion tests
Trecek Oct 15, 2026
339c615
refactor(review): share merge-state failure payload construction
Trecek Oct 15, 2026
007ebe7
refactor(review): share CI watcher failure payload construction
Trecek Oct 15, 2026
753718c
fix(review): assert recovered credential admission at the binding bou…
Trecek Oct 15, 2026
0acbd08
fix(review): retrieve pulled verification bodies in the plan walk
Trecek Oct 15, 2026
1e03c84
fix(review): bind typed runtime values with recovered protocol creden…
Trecek Oct 15, 2026
882a312
fix(review): permit advertised recipe pulls within the response-only …
Trecek Oct 15, 2026
75a3b35
fix(review): match research attestation step names to recipe keys
Trecek Oct 15, 2026
5e4b0fe
build(review): compile corrected research attestation step names
Trecek Oct 15, 2026
e81f935
test(review): track the relocated CI auto-trigger Git ref site
Trecek Oct 15, 2026
2a4f9a5
docs(review): refresh research diagram identity after attestation fixes
Trecek Oct 15, 2026
7ff8a18
fix(review): standardize pre-effect checkpoint failure envelopes
Trecek Oct 15, 2026
e9291da
refactor(review): clarify pull-only checkpoint carrier naming
Trecek Oct 15, 2026
bb8217a
refactor(review): remove duplicate run command response branch
Trecek Oct 15, 2026
17022e4
refactor(review): centralize research ready fixture inputs
Trecek Oct 15, 2026
1e985df
test(review): type the completion receipt helper context
Trecek Oct 15, 2026
946e683
test(review): share completion receipt request session identity
Trecek Oct 15, 2026
5d0c66a
test(review): assert canonical clone checkpoint denial envelope
Trecek Oct 15, 2026
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
9 changes: 6 additions & 3 deletions docs/decisions/0004-recipe-redelivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ snapshot, while the public `open_kitchen` response contains only the segment ove
initial bodies. Each mapped checkpoint tool verifies READY against that exact durable
generation before its effect and then returns `recipe_segment` with either authenticated
future bodies or an ordered manual `recipe_pull` closure. No raw YAML, LRU lookup, runtime
skip carrier, or separately persisted projection participates in recovery.
skip carrier, or separately persisted projection participates in recovery. Carriers are
attached by the shared response wrapper from each tool's registry outcome declaration.

Acknowledged `run_skill` receipts are replayable only for the same kitchen, request
session, and receipt identity. Their tracker effect remains one-shot and cached, so a
Expand Down Expand Up @@ -127,8 +128,10 @@ Four invariants are non-negotiable:
for envelope responses, `get_recipe_section`).
- Terminal-page delivery enforces the four invariants above while preserving the
separate completion call as a compatibility fallback for non-receipt responses.
- Segmented recipes recover from the latest `recipe_segment`; the startup credential is
intentionally scoped to delivered bodies rather than the full future recipe.
- Segmented recipes recover from the latest `recipe_segment`; the startup carrier's
credential is scoped to the first segment's bodies by the carrier byte cap, while the
completion receipt (invariant 4) always carries the complete credential and is
re-requestable while the recipe is READY.
- The delivery-reachability suite verifies `load_recipe` / `open_kitchen` /
`get_recipe_section` re-delivery restores pipeline execution capability, including
complete-envelope and bounded-envelope per-step pulls.
Expand Down
15 changes: 11 additions & 4 deletions src/autoskillit/cli/prompts/_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,19 @@ def render(self) -> str:
"recipe_delivery.payload_metadata when the body is attested); the same "
"forwarding rule applies. If any response contains recipe_segment, "
"consume it as mandatory authority: verify and install ordered bodies "
"for startup/success carriers, use the carrier-scoped invocation "
"digests, and reconstruct every recovery pull_closure entry through "
"for startup/success carriers and reconstruct every recovery "
"pull_closure entry through "
"the unchanged recipe_pull. The overview is only a table of contents. "
"If subtype=recipe_segment_post_effect_delivery_failure, the operation "
"already ran; do not repeat it. Do not assume startup contains "
"full-horizon invocation_template_digests. For structured child inputs, "
"already ran; do not repeat it. "
"The complete_recipe_initialization(initialization_id) receipt carries every "
"named step's recipe_execution credential. Use the delivered completion receipt "
"when one is already present, including a terminal-page embedded receipt; "
"otherwise call complete_recipe_initialization(initialization_id) before the "
"first run_skill. The receipt can be re-requested at any time while the recipe "
"is READY, and a digest rejection names that exact call. Absence of a top-level "
"recipe_execution block alone does not identify segmented delivery. "
"For structured child inputs, "
"select "
"recipe_execution.skill_input_shapes[step_name], initialize skill_inputs "
"with exactly its ordered keys, and replace available values in place. "
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 @@ -1205,6 +1205,7 @@ from .types import RecipeDeliveryBudgetError as RecipeDeliveryBudgetError
from .types import RecipeDeliveryDecision as RecipeDeliveryDecision
from .types import RecipeDeliveryEvidenceDef as RecipeDeliveryEvidenceDef
from .types import RecipeDeliveryMode as RecipeDeliveryMode
from .types import RecipeDeliveryOutcomeDef as RecipeDeliveryOutcomeDef
from .types import RecipeDeliveryRequest as RecipeDeliveryRequest
from .types import RecipeDeliverySurfaceDef as RecipeDeliverySurfaceDef
from .types import RecipeExecutionCredential as RecipeExecutionCredential
Expand Down
34 changes: 34 additions & 0 deletions src/autoskillit/core/_tool_registry_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from types import MappingProxyType

from .types.recipe._type_recipe_binding import (
RecipeDeliveryOutcomeDef,
ToolDef,
ToolInitializationOperation,
ToolParamDef,
Expand Down Expand Up @@ -139,6 +140,38 @@ def _initialization_operation(name: str) -> ToolInitializationOperation:
)


_RECIPE_DELIVERY_OUTCOMES: Mapping[str, RecipeDeliveryOutcomeDef] = MappingProxyType(
{
"check_repo_merge_state": RecipeDeliveryOutcomeDef(),
"enqueue_pr": RecipeDeliveryOutcomeDef(success_field="success"),
"wait_for_merge_queue": RecipeDeliveryOutcomeDef(success_field="success"),
"wait_for_ci": RecipeDeliveryOutcomeDef(
success_field="conclusion",
success_value="success",
),
"push_to_remote": RecipeDeliveryOutcomeDef(success_field="success"),
"bootstrap_clone": RecipeDeliveryOutcomeDef(),
"run_python": RecipeDeliveryOutcomeDef(success_field="success"),
"complete_run_skill_result": RecipeDeliveryOutcomeDef(
success_field="run_skill_success",
step_name_field="step_name",
),
"merge_worktree": RecipeDeliveryOutcomeDef(success_field="merge_succeeded"),
"check_pr_mergeable": RecipeDeliveryOutcomeDef(),
"test_check": RecipeDeliveryOutcomeDef(success_field="passed"),
"bind_plan_set": RecipeDeliveryOutcomeDef(success_field="success"),
"run_cmd": RecipeDeliveryOutcomeDef(success_field="success"),
"create_and_publish_branch": RecipeDeliveryOutcomeDef(),
"claim_and_resolve_issue": RecipeDeliveryOutcomeDef(success_field="claimed"),
"release_issue": RecipeDeliveryOutcomeDef(success_field="success"),
"verify_review_receipt": RecipeDeliveryOutcomeDef(
success_field="reviews_posted",
success_value="true",
),
}
)


def _tool(
name: str,
params: tuple[str, ...] = (),
Expand Down Expand Up @@ -171,6 +204,7 @@ def _tool(
initialization_operation=_initialization_operation(name),
automatic_recipe_delivery=automatic_recipe_delivery,
recovery_recipe_delivery=recovery_recipe_delivery,
recipe_delivery_outcome=_RECIPE_DELIVERY_OUTCOMES.get(name),
)


Expand Down
4 changes: 2 additions & 2 deletions src/autoskillit/core/types/constants/_type_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,8 @@ def _render_stop_step_evidence_doctrine(indent: str = "") -> str:
RECIPE_EXECUTION_ATTESTATION_MISSING_MESSAGE: str = (
"an active recipe requires recipe_execution_id and invocation_template_digest; "
"take both from the recipe_execution block of the complete_recipe_initialization "
"receipt (bounded delivery) or of the open_kitchen response (inline delivery), "
"using invocation_template_digests[step_name] for this step; structured calls must "
"receipt, which carries every named step's credential, using "
"invocation_template_digests[step_name] for this step; structured calls must "
"initialize skill_inputs from skill_input_shapes[step_name] ordered keys, replace "
"available values in place, copy only advertised absence_values by key presence "
'so "", 0, and False remain verbatim, and never delete or invent a key'
Expand Down
42 changes: 42 additions & 0 deletions src/autoskillit/core/types/recipe/_type_recipe_binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"RecipeFlowEdge",
"RUNTIME_ADMISSION_BY_ROLE",
"RuntimeAdmission",
"RecipeDeliveryOutcomeDef",
"ToolDef",
"ToolInitializationOperation",
"ToolParamDef",
Expand Down Expand Up @@ -161,6 +162,23 @@ class ToolParamDef:
role: ToolParamRole = field(kw_only=True)


@dataclass(frozen=True, slots=True)
class RecipeDeliveryOutcomeDef:
"""How the response wrapper reads a delivery tool's outcome from its response."""

success_field: str | None = None
success_value: bool | str = True
step_name_field: str | None = None

def succeeded(self, payload: Mapping[str, object]) -> bool:
if self.success_field is None:
return "error" not in payload and payload.get("success") is not False
actual = payload.get(self.success_field)
if isinstance(self.success_value, bool):
return actual is self.success_value
return actual == self.success_value


@dataclass(frozen=True, slots=True)
class ToolDef:
"""Static definition of one registered MCP tool."""
Expand All @@ -172,6 +190,7 @@ class ToolDef:
automatic_recipe_delivery: bool = False
# Failure edges may carry a recovery segment; this can be enabled independently.
recovery_recipe_delivery: bool = False
recipe_delivery_outcome: RecipeDeliveryOutcomeDef | None = None

def __post_init__(self) -> None:
if not isinstance(self.initialization_operation, ToolInitializationOperation):
Expand All @@ -195,6 +214,29 @@ def __post_init__(self) -> None:
)
if structured and self.name != "run_skill":
raise ValueError("Only run_skill may declare structured skill_inputs")
self._validate_recipe_delivery_outcome()

def _validate_recipe_delivery_outcome(self) -> None:
delivery_enabled = self.automatic_recipe_delivery or self.recovery_recipe_delivery
if not delivery_enabled:
if self.recipe_delivery_outcome is not None:
raise ValueError(
f"ToolDef {self.name!r} declares a recipe delivery outcome "
"without a delivery flag"
)
return
if self.recipe_delivery_outcome is None:
raise ValueError(
f"ToolDef {self.name!r} enables recipe delivery without an outcome declaration"
)
if (
self.recipe_delivery_outcome.step_name_field is None
and "step_name" not in self.param_set
):
raise ValueError(
f"ToolDef {self.name!r} uses argument-sourced recipe delivery "
"without a step_name parameter"
)

@property
def param_names(self) -> tuple[str, ...]:
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:7e7c9adec5fd90bd4086a03207b09949466e883cf04b24183b00f5484324fe85 -->
<!-- autoskillit-recipe-hash: sha256:7e595fc0971b6e4b1348beaee6fecadfc41a9c251dd74991e2f78660e7418727 -->
<!-- autoskillit-diagram-format: v7 -->

## research
Expand Down
8 changes: 4 additions & 4 deletions src/autoskillit/recipes/research.json
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@
"source_doc": "${{ context.experiment_plan }}"
},
"cwd": "${{ context.worktree_path }}",
"step_name": "decompose",
"step_name": "decompose_phases",
"output_dir": "{{AUTOSKILLIT_TEMP}}/make-groups"
},
"capture": {
Expand Down Expand Up @@ -1084,7 +1084,7 @@
"adjust": true
},
"cwd": "${{ context.worktree_path }}",
"step_name": "adjust",
"step_name": "adjust_experiment",
"output_dir": "."
},
"capture": {
Expand Down Expand Up @@ -1244,7 +1244,7 @@
"group_manifest": "${{ context.group_manifest }}"
},
"cwd": "${{ context.worktree_path }}",
"step_name": "generate_report",
"step_name": "generate_report_inconclusive",
"output_dir": "{{AUTOSKILLIT_TEMP}}/generate-report"
},
"capture": {
Expand Down Expand Up @@ -1276,7 +1276,7 @@
"worktree_path": "${{ context.worktree_path }}"
},
"cwd": "${{ inputs.source_dir }}",
"step_name": "fix",
"step_name": "fix_tests",
"output_dir": "."
},
"capture": {
Expand Down
8 changes: 4 additions & 4 deletions src/autoskillit/recipes/research.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ steps:
skill_inputs:
source_doc: ${{ context.experiment_plan }}
cwd: ${{ context.worktree_path }}
step_name: decompose
step_name: decompose_phases
output_dir: '{{AUTOSKILLIT_TEMP}}/make-groups'
capture:
group_files: ${{ result.group_files }}
Expand Down Expand Up @@ -930,7 +930,7 @@ steps:
worktree_path: ${{ context.worktree_path }}
adjust: true
cwd: ${{ context.worktree_path }}
step_name: adjust
step_name: adjust_experiment
output_dir: .
capture:
experiment_results: ${{ result.results_path }}
Expand Down Expand Up @@ -1060,7 +1060,7 @@ steps:
classification_timestamp: ${{ context.classification_timestamp }}
group_manifest: ${{ context.group_manifest }}
cwd: ${{ context.worktree_path }}
step_name: generate_report
step_name: generate_report_inconclusive
output_dir: '{{AUTOSKILLIT_TEMP}}/generate-report'
capture:
report_path: ${{ result.report_path }}
Expand Down Expand Up @@ -1089,7 +1089,7 @@ steps:
base_branch: ${{ inputs.base_branch }}
worktree_path: ${{ context.worktree_path }}
cwd: ${{ inputs.source_dir }}
step_name: fix
step_name: fix_tests
output_dir: .
capture:
verdict: ${{ result.verdict }}
Expand Down
52 changes: 35 additions & 17 deletions src/autoskillit/server/_notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
complete_section_response,
)
from autoskillit.server.recipe._recipe_section_pagination import resolve_recipe_section_bound_bytes
from autoskillit.server.recipe._recipe_segment_delivery import (
attach_checkpoint,
prepare_checkpoint,
)
from autoskillit.server.response._response_budget import (
bounded_response_budget_failure,
enforce_response_budget,
Expand All @@ -64,6 +68,31 @@ def _get_ctx_or_none() -> ToolContext | None:
return _ctx_none_fn()


async def _invoke_handler(
fn: Callable[..., Awaitable[Any]],
args: tuple[Any, ...],
kwargs: dict[str, Any],
tool_name: str,
) -> Any:
try:
return await fn(*args, **kwargs)
except Exception as exc:
result = json.dumps(
{
"success": False,
"error": f"{type(exc).__name__}: {exc}",
"exit_code": -1,
"subtype": "tool_exception",
"user_visible_message": (
f"An internal error occurred in {tool_name}: "
f"{type(exc).__name__}. Run 'autoskillit doctor' or reinstall."
),
}
)
logger.exception("track_response_size_handler_failed", tool_name=tool_name)
return result


def _resolve_backend_capabilities(ctx: ToolContext | None) -> BackendCapabilities | None:
"""Extract the resolved backend's capabilities from ``ctx``, if any."""
backend = getattr(ctx, "backend", None) if ctx is not None else None
Expand Down Expand Up @@ -126,22 +155,12 @@ async def get_token_summary(...) -> str:
def decorator(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
@functools.wraps(fn)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
try:
result = await fn(*args, **kwargs)
except Exception as exc:
result = json.dumps(
{
"success": False,
"error": f"{type(exc).__name__}: {exc}",
"exit_code": -1,
"subtype": "tool_exception",
"user_visible_message": (
f"An internal error occurred in {tool_name}: "
f"{type(exc).__name__}. Run 'autoskillit doctor' or reinstall."
),
}
)
logger.exception("track_response_size_handler_failed", tool_name=tool_name)
ctx = _get_ctx_or_none()
prepared, early = prepare_checkpoint(ctx, fn, args, kwargs)
result = (
early if early is not None else await _invoke_handler(fn, args, kwargs, tool_name)
)
result = attach_checkpoint(ctx, fn, result, prepared)
finalized = (
result
if isinstance(
Expand All @@ -159,7 +178,6 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any:
response_value = finalized.rendered
else:
response_value = result
ctx = _get_ctx_or_none()
try:
response_str = (
response_value
Expand Down
3 changes: 2 additions & 1 deletion src/autoskillit/server/recipe/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ Compiled-recipe generation and its persisted artifacts are owned by
`_recipe_generation` and `_recipe_artifact` respectively. Transactional
finalization lives in the `_recipe_delivery/` sub-package, with its own
decision-support helpers in `_recipe_delivery_helpers` and its segmented
startup/checkpoint carriers in `_recipe_segment_delivery`. Server-owned
startup/checkpoint carriers in `_recipe_segment_delivery`, which also owns checkpoint
attachment through `track_response_size`. Server-owned
execution and initialization commits are split across `_recipe_execution`
and `_recipe_initialization`. Section pagination is split between the
page-fitting engine (`_recipe_section_pagination`, `_recipe_section_planning`)
Expand Down
Loading
Loading