Headless One-Shot Recipe Dispatch CLI#4191
Merged
Trecek merged 7 commits intoJul 6, 2026
Merged
Conversation
…mand skeleton Part A of #4189 — registers the `fleet_headless_run` feature in FEATURE_REGISTRY and adds the `fleet run` CLI command to fleet_app. Implementation: - FEATURE_REGISTRY: new `fleet_headless_run` entry (EXPERIMENTAL, default_enabled=False, depends_on={"fleet"}, tier=1). - cli/fleet/__init__.py: new `_fleet_run_error` helper emits a JSON error envelope on stdout and raises SystemExit (machine-readable refusal); new `fleet_run` command with session-type guard, fleet feature check, and fleet_headless_run feature check. CLAUDECODE guard intentionally absent per REQ-HFD-006. Stub exits with FLEET_NOT_IMPLEMENTED — dispatch body deferred to Part B. Tests: - Replace canary `test_fleet_run_command_not_registered` with positive `test_fleet_run_command_registered`. - Add feature registry integrity tests for fleet_headless_run (`test_fleet_headless_run_in_feature_registry`, `test_fleet_headless_run_promoted_by_experimental_blanket`). - New tests/cli/test_fleet_run.py exercising session-type guard (skill, leaf), CLAUDECODE relaxation, and feature gates. - Register test_fleet_run.py in _FLEET_CROSS_DIR_FILES so the feature("fleet") pytestmark is enforced. Docs: - tests/cli/AGENTS.md: add test_fleet_run.py row. - src/autoskillit/cli/fleet/AGENTS.md: append `run` to command list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uff E501 fleet/__init__.py exceeded the 415-line arch gate after fleet_run was added. Extracted fleet_run and _fleet_run_error to _fleet_run.py; __init__.py now registers the command via fleet_app.command(name="run")(fleet_run). Also shortened three docstrings in test_fleet_run.py that exceeded 99 chars. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fleet_run's _fleet_run_error emits JSON to stdout via print() — same intentional machine-readable pattern as other exempted fleet files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the FLEET_NOT_IMPLEMENTED stub in cli/fleet/_fleet_run.py with the Part B dispatch pipeline: - _execute_fleet_run async helper constructs ToolContext via make_context(cfg, project_dir=Path.cwd()) — auto-wires FleetSemaphore from cfg.fleet (REQ-HFD-007) - Builds prompt_builder via functools.partial matching the server-side _get_food_truck_prompt_builder pattern - Quota guard: real check_and_sleep_if_needed by default, no-op async callable when --disable-quota-guard is set (REQ-HFD-008) - quota_refresher accepts the config arg as a no-op async callable to satisfy _post_dispatch_cleanup's contract - asyncio.run() bridges sync fleet_run → async execute_dispatch - Ingredient parsing: --ingredient k=v tuples → dict - Backend resolution via resolve_backend_override (REQ-HFD-001) - JSON result envelope on stdout via to_envelope() (REQ-HFD-003) - Exit codes: 0=SUCCESS, 1=FAILURE, 2=RESUMABLE, 3=REJECTED (REQ-HFD-003) - KeyboardInterrupt/SystemExit handled explicitly; BaseException fallback catches CancelledError and unexpected crashes - Resume parameters forwarded to execute_dispatch (REQ-HFD-004) Tests in tests/cli/test_fleet_run.py: TestFleetRunDispatch class adds T-B.1 through T-B.12 covering ingredient parsing, backend resolution, exit code mapping, JSON envelope output, quota guard opt-out, resume parameter forwarding, and FleetSemaphore wiring.
…dule rule Replace direct imports from autoskillit.server._factory and _misc with the public autoskillit.server gateway. Add resolve_backend_override to server/__init__.py __all__ and re-export it. Update test monkeypatch targets to the public paths to satisfy the re-export bypass check. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Catch only KeyboardInterrupt (not SystemExit) so semantic exit codes (2=RESUMABLE, 3=REJECTED) raised inside asyncio.run() propagate naturally instead of being overwritten by FLEET_DISPATCH_INTERRUPTED. Change except BaseException to except Exception so GeneratorExit and other non-Exception base classes are not swallowed by the crash handler. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove monkeypatch.setattr(load_config) from the two session-type guard
tests: the guard fires before load_config is imported, so the patch was
never exercised
- Replace identity-check assertion with a behavioral assertion in
test_fleet_run_disable_quota_guard: call the coroutine and verify
the return value is {"should_sleep": False}
- Replace vacuous fleet_lock assertion with a real assertion in
test_fleet_run_uses_fleet_semaphore: capture tool_ctx from
execute_dispatch kwargs and assert it is the context from make_context
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This was referenced Jul 7, 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
Adds the
autoskillit fleet run <recipe>CLI command for non-interactive, one-shot recipe dispatch. The command is gated behind a newfleet_headless_runexperimental feature flag and reuses the existing dispatch machinery (execute_dispatch/_run_dispatch) — dispatch record persistence, sidecar, liveness heartbeat, automatic label cleanup,FleetSemaphoreconcurrency control, and quota-guard behavior — so CLI and kitchen-initiated dispatches share the samemax_concurrent_dispatchesbudget. Results are emitted as a JSON envelope on stdout with distinct exit codes for SUCCESS / FAILURE / REFUSED / RESUMABLE / timeout outcomes, and resume is supported via--resume-session-id/--prior-dispatch-id. The CLAUDECODE guard is intentionally relaxed for this headless path while the skill/leaf session-type guard is retained to prevent recursive dispatch.Individual Group Plans
Group 1: Part A — Feature flag registration, CLI skeleton, runtime gates
Part A registers the
fleet_headless_runfeature flag inFEATURE_REGISTRYand adds thefleet_runCLI command skeleton tofleet_appwith all argument definitions (REQ-HFD-001/004/008). The command applies runtime feature gates (REQ-HFD-009/010) that produce JSON error envelopes on stdout for machine-readable refusal, enforces the session-type guard with intentionally relaxedCLAUDECODE(REQ-HFD-006), and exits with a stub error when gates pass (dispatch implementation deferred to Part B). The canary testtest_fleet_run_command_not_registeredis deleted in the same commit as command registration. Feature registry integrity tests and CLI gate tests are added.Part B will cover the dispatch implementation —
ToolContextconstruction viamake_context,asyncio.run(execute_dispatch(...))call, JSON result envelope output, exit code mapping, resume support,FleetSemaphoreacquisition, quota guard behavior, and backend resolution (separate task).Group 2: Part B — Dispatch body, exit codes, quota guard
Part B implements the dispatch body of the
fleet_runCLI command registered in Part A. It replaces theFLEET_NOT_IMPLEMENTEDstub with a working dispatch pipeline:ToolContextconstruction viamake_context(REQ-HFD-005),asyncio.run(execute_dispatch(...))reusing the existing fleet dispatch machinery (REQ-HFD-002), JSON result envelope output on stdout (REQ-HFD-003), exit code mapping with distinct code for RESUMABLE outcomes (REQ-HFD-003), resume support via--resume-session-idand--prior-dispatch-id(REQ-HFD-004),FleetSemaphoreconcurrency control (REQ-HFD-007), quota guard with--disable-quota-guardopt-out (REQ-HFD-008), and--backendresolution viaresolve_backend_override(REQ-HFD-001).Part A covered feature flag registration, CLI command skeleton, argument parsing, runtime gates, and canary test deletion (separate task, already implemented).
Requirements
REQ-HFD-001: A new CLI command (suggested:
autoskillit fleet run <recipe>) must execute a single recipe dispatch non-interactively, accepting a positional recipe name, a--taskstring, repeatable--ingredient key=valueoverrides, an optional--backend(names fromBACKEND_REGISTRY, with the sameresolve_backend_overridesemantics and per-dispatch admission control as #4179/#4182), and an optional--timeout-sec.REQ-HFD-002: The command must reuse the existing dispatch machinery (
execute_dispatch/_run_dispatchinfleet/_api.py) — dispatch record persistence, sidecar, liveness heartbeat, and automatic label cleanup on failure — with no parallel implementation of any of it.REQ-HFD-003: The command must print the dispatch result envelope as JSON on stdout (
dispatch_id,dispatched_session_id,dispatch_status,l3_payload,token_usage) and exit 0 on SUCCESS and nonzero on FAILURE/REFUSED/timeout, with a distinct exit code or JSON field identifying RESUMABLE outcomes.REQ-HFD-004: The command must support resume via
--resume-session-idand--prior-dispatch-id, mirroringdispatch_food_truck's resume discipline for RESUMABLE outcomes.REQ-HFD-005: The command must run entirely in the invoking process tree on the package version installed at invocation time, with zero dependency on any long-lived kitchen MCP server or open kitchen gate.
REQ-HFD-006: The command must not require a TTY and must be invocable from inside a Claude Code session: the
CLAUDECODEguard is relaxed for this headless command (that guard protects interactive terminal UX, which does not apply), while the skill/leaf session-type guard (AUTOSKILLIT_SESSION_TYPEin("skill", "leaf")) is retained to prevent recursive dispatch from leaf sessions.REQ-HFD-007: The command must respect FleetConfig concurrency by acquiring the same
FleetSemaphoregate as kitchen-initiated dispatches, so CLI and kitchen dispatches sharemax_concurrent_dispatches.REQ-HFD-008: Quota-guard behavior must be defined for the headless path: either the guard is honored with a clear JSON refusal envelope, or an explicit
--disable-quota-guardflag provides the same semantics as the MCP tool.REQ-HFD-009: The command must be gated behind a new dedicated feature flag registered in
FEATURE_REGISTRY(core/types/_type_constants_features.py:42) — suggested namefleet_headless_run— declared withlifecycle=FeatureLifecycle.EXPERIMENTAL(core/types/_type_enums.py:456),default_enabled=False,depends_on=frozenset({"fleet"}),tool_tags=frozenset()andskill_categories=frozenset()(this feature adds a CLI command, not MCP tools or skills),import_package=None,tier=1(matching the fleet/planner/providers entries), andsince_versionset to the shipping release.REQ-HFD-010: The CLI entry point must gate at runtime following the
_require_fleetprecedent (cli/fleet/__init__.py:40): checkis_feature_enabled("fleet_headless_run", cfg.features, experimental_enabled=cfg.experimental_enabled)AND retain the existing_require_fleet(cfg)check — both checks are required becauseis_feature_enabled(core/feature_flags.py:16) does not enforcedepends_onat resolution time. On refusal, exit nonzero with an enablement hint (blanket:features.experimental_enabled: truein config; per-feature:features.fleet_headless_run: trueorAUTOSKILLIT_FEATURES__FLEET_HEADLESS_RUN=true); in the headless path the refusal must also be a machine-readable JSON envelope on stdout.REQ-HFD-011: Feature resolution must follow the existing experimental contract with no new mechanism: the blanket switch is the config key
features.experimental_enabled(popped from the features dict atconfig/settings.py:404intoAutomationConfig.experimental_enabled,settings.py:388, defaulting tois_dev_install()when unset,settings.py:407); the new entry must surface automatically inautoskillit features list/features status(cli/_features.py); registry integrity assertions must be added following thetests/fleet/test_fleet_rename_integrity.pyprecedent (assert the entry's lifecycle, depends_on, and tag fields).Closes #4189
Implementation Plan
Plan files:
/home/talon/projects/autoskillit-runs/impl-20260705-214515-338429/.autoskillit/temp/make-plan/fleet_headless_run_plan_2026-07-05_214800_part_a.md/home/talon/projects/autoskillit-runs/impl-20260705-214515-338429/.autoskillit/temp/make-plan/fleet_headless_run_plan_2026-07-05_214800_part_b.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