Architecture

Seven steps. One of them refuses, even in a dry run.

Almost every command in cwp is one pipeline with the same seven named steps. Naming them is what turns guard placement from something each command remembers into a property of the runtime — and it is what makes the refusal survive a dry run.

This describes cwp v1.0, which is being built. The rebuild is under way on a branch and supersedes the current design section by section as each phase lands. Read it as the target rather than as a description of the released tool.

The spine

Every effectful command is an instance of the same seven steps. Two of them behave in opposite ways under --dry-run, and that is not a detail: Refuse runs, Consent does not. Merge them into one guard phase — the obvious simplification — and cwp push prod --dry-run stops refusing and starts printing a plan for a write it would never permit.

Pick a command, then set the conditions. The refusal fires under a dry run; only consent, apply and assert stand down.

  1. SurveyRead-only. Runs even under --dry-run.
  2. PlanA pure value. May carry unresolved slots and explicit unknowns.
  3. RefuseAlways runs, including under --dry-run.
  4. ConsentSkipped under --dry-run — there is nothing to consent to.
  5. ApplyA fold over the effects. Each one verified as it lands.
  6. AssertPost-conditions that must hold on every path.
  7. ReportA renderer over the event journal.

Survey

Today's probe() convention becomes a phase. A dry run inspects reality and prints a truthful plan rather than a guess.

plan rendered — 3 effects, 1 unknown

The plan, as --json renders it
{
  "effects": [
    { "kind": "transfer", "path": "wp-content/themes/site",
      "writes": "remote", "source": "wp-content/themes/site" },
    { "kind": "transfer", "path": "wp-content/plugins/site",
      "writes": "remote", "source": "wp-content/plugins/site" },
    { "kind": "unknown",
      "reason": "provider cannot predict deletions without a remote listing" }
  ]
}

The unknown entry is the honest part. A plan that cannot know something says so rather than guessing — and the dry run prints it.

Why verification happens per effect

Apply is a fold, and each effect is verified as it lands rather than in a phase afterwards. Moving that verification out is the obvious simplification and it is strictly more damage than doing nothing — because a push that fails cannot be repaired by running it again.

Three transfers. The second one fails its digest.

  1. wp-content/themes/site

    transferverify

  2. wp-content/plugins/site

    transferverify

  3. wp-content/mu-plugins/cwp

    transferverify

stopped after two transfers. The third path never moved, and the remote is missing one file rather than carrying a corrupt one.

The layers

Five directories with one rule between them, and it is the half people get backwards: the layering test forbids upward imports only. Sibling imports are composition — pull using scrub, doctor using its check modules — and a rule against those invents a shared/ directory within a month.

  1. The command manifest, the shared runtime, renderers, prompts.

    The only layer that knows commander exists.

  2. Pipeline instances, conduits, surveys.

    No commander, no prompts — an op is driven, it does not drive.

  3. wpcli/, abilities/, ddev/ — and the two seams below.

    Everything that talks to something outside this process.

    • providers/cloudron · ssh · local
    • builders/bricks · none · elementor
  4. Config, settings resolution, paths, policy, plan and effect types, the content model, canonical serialisation.

    No subprocesses and no network — which is not the same as no filesystem. A push planner may stat the tracked paths; writes go through the Workspace.

  5. Executor, Journal, Workspace, Findings, Facts, concurrency.

    Knows nothing about WordPress, hosts or builders — and imports nothing above it.

The layering test forbids upward imports only. Select a layer to see what it may reach.

171 runtime
copies of the command shell
~301 workspace
hand-written dry-run branches
~201 Session
bespoke context structs

Figures from the architecture document's own accounting, not measured here.

The shapes that are not this one

Being honest about the exceptions is what stops a spine eroding into an escape hatch, so all three are first-class types rather than opt-outs.

Conduits
cwp wp, cwp shell, cwp logs. They announce their target, hand over, and forward the exit code. stdio is inherited, so the output you came for never enters the journal and cannot — capturing it would break wp shell and strip WP-CLI's colour. No --json, and unguarded by design: cwp cannot classify WP-CLI subcommands, and a reflexive --force on wp plugin list is worse than no guard at all.
Surveys
cwp doctor, status, projects. Survey → Report, and the result is a list of findings. Nothing to refuse, nothing to consent to, nothing to apply.
init, outside all three
Its survey target is an answer nobody has given yet — it asks for the app, then probes it. Interleaving Survey and Plan is the pipeline with its guarantee removed, so init keeps its own shape: conversational input, then a pure scaffold planner that returns created, skipped and repaired before writing anything.