ADR-028: Unattended delivery without branch protection
Status: Accepted Date: 2026-10-22 Amends: ADR-027: dev follows main through pin PRs (pin PRs no longer depend on auto-merge), ADR-026: Feature specs drive the build (a spec can be shipped unattended after approval) Amended by: ADR-070: The Stop hook holds only the run’s owning session, and only while it makes progress (the 8-stop limit), ADR-075: An unattended ship or build runs as one session per unit, each saved at a checkpoint (one session per unit)
Context
Section titled “Context”A review of three repos on the lan cluster (stock-manager, stock-manager-gitops, ai-ops; review) found that the platform’s delivery chain breaks in two places for a founder on GitHub Free with private repos:
- No branch protection, no auto-merge. ADR-027’s pin PRs call
gh pr merge --auto, which a private repo on the free plan refuses, sodevnever followsmain. Nothing makes any merge wait for CI. ai-ops worked around it by pushing images from a laptop and pinning them by hand. - Runs end at “PR opened”, or earlier.
/svc:buildstops at the PR; the user then waits for CI, fixes red checks, merges, waits for the pin and checksdev. Sessions also end whenever the model decides to stop, for example after a red test. stock-manager’s user ran a launchd job withclaude -p --permission-mode dontAskand all of Bash allowlisted to get some of this unattended.
Options
Section titled “Options”- Pay for GitHub Pro/Team. It restores protection and auto-merge, but the platform should not need a paid plan to be safe.
- The platform merges, after it checked. Code that waits for every check and merges only on green: in the gitops-app’s workflow for pin PRs, in
cplatfor feature PRs. (chosen) - Merge queues or rulesets. These are not available on the free plan for private repos either.
Decision
Section titled “Decision”- pin-dev merges itself. The gitops-app’s
ci.ymlis also a reusable workflow (workflow_callwith aref).pin-devruns it on the pin branch and merges the PR in a following job only when it succeeded; a red CI comments the run on the PR and leavesdevon the old image. Where branch protection exists and refuses the direct merge, it falls back to CI on the PR plus auto-merge (spec 065). Chaining jobs instead of polling means no runner waits for another, which matters on small self-hosted runner sets. /svc:ship <spec>takes an approved spec through plan, build, PR, CI, merge and thedevpin (spec 067). The judgement stays in the command (plan and build as before, the CI fix loop with at most 3 rounds). Everything that can be code is incplat ship: the run’s state in the git directory, the push of the feature branch only, the PR, waiting for checks, merging only when at least one check was reported and none is pending, failed or cancelled, and waiting for the merged pin PR and Argo health.- A Stop hook in the shared plugin blocks the session from ending while a ship run is unfinished, and says what is next and what is red (spec 066). The run may stop only when it is done, after
cplat ship escalate(a rule needs the user), or after 8 blocked stops. Without a run the hook exits in shell. cplat headlessruns a prompt withclaude -p --permission-mode acceptEdits(file edits inside the repo, otherwise only the repo’s committed allowlist applies; nothing prompts;dontAskuntil 4.5.0 refused every edit) and starts fresh sessions while the ship state is unfinished, outside a quiet window, with a log, a summary line and a notification (spec 070).doneneeds a passingverification.md(spec 068), so an unattended run cannot close a spec without the reviewer’s evidence.
Consequences
Section titled “Consequences”- One human decision per feature: approving the spec. After that the run ends merged and deployed to
dev, or escalated with a reason. Staging and prod stay reviewed/gitops:promotePRs. mainis still writable by humans on the free plan. Agents stay off it:git push origin mainis in the template’sasklist, so headlessdontAskruns refuse it, andcplat ship prpushes onlyfeature/<spec>.cplat shipcan push and open PRs althoughgit pushandgh pr createareaskrules:cplatis allowlisted by the shared plugin. That widens what an unattended run may do, but only to these narrow, tested steps.- A merge happens on green checks without a human review of the PR. The spec, the acceptance tests written before the code, and the reviewer’s verification are the review. Use
--no-mergewhere a human should read the PR first. - The check that pin PRs pass is in the pin-dev run, not on the PR’s own checks tab; the comment on a red pin links the run.