# CI, native delivery and releases

The extension and Occipital have independent release versions. Repository manifests use `0.0.0`: root `package.json`/`package-lock.json`, and the Occipital Cargo manifest/lock entry. Native npm templates retain their substitution marker until staging.

## Supported platforms

The current support policy covers five OS/architecture combinations:

| Operating system | Architecture |
| --- | --- |
| GNU Linux, glibc 2.28+ | x64 |
| GNU Linux, glibc 2.28+ | ARM64 |
| macOS | x64 |
| macOS | ARM64 |
| Windows | x64 |

The installed Pi/Node host's OS, kernel and CPU requirements also apply; a successful hosted runner is not proof of a minimum OS version.

**Windows ARM64 is temporarily unsupported for the entire pi-tidy-ui extension**, not just Occipital or syntax highlighting. An existing package or native binary that installs or runs there does not establish current support. This policy does not by itself introduce an installation/startup block or change already-published packages.

### Why Windows ARM64 support is suspended

The policy is based on the recurring cost of required hosted installation/consumer checks, not a known functional incompatibility. After redundant preparation was removed, fetching/unpacking the selected-host dependencies and performing two independent npm consumer installations still dominated Windows ARM64 validation on both supported Nodes. Removing copy/archive overhead would not remove those required installs.

The cause is not attributed to a specific filesystem, scanner, CPU or runner defect. Support can be reconsidered when complete hosted installation and consumer verification becomes practical, without dropping optional-package/omission, cold/warm download, incremental-classification or cleanup controls. A package that happens to run, new runner release notes or a smoke test alone do not restore support.

### Platform selection and immutable releases

`scripts/release/platforms.mjs` is the single platform catalogue. Windows ARM64 remains fully described but disabled by default. Set the repository Actions variable `CI_WINDOWS_ARM64=true` to include its existing build and release inputs again, with minimum-Node consumers for ordinary events and both-Node consumers for releases; absent/`false` excludes it, and any other value fails planning. Compilation, consumers, release assembly and generated native dependencies use the same selected targets. Enabling that platform does not by itself establish restored support: both runtime profiles and complete release admission must pass.

Published artifacts/tags remain immutable, including existing Windows ARM64 binaries. Do not delete or substitute them. Admission validates the identities and public raw/npm bytes actually declared by the selected public release, and requires current CI equality for every enabled platform. Generated extension dependencies include only those enabled platforms; no stale ARM64 CI binary is invented when it is disabled. No installation/startup prohibition is introduced.

## Version and publication routing

| Ref | CI version substitution | Publication |
| --- | --- | --- |
| Branch / pull request | Extension remains `0.0.0`; daemon uses the nearest reachable `occipital-v*` version | None |
| `pi-tidy-ui-v0.2.0` | Extension release version; daemon uses the nearest reachable `occipital-v*` version | Admitted npm tarball published through OIDC with required provenance and exact public-byte verification, followed by a notes-only GitHub Release |
| `occipital-v0.1.0` | Only the daemon Cargo manifest/lock entry, **before compilation** | Occipital GitHub Release, followed automatically by native npm publication |

Tags must contain a complete SemVer version. Prerelease tags such as `occipital-v0.1.0-rc.1` produce prereleases. CI substitutions affect disposable checkouts, not repository commits or dependency versions. The npm package is named `pi-tidy-ui` and the command is `/tidy-ui`; tags select versions without changing these identities.

## Extension delivery

The source `package.json` maintains the npm listing description, search keywords, author attribution and repository/homepage/issue links; release staging preserves those fields. Keep `pi-package` among the keywords so the package is eligible for Pi's package gallery. The root README supplies the user-facing npm package page, including its opening work-in-progress notice. Its preview and `pi.image` use the same public PNG at `docs/assets/pi-tidy-ui.png`, which is also included in the package's `files` list and preserved by release staging. The selected preview represents an actual offline Pi session rendered by native foot; model identity and cache/context figures are sample data, not measured provider traffic or performance evidence. Its tracked generation inputs and checkout-only command are described in [README screenshots](screenshots.md); publication does not regenerate it. Author attribution uses the maintainer's name and public GitHub URL; an email address is not required.

The operator selected the extension's **own package version** as the only local/published discriminator: exactly `0.0.0` is development; a real release version uses the published-package contract. This does not compare the extension version with the daemon version. `0.0.0` is reserved for development, not a production extension release. No `.git`/path/install-method heuristic, environment override or extra mode flag is required.

Development uses only `occipital` (`occipital.exe` on Windows) in the extension root, never shared discovery, a release cache, native npm payload or download. Resolve that directory from the extension, not the current project cwd or `PATH`; building the binary is the developer's explicit action. If it is missing, the UI warns with a `[pi-tidy-ui]` prefix, and Occipital is not used. Files may use permitted exact cached Pi-core highlighting; otherwise source remains neutral under the [ordinary fallback policy](syntax-highlighting-spec.md). Bash never invokes the Pi classifier and uses whole-command `toolTitle` in whole-card Follow Pi fallback. Existing protocol/capability checks and owned-process cleanup still apply to the local build. Published packages keep shared-first selection. Their downloaded executables use the extension-owned agent-directory storage described in [local process acquisition](syntax/providers.md#acquiring-and-owning-the-local-process), not the installed package directory or development-only executable. Successful acquisition best-effort retains the caller's exact version and prunes other downloaded versions, including on warm/installed hits; this does not choose a latest executable or modify installed native packages. See [retention and concurrent-startup limitations](syntax/providers.md#downloaded-release-retention).

Keep concrete native package versions out of the repository `package.json` and `package-lock.json`. During extension release preparation, select the nearest `occipital-v*` tag once, verify the published artifacts/native npm packages and compatibility, then generate exact optional native dependencies and matching version/URL/SHA-256/size metadata in the disposable release package. Do not commit those generated native pins or change the source lockfile. The installed archive remains fixed; no runtime Git/latest lookup is introduced. Consumer lockfiles may record the resolved native versions.

Select the nearest reachable `occipital-v*` by Git history from the built commit (the commit of `pi-tidy-ui-v*` for an extension release), using `git describe --tags --match 'occipital-v*' --abbrev=0`, not maximum SemVer or publication time. Complete history and tags are required. A malformed nearest tag, missing release or missing required native npm package stops preparation; never silently choose an older release. Development without a usable colocated binary falls back without downloading or compiling. Using the package version keeps a Git checkout's local binary choice explicit without relying on filesystem layout or silently downloading a different native implementation.

`src/occipital/release.ts` is an unpinned source template. `scripts/release/extension-release.mjs` prepares a real-version package after release admission; its lower-level staging helper also creates isolated test packages. Neither modifies repository manifests/lockfiles nor publishes the extension. The npm package is `pi-tidy-ui`; its public command is `/tidy-ui`.

## Compiler and release equality

`rust-toolchain.toml` pins Rust **1.99.0**. Native CI resolves that exact version from the file rather than following `stable`; compiler upgrades are explicit reviewed changes. Repository Cargo manifests keep `0.0.0`, but disposable CI checkouts stamp the nearest daemon tag before compilation so embedded version text alone does not change an otherwise identical executable. On an Occipital tag, the tag itself supplies its release version even when multiple tags share a commit.

When both products change, complete the Occipital GitHub/native npm release before releasing the extension. The extension-release barrier compares every platform's raw CI executable with the selected published Occipital artifact, including SHA-256/size, and verifies the declared npm archives and their contained executables. Current CI equality and generated pins cover every enabled platform. It does not block ordinary CI or independent daemon releases merely because new native code has changed the bytes. Hosted C compilers/linkers/SDKs can still change: a future mismatch fails admission visibly rather than treating source equality as proof or normalizing bytes.

Windows target flags pass `/Brepro` to the MSVC linker because PE timestamps and the CodeView GUID otherwise introduce build-to-build byte differences. CRT static linking and native runtime/import checks remain separate requirements. This option is described by [LLVM's deterministic-build guidance](https://blog.llvm.org/2019/11/deterministic-builds-with-clang-and-lld.html), but is not a documented Microsoft linker compatibility guarantee. Admission compares complete raw files without stripping, replacing metadata or accepting a normalized hash; older published bytes are never rewritten.

Manual CI dispatch has an optional `clean-native-build` control, default `false`. It disables the bundled Cargo cache step: restoration and saving of `${CARGO_HOME}/registry`, `${CARGO_HOME}/git` and `target`. It therefore changes dependency-cache reuse as well as compiled-output reuse, while preserving all native tests/lint/ABI/protocol and installed checks. Compare its fresh raw binaries with ordinary CI and the published release to establish the equality prerequisite; no tag or publication is needed. Ordinary CI retains its compilation cache.

## Workflow relationship

`.github/workflows/ci.yml` runs on pull requests, pushes to `main`, both tag families, ordinary manual dispatch and a best-effort **03:23 UTC** nightly schedule. `scripts/test/ci-plan.mjs` plans fixed event obligations, never source-impact/diff routing or maintainer-approved narrowing. Every job uses one exact tested source SHA: PR HEAD for a pull request and the pushed commit for a push. Ordinary and extension-tag type/unit/SDK/GC and physical CLI jobs select **Node 22.19.0 and 26.7.0 × minimum Pi 1.0.1 and npm dist-tag `latest`, resolved once and pinned to one exact version per workflow** (de-duplicated when equal). Invalid/unavailable latest metadata fails planning without an older fallback. Each profile prepares its coherent CLI/SDK/TUI graph; an alternate executable with baseline SDK imports is not compatibility coverage.

Preparation owns networking, locked dependency resolution, toolchain provisioning and current-source artifact admission. Execution uses `scripts/test/run.mjs --inputs ABSOLUTE_INPUTS --suite NAME`, a fresh sanitized process tree and owned runtime/config/cache roots outside checkout ancestry. Source edits invalidate the descriptor. Required native, GC, shared-host and integration inputs fail setup instead of selecting skips. npm/Make route to this same interface; see [testing commands](testing.md#requirements-and-commands).

Every ordinary `main` push, PR and manual dispatch runs **all enabled native platforms and one minimum-Node/minimum-Pi current-source consumer per target**, plus complete `unit` and types on every Node/Pi profile. Complete CLI runs on Linux x64 / minimum Node / latest Pi; each other profile runs one fixed physical smoke/editor compatibility union, not every dedicated physical scenario. `unit` includes all ordinary harness and Pi-sensitive `harness/packaging` tests; inert delivery runs separately once per Node/minimum Pi. Every complete CLI profile runs `--suite cli --shard I/4` in four exhaustive, disjoint partitions, without file/group/pattern/skip/layer narrowing. Secondary ordinary profiles run `--suite cli-compatibility` once, unsharded, rejecting those same filters and shard arguments; selection reads all tests under the existing smoke/editor folders, without a filename registry. Compatibility-role names and reports distinguish this shorter coverage. New tests are discovered recursively by structural role, not registered in filename lists. Provisioning native/shared/profile prerequisites from discovered categories does not select away tests. The 2–3-minute feature-loop target is **local only**, not a full-CI budget; local feature/group options never narrow CI.

Nightly runs only exact-latest Pi compatibility on both Nodes: types, all `product/` and `integration/` tests plus Pi-sensitive `harness/packaging`, and the complete four-shard CLI union. Its Linux x64 Pi-free native producer supplies a same-source helper prerequisite, not a native-delivery matrix. There are no minimum-Pi profiles, native consumer jobs, other-OS builds or inert/public delivery jobs in nightly. It has distinct concurrency and a `Nightly latest-Pi result` check, separate from `Complete CI result`. The required final result is green only when every planned branch/matrix succeeds; failure, cancellation or required skips cannot be hidden by nonapplicable branches or a later commit. GitHub's schedule is best-effort, not an execution-time guarantee.

Occipital push tags retain both-Node/minimum-Pi consumers on every enabled platform, once-per-Node/minimum-Pi inert delivery, and a mandatory **daemon-regressions** branch on both Nodes/minimum Pi: all `integration/native` and Pi-sensitive `harness/packaging`. Publication waits for these checks as well as platform gates; unrelated UI/CLI profiles and latest resolution are omitted. Extension push tags retain the complete Node/SDK/types and full CLI matrix, both-Node/minimum-Pi consumers on every enabled platform, public native admission/consumers and admitted extension packing/publication. The ordinary runner-budget reductions do not narrow either release kind. Manual/scheduled execution cannot confer push-tag publication authority. See the [fixed CI obligations](testing.md#fixed-ci-obligations). No automatic lockfile update is introduced, and the graph does not grant publication authority to a local run.

Linux extension/terminal/inert-fixture jobs invoke `--network-isolated` with declared bubblewrap. The isolated tree permits required loopback traffic but denies public egress and uses trusted connection-file ancestry. This restriction covers the spawned process tree, not just a JavaScript fetch function. Native GNU execution uses Docker `--network=none`. Non-Linux native CI uses prepared-local inputs and sanitized environments, but currently lacks equivalent OS-level egress-enforcement validation; do not infer equal enforced isolation or passing platform acceptance from the workflow matrix.

Shared syntax CLI coverage requires the explicitly provisioned official **Subconscious 0.20.83** host. Linux x64 preparation downloads `ck-subc-linux-x64.zip` and verifies SHA-256 `c905fd8c5eba045c38fb61dd0c6c0ffc8254ae2cb0be50ef1017cde5470e62c4` before admitting the extracted executable hash. This is a third-party prerequisite, **not published Occipital**. The fixture runs it with no arguments, private XDG configuration/runtime/connection files and the same-source Occipital module, never an installed service. Shared discovery, actual loss and owned-to-shared return retain strict traffic/peer counters and process reaping; host provisioning alone does not prove those controls passed.

The reusable `.github/workflows/occipital.yml` performs only native production: Rust correctness, pipe protocol, parser-affinity doctests, formatting, strict Clippy, release build and staging. `prepare-native.mjs` materializes the verified neutral source and explicit tools without installing Pi/npm dependencies or copying a Cargo cache. Linux x64 has an independent producer so product SDK/CLI feedback does not wait for other platforms. The existing native descriptor/bootstrap and Node test runner execute every test under `tests/native-build/` before source stamping, including the real positive/neutral `checkCargoAncestry` once in each producer (under bubblewrap on Linux). `test:rust-harness` and its Make counterpart require that descriptor, not general Pi inputs. Consumer jobs install neither Rust nor an MSVC build environment. Each other platform runs its own `occipital-platform.yml`: its producer immediately unlocks `occipital-consumer.yml` for the event-selected profiles (minimum Node ordinarily, both Nodes in parallel for releases). Those consumers do not await other platform builds. Pi-independent packaging/version regressions run once per Node, not once again in native production. Provision Rust **1.99.0** plus rustfmt/Clippy and locked Cargo dependencies before execution, using owned artifact stores without user Cargo config. Declared Rust test/build commands execute offline; `rust-check` orders fmt before Clippy. Version stamping affects only the verified neutral source copy before compilation. After staging, CI verifies that the original checkout is unchanged before commit-only artifact admission.

GNU jobs run format/correctness/allocator/Clippy checks, locked release build and ELF glibc admission inside pinned multiarch AlmaLinux 8 userspace, asserting glibc 2.28 and native architecture. `scripts/release/linux-gnu.sh` receives an explicit provisioned Rust sysroot, official Node 22.19.0 and owned fetched Cargo artifacts. Compilation/testing is network-disabled; `_gnu-native` is an internal ordered gate, not a standalone host-baseline command. **Staging is separate:** CI stages version/source/notices/hash metadata for the exact executable, verifies that neutral-copy stamping left the original checkout unchanged, prepares the staged current-commit artifact, then runs actual-helper smoke and the current consumer on each event-selected Node version. No published helper is used for this smoke. Required ELF symbol versions above 2.28 fail admission; optional weak versions do not create a loader prerequisite. Both native architectures must pass before release acceptance. Published `0.1.4` uses this baseline; earlier `0.1.3` GNU binaries still require glibc 2.34 and remain immutable. A source-policy change cannot upgrade their bytes. Containers prove userspace compatibility, not an older kernel.

Windows uses `crt-static` for both MSVC targets, including the locked Oniguruma C build through `cc`, and `/Brepro` deterministic linking. Jobs provision `llvm-tools-preview` and inspect normal/delayed executable imports before staging, refusing compiler-runtime DLL requirements or missing/failed inspection output. System Windows DLLs remain valid dependencies. Static runtime security updates require rebuilding/releasing Occipital; published `0.1.2` remains unchanged. Platform compiler/SDK inputs remain prerequisites; matrix presence is not evidence that all six hosts passed or a minimum-OS guarantee.

Disposable hosted CI preserves the exact effective `CARGO_HOME` exported by Rust setup: dependency-source paths can survive stripped release builds. Before Cargo starts, the workflow verifies the store is inside the declared hosted setup roots and contains no root configuration or credentials; unexpected state fails without reading or deleting it. Only registry/Git artifacts and build outputs are cached. Compiler-cache keys include native source hashes and restore only compatible compiler/lock/target prefixes, so changed source can save its updated outputs. Product jobs cache the actual owned npm `_cacache`, not unused user npm state. Local preparation still uses its own Cargo home. Both direct and GNU/Make environments select `RUSTUP_TOOLCHAIN` by the absolute pinned Rust root so Cargo-home Rustup proxies do not need user defaults. Preserving this path is not proof of all-platform current/public byte equality; the unchanged release admission remains authoritative.

The only Actions uploads are one-day native staging transfers required by downstream jobs. A producer retry replaces its own same-run transfer by name; no published release asset is replaced. Transfer names are unique per version/platform and only that producer writes them. Transfers contain: raw executables, native npm package staging, SHA-256/size metadata, source commit identity, LICENSE and third-party NOTICE inventory. No diagnostics, test results, screenshots, development tarballs or publication receipts are uploaded. Safe summaries stay in logs; raw npm logs, environment, commands, URLs and opaque error text are excluded from diagnostic projections. Existing stored artifacts are not deleted by this policy. Real current-source consumers already pack the extension; full product jobs do not repeat a development pack. Durable distributables belong only in GitHub Releases/npm. `Cargo.toml` already uses `strip = true`, with release debug information disabled by default; no debug binaries or new debug build mode are introduced. Staging/notice preparation uses the job-owned runner temporary directory, outside cached Cargo `target/`; restored compiler outputs must never restore an old release destination. Every resolved build/target dependency is fetched for the notice inventory; missing texts fail staging. The maintained Subconscious supplement copies original LICENSE bytes from exact crate source commits. `.gitattributes` disables checkout newline conversion only for the project's `LICENSE` and maintained supplementary notice texts, preserving these bytes on Windows.

GNU jobs cache only the public system-tool image separately from Cargo outputs. Its exact key includes native platform and Dockerfile hash/base digest, without recipe fallback; invalid archives fail visibly and misses rebuild. No source tree, Rust SDK, native output or credentials are baked into that image. Its Git/Node runtime prerequisites are tooling, not delivered Occipital dependencies. The image's package-install layer may be reused between source commits: system packages refresh when the recipe/base is invalidated, not on every source edit. `clean-native-build` disables the bundled Cargo registry/Git/target cache, not this image cache, tests, artifact admission or consumer gates.

Ordinary CI wires every enabled native platform to the **current-source consumer** on minimum Node/minimum Pi; releases retain both Node versions, including Occipital tags. Nightly omits consumers. Only the number of ordinary Node profiles changes; consumer assertions and both fresh installation modes remain intact. `prepare.mjs --archives --native-artifact ...` supplies the exact staged commit/nativeTree executable and integrity-checked transitive package archives. The ordinary `consumer` contract uses real npm, a fixture-local registry/download server and fresh consumers; optional-package installation and the raw cold-download endpoint must serve the same current-source bytes. Preserve zero optional bootstrap downloads, exactly one cold fixture download without optional packages, zero warm HTTP, UTF-8/incremental counters and owned-process cleanup. A successful local consumer is not public npm/GitHub delivery acceptance.

GNU consumers use `scripts/release/installed-linux-gnu.sh TARGET SUITE PREPARATION ARTIFACT [RELEASE_REF]`. `native`, `consumer` and the combined `native-consumer` execute with `--network=none`; re-preparation uses explicit `--offline --npm-cache` inputs inside the baseline container. Online closure preparation uses lock-only resolution plus fresh-cache `npm ci --dry-run`; offline closure preparation replays the frozen locks with `npm ci --dry-run`, not materialized consumer `node_modules`. Both validate the exact graph/peers and every required archive; zero preflight unpack is a fixed-graph control, not a universal npm guarantee. The fixture retains two independent actual `npm ci` installs and all optionality, native-counter and cleanup controls. Only the later mandatory fresh installs establish installability and actual SDK/native behavior; preparation success alone does not. Non-Linux execution uses the prepared runner directly, with the enforcement gap noted above. Each consumer gets a fresh home/native cache; prepared npm package data does not replace cold/warm assertions. The combined `native-consumer` entry shares one offline preparation per Node, then invokes the native and consumer runners separately with fresh execution roots. Safe projected npm/fixture summaries are printed to job logs; diagnostics, receipts, screenshots and test results are never uploaded.

Published Occipital checks are a **separate external verification**: `published-consumer` / `test:installed-native:published`, with an explicit `--release-ref refs/tags/pi-tidy-ui-vVERSION` identifying the tested commit. Only extension push tags run this public npm/GitHub delivery lane, on both Node versions across the native platform matrix. It deliberately needs public networking and does not count as hermetic acceptance. CI never runs it for main/PR, Occipital releases, nightly compatibility or manual dispatch merely pointing at a tag. Ordinary tests must not fall back to public binaries if preparation fails.

On an Occipital push tag, CI passes the same-run, same-commit, tag-versioned artifacts to `.github/workflows/occipital-release.yml`, without a second build or historical-run lookup. Branch artifacts may carry the nearest tag's compiled version without being that published release: source authority and raw hashes govern admission. Extension tags do not publish Occipital; daemon tags do not publish/change the extension. The selected GNU floor is glibc 2.28 on both architectures, in addition to Pi/Node kernel/CPU/OS prerequisites.

## Execution timeouts

Every runner job has an explicit `timeout-minutes`; matrix entries each receive the same job-specific bound. Reusable-workflow callers do not accept this setting, so their executable jobs own it.

| Job | Minutes |
| --- | ---: |
| Ref/version and workflow lint | 5 |
| Required complete/nightly result | 5 |
| Prepared SDK, inert delivery or daemon regressions | 10 |
| Native checks, cold Rust build and artifact staging | 60 |
| One platform/Node consumer job | 25 |
| Each complete physical CLI partition or unsharded physical compatibility profile | 30 |
| GitHub Release assembly and publication | 10 |
| Automatic native npm publication | 15 |
| Extension release admission, packing and npm publication | 15 |
| Post-npm extension GitHub release notes | 5 |

The unit/SDK/GC step also has a five-minute limit within its ten-minute job. Current-source and extension-release-only published-consumer steps have fifteen minutes each within the twenty-five-minute platform/Node consumer job; npm commands have a five-minute bound, not relaxed native source/classification deadlines. Time limits are configured bounds, not evidence that required jobs passed. Expiry stops the process or cancels the job; it does not bypass assertions or trigger a successful retry. Job execution timeouts do not bound waiting for a runner. Interrupted release publication can leave a draft; the existing inspection/no-overwrite rule below still applies.

### Notice inventory and authority

`scripts/release/native-notices.mjs` inventories actual texts from the locked Cargo sources, including resolved build and target dependencies even when a particular executable does not link them. This is a source-text inventory, not legal-compliance certification. SPDX identifiers cannot replace missing license/notice bytes; missing or empty texts fail preparation. Explicit supplements identify the exact upstream source rather than inventing notices from labels.

The pinned `two-face` 0.5.2+bat-0.26.1 inventory additionally decodes syntax/theme acknowledgements from its compressed fixed-format data and includes its `bat` NOTICE. A version change requires reviewing that format. The pinned `r-efi` 5.3.0 inventory reads `AUTHORS`, which contains its license texts. Occipital's ported Pi TUI sRGB/OKLab routines have their own maintained MIT notice. Per-source hashes and explicit gaps accompany the generated NOTICE; publication admission validates these inventories rather than treating the presence of a NOTICE filename as sufficient.

## Published assets

The release workflow checks every enabled platform artifact's source commits, versions, targets, raw executable hashes/sizes, URLs and notice inventories before publication. It uploads:

- `occipital-vVERSION-TARGET` raw executables (`.exe` for Windows);
- one common `NOTICE` covering every platform, plus the project's `LICENSE`;
- `native-release.json` with the exact version, platform URLs, SHA-256 and byte sizes;
- `SHA256SUMS` for every preceding asset.

The common `NOTICE` preserves each distinct platform inventory byte-for-byte and labels its covered targets; identical complete inventories appear once. Native npm packages retain their own platform `NOTICE`. This layout is used by `0.1.3` and subsequent releases; earlier published assets remain unchanged.

If absent, it creates a draft, uploads all files, then publishes it. A retry may reuse an already-published release only when tag/prerelease identity, title, changelog-derived body, exact asset names/sizes and every raw asset byte match the newly admitted same-source assembly; that branch performs no remote mutation. Drafts, changed/missing/extra assets and lookup/download failures stop publication without repair or overwrite. After a failed draft upload, inspect it before an explicitly authorized cleanup/rerun. It uses the job-scoped `GITHUB_TOKEN` with `contents: write`, not an npm token. Do not move published tags.

Creating/pushing a tag is a separate operator action. The tag-triggered workflows do not create tags, commit version changes or update `src/occipital/release.ts`. An Occipital tag automatically publishes its admitted native npm packages after GitHub Release success; it never publishes the extension. Extension release admission downloads/verifies the selected public family and generates its exact `NATIVE_RELEASE` only in the disposable package, independently of the extension's version and repository placeholders.

## Extension release preparation

On an extension push tag, `extension-package` runs only after native, unit/SDK and physical CLI gates. It fetches the selected completed public release, compares current executables for every enabled platform and validates the declared public registry payloads, and stages the package only after every check passes. Changed bytes, missing assets, incomplete npm versions or a malformed nearest tag fail instead of choosing an older release. The same job packs and publishes exactly that admitted archive through OIDC with mandatory provenance; `Complete CI result` requires this job, including public-byte verification, to succeed. After verified npm publication, the separate `release-extension` job creates a notes-only GitHub Release from the same exact source/tag. It has only `contents: write`; npm's job retains only its existing read/OIDC permissions. The final CI result requires both jobs. No extra build, tarball upload or Actions artifact is introduced. Local preparation alone does not publish or grant authority. The preparation command takes explicit current/public artifact directories:

```sh
SOURCE_COMMIT=FULL_EXTENSION_COMMIT node scripts/release/extension-release.mjs \
  refs/tags/pi-tidy-ui-vVERSION CI_ARTIFACTS PUBLIC_ASSETS NEW_OUTPUT
```

`PUBLIC_ASSETS` includes the public release assets and `release.json` from the GitHub API. `CI_ARTIFACTS` contains every enabled platform's same-commit `occipital-vVERSION-TARGET` directories. The ref must resolve to the checked-out source commit. The generated package directory and `plan.json` are disposable release artifacts, not source files. Release preparation also requires the exact dated extension changelog entry, generates the packaged history and adds the selected Occipital dependency version. Lower-level synthetic staging without generated history keeps automatic changelog tracking disabled; it is not a release preparation shortcut.

### Extension npm publication

`scripts/release/extension-publication.mjs` separates archive preparation from the authorized mutation:

```sh
# Packs locally; no registry publication.
node scripts/release/extension-publication.mjs pack NEW_OUTPUT/plan.json
# CI-only: exact extension push-tag/OIDC caller required.
node scripts/release/extension-publication.mjs publish NEW_OUTPUT/publication.json
```

Packing checks the admitted manifest's package name/version and canonical Git repository, runs `npm pack --ignore-scripts --json` in that package directory, and records the resulting archive's actual SHA-512 in `publication.json`. Its sibling `tarballs/` directory must be new. The preparation plan and staged manifest remain unchanged; no archive glob selects a different file. `0.0.0` is reserved for development and npm-incompatible SemVer `+build` metadata is refused. Stable versions use npm's normal implicit `latest` rules; prereleases explicitly use `next`.

Publication requires `GITHUB_ACTIONS=true`, this repository, a `push` event, the exact `pi-tidy-ui-v*` ref, matching source/workflow SHA and caller `chpock/pi-tidy-ui/.github/workflows/ci.yml@REF`, plus both Actions OIDC request fields. Branches, PRs, manual dispatch and ordinary local npm login do not satisfy this entry-point contract. The job grants only `contents: read` and `id-token: write` and installs npm **12.0.2** on Node **26.7.0**. It publishes the recorded archive with explicit `--provenance`, `--access=public`, `--ignore-scripts` and the public npm registry; no token secret, local-login fallback or provenance bypass is configured.

Configure the `pi-tidy-ui` package's trusted publisher with GitHub owner **chpock**, repository **pi-tidy-ui**, workflow **ci.yml**, direct `npm publish` allowed and no environment name. npm requires an existing package for that configuration. For a first release, an explicitly authorized minimal staged bootstrap can establish the package using a different reserved prerelease version, without publishing the extension code. Do not approve/install that bootstrap or reuse its occupied version for the real release. This is a separate one-time maintainer action, not something CI creates or approves. A staged real release also cannot be bypassed by direct publication; inspect it and use the appropriate separately authorized owner action.

Before upload, the publisher rehashes the local archive and reads the exact registry version. Only structured `E404` means absence; conflicting name/version/integrity or other lookup failures stop without upload. A matching existing version skips upload but still verifies the exact public version record and raw streamed archive through the existing [bounded registry verifier](#automatic-native-npm-publication). This is immutable retry behavior, not overwrite or a second publish. No delay/retry or alternate registry is used to mask missing or mismatched public bytes.

Job-local `receipts.json` and safe log projections contain only package name, version, source commit, archive integrity, outcome and—after successful public verification—`registryByteVerified: true`. Outcomes are `not-started`, `unconfirmed`, `published` or `already-published`. `published` means npm returned the matching successful response, not that final registry verification passed. Upload/provenance failure, interruption or an invalid response leaves publication unconfirmed; an upload may already have reached npm. Inspect registry state before an explicit same-artifact rerun. Verification failure fails the job even after upload; no automatic repair, forced publication or success claim follows. No raw npm output, credentials, URLs, environment, archive paths or publication receipts are uploaded as Actions artifacts.

These checks implement release routing, not proof of actual trusted-publisher authentication or public installability. Those require the authorized tag run and [external consumer verification](testing.md#current-source-consumer-and-external-delivery). This workflow does not create/push tags, commit version changes, publish Occipital or regenerate the README screenshot.

### Exact native npm admission

Extension release admission checks registry metadata and the manifest **inside each downloaded archive** independently. `npm pack`'s reported name/version can originate from registry metadata and does not authenticate the archived manifest. Both must have the exact native package name/version, OS/CPU/libc declaration and sole `./package.json` export. The explicit rejected fields are `scripts`, `dependencies`, `optionalDependencies`, `devDependencies`, `bundleDependencies`, `bundledDependencies`, `bin`, `main`, `private` and `publishConfig`. This is not an exhaustive rejection of all dependency-related metadata: these checks do not reject `peerDependencies` or `peerDependenciesMeta`. The archive contains exactly the executable, `package.json`, `LICENSE` and `NOTICE`.

The archive's SHA-512 must agree with registry integrity and npm's packed integrity; the extracted executable is separately checked against the public declaration's SHA-256 and exact byte size. Both `--registry=https://registry.npmjs.org` and `--@kchpock:registry=https://registry.npmjs.org` are fixed, because a scoped registry setting can override the general flag. All identities declared by the selected public family are admitted, even when current CI builds only the enabled subset. None of these checks substitutes for current-CI raw-byte equality or authorizes replacing an immutable release.

## Automatic native npm publication

`.github/workflows/ci.yml` calls `occipital-npm-publish.yml` automatically for a new Occipital push tag after all required build/consumer/delivery/daemon-regression gates and GitHub Release publication. It consumes the same-run staged artifacts, exact source SHA and selected targets; no cross-run download or rebuild occurs. The caller must be this repository's exact-tag `ci.yml` with matching workflow/source SHA and OIDC authority. PRs, branches and manual dispatches cannot publish. Transfers expire after one day: rerun failed jobs before expiry; missing transfers fail rather than selecting another build. Automatic native releases reject SemVer `+build` metadata in the initial version gate, before building or publishing GitHub assets, because npm removes it from the version. Stable/prerelease SemVer remains supported; no version aliases or rewrites are introduced.

The workflow compares every assembled executable, notice, license, metadata and checksum file with the already-public GitHub release. It validates every selected platform's package manifest, payloads and platform notices before packing, restores executable permissions lost by artifact ZIPs, and never runs package scripts. Generated native manifests must include the canonical `repository.url` required by npm provenance; publication validates that field before packing. Already-published `0.1.4` packages remain unchanged.

Configure trusted publishing for **each** `@kchpock/occipital-*` package: GitHub owner `chpock`, repository `pi-tidy-ui`, workflow filename **`ci.yml`**, direct `npm publish` allowed, and no environment name (this workflow does not use one). Both caller and reusable workflow grant `id-token: write`. npm's [trusted-publisher documentation](https://docs.npmjs.com/trusted-publishers/#troubleshooting) matches the calling workflow, not the nested file: existing publisher registrations must be updated to `ci.yml` before automatic publication can authenticate. The GitHub-hosted job installs exact npm `12.0.2` on Node `26.7.0` (npm 12's own Node requirements do not change the extension's Node 22.19.0 minimum). It uses short-lived OIDC authentication, not an npm token, approval command or bypass-2FA credential. Although npm normally generates provenance automatically with trusted publishing, the workflow explicitly passes `--provenance` to require generation and stop publication on generation failure. The provenance describes this verified-artifact packaging/publication workflow, not a source audit or proof of reproducible Rust compilation; the original Rust build's authority remains the checked source CI run/commit.

Before any upload, every selected registry version is inspected. An existing version is skipped only when its integrity matches the prepared archive; a mismatch or registry error stops the family before publication. Prereleases use `next`; stable versions retain npm's normal implicit-`latest` checks. After publication, each exact-version registry record and its raw tarball are read directly, refusing redirects and non-HTTPS/outside-registry archive URLs and bounding streamed bytes to the prepared archive size before accepting SHA-512 equality. Direct record/tarball verification avoids re-resolving a just-published version through potentially stale npm package-selection metadata. An unavailable record/byte mismatch fails immediately; there is no delayed retry or republishing fallback. npm publication is not atomic: interruption can leave some packages public. Per-package receipts distinguish not-started, unconfirmed, published and already-published outcomes; an explicit rerun with identical artifacts skips matching versions instead of overwriting them. Safe per-package receipt projections appear in job logs, not Actions artifacts; plans and package staging are job-local only.

A staged version already occupies its package/version identity and cannot be published again through this workflow. The maintainer must approve it with 2FA; OIDC does not authorize stage approval. This workflow never publishes the root extension, changes tags, adopts another pin or dispatches itself.

## Validation reference

[Release and platform validation](testing.md#release-and-platform-validation) contains local commands and the distinction between inert packaging tests, actual ABI/runtime checks and authorized public-consumer verification. The workflow's mandatory gates and immutable-artifact rules above are delivery behavior, not evidence that a particular run succeeded.

The installed Pi/Node host remains an independent prerequisite. In particular, [official Node 25+ Linux binaries require `libatomic`](https://github.com/nodejs/node/blob/v26.7.0/BUILDING.md#official-binary-platforms-and-toolchains), supplied by the host distribution rather than by Occipital.

## Changelogs and release notes

### One history per product

The canonical histories are [`CHANGELOG.md`](../CHANGELOG.md) for pi-tidy-ui and
[`docs/occipital/CHANGELOG.md`](occipital/CHANGELOG.md) for Occipital. They use the
[restricted Keep a Changelog format](changelog-format.md): one first `Unreleased`
section, followed by dated, unique, descending SemVer entries; entry subsections
use H3. Published baselines are extension 0.1.1 and helper 0.2.0. Earlier history
has not been reconstructed, and tag existence alone is not evidence of publication.
The native history is documentation, not an input to compiled-native source identity.

During each task, add user-visible changes to the affected product's `Unreleased`.
Use `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security` and an explicit
`Breaking changes` section as applicable; omit empty categories. State necessary
upgrade/downgrade actions and compatibility changes. Describe effects, not commit
subjects or internal test execution. When a change affects both products, describe
each product's effect in its own history. Internal refactors and test-only fixes
need no duplicate user-facing entry. Conventional Commits and the Git diff are
completeness aids, not an automatic substitute for reviewed release prose.

Before every commit, review the staged changes against both histories, update or
reuse the relevant entry, and run:

```sh
npm run changelog:check
```

`AGENTS.md` makes this a commit prerequisite. CI's metadata job independently
validates both histories and the development manifest before any build/publication.
Format checks cannot determine whether prose accurately covers the staged change;
that remains an author/reviewer responsibility. Use absolute URLs in entries so
links remain meaningful outside the repository.

### Preparing a release

1. Review the selected product's `Unreleased` against changes since its previous
   **published tag in the same family**. Do not compare against the other product's
   version or infer publication from the largest tag.
2. Choose that product's version explicitly. Turn its reviewed `Unreleased` body
   into `## [VERSION] - YYYY-MM-DD` with the release's calendar date, and add a new
   empty `## [Unreleased]` above it. Do not change the other product's pending body,
   repository `0.0.0` manifests/lockfiles or generated native pins. Never rewrite
   previously published entries to describe new behavior.
3. Validate and preview the exact notes **before** requesting a tag:

   ```sh
   npm run changelog:check
   node scripts/release/release-notes.mjs check refs/tags/occipital-vVERSION
   npm run release:notes -- occipital-vVERSION
   # For an extension release, use pi-tidy-ui-vVERSION instead.
   ```

   The tag-specific check requires that product's exact nonempty dated entry and
   empty `Unreleased`; it never publishes pending changes as a substitute. It does
   not choose or create a tag. Extension note generation also reads the nearest
   reachable Occipital tag from the checked-out source and appends its exact
   dependency version/link. Shared services remain selected by protocol
   compatibility, so this identifies the packaged helper rather than promising
   that every runtime uses that exact helper process. The generated
   `### Occipital dependency` section must not be authored by hand.
4. Obtain authorization for the release commit/tag/push. If native bytes changed,
   finish the matching Occipital GitHub/npm release before the extension tag. The
   existing native-byte and public npm admission barriers still apply.

`npm run release:notes -- PRODUCT-TAG` writes the selected entry to stdout; it
neither edits files nor creates GitHub drafts. Both tag families reject development
`0.0.0` and npm-incompatible `+build` versions. Prerelease SemVer is supported, and
versions remain independent. Selecting a backport entry does not change the rule
that the release source/tag and admitted artifact bytes must match exactly.

### Publication and immutable retries

`github-release.mjs` is the common GitHub boundary. It checks authenticated push-tag
authority, local HEAD and tag commit, admitted notes and applicable asset bytes.
Occipital's existing publication wrapper uses the selected native entry instead
of generic packaging text. Its executable/NOTICE/LICENSE/metadata/checksum assets
and subsequent npm pipeline are unchanged.

An extension tag first completes its current npm admission, OIDC/provenance upload
and public-byte verification. Only then does `release-extension` publish the
extension entry as a GitHub Release, with the same generated native-version footer
used in the npm changelog. It attaches no extra archive or diagnostic assets and
sets `--latest=false` both when creating the draft and when publishing it: GitHub's
publish PATCH otherwise defaults to marking the newly public release Latest. The
repository-wide badge remains with the independent native release stream. Neither
stream resolves releases by that badge.

A missing release is created as a draft and made public after creation completes.
Existing releases are reused only after checking their tag, prerelease state,
title and notes (normalizing line endings/trailing newlines), and every admitted
asset where applicable. Changed public notes, assets, incomplete drafts and API
failures stop without editing or replacing an existing release. If npm succeeds
but the extension notes job fails, the overall result fails; inspect the partial
public state, then rerun only the failed job with the identical source. No automatic
republish, tag movement, cleanup or adoption of a different artifact occurs.

### Packaged history and Pi's update view

Source/development `package.json` sets `pi.changelogPath` to `""` because its version
remains `0.0.0`. A release archive includes `CHANGELOG.md`, projected from its own
canonical history: no `Unreleased`, no future entries, and exactly the generated
current entry/native dependency footer used for GitHub. Historical entries retain
their authored text, not today's dependency version. The helper's independent
history also ships as documentation; it is not a second Pi extension package.

Stable release staging enables `pi.changelogPath: "./CHANGELOG.md"` and includes
only stable historical entries. Pi 1.5 compares only numeric version components;
prerelease staging therefore keeps the complete eligible history in the archive
but leaves automatic tracking disabled. Prerelease notes remain readable in the
package and on GitHub. Older supported Pi versions do not gain a custom popup or
new runtime hook from this metadata.

Pi 1.5's native behavior is authoritative: a package must load an extension; the
first detection only records its version; newer entries appear on a subsequent
fresh launch, not a resumed session. `collapseChangelog` condenses the display.
`/changelog` shows Pi's own history, not extension histories. A separately updated
Occipital service has no independent Pi notice. The first changelog-enabled release
may merely establish Pi's baseline, so breaking-change instructions must also be
available in the repository/GitHub notes; update popups are not migration enforcement.
No extra extension UI, persisted version database, timers or startup rendering is
introduced.
