---
name: generate-changes-page
description: Generate a temporary /changes/ review page for a multi-page rewrite. Reviewers use this page to leave per-item approve/reject/change-request decisions.
arguments: []
---
# Generate /changes/ review page (PinAppAI-ready)

When I've made significant content / copy / structural changes to multiple pages of a website (e.g., after a content migration, redesign, or major rewrite), generate a **temporary review page** at `/changes/` that lists every change as a reviewable item. The page must integrate with the **PinAppAI feedback widget** so reviewers can Approve / Reject / Request-change on each item individually.

> **You are a change-tracking AI. You DO NOT modify source files for any reason — that's the cardinal rule of this task.** Your only output file is the /changes/ page itself. You read source markup to understand it; you read git diffs to summarize them; you do not edit the originals. To get a unique selector for each reviewable item you walk the source markup as-is and combine tag + class + structural position + `:nth-of-type(N)` until exactly one element matches. See the **Deterministic selectors** section below for the exact procedure. A non-unique or fabricated selector silently breaks every rollback that ships from this review — take it seriously.

## Resolve the widget credentials (project_key + api_base)

The generated /changes/ page embeds a PinAppAI widget snippet that needs
both `project_key` (the `pk_...` value) and `api_base`. Resolve both
before generating the page.

**project_key override:** if the user pasted a `pk_...` value in their
message ("use project_key pk_AbCd..."), use it verbatim and SKIP the
slug resolution below — proceed straight to api_base. This is the rare
edge case (embedding for a project not in the authenticated workspace).

Otherwise, resolve the project from the environment, then derive the key:

{{include: _shared/project-resolution.md}}

Once the project slug is resolved, call
`get_project({project: "<resolved-slug>"})` and read the `project_key`
field — that's the `pk_...` value the widget snippet needs. The same
value is also exposed as `api_key` in the response for back-compat
(legacy field name). Use it as the effective project_key throughout
this prompt.

**api_base:** default to `https://api.pinappai.com`. Honor an explicit
override only if the user named one in their message ("use api_base
https://api.staging.pinappai.com").

**Convention for templates below:** when you see `<PROJECT_KEY>` or
`<API_BASE>` in HTML snippets later in this prompt, substitute the
resolved values from this section, not the literal placeholder text.

## When to create this page

- I tell you "generate /changes/" or "create a changes review page"
- I've just finished a multi-page rewrite and want client / team to review
- After applying a content doc → site translation across multiple pages

## Source of changes

Read recent changes from one of these, in order of preference:

1. **`.pinappai/last-applied.json` (preferred when present).** Read the file's
   `last_applied_at` ISO timestamp — written by the last `/pinappai:apply`
   run (or a legacy fix-changes / apply-decisions run). Find the first commit made at or after
   that timestamp (the boundary commit, i.e. the previous batch's
   resolution), and diff from there to HEAD:

       BOUNDARY_SHA=$(git log --since="<last_applied_at>" --reverse \
                      --format=%H | head -1)
       git log $BOUNDARY_SHA..HEAD

   The `--reverse | head -1` picks the FIRST commit at or after the marker
   timestamp. The resulting `git log` range EXCLUDES that commit (because
   `..` ranges are exclusive of the lower bound) — that's intentional: the
   boundary commit is the previous batch's resolution and must NOT appear
   as new content for review.

   **Exception — `"changes_page_pending": true` in the marker.** That flag
   means the last apply batch (or several) landed WITHOUT a `/changes/`
   page — the user answered `n` to apply's Case-A prompt — so those
   commits are applied but **unreviewed**, and the exclusive-boundary
   rationale does not apply to them. Scope from the recorded base instead:

       git log <batch_base_sha>..HEAD

   (`batch_base_sha` is the parent of the first declined batch's landing
   commit, so the range INCLUDES every declined batch.) After the page is
   generated and its items registered, rewrite the marker: set
   `last_applied_at` to now, keep a one-line `bundle_summary`, and DROP
   `changes_page_pending` + `batch_base_sha` — the pending debt is paid.
   Never generate from the timestamp boundary while the flag is set: that
   range is empty by construction (the marker landed inside the batch it
   points at) and would wrongly report "nothing to review".

2. The most recent merge commit on the current branch:
   `git log --merges -1 --pretty=format:%H` then `git show <hash>`
3. Commits since the branch diverged from `main`:
   `git log main..HEAD --oneline` then per-commit `git show <hash>`
4. If none applies (trunk-based dev, no branch, no marker), ask me which commits or files to summarize.

For each change write a 1-2 sentence summary of what's different from before. If **visible** (copy edit, color, layout), include before / after snippets. If **behavioral** (new rule, sort order), describe the new behavior.

## File location (auto-detect by stack)

- Astro: `src/pages/changes.astro`
- Next.js (App Router): `app/changes/page.tsx`
- Next.js (Pages Router): `pages/changes.tsx`
- Eleventy: `src/changes.html` (or wherever the pages collection lives)
- Vite / static: `public/changes.html` or `changes.html` adjacent to `index.html`
- Hugo: `content/changes.md` with appropriate template
- WordPress: page template `page-changes.php`
- Plain HTML: `changes.html` at project root accessible via routing

If unsure which stack or where pages live, **ask first**.

## Page must be standalone

The normative page-level rules live in the shared skeleton block below
(same block every producer prompt embeds — keep them in sync by editing
`_shared/changes-page-skeleton.md`, not per-prompt copies). Additional
notes specific to this prompt: don't add the page to sitemap.xml (Astro
/ Next: exclude from sitemap generation), and see the "Generated stamp"
section below for the stamp's exact HTML.

{{include: _shared/changes-page-skeleton.md}}

## Structure: gather changes by page

For each significantly-changed page, group changes into sections:

```html
<h2 id="{page-slug}">{page-url}</h2>
<div class="meta">
  <strong>Source:</strong> <a href="{spec-url}">spec / doc</a> ·
  <strong>Files:</strong> <code>{relative-paths}</code>
</div>

<!-- Optional: structural removals first (deletions, reordering, removed sections) -->
<h3><span class="pill">Structural</span></h3>
<ul class="compact">
  <li><strong>{What was deleted}</strong> — <em>{why}</em></li>
</ul>

<details>
  <summary><strong>All text changes ({N})</strong></summary>

  <!-- Each atomic decision unit goes inside .pp-change-item.
       The widget reads two MANDATORY attributes at decision time and ships
       them with every rejection / change-request to the Approvals export:
         data-pp-page-url       — root-relative URL of the actual page
         data-pp-source-selector — CSS selector to the element on the SOURCE
       These are how the eventual Claude-prompt-on-paste pinpoints the exact
       element to roll back / modify. WITHOUT them, short snippets like
       "Save" or "Submit" match dozens of files and the prompt is unsafe.
       data-pp-source-files is optional but recommended. -->
  <div class="pp-change-item"
       data-pp-item="{page-slug}:{slugified-title}"
       data-pp-title="{page-slug} · {N}. {Title}"
       data-pp-page-url="{root-relative URL of the actual page, e.g. /tr/features/asset-management/ — NOT this /changes/ URL}"
       data-pp-source-selector="{CSS selector to the element on the SOURCE page, e.g. main > section.hero > h1.hero-title}"
       data-pp-source-files="{optional comma-separated source-repo file paths, e.g. src/pages/tr/features/asset-management.astro}">
    <div class="change-num">1. {Section / Field name}</div>
    <div class="change-row b"><div class="lbl">Before</div><div class="val">{old text}</div></div>
    <div class="change-row a"><div class="lbl">After</div><div class="val">{new text}</div></div>
  </div>

  <div class="pp-change-item"
       data-pp-item="{page-slug}:{another-slugified-title}"
       data-pp-title="{page-slug} · 2. {Title}"
       data-pp-page-url="..."
       data-pp-source-selector="..."
       data-pp-source-files="...">
    <div class="change-num">2. {next item}</div>
    <div class="change-row b">...</div>
    <div class="change-row a">...</div>
  </div>

  <!-- Continue: ONE wrapper for every numbered change. No exceptions. -->
</details>

### ⚠️ MANDATORY locator attributes — `data-pp-page-url` + `data-pp-source-selector`

Every `.pp-change-item` MUST carry both. The widget records them at decision time; the Approvals → Claude-prompt export hands them to the next AI as the **primary** locator. Without them the next-AI has no choice but to grep the whole repo for snippet text — and a one-word snippet like "Save" or "Submit" matches dozens of unrelated places, so the rollback applies to the wrong element. **Non-negotiable.**

**`data-pp-page-url`** — the URL of the actual page where this change lives, **not this /changes/ URL**. Use the **root-relative** form (e.g. `/tr/features/asset-management/`, `/pricing`, `/blog/2026-01-launch/`). Read it from the page's frontmatter / route definition in the source. Required per-item even when the page-section `<h2 id>` already names the page — keeps each item self-contained.

**`data-pp-source-selector`** — a CSS selector that uniquely identifies the element being changed **in the source markup of that page**. The selector MUST resolve to exactly one element when applied against that source file. Walk the resolution rules below.

**`data-pp-source-files`** (optional) — comma-separated source-repo paths that contain this element. Useful as a per-item override; the page-meta block usually carries the page-level file list already.

---

### Deterministic selectors — non-invasive only

**You do not modify source files. Period.** No new classes, no new ids, no new attributes, no markup refactors. Source is read-only for this task — the only file you write is the /changes/ page itself. The selector you put in `data-pp-source-selector` must work against the source **as it exists right now**.

For every reviewable item, after you've identified the changed element in the source markup, walk the rules below in order. The first one that **uniquely resolves to that one element** wins.

**1. Existing unique anchor** (best — use whenever it exists)

If the element already has an `id`, a class that appears nowhere else in the page, or a uniquely-identifying attribute, use it:
- `#hero-title` — id
- `h1.hero-title` — class that's the only one of its name on the page
- `[data-testid="cta-primary"]` — single matching attribute
- `section[role="banner"] > h1` — combined tag + role attribute

**2. Stable structural path** (next-best — most pages have enough structure for this)

Build a descendant chain combining tag + class + position. The chain is "stable" when the surrounding markup carries semantic anchors a human author chose deliberately (`<main>`, `<header>`, `<section class="hero">`, `<form data-form="contact">`):
- `main > section.hero > h1` — uses the hero section's class as the anchor
- `form[data-form="contact"] > button[type="submit"]` — attribute + type narrowing
- `main > article > footer p` — semantic-tag chain

**3. Sibling counting with `:nth-of-type(N)`** (when 1 and 2 don't disambiguate)

When the source markup truly has no distinguishing class / attribute / semantic anchor — e.g. the page is three identical `<section>` blocks each with an `<h1>` — fall back to **counting occurrences** within the parent. Use `:nth-of-type(N)` (1-indexed, counts same-tag siblings only) — it's the standard CSS way to express "the Nth occurrence of this tag among its siblings":

- `main > section:nth-of-type(2) > h1` — the h1 inside the 2nd section in main
- `ul.feature-list > li:nth-of-type(3) > strong` — the strong inside the 3rd `<li>`
- `body > main > section:nth-of-type(1) > div:nth-of-type(2) > h1:nth-of-type(1)` — full positional path when nothing else exists

Prefer `:nth-of-type` over `:nth-child` — `:nth-of-type` only counts same-tag siblings, so adding a `<hr>` or `<p>` between sections won't shift your index.

**4. ❌ Never do these:**
- **Modify the source.** Don't add classes, ids, attributes, or markers. Don't refactor markup. Source files are off-limits.
- **Generic tag selectors** (`h1`, `button`, `p`) — match dozens; useless on their own.
- **Omit `data-pp-source-selector` entirely** — every item must carry one. If after walking 1–3 you genuinely can't write a unique CSS selector, surface it: emit your best-effort selector AND add a `data-pp-selector-warning` attribute saying why you're unsure (e.g. `data-pp-selector-warning="ambiguous: 3 identical h1s under main, used :nth-of-type(2) by document order"`). Don't silently ship a fragile selector.
- **`:contains()` and other non-CSS-standard pseudos** — downstream tools may not support them.
- **Fabricate** a selector you didn't actually verify against the source — the rollback will silently apply to the wrong element.

---

### Self-check before you ship

Before declaring the /changes/ page done, run this audit. **Do not skip it.** A selector that doesn't resolve uniquely is worse than no selector — it makes the rollback prompt confidently wrong.

1. For every `.pp-change-item`, take the `data-pp-source-selector` value and **mentally apply it** to the indicated source file's markup tree. Confirm it resolves to **exactly one element**, and that element is the one being changed.
2. `data-pp-page-url` is set on every item, root-relative form (e.g. `/tr/features/asset-management/`).
3. Every selector you wrote uses only standard CSS (tag, class, id, attribute, `>`, descendant space, `:nth-of-type(N)`, `:first-child`, `:last-child`).
4. Any item you couldn't pin down to exactly one element carries a `data-pp-selector-warning` attribute explaining why.
5. **No source files were modified.** Only the /changes/ page was written.

Report at the end: `Audit: N items. All selectors unique-match in source. M items flagged with data-pp-selector-warning.` If anything failed audit point 1 (selector matches zero or 2+ in source) and you can't disambiguate within the rules above, list it and ASK — don't ship a wrong selector.
```

The normative per-item contract below is shared verbatim with the
`/pinappai:apply` prompt (which also generates `/changes/` items). The template block above
must stay consistent with it — if they ever disagree, the shared contract
wins.

{{include: _shared/changes-item-contract.md}}

## ⚠️ MANDATORY: One wrapper per atomic change. No grouping. No shortcuts.

This is the most common failure mode of this prompt — the AI gets tired and starts grouping changes or punting to git diff. Read this section carefully.

**Every numbered change MUST be inside its own `<div class="pp-change-item">` wrapper.** The reviewer needs to be able to Approve / Reject / Request-change *each item individually* — that requires a separate wrapper per item. Bundling defeats the entire point.

### ❌ Forbidden patterns (do NOT do any of these)

**1. Multi-item ranges in a single change-num** — e.g., `5–8. Why card headings + descriptions + Problem H2` covering four conceptually distinct decisions:

```html
<!-- WRONG: bundled, no wrapper, no decision bar -->
<div class="change-num">5–8. Why card headings + descriptions + Problem H2/description/4 items</div>
<p class="small">(Detail: compare with the previous HTML — see git diff)</p>
```

This bundles four reviewable items into one un-wrapped paragraph that points the human to git diff. The reviewer cannot decide on items 5, 6, 7, 8 individually — and there is no decision bar at all because there is no `.pp-change-item` wrapper.

```html
<!-- CORRECT: four separate wrappers, one per decision -->
<div class="pp-change-item" data-pp-item="page:why-card-1-heading" data-pp-title="page · 5. Why card 1 heading">
  <div class="change-num">5. Why card 1 heading</div>
  <div class="change-row b"><div class="lbl">Before</div><div class="val">old text</div></div>
  <div class="change-row a"><div class="lbl">After</div><div class="val">new text</div></div>
</div>
<div class="pp-change-item" data-pp-item="page:why-card-2-heading" data-pp-title="page · 6. Why card 2 heading">
  <div class="change-num">6. Why card 2 heading</div>
  ...
</div>
<!-- 7, 8 — separate wrappers each -->
```

**2. "See git diff" placeholder paragraphs** instead of actual before/after content:

```html
<!-- WRONG -->
<p class="small">(Detail: see git diff for full comparison)</p>
<p>For all 12 hero copy changes refer to the source file.</p>
```

If you don't have time to enumerate every change, **enumerate fewer changes but expand each one fully** rather than punt to git. The page is supposed to *replace* the need for git diff, not point at it.

**3. "And X more changes" implicit ellipsis** — every change you mention must appear in the page with a wrapper, full before / after, even if there are 50 of them:

```html
<!-- WRONG -->
<div class="pp-change-item">...hero-h1...</div>
<div class="pp-change-item">...hero-sub...</div>
<p>... and 23 other minor copy edits across the page.</p>
```

Either include all 25 wrappers, or filter the list and document what you filtered (e.g., "Excluded: 23 single-character typo fixes — out of review scope, see commit `abc1234` for the raw list"). Do not silently elide.

**4. Bundling structural removals into the text-changes `<details>` block** — structural removals (sections deleted, reordering) belong in the `<ul class="compact">` Structural list ABOVE the `<details>`. They are page-level decisions, OK to leave un-wrapped. But text changes inside the `<details>` block ALL need wrappers, no exceptions.

**5. Headline count claims that don't match the wrappers below them** — this is the subtlest failure mode. If a section's `<summary>` (or any header) promises "All text changes (25 items)", you MUST emit exactly 25 `<div class="pp-change-item">` wrappers in that section. Dropping items while keeping the count number intact is the worst case: the coverage check (change-num count vs wrapper count) still passes because both are dropped together, but the page lies to the reader about scope.

```html
<!-- WRONG: claims 25, only 11 wrappers below -->
<summary><strong>All text changes (25 items)</strong></summary>
<div class="pp-change-item">...item 1...</div>
<div class="pp-change-item">...item 2...</div>
...
<div class="pp-change-item">...item 11...</div>
</details>
<!-- items 12-25 silently missing -->
```

Three correct paths if you can't (or don't want to) wrap every item:

```html
<!-- CORRECT A: wrap all 25 -->
<summary><strong>All text changes (25 items)</strong></summary>
<!-- 25 wrappers, no exceptions -->

<!-- CORRECT B: drop the count from the headline -->
<summary><strong>All text changes</strong></summary>
<!-- N wrappers, no count promise to violate -->

<!-- CORRECT C: filter explicitly, document the filter -->
<summary><strong>Reviewable text changes (11 of 25)</strong></summary>
<p class="note">Excluded 14 items: 12 punctuation-only fixes and 2 identical CTA renames — out of review scope. See commit abc1234 for the raw list.</p>
<!-- 11 wrappers -->
```

Never silently drop while keeping the headline count. The headline count is a contract with the reviewer.

**6. Bundling multiple distinct fields of the same UI element into one wrapper** — even when fields belong to the same logical block (a card with title + description, a section with H2 + lead paragraph + bullet list), each field that can be independently Approved / Rejected MUST get its own wrapper.

```html
<!-- WRONG: bundles two distinct decisions -->
<div class="pp-change-item"
     data-pp-item="page:why-card-2-title-and-description"
     data-pp-title="page · 6. Why card 2 title + description">
  <div class="change-num">6. Why card 2 title + description</div>
  <div class="change-row b"><div class="lbl">Before title</div><div class="val">Old title</div></div>
  <div class="change-row a"><div class="lbl">After title</div><div class="val">New title</div></div>
  <div class="change-row b"><div class="lbl">Before desc</div><div class="val">Old desc</div></div>
  <div class="change-row a"><div class="lbl">After desc</div><div class="val">New desc</div></div>
</div>
```

```html
<!-- CORRECT: two separate wrappers -->
<div class="pp-change-item"
     data-pp-item="page:why-card-2-title"
     data-pp-title="page · 6a. Why card 2 title">
  <div class="change-num">6a. Why card 2 title</div>
  <div class="change-row b"><div class="lbl">Before</div><div class="val">Old title</div></div>
  <div class="change-row a"><div class="lbl">After</div><div class="val">New title</div></div>
</div>
<div class="pp-change-item"
     data-pp-item="page:why-card-2-description"
     data-pp-title="page · 6b. Why card 2 description">
  <div class="change-num">6b. Why card 2 description</div>
  <div class="change-row b"><div class="lbl">Before</div><div class="val">Old desc</div></div>
  <div class="change-row a"><div class="lbl">After</div><div class="val">New desc</div></div>
</div>
```

The reviewer might approve the new title but reject the new description (or vice versa). Bundling forces a single decision they cannot disambiguate.

A "Problem H2 + intro paragraph + 4 bullet points" block is **6 distinct decisions**, not 1. Wrap each. Same for "card title + card description" (2 decisions per card, not 1). Same for "FAQ question + FAQ answer" (2 decisions per item, not 1).

If you find yourself naming an ID with `+` or `-and-` joining concepts (e.g., `title-and-description`, `h2-aciklama-4-madde`), that's a strong smell that you're bundling. Refactor into separate wrappers.

**7. List-form bundling vs holistic-block bundling — the judgment call.** When a section contains N parallel items of the same kind (cards, bullets, FAQ Q&As, metrics), you have to decide: is each item a separate decision, or is the whole block one editorial decision?

**The decision rule — ask yourself:**

> "Could the reviewer reasonably approve some items in this block while rejecting others?"

- **Yes → split into N wrappers.** Items are independent claims; reviewer's mental model is per-item.
- **No → one wrapper for the whole block.** Items have no independent identity; reviewer's mental model is "this new block vs the old block."

**Strong signals each item is a separate decision (→ split):**

- The block has a clear 1-1 before→after mapping. Old card 1 became new card 1, old card 2 became new card 2. Each pair is its own edit.
- Items are independent claims a reviewer could agree with selectively. Three "Why this matters" cards — the reviewer might love card 1's new framing but find card 3's wording weak. Three product feature pillars. Three pricing tier names. Three navigation labels.
- Removing one item leaves the others meaningful and unchanged. If you can drop card 2 and cards 1 and 3 still make sense as-is, they're independent.

**Strong signals the block is one holistic decision (→ one wrapper):**

- No 1-1 mapping. Old block had 3 questions, new block has 5 questions, and the new ones aren't rewordings of the old ones — the entire question set was reconceived. Forcing per-question pairs is artificial.
- Items only make sense as a set. A 6-bullet feature list rewritten end-to-end as a coherent narrative — bullet 4 in isolation doesn't carry the argument, the bullets only work together.
- Reviewer's mental model is "should this whole block replace the whole old block?" If the natural reaction is "I want to keep some of the old content," the reviewer can use the **Request changes** button with a comment listing specifically what to keep. They don't need 5 separate Approve/Reject buttons.
- The change is structural at the block level: section added, section deleted, section completely rewritten with no shared lineage.

**When in doubt, ask: would 5 reviewer clicks of "Approve" all in a row, all on items they've never seen before, give the team any more information than 1 click on the whole block?** If no, you're forcing busywork. Bundle.

### Examples (use these as templates for the judgment)

**Example A — three Why cards, each with a 1-1 before/after pair → SPLIT into 3 wrappers**

```html
<!-- Before: card 1 "Reliability", card 2 "Cost savings", card 3 "Compliance" -->
<!-- After:  card 1 "Asset visibility", card 2 "Lower TCO", card 3 "Audit-ready" -->
<!-- Each card title is an independent value-prop claim. Reviewer might approve
     card 1 and card 2's new framing but reject card 3 as too jargony. -->

<div class="pp-change-item" data-pp-item="page:why-card-1-title" data-pp-title="page · 5a. Why card 1 title">
  <div class="change-num">5a. Why card 1 title</div>
  <div class="change-row b"><div class="lbl">Before</div><div class="val">Reliability</div></div>
  <div class="change-row a"><div class="lbl">After</div><div class="val">Asset visibility</div></div>
</div>
<!-- 5b for card 2, 5c for card 3 — three wrappers total -->
```

**Example B — FAQ block where the whole question set was reconceived → ONE wrapper**

```html
<!-- Before: 3 questions about UI mechanics ("How do I add a record?") -->
<!-- After:  5 different questions about strategy ("What is X used for?")
     The new questions are NOT rewordings of the old ones; the FAQ was
     redesigned as a different content artifact. Per-question pairing is
     impossible (3 ≠ 5) and per-question approval doesn't help — the
     reviewer's question is "is the new FAQ better positioned than the old
     FAQ?", not "do you approve question 4 in isolation?". -->

<div class="pp-change-item" data-pp-item="page:faq-section-rewrite" data-pp-title="page · 11. FAQ — full rewrite">
  <div class="change-num">11. FAQ — full block rewrite (3 questions → 5 new questions)</div>
  <div class="change-row b"><div class="lbl">Before</div><div class="val">[old 3 questions, listed]</div></div>
  <div class="change-row a"><div class="lbl">After</div><div class="val">[new 5 questions, listed]</div></div>
  <p class="small">Holistic block rewrite. To request changes to specific questions, use the Request changes button.</p>
</div>
```

The reviewer who wants Q3 reworded clicks **Request changes** and writes "Q3 too long, shorten." They don't need 5 separate buttons.

**Example C — same FAQ block, but each new question is a clear reword of the corresponding old one → SPLIT into N wrappers**

```html
<!-- Before: Q1 "How does AI help maintenance?" -->
<!-- After:  Q1 "How does AI-driven maintenance work?" (same intent, reworded) -->
<!-- 1-1 mapping is clear; reviewer might approve Q1's reword but reject Q2's
     reword as losing the original meaning. Split. -->

<div class="pp-change-item" data-pp-item="page:faq-q1-reword" data-pp-title="page · 11a. FAQ Q1 reworded">
  <div class="change-num">11a. FAQ Q1 (reworded)</div>
  <div class="change-row b"><div class="lbl">Before</div><div class="val">How does AI help maintenance?</div></div>
  <div class="change-row a"><div class="lbl">After</div><div class="val">How does AI-driven maintenance work?</div></div>
</div>
<!-- 11b for Q2 reword, 11c for Q3 reword -->
```

**Example D — section with H2 + intro paragraph + 4 bullets, each independently editable → SPLIT into 6 wrappers**

The H2 is a heading claim, the intro is an argument paragraph, each bullet is a discrete supporting point. Reviewer might keep the H2, accept the intro, but reject 2 of the 4 bullets. These are independent decisions.

### Smell checks before submitting

Look at every wrapper's `change-row .val` content:

- If a value contains `·`, `/`, or "+" separating what are clearly **independent items the reviewer might evaluate separately** → that's bundling, split it.
- If a value contains a numbered or bulleted list (`1. ... 2. ... 3. ...`) where each item is **an independent claim** → split it. (If the list is a single coherent argument that only works as a sequence, one wrapper is fine.)
- If the wrapper's `change-num` title uses words like "consolidated", "all of X", "(N items)", "(N rewritten)" — pause and apply the decision rule above. These titles often signal bundling that should be split, but not always (a holistic block rewrite legitimately uses such phrasing).

### Acceptable bulk simplification

If a literal edit (same string → same string) repeats in N physical locations on the page (e.g., a brand rename applied to 5 identical heading occurrences), one wrapper with a clear "Bulk:" prefix is fine:

```html
<div class="pp-change-item" data-pp-item="page:bulk-brand-rename"
     data-pp-title="page · Bulk · 'OldBrand' → 'NewBrand' (5 locations)">
  <div class="change-num">Bulk · 'OldBrand' → 'NewBrand' (5 locations)</div>
  <div class="change-row b"><div class="lbl">Before</div><div class="val">OldBrand</div></div>
  <div class="change-row a"><div class="lbl">After</div><div class="val">NewBrand</div></div>
  <p class="small">Affected: H1, H2 (3x), footer CTA. Same literal edit applied uniformly.</p>
</div>
```

This is for *literal-string* replays, not for "five different headings that all happened to change." Different edits = different decisions, even if stylistically related.

### Coverage verification (do this before claiming done)

After generating, count two numbers and verify they match:

- `grep -c '<div class="change-num">' changes.html` — total numbered enumeration
- `grep -c '<div class="pp-change-item"' changes.html` — total decision-bar wrappers

If the numbers differ, you have orphan change-nums that won't get a decision bar. Wrap them and re-count. **Include both counts in your ID preservation report** (see "Regenerating" section below) so the human can verify you didn't shortcut.

## ID convention (important — read this section twice)

Every atomic decision unit MUST be wrapped in `.pp-change-item` with both attributes:

- `data-pp-item`: `{page-slug}:{slugified-title-max-50-chars}`
  - `page-slug`: the source page (lowercase, alphanumeric + dash)
  - `slugified-title`: derived from the change content itself — lowercase, non-alphanumeric → dashes, trimmed. Max 50 chars.
  - Examples: `asset-management:hero-h1`, `pricing:trust-signals-2026`, `about:team-section-leadership`
  - **No positional counters** (`c1`, `c2`, `c3` — these were used in older prompts but they shift when items are reordered or removed, which orphans decisions. Content-derived slugs are reorder-safe.)
  - **Disambiguation if two slugs collide:** append a meaningful qualifier, NOT a counter. `hero-h1-mobile` vs `hero-h1-desktop`, not `hero-h1-1` vs `hero-h1-2`.

- `data-pp-title`: `{page-slug} · {N}. {Title}` — human-readable, appears in popover headers and admin export. The `{N}` here is just visual ordering for the human reading the export; it's NOT in the matching ID, so changing it doesn't break decision matching.

### Optional third attribute — `data-pp-applied-cr-id` (recommended when generating /changes/ inside the Apply flow)

If you're generating /changes/ inside a `/pinappai:apply` session (Step 7 of that prompt runs you here), each /changes/ item corresponds 1:1 to one of the CRs you just applied via `pinappai_apply_change_requests`. Tag each item with the originating CR's id:

```html
<div class="pp-change-item"
     data-pp-item="page:hero-headline"
     data-pp-title="page · 1. Hero headline"
     data-pp-applied-cr-id="cr_abc123">
  ...
</div>
```

**Why it matters**: when a reviewer rejects / change-requests this item on /changes/, the widget passes `applied_cr_id` to the server. The server uses it to transition THAT exact CR (in_review → inbox_rejected / inbox_change_requested) instead of falling back to the legacy `target_item_id` heuristic, which can spawn a duplicate CR when widget-pin and /changes/-slug namespaces disagree.

**When to omit**: standalone runs (no preceding Apply), hand-authored items, or items that genuinely represent fresh dev-authored changes not derived from any prior CR. The server then falls through to the legacy match path and spawns a fresh CR — the correct path for those cases.

**Don't fabricate cr_ids**: only set this attribute when you have a real `cr_id` from the apply-batch result you just produced (or from an explicit user instruction). If you're not sure, leave the attribute off.

## Regenerating an existing /changes/ page (most important section)

**If `/changes/` (or your target path) already exists in the repo**, your job is to PRESERVE existing decisions. Decisions are matched by `data-pp-item` value in the database — if you regenerate the page with new IDs for items that previously existed, the old reviewer decisions become orphaned and silently disappear from the UI.

Do this every time:

1. **Read the existing file first** (`/changes/` or whatever path you're regenerating) before generating the new one.
2. **Extract every `data-pp-item="..."` value** and the change content (before / after text, title) it was attached to.
3. **For each change in the new content:**
   - If the change matches an existing one (same before / after, or recognizably the same item with minor wording shifts) → **reuse the existing `data-pp-item` value verbatim**. Do not re-slugify even if the title changed slightly.
   - If the change is genuinely new (no equivalent in the old page) → generate a fresh `data-pp-item` using the slug convention.
4. **For changes removed from the new content:** drop them from the page (no orphan rows). Their decisions remain in the database for audit but won't appear in the regenerated UI.
5. **Overwrite the `<p class="generated-stamp">` with NEW current local + UTC time** (run `date "+%Y-%m-%d %H:%M %z (%Z)"` and `date -u "+%H:%M UTC"` again — do NOT carry the old timestamp forward, do NOT keep the old date if the calendar day changed). This is non-negotiable: reviewers correlate decisions to a generation by reading this stamp, so a stale stamp on a regenerated page is a correctness bug. Confirm in your ID-preservation report (below) that the stamp was refreshed.

After generating, **report at the top of your response** (NOT in the page itself):

```
ID preservation report:
- Preserved: 12 IDs from previous version
- New: 3 items added with new IDs
- Removed: 1 item (was: asset-management:hero-h1-old) — its decisions remain in the database for audit
- Stamp refreshed: previous "Generated: 2026-04-30 09:14 +0300 (Europe/Istanbul) · 06:14 UTC"
                   →  current "Generated: 2026-05-01 17:32 +0300 (Europe/Istanbul) · 14:32 UTC"

Coverage report:
- change-num blocks: 154
- .pp-change-item wrappers: 154
- ✅ Match — every numbered change has its own wrapper

Headline count audit (per section):
- asset-management: summary "(25 items)" — 25 wrappers ✅
- ai-powered-maintenance: no count promised — 12 wrappers
- vendor-management: summary "(9 items)" — 9 wrappers ✅
- (... one line per section ...)

Self-rejection audit (must all be ✅ or output is invalid):
✅ 1. Coverage:   change-num=154, pp-change-item=154
✅ 2. Headlines:  31 sections promise counts, 31 match (or counts dropped / filters documented)
✅ 3. Banner:     page-header total = 154 matches global wrapper count, no quality buzzwords
✅ 4. List-form:  every parallel-item section judged correctly (independent items split, holistic blocks bundled)
✅ 5. Multi-field: no wrappers bundle two distinct 1-1-mapped fields of the same UI element
```

If any audit row mismatches, fix the page before reporting:

```
- change-num blocks: 25
- .pp-change-item wrappers: 18
- ❌ Mismatch — 7 numbered changes are missing wrappers. Fix and re-run.

Headline count audit:
- asset-management: summary "(25 items)" — only 11 wrappers ❌
  → Either wrap the missing 14, drop the "(25 items)" claim, or document the filter explicitly. Fix and re-run.
```

If you can't read the existing file (first generation, or it's gone) → just generate fresh IDs. Note in the report that this is a first-generation run.

## ⛔ Self-rejection protocol (DO NOT skip this — it is the contract)

After you generate the page and BEFORE you write the response containing the file, run these checks. If a check fails, **fix it and regenerate before submitting** — do NOT submit a known-failing output with a "❌ failed, please regenerate yourself" report. You regenerate, not the human.

Run these in order:

1. **Coverage match.** `grep -c '<div class="change-num">'` must equal `grep -c '<div class="pp-change-item"'`. If not equal → fix orphan change-nums and regenerate.

2. **Headline count honesty.** For every `<summary>` or header that claims `(N items)` / `(N changes)` / a numeric total: count actual `.pp-change-item` wrappers in that section. The two must be equal. If a section mismatches, choose one of the three CORRECT paths from forbidden-pattern #5 (wrap all N, drop the count, or document the filter explicitly) — then regenerate.

3. **Banner / page-header claim audit.** If you wrote a header line like "X pages · Y atomic changes" or any total in the `<h1>` / banner / meta line, that total must equal the actual page-wide `.pp-change-item` count. If it doesn't, fix the count or remove the claim. **Do NOT use buzzwords like "single-field strict" / "all atomic" / "fully decomposed" / "Rule #N strict" in the header.** These are claims about your own output that you cannot self-verify; if a reviewer audits and finds violations, the buzzword becomes a lie that erodes trust in the whole page. Just describe what's there ("31 pages, 200 changes") without quality claims.

4. **List-form bundling sanity check.** For every section that contains parallel items of the same kind (cards, FAQ Q&As, bullets, metrics), verify you applied the decision rule from forbidden-pattern #7:
   - Independent claims (cards with 1-1 before→after pairs, navigation labels, pricing tiers) → split into N wrappers.
   - Holistic block rewrites (FAQ rewritten end-to-end with no 1-1 mapping, bullet list reconceived as a coherent narrative) → one wrapper with the full before/after content shown.

   This is a judgment call, not a syntactic rule. The smell test: read the wrapper's `change-row .val` values aloud. If you naturally pause at `·` or `/` or list separators and the segments before/after the pause are *each* something a reviewer might evaluate independently — split. If the segments only make sense together as a sequence — bundle is fine.

5. **Multi-field bundling check (forbidden #6).** Distinct fields of the same UI element with a 1-1 before→after pair (a card's title AND its description, an H2 AND its intro paragraph) get separate wrappers. ID names containing `+`, `-and-`, or joining two field names with a separator (`title-and-description`, `h2-and-subhead`) are smells of this specific bundling — refactor.

If all five pass, write the response. The response must include this audit table verbatim:

```
Self-rejection audit:
✅ 1. Coverage:   change-num=N, pp-change-item=N
✅ 2. Headlines:  X sections promise counts, all match (or counts dropped / filters documented)
✅ 3. Banner:     page-header total matches global wrapper count, no quality buzzwords
✅ 4. List-form:  every parallel-item section judged correctly (split for independent items, bundled for holistic blocks)
✅ 5. Multi-field: no wrappers bundle two distinct 1-1-mapped fields of the same UI element
```

If you find yourself tempted to write "❌ Failed, please regenerate" → instead, **you** regenerate. The human's review job is reviewing change content, not catching your bundling failures.

## CSS (paste into `<style>` block — already polished, just paste)

```css
:root {
  --brand: #E30A17; --brand-hover: #B8081A;
  --ink: #1f2937; --muted: #6b7280;
  --bg: #f9fafb; --bg-alt: #f3f4f6; --br: #e5e7eb;
  --success: #16a34a; --danger: #b91c1c;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  color: var(--ink); background: #fff; line-height: 1.55;
}
.wrap { max-width: 1100px; margin: 0 auto; padding: 32px 24px 80px; }
.banner {
  background: #fff7ed; border: 1px solid #fed7aa; border-radius: 12px;
  padding: 14px 16px; margin-bottom: 28px; font-size: 14px; color: #7c2d12;
}
.generated-stamp {
  font-size: 12.5px; color: var(--muted); margin: -16px 0 24px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.generated-stamp strong { color: var(--ink); font-weight: 600; }
h1 { font-size: 30px; margin: 0 0 8px; letter-spacing: -0.01em; }
h2 {
  font-size: 20px; margin: 36px 0 10px; padding-top: 24px;
  border-top: 2px solid var(--brand); letter-spacing: -0.01em; color: var(--brand);
}
h3 { font-size: 15px; margin: 22px 0 6px; color: var(--ink); }
.meta { color: var(--muted); font-size: 13px; margin-bottom: 12px; }
.meta a { color: var(--brand); }

/* Wrapper for each reviewable change — scroll-margin keeps anchored items
   from sliding under the summary panel when navigated to via TOC. */
.pp-change-item { scroll-margin-top: 16px; padding: 4px 0; }

.change-num { font-weight: 600; color: var(--ink); margin: 16px 0 4px; font-size: 14px; }
.change-row {
  display: grid; grid-template-columns: 64px 1fr; gap: 10px;
  padding: 8px 12px; border: 1px solid var(--br); border-radius: 6px;
  margin: 4px 0; font-size: 13.5px;
}
.change-row.b { background: #fef2f2; border-color: #fecaca; }
.change-row.a { background: #f0fdf4; border-color: #bbf7d0; }
.change-row .lbl {
  font-weight: 700; font-size: 11px; letter-spacing: 0.04em;
  text-transform: uppercase; padding-top: 2px;
}
.change-row.b .lbl { color: var(--danger); }
.change-row.a .lbl { color: var(--success); }
.change-row .val { white-space: pre-wrap; word-break: break-word; }

code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  background: var(--bg-alt); padding: 1px 6px; border-radius: 4px; font-size: 13px;
}
.pill {
  display: inline-block; background: #fee2e2; color: var(--danger);
  padding: 2px 8px; border-radius: 9999px; font-size: 11px; font-weight: 600; margin-right: 6px;
}
ul.compact li { margin: 2px 0; font-size: 14px; }

details { margin: 8px 0; }
details summary { cursor: pointer; padding: 4px 0; font-size: 14px; list-style: none; }
details > summary::-webkit-details-marker { display: none; }
details > summary::before {
  content: "▶"; display: inline-block;
  transition: transform 0.15s; margin-right: 6px; font-size: 11px; color: var(--muted);
}
details[open] > summary::before { transform: rotate(90deg); }

.toc {
  background: var(--bg); border-radius: 12px;
  padding: 16px 20px; margin: 20px 0 32px;
}
.toc h3 {
  margin: 0 0 8px; font-size: 14px; color: var(--ink);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.toc ol { margin: 0; padding-left: 20px; font-size: 14px; columns: 2; column-gap: 24px; }
.toc li { margin: 3px 0; break-inside: avoid; }
.toc a { color: var(--brand); text-decoration: none; }
.toc a:hover { text-decoration: underline; }
```

## Banner HTML (top of body)

```html
<div class="banner">
  <strong>TEMPORARY FILE.</strong> This page is for content review only.
  Delete <code>{path-to-this-file}</code> before merging to production.
  <code>noindex</code> is active — search engines will not index it even
  if accidentally deployed.
</div>
```

## Generated stamp (MANDATORY — top of body, under the banner)

The page must carry a visible "generated at" stamp so reviewers know which run they're looking at and can match decisions back to a specific generation.

**Use the actual current system time** when you generate the page. Do not leave a placeholder, do not use a build-time variable, do not write `{{ now }}` — substitute the real wall-clock time at generation. Read it the way you'd read any other system fact:

```sh
# Run this in your shell at generation time and use the output literally.
date "+%Y-%m-%d %H:%M %z (%Z)"
date -u "+%Y-%m-%d %H:%M UTC"
```

Then write BOTH the local time AND the UTC equivalent on the page:

```html
<p class="generated-stamp">
  <strong>Generated:</strong>
  2026-05-01 17:32 +0300 (Europe/Istanbul) ·
  <span class="utc">14:32 UTC</span> ·
  {N} pages · {M} atomic changes
</p>
```

Format rules:
- **Always show both local and UTC** — local first (with offset + tz name in parens), then a separator, then the UTC equivalent. The reviewer may be in a different timezone than the machine that generated the page, so giving them both lets anyone reconcile without doing arithmetic.
- Local time format: `YYYY-MM-DD HH:mm ±HHMM (Region/City)`. UTC format: `HH:mm UTC` (the date is the same as the local-side date 99% of the time; if generating right around a UTC midnight crossover, write the full `YYYY-MM-DD HH:mm UTC` to avoid ambiguity).
- 24-hour clock for both halves.
- If you cannot determine the local timezone (no `date` access, no system info), fall back to UTC only — but do try `date` first.
- The page-count + change-count after the timestamp must equal the actual numbers in the page. They are the same totals you report in the self-rejection audit.
- On regeneration, **overwrite** this stamp with the new generation time. Do not append a history of past generations — the live stamp always reflects the current page. (See the Regenerating section below — updating the stamp is on the regeneration checklist, not optional.)

## PinAppAI widget snippet (place before `</body>` — DO NOT modify the URL or key)

```html
<script src="<API_BASE>/widget.js"
        data-project="<PROJECT_KEY>" defer></script>
```

## Output structure (top-down)

1. `<!DOCTYPE html>` + `<html lang="..">`
2. `<head>`: charset, viewport, robots noindex, title, inline `<style>` (CSS above)
3. `<body>` with `<main class="wrap">`:
   - Yellow banner (delete reminder)
   - `<h1>` ("Content Changes — Pending Review" or similar)
   - `.generated-stamp` line — `Generated: YYYY-MM-DD HH:mm TZ · N pages · M atomic changes` (real current system time, MANDATORY)
   - `.toc` with anchor links to each `<h2>`
   - For each changed page:
     - `<h2 id="{page-slug}">{page-url}</h2>`
     - `.meta` (source doc + file paths)
     - Optional `<h3>` with `<ul.compact>` for structural changes
     - `<details>` block with all atomic text changes wrapped in `.pp-change-item`
   - `<h3>` "General notes" at the end (catch-all)
4. PinAppAI widget snippet before `</body>`

## Final checklist (verify before declaring done)

- [ ] File at correct location for the stack
- [ ] `noindex` meta tag present
- [ ] Standalone HTML (no layout extends, all CSS inline)
- [ ] Yellow banner with delete-before-merge warning
- [ ] **Generated stamp** at top of body, with **BOTH local and UTC time**: `<p class="generated-stamp"><strong>Generated:</strong> YYYY-MM-DD HH:mm ±HHMM (Region/City) · HH:mm UTC · N pages · M atomic changes</p>`. Use the actual current system time at generation (run `date "+%Y-%m-%d %H:%M %z (%Z)"` and `date -u "+%H:%M UTC"` and substitute literally — no placeholders). On regeneration, **overwrite** the stamp with the new local + UTC time; if the calendar date crossed midnight since the previous run, the date updates too.
- [ ] TOC at top with anchor links to each `<h2>`
- [ ] Each page has `<h2 id="{page-slug}">`
- [ ] Each atomic change wrapped in `<div class="pp-change-item" data-pp-item="..." data-pp-title="...">`
- [ ] **Coverage match:** `grep -c '<div class="change-num">'` equals `grep -c '<div class="pp-change-item"'`. Both counts reported in your response.
- [ ] **Headline count honesty:** every `(N items)` / `(N madde)` promise in section summaries equals the actual wrapper count below it. If you can't wrap all N, either wrap them all, drop the count, or document the filter explicitly. Never silently drop while keeping the count intact.
- [ ] **Banner / page-header total honesty:** any total claim in `<h1>` / meta / banner ("31 pages · 154 changes") matches the actual global `.pp-change-item` count. **No quality-claim buzzwords** ("single-field strict", "all atomic", "fully decomposed", "Rule #N strict") — these are self-tasdik claims that become lies if a reviewer finds violations.
- [ ] **No multi-field bundles (forbidden #6):** distinct fields of the same UI element with a 1-1 before→after pair (card title AND card description, H2 AND intro paragraph) get separate wrappers. IDs containing `+`, `-and-`, or joining two field names with a separator (`title-and-description`) are smells.
- [ ] **List-form bundling judged correctly (forbidden #7):** every section containing parallel items of the same kind (cards, FAQ Q&As, bullets, metrics) follows the decision rule. Independent claims with 1-1 before→after pairs → split into N wrappers. Holistic block rewrites with no 1-1 mapping → one wrapper showing the full before/after content. When uncertain, ask "could the reviewer reasonably approve some items but reject others?" — yes → split, no → bundle.
- [ ] **No grouped change-nums** (no `5–8.`, no `+`-joined titles, no "see git diff" paragraphs, no "and X more"). Every numbered change has its own wrapper with full before / after.
- [ ] All `data-pp-item` values unique within the page
- [ ] No positional counters in IDs (`c1`, `c2` — these break on reorder)
- [ ] **If regenerating:** existing IDs preserved for unchanged items, ID preservation report + coverage report included in your response
- [ ] PinAppAI widget snippet present before `</body>` with the URL and key from the prompt (do not invent or substitute)
- [ ] Build succeeds; page accessible at `/changes/`
- [ ] No links to it from main nav / sitemap
- [ ] If using Astro / Next: file excluded from sitemap generation

If anything is ambiguous (which stack, which commits, branding), **ask first**. If you find yourself thinking "I'll group these because there are too many," instead **filter** (and document the filter) — don't bundle.

---

# How PinAppAI works (context for you, not for the generated page)

PinAppAI is a self-hosted feedback widget. The single `<script>` tag activates different UX based on what's on the page:

**Pattern A — Page-level free-form feedback (any page on the site):**
Add JUST the `<script>` tag to every page's `<body>`. Reviewers see floating buttons (💬 + 🎯) to drop pin comments anywhere on the page, or pick specific elements with a DevTools-style hover overlay. No `data-pp-item` markup needed. Works on any URL.

**Pattern B — Item-by-item decisions (this `/changes/` page):**
Add the `<script>` tag AND mark each item with `data-pp-item="..."` (this is what you're doing). Reviewers see an inline Approve / Reject / Request-changes bar on every marked section, plus a draggable summary panel in the bottom-right showing progress (Total · Approved · Rejected · Change-requested · Pending). The free-form floating buttons are hidden on these pages — focus is decisions, not ad-hoc feedback.

**You can mix both patterns in one project.** Recommended setup: every page on the site gets the script (Pattern A — reviewers can flag anything you didn't think to mark), and dedicated review pages like this `/changes/` add `data-pp-item` markers (Pattern B — drives explicit decisions on intended changes).

Cross-page note: the widget scopes decisions per page automatically (matched on `page_path + target_item_id` server-side). The same item ID on different pages keeps independent decision histories — but use `{page-slug}:c{N}-...` IDs anyway because they read better in the admin export.

# Workflow (after generation)

```
You ────────► Open /changes/ in browser, share URL with reviewers
Reviewers ──► Each item shows [✓ Approve] [✗ Reject] [↻ Request change]
Reviewers ──► Click decisions; a draggable summary panel tracks progress
You ────────► Open admin dashboard to see decisions per project
You ────────► Export decisions as Markdown (DOM-ordered, grouped by status)
You ────────► Paste Markdown into Claude / Cursor → AI applies the decisions
You ────────► After applying → DELETE this /changes/ file → merge
```

---

# After generation — ASK before committing or pushing

You are operating inside someone else's repository. **You do not commit, branch, or push on your own.** Once the page is generated and the self-rejection audit passes, your last move is to surface the question to the repo owner and wait for their answer.

Print this block verbatim at the end of your response (after the audit table, after the file is written):

```
✅ /changes/ page generated at <path>.
Audit: passed (see table above).
Generated stamp: <the same local + UTC line you put in the page, e.g. 2026-05-01 17:32 +0300 (Europe/Istanbul) · 14:32 UTC>.

Before you ship this, decide how you want to land it. Pick one:

  (a) New feature branch — suggested name:
      chore/changes-review-YYYY-MM-DD-HHMM
      I'll: create the branch from your current HEAD, commit only the
      new /changes/ file (and any .gitignore tweak if needed), and
      either push or stay local — your call.
  (b) Commit on the current branch — same commit, no branch switch.
  (c) Stage only — leave it uncommitted so you can review the diff first.
  (d) Skip — don't touch git, you'll handle it manually.

Suggested commit message:
  chore: generate /changes/ review page (N items across M pages)

Tell me which option (a / b / c / d), whether to push to remote after
the commit, and any commit-message tweaks. I'll wait.
```

**Hard rules while waiting for the answer:**

- Do NOT run `git add`, `git commit`, `git checkout -b`, `git push`, or any other git command until the owner picks an option and (for push) explicitly says push.
- If the owner says "your call, just do it" → DEFAULT to **(b) commit on current branch + do NOT push**. Pushing to someone else's repo without explicit authorization is a hard no, even if the working tree is clean and the test suite is green.
- If the repo's branch protection rules / CI guards block the chosen path (e.g. `main` is protected and the owner picked (b) on `main`), surface that rather than auto-falling-back to a different branch.
- Never use `--no-verify` to skip pre-commit hooks. If a hook fails, report the failure and ask.

Once the owner answers and you've executed (or deferred) the landing, one server call remains:

{{include: _shared/register-change-items.md}}

**pk_ override edge case:** if this run used a pasted `pk_...` project_key
instead of resolving the project from the authenticated workspace, the
registration tool can't reach that project — skip the call and tell the
owner the items still need registering by someone with access to that
project's workspace (an MCP-connected AI running this flow there, with
this page's item list).

After the registration call (or its documented skip), you're done.

---

Generate the HTML now using the most recent changes you can find. If you can't determine recent changes from git, ask me to point you at the right commits or files.
