--- name: release-cut description: 'Cut a new pi-agent-dashboard release: promote `## [Unreleased]` in CHANGELOG.md, bump every workspace package.json per SemVer, commit, tag `v`, and push — triggering the Release workflow that publishes every non-private workspace, builds the Electron artifacts, and creates a GitHub Release. Use on "cut a release", "release vX.Y.Z", "publish a new version", "tag a release".' license: MIT metadata: author: pi-dashboard version: "1.0" --- # Cut a pi-agent-dashboard Release Canonical reference: [`docs/release-process.md`](../../../docs/release-process.md). This skill automates steps 1–5 of that doc. **Production tags (`vX.Y.Z`) publish the GitHub Release automatically** — electron-updater's default GitHub provider only resolves published, non-draft releases, so a draft would silently block auto-update. **Pre-release tags (`vX.Y.Z-rc.N`) stay drafts** so a maintainer can eyeball artifacts before flipping to published. See change: fix-electron-auto-update-pipeline. ## Pre-flight (MUST pass before touching anything) Run these in order. If any fails, **stop and report** — do not continue. 1. **Clean working tree** ```bash git status --porcelain ``` Must be empty. If not, ask the user to commit or stash. 2. **On the release branch** ```bash git rev-parse --abbrev-ref HEAD ``` Must be `develop` (this repo has no `main`). If elsewhere, ask user to confirm before continuing. 3. **Up to date with origin** ```bash git fetch origin && git status -sb ``` Branch must NOT be "behind". If behind, ask user to pull first. 4. **Tests pass** ```bash pnpm test ``` 5. **Build succeeds** ```bash pnpm run build ``` 6. **Dependency-shape gate** (introduced by `enable-standalone-npm-install` to prevent regressions of v0.5.3 publish-time bugs) ```bash node scripts/verify-release-deps.mjs ``` Asserts critical runtime deps (`jiti`, pinned `node-pty`, etc.) are still declared in the publishable workspace `package.json` files. Failure means the next published tarball would be broken — STOP and fix the workspace before cutting. > **Known false-positive (substring gate).** `verify-release-deps.mjs` checks the declared range with a naive `String.includes(minVersion)` — NOT semver math. So a legitimate pi bump ABOVE the floor (e.g. floor `0.74.0`, pin `^0.80.10`) fails the gate because `"^0.80.10"` does not contain the substring `"0.74.0"`. When this fires and the pin is genuinely newer than the rule's `minVersion`, the FIX is to bump that rule's `minVersion` (+ its evidence note in the RULES array, and the `scripts/AGENTS.md` row) to the new floor — do NOT downgrade the pin. This recurs on every pi version bump. See change: fix-release-lockfile-drift (gate lives in `scripts/verify-release-deps.mjs`). 7. **Dispatch `ci-smoke.yml` against `develop`** (recommended; catches installer regressions BEFORE the tag exists) The release pipeline (`publish.yml`) gates `publish` on a `release-gate` that runs the full 7-leg standalone-install-smoke matrix. If that gate fails on `workflow_dispatch`, `tag-and-push` is skipped — clean abort, no commit, no tag. But on a `git push --tags` cut, the tag already exists when the gate fires; failure leaves a dangling tag requiring `release-revoke`. Operators SHOULD run the smoke matrix first against `develop`: ```bash gh workflow run ci-smoke.yml --ref develop gh run watch # or open the Actions UI ``` All 7 legs must be green before cutting. If any leg fails, fix the regression on `develop` first — do NOT cut a tag that you know will fail the gate. Skip this step only when the change since the last release is provably installer-irrelevant (no lockfile, bundle-server, native dep, or preload-fastify touch). See change: gate-publish-on-smoke-and-tests. If any pre-flight step fails, stop and surface the exact error to the user. ## Step 1 — Read current state ```bash git describe --tags --abbrev=0 # last tag, e.g. v0.2.9 node -p "require('./package.json').version" # current pkg version ``` Confirm they match (e.g. tag `v0.2.9` ↔ pkg `0.2.9`). If they diverge, surface the mismatch and ask the user how to proceed. ## Step 2 — Curate `## [Unreleased]` 1. List commits since last tag: ```bash git log ..HEAD --oneline ``` 2. Read `CHANGELOG.md` and extract the current `## [Unreleased]` section. 3. Cross-check: every `feat:` / `fix:` commit should have a corresponding user-visible bullet under Added / Changed / Fixed. 4. If gaps exist, **use AskUserQuestion** to list missing items and confirm whether the user wants to add them now. If yes, draft bullets in end-user language (not commit-subject shorthand) and insert them. 5. Never invent behaviour — only summarise what the commits actually did. > **Far-behind escape hatch (long release cycle).** If `[Unreleased]` was not > maintained per-change and the tag→HEAD span is huge (v0.6.0 was a 2-month, > 906-commit release with only 24 of ~234 feat/fix changes documented), do NOT > re-audit hundreds of commits by hand and do NOT dump raw commit subjects. > Generate the deduped input set — `git log ..HEAD --oneline` filtered > to `feat|fix|perf`, minus the change-tags already in `[Unreleased]` — then > **delegate grouped drafting to a subagent** (keeps quality high + your context > focused). Merge the returned bullets under the existing headings > programmatically (existing bullets first, new appended), scoped to the > `[Unreleased]` section only, and cap the long tail with one rolled-up > "Additional fixes" line. This is the exact path that worked for v0.6.0. ## Step 3 — Decide next version (SemVer) Propose per this decision tree, then **use AskUserQuestion to confirm**: | `## [Unreleased]` contains | Bump | |----------------------------------------------------|---------| | Any breaking change / removal (call it out) | major | | Any `### Added` bullet (new user-visible feature) | minor | | Only `### Fixed` / `### Changed` internals | patch | Current version `X.Y.Z` → propose `X.(Y+1).0` for minor, etc. **Do NOT auto-select** — always ask the user to confirm the target version (offer the proposal as default). ## Step 4 — Promote `## [Unreleased]` → versioned section In `CHANGELOG.md`: 1. Rename `## [Unreleased]` to `## [] - ` (use today's date from `date +%Y-%m-%d`, no leading `v`). 2. Insert a fresh empty `## [Unreleased]` section **above** it: ```markdown ## [Unreleased] ### Added ### Changed ### Fixed ## [] - ...existing bullets... ``` Verify afterwards with: ```bash grep -n "^## " CHANGELOG.md | head ``` ## Step 5 — Bump all workspace versions + sync inter-package dep specifiers ```bash npm version --workspaces --include-workspace-root --no-git-tag-version node scripts/sync-versions.js pnpm install --lockfile-only ``` The first command bumps the `version` field on the root + every workspace (`npm version` only edits package.json — no lockfile, no install — so it stays npm even under the pnpm migration). The second rewrites every inter-package `dependencies` specifier (e.g. `"@blackbelt-technology/pi-dashboard-shared": "^"`) to the new version. The third regenerates `pnpm-lock.yaml` so its recorded cross-ref specifiers match the bumped versions — without it, strict prerelease semver causes consumer installs to fall back to stale registry tarballs. The CI `tag-and-push` job runs the same three commands; doing it locally keeps the commit honest. See changes: fix-release-lockfile-drift, adopt-pnpm-for-dev-ci. > **Why the second step?** The npm CLI does not implement the `workspace:` > protocol (it's a pnpm/yarn feature). We use plain semver ranges and > synchronise them at bump time so the published tarballs have consistent > metadata. CI's `publish.yml` runs `sync-versions.js` defensively too, but > running it locally keeps the commit honest. > **Skew guard for `distill-session-knowledge` → `session-distiller`.** The > thin skill package `@blackbelt-technology/pi-dashboard-distill-session-knowledge` > deps on the engine `@blackbelt-technology/pi-dashboard-session-distiller`. > Both are non-private, so `npm publish -ws` publishes them in the SAME run > (engine first — `-ws` walks in topological/dependency order) and > `sync-versions.js` pins the dep specifier to the just-cut version. Never > publish one without the other; that is what prevents cross-package skew. Verify with: ```bash git diff --stat package.json packages/*/package.json pnpm-lock.yaml ``` Should show `version` bumps in `package.json` and every `packages/*/package.json` plus synchronised `@blackbelt-technology/pi-dashboard-*` dependency specifiers, plus a regenerated `pnpm-lock.yaml`. No other files. ## Step 6 — Commit ```bash git add CHANGELOG.md package.json pnpm-lock.yaml packages/*/package.json git commit -m "chore(release): v" ``` **Use AskUserQuestion (confirm)** before committing — show the user the exact message + file list. ## Step 7 — Tag and push ```bash git tag v git push origin develop git push origin v ``` **Use AskUserQuestion (confirm)** before pushing. Surface this warning: pushing the tag triggers the Release workflow immediately. Reverting requires `git push --delete origin v` + re-tag. ## Step 8 — Post-push instructions (print to user) Give the user this summary: ``` ✅ Tag v pushed. Next steps (human): 1. Watch CI: https://github.com/BlackBeltTechnology/pi-agent-dashboard/actions The Release workflow will: • publish every non-private workspace (~32 @blackbelt-technology/* packages via `npm publish -ws --include-workspace-root`) to npm • build Electron installers (macOS DMG × 2 — Apple Silicon + Intel, Linux DEB+AppImage, Windows NSIS+ZIP+portable per arch) • create a GitHub Release with artifacts + latest*.yml metadata. PRODUCTION tags (vX.Y.Z) publish immediately; PRE-RELEASE tags (vX.Y.Z-rc.N) land as a draft. 2. Open the release: https://github.com/BlackBeltTechnology/pi-agent-dashboard/releases 3. Verify the body (auto-extracted from CHANGELOG.md [] section) and all 7 platform artifacts are attached: • PI-Dashboard--arm64.dmg (Apple Silicon) • PI-Dashboard--x64.dmg (Intel) • pi-dashboard__amd64.deb (Linux x64) • pi-dashboard__arm64.deb (Linux arm64) • PI-Dashboard-.AppImage (Linux x64) • PI-Dashboard- Setup.exe + .zip + portable.exe (Windows x64) • .zip + portable.exe (Windows arm64) 4. PRODUCTION tag: the release is already published — nothing to click; `release: published` fires automatically and redeploys GitHub Pages. PRE-RELEASE tag: review the draft, then click "Publish release". If something is wrong, see `.pi/skills/release-revoke/SKILL.md`. ``` ## Step 9 — Drive the post-tag Release pipeline (the tag push is the START, not the end) Pushing the tag begins a gated pipeline in `publish.yml` that fails in ways you cannot see until release time. Both v0.6.0 and v0.6.1 needed MANY tag moves before a Release was published. Stay on it until `github-release` is green. **Pipeline shape (each is a gate; a failure before `github-release` means NO GitHub Release exists yet):** ``` release-gate ( ci-checks + 7-leg smoke ) → publish (npm, OIDC) → electron (6-leg matrix) → github-release ``` **Latent-bug warning:** the FIRST release where `publish` finally goes green exposes CI bugs that never ran before (v0.6.1's `electron` job had been silently skipped every prior cut because `publish` had never succeeded). Expect the electron/publish legs to surface never-before-exercised failures. ### Recovery loop (the normal rhythm) Fix on `develop` → **force-move the tag to the fix commit** → re-run. `npm publish` is idempotent (skips already-published packages), so a partial publish + tag move is safe. This is the expected loop **until a GitHub Release is published** — see the reconciled guardrail below. ```bash git commit ... && git push origin develop git tag -f v && git push -f origin v # re-triggers a clean single-pass run ``` ### Do NOT use `gh run rerun --failed` for gate failures GitHub does **not** re-dispatch skipped downstream *reusable-workflow* jobs (`electron`, `github-release`) on a `--failed` rerun — even after `publish` turns green. You get npm published but no installers / no Release, repeatedly. A fresh (or force-moved) **tag push** runs the pipeline top-to-bottom in one pass. `rerun` is fine ONLY for an isolated flaky leg whose downstream hasn't been reached yet (e.g. a single red smoke leg). ### Failure triage | Symptom | Class | Action | |---|---|---| | `ci-checks` red but all tests passed — vitest "Uncaught Exception" (`window is not defined`, react-virtual `setTimeout` after jsdom teardown, `ChatView.test.tsx`) | **flake** | re-run the `ci-checks` job | | One smoke leg: `ECONNRESET` / `network aborted` during `pnpm install`, or Windows "web UI not reachable" 5s timeout on a cold runner | **flake** | re-run just that leg | | `publish` install fails resolving the `@electron/node-gyp` git dep (`ERR_PNPM_EXOTIC_SUBDEP`) | **config** | `pnpm-workspace.yaml` must keep `blockExoticSubdeps: false`. (The old `npm@11.12.1` EALLOWGIT pin is GONE — post-migration the publish job installs with pnpm and upgrades to `npm@latest` only for the OIDC `npm publish --provenance` step. See change: adopt-pnpm-for-dev-ci §8.3.) | | `publish` 422 `Error verifying sigstore provenance bundle: repository.url is ""` | **metadata** | the offending non-private `package.json` is missing a `repository` block (url + `directory`). Add it, matching a sibling like `shared`. Pre-check: `for f in package.json packages/*/package.json; do node -e "const p=require('./$f'); if(!p.private && !p.repository) console.log(p.name)"; done` | | `publish` **E404** (not 403) on one package's `npm publish` | **human / npmjs.com** | Trusted Publisher not configured OR mismatched for THAT package. Since the other packages published with the same OIDC token, the config differs in one field. It must match EXACTLY: repo `BlackBeltTechnology/pi-agent-dashboard`, workflow **filename** `publish.yml` (NOT the display name "Release"), environment `npm-publish`. Web-UI action only — hand off to the user. | | `electron`/`github-release` skipped instantly (<1s) even though `publish` is green | **workflow-if** (fixed in-repo; watch for regressions) | a skipped `tag-and-push` in needs-ancestry poisons the default `if: success()`. Those jobs now carry explicit `if: !cancelled() && needs.publish.result == 'success'`. | | Windows electron leg: `koffi prebuild GO/NO-GO failed` | **guard-path** (fixed) | koffi 3.x ships per-platform packages (`@koromix/koffi-win32-x64/win32_x64/koffi.node`), not the koffi-2.x `koffi/build/...` path. Guard lives in `scripts/windows-liveness-smoke.ts`. | | Windows **arm64** electron leg: NSIS install smoke "pi-dashboard.exe not found after 150s" | **arch** (fixed) | an arm64 binary can't execute on the x64 GitHub runner; install/uninstall smoke must be skipped on arm64 (needs a windows-11-arm runner). | | The smoke matrix false-positives a stale import (e.g. a symbol deleted by an earlier PR still imported by a probe) | **latent develop bug** | fix on `develop`, force-move the tag. The 7-leg smoke only runs at release time, so these surface here. | ## Guardrails - **Never skip pre-flight.** A failing test or dirty tree means the release is not ready. - **If a gate-fix commit lands AFTER `chore(release)`, tag `HEAD`, not the release commit.** When the pre-tag smoke matrix (step 7) surfaces a latent `develop` bug, you fix it in a follow-up commit on top of `chore(release)`. The Release workflow re-runs the release-gate against the TAGGED tree, so the tag MUST include that fix — tag current `HEAD`. The version files (`0.6.0`) live in the ancestor `chore(release)` commit, so the tagged tree still carries the right version. Tagging the release commit instead would re-run the gate WITHOUT the fix and fail the publish (dangling tag). - **Production tags publish automatically** (electron-updater needs a published release). Only pre-release tags (`-rc.N`, `-beta.N`) stay drafts for manual review — never hand-edit a production release to draft. - **Force-moving the tag is the STANDARD post-tag recovery — UNTIL a GitHub Release is published.** Before `github-release` completes, no Release exists and `npm publish` is idempotent, so fixing a publish/electron-phase bug on `develop` and `git push -f origin v` to the fix commit is the expected loop (Step 9), not a violation. Once a GitHub Release IS published, STOP force-moving — surface the conflict and hand off to `release-revoke`. - **After tagging, always verify the tag SHA.** The dashboard git-polls and concurrent pi sessions can hold `.git/index.lock`; a blocked commit can silently drop your fix and leave the tag on a sibling session's commit. Confirm: `git rev-parse v` == the intended fix commit, and `git log -1 --oneline v` shows YOUR change — before watching the run. - **One version at a time.** If the user asks to release two versions in a row, run this skill twice. - **Respect the checkpoint in `docs/release-process.md`** — human clicks Publish, not the skill.