refactor(apply): unify init'd and stateless dispatch behind ApplyTarget#37
Merged
refactor(apply): unify init'd and stateless dispatch behind ApplyTarget#37
Conversation
`core-ops apply --source-repo PATH` (stateless mode, spec/017) printed
a single wall-of-text summary at the end of apply regardless of TTY
state. End users running the canonical `examples/03-immich`
walkthrough saw a multi-second pause followed by a fully-rendered
final report — none of the line-by-line "creating... → created"
progression that init'd-mode apply has rendered since spec/006.
Root cause: `src/main.rs:143-207` (stateless dispatch) called
`apply_cmd::apply_with_report_stateless` unconditionally and printed
the final `human_report` once. The init'd dispatch (lines 216-269)
had a `if json / if interactive / else` ladder that selected between
`apply_with_report` (batched), `apply_with_report_streaming_interactive`
(TTY spinner), or `apply_with_report_streaming` (plain streaming).
Stateless never picked up the latter two because it had its own
dedicated entry point.
This is the kind of init'd-vs-stateless drift future agent + human
work would compound, not resolve, while the two paths remain
separately implemented.
Resolution: introduce `pub enum ApplyTarget { Initd { repo_source,
revision, state_path }, Stateless { source } }` (`src/cli/apply.rs`)
that abstracts the desired-state source and the state-backend
behaviour. The three streaming variants now accept `&ApplyTarget`
and dispatch on `target.is_stateless()` for the few mode-dependent
decisions: `target.load_desired()` (path-vs-git loader),
`target.state_path()` (None for stateless, suppresses
persist_in_progress / persist_finished / deterministic-state
writes), `target.classify_run_display(...)` (uses the new
`Stateless` variant for non-empty stateless hosts).
`apply_with_report_stateless` is deleted; stateless callers
construct `ApplyTarget::stateless(source)`. `src/main.rs` collapses
the two parallel apply ladders into a single ladder that selects
between the three shared streaming variants based on `--json` /
TTY-detection — for both modes. Audit emission still branches on
mode (init'd reads persisted state; stateless builds a synthetic
provenance via `synthetic_stateless_provenance`).
Verified end-to-end on `core-ops-uat` (Fedora CoreOS guest):
$ ssh -t core@uat 'cd .../coreops-uat && sudo core-ops apply \
--source-repo examples/03-immich --host example'
Apply for host example @ (stateless) (first run)
────────────────────────────────────────────────
Execution
─────────
[+] volume/immich-db-data.volume creating...
[+] volume/immich-ml-cache.volume creating...
...
[+] container/immich-database.container creating...
requires
- network/immich-internal.network
- volume/immich-db-data.volume
[+] container/immich-ml.container creating...
...
Outcome: converged
The "creating..." progress lines stream in their natural cadence;
container creation interleaves with volume/network creation as
podman processes each unit. Identical UX to init'd-mode apply.
Patch bump (`rust_source_surface` for `src/`); fixture pin updated
in lock-step. Six integration test files updated to call
`apply_with_report` / `apply_with_report_streaming` / etc. through
the new `ApplyTarget` API. No behavioural change to init'd mode.
$ cargo test 473 passed
$ cargo clippy --all-targets -- -D warnings clean
$ cargo run --bin core-ops-release -- validate --base-ref master passed (patch)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
outergod
added a commit
that referenced
this pull request
May 7, 2026
PR #37 (refactor(apply): unify init'd and stateless dispatch behind ApplyTarget) landed on master and the post-merge promote tagged v2.2.4. The fix delivers the spec/006 streaming output to stateless mode, which is the prerequisite spec/018's recording was waiting on. Bump 2.2.4 -> 2.2.5 (`packaged_readme_surface` carve-out; provenance-state fixture pinned in lock-step). FR-007 amendments: - The recording is now **apply-only**. The static plan-output and idempotent-re-run blocks in the README walkthrough section (per FR-006) are the canonical source of plan/re-plan content; including those beats in the recording adds duration without motion. The 90s budget is preserved for the apply step where the line-by-line streaming output now matters. - The recording MUST be produced on a host where `core-ops` runs natively (not via SSH delegation). SSH transport (even with `-t` PTY allocation) collapses the streaming timing that the recording is meant to capture; SSH-delegated recording is explicitly out of spec going forward. The decision file decision_018-recording-ssh-delegation captures the prior procedure as a session-3 historical artifact, not a forward recipe. Tooling amendment: - `docs/onboarding-script.sh` now records a single beat (apply only) instead of the prior plan -> apply -> re-plan trio. The command sequence in the BASH heredoc is one `demo 'sudo core-ops apply --source-repo examples/03-immich --host example'` invocation. Header rewritten to match. The actual cast + GIF re-recording is the operator's next step, to be done natively on a host with `core-ops` 2.2.5 + asciinema 2.4.0 + agg 1.7.0 + the repo present (or an equivalent on-host setup). The current `docs/onboarding.cast` and `docs/assets/core-ops-demo.gif` remain in tree from the prior SSH-delegated session-3 work and will be replaced once the operator records natively. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
outergod
added a commit
that referenced
this pull request
May 7, 2026
…s v3 Operator re-recorded `docs/onboarding.cast` natively against the PR #37 fix (core-ops 2.2.5 with stateless streaming). Duration 4.97 s. The recording captures the line-by-line "creating... → created" progression that asciinema 3.x's PTY observation preserves end-to-end — exactly the UX FR-007 was rewritten to require, and the SSH-delegation procedure was unable to capture. Cast was produced by asciinema 3.x, which writes asciicast v3 by default. Spec amendments to accommodate: - FR-008: now accepts asciicast v2 OR v3. Both are rendered by `agg` 1.7.0 and played by asciinema.org. The header MUST declare `"version": 2` or `"version": 3`. - SC-005: same — version MUST be 2 or 3. - SC-005a: duration calculation now branches on version. v2 has an absolute `duration` header field; v3 events are `[delta, type, data]` triples and total duration = sum of all deltas. Either MUST be ≤ 90. - Checklist C-010 updated with the version-branching one-liner. Post-recording sanitization (per decision_018-recording-ssh-delegation and checklist C-006): - OSC 3008 sequences stripped from event data (pam_systemd-emitted `hostname=core-ops-uat`, `machineid=...`, PID metadata under sudo). The post-processor is v3-aware (event shape is identical to v2; header schema differs but the SHELL/duration handling is unchanged for our purposes). - nix-store-path SHELL env value cleaned to `/bin/bash`. GIF re-rendered from the sanitized cast: 56 KB GIF89a, well under SC-005b's 1 MB soft cap. Verification: $ head -1 docs/onboarding.cast | jq '.version' 3 $ awk 'NR>1' docs/onboarding.cast | jq -s 'map(.[0]) | add' 4.965 (≤ 90) PASS SC-005a $ grep -c '3008' docs/onboarding.cast 0 PASS FR-009a $ grep -iE '(not\.one|ulthar|192\.168\.|10\.0\.|172\.16\.)' docs/onboarding.cast docs/onboarding-script.sh (no matches) PASS SC-006a $ head -c 6 docs/assets/core-ops-demo.gif GIF89a PASS SC-005b $ wc -c < docs/assets/core-ops-demo.gif 56923 (≤ 1MB) PASS SC-005b $ cargo run --bin core-ops-release -- validate --base-ref master passed (patch) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
`core-ops apply --source-repo PATH` (stateless mode, spec/017) printed a wall-of-text summary at the end of apply regardless of TTY state. End-users running the canonical `examples/03-immich` walkthrough saw a multi-second pause followed by a fully-rendered final report — none of the line-by-line "creating... → created" progression that init'd-mode apply has rendered since spec/006.
Root cause: stateless dispatch in `src/main.rs` called a separate batch-only `apply_with_report_stateless` entry point. The streaming + interactive variants that init'd dispatch routes to (`apply_with_report_streaming{,_interactive}`) never existed for stateless mode. The two paths drifted, and future agent + human work was at risk of compounding the drift while the two implementations remain parallel.
This PR introduces a single `pub enum ApplyTarget { Initd { ... }, Stateless { ... } }` abstraction and routes both modes through the same three streaming variants. The three init'd-mode entry points are unchanged in shape (still `apply_with_report{,_streaming,_streaming_interactive}`); they now accept `&ApplyTarget` and dispatch on a small set of mode-dependent helpers (`target.load_desired()`, `target.state_path()`, `target.classify_run_display(...)`). `apply_with_report_stateless` is deleted. `src/main.rs` collapses the two parallel apply ladders into one.
Patch bump (`rust_source_surface`); fixture pin updated in lock-step. Six integration test files updated to call through the new `ApplyTarget` API. No behavioural change to init'd mode.
Test plan
`sudo core-ops apply --source-repo examples/03-immich --host example` now streams the "creating... → created" progression line-by-line for all 10 services. Same UX as init'd-mode apply.
🤖 Generated with Claude Code