ADR 0013: Promotion
- Status: accepted
- Date: 2026-09-20
Context
A candidate must prove itself before it becomes the tool used to build the next one.
Decision
scripts/promote.sh runs make accept (lint, unit tests, exfil suite,
structural lints) and scripts/selfcheck.sh (the candidate completes a
scripted session: read a file, register a dev tool, run
go test ./internal/log, replay its own log). Only then: nodwyn-stable →
nodwyn-stable.prev, candidate → nodwyn-stable. The candidate's version is
the commit it was built from, so the tree must be clean (amended 2026-09-30;
it was a git tag v0.N-<phase>).
Consequences
- Promotion is the only path to
nodwyn-stable; copying a binary by hand is a process violation worth a friction.md line.
Amendment — 2026-09-30 (tags)
Promotion no longer tags. make promote takes no argument; the stable
binary's version (nodwyn-stable version) is the commit's 12-character id,
and git log says what that commit holds. A phase name added nothing the
commit does not, and a tag to invent stood between a green tree and a
promotion. The tags v0.1-bootstrap to v0.42-console stay as history.
Amendment — 2026-09-30 (the model turn)
The selfcheck's model turn goes to a fake model, test/fakes/ollama/serve,
started by the selfcheck on loopback, and the candidate runs on a temporary
config and session store with no keychain. Building nodwyn must cost
nothing and depend on nothing outside the checkout: the turn had gone to
whatever [control] named, which meant a paid model, a passphrase typed at
every promotion, or a particular machine being reachable. What the turn
proves is unchanged — the candidate's model client, window, log and replay
work end to end — and what it cannot prove, that a real model still
understands the prompts, was never a promotion gate: scripts/smoke.sh
remains the manual check against a real model.
Amendment — 2026-10-06 (project trust)
Since project trust (ADR 45), .nodwyn/init.scm, which registers the dev
tools the selfcheck runs go-test through, loads only once its revision is
accepted, and the selfcheck's temporary config trusts nothing: every
promotion failed with unbound variable: go-test. The selfcheck now reviews
the repository with nodwyn trust --repo . and accepts the revision it
shows, in that temporary config, so the user's own trust record is neither
read nor written. This trusts no file the person has not: promote refuses a
dirty tree, so the files accepted are the committed ones the candidate was
built from. It also proves the candidate's trust review and acceptance work
end to end.
Amendment 2026-10-16 (the browser checks, and WebKit)
Promotion ran make accept only, which does not run the console's
JavaScript: the sixteen browser checks (test/browser) ran when a session
called validate (ADR 37), so a commit made without validate could leave the
console blank and become the person's daily nodwyn-stable. make browser
(bash test/browser/run.sh) is now a step of scripts/promote.sh, between
accept and the build; it fails, saying how to install it, when Node or
Playwright's Chromium is missing. It is not part of make accept, which
the Gitea workflow and the GitHub mirror also run, because their runners are
Alpine containers on which a Playwright Chromium was never shown to run
(an unverified step there would be red or skipped for the wrong reason); the
workflow is unchanged.
The checks run Chromium, and nodwyn app opens WebKit (WebKitGTK, WKWebView),
whose layout and CSS support differ. BROWSER=webkit (bash test/browser/run.sh
or make browser) runs screens, approve and tasks in Playwright's
WebKit, with screenshots under out/webkit/. It is a separate download, so it
is in neither validate nor promote: where it is not installed the run says so
and runs nothing (exit 0). It was run on no machine for this amendment, since
WebKit was not installed on the one used: only the missing-WebKit path was run,
and the WebKit launch itself is untried. The other checks stay Chromium-only (remote maps a host name
with a Chromium flag).