# Review Latest Feedback

> Sweep the newest Slack, GitHub issue, and Sentry feedback: first answer the reporters who answered you, then fix clear verified repo bugs at the owning boundary, build the UX and feature requests the invoking user endorsed with an :upvote:, reply only where the reply carries information, and recap every disposition. Use for scheduled or manual feedback sweeps.

- Skill: `builderio/review-latest-feedback` (Agent Skill)
- Install (CLI): `npx skillmds@latest add builderio/review-latest-feedback`
- Raw SKILL.md: https://api.skillmd.com/api/skills/builderio/review-latest-feedback/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: Builder.io (https://skillmd.com/u/builderio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/builderio/review-latest-feedback

---


# Review Latest Feedback

Four phases, in order. Phase 0 comes before any investigation, not after.

0. **Claim** every item you intend to tackle with `👀`, before investigating
   any of it.
1. **Answer the people who answered you.** Older open questions first.
2. **Fix** what the evidence actually proves, at the owning boundary.
3. **Reply**, under a hard question budget, then recap.

Output is fixes; reply only when informative. Two fixes and three messages
beats thirty replies.

## Phase 0: claim what you are taking

Other agents work concurrently. The eye is a temporary work lock: keep it only
while investigating, fixing, or waiting on one targeted detail.

**Defects are in scope: fix them or ask for the one detail needed to fix them.**
Failure to reproduce means ask, not close; state what you tried and request one
unblocker - request id, time, screenshot, account, or URL.

Use **Skipped** only for non-defects: feature requests, enhancements, or
subjective preferences. Breakage is never skipped. **Open - no reply** is a last
resort after working the defect and finding neither a fix nor a useful question;
document why.

Release your eye for every terminal disposition: **Fixed**, **Shipped**,
**Open - no reply**, **Resolved elsewhere**, **Skipped**, **Clustered**, or
**Abandoned - no answer in 4 days**. Only **In progress** and pending
**Clarification needed** retain it. **Clustered** is terminal for a duplicate
row that is not the single owning investigation.

**Releasing means add `✅`, not delete `👀`.** Slack exposes
`add_reaction`/`get_reactions`, not removal, so `✅` is the durable release
marker. Only `👀` without it is open, matching the Phase 1 cursor.

Release only this workflow's eye. A foreign workflow eye is ownership, even
stale: do not release, duplicate, or reply over it. Record **Owned elsewhere**
and leave it for handoff; preserve it as a blocker when needed.

Enumerate `slack_read_channel` newest backward through `next_cursor` until a
parent has your open `👀` without `✅`, or is older than 5 days. Use its oldest
timestamp as the recap cursor. Classify from parent text, attachments, and
reactions; do not open threads yet.

**`slack_search` is not a scan.** It ranks and truncates. Use channel reads for
enumeration and put their count in the recap; use search for known things such
as prior replies, eyes, and repeat symptoms.

A channel read returns parents, so use its timestamps directly; *search* hits
are usually replies, so resolve those through the permalink `thread_ts` first.

Add `👀` to every intended item and read reactions back before investigation.
Claim all actionable reports, including carried-over parents, without adding a
second reaction.

Claiming only marks work; it does not investigate or reply, so older open
questions still outrank newer reports.

Phase 1 searches reach past this window. Claim search-discovered work when it
enters the worklist - eye first, read back, then investigate.

Claim generously, correct cheaply: release out-of-scope work with `✅`; record
foreign ownership and leave its eye; stop on an unverified reaction.

**Never end with an unworked claim.** Give carried-over eyes a disposition or
release them with `✅`; peers treat an unexplained eye as owned.

**The eye means "I have this," not "I owe you a message."** Every item gets a
recap row; only informative outcomes get a reply, and terminal outcomes release
the eye.

Claim what the classification rules put in scope. A duplicate is the same
message, repost, or cross-post. **A fresh symptom after an answer is a repeat:**
claim and cluster it so Phase 2 can test the prior fix.

## Phase 1: answer the people who answered you

Every question you ask creates an obligation to come back for the answer.
Discharge it before reading anything new.

Slack is the ledger. Do not keep a local one — a per-run state file cannot
see the previous run, which is why the follow-up never happened. Run this
first, every time:

```
slack_search: "this was sent from a bot." in:<#CHANNEL>
  sort=timestamp sort_dir=asc include_context=true max_context_length=300
```

Two details are load-bearing; a run that changed them missed all eight of its
answered threads.

**`include_context=true` on every page, to the last.** Its `Context after`
block names who spoke after each reply, which is how you find answers without
opening ~80 threads. Dropping it on later pages to save tokens hides every
answer past page one — that alone caused the miss. Read the context, then open
only the threads where someone replied.

**Match the disclosure, nothing narrower.** Do not filter to replies ending in
`?`: a clarification often reads "if you can share a deck URL, that would help
us dig in" and carries no question mark at all, so narrowing drops real
pending questions. The context block, not the query, is what separates
answered from terminal.

**The parent is the permalink's `thread_ts`.** `Message_ts` is your own
reply's timestamp; acting on it targets the wrong message.

Also search for the invoking identity's eye-marked parents before applying the
disclosure filter:

```
slack_search: hasmy::eyes: -hasmy::white_check_mark: in:<#CHANNEL>
```

The emoji-delimited modifiers are required. They scope the search to messages
with the connected identity's eye and without its release marker. Do not
replace them with emoji text searches.

The test for "answered" is mechanical: **did a person speak after your
question?** Someone counts when their message carries no disclosure marker —
a disclosure-marked message is this workflow under any identity, so a later
run's own reply never counts as an answer to an earlier one.

Apply that test to the `Context after` block, then open the thread to read
what they actually said before acting. What the test decides is only whether
the thread enters the answered set, not whether the answer is sufficient: a
partial, unrelated, or "will check later" reply leaves the original question
pending under the one-question rule, and does not earn a second question.

A reply counts as answered **once**. If you already read it on an earlier
sweep and it left the question pending, it is not new evidence — leave the
thread pending, keep it out of `Answered since last run`, and do not let it
outrank newer work again. Only a message newer than your last look at the
thread re-enters the answered set. Otherwise one unhelpful reply would take
priority on every run forever.

Enumerate the answered set **before** any other work and write its count into
the recap's `Answered since last run` field. Searching is not working the
results — a run that finds eight answered threads and then spends itself on
newer reports has skipped the phase while appearing to satisfy it. A non-zero
count with none of those threads in your dispositions means the run is not
finished.

Then identify the latest disposition from
this workflow or its companion. Keep every unanswered **Clarification needed**
question in the pending-question set until it is answered, explicitly resolved,
or aged out at four days. **Fixed**, **Shipped**, and **In progress** are not
pending questions. **Open - no reply** cannot replace an unanswered
clarification. Apply the Phase 0 release contract to every terminal state; a
non-owning **Clustered** row releases its eye too.

Only an unanswered **Clarification needed** thread enters the age branches
below — never one whose latest reply is **Fixed**, **Shipped**, or **In
progress**. If an older thread was recorded **Open - no reply** despite an
unanswered clarification, restore it to the pending set.

- **Someone answered** → highest priority in the run, ahead of every newer
  report: the evidence you said blocked you now exists. Rebuild it and attempt
  the fix. Reply **Fixed** only after all four bars pass; otherwise keep the
  clarification open. Never ask a follow-up before trying the fix.
  An answer that the issue is already resolved, fixed elsewhere, or not ours —
  a linked PR, "not a Clips issue" — is still an answer. Close it as
  **Resolved elsewhere** (terminal, and distinct from **Skipped**, which means
  out of scope): release the `👀` with `✅`, name who resolved it and where, post
  nothing. Adding `✅` is what makes the closure durable, or the next run's
  open-claim cursor resurfaces it as unfinished forever.
- **No answer, posted under 4 days ago** → leave it. Post nothing. A second
  message is a nag, not a follow-up.
- **No answer, posted over 4 days ago** → the question failed. Drop it
  silently: no reminder, no re-ask, no new reaction. Release the `👀` with
  `✅` and record **Abandoned - no answer in 4 days**, which is a terminal ledger disposition
  ranking with **Fixed** and **Open - no reply** — an expired thread keeps no
  eye and owes no reply, in this workflow or a standalone companion run. If the
  bug still matters, carry it forward as an internal investigation with no
  reporter dependency — dropping the question is not dropping the bug.

Four days is the retention rule, deliberately. A question unanswered for four
days will not be answered on day thirty, and an ever-growing open set becomes
the first thing every run reads, twice a day, forever. Expiry is what keeps
this phase cheap enough to run first.

Discovery is a separate concern from retention, which is why the search above
carries no `after` filter: a date-bounded cursor would miss an older question
still inside its window under a different clock. Search unbounded to **find**
them, then apply the age branches to what comes back. Finding an old question
does not exempt it from expiry.

Every new reply carries the disclosure, so the search above is the primary
cross-identity cursor. Legacy replies predating the marker need one more pass,
since they carry neither disclosure nor eye — run it once per valid workflow
identity, not just your own, or the claim that these searches cover every
run's questions is false:

```
slack_search: from:<EACH_WORKFLOW_IDENTITY> in:<#CHANNEL>
  sort=timestamp sort_dir=asc
```

Classify those hits by clarification wording such as `if you can share` — as a
filter on results, never as the discovery cursor itself.

These searches cover **every** run's questions, not just yours. Inspect the
author and full thread so a later run under another valid workflow identity
finds the existing question. Anything either search returns is already
handled - never re-ask it, whichever run posted it.

Search for the disclosure string, not for your own display name. Replies from
this workflow are the messages that carry it, and it survives edits. It is
also the only signal a reporter has that they are talking to a bot, so a reply
that ships without it is both undiscoverable here and a small lie in the
channel. Never omit it.

## Classification rules

Phase 0 applies these from parent-level evidence to decide what to claim.
Phase 2 re-applies them once the full thread is read, and retracts an eye that
no longer holds.

Use the workspace's product feedback channel; here that is
`#product-agent-native-feedback` (`C0ATH3CCZT4`) unless the invocation names
another.

**Clear bugs only.** A clear bug has observable broken behavior: a click or
submit does nothing, an action errors, data is lost or reverted, the result is
wrong, or a working flow regressed. A credible "nothing happens" is valid
evidence — inspect the owning path before doubting the reporter.

Do not react, reply, question, or change code for a preference, product idea,
copy or layout suggestion, praise, status update, merge or review request, bot
forward, duplicate, or anything else. Design feedback, including Design clips
and imported-design usability, goes to Sid. Content belongs to Alice. Never
turn a subjective concern into a poll about which option people prefer.

### `:upvote:` overrides the clear-bug gate

An `:upvote:` from **the invoking identity** - not from anyone else - promotes
an otherwise out-of-scope item into scope and authorizes the work. It is the
endorsement that settles the product question: the person who would otherwise
route this away has read it and decided it should happen. Build it.

Do not wait for a second sign-off. The invoking identity is the authorization,
and treating their own endorsement as a request for someone else's permission
is how this rule becomes a no-op.

Find them alongside the newest-message scan:

```
slack_search: hasmy::upvote: in:<#CHANNEL>
```

`hasmy:` is already scoped to the connected identity you verified, so every
hit is an endorsement by definition. Hits are not self-evidently in scope —
the query also returns ordinary replies and old polls that happen to carry the
reaction. Take the ones that name a concrete improvement; skip the rest
without comment.

An upvoted item is a **feature or UX change**, so it is exempt from the
clear-bug bar and from the demand for observable broken behavior. Everything
else still applies: it gets the same `👀`, the same fix-altitude gate, the
same verification, and it counts against the question budget.

The upvote overrides the bug gate, not the ownership map. An upvoted Design or
Content item still gets built — name Sid or Alice in the recap row so the
mapped owner is not surprised by a change in their area. Naming them is a
courtesy, not a gate: do not stall the work waiting for their reply.

Because the upvote already is the product decision, do not ask which variant
people would prefer. Ship the smallest version that delivers the endorsed
improvement, and let the reporter react to something real.

For every authorized upvoted improvement, add `👀` before investigation or
delegation and read it back. Audit it with the clear-bug ledger, using
**Shipped** or **Open - no reply** as terminal dispositions; the Phase 0 release
contract applies.

Phase 0 already claimed these with `👀`. If this workflow earlier eyed
something out of scope, release it with `✅`; do not post a compensating message.

Run an unbounded reaction search across identities as well:

```
slack_search: has:reaction in:<#CHANNEL>
```

Read each matching parent and its reaction metadata. Use other valid workflow
identities' eyes only to detect **Owned elsewhere**; leave those items out of
your worklist. The `hasmy::eyes: -hasmy::white_check_mark:` cursor optimizes the
current identity's scan but is never the only cursor. Keep your active claims in the worklist until a verified fix,
targeted clarification, or Phase 0 release.

Group repeat symptoms into one cluster with one owning investigation; the
repeat gate in Phase 2 owns how they are worked.

For GitHub and Sentry, use native state as the cursor: recent open or
unresolved items with no maintainer disposition, deduplicated against Slack.
If a source cannot be read, record it as **unavailable**. Never report
"nothing matched" for a source you could not query.

## Phase 2: fix

Before changing code, read `fix-at-the-boundary`, `verifying-changes`, and
`concurrent-agents`. Read `ship` when a verified fix is ready to publish.

**Read the evidence the reporter already attached before forming a hypothesis.**
Open every screenshot, clip, and linked artifact. The error text in a
screenshot is usually the whole diagnosis. Track an artifact that is
permission-gated or expired separately from one that was never provided —
inaccessible is not absent.

**Sweep siblings before you claim anything is fixed.** Derive the fingerprint
from the symptom, not the file — the exact crashing token, call shape, or
literal — then search the repo for it and enumerate every hit in your recap
before editing. `fix-at-the-boundary` owns the method. A fix that repairs the
reported route and leaves the identical crash in its sibling is not a fix, and
the reporter was told otherwise.

### Repeats get more time, not the same fix again

Before fixing anything, search the channel for prior reports of the same
symptom:

```
slack_search: <2-4 distinctive symptom words> in:<#CHANNEL>
  sort=timestamp sort_dir=desc
```

Search in the reporter's words — `zoom invalid_client`, `logout twice` — not
your diagnosis. People describe one bug differently, so read the hits rather
than trusting the count.

**A repeat report after a Fixed claim is evidence that fix failed.** It is the
only falsification signal this workflow gets, and it outranks your belief that
the code is correct. Treat it as a stop, not a fresh report:

1. **Find what we said last time** — the prior thread, its **Fixed** reply,
   and the commit behind it. You want the claim that turned out wrong.
2. **Name why it did not take**: never deployed; fixed a sibling path; root
   cause misdiagnosed; or one symptom of several. Each needs a different
   repair, and re-applying the same class of change is how one bug ships
   three times.
3. **Reproduce end to end before editing, verify end to end after.** A passing
   unit test is not sufficient for a repeat — exercise the surface the reporter
   used. `verifying-changes` owns the proof.
4. **Cluster the reports**: one investigation and one fix, not one per report.
   Clustering changes the work, not the bookkeeping — every source thread
   keeps its own recap row, and Phase 3's reply rules apply unchanged.

Record `Repeat of: <link>` and the prior failed fix in each row so the next
run inherits the history instead of rediscovering it. Never tell a reporter a
repeat is fixed on the same evidence that supported the last claim.

Choose the narrowest seam the evidence supports:

- One isolated symptom → fix the owning local seam, add a regression check.
- Repeated or cross-surface symptoms → fix the shared primitive or contract.
- Missing capability or wrong tool → fix discovery, registry, or action wiring.
- Source-versus-live mismatch → diagnose build, deployment, or release state
  before changing source.

Never hard-code a rule for the wording of one report. One data point justifies
a local regression test or a contained fix; it never justifies a global agent
instruction or prompt exception.

### The bar for saying "Fixed"

You may tell a reporter something is fixed only when all four hold:

1. You can name the reporter's **observed symptom** — the error text, the
   ignored click, the wrong value — not just a code smell near it.
2. Your check **fails before your change and passes after**, and it exercises
   that symptom. A test asserting that a prop got threaded through is not a
   regression test for "double-click schedules two emails."
3. The sibling sweep is clean, or the remaining hits are listed and triaged.
4. The change is in the snapshot that ships.

If any of the four is missing, it is not **Fixed**. Say what is true instead,
or say nothing and keep working. A confident wrong "fixed" costs more than
silence: the reporter stops watching, and the bug comes back as a new thread.

An upvoted improvement has no symptom to reproduce, so bar 1 becomes: you can
state the behavior the reporter asked for and the behavior that now exists.
Bars 2–4 hold unchanged — a new capability still needs a check that fails
without it. Call it **Shipped**, not Fixed; nothing was broken.

## Phase 3: reply

`address-feedback-with-replies` owns reply voice, wording, and the thank-first
rule. Follow it; do not restate or re-derive it here. Every reply from this
workflow ends with `this was sent from a bot.` after the plain-language status.

Reply only where the reply carries information the thread does not already
have. Three kinds qualify:

- **Fixed** / **Shipped** — all four bars above are met. Say it will be on
  beta later today. Use **Shipped** for an upvoted improvement.
- **In progress** — the thread already has real, concrete ownership (a named
  PR, a person actively working it). Acknowledge it; ask nothing.
- **A question** — subject to the budget below.

Everything else gets an internal recap row and **no message**. Follow the Phase
0 contract for the eye. A defect you could not fix earns a question, not
silence; record **Open - no reply** only when no question would unblock it, and
do not message merely to hand off.
Never post the same sentence into multiple threads: if three reports share one
cause, reply in one and record the rest as clustered.

Before replying, re-read the full thread to the end. If a human is actively
working it, stay out — do not narrate over someone mid-conversation.

### The question budget

**At most three questions per run, across all sources.** Most runs ask zero or
one.

So rank before you ask. For each candidate, state: *if I get this answer, I
can ship the fix.* Ask the three with the strongest answer. If fewer than
three clear that bar, ask fewer. Everything below the cut is an internal open
item, not a message.

Never ask for:

- Anything already in the thread — a screenshot that is attached, an app the
  message is tagged with, a slide number that is in the linked URL, a file
  type the report already enumerated.
- A run, request, or session ID as the primary ask. Reporters often cannot get
  one — the `...` menu exposing it is not always present — and an unfulfillable
  request reads as a brush-off. Prefer the surface URL, which they always have
  and which usually contains the same id.
- A build number, unless two builds plausibly differ and you will act on it.
- Anything you could determine yourself from source, logs, the linked
  artifact, or the deployed surface. Exhaust those first.
- A subjective product choice — including on an upvoted item, where the
  upvote already made the call. Build the smallest version instead of asking
  which variant they want.
- An internal blocker. Missing test tooling or a broken local install is your
  problem, never a reporter question.

At most one clarification question may be pending per thread at a time. Once it
is answered or resolved, attempt the fix; if that exposes a different required
detail, ask at most one new, non-repeating question. Never stack questions or
repeat a pending one. If a needed artifact is inaccessible to you, ask for a
fresh link - not for its contents again.

### Ask a fork, not for evidence

The questions that got fast answers named two candidate causes you had already
located and asked the reporter to pick:

> is the logout happening in the browser, the Desktop app, or both? that one
> detail will help isolate the session path.

> can you confirm whether the stop/pause controls are visible in the saved
> video itself, or only over the shared playback page while viewing it? the
> fix path differs between capture exclusion and player chrome.

The ones that went unanswered outsourced the investigation:

> can you share the Clips build and whether this happens in the native desktop
> bubble or the browser share page?

> can you share the run id for the .fig indexing failure?

The difference is who did the work first. A fork proves you already read the
code and narrowed it to two seams; the reporter spends five seconds and you
can ship. An evidence request means you have not started, and it reads that
way. If you cannot name the two candidate causes, you are not ready to ask —
go read the owning path.

Write it so they can answer in one line from memory, in their own words,
without opening a devtool.

## Verification and identity

Follow the `## Slack identity` contract in `address-feedback-with-replies`:
confirm the connected profile is the invoking user before the first write, and
keep that identity for every read, reaction, reply, and read-back.

Resolve the Slack, GitHub, and Sentry tool schemas once at the start of the run
and reuse them, rather than re-searching the catalog before each call. This is
minor — 2.6% of exec calls across 40 measured runs, 10% in the worst one — so
do it and move on; it is not worth a pass of its own.

For every Slack write: use the exact parent `thread_ts` from a full-thread
read, never a search-result or adjacent timestamp, and re-read after posting.
Do not close, label, assign, or comment on GitHub or Sentry unless the
invocation authorizes it; link them in the recap instead.

## Publishing

A worktree is a valid PR source — commit, push, and open or update the PR from
this worktree's branch and cwd. Use `corepack pnpm ship:push` for the complete
snapshot and update the existing PR rather than opening a second one.

With shipping authority — an explicit request, or a caller that already
granted it — continue straight into `ship` in the same worktree without asking
again. Without it, prepare the ready-to-ship handoff and say shipping is
pending authorization. Carry the start cursor, grouped reports, evidence
links, owning seam, sibling-sweep results, and every disposition into the PR
body. Keep source-tested, built, deployed, and observed-live claims separate.

If the sweep found no verified fix, finish with the recap and say why no ship
started. Clarifications, unavailable connectors, and external failures are
not shipping blockers.

## Recap

Every item inspected gets a row, including ones you deliberately stayed silent
on - that is how silence stays auditable.

```md
## Feedback sweep
Start cursor: [Slack message](...)
Messages enumerated: N · Claimed: N · Answered since last run: N
Questions asked: N/3 · Dropped at 4 days: N
Repeats of a prior Fixed claim: N (each with its earlier thread and failed fix)
Upvoted items in scope: N (built: N)

| Source / item | Disposition | Replied? | Eye | Why and evidence |
| --- | --- | --- | --- | --- |
| [Slack thread](...) | Fixed / Shipped / In progress / Asked / Open - no reply / Clustered / Resolved elsewhere / Skipped / Abandoned - no answer in 4 days / Owned elsewhere | yes / no | held by me / held by other / released with `✅` | ... |

Sibling sweep: <fingerprint> - N hits, M fixed, K triaged
Unavailable or unverified: ...
```

`Open - no reply` is a last resort, not a success state. It requires that you
worked the defect, could not fix it, and could not form a question that would
unblock it; a run whose ledger is mostly `Open - no reply` has under-asked, not
finished. It always means the eye was released with `✅`. "Nothing matched" is valid only after each
source was queried successfully, with the cursor stated.

## Related skills

`address-feedback`, `address-feedback-with-replies`, `fix-at-the-boundary`,
`concurrent-agents`, `verifying-changes`, `ship`

