Jesús Erro

The Rehearsal Pattern: safe automation before mutation

Useful automation eventually touches something consequential. It installs packages, rewrites configuration, synchronises repositories, or materialises state on a workstation. At that point it is no longer merely a program; it is an operation. The important question is not only whether the code can make the change, but whether a person—or an agent—can understand the intended effect before authorising it.

I use the term Rehearsal Pattern for an operational practice that deliberately separates knowledge of an operation from permission to mutate. The system first observes, then rehearses or previews intent, validates prerequisites, requests explicit approval, performs a bounded mutation, and checks the result. This is the name used in this project for that sequence, not a claim that it is a recognised formal or industry-standard design pattern.

The problem: an interface with too much power

Maintenance scripts tend to begin as small conveniences. One command updates a tool; another applies a configuration. Over time they gain access to networks, credentials, package managers, and multiple operating systems. When the same entry point can inspect and change state, a context mistake can turn a routine query into a mutation that is hard to reconstruct.

The danger is not limited to commands labelled destructive. A legitimate operation can run against the wrong checkout, assume a missing dependency, select an unexpected version, or affect more of the environment than intended. A zero exit status does not prove that the final state is correct either. A confirmation prompt alone is therefore weak protection: approval is meaningful only after the operator has seen evidence about context, scope, and intent.

This boundary matters even more in agentic automation. An agent can compose and execute steps much faster than a person, but speed magnifies a false premise. The ability to reason about an operation should not silently confer the authority to perform it.

Rehearse before changing

A rehearsal is not necessarily a perfect simulation. Some operations provide a genuine --dry-run that computes the plan of a mutating action without applying it. Others expose a read-only --check that validates readiness or detects drift. Both produce evidence before mutation, but they are not interchangeable: one previews an action, while the other checks state.

The interface should preserve that distinction. Diagnostics should be safe by default. Mutating intent should be visible in the invocation instead of hiding behind an implicit default. Approval should follow inspection of the actual scope. Verification should then be a separate phase, because an accepted operation may still complete partially or leave unexpected state.

The pattern as a workflow

The sequence I apply is:

  1. Observe. Read effective state, configuration sources, versions, and environmental conditions without writing.
  2. Rehearse. Produce a plan or drift signal using the preview mechanism the operation genuinely supports.
  3. Validate. Run preflight checks that can block execution when prerequisites or context are wrong.
  4. Approve. State unambiguously that a particular mutation and scope are accepted.
  5. Mutate within bounds. Invoke the intended backend without treating approval as general authority for adjacent work.
  6. Verify. Observe again and run checks that can expose an incomplete or divergent result.

Not every tool can package all six stages into one command. The essential property is that the complete journey exists and that the boundaries between inspection, intent, and writing remain legible.

How it appears in Dotfiles and devx

The Dotfiles project applies this approach to an Ubuntu/WSL2 workstation. Its current interface combines read-only diagnostics with a protected update flow. A representative journey is:

devx dotfiles doctor
devx dotfiles update --check
devx dotfiles update --apply --yes
devx dotfiles doctor --strict

Those commands have deliberately different meanings. doctor inspects readiness and configuration-source alignment. Adding --strict keeps it read-only but turns readiness warnings into a failing exit status. update --check delegates to the update readiness backend; it is not a complete simulation of every future write. The --apply variant is the protected mutating path: it refuses to proceed without --yes, repeats the preflight, and skips its backend when that preflight fails.

The --yes flag enables non-interactive confirmation; it does not mean “anything is allowed”. The project policy says that agents must not add it without explicit human instruction for that specific operation. Some broad or sensitive actions remain outside the mutating devx surface altogether, even though they could technically be wrapped in a confirmation flag.

Agents and mutation boundaries

For an agent, rehearsal provides a comprehensible control structure. It can gather evidence through read-only operations, compare current state with intended state, and prepare a proposal without crossing the write boundary. When a person authorises the change, that authorisation can name one command and one scope. Afterwards, the agent can run result checks instead of treating process success as sufficient proof.

The same separation improves developer experience for people. Commands become a shared vocabulary across documentation, tests, and review. A preflight failure explains why mutation did not begin; a post-check failure shows that the result needs attention. Observability becomes part of the operational interface rather than an afterthought.

The boundary is still both social and technical. A well-named flag cannot rescue a badly implemented backend. Process permissions, isolation, credentials, and review of the affected surface remain independent controls.

What rehearsal reduces—and what it cannot guarantee

Rehearsal reduces risk by moving detectable mistakes earlier and making the transition to writing explicit. It also improves diagnosis: we can distinguish failures in observation, preflight, approval, execution, and verification.

It does not automatically guarantee idempotence. Running a mutation twice may create two effects unless the backend is designed to converge. It does not provide transactionality either: a multi-step operation may finish only part of its work. The pattern does not invent rollback, prove the correctness of the intended change, or ensure the absence of side effects in a faulty check or dry-run implementation.

Those properties must be engineered and tested for each concrete operation. Rehearsal is not a replacement for backups, isolation, least privilege, tests, or recovery procedures. Its contribution is narrower and more useful: it turns an opaque mutation into an informed, bounded, and verifiable decision.