Skip to content

Headless One-Shot Recipe Dispatch CLI#4191

Merged
Trecek merged 7 commits into
developfrom
autoskillit-fleet-run-headless-one-shot-recipe-dispatch-cli/4189-2
Jul 6, 2026
Merged

Headless One-Shot Recipe Dispatch CLI#4191
Trecek merged 7 commits into
developfrom
autoskillit-fleet-run-headless-one-shot-recipe-dispatch-cli/4189-2

Conversation

@Trecek

@Trecek Trecek commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the autoskillit fleet run <recipe> CLI command for non-interactive, one-shot recipe dispatch. The command is gated behind a new fleet_headless_run experimental feature flag and reuses the existing dispatch machinery (execute_dispatch / _run_dispatch) — dispatch record persistence, sidecar, liveness heartbeat, automatic label cleanup, FleetSemaphore concurrency control, and quota-guard behavior — so CLI and kitchen-initiated dispatches share the same max_concurrent_dispatches budget. 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_run feature flag in FEATURE_REGISTRY and adds the fleet_run CLI command skeleton to fleet_app with 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 relaxed CLAUDECODE (REQ-HFD-006), and exits with a stub error when gates pass (dispatch implementation deferred to Part B). The canary test test_fleet_run_command_not_registered is 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 — ToolContext construction via make_context, asyncio.run(execute_dispatch(...)) call, JSON result envelope output, exit code mapping, resume support, FleetSemaphore acquisition, 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_run CLI command registered in Part A. It replaces the FLEET_NOT_IMPLEMENTED stub with a working dispatch pipeline: ToolContext construction via make_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-id and --prior-dispatch-id (REQ-HFD-004), FleetSemaphore concurrency control (REQ-HFD-007), quota guard with --disable-quota-guard opt-out (REQ-HFD-008), and --backend resolution via resolve_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 --task string, repeatable --ingredient key=value overrides, an optional --backend (names from BACKEND_REGISTRY, with the same resolve_backend_override semantics 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_dispatch in fleet/_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-id and --prior-dispatch-id, mirroring dispatch_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 CLAUDECODE guard 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_TYPE in ("skill", "leaf")) is retained to prevent recursive dispatch from leaf sessions.

REQ-HFD-007: The command must respect FleetConfig concurrency by acquiring the same FleetSemaphore gate as kitchen-initiated dispatches, so CLI and kitchen dispatches share max_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-guard flag 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 name fleet_headless_run — declared with lifecycle=FeatureLifecycle.EXPERIMENTAL (core/types/_type_enums.py:456), default_enabled=False, depends_on=frozenset({"fleet"}), tool_tags=frozenset() and skill_categories=frozenset() (this feature adds a CLI command, not MCP tools or skills), import_package=None, tier=1 (matching the fleet/planner/providers entries), and since_version set to the shipping release.

REQ-HFD-010: The CLI entry point must gate at runtime following the _require_fleet precedent (cli/fleet/__init__.py:40): check is_feature_enabled("fleet_headless_run", cfg.features, experimental_enabled=cfg.experimental_enabled) AND retain the existing _require_fleet(cfg) check — both checks are required because is_feature_enabled (core/feature_flags.py:16) does not enforce depends_on at resolution time. On refusal, exit nonzero with an enablement hint (blanket: features.experimental_enabled: true in config; per-feature: features.fleet_headless_run: true or AUTOSKILLIT_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 at config/settings.py:404 into AutomationConfig.experimental_enabled, settings.py:388, defaulting to is_dev_install() when unset, settings.py:407); the new entry must surface automatically in autoskillit features list / features status (cli/_features.py); registry integrity assertions must be added following the tests/fleet/test_fleet_rename_integrity.py precedent (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 Model count uncached output cache_read peak_ctx turns cache_write time
rectify* opus[1m] 1 53 13.2k 3.0M 182.3k 100 33.3k 34m 38s
review_approach* opus[1m] 1 8.9k 6.1k 266.4k 49.2k 14 42.2k 5m 50s
dry_walkthrough* opus 1 53 18.6k 1.9M 90.0k 51 90.1k 11m 8s
implement* MiniMax-M3 1 155.2k 25.6k 10.3M 0 162 0 9m 28s
assess* opus[1m] 1 158 45.7k 22.5M 232.8k 256 213.5k 37m 59s
Total 164.4k 109.2k 38.0M 232.8k 379.2k 1h 39m

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

Token Efficiency

Step LoC Changed cache_read/LoC cache_write/LoC output/LoC
rectify 0
review_approach 0
dry_walkthrough 0
implement 900 11423.6 0.0 28.4
assess 221 101736.4 966.2 206.7
Total 1121 33874.3 338.2 97.4

Model Usage Breakdown

Model steps uncached output cache_read cache_write time
opus[1m] 3 9.1k 65.0k 25.8M 289.0k 1h 18m
opus 1 53 18.6k 1.9M 90.1k 11m 8s
MiniMax-M3 1 155.2k 25.6k 10.3M 0 9m 28s

Trecek and others added 7 commits July 5, 2026 22:36
…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>
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