* 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>
The list of Live chrome surfaces was inlined into live-browser.js as a
function-scope const when live/ui-core.mjs was deleted for having zero
in-repo references. It had one out-of-repo reference. The private
impeccable-site repo imports it at build time: its Live UI lab must hold
a snapshot for every surface Live defines, and the site build fails with
the surface name when one is missing. Inlining put the list out of reach
of every Node importer, so the site had to regex it back out of the
browser script, and the guard only kept passing because the site's
materialized copy of skill/ was stale.
A guard that reads a list the site itself maintains guards nothing, so
the fix is a real export rather than a better parser.
skill/scripts/live/ui-surfaces.mjs is now the single definition. The
browser-runtime constraint is unchanged and satisfied the same way the
command palette already solves it: live-browser.js is served raw and
injected as a classic <script>, so it cannot import an ES module. The
/live.js assembler serializes the module into
window.__IMPECCABLE_LIVE_UI_SURFACES__ in the prelude it already writes
for the token, port and vocabulary, and live-browser.js reads the global.
assembleLiveBrowserScript defaults the value from the module rather than
taking it from live-server.mjs, so the bundle carries the canonical
inventory by construction instead of by a caller remembering to pass it.
The emitted inventory is byte-identical to the inlined one.
tests/live-ui-surfaces.test.mjs pins both halves of the seam: the module
is the definition (live-browser.js must not redeclare it), the prefix the
module builds ids from matches the PREFIX live-browser.js hardcodes, and
the assembled bundle still carries the list. live-server.test.mjs gets
the matching integration check against a served /live.js.
One existing assertion changed. live-browser-regression.test.mjs checked
that the steer Send control is registered as live chrome by matching the
text of the inline literal's last line. That encoded where the list was
written, not what it contains; it now asserts membership in the imported
LIVE_UI_COMPONENT_IDS, which is the behaviour it was after.
Verified with the full default suite plus a live-e2e fixture run
(vite8-react-modal), so the overlay is exercised end to end in a browser.
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Make critique storage the single owner of snapshot discovery and frontmatter parsing, and keep context signals focused on summarizing the canonical result.
AI-assisted: Prepared by Codex under pbakaus's scheduled architecture-refactor authorization.
Emit Codex-compatible top-level keys while preserving the argument hint under metadata. Keep existing Claude-style pin frontmatter unchanged for other harnesses.
AI assistance: Codex implemented and validated this change under maintainer pbakaus's standing authorization.
Optional `avoid`, two or three negations of 12 to 160 characters. A world built
from posters is at risk of shouting; one built from instruments is at risk of
dead greys. The global detector cannot know which and the author can, so the
"do not" belongs beside the "do" rather than in a rulebook that applies to
everything equally.
Optional on purpose: 541 entries predate it and none of them are wrong for
lacking it, so nothing needs backfilling.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A 3-star held two tickets and a 1-star held none. On a pool this size that is
not a nudge, it is the shape of the draw. Measured against the live catalog:
3-star worlds absorbed 57% of the graphic draw from 65 of 163 eligible worlds,
46% of atmosphere from 13 of 43, and 75% of interaction from 15 of 25. The
reviewer's report that the same worlds keep returning is exactly what a rating
multiplier does to a corpus whose thinnest tier holds 25 worlds.
Now a 3-star draws level with a 2-star, and a 1-star draws at half rather than
not at all. Excluding a marginal keep made rating do a job breadth already does
properly: breadth still removes a niche world from the pool entirely, which is
the honest way to say "too narrow to challenge an arbitrary build", while a
1-star records "unexceptional" and is still worth showing sometimes.
Effect on the same catalog: the 3-star share falls to 39% on graphic, 30% on
atmosphere and 60% on interaction. That last one is no longer a weighting
artefact, it is simply what the tier contains, since 15 of its 25 eligible
worlds are rated 3.
Compositions get the same treatment; the two ticket functions had the identical
shape and no reason to disagree. Both tests asserted the old policy directly
and now assert the new one.
Co-Authored-By: Claude Opus 5 (1M context) <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>
* test: harden the test strategy (triggers, runner speed, release guards)
Follow-ups from an end-to-end testing strategy review:
- Suite triggers are now auto-generated from each suite's own file list,
so change-based CI can never miss a test file again (four files were
unreachable by their own edits, and tests/lib/detector-bundle.test.js
triggered core while running in detector). Two new meta-tests pin the
invariant. Hand-written trigger patterns now carry only source paths
and fixture dirs; palette dropped from the live triggers since no
suite tests it.
- The node runner batches all files into one node --test invocation at
concurrency 4 instead of spawning per file. Default suite drops from
~159s to ~100s; the live suite soaked clean three times.
- scripts/release.mjs gets its first tests: 12 scenarios spawning the
real script inside a disposable git repo with a local bare origin,
covering every refusal guard plus notes/tweet rendering, all under
--dry-run.
- skill/scripts/live/ui-core.mjs deleted: zero references repo-wide,
superseded by the July live rewrite, yet still shipping to users.
cli/lib/download-providers.js annotated with its cross-repo consumers
(impeccable-site Pages Functions) so it is not mistaken for dead code.
- CLAUDE.md gains an area-to-suite table for the opt-in suites a change
owes; AGENTS.md syncs the plugin-e2e commands and obligations.
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Code <noreply@anthropic.com>
* fix: exclude peeled tag lines from release-test origin cleanup
Copilot: git ls-remote --tags emits ^{} peel lines for annotated tags,
which are not deletable refs; --refs filters them so the cleanup loop
survives a future scenario that pushes an annotated tag.
AI-assisted via Claude Code under maintainer direction.
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* Fix: unescape YAML quote escapes in DESIGN.md frontmatter scalars (#428)
parseScalar() stripped a double-quoted scalar's outer quotes without
processing the backslash escapes inside, so a font stack that quotes a
multi-word family the CSS way, e.g.
fontFamily: "\"IBM Plex Sans\", system-ui, sans-serif"
reached allowedFonts as '\"ibm plex sans' and design-system-font flagged
fonts DESIGN.md declares. Also collapses the doubled-quote escape in
single-quoted scalars and keeps a lone quote literal instead of slicing
it to an empty string. Applied to both copies of the parser
(cli/engine/design-system.mjs and skill/scripts/lib/design-parser.mjs).
Co-authored-by: Cursor Agent (AI-assisted change, reviewed and directed
by a maintainer)
* Decode YAML hex and Unicode escapes in double-quoted scalars
Review follow-up: the escape scanner only handled the simple set, so
\xNN, \uNNNN, and \UNNNNNNNN sequences stayed encoded and an escaped
token like "\x23b8422e" never matched #b8422e in CSS. Decode validated
hex escapes in both parser copies; malformed or out-of-range sequences
stay literal. Regression coverage for all three forms.
Co-authored-by: Cursor Agent (AI-assisted change, reviewed and directed
by a maintainer)
* Complete the YAML 1.2 double-quote escape set
Review follow-up: the escape map omitted the escaped space (\ ) and
non-breaking space (\_) forms, so fonts declared with them kept a
literal backslash in allowedFonts and their CSS declarations were
reported as undeclared. Map the full spec 5.7 set (\a \b \v \f \e
\N \L \P included) in both parser copies instead of chasing one escape
at a time. Regression coverage for both named forms.
Co-authored-by: Cursor Agent (AI-assisted change, reviewed and directed
by a maintainer)
Keep unmanaged detector fields when ignore-file updates the canonical detector configuration. Add a regression covering existing extension mappings.\n\nAI assistance: Codex implemented and validated this change under maintainer authorization.
The containment gate re-canonicalized projectCwd for every target file
in the per-edit and Stop loops. The hook runs as a fresh process per
tool event, so a module-level memo makes it once-per-event work; the
size cap only matters to long-lived importers like the test runner.
Addresses Copilot review feedback on PR #471.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
hook-before-edit.mjs kept its own string-based isInsideProject; it now
uses the shared isScanTargetInsideProject so all three hook passes
apply one containment semantic, symlink canonicalization included.
Because the before-edit hook gates proposed Writes whose target does
not exist yet, canonicalPath now resolves the nearest existing
ancestor and re-appends the remainder instead of falling back to the
raw resolved path — a new file under a symlinked root compares equal
to its canonical project.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
The per-edit and Stop deep passes gated on sensitive paths, generated
paths, extension, config ignores, and size, but never on containment.
Any file the session touched outside the project (harness scratchpad
dirs under the system temp root, sibling checkouts) was scanned and
judged against THIS project's config and DESIGN.md palette, producing
design-system findings that are wrong by construction.
Both loops now check isScanTargetInsideProject() (audit reason:
outside-project), matching the gate hook-before-edit.mjs already had.
Paths are canonicalized so a symlinked root doesn't split the
comparison. The Stop pass re-checks containment itself because caches
written by older hook versions can still list out-of-project paths.
Umbrella-dir launches (issue #305) are unaffected: their projectCwd
resolves to the edited file's own project root, so containment holds.
Written with AI assistance (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
context.mjs now probes cwebp/sips/magick/ffmpeg once (which/where per
OS) and prints IMAGE_TOOLS, replacing macOS-specific prose; the
IMAGE_GEN_AVAILABLE directive leads with the harness-native tool so a
present OpenAI key stops reading as an instruction to bill it; and the
sandboxed board-start guidance sheds codex vocabulary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recognize both slash- and dollar-prefixed prescribed seed markers and exercise each variant in coverage tests.
AI assistance: Codex addressed Cursor and Copilot review feedback and reran validation under maintainer authorization.
Treat Components as optional only when DESIGN.md carries the prescribed seed marker, while retaining Colors and Typography checks.
AI assistance: Codex reproduced the issue, implemented the fix, and added regression coverage under maintainer authorization.
Join indented Markdown bullet continuations and keep them out of Overview philosophy text.
AI assistance: Codex reproduced the issue, implemented the fix, and added regression coverage under maintainer authorization.
Three of the six axes cannot be read from a world's prose, and widening their
keywords manufactures signal rather than finding it. Depth's probe matched
worlds that said "no cast shadow anywhere" and "without perspective or depth";
motion and colour strategy describe properties the system rules never state, so
they place 28% and 7%.
An optional axes object on the concept records the value instead. Absent means
inferred from the rules as before, so nothing needs backfilling. Validated
against the axes definition when the caller supplies it, because a typo would
read as "unrecorded" and fall back to a probe already known not to work, which
is the quietest way for this to fail.
This is what makes an assigned wave measurable. If a wave draws "drenched" and
"simulated physics" before designing anything, the world it produces has to
carry those values or the assignment is lost the moment it lands, and occupancy
goes back to guessing at prose.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex's risk reviewer rejected the network-escalated roll rerun for
'contacting an unspecified external domain' and the assumed export of
project context, so the run degraded to no challengers. Both concerns
are answerable: the script's only network contact is one GET to
impeccable.style/api/roll carrying scope, mode, an eight-hex key, and a
re-roll counter, nothing project-derived. The degraded message now
states that verbatim and tells the model to put the URL and payload in
its approval request, so the reviewer judges the real action instead of
an unknown.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The composition pool (stagings) is not ready: too thin to help, and its
draws crowd the decision it rides along with. concept-seed.mjs stops
rendering the staging block by default; IMPECCABLE_COMPOSITIONS=1
re-enables it for catalog development, and the draw machinery,
rating-weighted selection, and mode scoping stay intact and tested for
its return. new-work.md drops the dress-the-staging-challengers
instruction and the FORM contract's staging clauses; the surface-scope
roll still assigns which of the model's own structures gets built.
Co-Authored-By: Claude Fable 5 <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>
The area taxonomy was wrong, and wrong in a way worth recording. It was
derived from Mobbin-style categories in the abstract rather than from what
the skill can be asked for, and measured against the catalog most of it
described problems that were not there: onboarding, settings, empty-state and
search each had zero entries.
Reframed against demand instead. A user asks for a docs site, an onboarding
flow, a landing page, or a data table, and those differ in how much of the
product is in play. Register already says what kind of work it is; grain says
how much: product, flow, view, region.
Named grain rather than scope because scope already means direction-or-surface
on every roll and 'surface' is already a register value, so a scope of
'surface' would have collided with both.
Platform is the second axis: web, ios, android. Unlike grain it is a hard
filter with no fallback, because a composition that leans on hover or a
pointer does not degrade on a phone into something slightly worse, it stops
working, and an empty deal is a visible gap where a broken one is not.
Both fields are optional and absence means eligible everywhere, so nothing
needs backfilling and no existing roll changes.
The third piece is the one a trace turned up. Asking for an onboarding flow
resolves to register=operate, grain=flow, and the catalog holds zero
flow-grain compositions, so the top-up would have dealt three plausible
single-screen compositions with no signal that none matched. The model would
have improvised the flow structure while believing it was handed one, which is
the same silent plausibility the axis exists to remove. Selection now returns
a match alongside the picks, and the rendered seed says when the structure is
borrowed and why.
Measured at the time of writing: 137 of 173 approved compositions are view
grain, product grain is empty, flow grain holds one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The roll API validates its `area` parameter against the surface's list, which
meant importing the taxonomy into a Pages Function. composition-catalog.mjs
reads the filesystem, so importing from there would have pulled node:fs into
the Worker bundle, the same trap WELL_TIERS hit. roll-selection.mjs has no
imports at all and is what both callers already load, so it owns the taxonomy
and composition-catalog re-exports it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps, both reported from real use. Worlds were drawn with no mode
awareness at all: selectApprovedChallengers never received the mode, so a
build asking for an app UI could draw six worlds that only make sense on a
landing page. And surface alone is too coarse for compositions, because
"operate" spans onboarding, dashboards, editors and settings, so an
onboarding flow could legitimately be dealt a settings composition.
Worlds gain `allowedModes` on the review record, beside breadth and rating,
because it is a reviewer judgment rather than authored content. Absent means
eligible in every mode, so nothing needs backfilling and no existing roll
changes. Applied per tier and skipped where it would empty one, matching how
minRating and strength already degrade. It is a ceiling the reviewer lowers,
not a category they assign: a world is an identity, and identities transfer
across modes further than compositions do.
Compositions gain an optional `area`, one level below surface, with a
taxonomy per surface (COMPOSITION_AREAS). Area is a preference rather than a
filter: a request reorders the ranking to put area matches first and tops up
from the rest of the surface, because the per-area pools are small and
dealing one on-target composition would be worse than three good ones. A
stable partition of an already deterministic ranking stays deterministic.
`--area` on the CLI requires `--mode`, since areas are scoped to a surface,
and is validated against that surface's list so a wrong-surface area fails
loudly instead of silently matching nothing.
Also validated `breadth`, which selection has honoured for a while with
nothing checking it, so a typo read as "general" and quietly returned a
narrow world to the pool.
Four new tests: worlds excluded from a mode stay out, absent allowedModes
stays eligible everywhere, a tier whose every world excludes the mode falls
back instead of starving, and an area-scoped deal prefers its area, tops up
to three, and reproduces from its key.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The data layer has said compositions since the catalog was split, while the
code, the model-facing text, and the UI still said stagings. The rename was
held back by the selection logic existing twice; it exists once now, so this
is one pass instead of two coordinated ones.
Renamed: selectApprovedStagings, selectApprovedStaging, renderStaging, and
the model-facing STAGING GRAMMAR / STAGING CHALLENGERS / FIRST-SURFACE
STAGING INPUTS headings. The block that introduces them now states what
they are for rather than only what they are not: what is the cleverest way
to present, organize, or make interactive the problem in front of you.
Three places keep the old word on purpose:
- The rank salt, `${scope}:${key}:staging`. It is hash input, so renaming
it would re-deal every roll anyone has ever reproduced by key. Verified:
240 seeder rolls and 252 API rolls reproduce exactly.
- `Staging/hierarchy:`, the first composition grammar prefix. Inside a
composition, staging names one of its four aspects, which is a different
word-sense from staging as the name for the whole artifact. It is also a
schema constant that 317 catalog entries are validated against.
- The wire fields. The API keeps emitting `stagings` and `staging` beside
`compositions`, because the wire is the one place a rename cannot be
coordinated with already-installed skills. Clients prefer the new field
and fall back through both old ones.
Separately, WELL_TIERS had two definitions after the extraction.
roll-selection.mjs owns it now and concept-catalog.mjs imports it, in that
direction because concept-catalog reads the filesystem and a Pages Function
must not pull node:fs into its bundle. Imported and re-exported rather than
re-exported alone: a bare `export { X } from` does not bind X locally, and
validateConceptCatalog needs it, which cost one round of red tests.
Dropped concept-catalog's synchronous deterministicRank. Nothing imports it
since selection moved out, and leaving a second ranking implementation
around is how the first drift started.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
concept-seed.mjs and the service repo's functions/api/_worldroll-core.js
were two implementations of the same selection, and the API core's header
claimed they matched "exactly". They did not: it had no breadth gate on
either pool, no rating weighting for compositions, and dealt one
composition where the seeder dealt three. Since the catalog never ships
with the skill, every real user rolls through that API, so those gates
reached nobody. Two copies is the defect; this removes the second.
Written as generators rather than plain functions because the callers
cannot agree on a hash. Node has a synchronous one, Workers only have
async crypto.subtle, and renderConceptSeed's local path is deliberately
synchronous so prepared eval sessions and tests can call it without
awaiting. The selection yields batches of strings to hash and resumes
with their digests; runSyncSelection and runAsyncSelection are the only
runtime-specific code, eight lines each. Forcing the seeder async would
have broken the eval harness; forking the logic is what got us here.
No roll changes. Node's crypto.createHash('sha256') and Web Crypto's
SHA-256 return the same bytes, verified, and 240 seeder rolls plus 252
API rolls across both scopes, five modes, three reroll depths and the
rating gate reproduce their pre-refactor output exactly. The 23 existing
concept-seed tests pass unmodified, which is the point: the synchronous
contract survived.
The service repo's core keeps its own copy until this is on main, because
its deploy materializes skill/ from main and would fail to resolve an
import that is not there yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two review findings:
- restoreSvelteMarkup visited an {#each} key with outer scopes only, so a
contract prop sharing a loop binding name rewrote the key: with prop
name -> user.name and loop context "name", the key (name.id) became
(user.name.id) in the accepted route. The key evaluates per item, so it
is now visited with the loop context and index bound. Regression test
verified failing on the previous code.
- enterLiveRoot silently kept the ambient working directory when the
resolved appRoot no longer existed or chdir failed, letting a helper
derive server, session, and source paths from the wrong project. Both
cases now exit with a clear error naming the app root and the --target
escape hatch.
AI-assisted (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
removeSelectorAt walked backward to find the rule prelude and stopped at
any '>', added so the walk would not escape past the <style> open tag.
That same character is the CSS child combinator, so pruning one unused
selector from a list like '.wrap > .orphan, .orphan' cut the prelude
mid-list; when every remaining fragment equaled the flagged selector, the
whole-rule branch then deleted from the cut point and left a dangling
'.wrap >' in source. A '>' now bounds the walk only when it actually
closes a <style ...> tag; combinators are walked through.
Regression tests cover a mid-list combinator prune and the dangling-
fragment shape (verified failing on the previous code).
AI-assisted (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Addresses two cursor findings on extractMatchingSourceCss plus an adjacent
hazard in the same removal machinery:
- Class matching is token-bounded, never substring: .btn no longer seeds
.btn-primary and .stage no longer seeds .stages. A falsely seeded
selector was an accept-time deletion of a hand-written rule, since any
seeded selector the variant does not re-declare is removed as
superseded.
- Tag rules that style the pick (h1, a, p) now seed the preview stub, so
unclassed selections start from the real cascade. They are excluded
from the supersedable set: tag rules style shared elements across the
route and must never be removal candidates.
- Supersession removal is now bounded by ownership: a seeded class
selector whose class is still used by markup OUTSIDE the replaced
region survives the accept, because removing it would strip styling
from markup the accept never touched.
Tests cover substring non-matches, tag seeding with a tag-free
supersedable set, and a shared-class accept where .card is used both
inside the pick and elsewhere.
AI-assisted (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Field session on a nested SvelteKit app surfaced a self-reinforcing leak:
localStorage is per-origin, two projects reused 127.0.0.1:5174, and a
React project's leftover cycling session was resumed inside the Svelte
project. Its checkpoints then materialized a ghost session in the new
project's durable store that kept reattaching after every discard, and a
stale adapter module 401'd on live.js, hiding the picker.
Four fixes:
- Server: only session-creating events (generate, steer) may mint a
journal. Progress events (checkpoints, mount acks, accept/discard) for
unknown ids are refused with 404 unknown_session and never enqueued, so
foreign browser state cannot create ghost sessions. Browser sends are
gated so progress never overtakes its own creating POST (the Go-time
checkpoint and generate are concurrent fetches; the first sweep caught
the out-of-order arrival breaking every SvelteKit flow). Steer
checkpoints now follow the steer event for the same reason.
- Browser: saved sessions carry the server's appRoot; a session stamped
by another project is dropped at load time. Unstamped legacy state is
caught by the unknown_session refusal, which clears local state and
re-arms the picker with an explanatory toast.
- SvelteKit adapter: the layout import carries a token-derived revision
query so a helper restart changes the module specifier and no Vite
client/SSR cache can serve an adapter with a rotated-out token;
live-inject --port reads the running helper's token from server.json
instead of writing an unauthenticated live.js URL; script load failures
log an actionable console error; and adapter removal is byte-exact
(the old regex swallowed the next line's indentation).
- live.mjs resolves surface briefs from appRoot, then contextRoot, then
repoRoot, matching context.mjs in nested-app repos.
Tests: server unknown-session rejection units, adapter revision/
byte-exact-removal units, and a foreign-session e2e scenario that seeds
another project's localStorage state and asserts it is cleared, no ghost
journal materializes, and picking still works.
AI-assisted (Claude Code).
Co-Authored-By: Claude Code <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>
Stagings now honour approval ratings exactly as world challengers do, a
3-star earning a second ticket and a 1-star marginal keep leaving the
pool, which matters more here because per-surface staging pools are
small enough that an unweighted shuffle repeats a weak staging often.
Each ticket carries its index into the deterministic ranking so the
id-dedupe cannot silently collapse the doubled odds into a no-op, and
an all-marginal pool still deals rather than starving. The composition
catalog validates the new grades: 1-3, approved entries only. Tests
cover the weighting, the dedupe subtlety, and the fallback.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>