v1.14.0: the merge phase learns to check before it claims

Released 2026-08-21. Most of this release is merge-pr being caught reporting outcomes it had asked for rather than outcomes it had observed, plus one command that finally runs the whole CI gate in one go.

What we were trying to fix

merge-pr is the skill that waits for CI, keeps a pull request mergeable, squash-merges it and tears the branch down. Every one of those steps had a place where the request and the result were being treated as the same thing.

A CI run that had not started yet was being read as green, because “no failing checks” and “no checks” look identical if you only count failures (#191). The remote-branch delete in Step 7 was issued and never verified, so a delete that silently did not happen was reported as cleanup (#185). One layer up, in auto-dev, a takeover merge was decided on gh’s exit code rather than on GitHub’s own state, which is the same class of mistake (#184). And the fallback for “cannot push” was covering a genuinely dirty conflict as though it were a permissions problem (#211).

Separately, running the kit’s gates locally meant remembering roughly eight commands in the order CI runs them, and getting one wrong meant a red build discovered on GitHub (#170).

What we decided

For merge-pr, in every case: re-read the authority. Ask GitHub what the run state is rather than inferring it from an absence, read the remote back after the delete, decide the takeover on the merged state rather than on a process exit code, and let a dirty conflict be a dirty conflict.

For the gates, ./scripts/run-all-tests.sh became the one command that runs every structural gate and every golden suite in the same order CI does, with a distinct exit code for a missing prerequisite so a machine without PyYAML gets one honest refusal instead of forty false failures. --quick skips the .NET fixture leg for a faster local pass.

The other durable decision here is #208: a decision gets one home, and the guard refuses a second. That is the rule decisions/registry.json and docs/decisions.md are built around, and it is the ancestor of most of the “one home” work in the releases that follow.

setup-repo also grew two refusals worth having: plan now fails on placeholder area names instead of quietly planning them (#198), and a refused label write names the status it actually observed (#200).

What got cut

The decision registry stayed a registry of programs, with verdict vocabularies and rules that can go red. There was an obvious pull toward letting it also hold architectural decisions, since both are “decisions”, and it was left alone. Those got their own home later, as ADRs in v2.0.0, precisely because they have no program and no verdict vocabulary and would have had to be exempted from every rule the registry exists to enforce.

run-all-tests.sh also stopped short of running the network-dependent Renovate acceptance gate by default. It is behind --with-network, because a local run that fails on a flaky network call teaches people to ignore the command.

What bit us

survey.sh needed the same class of fix twice inside one release. First it had to stop claiming a manifest had no effort axis when what had actually happened was a parser failure (#230), then it had to distinguish a missing parser from a manifest with genuinely no effort axis (#239). Two shipped fixes, one root cause, and the third spelling of it turned up in the very next release (#251).

The other one worth remembering is #233. create-issue was deriving the plan’s example commit type from how prose-like the change read, which produces a non-releasable docs: title for a change that touches shipped paths. It was fixed here, and then the identical bug had to be fixed again in implement-issue in the next release (#245), because the rule was written out twice rather than shared once.

ci-wiring-check.py also learned that a suite wired into CI but never git added is not wired into anything (#210).