# Handoff — 2026-08-31 — The version in the About box ## State Ticket 0010 is implemented and **uncommitted**, on `main`, on top of PR #7 (`88a4c38`). Nothing was branched: the changes are in the working tree, so `git checkout -b feature/about-version` at any point before committing carries them along. `internal/version` is new. `app.Version` is gone. The Makefile, `scripts/install.sh` and a new `make version` target stamp `git describe --tags --dirty`, the short commit and a UTC build time through `-ldflags -X`. About and `-version` show whatever the build recorded and stay silent about the rest. Full suite green, green under `-race`. Quality gate PASS — 0/0/0, complexity 1592. Docs in both languages, four READMEs, and the drawio diagram all in sync. Also uncommitted, and **not mine**: `.tickets/issues/0004` and `0017` at `state: closed`, from the previous session. ## In flight Nothing. ## Next steps 1. **Review and commit.** A commit message was proposed at the end of the session. 2. **Close ticket 0010** — the user's call, as always. 3. The first real release is untested by definition: nothing has been tagged since `v0.1.0`. `make version` on a tagged commit should print the bare tag with no `-N-g` suffix, and that is the one assertion no test in this repository can make for you. ## Open questions / blockers None. ## Watch out for - **Go 1.26 does not say `(devel)`.** A plain `go build .` in a checkout reports a *pseudo-version* — `0.1.1-0.20260831165958-88a4c3859bf3+dirty` — derived from the last tag. `isPseudoVersion` catches it and reports `devel`. If a future Go changes that shape, `TestEveryFormOfPseudoVersionIsRecognised` is where it will show. - **The separator before a pseudo-version's timestamp is a dot, not a dash**, whenever a base tag precedes the commit — the base ends in `-0.` or `-pre.0.`. My first recogniser assumed a dash and silently matched nothing. Three of the four forms in the test caught it. - **`vcs.time` is not a build date.** It is the commit's timestamp. Anyone tempted to fill the empty `Built:` line for unstamped builds from it will make it wrong on every binary. - **`resolve` is separate from `Current` on purpose.** A test binary cannot be built with linker stamps, so testing through `Current` would leave every interesting case uncovered. Add new cases to `resolve`, not to `Current`. - **`unknown` is load-bearing.** It is what `-version` prints when nothing named the build, and `TestTheInstalledBinaryDoesNotReportAnUnknownVersion` uses its absence to prove the installer's ldflags reached the linker. Do not make it a version number. - Everything under `.memory/handoffs/2026-08-31-tool-menus.md` still applies — in particular the VT-emulator recipe, which verified this feature too, and the rule that `internal/terminal/tmprender/` must be deleted again before the quality gate. ## Afterwards — the release script, and what removing a constant costs The user ran their own `./03-build-releases.sh` and it failed. Two defects, both mine: 1. It read the version with `awk '{print $NF}'`, which took the build timestamp once `-version` grew a parenthetical. 2. **The cross-compile loop had no `-ldflags` at all.** All five downloadable binaries would have said `devel` while the release announced `v0.2.0`. The host binary was stamped and correct, so nothing but a hand check would have caught it. The second is the lesson: **removing a compiled-in constant moves a cost from visible to invisible.** A stale constant at least travelled into every build; a stamp only reaches the builds that ask for it. Every build path has to be found and stamped — `make build`, `scripts/install.sh`, and the five cross-compiles in `03-build-releases.sh`. Fixed by adding a `make ldflags` target the script reads, so the `-X` paths exist once. The script's version check is now three plain questions to git and one `grep -F`, none of which parses the `-version` sentence. `release_test.go` covers all of it. **If a sixth build path ever appears, stamp it.** `TestTheReleaseScriptStampsTheBinariesItShips` only guards the one that exists. ## And then — the tag script was the real culprit `03` refused again, and this time it was right but misdiagnosed: it said "HEAD carries no tag" when the tag existed and HEAD had merely moved one commit past it. The cause was three steps upstream. **`01-release.tag.sh` had no `set -e`.** Run twice, its `git tag` failed with "already exists", the failure was ignored, and the `git push origin "${TAG}"` on the next line pushed the *old* tag. Everything downstream was then working correctly on a lie. Fixed: `set -euo pipefail`, a tag-exists check against the local ref **and** `git ls-remote` (the state the user was in — local tag deleted, remote tag still there — is invisible locally), nothing-to-commit tolerated, and the tag applied only after a successful push. **`02-release.publish.sh` still has no `set -e` and must not naively be given one**: its `read -r -d '' DATA <<-EOM` always exits non-zero by design and would kill the script immediately. It also needs the curl HTTP status checked, since curl exits 0 on a 4xx. Left alone deliberately — it publishes to Codeberg and the user was not asking for it. **The remote is unreachable from this sandbox** (SSH, no key), so whether `v0.2.0` still exists on Codeberg could not be established. That question was handed back to the user. ## Finally — the simplification, and the lesson The user stopped me: *"fais quelque chose de plus simple, tu build comme avant avec le tag de release."* They were right. I had `03` stamp `git describe` and then **verify** it agreed with `TAG` — three gates that each looked reasonable and together blocked a release three times running. `git describe` answers *where is HEAD*; a release builder is asking *what release is this*. Those are different questions, and the gates existed only to reconcile an answer I should never have been asking for. `03` now stamps `TAG` directly (`make ldflags VERSION="${TAG}"`, `make build VERSION="${TAG}"`) and the gates are gone. Building needs no tag at all; only `02` does. **Do not reintroduce those checks** — the mismatch they detected cannot occur once the tag is the single source. `01`'s guards stay: they prevent pushing the *wrong* tag and block no build. The lesson, for whoever hits something like this: when you find yourself adding checks to reconcile two sources of truth, delete one of the sources instead.