# Release runbook

`omp-spt` has one release topology: this repository builds one multi-platform `adapter.spt`, and the GitHub release for `BigscreenVR/omp-spt` publishes that file under the default asset name `adapter.spt`.

<!-- [doc->REQ-DIST-ADAPTER-RELEASE] -->

The archive contains the complete adapter:

```text
manifest.toml
strings/omp-spt.mjs
strings/package.json
strings/skills/commune/SKILL.md
strings/skills/knock/SKILL.md
strings/skills/role/SKILL.md
strings/skills/setup/SKILL.md
strings/skills/signoff/SKILL.md
x86_64-pc-windows-msvc/omp-spt.exe
x86_64-unknown-linux-gnu/omp-spt
x86_64-unknown-linux-musl/omp-spt
```

spt-core keeps the shared root and flattens the current node's recognized target directory into the installed adapter directory. There are no per-platform release assets. Installing this one asset completes adapter installation; there is no secondary publication target or session reload action.

## Release-target limitations

<!-- [doc->REQ-DIST-LINUX-MUSL] -->
The asset supports exactly three helper targets: **x86_64 Windows** (`x86_64-pc-windows-msvc`), **x86_64 GNU Linux** (`x86_64-unknown-linux-gnu`), and a static **x86_64 musl Linux** compatibility tier (`x86_64-unknown-linux-musl`). It contains no macOS, Arm64, or other target payload. The musl member exists for OMP-capable x86_64 Linux hosts where the GNU helper is incompatible; it does not claim that OMP runs on Alpine generally. Adding any target requires a real helper build, a recognized archive target, deterministic package validation, and tagged native acceptance before it can be advertised.

User commands are:

```sh
spt adapter add --release BigscreenVR/omp-spt
spt adapter update omp-spt
```

## Release prerequisites

The release operator needs:

- write access to `BigscreenVR/omp-spt` and an authenticated `gh` CLI;
- Rust and Cargo for the `tools/omp-spt` crate;
- Node.js for the native extension tests;
- Python with TOML and `jsonschema` support for manifest validation and stdlib `tarfile` support for target validation and reproducible portable archive authoring;
- `traceable-reqs` for the required coverage gate;
- Zig plus `cargo-zigbuild` when cross-building the GNU and musl Linux helpers from Windows; and
- disposable native acceptance hosts for `x86_64-pc-windows-msvc`,
  `x86_64-unknown-linux-gnu`, and `x86_64-unknown-linux-musl`, each with real `spt` at or above
  `[adapter].min_spt_core_version` and OMP at or above the release-evidence floor `16.3.15`. Under
  [ADR-0019](adr/0019-musl-acceptance-follows-selected-helper.md), the musl host may use glibc when
  its spt-core build selects and installs the tagged archive's musl helper with exact digest
  equality.

The release commit must contain the intended `adapter/omp-spt.toml`, exact `adapter/strings/` plugin payload, helper source, tests, and changelog. The tag tree is the release source of truth.

## 1. Set the version and release notes

Set `[adapter].version` in `adapter/omp-spt.toml` to `X.Y.Z`. The Git tag and GitHub release tag must be `vX.Y.Z`; spt-core's `gh_release` updater resolves this conventional tag form. The inherited claude-spt lineage tags that once occupied `v0.4.0`–`v0.17.3` were renamed to `claude-spt-vX.Y.Z` on 2026-09-06 (`docs/INHERITED-TAGS.md` holds the reversal map), so `git tag vX.Y.Z` must never collide again; if it does, stop and consult that file before touching any tag.

Add `## [X.Y.Z] - YYYY-MM-DD` at the top of the current `omp-spt` section in `CHANGELOG.md`. Release notes must describe user-visible changes, required spt-core or OMP version changes, breaking behavior, and recovery steps. Do not include requirement ids, source paths, test-machine details, commit hashes, or incident forensics.

Each release-note paragraph, blockquote, and bullet should be one physical line so GitHub performs display wrapping. If needed, the repository includes the verified normalizer:

```sh
python ci/publish/unwrap-changelog.py CHANGELOG.md
```

Prepare `<release-notes-file>` outside the Git worktree from that version's changelog body, excluding the `## [X.Y.Z] - YYYY-MM-DD` heading. Commit the manifest version, changelog, source, tests, and release documentation before proceeding.

### Pin the claude-spt parity baseline

<!-- [doc->REQ-PARITY-BASELINE] -->
Whenever `claude-spt` behavior, documentation, or release contents inform feature-parity work,
first identify the exact published sister release being consulted. Query both release lineages:

```sh
gh release list --repo BigscreenVR/claude-spt-bs \
  --limit 100 --json tagName,publishedAt,isPrerelease,isDraft
gh release list --repo SaberMage/claude-spt \
  --limit 100 --json tagName,publishedAt,isPrerelease,isDraft
```

Use a stable, non-draft release and inspect it with `gh release view <tag> --repo <repo>`.
Fix the baseline when parity work starts, not when its eventual release is tagged: identify the
preceding `omp-spt` release publication time, then the first repository commit after that
publication, and select the latest `claude-spt` release published no later than that first commit.
Record the preceding release timestamp, first-commit identity and timestamp, and selected sister
release timestamp in `docs/PARITY.md`. Update its `Current baseline:` record in the same change
that first uses the sister surface. The record must name the exact repository and version; a
branch, moving `latest`, local checkout, or unversioned comparison is not a durable baseline.

Every parity-informed release must advance the baseline's `omp-spt` version to the release being
built, even when the selected `claude-spt` version is unchanged. The deterministic
version-consistency gate validates that correspondence and rejects a missing, malformed, or stale
baseline. Re-run the parity audit for sister releases newer than the recorded baseline; do not
claim their capabilities as covered until the baseline is deliberately advanced.

## 2. Run deterministic gates

From the repository root:

```sh
sh tests/ci-gates.sh
sh ci/run-gates.sh
node tests/omp-extension.mjs
```

All commands must pass. For a release, a dependency-related `SKIP` is not a pass. Install the missing dependency and rerun. See `docs/CI.md` for the deterministic coverage and the separate real-endpoint acceptance bar.

## 3. Freeze a clean release commit

Use one shell for the remaining release commands. Pin the canonical tag name, commit identity, and tree before any release build:

```sh
RELEASE_TAG=vX.Y.Z
RELEASE_COMMIT=$(git rev-parse --verify HEAD^{commit}) || exit 1
RELEASE_TREE=$(git rev-parse --verify "$RELEASE_COMMIT^{tree}") || exit 1
test -z "$(git status --porcelain=v1)" || {
  echo "release tree is not clean and committed" >&2
  exit 1
}
```

Record `RELEASE_COMMIT` and `RELEASE_TREE` with the release evidence. Do not build from a different checkout, amend the commit, or change `HEAD` after this point. Every later identity guard must pass in the same shell; otherwise discard the artifacts and restart from this section.

## 4. Build all three target binaries

The verified packer requires exactly one helper for each release target.

Immediately before building, reassert the frozen clean source:

```sh
test "$(git rev-parse --verify HEAD^{commit})" = "$RELEASE_COMMIT"
test "$(git rev-parse --verify HEAD^{tree})" = "$RELEASE_TREE"
test -z "$(git status --porcelain=v1)"
```

Build and test the native Windows release helper:

```sh
sh ci/digest/build.sh
```

This writes:

```text
tools/omp-spt/target/release/omp-spt.exe
```

Install both Linux Rust targets and the cross-linker once:

```sh
rustup target add x86_64-unknown-linux-gnu
rustup target add x86_64-unknown-linux-musl
cargo install cargo-zigbuild
```

Build the GNU helper:

```sh
cargo zigbuild --release \
  --target x86_64-unknown-linux-gnu \
  --manifest-path tools/omp-spt/Cargo.toml
```

Build the static musl helper from the same frozen source:

```sh
cargo zigbuild --release \
  --target x86_64-unknown-linux-musl \
  --manifest-path tools/omp-spt/Cargo.toml
```

The Linux outputs are:

```text
tools/omp-spt/target/x86_64-unknown-linux-gnu/release/omp-spt
tools/omp-spt/target/x86_64-unknown-linux-musl/release/omp-spt
```

Zig itself must be on `PATH`. Native Linux builders may produce the same target paths with a suitable Cargo linker. Do not copy or rename the GNU output into the musl path.

Validate each built file before packaging:

```sh
python ci/publish/validate-release-binary.py \
  x86_64-pc-windows-msvc \
  tools/omp-spt/target/release/omp-spt.exe
python ci/publish/validate-release-binary.py \
  x86_64-unknown-linux-gnu \
  tools/omp-spt/target/x86_64-unknown-linux-gnu/release/omp-spt
python ci/publish/validate-release-binary.py \
  x86_64-unknown-linux-musl \
  tools/omp-spt/target/x86_64-unknown-linux-musl/release/omp-spt
```

The validator requires PE32+ x86-64 for Windows, ELF64 x86-64 with the glibc x86-64 program interpreter for GNU, and ELF64 x86-64 with neither `PT_INTERP` nor `DT_NEEDED` for the static musl tier. A file with only `MZ` or ELF magic, a wrong architecture, a dynamic binary mislabeled as musl, or a renamed target output fails.

## 5. Pack the single release asset

The repository packer is dry-run by default:

Before either the dry-run or applied pack, repeat the `HEAD`, tree, and clean-status guards from section 4. A failed guard invalidates the input binaries; rebuild them after freezing a new release commit.

```sh
sh ci/publish/package-adapter.sh
```

Review the plan, then create the artifact:

```sh
sh ci/publish/package-adapter.sh --apply
```

The applied command writes only:

```text
dist/adapter.spt
```

`ci/publish/package-adapter.sh` validates the manifest, enforces a fat-archive-compatible spt-core floor, and requires exactly the shared plugin files plus all three validated target helpers. It rejects missing, mislabeled, malformed, wrong-architecture, or dynamically linked musl input; a non-executable archived Linux member; any byte difference between source and archived payload; and any archive member outside the exact public contract. Its Python archive writer gives both Linux members mode `0755` even on MSYS/NTFS and fixes archive metadata from `SOURCE_DATE_EPOCH` (default `0`) for reproducible output. The packer renames `adapter/omp-spt.toml` to root-level `manifest.toml` and copies only the named `adapter/strings/` plugin files.

For an independent listing check:

```sh
tar -tzf dist/adapter.spt
```

After normalizing directory-entry trailing slashes, the listing must contain exactly the ten release files shown at the top of this runbook and their required parent directories. Retain the packer's validation output and the SHA-256 hashes of `dist/adapter.spt` and all three input helpers.

## 6. Tag and stage the GitHub release

Before tagging, prove that the working checkout is still the clean frozen source, create the canonical tag at that exact commit, and verify its identity:

```sh
test "$(git rev-parse --verify HEAD^{commit})" = "$RELEASE_COMMIT"
test "$(git rev-parse --verify HEAD^{tree})" = "$RELEASE_TREE"
test -z "$(git status --porcelain=v1)"
git tag "$RELEASE_TAG" "$RELEASE_COMMIT"
test "$(git rev-parse --verify "$RELEASE_TAG^{commit}")" = "$RELEASE_COMMIT"
test "$(git rev-parse --verify "$RELEASE_TAG^{tree}")" = "$RELEASE_TREE"
git push origin "$RELEASE_TAG"
```

Create a tagged prerelease so the exact hosted asset can be acceptance-tested without making it the latest stable release. Immediately before upload, repeat the commit, tree, clean-status, and tag-identity guards; any failure means the asset must not be uploaded:

```sh
test "$(git rev-parse --verify HEAD^{commit})" = "$RELEASE_COMMIT"
test "$(git rev-parse --verify HEAD^{tree})" = "$RELEASE_TREE"
test -z "$(git status --porcelain=v1)"
test "$(git rev-parse --verify "$RELEASE_TAG^{commit}")" = "$RELEASE_COMMIT"
test "$(git rev-parse --verify "$RELEASE_TAG^{tree}")" = "$RELEASE_TREE"
sha256sum dist/adapter.spt \
  tools/omp-spt/target/release/omp-spt.exe \
  tools/omp-spt/target/x86_64-unknown-linux-gnu/release/omp-spt \
  tools/omp-spt/target/x86_64-unknown-linux-musl/release/omp-spt
gh release create "$RELEASE_TAG" dist/adapter.spt \
  --repo BigscreenVR/omp-spt \
  --verify-tag \
  --prerelease \
  --latest=false \
  --title "omp-spt vX.Y.Z" \
  --notes-file <release-notes-file>
```

These guards tie the uploaded artifact's build session to the clean commit and tree named by `vX.Y.Z`. The uploaded file name must remain exactly `adapter.spt`. Do not upload per-platform variants or rebuild between hashing and upload.

## 7. Verify acquisition and native acceptance

Repeat this entire section independently on disposable native `x86_64-pc-windows-msvc`, `x86_64-unknown-linux-gnu`, and selected-helper `x86_64-unknown-linux-musl` acceptance hosts. First prove tagged acquisition, the exact installed version, installed strings, and the target key used by the release record:

```sh
OMP_SPT_ACCEPTANCE=1 \
OMP_SPT_ACCEPTANCE_TARGET=<exact-target-triple> \
OMP_SPT_RELEASE_TAG=vX.Y.Z \
OMP_SPT_EXPECTED_VERSION=X.Y.Z \
OMP_SPT_EXPECTED_ADAPTER_SHA256=<64-lowercase-sha256-from-step-6> \
OMP_SPT_EXPECTED_HELPER_SHA256=<selected-helper-sha256-from-step-6> \
sh ci/publish/release-acquire-int.sh
```

Use exactly one of the three target triples listed above. The target, tag, version, expected adapter
digest, and expected selected-helper digest are all mandatory once acquisition is opted in. Both
digests must be the corresponding 64-character lowercase SHA-256 values retained before upload in
step 6. The integration rejects malformed digests and a tag other than
`v$OMP_SPT_EXPECTED_VERSION`, downloads the exact hosted `adapter.spt` from
`BigscreenVR/omp-spt` at that tag with `gh`, verifies its SHA-256, then extracts and verifies exactly
`$OMP_SPT_ACCEPTANCE_TARGET/omp-spt[.exe]` before it removes or installs an adapter. A missing target
member or either mismatch fails the run before acquisition can be accepted. After installation it
hashes the flattened selected helper and requires it to equal the hosted member. It also verifies
that `omp-spt` registers active from that repository's GitHub release directory, asserts
`spt adapter version omp-spt` is exactly `X.Y.Z`, and resolves the packaged adapter label. The final
result prints the acceptance target plus authoritative `adapter_sha256`, `helper`, and
`helper_sha256` values for that target's release record. The target is an evidence key, not host
auto-detection: the operator remains responsible for running on the matching native host. Missing
inputs, tools, or GitHub authentication are failures after opt-in, not skips. The script
temporarily removes the `omp-spt` registration, so do not run it against a registration you need to
preserve.

On the musl acceptance host, first retain native `spt --version` and `omp --version` output and prove the real OMP application starts there. The tagged acquisition must prove that spt-core selected `x86_64-unknown-linux-musl/omp-spt` and that the installed helper digest equals that archive member. Manually running the static helper or substituting a container without real OMP does not qualify.

Reinstall the tagged candidate on that same host, assert its version again, then complete every native endpoint check in `docs/CI.md`:

```sh
spt adapter add --release BigscreenVR/omp-spt --tag vX.Y.Z
spt adapter version omp-spt
spt endpoint create omp-spt-accept-fresh --adapter omp-spt
spt go omp-spt-accept-fresh
```

The version command must print exactly `X.Y.Z`. Release acceptance on each target requires evidence for tagged acquisition, fresh native bringup, attached OMP TUI, same-node message delivery with an explicit CLI or shortform reply, proof that ordinary assistant output remains local, idle/busy/idle lifecycle, blocked in-TUI session switching, native launch-time resume, graceful shutdown, and both ReadyAgent and LiveAgent behavior. No target can substitute for another.

Cross-node transport is not an `omp-spt` release gate. Do not delay the adapter release on a subnet routing check after same-node delivery has proved the adapter boundary.

If any acquisition, target identification, version, or native acceptance item fails on any target, leave the release marked as a prerelease, fix the source, and cut a new version. Do not replace a tested tag or asset in place.

## 8. Assemble and attach release evidence

<!-- [doc->REQ-DIST-RELEASE-EVIDENCE] -->

While the GitHub release is still a prerelease, author one JSON record per target using the
contract in `docs-site/src/reference/release-evidence.md`, and keep its referenced acceptance logs
beside it. Use the authoritative `adapter_sha256`, `helper`, and `helper_sha256` values printed by
that target's acquisition result. All three records must name the same `adapter.spt` digest.

Record host versions as bare canonical SemVer without a `v` prefix. OMP must be at least `16.3.15`;
spt-core must be at least `[adapter].min_spt_core_version` from `adapter/omp-spt.toml`. Every
fresh/resume endpoint or session ID must be a non-whitespace opaque value. Every outcome evidence
reference must be a GitHub release asset basename plus its digest, and that exact `(name, sha256)`
pair must also occur in the record's `artifacts` inventory.

Validate all three records together so the validator can reject duplicate targets, mixed source
identities, different `adapter.spt` digests, below-floor hosts, or unbound log references:

```sh
python ci/release/validate-release-evidence.py \
  release/evidence-v1.schema.json adapter/omp-spt.toml \
  dist/evidence/release-evidence-vX.Y.Z-x86_64-pc-windows-msvc.json \
  dist/evidence/release-evidence-vX.Y.Z-x86_64-unknown-linux-gnu.json \
  dist/evidence/release-evidence-vX.Y.Z-x86_64-unknown-linux-musl.json
sha256sum dist/evidence/*.log
```

The validator must print `RELEASE-EVIDENCE OK`. Compare every printed log digest with its
inventoried value; any missing file or mismatch fails the candidate. Upload the validated records
and every inventoried log attachment to the still-prerelease GitHub release:

```sh
gh release upload "$RELEASE_TAG" \
  dist/evidence/release-evidence-vX.Y.Z-*.json \
  dist/evidence/*.log \
  --repo BigscreenVR/omp-spt
test "$(gh release view "$RELEASE_TAG" \
  --repo BigscreenVR/omp-spt \
  --json isPrerelease \
  --jq '.isPrerelease')" = true
gh release view "$RELEASE_TAG" \
  --repo BigscreenVR/omp-spt \
  --json tagName,isPrerelease,assets
```

The final view must report the canonical tag with `isPrerelease: true`. Its assets must include
exactly one `adapter.spt`, all three validated target records, and every log basename inventoried by
an evidence reference. Download the attached logs to a clean directory and compare their hashes to
the same inventories:

```sh
EVIDENCE_VERIFY=$(mktemp -d)
trap 'rm -rf "$EVIDENCE_VERIFY"' EXIT
gh release download "$RELEASE_TAG" \
  --repo BigscreenVR/omp-spt \
  --pattern '*.log' \
  --dir "$EVIDENCE_VERIFY"
sha256sum "$EVIDENCE_VERIFY"/*.log
```

Missing, stale, extra-path, or digest-mismatched evidence is a release failure. Do not promote until
the combined validator passes and every inventoried attachment is present with matching bytes on
the prerelease.

## 9. Promote and verify the default install path

Only after all tagged acquisition and native acceptance checks pass on all three supported helper
targets, the combined evidence validator passes, and the validated records plus every referenced
log are attached and verified on the prerelease, promote the release:

```sh
gh release edit "$RELEASE_TAG" \
  --repo BigscreenVR/omp-spt \
  --prerelease=false \
  --latest
```

Verify that the latest release is the intended stable tag and retains the complete candidate asset
set: `adapter.spt`, all three target records, and every inventoried log:

```sh
gh release view "$RELEASE_TAG" \
  --repo BigscreenVR/omp-spt \
  --json tagName,isPrerelease,assets
```

Finally, on the disposable acceptance node, prove the shared unpinned install command resolves that
latest release:

```sh
spt adapter add --release BigscreenVR/omp-spt
spt adapter version omp-spt
```

The reported adapter version must be `X.Y.Z`.

## 10. Verify the update path

On a disposable node that still has the prior stable `omp-spt`, run:

```sh
spt adapter update omp-spt
spt adapter version omp-spt
```

The update must install `X.Y.Z` from the same default `adapter.spt`. A running endpoint keeps the
extension and helper it loaded at bringup; restart it to exercise the new release:

```sh
spt endpoint stop <id>
spt endpoint start <id>
```

`start` opens a fresh session on the existing endpoint; `spt go <id>` does the same and attaches
the native OMP TUI. No additional installation source or reload command is part of the update.

## Release custody

Retain:

- frozen release commit/tree and canonical `vX.Y.Z` tag identity;
- deterministic gate outputs with no release-blocking skips;
- hashes for `dist/adapter.spt` and all three input helpers;
- `gh release view` output showing stable tag `vX.Y.Z` and the complete attached asset set;
- three machine-readable target records, one each for `x86_64-pc-windows-msvc`,
  `x86_64-unknown-linux-gnu`, and `x86_64-unknown-linux-musl`;
- tagged acquisition results with exact version, target, adapter digest, helper member, and helper
  digest assertions in each record;
- the full per-target native acceptance evidence required by `docs/CI.md`, including proof that
  spt-core selected the musl member and installed its exact digest on the musl acceptance host;
- the unpinned acquisition result; and
- the `spt adapter update omp-spt` result and final reported version.
