diff --git a/docs/superpowers/plans/2026-11-02-whats-new-release-ids.md b/docs/superpowers/plans/2026-11-02-whats-new-release-ids.md
new file mode 100644
index 0000000..c16df8a
--- /dev/null
+++ b/docs/superpowers/plans/2026-11-02-whats-new-release-ids.md
@@ -0,0 +1,605 @@
+# What's New — release numbers and commit IDs instead of dates — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** The What's New popup identifies every release by a release number and the commit SHA it shipped with, and shows no dates at all.
+
+**Architecture:** `src/lib/release-notes.ts` stays a hand-written static array. Its `version: string` (a date-shaped string) is replaced by `release: number` plus a new `commit: string`. `src/components/whats-new-dialog.tsx` renders "Release N" and the SHA in the slot the date used to occupy, and the date formatter is **deleted** so no code path can turn a date into text. The release number doubles as the localStorage "seen" marker.
+
+**Tech Stack:** Next.js (App Router), React, TypeScript (`strict: true`), Tailwind, vitest + @testing-library/react (jsdom), base-ui `Dialog`.
+
+**Design:** `docs/superpowers/specs/2026-11-02-whats-new-release-ids-design.md`
+
+## Global Constraints
+
+- **No dates in the popup.** Not the `date` field, not the version string, not a formatted date. The date formatter is deleted, not merely hidden.
+- The `date` field **stays** in `src/lib/release-notes.ts` as an authoring reference and is **never rendered**.
+- Release numbers are contiguous `1..N`, unique, and the array is ordered newest first. The next release after this change is **Release 6**.
+- Every release carries the short SHA of the last commit that landed in it.
+- The commit SHA is **plain text, never a link** — the Forgejo instance is on an internal host.
+- The release notes stay hand-written. No generator, no fetch, no changelog table.
+- The popup stays client-side and offline-capable: no server round-trip, no account state.
+- No date is removed anywhere else in the app. `BuildStamp`, order dates, invoice dates and every other date are untouched.
+- Stage by explicit path. **Never `git add -A`** — this repo carries unrelated untracked artifacts (`reports/`, `scripts/data/backups/`).
+- Commit and push to `main` when a task's tests pass.
+
+## Task order, and why it is this way
+
+Task 1 adds the new fields without removing the old one, so the component still compiles and the popup still behaves exactly as it does today. Task 2 switches the popup over and removes `version` in the same commit. Task 3 documents the convention and verifies the whole thing.
+
+Every task ends with the full suite green. Do not reorder them.
+
+---
+
+### Task 1: Give every release a number and a commit
+
+**Files:**
+- Create: `src/lib/release-notes.test.ts`
+- Modify: `src/lib/release-notes.ts`
+
+**Interfaces:**
+- Consumes: nothing.
+- Produces: `ReleaseNote` gains `release: number` and `commit: string`. `RELEASE_NOTES: ReleaseNote[]` (newest first) and `LATEST_RELEASE: ReleaseNote` (the same object as `RELEASE_NOTES[0]`) are unchanged in shape. `version: string` is still present — Task 2 removes it.
+
+- [ ] **Step 1: Write the failing test**
+
+Create `src/lib/release-notes.test.ts`:
+
+```ts
+import { describe, expect, it } from "vitest";
+
+import { LATEST_RELEASE, RELEASE_NOTES } from "./release-notes";
+
+describe("the release-note registry", () => {
+ it("numbers the releases 1..N with no gaps and no duplicates", () => {
+ const numbers = RELEASE_NOTES.map((r) => r.release);
+
+ expect(new Set(numbers).size).toBe(numbers.length);
+ expect([...numbers].sort((a, b) => a - b)).toEqual(
+ Array.from({ length: RELEASE_NOTES.length }, (_, i) => i + 1)
+ );
+ });
+
+ it("keeps the newest release at the top of the list", () => {
+ const numbers = RELEASE_NOTES.map((r) => r.release);
+
+ expect(numbers).toEqual([...numbers].sort((a, b) => b - a));
+ expect(LATEST_RELEASE).toBe(RELEASE_NOTES[0]);
+ });
+
+ it("gives every release a commit SHA", () => {
+ for (const release of RELEASE_NOTES) {
+ expect(release.commit).toMatch(/^[0-9a-f]{7,40}$/);
+ }
+ });
+
+ it("gives every release a headline and at least one item", () => {
+ for (const release of RELEASE_NOTES) {
+ expect(release.title.length).toBeGreaterThan(0);
+ expect(release.items.length).toBeGreaterThan(0);
+ }
+ });
+});
+```
+
+- [ ] **Step 2: Run the test to verify it fails**
+
+Run: `npx vitest run src/lib/release-notes.test.ts`
+
+Expected: FAIL. `r.release` and `r.commit` do not exist yet, so `numbers` is `[undefined, undefined, undefined, undefined, undefined]` and the numbering and SHA assertions fail.
+
+- [ ] **Step 3: Add the two fields to the interface**
+
+In `src/lib/release-notes.ts`, change the interface to:
+
+```ts
+export interface ReleaseNote {
+ /** Version marker. Bumped to re-notify operators. */
+ version: string;
+ /**
+ * Release number. 1 = the first release. This replaces the date-shaped
+ * `version` string and becomes the "seen" marker.
+ */
+ release: number;
+ /** Short SHA of the commit this release went out with. */
+ commit: string;
+ /**
+ * Authoring reference only — the day the release was written up.
+ * Deliberately never rendered: the popup carries no dates.
+ */
+ date: string;
+ /** One-line headline for the release. */
+ title: string;
+ /** The changes, one sentence each. */
+ items: string[];
+}
+```
+
+`version` stays for now. Task 2 removes it.
+
+- [ ] **Step 4: Fill in the release number and commit on every entry**
+
+In `src/lib/release-notes.ts`, give each of the five entries its `release` number and `commit` SHA. The array is newest first, so the numbers run 5 down to 1:
+
+```ts
+export const RELEASE_NOTES: ReleaseNote[] = [
+ {
+ version: "2026.11.01",
+ release: 5,
+ commit: "139ea25",
+ date: "2026-11-01",
+ title: "Delivery Pool, letterhead lock and a dark mode section",
+ items: [
+ "Delivery Pool: invoiced lines waiting to go out, one card per customer invoice. Assign a driver, upload the signed copy, and the delivery closes.",
+ "Quote letterheads are locked to the customer's company — one letterhead per company, so quotes always go out on the right paper.",
+ "The counter never sells at R0. Items without a price stay off the till, and customer lines go to the price list instead.",
+ "The sidebar now has a Dark Mode section with a light/dark switch. Theme colours live in Settings → Appearance.",
+ "This What's New popup — press it in the sidebar to see what changed.",
+ ],
+ },
+ {
+ version: "2026.10.30",
+ release: 4,
+ commit: "83aec14",
+ date: "2026-10-30",
+ title: "Collection Confirm gets a second pair of eyes",
+ items: [
+ "Collection Confirm: a Notes button on each PO card, so the driver photos can be checked against the PO before you confirm.",
+ "The AI never changes a price. A different cost on the invoice is flagged as a red Price tick for a person to settle.",
+ "Collection Pool: stock lines can go back to pending and be marked ready again.",
+ "Quotes now record who they were sent to.",
+ ],
+ },
+ {
+ version: "2026.10.28",
+ release: 3,
+ commit: "3eea87e",
+ date: "2026-10-28",
+ title: "Auto-push and a shortcut to AI settings",
+ items: [
+ "Collection Confirm can auto-push purple cards straight on to invoicing, with a two-day log of everything it moved.",
+ "The Settings page links straight to AI settings.",
+ ],
+ },
+ {
+ version: "2026.10.26",
+ release: 2,
+ commit: "b212d0f",
+ date: "2026-10-26",
+ title: "A phone board for the managers",
+ items: [
+ "/manager is a mobile board for the managers: every order and where it is, on a phone.",
+ "The preview popup is the same one the desk uses on /search.",
+ ],
+ },
+ {
+ version: "2026.10.24",
+ release: 1,
+ commit: "55cc7fa",
+ date: "2026-10-24",
+ title: "One POS matcher, AI-only quote matching",
+ items: [
+ "One size-aware matcher now drives the quote assistant, the order fill and the blue price check — so they can't disagree about an item.",
+ "The quote assistant prices only an exact match and shows the closest one instead when it isn't sure.",
+ "Quote matching is AI-only. The old rule-based scorer is retired; the AI links an order when it's sure and suggests otherwise.",
+ "The blue price check badge appears on both Process pages.",
+ ],
+ },
+];
+```
+
+Each `commit` is the **last commit that landed in that release**:
+
+| Release | Anchor commit | What landed there |
+|---|---|---|
+| 5 | `139ea25` | What's New button + Dark Mode section in the sidebar |
+| 4 | `83aec14` | quotes record `email_to` |
+| 3 | `3eea87e` | Collection Confirm auto-push with a 2-day log |
+| 2 | `b212d0f` | `/manager` mobile orders board |
+| 1 | `55cc7fa` | merge of `feat/ai-only-quote-match` |
+
+Titles, items and `date` values are carried over untouched. Only the identity is added.
+
+- [ ] **Step 5: Run the test to verify it passes**
+
+Run: `npx vitest run src/lib/release-notes.test.ts`
+
+Expected: PASS, 4 tests.
+
+- [ ] **Step 6: Run the whole suite to confirm nothing else moved**
+
+Run: `npx vitest run`
+
+Expected: PASS. The popup still renders exactly as before — Task 1 changes the data, not the screen.
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/lib/release-notes.ts src/lib/release-notes.test.ts
+git commit -m "refactor(release-notes): give every release a number and a commit
+
+The version strings are dates (\"2026.11.01\"), so a release has no
+identity that is not a date. Every entry now also carries a release
+number and the short SHA of the commit it went out with.
+
+The date-shaped version string is left in place for now — the popup
+switches over and drops it in the next commit, so this one stays
+green on its own.
+
+A test holds the numbering unique, contiguous and newest-first, and
+holds every commit to a real SHA."
+```
+
+- [ ] **Step 8: Push**
+
+```bash
+git push origin main
+```
+
+---
+
+### Task 2: The popup shows the number and the commit, and no date
+
+**Files:**
+- Modify: `src/lib/release-notes.ts`
+- Modify: `src/components/whats-new-dialog.tsx`
+- Modify: `src/components/whats-new-dialog.test.tsx`
+
+**Interfaces:**
+- Consumes: `ReleaseNote.release` (number) and `ReleaseNote.commit` (string) from Task 1.
+- Produces: `WhatsNewButton` — unchanged signature `({ className }: { className?: string })`. The "seen" marker written to `localStorage["wfdash-whats-new-seen"]` is now `String(LATEST_RELEASE.release)`, i.e. `"5"`.
+
+- [ ] **Step 1: Write the failing test**
+
+Replace the whole of `src/components/whats-new-dialog.test.tsx` with:
+
+```tsx
+import { fireEvent, render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it } from "vitest";
+
+import { WhatsNewButton } from "./whats-new-dialog";
+import { LATEST_RELEASE } from "@/lib/release-notes";
+
+const SEEN_KEY = "wfdash-whats-new-seen";
+
+/**
+ * Any date that could reach the popup: the ISO shape ("2026-11-01"), the
+ * "1 Nov 2026" shape, and the "November 1, 2026" shape.
+ */
+const DATE_PATTERN =
+ /\d{4}-\d{2}-\d{2}|\d{1,2}\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{4}|(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},?\s+\d{4}/i;
+
+describe("WhatsNewButton", () => {
+ beforeEach(() => {
+ window.localStorage.clear();
+ });
+
+ it("flags the latest release as new until it has been read", () => {
+ render(