---
name: cut-blaise-release
description: |
  Cut a new Blaise compiler release. Bumps the version in Blaise.pas + project.xml,
  rebuilds compiler + RTL + stage-2 binary, verifies all four self-hosting
  fixpoints (QBE, native, internal-assembler, warm-cache), runs the full test
  suite, commits, tags, archives the NATIVE stage-2 binary under releases/vX.Y.Z/
  (NOT committed — releases/ is gitignored), renames + refreshes the -pre
  bootstrap dir to the next cycle, writes the community post + changelog, then
  bumps Blaise.pas + project.xml + uCompilerId.pas to the next -SNAPSHOT version.
  Use when asked to "cut a release", "release v0.X.Y", "tag a release", or after
  the user confirms a fixpoint is achieved.
allowed-tools:
  - Bash
  - Read
  - Edit
  - Write
  - AskUserQuestion
---

# Cutting a Blaise release

This skill mirrors the verified release process used for v0.11.0.
Run every step in order. Stop and surface the failure to the user if any
step fails — never paper over a missing fixpoint.

## Inputs

The user will name the release version (e.g. "0.11.0"). If they didn't,
ask them via AskUserQuestion. The next-dev version is the next minor with
a `-SNAPSHOT` suffix (0.11.0 → 0.12.0-SNAPSHOT). Confirm with the user if unsure.

## Preconditions to check before starting

1. Run `git status`. The working tree often carries **unrelated** uncommitted
   changes (e.g. `compiler/project.xml` and `tools/kanban/project.xml` build-
   option tweaks, plus many untracked `.txt`/`.adoc` working files). These are
   NORMAL on this repo and must NOT be swept into the release commit. The root
   `project.xml` may also carry unrelated edits — you will stage only its
   version line (see Step 5). If you see uncommitted changes that look like
   in-progress feature work, ask the user whether to proceed.
2. The current `Version` constant in `compiler/src/main/pascal/Blaise.pas`
   should look like `'<X.Y.Z>-SNAPSHOT'` (the dev marker). If it's already a
   plain non-dev version, ask the user — they may have already cut this release.
3. All commands run from the project root `/data/devel/new-pascal-compiler`
   (PasBuild requirement — sub-modules lack the `<version>` field).
4. `vendor/qbe/qbe` must exist (the fixpoint scripts need it). The current
   release binary lives at the newest `releases/v*-pre/blaise` or
   `releases/v*/blaise` — the fixpoint scripts auto-pick it via `sort -V`.

## Step 1 — bump version to release

Edit `compiler/src/main/pascal/Blaise.pas`. Find the line:

```pascal
  Version = '<X.Y.Z>-SNAPSHOT';
```

and change it to:

```pascal
  Version = '<X.Y.Z>';
```

Also edit `project.xml` in the project root. Find the line:

```xml
  <version><X.Y.Z>-SNAPSHOT</version>
```

and change it to:

```xml
  <version><X.Y.Z></version>
```

Edit `compiler/src/main/pascal/uCompilerId.pas`. The `COMPILER_ID` looks like
`'blaise-<X.Y.Z>-SNAPSHOT+bif<N>'`. For the release, **drop the `-SNAPSHOT`**
but **keep the `+bif<N>` tag** → `'blaise-<X.Y.Z>+bif<N>'`. Only bump the
`+bif<N>` number if the `.bif` interface format actually changed this cycle
(`IFACE_VERSION` in `uUnitInterfaceIO.pas` — the two must agree).

IMPORTANT (bif-coverage sync): the `bif-coverage` tool's `TestRun_BifCoverage_NoGaps`
asserts the project.xml base version (with `-SNAPSHOT` stripped) is a substring
of `COMPILER_ID`. So `COMPILER_ID` and project.xml `<version>` must carry the
**same base version** at all times — here at release, and again at the dev-cycle
bump in Step 7. A mismatch silently fails that test.

## Step 2 — verify the QBE fixpoint (NOT the binary we ship)

The project's `scripts/fixpoint.sh` does a whole QBE stage-2 build: it
rebuilds + installs the runtime, uses the latest release binary as stage-1 to
emit stage-2 IR, assembles + links the stage-2 binary to **`/tmp/fp_blaise2`**,
then emits stage-3 IR and diffs it.

NOTE: since v0.12.0 the **native backend is the default**, so the binary we
actually archive and ship is the NATIVE stage-2 binary from Step 3
(`/tmp/fpn_blaise2`), NOT `/tmp/fp_blaise2`. This step still runs — the QBE
fixpoint is a required reproducibility guard and prints the IR line count we
quote in the commit message and docs — but `/tmp/fp_blaise2` is only used to
rebuild `compiler/target/blaise` below so the native fixpoints run on the
released version.

```bash
./scripts/fixpoint.sh        # must print FIXPOINT_OK
```

Note the stage-2 IR line count it prints ("stage-2 IR: N lines") — you'll quote
it in the commit message and the docs.

The verified stage-2 binary is **`/tmp/fp_blaise2`**. (If the release stage-1
was too old to reach fixpoint in one round, `fixpoint.sh` extends a round and
the binary becomes `/tmp/fp_blaise3` — read the script's output to see which.)

`fixpoint.sh` only updated the IR-emitting path; `compiler/target/blaise` may
still be the old `-SNAPSHOT` build. Rebuild it from the fixpoint binary so the
native fixpoints and the test runner use the released version:

```bash
cp compiler/target/blaise_rtl.a /tmp/blaise_rtl.a   # FindRTL looks beside --compiler
pasbuild compile -m blaise-compiler --compiler /tmp/fp_blaise2
compiler/target/blaise --help | head -1             # must print: Blaise Compiler v<X.Y.Z>
```

ALWAYS pass `--compiler` to pasbuild — without it, PasBuild falls back to FPC,
which this project does not use.

## Step 3 — verify the native, internal-assembler, and warm-cache fixpoints

`fixpoint.sh` (Step 2) only exercises the QBE backend. Three more fixpoints
guard the native backend, the in-process internal assembler, and warm-cache
(incremental) rebuilds. Run all three:

```bash
./scripts/fixpoint-native.sh           # must print NATIVE_FIXPOINT_OK
./scripts/fixpoint-native-internal.sh  # must print NATIVE_INTERNAL_OK
./scripts/fixpoint-warmcache.sh        # must print WARMCACHE_FIXPOINT_OK
```

`fixpoint-native.sh` writes the **native stage-2 binary to `/tmp/fpn_blaise2`**
— that is the binary we archive and ship (Step 6), because native is the
default backend. Confirm it prints the right version:

```bash
/tmp/fpn_blaise2 --help | head -1      # must print: Blaise Compiler v<X.Y.Z>
```

If any of the four fixpoints does not print its OK line, **stop**. Show the
user the output and do not proceed. There is no acceptable "almost fixpoint" —
any divergence means the release is not reproducible.

## Step 4 — run the full test suite with the fixpoint binary

Build the TestRunner with the verified stage-2 binary, then run the whole suite
(do NOT use `pasbuild test` — build the runner and invoke it directly so you see
the real total):

```bash
cp compiler/target/blaise_rtl.a /tmp/blaise_rtl.a
pasbuild test-compile -m blaise-compiler --compiler /tmp/fp_blaise2
compiler/target/TestRunner          # must print: OK (N tests, ...)
```

Note the test count N. If any test fails, stop — do not release.

## Step 5 — commit and tag

Stage `Blaise.pas` whole (its only change is the version), but stage **only the
version-line hunk** of `project.xml` — it frequently carries unrelated working
changes that must not enter the release commit:

```bash
git add compiler/src/main/pascal/Blaise.pas
printf '%s\n' 'y' 'n' 'n' | git add -p project.xml   # stage hunk 1 (version) only
git diff --cached --stat   # MUST show exactly: Blaise.pas | 2 +-  AND  project.xml | 2 +-
```

If `git diff --cached --stat` shows project.xml with more than 2 changed lines,
you swept in unrelated changes — `git restore --staged project.xml` and redo the
`git add -p`. (The hunk-selection answers `y n n` assume the version line is the
first hunk; if project.xml has a different shape, inspect with `git add -p` and
stage only the `<version>` hunk.)

Commit with this pattern (HEREDOC for clean formatting):

```
release: v<X.Y.Z>

<one or two sentences: fixpoint verified on N lines of QBE IR (all four
fixpoints green: QBE, native, internal-asm, warm-cache), the headline themes
of the cycle, and the test count.>
```

Then tag:

```bash
git tag v<X.Y.Z>
```

**Never reference Claude in the commit message.** **Do not push.**
The user pushes manually.

## Step 6 — archive the release binary + refresh the -pre bootstrap binary

Archive the **native** stage-2 binary (`/tmp/fpn_blaise2` from Step 3) — native
is the default backend, so the shipped/bootstrap binary must be native, NOT the
QBE `/tmp/fp_blaise2`:

```bash
mkdir -p releases/v<X.Y.Z>
cp /tmp/fpn_blaise2 releases/v<X.Y.Z>/blaise       # NATIVE binary from fixpoint-native.sh
chmod +x releases/v<X.Y.Z>/blaise
cp compiler/target/blaise_rtl.a releases/v<X.Y.Z>/blaise_rtl.a
releases/v<X.Y.Z>/blaise --help | head -1          # sanity: Blaise Compiler v<X.Y.Z>
```

Every release directory must contain both `blaise` and `blaise_rtl.a` — the
bootstrap (`FindRTL`) looks for the archive beside the binary, so a release
missing `blaise_rtl.a` cannot serve as a stage-1 bootstrap.

Now **rename the rolling `-pre` directory to the next cycle** and refresh its
binary to this verified build, so cold-bootstrap of `master` stays current for
the dev cycle about to open. `releases/` is gitignored, so this is a plain
filesystem `mv` (no `git mv`):

```bash
# The -pre dir from the previous cycle is named v<X.Y.Z>-pre. Rename it to the
# NEXT minor so the newest releases/v*-pre/ tracks the cycle we are opening.
mv releases/v<X.Y.Z>-pre releases/v<next-minor>.0-pre   # e.g. v0.12.0-pre -> v0.13.0-pre
cp /tmp/fpn_blaise2 releases/v<next-minor>.0-pre/blaise
cp compiler/target/blaise_rtl.a releases/v<next-minor>.0-pre/blaise_rtl.a
rm -f releases/v<next-minor>.0-pre/*.bak             # drop any stale backup cruft
releases/v<next-minor>.0-pre/blaise --help | head -1 # sanity check
```

(If no `-pre` directory exists yet, create `releases/v<next-minor>.0-pre/`
directly. The GitHub Actions CI and `rolling-bootstrap.sh` both look for the
newest `releases/v*-pre/`, so the name must be the cycle just opened — not the
one just released.)

Update the test count in `README.adoc` (the "Testing: N tests" line in the
Project Status section) to match the suite total from Step 4, then commit it:

```bash
git add README.adoc
git commit -m "docs: update test count to <N> for v<X.Y.Z>"
```

Then build the release tarball for the GitHub Releases page — a single
top-level directory containing `blaise`, `blaise_rtl.a`, `README.adoc`,
`NOTICE`, and `LICENSE`:

```bash
STAGING=$(mktemp -d)
DIRNAME="blaise-v<X.Y.Z>-linux-x86_64"
mkdir "$STAGING/$DIRNAME"
cp releases/v<X.Y.Z>/blaise       "$STAGING/$DIRNAME/blaise"
cp releases/v<X.Y.Z>/blaise_rtl.a "$STAGING/$DIRNAME/blaise_rtl.a"
cp README.adoc                    "$STAGING/$DIRNAME/README.adoc"
cp NOTICE                         "$STAGING/$DIRNAME/NOTICE"
cp LICENSE                        "$STAGING/$DIRNAME/LICENSE"
tar -czf releases/blaise-v<X.Y.Z>-linux-x86_64.tar.gz -C "$STAGING" "$DIRNAME"
rm -rf "$STAGING"
tar -tzf releases/blaise-v<X.Y.Z>-linux-x86_64.tar.gz   # verify
```

**Do not `git add` any binary files or the tarball.** `releases/` is gitignored
and the user keeps binaries local on purpose.

## Step 7 — bump to next -dev version

Edit `compiler/src/main/pascal/Blaise.pas` again, changing `'<X.Y.Z>'` to
`'<next-minor>.0-SNAPSHOT'` (e.g. `0.11.0` → `0.12.0-SNAPSHOT`). Use
`-SNAPSHOT` in BOTH files — the older `-dev` suffix in Blaise.pas was retired so
the two version strings stay in sync.

Also edit `project.xml`, changing `<version>` to `<next-minor>.0-SNAPSHOT`.

REQUIRED: also bump `compiler/src/main/pascal/uCompilerId.pas` back to the
`-SNAPSHOT` form for the new base version, keeping the same `+bif<N>` tag →
`'blaise-<next-minor>.0-SNAPSHOT+bif<N>'`. This keeps `COMPILER_ID` in sync
with project.xml's base version — otherwise `bif-coverage`'s
`TestRun_BifCoverage_NoGaps` fails (it checks the project base version is a
substring of `COMPILER_ID`). Do NOT bump `+bif<N>` here; the `.bif` format
hasn't changed just by opening a dev cycle.

Stage selectively again (project.xml may still carry unrelated changes):

```bash
git add compiler/src/main/pascal/Blaise.pas compiler/src/main/pascal/uCompilerId.pas
printf '%s\n' 'y' 'n' 'n' | git add -p project.xml
git diff --cached --stat   # Blaise.pas, uCompilerId.pas, project.xml — version lines only
git commit -m "chore: begin v<next-minor>.0-dev cycle"
```

NOTE on CI / rolling-bootstrap anchor: when this release becomes the new
cold-bootstrap baseline, bump `STAGE1_TAG` in `.github/workflows/bootstrap.yml`
and the rolling-bootstrap default-`--from` anchor to `v<X.Y.Z>`. This is a
CI-only change and typically lands in its own commit alongside the release
(it does not block the release itself).

## Step 8 — write community post and changelog

Write two documents at the project root. Both are untracked (do not `git add`
them) — they are working files the user edits and publishes manually.

Both documents cover **only the commits between the previous release tag and
the tag being cut now** — the range `<prev-tag>..v<X.Y.Z>`, where `<prev-tag>`
is the most recent existing release tag before this one. Find it with
`git describe --tags --abbrev=0 v<X.Y.Z>^` (the latest tag reachable from the
commit just before the release commit), or `git tag --sort=-v:refname | head`.
Do NOT summarise the whole history — only this cycle's delta.

Survey the commits first (the `v<X.Y.Z>` tag already exists from Step 5):
`git log <prev-tag>..v<X.Y.Z> --oneline`, and group by theme. Helpful groupings:
`git log <prev-tag>..v<X.Y.Z> --oneline | grep ' feat'` (features),
`... | grep ' fix'` (fixes), `... | grep ' perf'` (performance).

### community-post-v<X.Y.Z>.md

A warm, readable announcement for the Blaise community (forums, mailing lists,
social media). Style:

- **Not** a commit-by-commit log — group changes into themes with a clear
  headline win up top.
- Sprinkle emoji throughout section headings and milestone call-outs.
- Warm, collaborative voice; British English spelling (see the global writing
  style guide).
- Include **tiny code examples** for the headline language features — a 3–6
  line snippet per feature is far more compelling than prose. VERIFY each
  snippet actually compiles before publishing: write it to `/tmp/snip.pas` and
  run `compiler/target/blaise --source /tmp/snip.pas --output /tmp/snip` (try
  both `--backend qbe` and `--backend native` for codegen-sensitive features).
- Cover: headline win(s), language/stdlib additions, bug-fix hardening, notable
  compiler internals, the fixpoint line count and test count, and a closing
  call to action pointing at the GitHub release page.
- End with a forward-looking line ("Onwards to v<next>! 🙌").
- Reference `changelog-v<X.Y.Z>.md` for full details.

### changelog-v<X.Y.Z>.md

A technical changelog for the GitHub Releases page. Style:

- Grouped by area (Native backend, Toolchain, Language, ARC/runtime, stdlib,
  Codegen, Semantic, Debugging, Performance, Tooling/CI).
- Each entry is one concise line: what changed and why it matters. More
  technical than the community post.
- A short "Examples" block under the Language section showing the new syntax is
  welcome (same verify-it-compiles rule applies).
- Close with the fixpoint IR line count and the test count.

## Step 9 — report to the user

Summarise:

- Tag created (e.g. `v<X.Y.Z>`)
- Stage-2 IR line count and all four OK lines: "FIXPOINT_OK / NATIVE_FIXPOINT_OK
  / NATIVE_INTERNAL_OK / WARMCACHE_FIXPOINT_OK"
- Test count (N tests passing)
- Binary path: `releases/v<X.Y.Z>/blaise` (note: not in git)
- `-pre` bootstrap binary refreshed
- New dev version on master
- Last 3–4 commit oneliners
- Community post: `community-post-v<X.Y.Z>.md` (untracked)
- Changelog: `changelog-v<X.Y.Z>.md` (untracked)
- Release tarball: `releases/blaise-v<X.Y.Z>-linux-x86_64.tar.gz` (upload to GitHub Releases)

Mention that the user must `git push --tags` themselves if they want the tag on
the remote — never push automatically.

## Common pitfalls

- **Unrelated changes swept into the release commit.** `git add project.xml`
  stages the WHOLE file, including pre-existing build-option edits. Always
  `git add -p` and confirm `git diff --cached --stat` shows exactly two
  one-line changes before committing. (This bit the v0.11.0 cut — the commit had
  to be reset and redone.)
- **Archiving the QBE binary instead of the native one.** Native is the default
  backend, so the shipped/bootstrap binary must be `/tmp/fpn_blaise2` (from
  `fixpoint-native.sh`), NOT the QBE `/tmp/fp_blaise2` (from `fixpoint.sh`). The
  QBE fixpoint is still required as a reproducibility guard — it just isn't the
  artefact.
- **Stale fixpoint binary in /tmp.** A `/tmp/fpn_blaise2` (or `/tmp/fp_blaise3`)
  left over from an earlier run can be days old. Always confirm the binary you
  archive prints the RIGHT version: `<binary> --help | head -1`.
- **COMPILER_ID out of sync with project.xml → bif-coverage fails.** At release
  `COMPILER_ID` drops `-SNAPSHOT`; at the dev-cycle bump it must re-gain
  `-SNAPSHOT` for the NEW base version. The base version in `COMPILER_ID` must
  always match project.xml's `<version>` base, or `TestRun_BifCoverage_NoGaps`
  fails. Keep the `+bif<N>` tag unchanged unless the `.bif` format changed.
- **Forgetting to rename the -pre dir.** The newest `releases/v*-pre/` must name
  the cycle being OPENED, not the one just released. After cutting v<X.Y.Z>,
  `mv releases/v<X.Y.Z>-pre releases/v<next-minor>.0-pre` (filesystem move —
  releases/ is gitignored). CI and rolling-bootstrap pick the newest by name.
- **`compiler/target/blaise` still on the old version.** `fixpoint.sh` does not
  rebuild it; do the `pasbuild compile --compiler /tmp/fp_blaise2` step (Step 2)
  before the native fixpoints, or they run against the previous version.
- **Forgetting `--compiler`.** Every `pasbuild` command must pass
  `--compiler <blaise-binary>`; without it PasBuild falls back to FPC, which is
  not part of this toolchain.
- **Missing `blaise_rtl.a` beside `--compiler`.** When pointing `--compiler` at
  `/tmp/fp_blaise2`, copy `compiler/target/blaise_rtl.a` to `/tmp/blaise_rtl.a`
  first — `FindRTL` looks beside the binary, and a stale archive causes
  undefined-reference link errors.
- **Wrong working directory.** All `pasbuild` commands fail outside the project
  root with a confusing version error. Stay at `/data/devel/new-pascal-compiler`.
- **Backgrounded fixpoint output drops.** Run the fixpoint scripts in the
  foreground with a generous timeout (600000 ms). Self-built compilation of the
  compiler takes ~2 min per stage.
