Skip to content

Dry-run vs apply

Every write verb defaults to dry-run. --apply is opt-in and requires authentication. The dry-run output is byte-identical to what apply would emit — the only difference is whether a PR is opened.

Terminal window
$ petrova open_decision kahn-hq --input /tmp/decision.json
open_decision dry_run 4f8c3e21d97a
upholds: MR-4, MR-7
branch: petrova/open-decision/phase-7-close
create docs/decisions/2026-04-29-phase-7-close.md (1842 B)

No PR has been opened. No GitHub API call has been made. The output shows exactly what the apply would produce: branch name, file paths, sizes, idempotency key.

Terminal window
$ petrova open_decision kahn-hq --input /tmp/decision.json --apply
open_decision applied 4f8c3e21d97a
PR #1234 petrova/open-decision/phase-7-close https://github.com/.../pull/1234

The --apply flag is mandatory. There is no “always apply” config flag and no way to set it via input JSON — it must be on the command line every invocation. This is intentional: apply is irreversible relative to the GitHub side (you’d have to close the PR), so the explicit flag forces a moment of attention.

--apply errors out before any GitHub call if neither PETROVA_GITHUB_TOKEN nor the GitHub App env triple is set:

error: AUTH_MISSING — set PETROVA_GITHUB_TOKEN before --apply

Dry-run runs without auth. This means a fleet (or human) can compose and validate a verb invocation entirely offline, then only set credentials when ready to emit.

The output of dry-run mode and apply mode are produced from the same internal pipeline. Apply additionally hits GitHub; it does not re-derive the diff. So:

  • The branch you see in dry-run is the branch that will be created.
  • The files[] list is the exact set of paths and contents that will be committed.
  • The idempotency_key is the key that will appear in the PR body.

The only fields added by apply are pr.url, pr.number, and pr.html_url — populated from GitHub’s response.

There is one legitimate skip: a fleet has a standing instruction for a specific verb on a specific repo, and is invoking that verb in a tight retry loop where the dry-run pass would just be noise.

In every other case — first-time invocation, manual operation, ad-hoc fixes — show the dry-run to the human first. The cost is a few hundred milliseconds; the benefit is catching a wrong path or missing constraint before a PR exists.