Rectify: Surface LoadRecipeResult Errors and Fail-Closed on Invalid Content#3995
Merged
Trecek merged 9 commits intoJun 11, 2026
Conversation
…ontent open_kitchen silently returned success=true with empty content when post-prune validation detected dangling routes or route-consistency errors. Three architectural weaknesses enabled this: 1. load_and_validate computed structural errors but discarded them at the LoadRecipeResult TypedDict boundary. 2. open_kitchen stamped success=true unconditionally, never checking valid or content. 3. _prune_skipped_steps produced unrepairable dangling routes silently when a pruned step had no computable redirect. This change surfaces errors through the TypedDict boundary, makes open_kitchen fail-closed on invalid/empty content, adds the errors field to fleet dispatch rejection messages, and adds regression tests for each layer of the data-loss path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Deferred recall mocks: add valid/content/errors fields to LoadRecipeResult - Recipe-not-found test: expect success=false (fail-closed behavior) - Project-dir test recipe: use kind=interactive + sentinel message to pass validation - Codex pruning test: xfail pending Part B (resolve_review when=None catch-all) - Cross-layer import: remove unnecessary workspace import from recipe test - JSON write allowlist: update shifted line numbers for tools_kitchen.py - Line limit: raise tools_kitchen.py exemption to 1150 for validity gate code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The workspace import (DefaultSkillResolver) was removed in the previous commit — update the cascade map to match. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rden tests The pre-fix YAML defined on_result blocks that enumerated specific verdict values (e.g. real_fix, already_green, flake_suspected, ci_only_failure) without a catch-all. When a step was pruned (skip_when_false ingredient set to false), the route-repair loop could not derive a valid redirect and left dangling references in surviving steps, which the dangling-route validator flagged and the pruning code treated as a silent content wipe. Add a `- route: <failure-target>` catch-all condition to every guarded step's on_result block in implementation, remediation, implementation-groups, merge-prs, research, and research-implement recipes. Also convert `review_pr`'s legacy `- when: "true"` string catch-all in three recipes to the canonical `- route:` form so the production redirect-derivation logic recognizes it. Test changes: - Remove the `continue` escape hatch in test_bundled_recipes_prune_produces_no_dangling_routes and assert no dangling routes for ALL pruning scenarios, not just those with computable redirects. - Add test_all_skip_guarded_steps_have_computable_redirect — a structural invariant that catches latent defects (like the research.yaml and research-implement.yaml fix_tests steps, and the merge-prs.yaml fix step) that would otherwise ship silently. - Extract _has_computable_redirect to module level for reuse across tests. - Add non-vacuousness guards (assert content is non-empty) in test_content_integrity.py and test_bundled_recipe_hidden_policy.py so empty-content code paths cannot pass vacuous assertions. - Remove the xfail marker on test_codex_overrides_remove_guarded_steps (the placeholder is no longer needed — the structural fix makes it pass). - Update test_load_and_validate_surfaces_errors_on_dangling_routes to reflect the post-fix invariant: pruning now succeeds without dangling route errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds server/test_backend_ingredient_injection.py back to the workspace frozenset in LAYER_CASCADE_CONSERVATIVE. TestRealCompositionPruning calls load_and_validate() which transitively imports DefaultSkillResolver from autoskillit.workspace at recipe/_api.py:366, requiring this cascade entry.
…nsitive overrides TestRealCompositionPruning calls load_and_validate() which transitively imports DefaultSkillResolver from autoskillit.workspace at recipe/_api.py:366. The cascade guard requires either a direct import or a transitive override entry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TestKitchenVisibility.test_tool_list_changes_after_enable_within_session
calls mcp.enable(tags={"kitchen"}) but the class lacked its own autouse
fixture — relying solely on the directory-level _reset_mcp_tags. Per
test AGENTS.md, classes that call enable/disable must have their own
clear+restore fixture. Adds defense-in-depth against xdist ordering
interactions that could leak accumulated transforms from other workers.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…plicate validation gates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
b62f26b to
299f57c
Compare
This was referenced Jun 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
open_kitchensilently returnscontent: ""withsuccess: truewhen post-prune validation detects dangling routes or route-consistency errors. The architectural weakness is threefold:load_and_validate()computes structural errors but discards them —LoadRecipeResulthas noerrorsfield, so the reason forvalid=Falseis lost at the TypedDict boundary.open_kitchenstampssuccess: trueunconditionally — it never checksresult["valid"]orresult["content"]before returning._prune_skipped_stepsproduces unrepairable dangling routes silently — when a pruned step has no computable redirect (noon_success, nowhen=Nonecatch-all inon_result), the route-repair loop is a no-op, and the only signal is a list of strings returned from_validate_no_dangling_routesthat gets appended to a local variable and then discarded.Part A addresses the core data-loss path: surfacing structural errors through
LoadRecipeResult, makingopen_kitchenfail-closed on invalid content, and adding tests that catch this class of bug. Part B will address the recipe YAML fixes (addingwhen=Nonecatch-all toresolve_review) and broader vacuous-assertion cleanup.Problem
When the orchestrator backend is Codex,
open_kitchen(name="implementation")returnssuccess: true,valid: false, andcontent: ""— the entire recipe body is silently withheld. The orchestrator receives no explanation for the empty content, has no prompt instruction for this state, and is structurally blocked from every recovery path (recipe_read_guard.pyPreToolUse hook + sous-chef raw-read prohibition +load_recipereturning the same empty result).In run
impl-20260610-095047-486925(TalonT-Org/the-token-reserve#147, PR TalonT-Org/the-token-reserve#189, AutoSkillit 0.10.745, 2026-06-10 09:49–11:31), the Codex orchestrator improvised the entire 60-minute pipeline from the Mermaiddiagramfield, the ingredients table, and the sous-chef prompt — executing steps the composition had pruned as backend-incompatible (e.g.implement→implement-worktree-no-merge, declaredrequires backend ['claude-code']), with no step routing, nocapture:contracts, no retry counts, and zerorecord_pipeline_stepcalls. The PR happened to merge green, but the run cannot be trusted to have followed the composed plan.Reproduced at repo HEAD
18f410893(0.10.748). This is the sixth manifestation of the step-pruning/content-divergence family.Root cause chain
207deda91(Implementation Plan: Backend-Conditional Routing — gate_backend_write Step #3960) introduced thebackend_supports_git_writeingredient and addedskip_when_false: inputs.backend_supports_git_writeto 7 steps inimplementation.yaml(and 7 inremediation.yaml) includingresolve_review— without givingresolve_reviewawhen=Nonecatch-all route. The bug became reachable <24h before the failing run._backend_capability_overrides()(src/autoskillit/server/tools/_auto_overrides.py:19-28) maps Codex'sgit_metadata_writable=Falsetobackend_supports_git_write="false", injected into session overrides atsrc/autoskillit/server/tools/tools_kitchen.py:540, promoted over user-supplied overrides via_promote_capability_keys(line 542), and passed toload_and_validate(lines 627-635)._prune_skipped_steps()(src/autoskillit/recipe/_recipe_composition.py:294-415(redirect derivation 352-364)) prunesresolve_reviewbut derivesredirect = None— the step has noon_successand all four of itson_resultconditions carrywhenclauses (the repair logic deliberately never derives a redirect fromon_failure/on_exhausted) (src/autoskillit/recipes/implementation.yaml:1089-1097). The survivingenrich_diff_context(on_success: resolve_review,on_failure: resolve_review,implementation.yaml:1064-1065) is left dangling. The code comment acknowledges this path: "redirect stays None and_validate_no_dangling_routescatches dangling refs."_validate_no_dangling_routesreturns["Step 'enrich_diff_context' routes to unknown step 'resolve_review'"]andload_and_validate()setsraw = ""(src/autoskillit/recipe/_api.py:415-418). Theerrorslist feeds onlycompute_recipe_validity();LoadRecipeResulthas noerrorsfield, so the reason never reaches the caller.open_kitchenreturnssuccess: true, kitchen: "open"without checkingvalid/content(tools_kitchen.py:690-692; the deferred-recall branch at 605-607 is equally unchecked). Neither sous-chef SKILL.md nor any orchestrator prompt coverscontent: "". The [FIX] Rectify: Backend-Specific Orchestrator Prompt Supplements — Codex Raw Recipe File Read Bypass — PART A #3736/Implementation Plan: Codex Sous-Chef Recipe Context Loss via Auto-Compaction #3947 defenses (raw-read prohibition,recipe_read_guard.py, compaction disable) eliminate self-recovery, converting the state into a trap.Reproduction (repo HEAD
18f410893, 0.10.748)Claude Code is unaffected because
git_metadata_writable=Trueproduces no pruning of these steps — full content is delivered. This is not Codex-side truncation: the run's rollout log records the fullopen_kitchenresults (90,635 and 86,672 bytes) with zero truncation markers;tool_output_token_limit=50000and compaction-disable were correctly written toconfig.toml.Remediation targets
errors: list[str]toLoadRecipeResultand populate it wheneverrawis cleared (_api.py:415-426).open_kitchen: whenvalid is Falseandcontent == "", return a failure envelope (success: false+ the dangling-route errors) — neversuccess: truewith no recipe.open_kitchenreturns emptycontent, halt and escalate; never execute from the diagram.resolve_reviewa computable redirect (when=Nonecatch-all) or guardenrich_diff_context's routes consistently so the pruned subgraph is removed atomically; audit the otherbackend_supports_git_write-guarded steps — 11 inimplementation.yamlat HEAD, added across207deda91,884e1f2ae,d88bda83a— and the other bundled recipes (remediation.yamlgained 7 guards in the same commit) for the same exposure.continueattests/recipe/test_hidden_ingredients.py:966-975with an assertion (and ideally a semantic validation rule) that every prunable step has a computable redirect; add a codex-composition test assertingvalid is TrueAND non-emptycontentAND all surviving steps present for each bundled recipe.--plugin-direquivalent;src/autoskillit/workspace/session_skills.py:319excludesBUNDLED), so the orchestrator could not map a post-hoc "run analyze health" request to/autoskillit:analyze-pipeline-health; consider listing invocable bundled skill commands in the Codex orchestrator prompt oropen_kitchenresult.Closes #3992
Implementation Plan
Plan file:
/home/talon/projects/autoskillit-runs/remediation-20260610-175022-808728/.autoskillit/temp/rectify/rectify_silent_recipe_content_wipe_part_a_2026-06-10_180512.md🤖 Generated with Claude Code via AutoSkillit
Token Usage Summary
* Step used a non-Anthropic provider; caching behavior may differ.
Token Efficiency
Model Usage Breakdown