Skip to content

Dry-run by default, and plan → review → apply flows

State: open/active · Genre: analysis

Use case: dry-run-plan-apply RFC mapping: cross-cutting (dry-run); partially new Source: cross-repo survey of sksizer/* (2026-07-05), companion to the workflow-constellation RFC (PR #682).

Dry-run-default with an —execute flag is re-implemented in essentially every orchestration script in the fleet, each hand-formatting its own preview and threading if(!dryRun) around every mutation site — with visible drift between what the dry-run shows and what execute does. Several tools extend the same need into a two-phase plan → human review → apply flow with a persisted plan artifact between the phases.

All 13 scripts (and their 6 fan-out drivers) parse —execute and default to dry-run, each hand-writing its own preview of prompt/runner/tools/command; quality_check.sh’s dry-run runs the checks but previews the fixer.

Where: scripts/bring_up_to_date.sh · scripts/quality_check.sh · justfile

Pain: N copies of an if/else that drift in detail and coloring; no guarantee the dry branch mirrors the execute branch. The identical pattern is vendored into rust-ontogen, kiosk, rust-markdown, rust-fluent-git and siblings.

vault-sync.mjs threads —dry-run manually as if(!dryRun) guards around 6+ mutation sites; migrate-permissions.mjs prints a grouped dry-run plan before its batched write phase.

Where: scripts/vault-sync.mjs · scripts/migrate-permissions.mjs

Pain: Every mutation site individually guarded; forgetting one is silent.

Two-phase plan/apply dedup flow: analyze pass stamps keep/delete statuses into an intermediate JSON plan, deletions only happen on re-invocation with —delete; the NudeNet tagger likewise defaults to DRY RUN with —persist.

Where: images/cziwak_similar_deletions.py · images/nudity/nudity_tag.py

Pain: Plan/apply separation and the human review between them re-implemented via argparse flags and a JSON file.

Per-script dry-run blocks copy-pasted across four scripts; the dry-run prints the prompt but cannot preview the gate/PR phases the execute path would run.

Where: scripts/bring_up_to_date.sh · scripts/deps_update.sh · scripts/sync_cousins.sh

Pain: Dry-run fidelity is structurally impossible when the plan isn’t a data structure.

Dry-run gate duplicated between forward and reverse sync with different defaults (—dry-run off for sync, on for reverse); the SyncPlan print is the de-facto plan artifact.

Where: src/sync_cmd.rs · src/reverse.rs

Pain: Inconsistent defaults for the same concern in the same binary.

Flowline’s cross-cutting dry-run covers the flag — but only if dry-run renders from the same declarative step graph the execute path runs, which is exactly what the hand-rolled versions cannot guarantee. The plan → typed-plan-artifact → approve → apply shape (2b, agent-dashboard, monosync) goes beyond a flag and is a taxonomy gap: it needs the plan to be a first-class typed output one run produces and a second run consumes.

Builds on: 03, 07 Introduces: run(wf, { dryRun }); extension: planRun() / applyRun()

Dry-run renders from the same declarative graph the execute path runs — the fidelity guarantee no hand-threaded if (!dryRun) copy can make:

const preview = await run(wf, { dryRun: true }); // every step, from the same graph
// Extension — plan -> review -> apply as a typed two-phase flow: one run produces
// a persisted, typed plan artifact; a later run consumes exactly it.
const p = await planRun(wf, { seed }); // p: Plan — serializable, versioned
await Bun.write("sync.plan.json", Plan.serialize(p));
// ... human review, or a confirm() gate (07), between the phases ...
await applyRun(Plan.parse(await Bun.file("sync.plan.json").text()));

The two-phase shape is the taxonomy gap: 2b’s dedup plan JSON, rust-ontogen’s REVIEW.md joins, and monosync’s SyncPlan print are all reaching for Plan as a first-class artifact.


← Back to Notes