goes to another screengoes to another state of this screenfaded = not mocked yetEverything else — filters, sorts, tabs, modals — works, it just stays on this screen. The URL always names the state you are on, so any view can be copied and shared.
Runs / Batches/Listing builder — eBay/J-0112eBay — main — channel account CA-02, no store contextCreated 2026-08-29 09:41 · resumable. Account defaults were copied onto the job at creation.
The account decides the marketplace, and it brings its operating defaults and price ladder onto the job. Defaults are copied at job creation — later edits on the account don't change a job already in flight.
Copied from CA-02 operating defaults
ConditionNewQuantity10 units (start, adjust later)ShippingeBay Free, 30-dayHandlingNext business dayShip-fromMiami, FLReturnsStandard policy (reused, never new)
Price ladder2-pack −5%/unit3-pack −10%5-pack −15%sale single −20%Edit on account →
Completed 09:41. Changing the account now would restart the job — SKUs, defaults and collision checks are account-bound.
Products on this job
Pick the catalog products the packages will be built from. Each pick shows what this account has already listed for it — start from a prior listing with Sell similar instead of from scratch.
Add another product
2 products picked. Combos become available in the package builder whenever a job holds more than one product.
Copied from CA-02 at job creationLadder —Sales channel fee 13%Promoted ad rate 2%Margin floor 35%set on the sales channel — not mocked yetPostage USPS Ground Advantage →
Packages are locked while drafts hold an approval.Changing a rule now invalidates the draft approval for every listing it moves and re-runs their collision checks.
1
Product Pool
2
Shape
A pack size and its discount are added together — the ladder has no row without one.
3
Free with this listing
4
Base Prices
🔥
One-unit item
Price
5
Price Ladder
Tier
Off per unit
Price
Per unit
G-margin
6
Promotions
Listings
PROMOTED
SKU plan approvedPaul, 09:41. Nothing is minted here — this is a plan. Approving it is what starts the draft generation on stage 6, which is the spend this gate sits in front of.
Proposed SKUs
4 of 4 clear · checked 09:39
Edit any SKU and the duplicate check re-runs on the row, against both systems.
Type
Units
Channel SKU
Duplicate check
Packages
Nothing is minted on this stage, so every SKU here is still free to change. The recipe in Packages is what a reader checks a SKU against; the finished good each unit resolves to belongs to the catalog product and is confirmed on stage 8.
Creative brief optional
Nothing here is required. What you set feeds title, description and image prompts across the whole job.
Sale type
Attach a campaign — its SKU token is stamped into every slot (OB-MEMO-3P-SUM-EBY) and its image set pre-fills shared slots — or type a one-off theme that only guides prompts.
What the first drafts should lean into. Every content and image prompt on this job reads it. To correct a draft afterwards, use Export to the agent.
Benefits
From the job’s products — Memopryl: Memory, Focus, Energy · CogniHoney: none. Changes here stay on this brief.
Marketing angles
Who these listings are pitched at, or the moment they sell into.
Completing this step created job J-0112 at 09:47 — it's resumable from the Listing builder list from here on.
Status strip
Compliance changed after these drafts were writtenThis account’s compliance rules were edited on Sep 14 at 11:05. The drafts below follow the old rules. Generate them again, or send the job through Agent Export, which reads the new rules.See the compliance rules ▸
Generating
Listing rail4
SKU and title
readyrunningneeds attentionapprovedreview
Listing draftVitaLifes Memopryl Memory Support Supplement Memory & Focus 60 CapsulesL1 · OB-MEMO-1-SUM-EBYSINGLEListing state: ready to pushdraft 2
No drafts yet.Generate drafts writes all 4 listings on Template 1 — the campaign, the brief and each listing's facts are already in the job.
Draft fields
Title
71/80 — over eBay's 80-char limit; approval is blocked until it fits · Brand + Name + Type + Key feature + Count
Category
eBay-suggested · verified by drafter — regulated category, never re-pick for exposure
Item specificsaspect list provisional
Drafted from catalog facts — "never guess": blank beats invented. A blank says why it is blank. Count is per-package (L2 says 120, L3 says 180). Bundles and combos send no UPC.
Description
0 rendered HTML characters·house target 900·eBay limit unknown — not in any reachable source
Markdown, rendered to eBay-safe HTML on send. The counter measures the rendered HTML, not the Markdown source, because markup counts against eBay's limit. The toolbar offers only what that HTML subset can express — no links, no images, no raw HTML.
Drafted per package from the job prompt (campaign + brief injected). value-adds bullet provisional — no value_adds field exists on the source formula yet.
Claim gate
Claims
Claim gate — clear. 3 claims matched the approved claim library; no BLOCK or REVIEW findings on this listing's content.
1 resolved finding
REVIEW (resolved 10:14): draft said "clinically proven recall" — not in the claim library. Reworded to "formulated for memory & focus"; approved in r2.
Review flagsrecord provisional
Priceladder · sale single −20%est. margin 63% · shipped total $31.99
From CA-02 account defaults — copied into the job at its start, not asked per listing.
ConditionNewQuantity10ShippingeBay Free, 30-dayHandlingNext business dayShip-fromMiami, FLReturnsStandard policy
Agent exportJ-0112 · copied
Paste this into a Codex or Claude Code session opened in the merch-riot-workflow project. It carries the job id and nothing else — the harness pulls the listings, dispositions and facts through the API, asks you about the ones marked review, proposes the rewrite, and pushes it back with a status per listing.
Templatestandard supplement listing5 slots0 selecteddrag onto another lane to move them, column for column
ShipStation products
Mints one ShipStation bundle product per listing on this job — the record that ties an eBay SKU to the bottles the warehouse picks off a shelf. Two jobs per listing: the bundle product itself, then the two settings that only exist inside ShipStation's own website. Physical SKUs are not minted here — they arrive with the catalog product and are only confirmed below.
writes to ShipStation on CA-02 · nothing is sent to eBay from this stepShipStation sign-in status: sign-in healthy · 6 days ago
ComponentsStatus: 2 of 2 have a ShipStation product and a weight
FP-COGH-30 has no weight. ShipStation would rate every order of the listing that contains it as though the box were empty, and we would pay the difference. This is a catalog problem, not a job problem — fix it on the catalog product, then start the sync.
Not started. The step finishes when every listing's bundle product is built, read back and matching, and both website-only settings are confirmed with a screenshot.
View
ContentSection status: approved
ImagesSection status: assembled
ProductSection status: approved
Pricing & fulfillmentSection status: approved
Every listing in job J-0112, with what each one is waiting on. Column headers sort; drag a header's right edge to resize it, double-click to reset.
PushPush to eBay · CA-02
Pushing creates run B-0114 and hands off to the run cockpit — APPLYING → RECONCILING, then the live-listing spot-check. Partial push is normal: ready listings go now, the rest follow when their blocks clear.
Mode
dry run is the default for a new batch — logs “would do X”, writes nothing
Design notes — MR-045 Listing Builder
Design: references/ebay-planning/LISTING-BUILDER-DESIGN.md (approved 2026-08-29). Dedicated screen, superseding the new-listing slice of MR-031; the other five presets stay on scr-newjob.
This job is on CA-02, and the fee is the sales channel fee (MR-395)
The job said it was on CA-01 · eBay · Response Commerce (US), and CA-01 is the Vital Depot Shopify store everywhere else — SAMPLE-DATA, the accounts list, the listings screen and the account screen. The eBay account this job copies from is now CA-02 · eBay — main throughout the screen, and the second card is CA-03 · eBay — outlet, drawn with its lost connection, where it had been an eBay account named for Visible Science that no fixture holds. The strip’s Final value fee is Sales channel fee, the name every channel shares on the account screen. Nothing else about the job changed; the ladder’s internal key is still ca01.
A free item that is not ready is refused (MR-399)
CP-0311, the Memopryl sample sachet, is not sold on its own and its front image is missing, so its pill in Free with this listing is disabled, tagged NOT READY, and the reason is named under the pool with a link to the product (RI-802).
Free with this listing, and the compliance notice (MR-394, MR-396)
What goes free is the listing’s choice. The Gift Pool is now Free with this listing and the card tick Can go free. A product the catalog says is not sold on its own (the Riot gift bag) is always in the pool, locked, and is no longer offered by the product search — it is never a product to sell (RI-786, RI-787).
Drafts say when compliance changed after them. Compliance is read live from the account (RI-785), so the Drafts stage shows an amber notice when the rules were edited after the drafts were written; generating again or Agent Export reads the new rules (RI-789).
The creative brief: Benefits and Marketing angles are two fields, and Guidance is the first draft’s (MR-380 / RI-747 … RI-749)
Two ideas had been one row of pills. The brief’s Benefits angle drew from the Benefits list, so Joints — what a product does — sat beside Over 50 — who buys it. Paul split them on 2026-09-14. Benefits start from the job’s products and can be trimmed or widened from the same closed list the catalog uses; nothing typed here changes a product. Marketing angles come from their own list, which belongs to listing jobs in general and is administered on the Listings screen (sales-channel-listings.html#state=angles), so the Shopify brief can read the same one. That list is open: a brief may add an angle, and the add says it went into the shared list. Both fields are job-level, as the angle was (RI-088).
The stage opens mid-search on purpose. Paul asked to see both searches with their autocomplete showing, so s is typed in Benefits and er in angles. Press anywhere else and they close; a real screen would open with both empty.
Guidance is not the box RI-513 refused. That one sat on the drafts and fed a regenerate — a second way to correct copy beside Export to the agent. This one sits before anything is written: it is one more input to the first generation, read inside prompts the operator never sees, and a correction still goes through the agent. The Shopify brief does not have it yet (P89).
What left: the pill row, the Or a custom angle text box (an angle typed into one job was reusable by no other) and the hint that explained per-listing overrides in the page body.
SKU Plan is stage 4, and the chicken-and-egg is a false cycle (MR-336 / RI-622, RI-624, RI-625)
Ported from the Shopify builder’s tab 4 on 2026-09-11, minus CheckoutChamp, which is not on
this channel. It sits before the brief and the drafts.
The question it settles. Paul, 2026-09-11: you mint a SKU, but what do you call the
thing it identifies? — and the mirror of it, you write a title, but for which SKU?
On eBay this bites harder than on Shopify, because an eBay listing is a package: there is no
product record above it carrying a name.
There is no cycle. The SKU comes off the package — its products, its pack shape, the
open-brand marker, the account’s channel token. The title comes off the package too. Neither is
derived from the other; OB-MEMO-2P-SUM-EBY and “VitaLifes Memopryl
… 2 Pack … 120 Capsules” are siblings, both built from Memopryl, 2-pack,
open brand. The SKU only looks like the parent because it is readable.
So the SKU is settled first and the drafts rail names it. That rail is already headed
SKU and title — it was built that way before this stage existed, which is the strongest
evidence the order is right.
Packages is a recipe, not a name — and that is the point (MR-337 / RI-624)
A package has an internal name — Memopryl × 3,
Memopryl + CogniHoney, built on the Packages stage — and it breaks
at four members. Four different products glued with + is a string
nobody can scan, and four is where it starts, not where it stops — MR-338 removed the
count limit, so the label grows with the pool. It was never the real name either: that arrives from
the drafts.
So the column does not try to name the package. It stacks the parts, one line per member,
which reads identically at one member and at four. Above them sits the listing title when there is
one, read live from L; a job whose drafts have not generated shows no
listing title yet rather than an invented name, so the missing name is a visible state.
The recipe is also what actually verifies a SKU. The question the stage asks is whether
OB-MEMO-COGH-BNDL-1-SUM-EBY is right, and a parts list answers it where a
title cannot — a title is prose and can be wrong in ways a count of bottles cannot be.
Two systems join the check, not three. eBay, because the SKU is the listing’s identity
there. ShipStation, because the bundle product carries the same SKU. CheckoutChamp is not on this
channel and QuickBooks mints nothing here.
It reads L and holds no listings of its own. The drafts, images,
build and review stages all read that one model; a fifth copy is how the two image-generation dialogs
drifted inside a single round (MR-322). items was added to
L as the structured form of the count prose the
drafts already show, so the composition is stated once.
There is no limit on how many products go in a box (MR-338 / RI-630)
Paul, 2026-09-11: "we definitely have use cases where there's up to four that commonly happen;
beyond that it's not so clear, but I don't think there's any reason to limit it."Four was a
policy number with nothing behind it — it had been the bound since MR-075, and no rule,
carrier or contract ever required it.
What a box can hold is physical, and that was already modelled.shipFor() fits every row by weight and volume against the real package
set and flags one it cannot fit. The count of four was a second answer to that question, and
the worse one: it refused a combination that fits and said nothing about one that does not. Removing
it loses no guard. Measured on the current fixture, the packer simply steps up — three members
take a Poly Mailer Large, four a Box (8×4×4),
five a Box (7×7×7), none oversize.
The ceiling is now the pool, because that is the only real one: a combo cannot name the
same product twice, so it can never hold more distinct products than the Product Pool offers. The
Up to spinner takes its max from the pool and re-clamps when a
product is switched off, rather than leaving a bound that names combos which cannot exist.
The gift rule went with it. MR-075 gave a row already at the bound no gift variant, because
the gift competed for one of the four slots. With no count limit there is no slot to compete for, so
every non-gift row gets one — and the four-member combo becomes a five-item box, which
the packer sizes without complaint. The control’s label dropped gift included for the
same reason.
It also made RI-624’s hard case walkable. The four-member combo was argued from a
fixture that topped out at two; it is now a row you can see, and it reads
Memopryl + CogniHoney + Beet Root 500mg + Avinol PM — the exact
unreadable string that is the reason the SKU plan stacks a recipe instead of printing a name.
The strip keeps the number and colours it, and says nothing else (MR-333 / RI-622, RI-623)
Same change as the Shopify builder, same reasons. A completed tab used to swap its number for a
tick — losing the one fact the circle carried, at the moment the reader is counting back to
find a stage. The number stays, the green fill stays, and the tick goes. The second line under every
tab (2, draft 2 approved, 6 of
12 done, not started, 1 of 4 ready) goes
with it: each was a second copy of a count its own stage states better
(MOCKUP‑GUIDANCE §10), and together they turned a sequence into a status board. The
four JS writers feeding them were removed rather than left pointing at absent nodes.
Stage 7 is renamed Build, matching the Shopify builder’s tab 7. The card inside
it still says ShipStation products, because that is what the stage builds — the tab names
the act, the card names the artifact.
What the fixture demonstrates
Count and type are derived, never asked — 4 slots → 4 listings; SINGLE/BUNDLE/COMBO computed from slot contents.
Collision check as the error-killer — L2 carries the resolved history of an operator SKU edit colliding with a live listing (the spreadsheet's swapped-fan-out bug class).
Gate invalidation — packages are locked while drafts hold approval r2; editing reopens r3 and voids downstream image approvals.
Prefill, not lock — the Memopryl shared set pre-fills L1–L3; L2 shows a detached custom lifestyle slot (ink underline = this listing only).
Two claim gates — content gate cleared in drafts; the image claim check catches L2's "boosts memory in weeks" overlay.
Partial push is normal — 1 of 4 ready; dry run is the default mode for a new batch.
ShipStation is its own stage (MR-053) — stage 7, between Images and Final review. Two jobs per listing, stated separately: the bundle product (read-back proof, never a bare 200) and the two website-only settings the robot sets. Failure recovery lives on the failed row, not in Attention; Attention only links back.
Cost telemetry — every generation logged with cost in the activity rail (REQ §13.1).
MR‑077 — stage 3 is the package rule stack, and package-builder.html is gone
Stage 3 was a fixed grid of four slots with hard-coded SKUs and prices. It is now the rule stack that makes the slots — Product Pool → Shape → Gift Pool → Base Prices → Price Ladder, and the listings they produce. The stack was built as its own screen (MR‑064 explored three designs, MR‑075 picked this one, MR‑076 added job pack sizes); that file is deleted rather than kept, because two screens that can each define a job's packages is precisely how they drift.
What the fold changed, and nothing else did. The standalone page's own header, breadcrumb and listing chip go — the stage already has a stepper and a job header. Its count now lands on the stepper's sub-label, which used to read a hard-coded 4 slots, and on the step footer. The lock gate survives: it is the listing builder's own rule, not the packages screen's, and the stack never had it.
Two counts that disagree, on purpose. The job header says 4 listings and the footer says the rules make more. Both are true: the job holds four approved listings — drafts, images, ShipStation and final review are all built on them — while the stack describes what a re-run would produce. That gap is exactly what the lock gate is about, so the footer names both rather than letting the header look stale. Open: the downstream stages' fixtures are still a four-listing job; rebuilding them against a 26-listing one is a separate round.
The sidebar collapses here now too, to the 58px icon rail, taken verbatim from catalog-product-setup.html rather than re-derived — the label survives as a native title while collapsed, because a CSS tooltip is clipped by .sb's own overflow.
Pack sizes were the open variant — settled in MR‑186.Account set only against + job sizes; a job may add sizes of its own, so branch 2 is now the only behaviour. The harness toggle, the packs= hash key and the index’s variant links are gone, and Add pack size and the per-row remove are unconditional — which is everything the losing branch changed.
MR‑078 — stage 2 searches the catalog, and the picked cards come off the same list
The Add another product field was a dead input under a dead heading. It now filters the account’s catalog as you type — name, brand, SKU and UPC, the four things its placeholder already promised — and a row adds that product to the panel as a card. Arrow keys move, Enter adds, Escape clears; the query and the focus survive an add, because picking three products out of one search should be three clicks and not three searches.
Every row carries its thumbnail. A supplement is recognised by its bottle long before its SKU, and two of these products are collagen. Only two have a catalog photo, so the rest fall back to the same four-letter initials block the cards already use — the fallback is the common case here, not the exception.
One list, both halves. The two cards the job started with are no longer hand-written markup: they render from the same fixture the search reads, marked picked. That is what makes removal honest — a product taken off the job goes back to being searchable, and every card’s × works, not just the ones added this session. Two lists would have meant a product could be in neither.
Gift‑eligible is a tick on the card. Rung 3 of the package stack has always said the flag is set “in the Products step” — the step had no such control, so the sentence described a screen that did not exist. It does now: one checkbox per card, and a product the catalog calls a giveaway (the Riot gift bag) arrives ticked, disabled and tagged catalog, because unflagging it is the catalog's decision and not this job's. Everything else about the gift — whether variants are built at all, and what they cost — stays in rung 3, where the maths is.
What it does not do. The search is fixture-local: adding a product here does not carry into stage 3’s Product Pool, which holds its own four-product fixture. Nothing is scoped by channel account either — a real catalog search would be, and would page. Sell similar is still a dead button.
MR‑084 / 085 — a per-unit price on every row, a ladder that shows margin, and a workspace that fits it
The Listings table carries Per unit beside Price — the price over the Units column next to it. It is the number a buyer compares across pack sizes, and until now the only place to see it was the ladder’s example column, which prices one product.
Rung 5 loses its net multiplier and gains a gross margin.2.85× was the discount restated in a second notation: it said nothing the −5% beside it did not. Gross margin — (per unit − cost) / per unit — is the question the tier is actually being set against. Price and Per unit are two columns rather than a price with a subscript — the per-bottle figure is what the tiers are compared on, so it reads down its own column instead of trailing the pack price. The header carries no tooltip: the picker directly above it already reads Memopryl — $39.99 a bottle, $7.40 cost, and a tooltip that restates its own control is one more thing to keep true.
The example product is picked, not assumed. It was pool[0], so the margin the ladder implied moved when somebody toggled an unrelated product in rung 1, with nothing on screen to say why. A dropdown beside Model names it, and the choice supplies both numbers: the one-bottle price saved in rung 4, and the cost from the catalog — the same figure the Listings table’s Cost column already spends. No new fields and nothing typed. A product switched off in the Product Pool hands the example back to the first one still in it, and picking one prices nothing: it redraws two columns and writes to no record.
Gross margin is not the 35% floor sitting inches above it. That one is net — it also takes off shipping, the 13% fee and the ad rate (D21). The hint under the table says which is which, and the column takes no colour for being low; only a negative reads red, because that is unambiguous under either measure. This is MR‑083’s rule, kept deliberately identical on the screen where the two numbers can be seen at once.
Promoted listings becomes rung 6, Promotions. It was a stray checkbox under the ladder’s table, which read as a property of the ladder; it is a job-level fee decision that moves every margin in the table beside it. Its own rung says so, carries the state in the header count (off · 13% fee only), and leaves somewhere for the rest of the promotion levers to land.
The Packages table is deleted. Tare, material and postage are inputs to a number the Listings table already shows — Ship — and the Package column already names which box each row lands in. What only that card said now lives in the listings tooltip: package is picked by weight, Ship is postage + material at the static v1 USPS rate (MR‑047), and an over capacity row is priced at a box that cannot hold it, so its cost is understated. The data is unchanged — PKGS still drives every row’s package and postage; only the read-only display of it is gone.
The listings card is as tall as the rule stack, and only then scrolls. It was a fixed 640px window regardless of how much was in it. Now it grows with its rows up to the height of rungs 1–6 and scrolls past that, with the totals bar pinned under it. The cap has to come from the other column, which is why the two columns are a grid: the listings column is kept out of the row’s sizing (height:0) so the row is as tall as the rules, then fills it (min-height:100%). A short job still draws a short card — nothing is stretched to fill.
Every listings column sorts, and the two columns scroll independently (MR‑086). Each header is a real <button>, so the keyboard gets sorting for free; a second click reverses, build order breaks every tie, and # keeps the number the row was built with rather than renumbering — a listing is the same listing after a sort. Sorting reorders what is shown and touches no price, no rule and no record.
Stage 3 is a two-pane workspace, not a long page (MR‑087). The stage is exactly as tall as what is left of the viewport; each column scrolls itself, and the listings table scrolls inside its card under headers that never move. Rung 6 and row 52 are both reachable, and neither costs you the other.
Sticky was tried first and it failed in the one place that matters. MR‑086 pinned the listings card under the sticky header, which worked until the page scrolled past the rule stack’s own height: a sticky box can never leave its containing block, so the card was pushed up from below and took the column headers with it. Runway you cannot extend is not a fix, and the right answer was to stop scrolling the page at all here.
Two numbers are measured in script rather than hard-coded — the header’s height, because it wraps at narrow widths and a fixed offset would be wrong exactly when it matters, and the scroller’s free height, because 100vh overshoots by the harness bar and by the 60px of bottom padding the long stages need. Under 560px of workspace the stage gives up and scrolls the page instead: two slivers are worse than one page.
MR‑095 — stage 6.1 is a second Images screen, drawn beside the first
The Images stage is being redrafted. Rather than editing stage 6 in place and losing the thing the redraft has to beat, 6.1 is a second pane in the same file: same job, same fixtures, same chrome, one click apart on the stage strip. Reviewing a replacement means flipping between them, and that only works if both are live at the same URL — #stage=images against #stage=images2.
It is a mockup state, not a job step. A real J‑0112 has no step 6.1; the pill is wide and grey and the sub-label reads redraft so the strip cannot be mistaken for the product’s own numbering. The harness bar carries the same jump. When the redraft is settled, one of the two panes is deleted — whichever loses — and the survivor takes the images id back. Two Images screens is a review device with an expiry date, not a design.
Stage 6 is not touched while this runs. No shared ids, no shared script: the by-asset / by-listing toggle, the role expander and the attention deep links all still belong to stage 6 alone, so a change in 6.1 cannot quietly move the baseline it is being compared against.
MR‑096 — stage 6.1 drawn: a lane per listing, a column per template slot
The row is the listing. Stage 6 organised by asset — a shared supplemental set here, a hero list there, and a segmented control to see it the other way round. The thing an operator actually works on, this listing's picture set, was the one view neither tab gave you whole. 6.1 makes it the row you read across: four lanes, five columns, and the answer for L2 is the second line.
The columns come from a template, not from the listing. One template per job — Main · Benefit 1 · Benefit 2 · How-to · Testimonials — so a column means the same thing in every row and the grid is readable down as well as across. The five the template ships with have settled types, so their headers say the name and nothing else; a column you add has to declare what kind of image it holds, and that one alone carries the type picker. Either way Add column extends the template and the new slot appears empty in every lane, because a template with per-lane exceptions is not a template. An added column can also be taken back out, and the two cases are deliberately not the same gesture: an empty one goes in a click, while one holding images says how many and how many of those are not in the library yet before it will go. Approving an image files it (MR‑092’s library), so approval is already what makes it outlive its slot — the warning is specific rather than an are-you-sure, which is the only kind anyone reads.
Main is the only slot that opens with something in it, tiled from the catalog front photo of each product in the listing. L4 draws Memopryl's photo beside an explicit no catalog photo tile, because CogniHoney has none in the fixtures. That is a fixture fact, not a rule about combos.
Sharing is gone, and that is the trade. Every slot belongs to one listing; two lanes showing one picture hold two independent slots. Selection stays lane-scoped because a selection spanning two lanes has no column-aligned drop. The cost is named rather than hidden: re-doing one benefit image across four listings is four actions, and nothing on screen says those four cells are the same picture. At four listings that is fine. At twenty-six it is the first thing that will need answering.
Dragging a slot moves it — in any direction, and never as a copy. Drop it on any other cell, in any lane and any column, sideways included. If the target already holds an image the two swap; if it is empty the source is left empty. Nothing is duplicated, so there is no modifier to remember and Ctrl means nothing here any more. A multi-slot selection is the one exception: it still drops column‑for‑column onto another lane, because N columns have no unambiguous mapping onto the single cell under the pointer.
Main is never overwritten by a move. A cell‑to‑cell drag cannot land on Main while Main holds anything, the seed included — the target refuses rather than swapping, because a swap still replaces what Main was showing, and Main is the one picture a marketplace leads with. An empty Main is a fair target: nothing is lost, and moving a pack shot generated into the wrong column is the repair worth keeping. Generating into Main and dropping a library image on it are unaffected — both aim at Main deliberately rather than passing through it. There is no button that can do it either: the ←/→ pair that used to sit in the hover cluster is gone, because a drag reaching any cell in any direction already covers the by-one nudge they existed for.
Dragging from the gallery is not a move, and cannot be. A slot holds a reference to a library asset rather than a copy of the file, so the library keeps its row and the same picture can sit in four lanes as one asset.
A filled slot carries three actions on hover, lower right — it briefly carried five, and the ←/→ pair came back out once drag could reach any cell: ↓ uses this image for the same column one lane down, ↓↓↓ uses it for that column in every lane, and the bin clears the slot. The work is nearly always “this benefit image is right, use it for the other pack sizes”, and those are the only two reaches that come up. Nothing is duplicated — a slot holds a reference, so one library row can be the answer in four lanes at one file’s cost.
An image type has a stored value and a name, and only the name is shown.ingredients is a storage key; Supplement Facts is what the team calls it. The list is ordered by the practiced eBay sequence (MR‑030) rather than alphabetically, so the next thing you want is where you expect it, and both controls that offer a type render from that one array — the vocabulary cannot be right in one place and stale in the other.
Confirmation goes where the reach exceeds what you pointed at. The bin and ↓ touch one cell you can see and aimed at, so they just happen. ↓↓↓ and remove column reach cells you never pointed at, so they state the cost first — and only when it is real: a slot already holding the very image being pushed into it counts as nothing, and an image already filed to the library survives being replaced. Without that exemption the natural sequence — ↓ to check it looks right on the next lane, then ↓↓↓ for the rest — would warn you about destroying a copy of the image you are spreading, which is the fastest way to teach somebody the warnings are noise.
The generation popup is draft-punk's cell modal, ported rather than reinvented (mockup/generate.html): big image, version filmstrip, reference strip, and the controls that spend money kept together on the right. The preview never crops. The grid’s thumbnails do — uniform squares are what make a column scannable — but this is where an image is judged and approved, and approving something you have only ever seen cropped is how a clipped logo or a cut-off claim reaches a marketplace. Two things are new. The prompt is editable — draft-punk showed it read-only — and prompt type, prompt and guidance are three separate inputs rather than two, so an operator's one-off correction cannot silently become the house recipe.
Amended 2026‑09‑10 (MR‑322, RI‑570): the popup moved into _image-module.js as MRImageGenDialog, and the Shopify builder mounts the same one. Its shape is unchanged — this screen’s — and that is the point: MR‑321 shared the library panel beside it and re‑drew the dialog on the other screen, which put the reference images in the settings column and lost the model select and the cost line. Paul saw the two side by side and asked why they differed; there was no answer. Prompt type is now a _picker.js search filter on both, over this screen’s own six recipes here and the twenty‑label administered vocabulary there. Model stays a plain select — two options, and a search box over two rows is furniture. What is left in this file is what has a fixture in it: the prompt recipe, the generation pool, the version model and what Approve files.
The gallery shows one library — the one this screen belongs to. REQ §9.0 (MR‑071) settles three libraries, one R2 prefix each, and every screen opens the library that belongs to it. A listing builder is a channel-listing screen, so the panel is sales-channel/ and names its prefix on screen. It read both that and catalog-product/ when first drawn, which broke the rule and then needed image-type pills to make the merged pile navigable — the pills were standing in for a folder, so the folder replaced them. Search still covers filename, label and role together, as §9.0 asks.
Approving files the image, and the panel shows it arrive. A new row, tagged with its image type, marked just filed, draggable into any other lane from that moment — which is REQ §9.0's whole promise, that an image uploaded once is in the library from then on rather than re-uploaded by the next record that wants it. Approve is the cataloguing act, not the storage one: the binary already exists (the preview renders it, the filmstrip returns to it, the cost log counted it) — what approval adds is the role and the row. Four lanes showing one picture still file one row, because they are one asset in four slots.
The base photos still reach this screen, by the other door. They seed Main, and the popup adds them as generation references — which is the split §9.1 already draws: catalog images are what generation draws from, sales-channel images are what a marketplace shows. Two prefixes, two jobs, two ways in. It stays true only while every column wants marketplace art; a template that gains a Supplement Facts column would want the other prefix, which is open below.
What this screen does not do yet, stated rather than implied: there is no image claim gate — the check that caught L2's “boosts memory in weeks” overlay on stage 6 — and no per-job image spend. The per-generation cost estimate survives in the popup, so cost is not invisible, but the job total has no home here. Both are open below rather than improvised.
Requirements are being written as the screen is drawn, not mined out of it afterwards, in docs/requirement-inputs/RI-images.md — twenty-two records so far, each with what it asserts, why, and what it rules out. The rejected shared-set model is recorded there too, with its reason, so the same argument does not get had twice.
MR‑099 — a blow-out column in rung 4, a sale in rung 6, and a column that scrolled for nothing
Blow-out is a tick that makes a product cost nothing. The case Paul named: stock you are about to write off. Cost is the number that makes that inventory look like a loser — it is real money, but it is money already spent on something heading for disposal. Zero it and the question becomes the only one that still matters: does the sale clear the fees? If it does, the listing is a win against an alternative of nothing.
The zero is not a display trick, and that is the whole point of putting it in rung 4. Every price on this screen runs through one costOf(p), so a ticked product reads zero as a single, as every bundle of itself, as its share of a combo — Memopryl + CogniHoney costs $12.40, not nothing — and in the ladder's own gross-margin column. A second cost field somewhere would have given two answers to one question.
A free-text cost override was the obvious alternative and it was dropped. A typed cost is a second source of truth for a number the catalog owns, and it would have to be reconciled with the Cost column beside it. The only case anybody has asked for is zero, so the control is the one case, as a checkbox.
The column is One-unit item now, not One-bottle item. The pool already holds 30 servings of honey and a gift bag; the header was describing the fixture's most common shape rather than the field. The word bottle survives where a bottle is really meant — a combo's bottle count, the ladder's per-bottle comparison.
The emoji is the tooltip. A 🔥 with an i beside it would be two glyphs explaining one column, so the emoji carries the bubble itself, on hover and on keyboard focus, and takes no help cursor — a question-mark pointer over a column header promises a help system that does not exist here.
Sale is rung 6's second row, and it is a ladder-reading tool. Fifteen, twenty or twenty-five per cent off every price the ladder derives. The hard thing to picture is exactly what it answers: with four pack sizes, four costs and a discount curve already applied, whether a further 25% still clears the floor is not something anyone works out in their head. Turn it on and the Listings table re-reads — price, per unit, fees, profit, margin, and the red under-floor cells — and rung 5's own Price / Per unit / G‑margin move with it.
Fees follow the discounted price, because the marketplace takes its 13% of what the buyer actually pays. Sale and Promoted stack: a promoted sale row pays 15% of the discounted price.
A price typed into a listing row by hand is left alone — D20 unchanged, and the same rule that already protects a typed price from the base-price cascade. The sale is a rule about derived prices; a hand-set price is not derived. The trade is that a row someone has already priced by hand does not move when the sale goes on, and nothing on the row says so.
The percentage is unreachable until the box is ticked. An armed dropdown beside an unticked checkbox reads as a live discount, which is a lie the operator has to test by looking somewhere else.
The rule column scrolled sideways for something nobody could see. Reported as a layout bug — the column drags left and right with nothing visibly wider than the column. It was the header tooltips: the bubble is 330px, anchored left:−10px on an i that sits up to 155px into a 428px column, so it projected to 475px. The column sets overflow-y:auto, and CSS then promotes overflow-x from visible to auto — a scrollbar for a pseudo-element at zero opacity. The tooltip is now anchored to the card header rather than to the button (left:0 on a positioned .rh), so 330px starts at the card edge and ends 98px short of the far side. Measured after the fix: scrollWidth === clientWidth === 428. Hiding the overflow was the other option and it was refused — overflow-x:hidden would have clipped 47px off the right of every tooltip to cure the symptom.
Requirements for this round are indocs/requirement-inputs/RI-pricing.md (RI‑034 … RI‑042), written as the controls were drawn, per GUIDANCE §11.
MR‑100 — the Listings header says SALE, loudly
A sale moved every price in the table and the table did not say so. The state lived in rung 6, three cards down the other column, and in a small grey tag in the footer strip — both true, neither of them where the numbers are read. The badge now sits in the Listings header beside the row count: SALE 20%, bold, on a saturated yellow. It follows the percentage as it changes and disappears when the sale is switched off.
The footer tag came out. One statement, in the place the affected numbers are read — two indicators for one state is how they end up disagreeing.
The yellow collides with GUIDANCE §2 and the collision is deliberate. Amber is attention-only, and by the letter of the rule a sale is not attention — it is a setting. But the amber wash this screen uses for attention (#fdf6ec) reads as no fill at all at a glance, which is the one thing this badge must not do: a discount silently applied to twenty-six prices is exactly what someone must not miss while reading a margin. Paul's call, logged here rather than silently diverged, and the third collision of its kind on this screen (see MR‑075's red margin cell). Ink on #ffdd57 measures 12.66:1.
MR‑101 — PROMOTED joins SALE in the header
Promoted was the other setting that quietly moved every number in the table. It adds the account's 2% ad rate to the 13% fee on every row at once, and it was announced in the same three places the sale was — a checkbox in rung 6, a counter beside its heading, and an amber tag in the footer strip. None of them is the table. It now takes a PROMOTED badge beside SALE, and the footer's amber tag comes out with it. Nothing is lost: the 13% and the 2% are already named in the facts strip at the top of the stage and in rung 6's own counter.
They are a pair, drawn as a pair. Both are job-level settings that change every row without touching a single control on the table, so they share a shape, a weight and a position, and differ only in hue — light green #a8e6b8 against the sale's #ffdd57, at 0.93 of its luminance so neither shouts over the other. Ink on the green measures 11.81:1. Read together, SALE 15% · PROMOTED is the whole job-level pricing state in two words.
Green now carries a second meaning on this screen, and that is the third §2 collision. Green has meant ok — a cleared gate, a done step (.tag.ok, --st-ok-txt). Promoted is not a success; it is a setting that is on, and it costs money. The badge is deliberately not the pale #e7f3ea the ok tag uses, so the two are not mistaken for each other — but hue is doing double duty and the fix is not a local one. Q15 now names three exceptions rather than two.
MR‑167 … MR‑173 — #stage=drafts: the review loop leaves the app, the agent hand‑off arrives (2026‑09‑05)
What was here was too much to use. The drafts round built an in‑app correction loop — a Prompt inspector over three documents, guidance that refactored the prompt into a diff, an invoked Review on a stronger model, notes per flag, prompt versions, behind template and a blast‑radius re‑run. It worked, and Paul's call is that an operator will not use it. Every one of those modules is retired: archived byte‑for‑byte at mockups/_archive/listing-builder-2026-09-05-pre-agent-handoff.html and described in references/ebay-planning/DRAFTS-REVIEW-LOOP-RETIRED.md. The sections below this one that describe them are kept as history, not as the screen.
Generate once. A job with no drafts shows an empty panel and one control, Generate drafts — 4 listings on Template 1. The brief's Open workspace — Drafts starts the same run when it opens a job that has none, so the operator arrives to the band already running. Once the run starts the button is gone for good: there is no re‑run from this screen. Template 1 is the three‑document prompt in PROMPT-ARCHITECTURE.md; it stays the system's template and stays versioned, and is no longer shown or edited here. #stage=drafts&drafts=none is the fixture before its first generation.
Approve, or Review. No note.Flag for edit is now Review, a plain toggle. The disposition set — approved · review · untriaged — is the whole per‑listing signal the outside agent receives, and the harness asks the operator in conversation what is wrong with a reviewed one. A listing left untriaged is still rewritten; an approved one is left alone unless the operator says otherwise there.
Agent Export. Right‑adjusted on the strip: a copy icon, disabled until armed; the Agent Export checkbox, which arms the icon and sets the job to Waiting on agent; and a status pill — a status word with a pulsing dot in the state's colour, every stamp and count in its tooltip, never a help cursor (MR‑174). The export has no state of its own (MR‑175): the copy icon puts the job id and the workflow file's name on the clipboard, and nothing else — the harness pulls listings, dispositions, facts and Template 1 through the API, because anything the interface can do the API can do. The slide‑over shows exactly what was copied, which is the show the act rule. Canonical text: SAMPLE-DATA.md §Agent export fixture.
The return. The harness pushes content and a status per listing: agent refreshed (the operator reads it here, untriaged again, its draft counter up one and a refreshed by agent tag on the header) or approved (approved in the harness; it proceeds). The pill reads Agent refreshed, or Agent push incomplete when some listings landed and some did not — the tooltip names which, and the harness reports the gap and retries (MR‑175). A first generation that fails is retried once by the system; a second failure is the pill's Generation failed, the gate reads generation failed · 2 attempts, Generate offers try again, and the job can still go to the agent, whose workflow writes the drafts itself. The mockup cannot receive a push, so the pill is clickable and advances waiting → incomplete → refreshed, and the harness bar's Agent segment sets any state directly (#stage=drafts&agent=partial).
The workflow the export calls is a brief, not code: docs/agent-workflows/drafts-refresh.md, in this repo while we test and moving to a merch-riot-workflow project after. It names the four API calls the workflow uses — read job, read listings, write listing content, set listing status — against an API that exposes everything the interface does. The credential the harness project holds is minted by an admin (RI‑216); it is never in the export and never on this screen. Prompt changes and lessons accumulate in the harness's own history and are folded into Template 1 by hand, from time to time; there is no control for that here.
Requirements for this round:RI-content.md RI‑198 … RI‑221 — including the two options rejected with their reasons (an in‑app review pass, RI‑219; prompt transparency and editing in the app, RI‑220), the one‑description‑per‑base‑product rule the workflow frames by (RI‑217), and the workflow's shape (RI‑218). The records that described the retired controls are deleted per the RI README rule; MR‑145 … MR‑157 remain the decision trail.
MR‑105 … MR‑110 — #stage=drafts redrawn: the rail is narrow, the draft is the work, and the prompt has a home
The columns are inverted. A wide table of seven columns sat beside a narrow panel, so the screen spent 60% of its width on a list that is picked from once and 40% on the thing being read and edited. The rail is now 20% and carries SKU and title only; the Listing draft is 80%. Price, Qty and Type left the rail because they are on the draft panel, and a number shown twice is a number that can disagree with itself. Selection is unchanged — same rows, same aria‑selected, same roving tabindex, same arrow keys.
State left the rail as a column and came back as marks. The strip speaks for the job and the panel header for the selected listing, but a blocked listing still has to be findable from a 200px rail. Each row carries a status dot (ready · running · needs attention, the readiness the final‑review fixture already computes), its disposition tag (approved / flagged for edit), and behind template while its draft is pending. Marks rather than columns, because they are independent and one dot cannot say them all. A legend under the rail names the colours, because a dot with no text is only honest if something says what it means.
Every module is named.Status strip · Listing rail · Listing draft (holding Channel defaults — locked, Draft fields and Claim gate) · Agent export. Six regions that a reviewer previously had to name for themselves, which is how two people end up describing the same screen differently.
MR‑106 — the item specifics are per listing, and the counter tells the truth
The defect Phase B found:specRows() returned one hard‑coded set for all four listings, so L4 — a capsule plus a food — rendered Formulation · Capsule, which the combo rule on this same screen forbids. Specifics are now per listing. L4 fills 2 of 9: Brand and Count, everything else blank, and each blank says why it is blank (a capsule and a food share none, no tag every member carries, CogniHoney has no Supplement Facts panel). A blank with a reason is the readable form of never guess.
Nine aspects, not eight — the panel's eight plus Active ingredients, whose source exists and is parsed (source-formula.html Supplement Facts). The list itself is adapter‑supplied and unregistered, so the label carries a tag stub: until the channel supplies the denominator, N of 9 is our number, not eBay's.
The counter names its provisionals separately from its guesses.9 of 9 filled · none guessed · 3 provisional. Provisional and guessed are different failures: Formulation, Department and Country hold real values that no field on any screen yet holds, so the value is honest and its source is missing. Collapsing them would have meant either lying (none guessed with three sourceless cells) or blanking values the register dispositioned assume.
MR‑107 — the title fixture contradicted the screen's own claim gate
Four titles said Brain Focus & Recall while the Compliance row on the same panel reported a recall claim caught, reworded and approved in r2.Recall is in no benefit tag — Memopryl carries Memory & Focus and Energy. Corrected everywhere the string appeared (rail, panel, compare, final review, push manifest, eBay preview, ShipStation mirror) and every hard‑coded count recomputed: L1 71, L2 79, L3 79, L4 51. L2/L3 regain the full Supplement Type — the shorter key feature means truncation step 1 is no longer needed.
L4's title and description were regenerated from the rules, not rewritten by taste. The combo key feature is dropped (it is present only when every member's tags carry the angle, and CogniHoney has none), the Count word falls to Item (no fixture says what CogniHoney is packed in), and WHAT IT IS falls to the contents form with a blocking combo.member_without_tags flag. Complementary… and paired for adults building a daily routine were a synergy sentence written without synergy inputs.
Correct a contradiction, mark an absence.Mental clarity, with food and cGMP facilities are unsourced but contradict nothing, so they stand, marked provisional. Replacing an unsourced phrase with an invented one buys a tidier screen and hides the gap.
MR‑108 — the description is Markdown, and the toolbar cannot express what eBay strips
The toolbar comes from the pinned subset and nothing else (docs/prompt-inputs/INPUT-REGISTER.md §4.4): bold · italic · H2 · H3 · Body · bulleted list · numbered list · table · rule. H1 is out (the listing page owns the h1); table is in because eBay names table markup explicitly. Links and images are out — eBay permits links for four narrow purposes only, and a general link button is a policy trap the operator discovers after going live.
Nothing gets in through the side door. Paste is intercepted and sanitised, and anything the subset cannot express — a link, an image, a blockquote, code, an h1, any style/class/id attribute — is removed and named on screen, the way the title gate names an overrun. Silently stripping it would teach the operator that it worked.
The counter counts rendered HTML, and says so. Markup counts against eBay's limit, so a Markdown‑source counter under‑reports and passes listings eBay rejects. L1 renders to 473 characters from 385 of source — the ~90‑character skeleton overhead. The limit itself is unknown: three developer.ebay.com pages returned HTTP 403 across two phases, so the denominator is a tag stub and no ceiling is invented. The 900 beside it is a house target, ours, shown as a separate figure so the two are never read as one number.
Written here rather than shared withcatalog-product-setup.html, whose rich‑text‑over‑Markdown shape this borrows. The two editors' allowed sets differ by design — that one has H1 and no table, this one is bounded by a channel — so a shared module would have to be parameterised by subset, which is a change to a screen this round is not touching. If a third consumer appears, extract it then.
Retired 2026‑09‑05 — MR‑145 … MR‑157, the prompt, guidance, review and re‑run (history; see MR‑167 above)
The prompt is three documents, compiled per listing. A system prompt (~335 tokens) identical for every listing in the job carries the channel constants: title format and ceiling, the claims rule, the aspect list and copy rule, the Markdown subset and length target, the description skeleton, the never‑mention list. A message (~140 tokens) is the job’s template filled per listing: the job line, the category, this listing’s facts and member lines. A package prompt holds one modifier per type — Single · Bundle · Combo · Free gift — and a listing is sent its shape’s modifier appended to the message, with the gift modifier after it when it has one. Nothing in any document is conditional on type; the compile step chooses, so the model never reads a rule that does not apply to it. Gift is a modifier, not a shape: a single with a gift is a single plus a gift, and a combo is two products the buyer pays for. The output shape is not prompt text — the harness enforces a structured‑output schema, shown as a one‑line footer. About 4× the output per call. The texts are in docs/prompt-inputs/PROMPT-ARCHITECTURE.md.
How the drawer shows it. The documents stack in compile order under a one‑line note of how they combine and what the job holds (1 single · 2 bundles · 1 combo · none with a gift). The package prompt is one document with four tabs, each carrying its count and naming its reach (appended for L2 · L3); the tab follows the listing the drawer was opened from. Template view: a job token renders {campaign:End of Summer Clearance}, a listing token the bare {brand}; { opens the token list in any editor and in the guidance and note boxes; members are positional, {product-1}, {product-2.count}. This listing view: the compiled prompt, every token filled, the appended block marked ↳ Package prompt · Bundle, read‑only. Click into a document in Template view and the same text is an editor sized to all of it, in the same box — nothing moves. A changed document reads edited · not saved in its header.
Guidance is the primary edit path. Each listing carries a disposition — Approve, Flag for edit, or nothing; the rail filters by it and the strip counts it. Flag opens a small slide‑over with an optional note; Flag without a note is the group edit. While anything is flagged a Guidance module sits above the columns as one amber row, and opens at the moments guidance is the next step: flagging a listing, Write guidance in the inspector, folding findings out of the review. Refactor prompt and re‑run closes it and the inspector shows a Proposed revision: each changed document as a line diff, the guidance and notes quoted, its reach stated. Accept saves the revision as the next prompt version and re‑runs the flagged listings at once; guidance and notes are kept as the next draft’s note. The refactor is canned on the mockup (Q21).
Save saves, Re‑run runs. The drawer footer holds one Save, disabled until something changed and labelled by what (Save · Package prompt · Combo), and a Re‑run (Save and re‑run L4 while dirty). Save is a new prompt version and runs nothing; closing the drawer or switching view saves too, so there is no unsaved‑changes dialog. The footer names the reach: Reaches L2 · L3 — 2 bundles or all 4 listings. Re‑run’s scope is derived, never chosen: the flagged listings within reach, else every listing within reach. A confirm opens only when a listing in scope has work approved downstream (on this fixture, L1’s approved hero); it names what each listing would lose and lets you untick it. While it runs a Re‑run band above the columns says 1 of 2 done · redrafting L3 and the rail dots go to running; when it finishes each listing is redrafted · not yet read and untriaged again.
Pending is visible; the prompt has a version. A saved prompt is v3 · saved Sep 4, 2026 · 10:18 PM · Paul with a note; a collapsed Versions list says which listings’ current drafts ran on each. No diff viewer, no restore. A listing whose draft ran on an older version than its documents now need is behind template: a chip on its rail row and in the panel header, and on the strip the prompt chip carries the warning icon and reads Prompt v3 · not run on L1 · L4. The footer says Not run yet and offers the two exits: Re‑run, or Discard v3 back to v2 — discard only while nothing has run on the version. Per listing, the panel header reads draft 2 · prompt v2 · drafted Sep 4, 2026 · 10:14 AM: draft counts writings of this listing, prompt v counts changes to the instruction. Every stamp is a date and a 12‑hour time.
The status strip. One line of chips under the stepper: Drafts · 1 approved · 2 flagged · 1 untriaged (status; its gate rule is the tooltip), Review · 7 findings · N undecided (once a review has run; reopens the drawer with every agree and disagree kept), Guidance · 2 flagged · 1 note (toggles the row), Prompt v2 · saved … · all 4 on it (opens the inspector), and Return for correction. A chip that opens something has a border, a shadow, a trailing ›, a hover lift, and inverts while open; a status chip has none of those. Amber while something is pending, green when nothing is.
The review is invoked, never automatic.Review all 4 sits in the rail header. While it runs a Review band above the columns says 2 of 4 read · reading L3 · SKU with a bar. It reads the whole job in one pass on a stronger model than the drafter (the fixture names Fable 5.1), which is what lets it see across listings. Every finding is Global (names every listing) or Specific (names some, and says which), and takes Agree or Disagree with a reason. Fold N agreed findings into guidance writes one line per finding prefixed All listings · or L2, L3 ·, flags every referenced listing, and opens the guidance row pre‑filled. The seven findings are real defects in the drafts; the seventh is global and is a product question (Q22). One is pre‑disagreed on purpose, to show a finding can be wrong against the rules.
Caching, measured rather than assumed. A cached prefix must be byte‑identical and at least 512 tokens on Opus 5 / Fable (1,024 on Sonnet 5). The system prompt is ~335, so it does not cache — and at ~$0.50 per 50‑listing job, perfect caching would save ~$0.07. So: design for readability, keep stable content first, and let a hit arrive for free if the prefix ever grows past the minimum.
MR‑147 — the panel: title in the header, defaults at the bottom, specifics in two columns
The header now reads the listing title with Ln · SKU beside it — the thing being reviewed was nowhere in the header. Channel defaults — locked is the least‑read block on the panel and was first; it moves below Images as three columns. Nine item specifics read down two columns (CSS columns, no markup change), halving the scroll before the description.
Provisional inputs, and where their markers are.tag stub on: the item‑specifics aspect list, Formulation, Department and Country of manufacture rows, the eBay description limit, and the description's value‑adds bullet. Their follow‑ups: channel_adapter.item_specifics.aspects[] and channel_adapter.description_max_chars on sales-channel-account.html; Formulation and country_of_manufacture and value_adds[] on source-formula.htmlProduct Attributes; department on catalog-product-setup.html Identity. The Review flags row is marked too: listing.review_flags[] is the model's own output and has no record yet (RI‑107). The revision's prompt_snapshot is provisional too and has no marker of its own, because it has no control — it is named in the confirm's closing line instead.
Requirements for these rounds are indocs/requirement-inputs/RI-content.md (RI‑119 … RI‑138 and RI‑158 … RI‑197).
MR‑185 — the ShipStation stage mints one thing, and says which
The stage was named for a sync and drawn as two kinds of work. It is one: one ShipStation bundle product per listing. Every physical SKU it is built from — and its QuickBooks item, and its ShipStation component product — was minted when the product was added to the catalog (catalog-product-setup.html). Nothing here creates one. The vocabulary now splits on that line: the catalog mints components, this stage mints bundles, and the listings column says Bundle product rather than API build, because the column should name what is made and not how.
Physical products became Components, and collapsed. A check is not a section of work, so the clean state is one status line — 2 of 2 have a ShipStation product and a weight — with the records behind a disclosure. Two things came out of it: the QuickBooks column, because QuickBooks has no part in anything this step writes and the ShipStation id already proves the component exists; and the four‑line prose block, whose operative half is now the i beside the heading.
Weight is the one thing that stops the step, so it is the one thing still shouted. On a bundle ShipStation ignores the weight we send and adds the component weights instead — a missing one rates the box as empty and we pay the difference. The warning names a row, so tripping it forces the disclosure open rather than pointing at a table nobody can see.
Why the records are built before anything is ready to push. A ShipStation product is internal — no buyer ever sees one — so all four listings are built here whether or not they are pushable, and pushing never waits on this step. That was a note on the page; it is rationale, so it is here (§0).
MR‑233 — final review is two views, and it shows the content instead of counting it
The stage answered one question and the reviewer was asking two. The old screen was a per‑listing walk and nothing else: pick a listing, read three label/value tables, pick the next. That is the right shape for is this listing right and it is useless for what is about to go out — the second question is about the batch, and the batch was only ever visible as a count on a button. So the stage now carries both, on one segmented control: Listing keeps the walk, Grid puts every listing in the job on one page. #stage=review&rv=grid addresses the second, the same way every other harness state is addressable.
The two biggest pieces of listing content were not on the final review screen. The description read “What It Is / Key Features / Details · 3 claims, all library‑matched” and the nine item specifics read “9 of 9 filled” — summaries of themselves, on the last screen before publication. To actually read what eBay was about to receive you had to open the preview modal. Both are now rendered in place: the description through the same Markdown renderer the drafts editor uses, the specifics as the real nine rows with their provisional tags and their blank‑with‑a‑reason cells. The claim gate, its resolved history and the model flags came with them — all three existed on the drafts panel and none reached review.
A masthead, because a table row is the wrong weight for a title. Hero image, the title against its 80‑character ceiling, SKU · type · state, then six figures a reviewer checks in one glance — title length, price, quantity, images, specifics, claim gate. Under it, when the listing is blocked, what it is blocked on. That reason used to live only in the push card at the bottom of the page, so the listing you were reading never told you why it was not going.
Images got bigger and got captions. 52 px thumbnails in a table row cannot be judged; 94 px with the slot name under each can. The shared / custom underline mark and the order are unchanged — the section simply became its own, with the readiness the Images stage already computes as its status.
The push card moved out from under the detail column. — superseded the same day by MR‑236 below, which took it off the stage entirely. The half this got right is the half that survives: the push is the batch’s action and not the selected listing’s, which is why it turned out to belong nowhere inside the stage at all.
Nothing on the stage holds its own copy of a status any more. The left list, the grid, the readiness line, the blocked list and the push button’s count are all derived from the same L[n].fr records. That was a live defect, not tidiness: the four rail statuses and the blocked list were hard‑coded HTML and the button said 1 of 4 as a literal, so after the agent pushed refreshed drafts (#stage=drafts&agent=refreshed) the review stage still reported the state the job was in before. It now says 2 of 4 ready, and a blocker whose draft has been rewritten is cleared rather than left standing.
Before the first generation the stage says so. On drafts=none the masthead reads Not drafted, the grid’s title cell reads not drafted, and Content refuses the word approved — it previously rendered a full title, a description and a green approved · draft 2 for a job that had generated nothing. Product and Pricing keep their status, because a package and a price come from stage 3 and are true before any draft exists.
What the grid deliberately does not have. — overruled the same day; see MR‑234 below. This round argued that §6’s grid furniture was for the catalog and the SKU map, that four rows do not earn sorting, and that the header lock could wait for the fixture to grow. Paul asked for sort, resize and the lock directly. The reasoning was measuring the fixture rather than the job, which is the mistake §11 warns about from the other direction — a drawing convenience laundered into a rule.
Requirements for this round are indocs/requirement-inputs/RI-listings.md (RI‑379 … RI‑389).
MR‑234 — the grid gets its furniture, and every image opens
MR‑233 argued the grid did not need sorting and was overruled the same day. Paul, 2026‑09‑07: “we should be able to adjust column width, sort, and so on. And lock the columns in case there are a lot of listings and we need to scroll.” The rejection reasoned from the four‑row fixture rather than from the job, and Q0 already has stage 3 describing twenty‑six listings. RI‑387 is corrected in place and RI‑388 is closed, rather than left standing beside a screen that now disagrees with them (§11).
Ported, not rewritten. The sort, the drag‑to‑resize and the locked header come from sales-channel-listings.html (MR‑208), so its two load‑bearing details come with them. table-layout:fixed is the first — under auto layout the browser re‑solves every column the moment content changes and a dragged width does not stick. The measured height is the second, and this screen needs it for the opposite reason RI‑289 did: there the conditional element was a banner above the grid; here it is the push card below it, which grows when a live push opens its watch panel. A calc() constant would be wrong the first time anyone pushes.
The Listing column pins too. RI‑289 named this as the header lock’s missing pair and did not draw it — scrolled right, a row loses the one cell that says which listing it is. It is cheap once the layout is fixed, and this grid needs it more than that one did, because its first column is the only place the SKU appears.
The sort is in the URL and the widths are not (§6). #stage=review&rv=grid&gsort=-state is everything in this job, sorted by what is blocking it — the view most worth sending someone. A dragged column is a fact about one screen and stays there.
Widths reset on reload here, and that is the drawn behaviour, not a gap (MR‑235, RI‑395). In the build they persist per operator in localStorage, per grid — which also settles the same absence on sales-channel-listings.html, where §6 has asked for it since before either grid existed and nothing recorded that it was missing. The mockups should not grow it: a persisted width is invisible on first load and indistinguishable from a default, so it demonstrates nothing a reviewer can see, and it makes every screen after the first inherit a layout somebody else dragged. These files are read cold on purpose — every addressable state depends on it — so per‑viewer stored state is the one kind these mockups should not carry.
Every assembled image is a button. In the grid’s Images cell and in the Listing view’s strip alike, a thumbnail opens a viewer: the image large, its slot name, whether it is from the product’s shared set or is this listing only, and a filmstrip of the listing’s other images with arrow‑key navigation. It carries no controls, deliberately. The generation popup is where an image is regenerated or approved and it spends money; on the last screen before publication an enlarged image should not be one keystroke from a paid API call. A slot that has not been built — L4’s hero — is drawn but is not a button, because a control whose only outcome is nothing is noise.
A preview icon on every grid row, same glyph and same modal as the Listing view’s Preview listing. The preview was reachable only from the detail view, so what will this one look like meant leaving the grid — and the grid is where listings are being compared against each other, which is when that question comes up most. It is the one column with no sort button rather than one that sorts on nothing.
Still not built, and recorded as such: a column picker, filters, saved views and pagination (RI‑394). Each needs a reason this screen does not have yet — a filter on a job you are about to publish in full hides part of the thing the view exists to show, and a saved view of a job pushed once and closed has nothing to be saved for.
Requirements for this round:RI‑390 … RI‑395, correcting RI‑387 and closing RI‑388.
MR‑236 — the push is a drawer off the header, and the icon carries the count
Paul, 2026‑09‑07:“For the module ‘Push to eBay · CA‑01’ Let’s remove it or move it into a drawer. And add a small icon somewhere in the header. Where we can trigger the drawer and run a dry run.” The card cost the review stage roughly 200 px of permanent height on the one screen whose whole purpose is showing every listing at once — and it grew, because the watch panel opens inside it after a live push. It was also the only thing on the stage that was not about a listing.
The header rather than the stage head is the substantive half of the choice. It makes the push reachable from every stage, so a dry run no longer means walking to stage 8 — and a dry run writes nothing, so it is the cheapest way to ask would this work. Rejected: a floating action button (a second header idiom for one control) and a footer bar pinned over the grid (which gives back the height the move was for). The drawer itself is the existing .pidrawer, not a third one.
The badge is the ready count, not a decoration. An icon in a header has to say what it is about before it is clicked, or it becomes a glyph people learn to ignore — and this one now hides the primary action of the last stage, so it carries more weight than any other icon here. Green with a number, blue reading live while a run is in progress, grey when nothing is ready; the aria-label and tooltip say it in words. It is shaped after the bell beside it so the two read as one family. The stepper’s Final review count is derived now too — it was a hard‑coded 1 of 4 ready that went stale the moment the agent refreshed a draft (the RI‑383 defect, in the one place visible from every stage), and the move made that worse by leaving the stepper and the badge as the only always‑visible statements of readiness.
The watch panel went with it, which is the part worth watching. It is this screen’s show the act surface and a drawer can be closed, so a running live push could have become invisible — the one thing that must not happen on a live write. Three things keep it visible and each is doing work: the drawer is already open because that is where you pushed from, the page flash persists behind it, and the badge states the run from any stage. The flash’s wording changed with it: it said watching below, which after the move points at empty space.
Open, and named rather than quietly fixed (RI‑399): nothing on the review stage is a push control any more. The stage head says ready to push in words inches away and is not clickable, which is where the friction will show up first. The cheap fix if it does is to make that line the trigger and leave the header icon as the from‑anywhere path — not drawn, because one affordance was the instruction.
Requirements for this round:RI‑396 … RI‑399, superseding RI‑384.
Open questions
Q20 · Is the gift item {product-n} or {gift}? Members are positional (RI‑162); a gift variant adds one free item. Drawn as the separate {gift} / {gift_unit} pair.
Q21 · The refactor is canned. — moot since MR‑167; the control is retired. Refactor prompt and re‑run returns fixed section changes keyed by the flagged listings’ shapes, so the diff always has something real to show. The band is indifferent to what a real refactor returns; nothing on screen yet says this one is a fixture.
Q23 · The Images fixture disagrees with the drafts gate. The stepper says Images is 6 of 12 done and L1’s hero is approved, while the drafts gate says Images unlocks only when all four are approved. Left as is this round: the approved hero is what makes the MR‑153 confirm reachable on the mockup at all. Which fixture is right is a separate fix.
Q25 · No listing in this job has a gift. — moot since MR‑167; the control is retired. The Free gift tab reads no listing in this job has a gift, so the stacked block in the compiled view is never shown on this fixture. Job sizes (MR‑186) add a gift variant of every row; wiring it into the drafts fixture is a separate round.
Q26 · A rule shared by two shapes is written twice. — moot since MR‑167; the control is retired. With no if‑else, a rule that applies to Bundle and Combo but not Single lives in both tabs. That is the price of a prompt with nothing in it that does not apply; whether a shared‑rules tab is worth having is undrawn.
Q22 · Does the campaign name belong in public listing copy? Every description’s PRODUCT DETAILS names the campaign (End of Summer Clearance … pricing). The prompt allows it, but to a buyer it reads as marketing copy inside a product description. If not, the campaign line goes and {campaign} has no place in the message. Needs Paul.
Q18 · Which benefit tag is primary? Memopryl carries Memory, Focus and Energy (the old Memory & Focus was split by MR‑345), all pressed, and nothing says which sources Main purpose. The resolver takes the first in list order and the screen shows that. One cell wide, so it was not a blocker — but it is a guess with a stable answer nobody has given. Research no longer needs a primary (RI‑649 reads every benefit), so what is left of this question is Main purpose alone.
Q19 · Nothing reads a revision back. — moot since MR‑167; the control is retired. The prompt is versioned as part of the draft revision, per the round's decision 2 — but no surface anywhere reads a past revision, so r2's prompt is recoverable is currently a claim about a record that does not exist. Drawing a revision browser now would be the prompt version stack that decision forbids, under another name. The gap is real and is logged rather than drawn.
Q0 · Which pack-size branch? — answered in MR‑186: + job sizes. A job may add sizes up to a dozen, the ladder tier required alongside; Account set only is deleted rather than kept behind a flag. Still open, and now on its own: drafts / images / ShipStation / review are fixtured on four listings while stage 3 describes twenty-six.
Q1 · Combo supplemental default. When a combo product set has no imagery, should "Generate set" or "Pick from library" be the primary action? The fixture shows both, unranked.
Q2 · Campaign image set precedence. An attached campaign carries its own image set. Does it pre-fill shared slots ahead of the product-set library, behind it, or as a third labeled source the operator picks from?
Q3 · -D<n> numbering inside one job. If a hero regenerates before push (v1 → v2), is that still -D1 until pushed, with -D2 reserved for post-push design revisions? Fixture assumes yes (L1 offers "Regenerate as -D2" only because v1 is approved).
Q4 · Does one job-level product list feed both stages? Stage 2's picks and stage 3's Product Pool are still separate fixtures, so a product added here does not appear there and the gift tick does not reach the Gift Pool. Settling it is a fixture rebuild, not a screen change.
Q6 · Does per unit mean per bottle in the box, or per bottle sold? A gift row’s Per unit divides by a bottle nobody paid for, because the free bottle counts in Units. Dividing by the paid bottles instead would make a gift variant read identically to the bundle it came from — which is arguably the truer number, and arguably hides the giveaway. It is a pricing decision, not a layout one.
Q7 · Which Images stage survives? Stage 6 and 6.1 are both live, on purpose, so they can be flipped between. They are not both shippable: when the pick is made the loser is deleted and the survivor takes the images id back.
Q8 · Where does the image claim gate live in the lane model? Stage 6 ran a claim check on image approval and caught L2's “boosts memory in weeks” overlay — the defect that stage existed to catch. The lane grid has no surface for it. Candidates: a per-cell state, a per-lane readiness column, or a gate that moves to final review. RI‑021.
Q9 · What does an image template belong to? The job, the channel account, or the product category. Drawn job-level because that is the only scope the screen can show. eBay's picture conventions differ from Amazon's, which argues account; a supplement's set differs from a food's, which argues category. RI‑016.
Q12 · Whose image-type vocabulary wins? This screen offers ten types in the practiced order (MR‑030). _image-module.js’s sales-channel library declares six — main · gallery · infographic · lifestyle · comparison · additional — and warns in its own source that a role a screen has never heard of renders a select with nothing selected. Nothing fails here because the mockup writes to no library, but a real tag of benefit or how-to has nowhere to land. Either the library adopts the practiced vocabulary, or this screen maps down onto the six. Not fixed here — that module has four other consumers. RI‑028.
Q11 · What fills a column whose images are supplied rather than generated? The gallery is scoped to sales-channel/, which is right while every column wants marketplace art. The practiced eBay order (MR‑030) also has front, back and Supplement Facts — supplied roles living in catalog-product/. If a template gains one, either the column declares its own library, or supplied columns auto-fill and are never hand-picked (what REQ §9.1 implies), or MR‑071's one-screen-one-library rule gets re-read for a screen that legitimately spans two. RI‑026.
Q10 · Where does job image spend surface? 6.1 drops stage 6's generation rail, which carried the running per-job total. The per-call estimate survives in the popup; the total currently has nowhere to go. RI‑020.
Q15 · Does the colour vocabulary need two more terms? GUIDANCE §2 gives this screen three: accent for active, amber for attention, danger red for refusal. Stage 3 has now broken it three times, each for a good local reason — a red fill on a margin cell that is a warning and still lists (MR‑075), a yellow badge for a setting that is neither attention nor refusal (MR‑100), and a green one for a second such setting, on a hue that already means ok (MR‑101). Three exceptions is not a set of exceptions, it is a missing vocabulary. The candidate fix is a named warned tint and a named setting-is-on family, decided once for every screen rather than argued per card — and if a setting-is-on family is right, the question after it is whether its members are told apart by hue at all, or by their words alone.
Q13 · Does a blow-out belong to the job or to the product? It is drawn as a tick on this job's rung 4, because that is the only scope this screen owns. But a product you are writing off is written off everywhere — the next job that lists it should presumably know, and so should any margin report. If it is really an inventory state on the catalog product, this control is a per-job override of it and needs to say which one it is. Nobody has asked the question yet.
Q14 · Is Sale a what-if, or a markdown that ships? Drawn as a job-level rule that moves every derived price, which is what makes the ladder readable under a discount. Two readings follow and the screen does not choose: either it is a preview like the ladder's example product (MR‑082, stored nowhere), or it is a real markdown that pushes discounted prices to eBay — in which case it belongs in the manifest, the drafts and the ShipStation mirror, and eBay's own promotions API is probably the mechanism rather than the listing price. The percentages offered (15/20/25) are Paul's and are not from any eBay constraint.
Q5 · Is the catalog search account-scoped, and does it page? Drawn against nine products with neither. A real catalog is thousands and a channel account cannot list all of them — but what narrows the list (account eligibility, brand, both) is a domain question this screen cannot answer alone.
Refinement round (MR-046)
Live push gate — Live is styled destructive, the button label is mode-aware, and a confirm dialog restates account + manifest before any live write. Dry run stays one-click.
Watch panel — after a live push the push card opens a read-only window into run B-0114 (APPLYING → RECONCILING → live + spot-checks); the cockpit still owns the run.
View toggles — Drafts adds Compare (fields × listings); Images adds By listing (readiness-first). Same segmented-control idiom.
eBay preview — per-listing buyer's-eye preview on Final review; deliberately approximate, marketplace chrome omitted.
Deliberately out of scope this round
Per-listing overrides of the brief’s benefits, angles or guidance (job-level only).
Recipe picker UI — exactly one recipe exists ("standard supplement listing"); the chooser lands when a second recipe does.
Post-push reconciliation surfaces — the watch panel is a read-only window; run cockpit / setup screens own the machinery.
List live on eBay · CA-02?
This writes to eBay — main — a real seller account. Dry run costs nothing; live listings are public the moment they apply.
Going now — 1 listing
#
SKU
Title
Price
Qty
L1
OB-MEMO-1-SUM-EBY
VitaLifes Memopryl Memory Support Supplement Memory & Focus 60 Capsules
$31.99
10
Not going — blocked
L2
OB-MEMO-2P-SUM-EBY
hero overlay claim REVIEW
L3
OB-MEMO-3P-SUM-EBY
hero still generating
L4
OB-MEMO-COGH-BNDL-1-SUM-EBY
combo supplemental set not built
Creates run B-0114 · followed here and in the run cockpit
Image
1 of 7
eBay preview — L1
1 of 4
Lightweight preview of the listing content we control. Marketplace chrome — similar items, sponsored carousels, payment methods, seller widgets — is deliberately omitted; eBay renders those.
VitaLifes Memopryl Memory Support Supplement Memory & Focus 60 Capsules
$31.99
ConditionNew
Quantity10 available
ShippingFree shipping · from Miami, FL · next-business-day handling