---
name: apply
description: v3 PinAppAI Apply — drain the project Inbox by editing source for every Apply-eligible change request (new / rejected / change-requested), then mark them all applied in one atomic batch.
arguments: []
---
You are operating inside the user's source repository for a PinAppAI project.
Your job: drain the project's **Inbox** by editing local source code to address
every Apply-eligible change request, then atomically mark them all `in_review`
so reviewers can accept / reject / change-request each. Pushing and
opening a PR are SEPARATE steps the user explicitly requests afterwards.

This prompt targets the 3-tab dashboard (Inbox / InReview / Closed) and
has **no open-iteration gate** — Apply itself creates the iteration
record as a side effect of the atomic batch. (It superseded the legacy
fix-changes / apply-decisions prompts, removed in 0.8.0.)

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

# Step 0 — Preflight

{{include: _shared/landing-chooser.md}}

Run the **Before you edit — preflight** subsection above (git status + branch
check). If the tree is dirty, stop and ask. Don't switch branches without
explicit instruction.

# Step 1 — Pull the Apply-Inbox in one round-trip

Call:

```
pinappai_list_apply_inbox({project: "<resolved slug>"})
```

The response shape is `{ apply_inbox: [...] }`. Each row carries:

- The CR's identity (`cr_id`, `seq_no`, `state`).
- The source-feedback context (`comment_text`, `suggested_text`,
  `selector_path`, `target_item_title`, `page_url`, `page_path`,
  `pin_kind`, `target_kind`, `x_pct`, `y_pct`, `screenshot_url`, and —
  on ✂️ text pins — `target_text_snippet` + `selection_prefix` /
  `selection_suffix` / `selection_occurrence`).
- `fb_before_text` / `fb_after_text` — the source feedback row's
  before / after snapshot. For CRs spawned by `/changes/`-page
  decisions (the common path: reviewer rejects an applied item on the
  /changes/ page), these are captured at decision-submit time and are
  the **primary source of truth** for revert + change-request edits.
- `prior_apply_payload` — the most recent `cr_marked_applied` event's
  payload JSON (or null). Non-null when the CR has been through an
  apply cycle before. Used as a **secondary source** of before/after
  when `fb_before_text` / `fb_after_text` are absent (legacy widget-
  pin path without a /changes/ submission).
- `latest_decision_payload` — the most recent `reviewer_decided` OR
  `admin_action` event payload JSON. For state=`inbox_change_requested`
  CRs, `change_request_text` may live here (admin-tab path: admin
  clicked Change-req in the InReview tab and typed new wording). The
  /changes/-page path stores the new wording in `comment_text` of the
  source feedback row instead — the Apply prompt checks BOTH sources
  (see Step 3's change-requested branch).
- `workspace_instructions` — nullable string, top-level on the response
  (not per-row). The workspace's standing house rules. See below.
- `device_label` / `orientation` and, for pins, `viewport_w` ×
  `viewport_h` — inline on every apply-inbox row: the device and the
  exact viewport the reviewer saw. When present, reproduce and verify
  your fix at that width, not just desktop. For the "Responsive" custom
  size the label is generic, so trust `viewport_w` for the breakpoint.
  (`get_change_request` still returns the same context as a nested
  `device` object — e.g. `{label: "iPhone 15", orientation: "portrait",
  viewport_w: 393, viewport_h: 852}` — for the detail-read flows.)

{{include: _shared/workspace-instructions.md}}

If `apply_inbox` is empty, print:

> Nothing to apply. The project Inbox has no CRs in `inbox_new`,
> `inbox_rejected`, or `inbox_change_requested`. Deferred CRs aren't in
> scope; the admin must undefer them first.

…and STOP. Do not run preflight git ops or edit any files.

# Step 2 — Frame the work + ask for confirmation

First, triage every CR SILENTLY — do not print a triage report (a
50-CR inbox would drown the user; /pinappai:analyze exists for deep
inspection). You are only deciding, internally, which CRs are UNCLEAR:
- the request is ambiguous (no target wording, subjective adjectives,
  multiple plausible readings),
- it conflicts with another CR in the batch (same element/text — order
  or intent collision),
- or it is blocked on input you cannot produce (an image/file asset, a
  credential, a business fact you'd have to invent).

State to the user in 2-3 sentences what you found:
- Total CRs in scope, broken down by substate.
- Roughly how many pages / source files are likely affected (group by
  `page_url`).
- Which substate flavors are present (so the user knows reverts /
  change-requests are coming, not just new applies).

Then present ONE picker (first option first — it is the default path):

> Apply these `<N>` CRs in the proposed order (`<one-line summary of
> the batch, e.g. "#21, #22, #24 text edits + #23 image replacement">`)?
>
> 1. **Confirm — apply all `<N>`**
>    Edit source for every CR, mark them in_review as one iteration.
>    Anything I can't complete (e.g. a missing asset) gets deferred
>    with a note instead of guessed.
> 2. **Only ask about the unclear ones (`<K>`)** — offer ONLY when K > 0
>    I'll walk you through the `<K>` CRs I'm not certain about, one at
>    a time, then apply the batch.
> 3. **Step through every CR — confirm each one**
>    One question per CR: apply / skip / adjust. Slower, full control.
> 4. **Let me reorder/drop**
>    Tell me which CRs to drop or how to reorder before I edit anything.

You may add batch-level options beyond these when the inbox shape
warrants it (e.g. "text edits only — leave the image swaps for later"
when kinds split cleanly). Keep option 1 first, always.

**WAIT for the answer.** Do not edit yet.

## The per-CR walkthrough (options 2 and 3)

Ask ONE question per CR, sequentially — never a combined form. Each
question shows: the CR's #seq_no + kind + target page, the reviewer's
request (verbatim or tightly summarized), and — when your reading
involves interpretation — YOUR planned edit in one sentence.

Base options, always present:
1. **Apply** (as requested / as my reading)
2. **Skip this run** — stays in the inbox
plus the client's free-text option, which is where the user types an
adjusted wording, an asset URL/path, or any custom instruction.

ADD context-specific options whenever they genuinely help — the base
set is a floor, not a ceiling. Examples: a blocked image CR gains
"I'll drop the file into the repo now — wait, then use it"; an
ambiguous rewrite gains a second candidate reading; a conflicting pair
gains "apply #A first, re-target #B on the result". Never pad with
filler options.

After the walkthrough ends, proceed straight to Step 3 with the
per-CR decisions — do NOT ask a second batch-level confirmation; the
questions WERE the confirmation.

# Step 3 — Per-CR edit loop (branch on `state`)

For each CR the user confirmed, branch on its `state`:

### 🆕 `state === 'inbox_new'` — standard apply

This is the default Apply path. The CR has never been applied before.
Use the CR's own `comment_text` / `suggested_text` + per-pin-kind rules
to derive the edit:

#### `pin_kind === 'text'` — ✂️ text-selection pin (highlighted an exact text range)

The reviewer highlighted an EXACT word/phrase/sentence inside a larger
block. `element_text` is the selected range verbatim — the complete and
only edit target. `selector_path` names the containing block;
`selection_prefix` / `selection_suffix` are the words immediately around
the selection; `selection_occurrence` (0-based) says WHICH instance when
the same words repeat inside that block.

- Locate the block in source (`page_url` → file, then `selector_path` /
  `selection_prefix` + `selection_suffix` as grep context).
- Replace EXACTLY the `element_text` range — nothing before, nothing
  after it. If `selection_occurrence` > 0, count matches inside the
  block and edit only that instance; earlier ones stay untouched.
- `suggested_text` set → paste it verbatim as the replacement.
  Only `comment_text` → draft the replacement from it, preserving the
  sentence's grammar around the spliced range.
- No screenshot fetch needed — the anchor is textual and complete.

#### `pin_kind === 'element'` — element pin (clicked a specific DOM node)

`selector_path` is populated. `target_kind` ∈ {`text`, `field`,
`mixed`, null} tells you what the picker classified the element as.
`suggested_text` (when set) is a literal text replacement the
reviewer typed.

- Call `get_change_request({id: <source_feedback_id>, include_image: true})`
  only if `target_kind === 'mixed'` or the request is structurally
  ambiguous, OR if `screenshot_url` is set on this row (the reviewer
  attached one — they intended the image to matter). For plain
  `text` / `field` pins with `suggested_text` and no screenshot, the
  selector + body alone suffice. **IMPORTANT**: `id` is the
  FEEDBACK id (`fb_*`), which is the row's `source_feedback_id`
  field — NOT `cr_id`. Passing `cr_id` returns 404.
- Read `related[]` for cross-cluster context.
- Read local source by mapping `page_url` → source file via the
  repo's routing convention, then locate `selector_path` (or
  fuzzy-grep `target_text_snippet` if the selector breaks under
  component-prop / i18n abstraction).
- Edit the source.

#### `pin_kind === 'spot'` with `screenshot_url` null — page/section pin

Reviewer dropped a pin at `(x_pct, y_pct)` on the page without
selecting any specific element and without capturing a screenshot.
`selector_path` is null. **There is no image to fetch — `comment_text`
and position are all the signal you have.** The intent is page-level
or section-level guidance, not a specific-element edit.

- Infer the target from `comment_text` keywords + `(x_pct, y_pct)`:
  y < 25% → header/hero, 25-75% → body, > 75% → footer.
- Only ASK the user when the target is ambiguous between two or more
  concrete sections. Don't ask just because there's no selector —
  that's the default shape for this pin kind, not a defect.
- Do NOT call `get_screenshot` on these rows — there is no image.
- Edit the source.

#### `pin_kind === 'spot'` with `screenshot_url` set — screenshot pin

Reviewer captured a region of the page as visual evidence. The
screenshot is the primary signal; `comment_text` is supporting.

- **You MUST fetch the screenshot before reasoning about the change.**
  Call `get_change_request({id: <source_feedback_id>, include_image: true})`
  on every such CR. The response embeds the image inline; reason
  visually. **Note**: `id` is the FEEDBACK id (`fb_*`) — pass the
  row's `source_feedback_id`, NOT `cr_id`.
- Map the captured region to the source file using `page_url` +
  visual landmarks.
- Edit the source.

### ↩ `state === 'inbox_rejected'` — REVERT

A reviewer or admin **rejected** this CR after it was applied. Your
job is to swap the prior edit back.

1. **Resolve the before/after pair** by checking sources in priority order:
   - **Primary**: `fb_before_text` / `fb_after_text` from the row.
     For /changes/-page rejections (the typical path), these were
     captured at submit time and ARE the canonical snapshot.
   - **Secondary**: parse `prior_apply_payload` as JSON and read
     its `before_text` / `after_text`. Use this if the fb fields
     are null (legacy widget-pin path without a /changes/ submission).
2. Locate the `after_text` (whichever source you picked) in source at
   this CR's `selector_path` / `target_item_id` / `page_url`. That's
   the text the prior Apply wrote.
3. Replace `after_text` with `before_text`. This is the revert.
4. The new `before_text` you record (in Step 4 below) is the source
   content BEFORE this revert (= the resolved `after_text`); the new
   `after_text` is the post-revert source (= the resolved
   `before_text`). The labels swap — `cr_marked_applied` always
   captures "what the edit transformed from / to" so revert is
   reversible by the same primitive.
5. If **both sources are null/empty** AND you can't locate the target
   text in source via the CR's selector/title, only then mark the CR
   `🟡 BLOCKED` and move on. Don't block when you have either source.

### 🔄 `state === 'inbox_change_requested'` — APPLY WITH NEW WORDING

A reviewer or admin **requested a change** to a previously applied CR.
The new wording lives in one of two places depending on which UI path
the reviewer used; check BOTH:

1. **Resolve the new wording** (check in priority order, take the
   first non-empty hit):
   - **Source A (/changes/-page path)**: the row's `comment_text`
     field (the source feedback's free-text submission). This is
     where reviewers using the /changes/ page type their new wording.
   - **Source B (admin-tab path)**: parse `latest_decision_payload`
     as JSON. Read its `change_request_text` field. This is where
     admins clicking Change-req in the InReview tab leave new wording.
   - **Source C (CR-native fallback)**: if A and B are both empty,
     fall back to the CR's own `suggested_text` / `comment_text` as
     a directive. Treat it like a fresh `inbox_new` apply.
2. **Resolve the current source text** (what's there now, to replace):
   - **Primary**: `fb_after_text` (captured at /changes/ submit time).
   - **Secondary**: `prior_apply_payload.after_text` (from a prior
     apply event).
3. Replace the current source text with the result of applying the
   new wording:
   - If the new wording is a **literal replacement** (e.g. reviewer
     typed "Use 'X' instead"), direct swap.
   - If it's a **directive** (e.g. "make it shorter", "use the
     alliteration"), interpret it relative to the current source text
     and produce the appropriate result.
4. Record the new `before_text` (= the resolved current source text)
   and new `after_text` (= the freshly-edited text) for Step 4.

### Common to all three substates

- Read each CR's `comment_text` carefully — even on revert / change-
  request, the original reviewer commentary explains intent.
- If the source layer can't be located after the per-substate
  attempts above, mark the CR `🟡 BLOCKED` and move on. Don't guess.
- Capture before/after snapshots for the edit you just made. The
  "before" is the source content as it existed PRIOR to your edit
  (or PRIOR to the revert, for `inbox_rejected`); the "after" is
  what you wrote. These get stored on the new `cr_marked_applied`
  event so a future admin Revert / Change-request has a
  deterministic source-of-truth.

# Step 4 — Atomic apply-batch

Once every CR in scope has been edited (or marked `🟡 BLOCKED`),
collect the successful edits into a single `applies` array and call:

```
pinappai_apply_change_requests({
  project: "<resolved slug>",
  bundle_summary: "<one-line summary, e.g. 'applied 12 CRs across 4 pages'>",
  applies: [
    { cr_id: "<cr_id_1>", before_text: "<src before>", after_text: "<src after>" },
    { cr_id: "<cr_id_2>", before_text: "...",           after_text: "..." },
    ...
  ]
})
```

This is **one atomic D1 batch** on the server. It creates a single
`iterations` row (visible in the project history page), writes one
`cr_marked_applied` event per CR with snapshots, and transitions each
CR's state from `inbox_*` to `in_review`. The response shape is:

```
{
  iteration_id: "iter_...",
  iteration_seq_no: <N>,
  results: [
    { cr_id: "...", success: true, new_state: "in_review" },
    { cr_id: "...", success: false, error: { code: "invalid_state", current_state: "...", message: "..." } },
    ...
  ]
}
```

Per-CR errors don't fail the whole batch — they happen when a CR's
state changed between Step 1 and now (admin deferred it, reviewer
acted on it mid-session, etc.). Surface them to the user as
"Skipped: <cr_id> (<reason>)" but don't roll back the successful
ones.

Do NOT call `pinappai_apply_change_requests` for CRs you marked
`🟡 BLOCKED` — leave them in their inbox substate so the next Apply
or human review can retry.

# Step 5 — Run tests + report

After the batch completes, run the project's tests if a test command
is detectable (respect the repo's `package.json` / `Makefile` /
`pyproject.toml` — don't invent test invocations).

Print a summary — headline counts, then ONE line per CR so every
outcome is accounted for (applied / skipped / deferred, with file and
reason where it applies):

> ✅ Applied **<N>** CR(s) in iteration #<seq_no>. Skipped **<M>**,
> deferred **<K>**.
>
> #21 ✓ applied   src/pages/index.astro       hero dek trimmed
> #22 ✓ applied   src/pages/index.astro       intro tightened (agreed reading)
> #24 ✓ applied   src/pages/journal/….astro   saltwater ×3
> #23 ⏸ deferred  awaiting image asset — still in the inbox
>
> Reviewers (and admins from the InReview tab) can now accept /
> reject / change-request each. If a reviewer rejects or
> change-requests, the CR returns to the Inbox in the appropriate
> substate; the next `/pinappai:apply` will handle it via the
> revert / apply-with-new-wording branches.

CRs the user skipped or that were deferred (blocked on input) stay in
the Inbox untouched — never mark them applied, and never silently drop
them from the report.

# Step 6 — Update the batch boundary marker

{{include: _shared/marker-write.md}}

The `bundle_summary` field for this run should describe the Apply
batch — e.g. `"applied 12 CRs across 4 files (iteration #<seq_no>)"`.
The marker file MUST land in the same commit as the source-edit
changes (see Step 8's landing-chooser).

# Step 7 — Keep the `/changes/` page in sync

The `/changes/` review page is the home of the in-page review-chip
(the "N pending changes on this page" pill that overlays source
pages — see `apps/widget/src/modes/decision-overlay.ts`). The chip
only renders when the `change_items` table has rows for the project,
and that table is populated ONLY by the privileged
`pinappai_register_change_items` call you make after writing the page
(the page does not register itself — see the registration block at the
end of Step 8). So: **no `/changes/` page → no in-page review chip →
reviewers can't see what's pending on each page**.

Two cases: creating a page that doesn't exist yet ASKS (opening a
new review surface is a real decision); refreshing an existing page is
AUTOMATIC (a stale page is never what the user wants).

**Detect first.** Grep the source dir for `data-pp-item` attributes
across HTML / Astro / JSX / Vue / Svelte. Zero matches → page does
NOT exist. Non-zero → page exists (the existing file is the one to
regenerate).

### Case A — `/changes/` page does NOT exist (first run / fresh install)

Print exactly (substitute the real count):

> Applied **<N>** CR(s). This project has no `/changes/` review page
> yet — without it, reviewers won't see the in-page chip ("N pending
> changes on this page") on source pages. Generate one now? **[Y/n]**

- **Y / empty** → Generate a `/changes/` page from scratch by
  following the rules of `/pinappai:generate-changes-page`. Reuse
  the `project` already resolved earlier — don't re-prompt. Walk
  the now-current source. If the marker read at Step 6 carried
  `changes_page_pending: true` from an earlier declined batch, the
  page must also cover that debt — walk
  `git log <batch_base_sha>..HEAD` and include those changes as
  items, then drop `changes_page_pending` + `batch_base_sha` from
  the marker you land. The new file (typically
  `src/pages/changes.astro` or repo-equivalent) is staged alongside
  the source edits and the marker so everything lands in one commit
  at Step 8.
- **n / no** → Skip the page, but do NOT let the marker strand the
  batch: re-write the Step 6 marker file adding
  `"changes_page_pending": true` and
  `"batch_base_sha": "<git rev-parse HEAD>"` (run it NOW, before the
  landing commit — that SHA is the landing commit's parent, so the
  batch stays inside the pending range). If the previous marker
  already had `changes_page_pending: true`, preserve its existing
  `batch_base_sha` instead of overwriting (oldest unreviewed base
  wins). Then surface a one-line reminder: *"In-page review chip
  won't appear until `/changes/` exists — run
  `/pinappai:generate-changes-page` when ready (it will pick this
  batch up via the pending marker)."* Then continue.

### Case B — `/changes/` page exists (every subsequent Apply)

Source edits from Apply leave a stale `/changes/` — the BEFORE /
AFTER snapshots the page captured no longer match current source.
Close the loop before landing.

**Regenerate automatically — do NOT ask.** A stale `/changes/` after an
apply is never what the user wants, and the landing step is where they
can still veto (option (c) stage-only shows the diff; typing "skip the
changes page" at the landing prompt drops the regenerated file from the
batch). Announce it in one line while working:

> Regenerating `/changes/` to match the applied source (say "skip the
> changes page" at the landing step to leave it out).

Regenerate following the rules of `/pinappai:generate-changes-page`.
The regenerated file is staged alongside the source edits + the marker
for the single-commit land at Step 8. An empty `/changes/` page (all
items resolved) is the canonical "nothing pending" state — fine to emit
as such.

When generating items from apply results it's easy to anchor on a
minimal example and drop attributes or invent your own before/after
markup. **Don't** — the page skeleton AND every item must follow the
shared contracts below exactly, in BOTH Case A and Case B; they are
what make in-page review work at all. "Following the rules of
`/pinappai:generate-changes-page`" means exactly these two blocks
(the page skeleton INCLUDES the widget snippet requirement) —
do not embed the page in the site's layout or skip the h2/meta
grouping just because this run touched a single page.

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

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

### Mapping `data-pp-applied-cr-id` from this run's apply batch

In **both cases above**, every `/changes/` item derived from the CRs
you just applied in Step 4 MUST carry `data-pp-applied-cr-id`, where
the `<cr_id>` comes from the just-completed
`pinappai_apply_change_requests` response. Each successful apply
result (`results[i]` with `success: true`) maps 1:1 to one /changes/
item: it's the CR whose `before_text` / `after_text` produced this
particular review item. Example: the apply-batch result
`{ cr_id: "cr_abc123", new_state: "in_review" }` becomes
`data-pp-applied-cr-id="cr_abc123"` on the item its edit produced.

The mapping is trivial because YOU just produced both halves: the
apply-batch result you got in Step 4 lists every cr_id you applied,
and you know which file/element each one targeted. Tag each /changes/
item with the cr_id of the CR whose edit produced it.

# Step 8 — Land the changes

{{include: _shared/landing-chooser.md}}

{{include: _shared/preview-origin-offer.md}}

For the suggested branch name, use `pinappai/apply-YYYY-MM-DD-HHMM`.
For the suggested commit message, follow the project's prefix
convention (e.g. `fix(...)` or `chore(content):`) and **include the
CR IDs** addressed: e.g. `chore(content): apply CRs #5, #6, #7
(iteration #12)`.

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

# Step 9 — Optional push & PR (only on explicit user request)

{{include: _shared/provider-fallback.md}}

For the PR body: list CR IDs addressed (with their substates so
reviewers know which were reverts / change-requests), link back to
the PinAppAI dashboard, summarize the diff at section level.

# Conventions

{{include: _shared/output-language.md}}

- Reference CRs by their per-project `#seq_no` (e.g. "CR #5") in
  commits, PR descriptions, and progress prints. The opaque `cr_id`
  (e.g. `cr_abc123`) is for API calls only.
- Never silently widen scope — if you spot a related issue not in the
  apply-inbox, ask before fixing.
- If a screenshot URL fails to fetch, say so and continue with
  text-only context.
- Test commands: respect the repo's `package.json` / `Makefile` /
  `pyproject.toml`. Don't invent test invocations.
