Skip to content

[FIX] Dispatch-Feasibility Preflight — Claim-Before-Validate Immunity#4087

Merged
Trecek merged 11 commits into
developfrom
open-kitchen-dispatch-feasibility-preflight-fail-orders-befo/4083
Jun 12, 2026
Merged

[FIX] Dispatch-Feasibility Preflight — Claim-Before-Validate Immunity#4087
Trecek merged 11 commits into
developfrom
open-kitchen-dispatch-feasibility-preflight-fail-orders-befo/4083

Conversation

@Trecek

@Trecek Trecek commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add a dispatch-feasibility preflight in the server layer that fires at open_kitchen and dispatch_food_truck time — validating that the backend can dispatch the recipe's run_skill steps before any irreversible side effects (clone, branch push, issue labeling) occur. On preflight failure, the gate is closed, structurally denying all downstream side-effect tools via _require_enabled(). This fixes the "claim-before-validate" anti-pattern identified in production run impl-20260611-163753-197057 where the backend-compat check fired only inside run_skill, after claims and branch pushes had already happened.

Individual Group Plans

Plan 1: Rectify — Dispatch-Feasibility Preflight — Claim-Before-Validate Immunity

The backend-compatibility gate (_check_backend_compat) fires only inside run_skill — after irreversible external side effects (clone, branch push, issue labeling) have already occurred. The incompatibility is statically knowable at open_kitchen time from constants (HOOK_REGISTRY, backend.capabilities). The architectural weakness is that open_kitchen enables the gate and reveals tools before validating that the backend can dispatch the recipe's steps, and no existing test models the temporal ordering between gate-enable, side-effect tools, and the compat check.

The immunity plan addresses three layers:

  1. LoadRecipeResult extension — expose post-prune step names from the recipe layer so the server can inspect them without duplicating parse logic.
  2. Server-layer preflight — a _check_dispatch_feasibility function shared by open_kitchen and dispatch_food_truck, evaluated against post-prune steps and the real backend, with gate-close on failure.
  3. Temporal-ordering test infrastructure — tests that assert gate state after validation failure, test with the real HOOK_REGISTRY, and verify that side-effect tools are structurally denied after preflight failure.

Plan 2: Implementation — dispatch_food_truck Preflight Fix and Missing Tests

Fix the active_recipe_steps={} bug in dispatch_food_truck that renders the dispatch-feasibility preflight a no-op, and add four missing tests: gate-closure after validation failure (1a), full open_kitchen integration preflight (1b), real HOOK_REGISTRY test (1e), and behavioral fleet dispatch preflight (1g).

The root cause is that dispatch_food_truck calls _check_dispatch_feasibility with an empty dict for active_recipe_steps, unlike open_kitchen which properly loads the Recipe object via recipes.find() + recipes.load() and filters with filter_steps_by_post_prune(). The fix mirrors the open_kitchen pattern.

Closes #4083

Implementation Plan

Plan files:

  • /home/talon/projects/autoskillit-runs/remediation-20260611-215136-833713/.autoskillit/temp/rectify/rectify_dispatch_feasibility_preflight_2026-06-11_215136.md
  • /home/talon/projects/autoskillit-runs/remediation-20260611-215136-833713/.autoskillit/temp/make-plan/remediation_dispatch_feasibility_preflight_plan_2026-06-11_230600.md

🤖 Generated with Claude Code via AutoSkillit

Token Usage Summary

Step Model count uncached output cache_read peak_ctx turns cache_write time
resolve_review* sonnet 10 3.0k 136.6k 15.9M 109.9k 608 628.2k 1h 6m
dispatch:44d3705c-3f62-459b-af5b-9678f2bbcb64* sonnet 1 114 3.6k 1.2M 94.6k 28 94.9k 23m 9s
plan* opus[1m] 10 9.8k 210.3k 14.0M 136.8k 432 818.6k 2h 19m
verify* sonnet 10 4.1k 122.6k 4.7M 74.1k 255 427.3k 1h 2m
implement* MiniMax-M3 10 24.0M 134.7k 0 0 722 0 1h 11m
audit_impl* sonnet 10 7.0k 83.3k 2.2M 50.6k 160 310.9k 48m 30s
prepare_pr* MiniMax-M3 9 2.1M 20.7k 0 0 129 0 9m 6s
compose_pr* MiniMax-M3 9 1.7M 14.3k 0 0 115 0 8m 6s
review_pr* sonnet 14 2.1k 347.6k 13.6M 101.1k 618 770.8k 1h 26m
dispatch:0145c757-e69c-4878-929a-ba4acb07e3d1* sonnet 1 322 13.7k 3.0M 86.3k 84 62.0k 42m 39s
fix* sonnet 5 854 38.0k 5.9M 98.2k 276 254.5k 27m 19s
dispatch:341f411a-3d76-4fcf-88a3-15a64c6d7bd4* sonnet 1 354 10.0k 3.4M 86.4k 89 62.0k 1h 18m
dispatch:7e27c189-e656-4ec2-b528-b128157c59a8* sonnet 1 490 17.2k 5.0M 98.3k 122 73.9k 1h 26m
dispatch:36a96fbc-6c25-4316-a068-66f069a326ac* sonnet 1 386 14.1k 3.8M 89.9k 102 65.5k 1h 14m
dispatch:540912dc-04a8-4bda-b6dd-1ba4a533b68f* sonnet 1 418 12.6k 4.1M 89.9k 106 65.5k 1h 27m
dispatch:726982d8-8d44-4345-91ee-7c36d41bf8c0* sonnet 1 330 11.6k 3.1M 84.9k 82 60.5k 1h 10m
dispatch:c51e35aa-e1dc-42fd-91a6-8cadbf463f94* sonnet 1 418 12.6k 4.1M 90.7k 107 66.3k 1h 36m
dispatch:136d06d2-7313-45ac-b80d-f17a96a36af8* sonnet 1 322 10.0k 3.0M 83.9k 80 59.5k 1h 8m
dispatch:04f878e3-4291-4f0d-bae2-8cc73470181d* sonnet 1 468 164 4.1M 81.4k 58 201.6k 1h 0m
Total 27.9M 1.2M 91.1M 136.8k 4.0M 20h 8m

* Step used a non-Anthropic provider; caching behavior may differ.

Token Efficiency

Step LoC Changed cache_read/LoC cache_write/LoC output/LoC
resolve_review 100 159103.9 6282.2 1366.2
dispatch:44d3705c-3f62-459b-af5b-9678f2bbcb64 0
plan 0
verify 0
implement 1691 0.0 0.0 79.6
audit_impl 0
prepare_pr 0
compose_pr 0
review_pr 0
dispatch:0145c757-e69c-4878-929a-ba4acb07e3d1 360 8363.5 172.2 38.2
fix 29 202454.8 8775.7 1309.2
dispatch:341f411a-3d76-4fcf-88a3-15a64c6d7bd4 446 7548.5 139.0 22.5
dispatch:7e27c189-e656-4ec2-b528-b128157c59a8 235 21444.0 314.5 73.2
dispatch:36a96fbc-6c25-4316-a068-66f069a326ac 0
dispatch:540912dc-04a8-4bda-b6dd-1ba4a533b68f 0
dispatch:726982d8-8d44-4345-91ee-7c36d41bf8c0 0
dispatch:c51e35aa-e1dc-42fd-91a6-8cadbf463f94 844 4875.8 78.5 14.9
dispatch:136d06d2-7313-45ac-b80d-f17a96a36af8 0
dispatch:04f878e3-4291-4f0d-bae2-8cc73470181d 0
Total 3705 24594.1 1085.6 327.6

Model Usage Breakdown

Model steps uncached output cache_read cache_write time
sonnet 15 20.8k 833.7k 77.1M 3.2M 16h 20m
opus[1m] 1 9.8k 210.3k 14.0M 818.6k 2h 19m
MiniMax-M3 3 27.9M 169.7k 0 0 1h 28m

Trecek and others added 11 commits June 12, 2026 00:47
…ide effects

The backend-compat check fired only inside run_skill, after side-effect
tools (bootstrap_clone, claim_and_resolve_issue, create_and_publish_branch)
could already execute. This moves the check to open_kitchen and
dispatch_food_truck so the gate is closed before any external state mutates.

Changes:
- Extend LoadRecipeResult with post_prune_step_names so the server can
  inspect surviving step names without duplicating parse logic
- Add _check_dispatch_feasibility in tools_execution.py: shared function
  evaluating post-prune run_skill steps against backend capabilities
- Wire preflight into open_kitchen (both normal and deferred-recall paths)
  with gate.close() and ctx.disable_components on failure
- Wire preflight into dispatch_food_truck before execute_dispatch
- Close gate on recipe validation failure on both open_kitchen paths
- Filter active_recipe_steps to post-prune names on both paths
- Suppress post_prune_step_names in formatter registries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move _check_dispatch_feasibility, _get_fix_required_hook_matchers, and
filter_steps_by_post_prune into server/tools/_preflight.py to satisfy
line-count and business-logic-in-handler arch constraints. Fix test
issues: BackendCapabilities constructor, missing post_prune_step_names
in mocks, MagicMock config_providers crash, missing pytest import,
cascade map entry, and HOOK_REGISTRY monkeypatch targets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ity check

The dispatch-feasibility preflight in dispatch_food_truck was called with
active_recipe_steps={}, making it a no-op. This commit mirrors the
open_kitchen pattern: after load_and_validate, load the Recipe via
recipes.find() + recipes.load() and build active_recipe_steps via
filter_steps_by_post_prune() before calling _check_dispatch_feasibility.

Also adds four missing tests:
- gate-closure after recipe validation failure (1a)
- full open_kitchen integration preflight blocks and closes gate (1b)
- real HOOK_REGISTRY test without monkeypatching (1e)
- behavioral fleet dispatch preflight: execute_dispatch not called on failure (1g)

And fixes the stale docstring path reference in test_tools_kitchen_preflight.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- TestPreflightGateClosure tests failed because open_kitchen hit the
  _require_orchestrator_exact headless guard and returned early without
  reaching the recipe validation or preflight paths. Added the same
  _require_orchestrator_exact patch (return_value=None) used by other
  open_kitchen tests (e.g. test_mcp_overrides.py).

- test_tools_fleet_dispatch_preflight.py imports autoskillit.hook_registry
  but was missing from LAYER_CASCADE_CONSERVATIVE["hook_registry"] file-level
  entries, causing test_cascade_map_guard to flag it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_apply_triage_gate awaits ctx.recipes.apply_triage_gate() which is a
MagicMock (not awaitable), causing a TypeError that returns early via
the _kitchen_failure_envelope path — never reaching gate.disable().

Patch _apply_triage_gate as a passthrough coroutine to let both tests
reach their target code paths (recipe validation and preflight).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tch text

`applicable_guards` was a Python variable name leaking into the
user-facing escape_hatch field of the dispatch feasibility error
envelope. Replaced with plain English.
The hardcoded `recipe_name = ""` placeholder caused all recipe-scoped
and step-scoped provider overrides in `_resolve_provider_profile` to
be silently skipped, producing incorrect feasibility verdicts for
recipes with configured provider overrides. Thread the actual recipe
name from all three call sites.
…hen pattern

Use None sentinel for _active_recipe_steps (matching open_kitchen's
active_recipe_steps = None on load failure) and guard the preflight
call with `_active_recipe_steps is not None`. Previously the empty
dict default caused preflight to receive no steps and silently pass
when recipe loading failed.
Module-level getsource included the import block where
_check_dispatch_feasibility is imported, making both the presence
assertion and the ordering assertion trivially true regardless of
whether the function body actually calls it.
The `or 'fix-required'` branch was vacuously true for nearly any
preflight failure, making the test unable to verify the dormancy hook
specifically. Assert on 'dormancy_test_hook' alone.
…ertion

tools_kitchen.py line numbers shifted by +2 after adding recipe_name
kwarg to both _check_dispatch_feasibility calls; update
_LEGACY_JSON_WRITES accordingly.

The dormancy hook's script name does not appear in the preflight error
envelope — only the hook's matcher does. Replace the vacuous
`or 'fix-required'` assertion with a direct check on
`unfixable_matchers` to verify the specific dormancy hook caused
the failure.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant