Commit Graph
437 Commits
Author SHA1 Message Date
Paul BakausandClaude Opus 5 ec189f4536 Flipping to comp demotes the inspiration instead of stacking a second slot
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>
2026-08-13 23:32:02 -04:00
Paul BakausandClaude Opus 5 816ffe92d0 Surface the build-path finding in doctor, and keep cwd out of the lookup
Round two of review findings, all four valid.

`doctor` builds its own finding list and never called `checkBuildPathUnset`,
so `config-build-path-unset` could not appear in the report even though
doctor.md documents it. That is also the only path left once stalenessCheck is
off, which is exactly when someone is looking for it.

The lookup chain included `process.cwd()`, which lets an ambient invoking
directory decide another project's workflow: run from workspace A with
--target resolving onto workspace B, and B inherited A's buildPath ahead of
the repository default. The chain is now the resolved project then the repo
root, matching `checkBuildPathUnset` exactly; cwd stands in only when no
project resolved at all.

Two prose contradictions, both mine. new-work said to write the value "when
the user says yes" and then to "record the answer either way", which reads as
persist-on-yes-only and leaves the decline to be asked again next session. It
now says the write always happens and the answer picks the value. The README
still pointed existing projects at re-running init, which is the problem this
PR exists to solve; it now names the toggle as the migration path.

The workspace-isolation test earned a correction of its own: the first version
passed a relative --target, which resolves against the caller's cwd and puts
projectRoot back on the calling workspace, so it asserted nothing.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:41:32 -04:00
Paul BakausandClaude Opus 5 c0e7f2d778 Read the repo-root build path, and stop overstating what a flip forbids
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>
2026-08-13 22:20:19 -04:00
Paul BakausandClaude Opus 5 65de2d294b Raise the skill-behavior timeout that was grading haste over thoroughness
`initialized natural build` looked like a third defect on main: sonnet began
implementation before the attended concept checkpoint, three runs in a row.
It is flaky, not broken, and the measurement setup was the larger problem.

A run that stops to put the concept to the user before building takes about
579s on sonnet. A run that skips the checkpoint and fails the assertion
finishes in 130-200s. The suite capped each test at 300s, so the thorough path
was killed as a timeout and the hasty path was graded as a result: the cap was
selecting for the behavior the scenario exists to forbid. Raised to 900s, with
the reasoning recorded next to the number so it is not trimmed back as a
mystery constant.

The baseline is corrected accordingly: the scenario is flaky (1 of 4), not
failing, and readers are told to check a duration against the cap before
calling a slow failure a behavioral one.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:13:10 -04:00
Paul BakausandClaude Opus 5 07663f5fbd Stop the update directive from spelling out a command it forbids
Two defects the skill-behavior baseline had recorded as failing on main.

`UPDATE_AVAILABLE` told the agent to ask once, then said "If they agree, run
`npx impeccable update`", then said to continue without waiting. Nothing gated
the run on an answer, and the same sentence removed the wait that could have
produced one, so the command read as the next step and sonnet took it. The
offer stays; the command leaves the turn. Running it mid-session rewrites the
files the session is reading and only takes effect next session, so there is
nothing to gain by running it now, and the directive says that rather than
relying on the model to infer it. Failed 3 of 3 before, passes 3 of 3 after.

Scenario 15 was a broken fixture, not a routing defect. The iOS workspace held
PRODUCT.md and nothing else, so `audit the app in this workspace` named an app
that was not there: sonnet spent its step budget hunting for it, including a
`find /` across the filesystem, and read no reference file at all. The
assertion reported "loaded audit.md instead of the variant" when the truth was
"loaded neither". One SwiftUI screen makes the request answerable, and the
scenario then passes on unmodified main, which is the evidence that the skill
text was never at fault. This is the convention MINIMAL_LANDING_HTML already
established for the web scenarios; the native fixture never received it.

Written with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 22:04:53 -04:00
Paul BakausandClaude Opus 5 c489335799 Build path becomes a config key existing projects can actually reach
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>
2026-08-13 21:52:17 -04:00
Paul BakausandGitHub 710aa57637 Merge pull request #576 from pbakaus/fix/ask-instruction-message-boundary
Make critique's report and close actually land
2026-08-13 17:12:52 -04:00
Paul BakausandGitHub 504b8f2a22 Merge pull request #571 from pbakaus/codex/issue-565-sketch-timeout
Fix stalled missing decision comps
2026-08-13 17:06:43 -04:00
Paul BakausandGitHub 628509b948 Merge pull request #553 from pbakaus/fix/issue-547-shadow-token-context
Allow documented sidecar shadow colors in shadow contexts (#547)
2026-08-13 17:06:00 -04:00
Paul BakausandClaude Opus 5 121602079c Deliver the report as its own step; retune the lineup
Two failures the trace test found were structural, not model quirks.

critique.md described the report's format and then went straight to writing a
temp file, with no step saying to output the report. gemini-3.6-flash and luna
both responded by bundling heredoc, snapshot write, trend read, and cleanup into
one bash call and stopping, leaving a perfect archive nobody had read. A
"Deliver the Report" step now precedes persistence, and persistence describes
itself as a copy of what was already sent. gemini-3.6-flash failed three
consecutive runs before this and its failures afterward all show the report
reaching chat.

The close is also step 6 of the persistence list rather than a section after it,
since the same shape is what fixed delivery.

Lineup: gpt-5.6-luna and deepseek-v4-flash out, gpt-5.6-terra in at
reasoningEffort high (IMPECCABLE_SKILL_BEHAVIOR_EFFORT overrides), gemini 3.5 to
3.6. Provider options resolve from the model object inside the harness so the 21
runTurn call sites are untouched. Verified the effort actually reaches the API
rather than being silently dropped.

The Gemini bump was not cosmetic: 3.5-flash passed critique closes twice and
3.6-flash then failed three times against identical text. A version bump inside
one family changed the outcome, so the README now treats cross-version carryover
as unmeasured.

Known floor, recorded: critique closes is flaky on gemini-3.6-flash, 1 run in 3.
Two structural attempts moved it from consistently failing to intermittently
passing and then stopped paying. claude-sonnet-5 and gpt-5.6-terra are clean.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 17:03:19 -04:00
Paul BakausandGitHub fde4a3ee71 Merge pull request #554 from pbakaus/fix/548-layout-transition-quoted-values
Fix: layout-transition false positives on JSX quoted transition values (#548)
2026-08-13 17:01:23 -04:00
Paul BakausandGitHub 7d907bbb14 Merge pull request #572 from pbakaus/codex/unify-svelte-accept-flow
Simplify Svelte accept orchestration
2026-08-13 17:00:57 -04:00
Paul BakausandClaude Opus 5 ebc63f071a Fix critique's close on the right mechanism
The earlier fix in this branch was built on a wrong diagnosis. It assumed a
structured question hides any prose sharing its message, so it split report and
question across two turns. A controlled check showed prose before a question
renders fine; what hides a report is emitting it AFTER the question. The split
therefore fixed nothing and introduced a worse failure: a turn that ends on the
report is a turn that ends, and the questions never arrived at all.

Persistence returns to main's ordering, byte for byte, and the boundary prose is
gone. What replaces it is a position rule: the question is the last thing in the
response.

The trace test added here found two failures beyond the reported one. Critique
can fail to land in three ways, and they are now all asserted:

  1. Question emitted before the report, hiding it behind the picker.
  2. No close at all: no questions and no skip line, so polish inherits nothing.
  3. Report authored into the persistence heredoc and never written to chat,
     leaving a perfect snapshot and a user who sees nothing.

Mode 3 predates this branch entirely. Persistence step 1 now says the temp file
is an archive copy, not delivery.

The Codex final-question gate is promoted out of its <codex> fence, where it was
stripped for three of four providers, and the skip branch is now a countable
threshold (fewer than 3 Priority Issues) rather than a judgment call.

Known floor, recorded in the suite README: gpt-5.6-luna passes 1 run in 6 and
deepseek-v4-flash is flaky. claude-sonnet-5 and gemini-3.5-flash are consistent.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 14:14:44 -04:00
Paul BakausandClaude Opus 5 0e5c6cbe17 Keep critique's report out of the question's message
The critique report and the AskUserQuestion call shipped in one assistant
message, so the report stayed hidden until the user answered the picker and
the command read as if it had never run.

Reorder critique's persistence steps so the temp-file cleanup runs after the
report and trend line are sent. That cleanup now ends the message carrying the
report, leaving the questions to open a fresh one. Both critique.md and
overdrive.md state the constraint and why it exists, so the ordering is not an
unexplained sequence a model can optimize away. Overdrive additionally moves
its direction descriptions inside the question options, where the user is
actually reading them.

Also fix the ask_instruction splices. The placeholder is a complete sentence,
but five call sites spliced it mid-sentence and shipped text like "stop and
STOP and call the AskUserQuestion tool to clarify. before expanding it". Every
call site is now sentence-initial and the twelve lowercase provider values are
capitalized to match, with a comment in utils.js pinning the contract.

Record a workflow-contract baseline for the current model lineup. The two
failures seen while validating this change are pre-existing: bolder refinement
fails on deepseek-v4-flash identically with bolder.md reverted to HEAD, and
redesign replaces DESIGN is flaky on assertions driven by new-work.md, which
this change does not touch.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 01:06:33 -04:00
Paul BakausandClaude Fable 5 76b9aaf021 Build-path toggle moves to the header; a code-to-comp flip confirms first
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>
2026-08-12 23:23:34 -04:00
Paul BakausandClaude Fable 5 64dd60a78a Build path becomes a setting plus a page toggle; the followup contract round retires
- 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>
2026-08-12 16:56:39 -04:00
Paul BakausandClaude Fable 5 ac4c3200db Surface rounds deal three structures, draw wireframes, and anchor comps on a reference screenshot
- 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>
2026-08-12 16:28:15 -04:00
Paul BakausandGitHub e36833ce21 Merge pull request #521 from digitallamb/pr/hermes-provider
Add Hermes Agent as a supported provider
2026-08-12 16:07:43 -04:00
Paul BakausandGitHub 6c837bd7d4 Merge pull request #562 from pbakaus/codex/issue-561-critique-signals
Fix critique routing snapshot metrics
2026-08-12 15:57:19 -04:00
Paul BakausandClaude Fable 5 f66eace20d Decision page: plain-language raises, IMPECCABLE'S PICK, sticky footer, short-viewport fit
- 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>
2026-08-12 15:26:34 -04:00
Paul Bakaus 3a26dcb809 Keep decision body order in fallback
Prepared and verified with AI assistance under maintainer authorization.
2026-08-12 15:25:04 -04:00
Paul Bakaus 37be3fa36b Fix stalled missing decision comps
Restated on current upstream main after the comp-field migration. Prepared and verified with AI assistance under maintainer authorization.
2026-08-12 15:09:27 -04:00
Paul Bakaus f1b7111503 Simplify Svelte accept orchestration
Unify Svelte component accept and discard around one operation dispatch, source lock, error path, and result emission while preserving their existing CLI contracts. Add direct CLI characterization coverage for both operations.\n\nAI-assisted implementation under pbakaus's scheduled-refactor authorization.
2026-08-12 14:15:14 -04:00
Paul BakausandClaude Fable 5 248a4a699a Retire the sketch era's wire name: the field is comp, sketch is an alias
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>
2026-08-11 15:53:25 -04:00
ac0416b655 Stop assuming white when a background cannot be read (#541)
* Stop assuming white when a background cannot be read

Dark themes came back from a scan buried in low-contrast findings that
all claimed the light text sat on #ffffff. Two live runs against
impeccable.style produced 102 and 95 of them.

Two causes, both fixed here.

Parsing. Browsers keep the authored color space in getComputedStyle
output: oklch() stayed oklch, but color-mix results come back as
color(srgb 1.04 0.72 -0.21), wide-gamut authors get color(display-p3
...), and lch()/lab() survive verbatim. The parser read none of those, so
those surfaces registered as unset. parseGradientColors was worse: it
matched only rgba() and #hex, so a ground painted as
linear-gradient(oklch(...), oklch(...)) counted as a gradient with no
stops at all.

Guessing. When the ancestor walk ran out of readable color it returned
white, and on a body-level gradient it returned white without even
looking. Light copy on a lacquer-black page then measured 1.3:1 against a
canvas the visitor never sees.

resolveBackgroundInfo now separates three outcomes: a resolved surface, a
gradient the caller should fall back to stops for, and an unreadable
layer. The last one makes both color adapters skip their contrast checks
entirely. White survives in exactly one case, the one that earns it:
every layer up to the document root was genuinely transparent.

Color conversions moved to cli/engine/shared/color.mjs and gained lab,
lch, and color() for srgb, srgb-linear, and display-p3. Spaces outside
that set return null, which now routes to abstention rather than to a
color nobody painted. Every conversion is pinned against what Chrome
itself paints for the same string.

Rescanning impeccable.style: 102 low-contrast findings down to 30, none
of them on an invented white ground.

Assisted-by: Claude Code

* fix: address PR review bot findings on background resolution

- Treat a url() image layer stacked above a gradient as an occluding,
  unreadable surface: resolveBackgroundInfo now returns unresolved so the
  gradient-stop fallback never measures stops the image hides
  (greptile-apps finding, reproduced in Chrome).
- Route the glow and AI-palette DOM adapters through resolveBackgroundInfo
  so an unresolved surface makes them abstain instead of hunting gradient
  ancestors past an unreadable layer (Cursor Bugbot finding).
- Resolve background-color keywords jsdom hands through verbatim:
  inherit now reads as no-paint (the ancestor walk IS its resolution) and
  currentcolor substitutes the element's own computed text color instead
  of forcing an abstention (Copilot finding).
- Regression coverage in the dark-theme fixture for all three, asserted in
  both the jsdom and real-Chrome suites; browser detector regenerated.

AI-assisted: prepared with Claude Code at the maintainer's direction.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: keep zero-offset glow findings when the surface is unreadable

The browser glow adapter abstained from the whole element when
resolveBackgroundInfo reported an unreadable surface, which also dropped
zero-offset chromatic halo findings that do not depend on the background
at all. It now skips only the gradient hunt past the unreadable layer and
scores the halo tell against a null surface, matching what the static
loop already did. Fixture cases pin both sides: the halo over a url()
image ancestor flags in both engines, and an offset chromatic shadow on
the same unknown surface stays abstained.

Also hardens the currentcolor background substitution with the
parseColorResolved fallback used by the text-color path, and adds fixture
coverage proving tokenized currentcolor surfaces already resolve through
the static cascade (flag when knowable, abstain when the token is
undefined).

Addresses Cursor Bugbot review findings on PR #541.

AI-assisted-by: Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: abstain on translucent gradients over images, drop phantom color-mix stops

Two follow-up review findings on the merge with main.

A gradient leading a url() layer was treated as a resolvable surface even
when its stops are translucent, so the glow and AI-palette hunts averaged
wash stops (a 20% black wash reads as pure black) while the real surface
blends with image pixels the engine cannot read. resolveBackgroundInfo now
marks gradient-over-image unresolved unless every readable stop of the
leading gradient is opaque, in which case the gradient provably covers the
image and remains the scorable surface.

parseGradientColorsModern predated this branch's parseGradientColors
rewrite: its second regex pass re-extracted color tokens nested inside
color-mix() stops that the shared parser already captures whole via
balanced-paren tokens, appending ingredient colors that are never painted.
The worst-case stop ratio then invented low-contrast findings against a
color nobody sees. The helper is removed; all callers use the shared
parser, which covers the modern syntaxes it existed for.

Fixture coverage pins both: the translucent-wash-over-image glow abstains
in both engines, an opaque gradient over an image still flags in the
browser, and the color-mix wash case stays clean in the static engine.
Each new assertion was verified to fail against the previous engine.

Addresses Greptile and Cursor Bugbot review findings on PR #541.

AI-assisted-by: Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-11 14:59:07 -04:00
dc4e4a4bd6 Denoise the design hook and let agents self-serve confident ignores (#508)
* Denoise the design hook and let agents self-serve confident ignores (#497)

The directive footer now emits in full once per session (a one-line
reminder after), the DESIGN.md staleness note is mentioned once per
session, rule descriptions dedupe within an emission, and the per-line
ignore suggestion shrinks to the bare rule/value pair. The footer and
hooks.md replace the confirmation-gated ignore policy with a three-way
triage: fix real problems, self-serve the narrowest ignore for confident
false positives or sanctioned exceptions and disclose it (with an honest
--reason), ask when unsure. Self-serve stops at ignore-value, and the
footer now gives a runnable hook-admin.mjs command instead of a slash
command agents cannot execute.

Measured on a seeded lab session replaying 11 hook events: 33,658 to
14,063 chars of agent-visible output (-58%).

AI-assisted (Cursor agent), directed and reviewed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Preserve the policy footer and honor maxChars under constrained budgets

Greptile's runtime check found two pre-existing clamp gaps that matter
more now that the full policy emits once per session: the last-resort
tail slice cut the footer off an over-budget emission, and the DESIGN.md
staleness note was appended after clamping, pushing past maxChars.

The clamp now gives the footer the budget first, clipping the finding
line and downgrading full to short policy when needed. The staleness
note defers, without consuming its session flag, to a later emission
with room.

AI-assisted (Cursor agent), directed and reviewed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Harden the constrained-budget clamp: keep findings, honest flags, guaranteed note

Review follow-ups from Bugbot and Greptile on the clamp fix:

- The clamp retries with the short policy before dropping finding lines
  that fit beside it, and a grouped result that kept only a file header
  no longer counts as a fit.
- The full-footer session flag commits only when the full policy
  actually survived the clamp, so a downgraded emission does not mark
  the session as having seen a policy it never received.
- Render paths reserve room for a pending DESIGN.md staleness note, so
  it is delivered inside the budget on the first emission instead of
  deferring behind full emissions indefinitely.

AI-assisted (Cursor agent), directed and reviewed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Route the Cursor deny cap through the clamp and match the whole footer

Bugbot follow-up: cursorBlockMessage tail-sliced at 4000 chars after
render, which the default 8000-char budget made reachable, and a cut
that spared the footer's opening words still committed the session
flag. The 4000 cap now feeds through the renderer's footer-preserving
clamp, and commitFooterShown matches the complete footer text instead
of a sentinel.

AI-assisted (Cursor agent), directed and reviewed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Reconcile hook.test.mjs expectations with main's per-platform quoting

Three tests fell out of sync when main's quoteCommandArg change (#533,
building on #476) met this branch's footer/hint rework. Test-only
changes; production logic untouched:

- The full-footer test now accepts either close quote after the
  hook-admin.mjs path, since quoteCommandArg single-quotes absolute
  paths on POSIX and double-quotes them on Windows. The short-footer
  guard rejects `node '` and `node "` alike.
- The #476 hostile-value test asserts the new bare
  `ignore-value <rule> '<value>'` hint format. The security property is
  unchanged: the value still passes through quoteCommandArg, so
  $(touch pwned) stays single-quoted and inert.
- The #533 test previously asserted a concrete quoted `--file` path in
  the footer; directiveFooter() now carries only literal placeholders,
  so that surface is gone. The per-platform assertion moves to the
  per-finding ignore hint, the remaining user-visible surface where
  scanned file content flows through quoteCommandArg.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix two Bugbot findings: Cursor prefix budget and footer-cutting tail slice

Both flagged by Cursor Bugbot on PR #508 after the main merge; both real.

1. cursorBlockMessage computed min(maxChars, 4000 - prefix), so a
   configured maxChars at or below the Cursor ceiling never charged the
   BLOCK_PREFIX against the budget: the final deny text could exceed
   maxChars by the prefix length, and appendDesignSystemNoteOnce's size
   check lost exactly the room designNoteReserve had held back. The
   prefix now comes off whichever limit binds. Default-config behavior
   is unchanged (min(8000, 4000) - 60 equals the old 4000 - 60).

2. The note reservation is subtracted after renderTemplate's 500-char
   floor, so the clamp can run below the budget clampLastLine assumed
   safe, and its last-resort path tail-sliced the rendered text, cutting
   the policy footer (the failure mode this PR exists to eliminate) when
   a deep file path met a pending DESIGN.md note. The reservation order
   stays (the staleness-note delivery guarantee at floor budgets depends
   on it); the last resort now drops the finding line and clips the head
   instead, so the footer survives every path. New regression test pins
   it: 6 findings, 100-char path, maxChars 500, reserveChars 134.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude <noreply@anthropic.com>

* Charge the Cursor deny prefix after the renderer's floor, not before

Greptile's runtime check caught the residual from ead0346d: subtracting
BLOCK_PREFIX from the maxChars passed to renderTemplate does nothing at
floor-tier configs, because the renderer re-raises any budget below its
500-char floor. At maxChars 500 with a stale design sidecar, the
prefixed denial landed at 432 chars and appendDesignSystemNoteOnce
could not fit the staleness note inside 500, deferring it (flag
unconsumed) for every equivalent denial in the session.

The prefix now rides in reserveChars, which comes off after the floor,
so it is charged at every config tier and the final prefixed message
plus a pending note closes exactly at the binding limit (499 chars in
the regression scenario). Default-config output is byte-identical:
max(500, min(8000, 4000)) - prefix equals the old min(8000, 4000) -
prefix. New end-to-end Cursor preToolUse test pins the path with a real
stale sidecar at maxChars 500.

AI-assisted (Cursor agent), directed and reviewed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Paul Bakaus <paul.bakaus@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-11 13:42:39 -04:00
Paul Bakaus 357f358050 Reject empty critique metrics
Treat empty and whitespace-only snapshot values as missing so malformed frontmatter cannot reintroduce plausible zeroes.

Prepared with AI assistance under maintainer pbakaus's standing automation authorization.
2026-08-11 13:00:31 -04:00
Paul Bakaus d4aacaccfd Fix critique routing signals
Read the documented critique snapshot keys while preserving legacy aliases, and surface missing metrics as null instead of zero.

Prepared with AI assistance under maintainer pbakaus's standing automation authorization.
2026-08-11 12:50:01 -04:00
digitallamb def69e157b fix(cli): use imported resolve/sep in hermesGlobalHome (#521)
The function called `path.resolve` and `path.sep` but only named-
imports `resolve` and `sep` from `node:path`. The ReferenceError was
swallowed by the try/catch, so $HERMES_HOME was silently ignored and
profile-scoped installs always landed in ~/.hermes instead of the
active profile. Greptile (P1) and Cursor Bugbot (High) flagged this
on 2026-08-10. Adds 6 regression tests covering default, default-
profile, active-profile, cross-home leakage, the override map
integration, and the full e2e pipeline. Verified by reverting the
fix and observing the relevant tests fail.
2026-08-10 22:47:56 -07:00
aee5ddd10c data-impeccable-ignore scoped waivers + occlusion and image-backed contrast FP fixes (#559)
* Add data-impeccable-ignore scoped waivers; fix occlusion and image-backed contrast FPs

Three changes that let a page hosting deliberate anti-pattern exhibits
scan clean without losing coverage, prepared with AI assistance (Claude
Code) on maintainer instruction:

- data-impeccable-ignore="rule-a rule-b" (or "*" / bare) on any element
  suppresses matching findings for its whole subtree, in the browser
  overlay, the extension, and the static engine. The DOM twin of the
  line-based impeccable-disable comments (which a live DOM cannot
  apply) and the generalization of data-impeccable-allow-kickers.
  Applied at the addBrowserFindings choke point, at the static element
  walk, and for regex findings that carry a live selector.

- text-occlusion: an occluder whose effective opacity multiplies out to
  ~0 paints nothing. An opacity-0 range scrubber stretched over a
  before/after comparison produced 16 "100% covered by an opaque
  element" findings on one page because elementFromPoint returns it and
  its UA background-color read as opaque paint. Invisible-at-rest
  elements are also no longer probed as victims.

- Analytic contrast now skips what it cannot measure: a url() image
  layer anywhere in the background stack ends the gradient-stops walk
  (dark ink on a bright gold-leaf image measured 2.6:1 against the wash
  composited over the wrong base), and elements that are invisible at
  rest (visibility hidden, effective opacity ~0 — hidden scene decks)
  are skipped by the color checks in both engines. The static cascade
  now tracks opacity to support this.

Covered by a new scoped-ignore fixture (exact rule, star, comma list,
nested depth, wrong-rule control) tested in both engines, a scrubber
pass case in the occlusion fixture, and image-backed / photo-panel /
hidden-scene pass cases in the gradient-ground fixture. Full suite
passes; browser and extension bundles regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* CSS-scan findings carry their enclosing selector; browser pass resolves them

Page-level CSS-text findings (marquee, dark-glow, radial-halo,
repeating-stripes, codex-grid, ai-color-palette, image-hover-transform,
pseudo/inset side-tab stripes) now attach the selector of the rule that
matched, via a best-effort enclosingCssSelector() helper or the
selector already in scope. The browser pass resolves that selector
against the live DOM: pseudo segments are stripped, a selector that
renders nowhere on the page drops the finding (the CSS ships there but
the pattern never paints — the live DOM is ground truth in a browser
scan), and matches under a data-impeccable-ignore ancestor are waived.
Static scans are unchanged: partial documents keep the text-level
findings. Applied with AI assistance (Claude Code).

Covered in the scoped-ignore fixture: a live marquee under a marquee
waiver is suppressed, and dead two-axis grid CSS matching no element is
dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Attribute selectors on gradient-text and bounce-easing page emitters too

Same mechanism as the previous commit, extended to the three page-level
motion/text emitters that were still selector-less. Applied with AI
assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* text-overflow: skip SVG content; scrollWidth lies there

Chrome reports arbitrary non-zero scrollWidth/clientWidth on SVG
elements (a <text> gave 78/48 while its rendered length sat inside its
box), so the box-metric delta is noise. SVG clips to its own viewport
anyway. Pass case added to the quality fixture. Applied with AI
assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Overlay samples image-backed text at the pixel level by default

Visual contrast gains a third mode. Explicit true still runs the full
sampled pass, explicit false still disables everything (the mode the
test suites use), and unset — the default overlay run — now samples
ONLY image-backed text: the one class the analytic walk deliberately
skips, because a url() layer's pixels are unknowable without looking.

The cost is bounded and the method is precise: at most a 3x3 grid of
sample points per candidate (degrading to 3 or 1 for small rects), the
source image drawn once to a canvas with only those pixels read, and
glyph ink never pollutes the samples because the image is drawn alone.
A cross-origin image without CORS headers reports unresolved rather
than guessing. Applied with AI assistance (Claude Code).

Covered by a new fixture: white text on a near-white same-origin
data-URI image background flags via sampled pixels under default
options; dark ink on the same image passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Review fixes: root opacity, keyframe steps, static parity, attributed fixtures

Applied with AI assistance (Claude Code), addressing all seven findings
from the automated reviews:

- effectiveOpacityDOM walks through body and html: a page-fade wrapper
  with body/html opacity 0 hides every descendant (Greptile executed a
  Chromium repro of the false positive).
- enclosingCssSelector refuses `from`/`to` keyframe steps, which read
  as never-matching type selectors and got valid findings wrongly
  dropped by the zero-match rule (Bugbot, high). Regression case: an
  overshoot bezier inside a `to` step must survive as page-level.
- The static cascade now inherits visibility, so descendants of a
  hidden container compute as hidden like the browser path; a declared
  visibility:visible still overrides.
- The static engine applies scoped waivers to selector-backed
  html-pattern findings, mirroring the browser — but keeps findings
  whose selector matches nothing, since static scans see partial
  documents.
- The scoped-ignore fixture grows to the mandated matrix: 4 flag cases
  (control, other-rule waiver, sibling waiver, misspelled rule id) and
  5 waived shapes (exact rule, nested depth, star, comma list, self),
  each with a unique border width so every finding attributes to
  exactly one case in both engines' tests.
- The image-backed contrast test pins its cases via the sampled
  finding's candidate text: the white-on-light specimen must flag and
  the dark-ink control must stay clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Review fixes: image-only starvation, selector rejection class, inset stripes

Second review round, applied with AI assistance (Claude Code):

- The image-only filter moves inside the candidate collector, before the
  cap: gradient/opacity/filter candidates earlier in DOM order no longer
  consume the 12-candidate budget and starve the url()-backed texts the
  mode exists to sample (Bugbot, high). The regression fixture packs 14
  gradient decoys ahead of the photo panels, and the test now drives the
  overlay entry (impeccableDetectAsync, default options) rather than
  detectUrl's Node-side full fallback, which is where the image-only
  mode actually lives.
- enclosingCssSelector no longer rejects the child combinator or quoted
  attribute selectors; only braces and angle brackets disqualify.
- The inset box-shadow side-tab scanner attaches its selector like the
  pseudo-element scanner does, so those findings waive and dead-drop
  the same way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:31:46 -07:00
5f7b001cbe Fix: measure gradient body grounds instead of assuming white (low-contrast false positives) (#557)
* Fix: measure gradient body grounds instead of assuming white (browser mode)

A page whose ground is set via background: linear-gradient(...) on body
leaves backgroundColor transparent, and resolveBackground assumed white
for any body/html-level gradient. In a real browser that assumption is
wrong: the shorthand is always decomposed there, so reaching that branch
means the ground truly is the gradient. On a dark oklch gradient ground
(impeccable.style's lacquer) this turned every light-on-dark text into a
~1.3:1 "on #ffffff" low-contrast finding, ~120 false positives on one
site. Browser mode now returns null so the caller measures against the
actual gradient stops; the white assumption stays for jsdom, where the
undecomposed-shorthand rationale still holds.

Gradient stops also now parse modern color syntax: computed
backgroundImage keeps oklch()/oklab()/hsl()/hwb() stops as authored, and
parseGradientColors only read rgb()/hex, so a token-driven gradient
ground was invisible even once the walk deferred to it. New
parseGradientColorsModern routes those stops through parseAnyColor.

Covered by a Puppeteer fixture (dark oklch body gradient): light text on
the ground must not flag, muted dark-gray ink must, proving the stops
are measured rather than the checks silently skipping.

Prepared with AI assistance (Claude Code), on maintainer instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Composite translucent layers over gradient stops; parse modern glow stops

Review fixes from PR #557's automated reviews, applied with AI
assistance (Claude Code):

- Cursor Bugbot found the new browser-mode early return discarded the
  translucent ancestors resolveBackground had collected: text on a
  frosted wash over a body gradient was measured against raw stops.
  resolveGradientStops now collects translucent layers during its own
  walk (through readCascadeBackgroundColor, extracted so both walks
  read surfaces identically) and composites every stop under them.
- Copilot flagged the other legacy parseGradientColors call sites. The
  glow-context fallback now uses parseGradientColorsModern, since body
  gradients reach it more often after this change. The AI-palette rule
  and the injected analytic sampler stay on the legacy parser
  deliberately: the former is a rule-behavior expansion deserving its
  own fixtures, the latter degrades to pixel sampling or a skip.
- Greptile asked for standard fixture structure: the fixture now has
  labeled flag/pass cases (3 flag, 5 pass) including the frosted-wash
  pair that locks the overlay compositing in both directions and a
  legacy hex-stop gradient guarding the original parser path.

The test scopes itself to the DOM path via visualContrast: false, the
suite's established pattern; the screenshot sampler is a separate
subsystem with its own coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Pin gradient-ground flag cases to their snippet signatures

Bugbot follow-up: a count-only assertion let an offsetting miss and
false positive cancel, especially the frosted pair. Each flag case now
asserts its full text-on-background signature, so the frosted case must
measure against the composited wash and the count guard excludes any
pass case flagging in its place. Applied with AI assistance (Claude
Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Comments: the static path is the custom engine now, not jsdom

jsdom left the dependency tree when the static-html engine (StaticElement
+ css-cascade.mjs) replaced it, and that engine does decompose the
background shorthand, so the comments this PR added were dated in both
name and rationale. Only comments touched by this PR are renamed; the
~40 legacy jsdom mentions elsewhere in checks.mjs are a separate sweep.
Applied with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Static engine: measure body gradients too, dropping the white assumption

Follow-up to the browser-mode fix: the white assumption for body/html
gradients was a jsdom guard, and jsdom is gone. The static cascade
decomposes the background shorthand (expandStaticDeclaration) and
preserves var() colors for later resolution, so a missing solid under a
body gradient is now as real in static mode as in a browser — and the
static engine had the identical false-positive class (light text on a
dark gradient ground flagged "on #ffffff") while missing the muted-ink
true positives on the same page.

The old catastrophic case cannot recur: opaque stops fully cover any
hidden solid (they are the ground), alpha stops composite over the
resolved base or the white canvas default, and unresolvable stops drop
rather than guess.

Static twin of the browser test added over the same fixture; the full
suite, the url()-ancestor guard, and a source scan of impeccable.style
(0 low-contrast findings) all stay clean. Applied with AI assistance
(Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 12:35:55 -07:00
Abdul WahabandCursor d23fa1c882 Fix: layout-transition false positives on JSX quoted transition values (#548)
The value-capture regex stopped only at ;{}, so in single-line JSX
style objects it ran past the closing quote and swallowed later
properties, flagging layout props that were never transitioned. The
capture now stops at the matching closing quote when the value is a
quoted string, falling back to the old bounds for real CSS.

Prepared with AI assistance under maintainer direction.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 14:00:55 +05:00
Abdul WahabandCursor 520a55547e Admit one brace level inside shadow interpolations
Review finding on #553: an object-literal argument like
${getOffset({ size: 2 })} ended the interpolation match at the inner
closing brace, losing the shadow context. Interpolations now admit one
level of braces (with paired quotes inside); the shared subpattern is
hoisted into compiled constants. Deeper nesting stays fail-safe by
design: a line-scoped regex cannot balance arbitrary braces, and the
miss produces a waivable finding, never a leak.

AI-assisted (Cursor agent), reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 13:28:11 +05:00
Abdul WahabandCursor 82234515e0 Admit paired quoted strings inside shadow interpolations
Review finding on #553: the interpolation subpattern excluded quotes,
so a documented shadow color after ${getShadow('lg')} or a quoted
ternary branch lost its context and fired as drift. Interpolations now
admit complete single/double-quoted strings; the quotes pair up inside
the ${...}, so an unpaired quote or the template's closing backtick
still ends the context and the allowance cannot leak to a later
property.

AI-assisted (Cursor agent), reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 13:18:10 +05:00
Abdul WahabandCursor 94e957d7fc Keep shadow context across template interpolations
Review finding on #553: the end-anchored shadow-context tails excluded
`}` (JS) and `{`/`}` (CSS), so a documented shadow color after a ${...}
interpolation in a boxShadow template literal or a CSS-in-JS
box-shadow line lost its allowance and fired as drift. Both tails now
admit complete ${...} interpolations; a bare `}`, quote, or `;` still
ends the context, so the allowance cannot leak past a template's
closing backtick into a later property.

AI-assisted (Cursor agent), reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 13:05:24 +05:00
Abdul WahabandCursor 92c857a9ef Allow documented sidecar shadow colors in shadow contexts (#547)
The detector never read the sidecar's extensions.shadows, and the only
workaround (a colors entry for black) allowlisted every black at every
alpha because colorKey() drops alpha. Shadow token colors now live in a
separate allowlist matched on alpha as well as r/g/b, and the allowance
applies only inside box-shadow / text-shadow values, so a documented
shadow black still fires as a page ground.

AI-assisted (Cursor agent), reviewed by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 12:39:17 +05:00
Paul BakausandGitHub 7fa695093e Bound copy-edit prompt context (#528)
* Bound copy-edit prompt context

Whitelist and truncate staged operation context before it reaches the local agent prompt.

AI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.

* Harden copy-edit prompt bounds

Bound repair, candidate, and element context consistently and preserve absent source positions as null.\n\nAI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.

* Preserve bounded repair context

Keep repair attempt metadata and nested diagnostics while retaining prompt limits.\n\nAI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.
2026-08-09 16:56:49 -07:00
Paul BakausandGitHub 63fb8a56f9 Fix Claude copy-edit prompt transport (#529)
Pass staged copy-edit prompts over stdin so large batches do not exceed platform argv limits.

AI assistance: Implemented and validated with OpenAI Codex under maintainer authorization.
2026-08-09 16:56:46 -07:00
045865918a Held for review: agent placeholder substitution, reviewer recapture contract, base-directory script form (#544)
* Resolve {{scripts_path}} in the agent bodies Codex ships

Three code paths emit an agent body: the degraded fallback reference, the
.toml nested inside the skill for Codex, and the native agent file. Only the
nested .toml skipped placeholder substitution and rule-marker stripping, so
the codex and .agents dists shipped `node {{scripts_path}}/embed-prompt.mjs`
verbatim in the asset producer, and every caller had to substitute the token
itself at load time.

All three now render through renderAgentBody(), and the new regression test
asserts a runnable embed-prompt command on each emitted surface plus a
synthetic agent proving markers and placeholders resolve in the nested .toml.

Prepared by an AI agent (Claude Code) under pbakaus's instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Give the finish reviewer's screenshots one fixed address

The Input Contract asked for "desktop and mobile screenshot paths captured by
the parent" and named none, so each session invented a filename and the
verdict pass went looking for a recapture that was never written there. Two
reviewer passes burned on that in the eval runs.

The parent now captures and recaptures to .impeccable/review/desktop.png and
.impeccable/review/mobile.png, and the reviewer reads those two first,
treating a brief-named path as the fallback for a parent that wrote elsewhere.

Prepared by an AI agent (Claude Code) under pbakaus's instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Lead Setup with the base directory the runtime reports

The rendered claude and codex skills opened with
`node .claude/skills/impeccable/scripts/context.mjs`, a project-relative path
that resolves in this repo and in nothing a user installs: a personal or
plugin install puts the scripts outside the project entirely. The working form
was already in the text, parenthesized, after the one that fails.

Setup now leads with `node <skill-base-dir>/scripts/context.mjs` and says once
that the base directory resolves every scripts-path command in the skill and
its references, leaving the project-relative path as the fallback for runtimes
that report no base directory.

Prepared by an AI agent (Claude Code) under pbakaus's instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Answer the Copilot review: brittle model assertion, missing review dir

Assert that {{model}} resolved rather than that it resolved to "GPT", which
belongs to PROVIDER_PLACEHOLDERS and can change without touching what the test
guards. And have the parent create .impeccable/review/ when the harness does
not, so a fresh project's first capture has somewhere to land.

Prepared by an AI agent (Claude Code) under pbakaus's instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Make the review-screenshot contract directory-based, not web-viewport-named

Two amendments to the recapture contract from review feedback:

1. The canonical location is the directory .impeccable/review/, one file
   per captured viewport; desktop.png and mobile.png are the web case,
   not the contract. Baking web-viewport names into the reviewer's spec
   would have hardened a web assumption into paths that a native
   (ios/android/adaptive) build cannot honestly write.

2. Precedence restored to explicit-beats-convention: paths the calling
   brief names are authoritative when the files exist; the canonical
   directory is where the reviewer looks when the brief names none or a
   named path is missing. This avoids stale canonical files from an
   earlier run silently winning over fresh explicit paths. The observed
   failure (the verdict round inventing a round-stamped filename) stays
   fixed: recapture happens over the same files, and invented filenames
   are still called out as pointing at nothing.

Assisted-by: Claude Code

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 19:26:43 -07:00
490dcfd678 Fix #476: stop using JSON.stringify/double quotes as shell quoting in four exec sites (#533)
* Fix: use argv exec and single-quote escaping for the four #476 shell-injection sites

JSON.stringify and raw double-quote interpolation were used as shell quoting,
but /bin/sh still expands $(...), backticks, and ${} inside double quotes.

- is-generated.mjs / live.mjs runScript: switch execSync string commands to
  execFileSync argv form, which never invokes a shell. Closes the remote path
  where a source file named `$(...)` executes during the live-mode walk.
- skills.mjs hook command + hook-lib.mjs ignore-value suggestion: values that
  must stay shell strings now use POSIX single-quote escaping instead of
  JSON/double quotes. The doctor's hook-token parser learns the single-quoted
  absolute form so it keeps verifying user-level installs.

Adds regression tests for the single-quoted absolute hook form and the
single-quoted ignore-value suggestion. Verified end to end in a browser through
a real live-mode wrap walk against a hostile-named source file.

Prepared with AI assistance (Cursor) under maintainer instruction.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Test: lock in POSIX single-quoting for a $(...) absolute install path (#476)

Follow-up from security review: prove an install path embedding $(...) is
single-quoted in the written hook manifest, not double-quoted.

Prepared with AI assistance (Cursor) under maintainer instruction.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix: quote ignore-command args per platform so Windows cmd.exe keeps spaces (#533)

Greptile flagged that switching quoteCommandArg to POSIX single quotes fixed
$(...) injection on /bin/sh but regressed Windows cmd.exe, where single quotes
are literal, so a --file path containing spaces was split and the ignore scope
was stored malformed.

The suggested command runs on the same machine the hook fired on, so branch on
process.platform (the pattern skills.mjs already uses): single-quote on POSIX
for the #476 fix, and keep the original double-quote escaping on Windows so
that path's behavior is unchanged. Adds a regression test asserting both forms.

Prepared with AI assistance (Cursor) under maintainer instruction.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Test: prove the POSIX hook guard is inert under /bin/sh and Windows keeps double quotes (#533)

Greptile's probe could not reach the generated manifest, leaving the hook
command contract unverified. Convert that into committed proof:

- POSIX: install with a $(touch pwned) absolute path, then actually execute the
  generated guard under /bin/sh from a clean cwd and assert no marker file
  appears and the guard exits 0 (single-quoted substitution stays inert).
- Windows: drive copyProviderHooks as win32 in-process and assert the command
  keeps the double-quoted absolute path (usable when the install path has
  spaces; $(...) is inert on cmd.exe anyway).

Test-only; source quoting is unchanged.

Prepared with AI assistance (Cursor) under maintainer instruction.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 18:43:17 -07:00
ddf4526fb5 Fix Windows libuv abort in concept-seed after a successful roll (#526)
* Fix Windows libuv abort in concept-seed after a successful roll

process.exit() with a live fetch keep-alive socket trips libuv's
UV_HANDLE_CLOSING assertion on Windows (nodejs/node#56645), aborting
the CLI with 0xC0000409 after complete output on the successful-roll
path. Destroy the global undici dispatcher before the explicit exit
so no socket is left to race; the hard exit stays, keeping the
no-linger guarantee on blackholed networks.

Fixes #504

Prepared with AI assistance (Cursor agent) under maintainer direction.

* Add regression test for the successful-API dispatcher teardown

The suite covered local rolls and the unreachable-API fallback but
never a successful roll, the one path where a pooled keep-alive
socket exists at exit (issue #504). Serve a real /api/roll from a
local server and assert the CLI destroys fetch's global dispatcher
before its explicit exit. Verified to fail without the fix.

Prepared with AI assistance (Cursor agent) under maintainer direction.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 18:41:19 -07:00
628aac5a40 Fix: point install's next step at the agent chat, not the terminal (#472) (#532)
The install completion message said to run /impeccable init "in your AI
harness", and users pasted it into their shell instead (bash: /impeccable:
No such file or directory). Say the command is typed in the AI coding
agent's chat, and give `npx impeccable init` a pointed redirect instead of
the generic unknown-command error. A real path named `init` still routes
to detect as before.

Prepared with AI assistance (Cursor agent), directed by @abdulwahabone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 18:40:26 -07:00
477484aaee Fix: install missing explicitly selected providers without --force (#536)
* Fix: install missing explicitly selected providers without --force (#500)

An explicit --providers list now treats "already installed" per selected
target: providers with an existing install take the update path, providers
with none get a fresh install (skills + hooks) in the same run. Previously
any existing install (e.g. .claude) made `install --providers=grok` exit 0
without writing .grok, leaving Grok Build on the Claude-variant fallback.

Written with AI assistance (Cursor agent), reviewed and tested by maintainer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix: copy provider agents for freshly installed mixed-install targets

Bugbot caught that the mixed explicit-providers path installed skills and
hooks for missing targets but skipped copyProviderAgents, which both the
update and fresh-install paths run. Written with AI assistance (Cursor agent).

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 18:33:23 -07:00
Paul BakausandGitHub fc05472a20 Restore reduced-motion animation guidance (#540)
* Restore reduced-motion build guidance

Restores the accessibility requirement and verification step to the animation playbook, with a regression test that keeps it on the build path.

Implemented and validated with OpenAI Codex assistance under standing maintainer authorization.

* Harden reduced-motion guidance regression

Normalizes CRLF input and accepts either reduced-motion spelling so the contract stays portable and intent-focused.

Implemented and validated with OpenAI Codex assistance under standing maintainer authorization.

* Anchor skill reference test to its module

Resolve the repository fixture path from the test module so the regression test is independent of the caller's working directory.

This change was prepared with AI assistance under maintainer authorization.

* Clarify reduced-motion guidance

Replace the double negative in the canonical animation guidance and keep the source contract aligned with the clearer wording.

This change was prepared with AI assistance under maintainer authorization.
2026-08-08 15:49:43 -07:00
dbff0880e6 Decision page: full-fidelity comps, raise cycler, declined sizing, canon order, full card anatomy (#545)
* 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>
2026-08-08 15:47:11 -07:00
c70bcbf6b4 Direction round: verdict-routed hand, MY PICK card, salience parity, Safer/Bolder registers (#531)
* 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>
2026-08-08 14:17:21 -07:00
Paul BakausandClaude Opus 5 aee6ce9352 Give the Live UI surface inventory one definition again
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>
2026-08-06 14:53:20 -07:00
Paul BakausandGitHub a075d89bdb Simplify CSS color channel parsing (#520)
Centralize CSS numeric token parsing and characterize every supported color unit while preserving config and filtering behavior.

AI-assisted: Codex implemented this refactor under pbakaus’s scheduled architecture-simplification authorization.
2026-08-05 15:28:17 -07:00
Paul BakausandGitHub e46e0da885 Centralize critique snapshot reading (#511)
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.
2026-08-05 15:27:02 -07:00
Paul BakausandGitHub 6886ab8c0e Fix Codex pinned skill frontmatter (#519)
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.
2026-08-05 15:25:42 -07:00