Bugbot caught that the exact-match allowlists 403 every request on --port 80
because browsers drop the default-port suffix; other ports stay strict.
Written with AI assistance under maintainer direction.
Co-authored-by: Cursor <cursoragent@cursor.com>
An unauthenticated POST /build-path wrote the flip event that makes --wait
instruct the agent to generate comps: same class as the /answer hole in #555.
Written with AI assistance under maintainer direction.
Co-authored-by: Cursor <cursoragent@cursor.com>
Unauthenticated POST /answer copied steer into the agent ANSWER line. The handler now requires the detached session key and rejects foreign Origin and Host.
Written with AI assistance under maintainer direction.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix: stop the direction page hanging forever after a re-roll (#469)
The re-roll leg of the decision-page protocol was documented only in
serve-question.mjs's own header, so agents never ran --update and the
open tab polled a round that could never arrive. Compounding failure
modes: the page poll swallowed every error, the daemon's --timeout was
an absolute guillotine that killed the server under a still-open tab,
a choice posted to a dead server confirmed nothing, and refresh or
Reload on an unresolved round resurrected heartbeats that held the
daemon alive indefinitely.
- new-work.md documents the re-roll leg: rerun concept-seed with
--from/--reroll, deliver with --update on the same key, never --start
a second server.
- The page poll terminates and says why: eight consecutive fetch
failures means the server is gone; the delivery deadline (the
server's own --idle-grace, inlined into the page) passing means the
hand never arrived. Both stop heartbeating.
- The daemon's --timeout bounds only the wait for a page to open; once
the page heartbeats, the server lives while the page does and exits
after --idle-grace (default 600s) without a beat, including under
--timeout 0.
- Build this and Re-roll against a dead server fail loudly instead of
silently swallowing the click.
- The server tracks the window between a collected re-roll answer and
the --update that replaces the round, and serves the page in waiting
mode there, so a native refresh re-enters the same bounded wait
instead of resurrecting dead cards; the in-page Reload button only
revives a delivered hand.
- --update is exempt from the headless gate and its liveness probe
trusts a fresh heartbeat over a failed kill probe (sandbox EPERM is
not death).
Squash of the six review-round commits on this branch, rebased onto
main after the decision-page revamp.
AI assistance: prepared with an AI agent operating under maintainer
instruction (abdulwahabone).
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review findings: persist the replacement deadline, refuse unloadable hands
A browser-native refresh of the waiting page re-entered the bounded wait
with a fresh delivery deadline and an immediate heartbeat, so refreshing
before each deadline expired could hold the daemon alive and keep --wait
on WAITING indefinitely. The server now records when the re-roll or
followup answer was collected, each served waiting page inherits only
what remains of that one allowance, and a page served after the deadline
renders stalled immediately and never starts its heartbeat.
And a next hand the round could not load used to reload-loop the tab:
GET /'s catch kept the file on disk, so /next-status stayed ready:true
forever. --update now refuses a payload without a non-empty options
array at the sender, and GET / discards an unloadable next file so the
bounded wait resumes.
AI-assisted (Cursor agent) under maintainer instruction.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review finding: a stalled page recovers a late hand without a click
The stall silenced heartbeats so the idle grace could reclaim the
daemon, but that silence read as a closed tab: after a late --update,
--wait saw the stale beat and reported PAGE CLOSED while the user sat
on the Reload screen, so the agent abandoned the browser path the
recovery UI exists for. The stall screen now keeps a beat-free
/next-status watch that reloads into a delivered hand on its own
(GET never beats, so an abandoned flow is still reclaimed), and --wait
no longer concludes closure from a stale beat while an undelivered
next hand sits on disk.
AI-assisted (Cursor agent) under maintainer instruction.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review finding: a delivered hand must not mask a closed page
The mid-delivery suppression keyed on the next file existing, but a
closed tab never claims that file, so an unconsumed delivery held
--wait on WAITING indefinitely instead of reporting the closed flow.
The suppression is now age-bound: a stalled page's watch reclaims a
delivered hand within seconds, so a file still unclaimed after a 10s
grace means no page is coming back and the stale beat reads as the
closed page it is.
AI-assisted (Cursor agent) under maintainer instruction.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review finding: stamp the delivery clock at --update, not the copy
--wait's mid-delivery grace reads the next file's mtime, but
copyFileSync's timestamp behavior is the platform's business: a copy
that preserves the source payload's older mtime would start the grace
already spent and report PAGE CLOSED under a live stalled tab. --update
now touches the delivered file itself, so delivery time is delivery
time everywhere.
AI-assisted (Cursor agent) under maintainer instruction.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review findings: disable canon during the wait, validate --timeout
The waiting and stall screens disabled only the re-roll buttons; the
footer canon action stayed clickable, and a canon pick posted after
--wait had consumed the re-roll could never be collected: it overwrote
the answer, marked the table closed, and exited the daemon under the
agent. Both disable sites now take the canon exit down with the re-roll
buttons; a delivered hand reloads the page and serves it live again.
And --timeout reached the lifetime timer unvalidated: NaN or a negative
value disarmed the no-page exit and the daemon leaked. It now takes the
default unless the value is a finite non-negative number, keeping 0 as
the explicit wait-forever.
AI-assisted (Cursor agent) under maintainer instruction.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review finding: a second click must not renew the delivery deadline
dealAgain left the re-roll and canon controls live through the answer
POST and the 700ms fly-out, so a second click posted another re-roll
and the server restamped awaitingNextSince, renewing the deadline this
PR made non-renewable on refresh and on the stall screen. The controls
now go quiet at the click itself, in dealAgain and in answer(), and the
server stamps the allowance only on the transition into the wait, so a
duplicate answer racing the disable keeps the first stamp.
Regression coverage on both sides: the unit deadline test posts a
duplicate re-roll mid-allowance and asserts the budget shrank instead
of resetting, and the e2e stall test asserts both controls are disabled
immediately after the click, before the fly-out.
AI-assisted (Cursor agent) under maintainer instruction.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review finding: a late delivery must survive its claim window
--update could land a replacement hand after the stalled page went
silent but moments before the daemon's idle deadline: the daemon exited
before the page's 1.5s watch could claim the hand, orphaning a delivery
--update had confirmed, and the next --wait reported a server failure.
The idle exit now defers while an unclaimed next hand is younger than
the claim grace --wait already reads (extracted as one shared
constant), so the page's watch deals it and heartbeats resume; a file
unclaimed past the grace still ends the daemon, bounded as before.
Regression test: deliver at idle-deadline-minus-a-beat, assert the
daemon survives past the deadline and serves the late hand.
AI-assisted (Cursor agent) under maintainer instruction.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review finding: the claim itself must hold the daemon
The idle-exit hold read only the next file's freshness, but GET /
deletes that file when it serves the claimed round, before the
reloading page can post its first heartbeat: a lifetime tick in that
gap saw no pending hand and a stale beat, and exited under the hand
just claimed. GET / now stamps the claim when it consumes a pending
hand, and the idle exit honors the same bounded grace from that stamp,
so the reloading page gets its seconds to beat while an abandoned claim
still ends the daemon at the grace.
The claim-window regression test now also fetches after the claim, past
another lifetime tick, and asserts the daemon survived the gap;
verified it fails on the previous commit.
AI-assisted (Cursor agent) under maintainer instruction.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix review finding: --wait must ride out the claim gap too
The claim deletes the next file --wait's mid-delivery grace watches,
and the reloading page has not beat yet, so --wait in that gap read the
stale beat as PAGE CLOSED while the daemon was alive serving the dealt
round, and the agent abandoned a browser session that had just
recovered. GET / now persists the claim stamp into the per-key state
file, and --wait's suppression honors it under the same bounded grace:
a fresh claim stays WAITING, a claim nobody followed with a beat still
reads as the closed page it is.
Regression test drives --wait through the gap (claim with a stale beat:
WAITING, not exit 4) and past it (backdated claim stamp: exit 4);
verified it fails on the previous commit.
AI-assisted (Cursor agent) under maintainer instruction.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Paul Bakaus <paul.bakaus@gmail.com>
Follow-up to the generation stamp: both probe callbacks returned on a stale
generation without clearing the elapsed-time interval. tryLoad clears it on
re-entry, but a probe that finishes stale schedules no re-entry, so flipping
back while one was in flight left the interval ticking for the rest of the
page's life. Both exits now clear it, which is what the isConnected guard above
them already did.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three review findings on #582, all real and all mine.
Delegating `.pip` and `.media` to `document` separately meant they could not
stop each other: stopPropagation ends bubbling, not siblings on the same
target. Clicking the corner inspiration opened the inspiration and then the
media handler replaced it with the comp, so the corner was unusable on exactly
the cards this PR set out to fix. All three targets now resolve in one
delegated listener in priority order, corner before chip before slot, and a
chip that is not expand keeps its own click instead of falling through.
Flip-back restored the face without clearing what the pending state had added,
so a slot that reached stand-in came back carrying "comp pending" beside a
fresh label, and one whose art had failed came back still marked unavailable.
Restore now clears both, and a slot with no art to restore returns to the
honest "artwork unavailable" treatment rather than being labeled inspiration.
Converting in place means the same node is reused across flip cycles, and the
old poll closure outlived its cycle: a probe from the first flip could settle
the second one, stripping the new shimmer and stopping the live poll while the
comp stayed hidden. Each run now carries a generation stamp that flip-back
bumps, and both probe callbacks bail when it moves.
The test covers the corner click against the landed comp, and I confirmed it
fails when the priority ordering is removed.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects in the same few lines, both from `enterComp` hand-building a media
slot after the deal instead of reaching the shape a comp-first render serves.
A code-led card carrying catalog art shows that art as its face. Flipping to
comp inserted a fresh shimmer slot above the body and left the face alone, so
the card rendered the inspiration full-bleed with the rendering comp stacked
under it: two images of equal weight, which is the one thing the corner
treatment exists to prevent. The flip now converts that slot in place, moving
the art into the `figure.pip` and dropping the face label, and flipping back
restores it, so a round-trip leaves the card as it was dealt.
The slot it built also carried no chips, and the zoom handlers were bound per
element at load, so a comp that streamed in after a flip could not be opened at
all: no expand affordance, and no click handler on the art. The three lightbox
handlers are now delegated, which is what makes any later-built slot work, and
a converted slot keeps the chips it already had. Polling learned to stop on a
slot that stays in the DOM but loses its pending state, which only happens now
that a flip back can restore rather than remove.
The existing toggle test covered a wireframe card, where the schematic is
hidden and a fresh slot inserted; that branch was fine, which is why this went
unseen. The new test drives the art-carrying card and fails on the stacking
assertion without this change.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two findings from Greptile on #579, both about the same key seen from
different roots.
`appendBuildPathDirective` searched projectRoot and cwd but never repoRoot,
while `checkBuildPathUnset` reads both. In a monorepo that committed the
preference once at the root, the two disagreed in the worst direction: the
staleness finding stayed silent because a value existed, and the directive
never named it, so nothing on screen explained why the recorded default was
not being honored. Roots are now ordered nearest first, workspace over repo
root, with regression tests for both the fallback and the override.
The ANSWER line for a flipped path said "never write it to settings". The
page indeed never writes it, but the sentence read as a rule and applied
itself to new-work's one-time offer, which exists for exactly the case a flip
creates: a project with no recorded default, asked once after the round
closes. It now states what the page does and names the exception.
The same report's first issue also named context.mjs, and that part does not
hold: its directive is emitted only when a value is already recorded, which is
precisely when session-only is the correct instruction. Left as is.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The build-path preference shipped as a question only `init` asks, written to
a file only `init` writes. Nothing routes an initialized project back through
init, so every existing project took the comp-first default without anyone
choosing it, and the only recourse was a footer toggle that binds one session.
Neither the setting nor the round that preceded it ever reached a release
(skill-v4.0.4 has no `buildPath`, no `comp-led`, no `.impeccable/settings.json`),
so the PRODUCT.md standing-commitment fallback describes an era that never
existed publicly. It is deleted rather than honored: told a field might exist,
models go hunting for it and preserve it.
- `buildPath` moves from `.impeccable/settings.json` into the unified
`.impeccable/config.json`, which already has a known-keys registry, doctor
coverage, and a gitignored `config.local.json` override. Whether a machine
has an image tool is a property of that machine, so the local file wins.
- new-work captures the answer from behavior instead of an interview: a toggle
flip on a project recording nothing asks once, after the round closes,
whether to keep it. The answer is written either way, because a declined
offer nothing writes down is an offer the next session makes again.
- Two findings: `config-invalid-build-path` (an unread value rides the default
rather than the opposite path) and `config-build-path-unset`, gated on a
product record plus evidence of direction work so polish-and-audit projects
never hear about a setting they do not use.
- init treats a recorded value as a confirmed answer, resolving its conflict
with Step 1's "do not reopen confirmed fields".
- The setting was undocumented in the README and doctor.md. Both now cover it.
Also records a measured skill-behavior baseline. Three cells fail on unmodified
main (scenarios 9 and 15, `initialized natural build`), verified against a clean
worktree; the suite README now says so, so the next person does not spend the
hour attributing them to their own branch.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same line as the round's title rather than the brand mark: the control
reads as part of the round it configures, and the brand row stays clean.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Top-left is the brand and reading-entry corner and mode controls belong top
right; on the brand row the toggle also costs no vertical space, so the
headline keeps its position.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The toggle sits top-left under the brand instead of in the footer bar, and
flipping to comp-first now opens a confirm dialog before anything renders,
since the flip starts billed, minutes-long generation; flipping back stays
free and immediate. The dialog lives at the document root so it never loses
the stacking fight with the deck. The schema blob also states harder that
toggle: true may only be offered when image generation exists.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- serve-question: payload buildPath { value, toggle } renders a footer
segmented control (comp first / code first) with the trade stated in one
line; the default comes from settings, a flip binds that session only.
Code-led rounds treat declared comp paths as flip reserves: wireframes
render, a flip to comp shimmers the slots and surfaces once through
--wait as BUILD PATH FLIPPED so the agent starts generating mid-round;
the flip back is free and a landed comp stays. The ANSWER carries
buildPath and buildPathFlipped with a session-only directive.
- init Step 5 asks the preference once (only when image generation exists)
and writes .impeccable/settings.json; context.mjs surfaces the recorded
default every session; PRODUCT.md standing commitments stay honored as
the fallback.
- new-work retires the two-card execution-contract round: no round asks a
workflow preference. followup stays as the generic same-table mechanism.
- e2e: new toggle test (14/14 with the wireframe test).
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- concept-seed --scope surface deals three grounded-list indices (dice-picked,
primary leads) instead of one: a single card is not a choice, and the
no-lineup rule stays direction-only, where it was written for worlds
- serve-question renders a new per-card wireframe field as a layout schematic
in the media slot: the code-led channel's visualization, no image
generation needed, no card back, no salience weight
- generate-image gains --ref (repeatable): routes through the edits endpoint
with input images, so an established world's comp inherits identity from a
captured screenshot of a real page instead of a prose paraphrase; tested
against impeccable.style, where the reference-anchored comp reproduced the
live site's chrome and the prose-only comp drifted
- new-work rung two rewritten around the dealt hand: lock-in is the
approval, a locked comp builds comp-led and discharges the visualize.md
three-option round, a locked wireframe builds code-led; visualize.md
records the exemption and the reference-image discipline, including the
reference-leak caveat (chrome carries, the reference page's content
does not)
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The raise block drops the side-tab left border for a quiet patina panel,
and drops the poker jargon: "Improved by Impeccable's worlds" with
per-line "From <world>" donors, on single raises too; tooltip, aria, and
screen-reader copy follow
- The pick-card kicker convention renames MY PICK to IMPECCABLE'S PICK at
every definition site, so users stop reading "my" as themselves
- The footer (steer, registers, canon exit) is a sticky full-bleed bar on
wide viewports, sharing one --page-inset with the content column; portrait
keeps it in flow where the deck scrolls internally
- Short landscape viewports compact the headline and narrow the cards so a
full round fits 1440x800
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The deliverable died in #545; the word survived as the decision-page
payload's field name, annotated everywhere it appeared with the same
compatibility apology. The page and the skill text ship together and
payloads are per-session, so the compatibility burden is one input alias,
not a frozen name.
serve-question.mjs: the card field, the answer key, the schema docs, the
--schema example, the help text, and every internal identifier (compSrc,
data-comp, .media.comp-pending, img.comp, comp-note) now say comp; a
payload declaring the legacy sketch key still renders and answers
identically. new-work.md and the asset producer drop their wire-name
parentheticals. The unit suite covers the canonical answer key coming
back from a legacy-key payload; the new-work e2e's declined-card stray
comp stays declared as sketch, which doubles as alias coverage.
AI-assisted (Claude Fable 5), prepared for maintainer review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Polish the decision page: raise cycler, declined height, canon order, full card anatomy
Field feedback from the first real rolls of the verdict-routed hand:
- Several raises stacked on the assigned card blew it out of proportion.
More than one raise now renders as a compact cycler: one visible, a
counter, click or Enter advances. A single raise stays inline.
- Declined cards inherited the row's stretch alignment, so a narrow card
stood at the tallest contender's height, a strange stilt beside the
hand. They now size to their content.
- Deck order becomes a gradient of standing: contenders, then the canon,
then declined dead last. The canon between full alternates and the
demoted row reads as the familiar door rather than the last resort
after the rejects.
- Root cause of bare-bones challenger and canon cards in the field: the
--schema example only gave the assigned card palette, materials, and
risk, and models author payloads by imitating the example, so the
"same anatomy on every card" instruction lost to it every time. The
example now carries full anatomy on every card and the schema note says
a card with no palette chips is an authoring gap, not a data gap.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Decision cards carry full-fidelity comps instead of sketches
Field verdict on the sketch contract: the sketches came back too simple
to inform the choice, and generation takes the same time at any
fidelity, so the deliberately-unfinished frame paid comp cost for sketch
quality. The decision card's image is now that direction's north-star
comp, produced under visualize.md's comp discipline (structure-led
prompt, real name and content, no invented commercial claims), saved
under .impeccable/mocks/ with its prompt sidecar. Fairness between cards
comes from equal fidelity in each card's own grammar rather than shared
unfinishedness.
The chosen card's comp is never spent by the choice: on a comp-led build
it enters the comp round as compositional option one (visualize.md now
generates two variations beside it; a round arriving with no decision
comp still renders all three), and on a code-led build it returns at the
finish review as the critique reference. Produce order still front-loads
a re-roll's spend onto the cards read first.
serve-question keeps the sketch field's wire name for payload
compatibility; docs, schema paths, shimmer labels, and the answer
directive (CHOSEN COMP) speak comp.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: address PR review bot findings on the comp round
- Producer still forced sketches (cursor, high): the asset producer's
Decision Sketches contract still mandated deliberately unfinished matte
sketches, so the parallel path would keep shipping sketch-era images.
The section is now Decision Comps: full-fidelity north-star comp,
structure-led prompt, equal commitment across siblings, no invented
claims, sidecar written.
- Mocks collided with the approval check (cursor, high): decision comps
now live under .impeccable/mocks/decision/, visualize.md scopes the
no-approval finding to comp-round output, new-work.md states the
unchosen hand implies no approval, and the code-led finish packet names
the chosen decision comp as the critique reference in the approved-comp
slot.
- Raise cycler announces (greptile, both P1s): a visually hidden
aria-live region reads out the newly active raise and its position on
advance; initial render stays quiet.
- Declined width in the vertical deck (cursor, medium): align-self:
flex-start shrank declined cards to content width in the portrait
column layout, where the cross axis is horizontal; they stretch there
and keep content height in the row layout.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: raise cycler tooltip and label name both input modes
Copilot: the tooltip said Click while the control also answers Enter and
Space; the title and a new aria-label now say activate/press Enter.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: finish reviewer exempts decision comps from the approval check
cursor[bot] follow-through: the reviewer's Persistence check still
treated any comps under .impeccable/mocks/ as approval-gated, and the
reviewer never reads visualize.md by design, so code-led and spent-hand
rounds could draw a false skipped-approval finding. The check now scopes
to comp-round comps, exempts .impeccable/mocks/decision/ as the direction
round's dealt hand, and defines how a code-led build's decision comp is
judged in the approved-comp slot: the critique reference, under the
no-approved-comp fidelity rules plus what the image dared that the build
did not.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: the critique reference is its own reviewer input, not the approved-comp slot
cursor[bot]: passing the code-led decision comp through the approved-comp
slot dragged in that slot's obligations (inventory-first reading, the
fidelity matrix, Truth's shipped-asset demand for every image-native
region), which contradicts code-led's premise. The input contract now
names it a separate labeled critique-reference input that nothing binding
"the approved comp" touches, and Fidelity defines its treatment where the
no-approved-comp rules live: provocation, not spec; no matrix, citations,
or asset obligations; its dares enter material_fixes as ordinary fixes.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* Route the direction hand by verdict, add the pick card, enforce salience parity
The decision round previously rendered every dealt challenger as an equal
full card whatever the weighing said, so a world that fused poorly (an
underwater world dealt to a flower shop) sat at the same visual weight as
the assigned direction, and concept-level fusion had no surviving output.
Three changes, all presentation-layer; the dice, the assignment, and the
two-axis weighing are untouched:
- Verdict routing: the weighing closes with wins / competitive / declined
per challenger, decided before any borrowing. Declined challengers render
demoted (narrow, quiet, catalog art as a labeled thumb, "Adopt anyway"),
reordered to the end of the deck by the page itself, still adoptable,
never silently dropped. Donations return as named "raised by" lines on
the assigned card: a declined challenger donates ambition and system
discipline, never its clothes.
- The pick card: one card for the model's top-ranked grounded candidate
when the dice assigned another, kicker MY PICK, honest familiarity risk
on its face. One card, never a ranked list, never the lead position; the
anti-menu rule survives with exactly this carve-out.
- Salience parity: a card's imagery weight is capped by the assigned
card's. With a text-only assigned card (no image generation in the
harness), full-bleed catalog heroes demote to labeled thumbs, so what
looks important is the verdict's call, never rendering luck.
serve-question payload gains additive fields (verdict, kept, raised); old
payloads render unchanged. concept-seed's rendered instructions carry the
verdict/donation contract and the pick-card carve-out. Covered by two
Playwright tests in the new-work e2e suite (verdict routing + parity).
Design exploration and rationale were worked through with the maintainer;
research grounding is impeccable.style/research lessons 3-5.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Add Safer/Bolder re-roll registers to the direction round
The re-roll gains the user's steering wheel on the familiar-to-bold axis.
The decision page renders two register buttons beside the plain re-roll
(payload: reroll: { registers: ["safer", "bolder"] }; booleans still work),
the answer carries the chosen register, and concept-seed gains --register.
The design constraint that shaped the implementation: a register changes
only what a round INSTRUCTS, never what it DEALT. The same key and reroll
count reproduce the same deal whatever the register, so the exclusion chain
never forks and the reproduction contract holds with no API change.
- bolder: the dealt foreign forms become the whole hand, every challenger a
full card; the first-dealt challenger leads (assignment by deal order, so
the dice still choose). The pick card sits out; the canon stays.
- safer: the round's dealt hand is spent unseen and stays excluded; the
model presents its remaining conventional grounded candidates (at most
three) plus the canon executed against named competitors. This is the one
sanctioned lineup of the model's own ranked list, existing only by
explicit user request. Works degraded (needs no catalog); bolder degrades
to a plain grounded round, disclosed.
Registers are user steering, never the model's to pre-select. Covered by a
concept-seed unit test (same-deal invariant, validation) and a Playwright
test (button, answer field, REGISTER directive).
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Add the execution-contract round: comp-led or code-led, chosen after the direction
The build previously went comp-led for everyone, silently: a generated comp
led and the build chased it, which produces the boldest compositions and
also the measured worst-of-both-worlds failure (ambitious design landed
poorly, no motion, fix rounds after). Models already defect from it by
quietly skipping comp generation, which is unsanctioned code-led with no
contract to catch it. This makes the fork explicit and both paths
defection-proof:
- Comp-led: the comp is law and non-optional once chosen; visualize.md and
the comp-is-king build phases run as today.
- Code-led: no comp of this page, skipped by contract rather than drift.
The QUALITY BAR boards still calibrate finish, and the ambition moves
into the written direction contract (FIRST VIEWPORT plus a named
signature interaction and motion grammar), audited by the finish
reviewer in behavior. Not a discount on commitment.
Placement: a second round on the same open table, right after the
direction lands. Sketches stay in the direction round (they pick the
world); comps are what code-led skips (they bind the composition). The
chosen world sets the default lead; the user flips freely; a standing
preference recorded in PRODUCT.md skips the round on later surfaces; with
no image generation there is no fork, code-led is the only path.
Mechanism: serve-question gains payload-level followup: true, which keeps
the detached server alive after a pick (exactly like re-roll), swaps the
page to the loading hand instead of goodbye, marks the answer with
followup: true so --wait keeps the table, and prints a FOLLOWUP OPEN
directive telling the agent to deliver the next round via --update.
Covered by a Playwright test driving the full two-round flow.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: address PR review bot findings
- Degraded safer register no longer contradicts itself (greptile,
Copilot, cursor): the degraded template previously said "the assigned
index is suspended; the user picks" and then emitted ASSIGNED INDEX,
the mandatory build instruction, and the restated footer anyway. The
degraded safer path now suppresses the assignment machinery entirely,
matching the non-degraded safer round, and restates the user-picks
behavior for truncated readers instead.
- A declined card's declared sketch no longer renders a full media face
(Copilot): the renderer ignores sketch slots on declined cards
outright, so a stray sketch cannot buy back the salience the verdict
took away.
- Bolder rounds no longer carry the generic weighing instruction
(cursor): it measures against the assigned grounded direction, which
the bolder register suspends; a leader-relative variant weighs the
fused challengers against the first-dealt leader instead.
All three pinned by new assertions in tests/concept-seed.test.mjs and
tests/new-work-e2e.test.mjs.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: followup never arms the loading hand in blocking serve mode
cursor[bot] caught a client/server disagreement: the page interpolated its
FOLLOWUP constant from the payload alone, so a followup: true payload served
in blocking mode (no --start) would leave the browser on a loading hand that
nothing resolves, since a blocking server exits on any pick and has no
update channel. The page constant is now armed only when the server is
detached, blocking rounds get the goodbye screen as before, and new-work.md
states that followup belongs only on a detached round; blocking and
structured-tool channels run the build-path round as its own second
question. Pinned in tests/serve-question.test.mjs.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* Add card-kind choice telemetry and the bolder routing disambiguation
The choice ping previously fired only when a dealt catalog challenger won,
so pick-share and canon-share had no denominator and the decision page's
new spectrum could not be measured. The ping now fires once per resolved
attended round on API-dealt rolls: --kind names which card class won
(assigned / pick / challenger / canon), --chosen carries the catalog id
only when a dealt challenger won, and --register rides along when the
round came from a steered hand. Grounded candidates' names never leave the
machine (the ping carries the kind alone), the legacy id-only shape stays
valid, and DO_NOT_TRACK / IMPECCABLE_NO_TELEMETRY still disable the ping
entirely. The seed's TELEMETRY block teaches the new invocation.
Also the naming-collision guard: "bolder" said while a direction round is
open routes to the Bolder hand register, never the bolder refinement
command; one line each in bolder.md and new-work.md.
The /api/chosen field additions land in a sister impeccable-site PR; the
API ignores unknown fields meanwhile, so this is safe to ship first.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: ping test survives a DO_NOT_TRACK shell
cursor[bot]: the pingChosen unit test cleared only IMPECCABLE_NO_TELEMETRY,
so a developer shell with DO_NOT_TRACK set failed the success-path
assertions. The test now clears both, restores prior values in finally,
and passes under DO_NOT_TRACK=1.
AI-assisted change.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* Gracefully handle the no-image decision page
Tested the new-work path without image generation and fixed what broke:
- A text-only card's back face (First viewport, The case) was unreachable:
the Details flip chip only rendered inside the media block. Cards with no
imagery now render their full read on the front and skip the back face.
- A hero/board that fails to load (retired catalog URL, offline shell) sat
as a dark void with a zoom cursor. The slot now collapses to a field
painted from the card's own palette with an "artwork unavailable" pill;
broken inspiration PIPs remove themselves.
- Sketchless catalog art rendered unlabeled as the card's face, reading as
the promise of the build. It now carries the same "inspiration" label and
hover title the PIP uses.
- The --schema example pointed at catalog URLs that 404 (missing family
prefix); updated to the real asset paths and noted the text-only front
behavior in the schema prose.
Extends e2e test (e) with the front-read and label assertions and adds
test (f) for the broken-image fallback.
AI-assisted (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: address PR review bot findings
- cursor[bot]: the unavailable-art scrim painted over the flip chips and
swallowed their clicks; it now passes pointer events through and the
chips render above it.
- Copilot: a palette-less card whose art failed still read as a dark void
and kept the stale Inspiration tooltip; the slot now falls back to the
graphite field in CSS and the tooltip is removed with the art.
Test (f) now covers both: a broken card with back facts must still flip
via Details, and a palette-less broken card gets the labeled fallback.
AI-assisted (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
A landscape frame was the silent default at every generation site,
which is a composition error before the build starts for native apps
and mobile-first surfaces. The sketch frame, the asset producer's
single-sketch contract, and the comp instruction now state it: portrait
at device viewport when the surface is a phone screen, landscape for
desktop web. The decision page adapts in kind: portrait art overrides
the 16/10 slot with its own exact ratio so nothing crops, and the deck
narrows so portrait cards line up side by side. The --schema guidance
tells the model the page handles either.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cards widen from 27vw to 34vw and the media slot matches the 16:10
sketch frame instead of cropping it to 16:9: at the old width the
imagery read as a thumbnail above a column of copy, and the copy won
the attention contest the sketch exists to win. The whole image is now
a zoom target with a zoom-in cursor, not just the expand chip; chip and
PIP handlers already stop propagation, so the art click is unambiguous.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A codex session declared the decision board dead while the user was
still reading it, then proceeded without their choice. The wait's
liveness probe was process.kill(pid, 0) with every error treated as
gone, but a sandboxed exec cannot signal a process outside its sandbox:
EPERM arrives for a living server. Liveness now leads with the page's
own heartbeat in the state file, falls back to the kill probe, and
reads EPERM specifically as exists-but-unsignalable. The exit-2 message
also stopped inviting the wrong recovery: it now states this is a
server failure, not a user decision, and orders a restart and reopen,
never an unattended proceed while the user's browser session is open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A hidden backface still hit-tests in Chrome, so after flipping a card
the front's picture-in-picture sat invisibly over the back's chips,
showing its zoom cursor and eating the flip-back click. Pointer events
now follow visibility: the back is inert until the card flips, and the
front goes inert while it is flipped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field data: the first image of a real batch took ninety seconds and the
page's 150-second fallback then silently promoted catalog art to full
bleed, unlabeled, which is exactly the this-is-your-design misread the
picture-in-picture treatment exists to prevent. The policy is now
patience while there is progress: a slot shows its inspiration only
after waiting four minutes with nothing landing anywhere on the page
for four minutes, the stand-in is dimmed and labeled 'inspiration ·
sketch pending', and polling continues so the real sketch still swaps
in whenever it arrives. Slots with no inspiration keep the honest
elapsed shimmer instead of folding. The parent's reclaim rule matches:
files landing steadily is health at any pace, and only total silence,
no first file in three minutes, takes the batch back inline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field feedback: with every fact stacked under the media the cards ran
past a screen tall. The front now carries only what the choice needs,
sketch, lineage, title, thesis, identity, and the honest risk clamped
to two lines, while first viewport and the case read on the back behind
a Details chip, sharing the face with the board when the world has one.
Risk stays on the front because the counterweights are pointless if the
downside hides behind a flip, and once the sketch lands the first
viewport is a picture anyway. The schema notes now ask for one-sentence
facts, since a long fact should cost the reader a flip, not the page
its scanability.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A codex field run dealt six challengers into an eight-sketch batch
behind an opaque subagent, and the user stared at a page of shimmer
asking whether anything was happening at all. Four fixes from that run.
A hand now holds at most three challengers, the rest banked for
re-rolls, so fairness within the hand stops multiplying into a queue.
Sketches greek everything but the product's real name and one real
headline, because an invented spec, price, or ship date in a sketch is
a claim PRODUCT.md never made, and comps have solved this for a century.
Sketch production follows the user's reading order with the first file
doubling as the producer's heartbeat, and the parent's --wait loop
checks the sketch directory each pass, reclaiming the batch inline when
two minutes pass with nothing landed. And a failed --start now captures
the daemon's stderr to a per-key log and names the sandbox as the usual
suspect, instead of reporting only that failure occurred. The shimmer
counts its elapsed seconds, and gives up at 150 instead of 300.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three field notes from a live review. The deck now escapes the content
column and runs edge to edge, so a cut-off card sits at the screen edge
where it reads as more cards instead of at an invisible container edge
where it reads as a bug; the first card still aligns with the column
via scroll padding. Whichever side hides more content wears a fade, and
a hard edge means the end. The vertical pager grows from a bare chevron
into labeled Back and More pills, because in a column deck it is the
primary way forward. And hovering the inspiration thumb now takes over
the whole media region instead of a timid zoom; the sketch is the
promise, the inspiration is a glance, and the glance must cost nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field-checked in a real browser, which surfaced three defects the DOM
tests could not: the generic .media img display rule defeated [hidden]
and floated an empty block over the shimmer and its sketching note; the
deal animation left every card at opacity zero in an unfocused tab,
because rAF throttling is real and decoration must never gate content;
and the sketch poll's cache-busting query missed the anchored /img
route, so a landed sketch kept shimmering forever.
The grid is now a snap-scrolling deck: one row in a wide viewport, one
column in a tall one, with edge arrows that appear only on overflow and
page one card at a time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decision page compared unlike things: the grounded direction was a
wall of text beside curated catalog art, the catalog art read as a
promise of the build, the weighing silently shrank the challenger set,
and the standing exit hid in the footer under the cards it must not
soften. Every card now shares one anatomy (thesis, palette chips,
material tags, first viewport, case, risk), every dealt challenger is
presented with the weighing written on it rather than applied to it,
the catalog image rides picture-in-picture as labeled inspiration with
the lightbox a click away, and canonCard renders the category standard
as one honest, subordinate card.
When image generation exists, each card declares a sketch slot the page
polls: serve first, generate after, through one shared deliberately
unfinished frame, so the comparison stays about direction instead of
rendering luck. The asset producer takes the batch when subagents
exist; the chosen sketch returns in ANSWER to seed at most one comp
probe, and the comp round still renders its full set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The release-gate campaign confirmed two skill bugs with transcripts.
Sandboxed harnesses reject absolute paths, so following the CHOSEN
CARD directive with the absolute card-base path failed view_image; the
directive and the quality-bar clause now say download into the
workspace and open the relative path. And under the openai worker's
turn cap, models spent the budget on init, cards, and comp generation
and never built the page (a third of small-n supplement slices); the
visualize mandate gains its one exception: at a hard cap the shipped
page outranks optional imagery.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The local-image map fell through to image/jpeg for anything that was
not webp or png, so an svg hero (the fake comp generator's native
format) silently failed to render on the decision page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
From Paul's approved UX and the eb686f36 session post-mortem:
The standing exit: direction rounds carry a quiet, permanent "Play it
straight" action (payload flag canon, reserved id) on the decision page
and as the last structured-tool option. It is the user's door, never
the model's: never recommended, never weighed against the roll, and
choosing it swaps the bar rather than lowering it, two or three named
reference products become the craft level, canon executed at full
commitment. Safer/conventional steers resolve here, never to a
stranger re-roll.
Session fixes, each mechanical where possible: the ANSWER line now
names the chosen card's hero and board and directs opening them before
code (the session built from text alone after viewing a different
world's card); generation scale joins the imagery rule (a library of
centered 128px subjects foreclosed the atmospheric hero); DESIGN.md
rules are checked against the world's native devices and never added
to silence a hook finding (the session banned arcade lettering's own
offset shadow and laundered 8px through the ramp); staging joins the
FORM contract block (the axis was dropped silently at world-choice);
the finishing reviewer audits the ceiling against the QUALITY BAR card
after persistence (floor rigor was disguising unreached ambition); the
icon-tile clause names hand-drawn icons as remedy, not target.
Dist rebuild deferred: the release-gate campaign reads the pinned dist.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A grounded direction with no hero rendered a blank 16:9 void where the
card image belongs (seen live: the assigned Xerox Zine card led the
hand as a black hole next to two rendered challengers). An option with
no imagery now drops the media region entirely and leads with its
kicker and text; an option with only a board shows the board as its
front image with no flip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The new self-detection ran before mode dispatch, so it also caught --wait,
--stop, and --schema. CI failed on the start/wait cycle test: --wait
returned 2 (no browser) where the documented poll loop expects 3
(WAITING). Under CI=1 the suite went 4 pass / 2 fail; it is 6 / 0 now.
Two of those modes were user-facing bugs, not just test breakage. --stop
exited 2 without killing the daemon it was asked to kill, leaking a
server process (verified: one daemon running, CI=1 --stop, still one).
--schema only prints a payload example, and new-work.md tells the agent
to read it before building a payload.
Detection can only tell whether this process can auto-open a browser, not
whether the user has one: SSH with a forwarded port and a harness with an
in-app browser both have a browser and no DISPLAY. The file already
treats serve-without-opening as first class, since --start spawns its own
daemon with --no-open. So the check now gates acquiring a session, not
managing or ending one. The blocking serve path still exits 2 on a
headless box, with a test pinning that.
Co-Authored-By: Claude <noreply@anthropic.com>
The env-var bypass (IMPECCABLE_QUESTION_DISABLED) relied on the harness
remembering to set it. The script now also self-detects CI, SSH-without-
display, and displayless Linux and exits 2 with the structured-question
advice; --no-open skips detection (caller opens the URL itself, as the
tests do) and IMPECCABLE_QUESTION_FORCE=1 overrides it. new-work.md now
frames the decision-page rule by capability: open a browser if you can,
structured question tool if you cannot, exit 2 means fallback not error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IMPECCABLE_CARD_BASE overrides the quality-bar URL prefix so eval
workers serve cards from the local checkout while impeccable.style
stays undeployed. IMPECCABLE_QUESTION_DISABLED makes serve-question
exit immediately with the structured-question fallback line, so
headless workers never block on a browser page nobody will answer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul's question exposed the blind spot: a closed tab left the agent
waiting out the full timeout. The page now sends a heartbeat every five
seconds while open; the server stamps lastBeat into the state file, and
--wait reports PAGE CLOSED with exit 4 when the beats stop for fifteen
seconds without an answer. The prose defines the fallback ladder:
re-present once through the structured question tool, then proceed
unattended with the assigned direction, stating assumptions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The loading hand now has the true proportions: 16:9 shimmer media, a
tier line, a title line, three detail lines, and a button-shaped block
pinned to the card bottom, with each skeleton inheriting the measured
height of the card it replaces.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live-fire bug from the demo: collecting a re-roll answer deleted the
state file, so --update could not find the still-running server and the
next hand had nowhere to land. Cleanup is now terminal-only: a re-roll
consumes just the answer file and leaves the server state for --update;
any other choice cleans up fully as before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two upgrades from the live session. Re-roll no longer ends the page: in
detached mode the server stays alive, the client gathers the cards back
into the center stack, deals skeleton cards with the site's shimmer,
and polls /next-status; the new --update mode delivers the next hand
and the page reloads into the fresh deal. Choices other than re-roll
still resolve and exit as before.
And the headline bug had a root cause: the fonts link never loaded the
weights in use, so the browser synthesized a fake 300 that read
off-brand and muddy. The link now loads Alumni 100 and 400 exactly, and
the h1 wears the homepage hero display role: weight 100 at
clamp(2.6rem, 5vw, 4.2rem), champagne.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#lightbox { display: flex } outspecified the UA's [hidden] rule, so an
invisible full-viewport layer sat over the page and blocked all hover
and click. #lightbox[hidden] { display: none } restores reality.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul's motion and exploration pass. The reveal is now a real deal: the
cards begin piled at the grid center, blurred and slightly rotated, and
travel to their seats with a 110ms stagger on the brand ease (JS
measures each card's seat, so the pile works at any grid shape;
reduced motion skips it). Hovering a card bleeds its hero into the page
ground behind a lacquer scrim. An expand chip beside Board opens
whichever face is showing in a zoom-out lightbox with Escape to close.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No cropping and no pillarboxing: the board spans the card width at its
native 16:9 exactly like the hero on the front, with the deep-lacquer
ground below and the label-and-CTA bar pinned to the card's bottom edge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The design-system board is an information sheet; the back face now
contains it fully on a deep-lacquer ground rather than cover-cropping
its top and bottom.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul's read: flipping only the image inside a static frame looked
unconvincing. The card is now the object: front face carries hero,
lineage, title, and CTA; the back face is the design-system board at
full card height with a slim bar keeping the label and Build-this
reachable. The outer card keeps fan, deal, and hover; each face carries
its own lacquer chrome and the rolled card's gold ring rides both
faces. 700ms preserve-3d turn, instant under reduced motion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third polish pass with Paul live. The details collapsible is gone: the
card media is now a 3D flip, hero on the front and the design-system
board on the back, toggled by a small mono Board/Hero chip with a 600ms
preserve-3d turn that reduced-motion collapses to an instant swap. The
headline and question sit directly above the dealt hand inside the
centered stage while the logo holds the top-left corner. Re-roll
stretches to the steer input's height and says just Re-roll beside the
five-pip die.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>