mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 14:16:28 +03:00
caef4b8e4ce46a2c5034d99f1281e8291be4e2a8
272
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
caef4b8e4c |
Install global OpenCode skills into the config dir OpenCode actually reads
npx impeccable install --providers=opencode --scope=global wrote to ~/.opencode/skills, but OpenCode discovers global skills from its config directory: $OPENCODE_CONFIG_DIR/skills, else $XDG_CONFIG_HOME/opencode/ skills, else ~/.config/opencode/skills. The install succeeded and `opencode debug skill` never listed it (issue #406, diagnosed by @dergachoff). HOME_SKILLS_DIR_OVERRIDES entries become functions of the home dir (the Pi override from #327 was the only entry and is unchanged in behavior), with OpenCode resolving through the env chain above. Detection gains a resolver-based GLOBAL_HARNESS_HINTS entry so a machine with only ~/.config/opencode (no legacy ~/.opencode) still routes global installs to OpenCode. After a global install, the skills just written are removed from the stranded ~/.opencode/skills location; sibling skills and the rest of ~/.opencode stay untouched, and the empty skills dir is pruned. Four new CLI tests (failing-first): default config-dir install, OPENCODE_CONFIG_DIR and XDG_CONFIG_HOME precedence, legacy-copy migration with sibling preservation, and config-dir-only detection. Prepared with AI assistance (Claude Code), directed by @pbakaus. Co-Authored-By: Claude Code <noreply@anthropic.com> |
||
|
|
73819ff573 |
Stop hook-build test from asserting an unbuilt dist artifact
The "Codex project hooks reference hook.mjs in the .codex skill payload"
test asserted dist/codex/.codex/skills/impeccable/{SKILL.md,hook.mjs}
exist. dist/ is gitignored, and CI's test:core step runs before the
Build step, so the fresh checkout has no dist/ when the assertion runs.
It only passed locally against a stale dist/. This turned every
sync-generated-output push on main red.
The dist/codex bundle's self-consistency is already covered by
build.test.js, which runs an actual build into a temp dir and verifies
the codex payload lands at .codex/skills/. Drop the two dist assertions;
the test keeps verifying the tracked outputs (the .codex/hooks.json path
and the .agents/skills payload) that exist at test:core time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6ff9f957ac |
Add radial-spotlight-glow detector rule
Flags the decorative low-opacity chromatic radial-gradient "spotlight" washed behind a hero or section and fading to transparent, an AI-slop reflex the saturated radial-halo gate lets slip (e.g. rgba(80,111,255, 0.26) -> transparent on a mobile hero). Gates: a non-repeating radial-gradient whose last stop is transparent, whose visible stops are all low-opacity (alpha < 0.45) with at most two of them, at least one chromatic (channel spread >= 24 exempts neutral vignettes), on a decorative-scale surface (width >= 240, height >= 160, exempting badges/avatars/small lights). The alpha band is disjoint from radial-halo (>= 0.7), so the two never double-report. Wired into both element loops (static-html + injected browser) with the pure checkRadialSpotlight shared by both adapters. TDD fixture with 5 flag / 9 pass shapes. Browser-path sweep over the eval corpus: 29 hits on 11 pages, 0 false positives. Count 59 -> 60. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bcf354cd0c |
Fix Codex hook path so .codex-directory installs run the detector
The committed .codex/hooks.json hardcoded .agents/skills/impeccable/scripts/
hook.mjs. On a .codex-directory install the skill payload lives at .codex/
skills/..., so the guarded command ([ ! -f X ] || node X) found no file and
silently no-opped, leaving the design detector dead for those users.
Derive the hook payload path from the emitting provider's own configDir rather
than hardcoding .agents:
- buildCodexHooksManifest(skillDir) now builds `${skillDir}/skills/impeccable/
scripts/hook.mjs`; hooksJsonFor threads each provider's configDir through. The
Codex provider (configDir .codex) emits .codex/skills; the root sync and the
self-consistent dist/codex bundle both point at their own payload.
- CLI installer: project-scope hook rewriting now derives the provider's own
project-relative path instead of preserving the bundle token. The Codex bundle
ships a .codex/skills command, but the CLI lays the skill at .agents/skills, so
the installed .codex/hooks.json is rewritten to .agents/skills (Claude keeps
its ${CLAUDE_PROJECT_DIR} token; global installs keep the absolute rewrite).
Per-provider hook payload path after the fix:
Emission hook path
dist/codex/.codex/hooks.json .codex/skills/impeccable/scripts/hook.mjs
root .codex/hooks.json (build sync) .codex/skills/impeccable/scripts/hook.mjs
CLI .agents (codex) project install .agents/skills/impeccable/scripts/hook.mjs
CLI .agents (codex) global install <home>/.agents/skills/.../hook.mjs (abs)
.claude / .cursor unchanged
Tests: extended hook-build (codex-dir -> .codex/skills, agents-dir -> .agents/
skills) and skills-cli (bundle ships .codex/skills, install rewrites to .agents/
skills). Regenerated tracked .codex/hooks.json via build:release.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
507725c935 |
Harden detector against form.id shadowing and gradient/non-rendered false positives
Fixes three detector bugs that surfaced on real-world (Shopify) URL scans: #407 — DOM named-property shadowing crash. On a <form> with a named control like <input name="id"> (every Shopify product form), HTMLFormElement's [LegacyOverrideBuiltIns] behavior makes `form.id` return the input element, not the id string, so `elId.startsWith(...)` throws and aborts the whole scan. Read the id via getAttribute whenever `el.id` is not a string, at all three sites: checkQuality (checks.mjs) and collectBrowserFindings + generateSelector (browser/injected/index.mjs). Regenerated the browser bundle. #408 — tiny-text / undersized-ui-text flagged non-rendered elements. On sites that set html{font-size:62.5%} the root computes to 10px, so <script>/<style>/ <title>/<noscript> and display:none / visibility:hidden blocks — whose JS/CSS/ JSON-LD text clears the hasDirectText gate — produced dozens of phantom "10px body text" findings. Added isNonRenderedText() (tag list + head descendants + display/visibility) and gated both text-size floors on it. #409 — contrast rules misjudged gradients. Case A: background-clip:text paints its glyphs with the element's own gradient, not a backdrop, so measuring the never-painted `color` against those stops is a guaranteed false positive; skip the backdrop-contrast checks when bgClip is 'text' (the gradient-text pattern flag still fires). Case B: a translucent gradient stop (e.g. a 9%-alpha accent glow) was treated as an opaque accent; composite alpha stops over the resolved surface beneath the gradient in resolveGradientStops(), dropping the stop rather than guessing when that surface is unresolvable. Fixtures + tests: shadowed-form-id.html (browser, #407), nonrendered-text.html (#408), and gradient-clipped + alpha-glow cases added to color.html (#409). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2fa0e7d327 |
Live: gate mid-generation source injection, monotonic bar, resumable disconnect
Three browser-side fixes for the same 3.5-to-4.0.1 regression. - Source-preview targets no longer source-inject per variant_progress checkpoint. Immediate injection raced framework (React/Vue) ownership and triggered removeChild errors, which surfaced as static previews. HMR now owns reconciliation while variants stream in; source injection runs only on the final done (its 750ms settle + retry ladder stays for non-HMR harnesses like Cursor). Progress counts still advance from the variant observer, and the svelte-component progressive path is unchanged. - The agent-phase progress bar advances monotonically. A behind/resumed checkpoint re-broadcasts an earlier phase (the server regresses the snapshot phase to generating), which moved the visible bar backward; a phase rank table now blocks a known-lower phase from overwriting a known-higher one. - The server-lost toast now frames the drop as resumable (session saved, reopen or restart live-poll.mjs) instead of "Session ended", which had led agents to rationalize bailing to direct edits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dbe0c12b91 |
Live: stop the preflight writing source, cache the resolution
The polling-rework preflight wrote the variant scaffold into source during the poll lease, before the agent acted. On source-preview targets (React/Vue/Vite, everything but the svelte-component path) that write full-reloaded the framework; a browser caught mid-reload missed the agent's variant write and the SSE done, and sat stranded at 0/N. Restore the 3.5 single-atomic-edit semantics: the preflight still resolves the element location and computes the scaffold, but --defer-source-write leaves source untouched and hands the agent the wrapper text plus the picked source range. The agent splices variants into the wrapper and replaces the range in one write, so the framework reloads exactly once. The svelte-component path is untouched (it never writes route source). The missed-completion recovery stays as defense in depth. Also cache the resolved source file per target signature (locator + route): the ~7.6s tree search re-ran on every generate for the same element; a hit now points the helper straight at the file via --file, invalidated when the target changes or a resolution fails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4cd5ea7547 |
Add TanStack Router + Start support to live mode
Live mode had no TanStack coverage: a TanStack Start user hit disconnects and static previews because there is no static index.html to inject and no adapter for the SSR root document. - New tanstack-adapter.mjs, modeled on the SvelteKit/Nuxt adapters: detects a TanStack Start project (@tanstack/react-start + src/routes/__root.tsx) and patches the __root document to mount a generated dev-only React component (src/impeccable/ImpeccableLiveRoot) that appends the live bundle on the client after hydration, carrying the ?token= param via buildLiveScriptSrc. Patch/unpatch round-trips byte-for-byte and is idempotent; refuses to clobber an unmanaged file at the component path. - Wire detection into live-inject.mjs (insert + remove + gitignore), ordered so SvelteKit/Nuxt win and a plain TanStack Router SPA falls through to the baseline Vite index.html path. - tanstack-router-vite fixture (baseline, no adapter) and tanstack-start fixture (SSR adapter), both with runtime blocks. Both pass the full live-e2e cycle (handshake, steer, pick, Go, cycle, accept, carbonize, reloadProbe). - Unit tests for detection + patch round-trip + apply/remove; tanstack-start branches in framework-fixtures.test.mjs; live.md framework table + adapter note. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3f9fccdfd0 |
Live: lock down the local server against same-machine token theft (#304)
Two defense-in-depth layers close the P1 in issue #304, where any browser tab on the machine could fetch /live.js, extract the embedded token, and drive every token-gated route. 1. Loopback-restricted CORS. The shared handler replaced its wildcard `Access-Control-Allow-Origin: *` with reflection gated on a strict isLoopbackOrigin() that URL-parses the Origin (so localhost.evil.com and 127.0.0.1.evil.com fail) and accepts only http/https on localhost, 127.0.0.1, or [::1]. Reflection always pairs with `Vary: Origin` so a cache never hands one origin's authorized response to another. Remote origins get no ACAO header; origin-less callers (script tags, curl, the agent's own fetches) are unaffected. 2. Token-gated /live.js. The handler now 401s unless `?token=` matches state.token, so the bundle (which embeds the token) is no longer served to unauthenticated local pages. The injected <script src> carries the token: live.mjs passes --token to live-inject.mjs, which threads it through every injection path (HTML/JSX tag, Nuxt plugin, SvelteKit root component) via a shared buildLiveScriptSrc(). The token stays optional in live-inject so static fixture tests keep their bare src. Tests: new live-server integration cases for the 401 gate, remote-origin denial, loopback reflection + Vary, and token-guarded routes under a loopback Origin; e2e session harness now injects with the token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
da2982ab95 |
Fix /source guard escaping the project root via sibling directories
The /source route confined paths with `absPath.startsWith(process.cwd())`, a string-prefix check with no separator. An absolute request path to a sibling directory whose name extends the project dir name (projeto -> projeto-backup) shared the prefix and was served. Switch to the relative-path check already used by sessionFileMetadataFromPollReply: reject when the relative path is empty (the root dir itself, never a file this route serves), starts with `..`, or is absolute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
55094aaa0d |
Fix false hook-script-missing in doctor when ${CLAUDE_PROJECT_DIR} is unexpanded
The deep staleness pass extracted a hook-script path with a greedy `\S*`
prefix that swallowed the `${CLAUDE_PROJECT_DIR}/` placeholder, then
existsSync'd the literal string. That string never exists, so every project
installed by `impeccable hooks on` got a `hook-script-missing` finding with
text claiming UI edits were going unscanned — the opposite of the truth.
Split extraction from resolution. hookScriptTokenFrom now pulls the path
token (quoted-first, so it handles the #399 guarded `[ ! -f "PATH" ] || node
"PATH"` form and absolute user-level installs) without absorbing shell
syntax. resolveHookScriptPath then applies a per-placeholder policy:
- ${CLAUDE_PROJECT_DIR} expands to the scanned root (the runtime mapping).
- ${CLAUDE_PLUGIN_ROOT} / ${PLUGIN_ROOT} / ${GROK_PLUGIN_ROOT}, $(...) command
substitution (GitHub's $(git rev-parse)), and any other $VAR are SKIPPED:
the doctor cannot know those locations and must never assert a negative it
cannot verify.
The check stays real: a placeholder that expands to a genuinely absent path
still flags. Adds TDD coverage for every command form.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
47aff2e0be |
Fix Stop-hook loop: honor stop_hook_active per Claude Code contract
The Stop deep pass (runStopHook) never read the stop_hook_active field from the Claude Code Stop-hook event. When a prior fire kept the turn alive via hookSpecificOutput.additionalContext and the agent legitimately declined to act, the hook re-scanned and re-blocked every re-invocation until Claude Code's consecutive-block cap force-ended the turn (issue #400). Read stop_hook_active early in runStopHook, right after the event is parsed and before any scan, and exit 0 with no output when it is true. The prior fire already surfaced the findings; acting on them is the agent's call. Only Claude Code sends this field, so the strict === true is a no-op for other harnesses. runHook (PostToolUse) and hook-before-edit.mjs (PreToolUse) never receive the field, so they are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
eda81f0937 |
Release prep: skill v4.0.1
Bump plugin + marketplace to 4.0.1 and sync the regenerated provider output: the guarded hook commands from issue #399 (a missing hook file exits 0 instead of crashing every turn of a user-level install), the canon standing exit, the visualize flow, the two shipped subagents, and the interactive-spine fixes from today's live testing. Detector count validates at 59 with undersized-ui-text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
13c078ae93 |
Fix user-level hook path crash and clarify skills update scope (#399)
Part 1 — user-level hooks got a project-relative command. copyProviderHooks
only rewrote the bundled ${CLAUDE_PROJECT_DIR}-relative hook command to an
absolute skill path when the skill lived elsewhere than the manifest root. A
user-level update (root === ~) kept ${CLAUDE_PROJECT_DIR}, which a global
~/.claude/settings.local.json resolves per-project — crashing node at module
resolution on every PostToolUse/Stop in any project without a local skill copy.
Now the command is rewritten to the resolved absolute path whenever the manifest
is a user/global file (isHomeDir(root)) as well as the pre-existing
skill-elsewhere case, and every hook command is wrapped with a missing-file
guard `[ ! -f "PATH" ] || node "PATH"`. The guard exits 0 when the script is
absent (upholding hook.mjs's "never break a turn" contract even before node can
load it) while preserving node's own exit code when present, so Claude's exit-2
blocking signal still reaches the agent. Project-scope hooks keep the portable
${CLAUDE_PROJECT_DIR} token.
Part 2 — skills update silently targeted CWD. update now resolves and names the
target explicitly (project vs user level, with the absolute path), honors
--user/--project, only counts a provider as installed when the impeccable skill
itself is present (so it never vendors a copy into a repo that merely tracks
other first-party skills), and offers the choice when both a project and a
user-level install exist instead of silently picking. Non-interactive runs
default to the project and print how to target the other.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
6ece0e588f |
Add deterministic new-work interactive smoke suite
A cheap, LLM-free E2E tier for the interactive parts of new-work, mirroring the two-layer live-e2e pattern (deterministic now, opt-in LLM tier later). - generate-image.mjs: IMPECCABLE_IMAGE_GEN_FAKE=1 writes a deterministic offline image (SVG with wrapped prompt + SYNTHETIC COMP label, or a valid palette-stripe PNG carrying the prompt/marker in a tEXt chunk). Same CLI contract, no key, no network, $0.00 cost line. - tests/new-work-e2e/user-bot.mjs: scripted user bot (module + CLI) that resolves the serve-question daemon from the workspace and drives the real page via Playwright (pick, re-roll + steer, canon, tab close). - tests/new-work-e2e.test.mjs: node --test coverage of the serve-question cycles (pick + CHOSEN CARD, re-roll + --update re-deal, canon + CANON CHOSEN, tab-close exit-4, text-only card) plus fake image determinism. - Registered as the opt-in new-work-e2e suite; added test:new-work-e2e. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
daec380cdb |
Add undersized-ui-text rule for functional text below an 11px floor
The existing `tiny-text` rule owns long body copy and deliberately exempts the UI furniture layer (nav, footer, links, buttons, labels, uppercase micro-labels). That left a real gap: a build shipped its entire furniture layer (nav links, category names, timecodes, meta rows) at 8px because the chosen pixel font only steps in 8px increments, and the design hook waved it through as merely "not on the DESIGN.md ramp" -- which the model resolved by adding 8px to the ramp. Being on the ramp launders the token, not the legibility problem. New `undersized-ui-text` quality rule closes that laundering path: - Flags interactive and short content-bearing text (links, buttons, nav items, labels, table cells, meta rows, timecodes) below an 11px floor. The floor holds inside a footer; only non-interactive legal smallprint gets the softer 10px floor. - Ignores the design system entirely, so a value ON the ramp is still flagged. - Uppercase letterspaced micro-labels stay in scope (still functional). - Exempts sup/sub, visually-hidden (sr-only) text, and code/terminal contexts. em/rem/%-sized text that computes at or above the floor never fires. - Complements tiny-text without double-flagging: long non-furniture body copy stays with tiny-text. Implemented as a single check in checkQuality (rules/checks.mjs), so both the static-html (jsdom) and browser adapters pick it up through the unified per-element path -- no dual wiring. Registered in registry/antipatterns.mjs. TDD: fixture tests/fixtures/antipatterns/undersized-ui-text.html (7 flag / 7 pass shapes), failing test first, then implement. Full fixtures suite 64/64. Deferred (blocked by an active release-gate eval reading build/_data/dist): regenerate the browser bundle (bun run build:browser -> cli/engine/detect-antipatterns-browser.js) and the extension detector (bun run build:extension -> extension/detector/detect.js + antipatterns.json) so the standalone browser/extension artifacts carry the new rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
270f4d20aa |
Make em-dash-overuse an advisory rule with browser parity
Em-dashes are used legitimately by humans, so em-dash-overuse fired far too often. Reclassify it as the first advisory-tier rule: detected, but never a failure. Engine - Add `advisory: true` to the rule metadata schema (em-dash-overuse is the first). findings.mjs stamps `advisory: true` on advisory findings so every consumer can partition without a registry lookup. Rule count stays 58. - Raise the firing threshold from a flat 5 dashes to two gates: an absolute floor of 8 and a density of about one dash per 500 characters of body text. A long article that uses a few em-dashes no longer trips; a short, dash-per-clause page still does. Entity decoding (mdash, numeric, hex) is unchanged. Thresholds live in shared/constants.mjs so every engine agrees. Browser parity - The browser bundle carried a registry entry but no logic, so the overlay and extension could never flag it. Add checkEmDashOveruse / checkEmDashOveruseDOM in rules/checks.mjs (reads rendered text, no entity decoding needed), wire it into the injected page-level pass, and carry the advisory flag through serializeFindings so the overlay/extension can render it with the mildest affordance. CLI - Advisory findings print under a separate dimmed "Advisory" section, are excluded from the failure count, and never change the exit code (an advisory-only scan exits 0). JSON keeps them with `"advisory": true`. `--no-advisory` suppresses them entirely. Hook - Advisory rules are skipped by default in both the per-edit and Stop deep-pass hooks, so the hook never nags about them. Opt in with `.impeccable/config.json` -> `detector.advisoryRules: "include"`. Tests - Fixture + threshold + browser-adapter coverage; advisory-skip default and opt-in for the hook; formatFindings partitioning. The em-dash-overuse stand for a deferred copy rule in the tier tests is swapped to marketing-buzzword. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
70fdc172b8 |
Resolve detect DESIGN.md from each target's project, not cwd
The detect CLI loaded DESIGN.md once from process.cwd() and applied it to every scan target. Scanning another project's files from inside a different repo therefore judged them against the wrong project's design system (cross-project contamination observed during eval work: running detect from impeccable-evals against a generated artifact elsewhere applied the evals repo's DESIGN.md). DESIGN.md now resolves by walking up from each scan target's own location to its design root: a directory carrying a DESIGN.md is the root; a directory carrying a project marker (.git / package.json / .impeccable) without a DESIGN.md is a boundary that stops the walk with no design system, so a sibling project never inherits a parent's or cwd's rules. A target with no design root above it falls back to no design system rather than cwd's. Resolution is memoized per root, so a multi-file scan reads each DESIGN.md once, and targets spanning projects each get their own. file:// URLs resolve from their path; remote http(s) URLs get no design system. Adds tests/detect-cli-design-contamination.test.mjs, which spawns the real CLI to prove B's file is not judged by A's DESIGN.md, that a project still governs its own file, that a mixed-project scan resolves per target, and that a marker-less bare file gets no design system. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9f5bbed8b8 |
Bump astro test fixture to ^7.1.0 to clear dependabot XSS alerts
The astro-vite7 live-e2e fixture pinned astro ^6.0.0, which resolves into the vulnerable range of three dependabot advisories: GHSA-4g3v-8h47-v7g6 (reflected XSS via View Transition animation properties, medium), GHSA-f48w-9m4c-m7f5 (XSS via spread attribute names in renderHTMLElement, medium), and GHSA-7pw4-f3q4-r2p2 (XSS via transition:* directive values, low). All three are patched by 7.1.0. Dev-only test fixture; the vulnerable code paths (View Transitions, transition directives, spread attributes) are not exercised by this static, non-hydrated page, so real exposure is nil. Bumped anyway as the cheap, correct fix. Also corrected the now-stale fixture label to "Astro 7 + Vite 7". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7dcca2bb36 |
Count em-dash HTML entities in em-dash-overuse
The em-dash-overuse text analyzer ran stripHtmlToText over raw markup, which drops tags but leaves character entities intact. A model that wrote —, —, or — rendered a real em-dash the counter never saw, so 12 entity-escaped dashes on a live page slipped through. Decode the em-dash entities (named, zero-padded decimal, upper/lower hex) to the literal glyph before counting. En-dash entities stay untouched: the rule counts em-dashes, and the literal en-dash was never counted either. The gap lived only in the regex / static-HTML path (detectText and detect-html's runTextContentAnalyzers, both over raw HTML). The browser adapter never ran this analyzer, so build:browser and build:extension produce no diff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f6cecf6149 |
Gate the concept roll on PRODUCT.md existing
Paul reproduced the Opus smoke failure in a fresh repo: given a natural-language build intent, the model runs concept-seed directly and skips the init divert entirely, so PRODUCT.md never exists and nothing grounds the challenger fusion. Prose already says init-first in both SKILL.md routing and new-work.md; prose alone does not hold the floor. The deal path now refuses with a NO_PRODUCT_MD directive routing to reference/init.md when loadContext finds no PRODUCT.md. The --chosen telemetry ping stays ungated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ea68bb4722 |
Scope headless detection to the path that opens a browser
The new self-detection ran before mode dispatch, so it also caught --wait, --stop, and --schema. CI failed on the start/wait cycle test: --wait returned 2 (no browser) where the documented poll loop expects 3 (WAITING). Under CI=1 the suite went 4 pass / 2 fail; it is 6 / 0 now. Two of those modes were user-facing bugs, not just test breakage. --stop exited 2 without killing the daemon it was asked to kill, leaking a server process (verified: one daemon running, CI=1 --stop, still one). --schema only prints a payload example, and new-work.md tells the agent to read it before building a payload. Detection can only tell whether this process can auto-open a browser, not whether the user has one: SSH with a forwarded port and a harness with an in-app browser both have a browser and no DISPLAY. The file already treats serve-without-opening as first class, since --start spawns its own daemon with --no-open. So the check now gates acquiring a session, not managing or ending one. The blocking serve path still exits 2 on a headless box, with a test pinning that. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
5575a027dc |
Flag and repair drift in Impeccable's own project artifacts
v4 changed PRODUCT.md's shape and retired the register axis, so an upgraded project can carry answers nothing reads. Nothing measured that. Two tiers, and the split is a performance contract: - Boot (context.mjs, emitting CONTEXT_STALE) spends only what a boot already spends: markdown already in memory, a bounded set of stats, the small JSON files the boot reads anyway. No new directory walks. One directive for the whole set, throttled to once a week per project so a finding the user declined does not reappear tomorrow. - doctor.mjs runs the deep pass on demand: git drift, ignore lists validated against the live rule registry, hook script paths that stop resolving, and the monorepo workspace sweep. --fix applies only the migrations that carry no decision. Findings are data, not prose, so the boot directive, the text report and --json all render one set. Severity says what should happen: auto (fix on the next write anyway), mention (state once), route (name the command that owns the repair). PRODUCT.md now carries a schema stamp so the checks stop reconstructing a file's vintage from which sections it happens to have. Schema version, not release version: a record written by 4.0.0 is not stale under 4.0.1. DESIGN.md gets no stamp, because it follows the external design.md spec that Stitch lints and every DESIGN.md signal is measurable without one. The highest-value catch is a project that resolves to web while carrying native build files, including a monorepo app inheriting a root record that says web. That one costs output quality silently; nothing failed before. doctor follows the hooks/pin pattern rather than the Commands table, so it stays out of the design menu and the count stays at 23. Also corrects CLAUDE.md, which still documented the register axis, reference/brand.md, reference/product.md, eleven deleted domain reference files, and an extractRegister() whose only occurrence in the repo was that sentence. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
6d47843867 |
Stop losing DESIGN.md and native platform refs across init
Bugbot flagged the "resume without rerunning context.mjs" instruction after init. It is right, and the gap is wider than the platform half it named: context.mjs has two output branches, and the no-PRODUCT.md branch omits DESIGN.md, the native platform references, and the unrecognized `## Platform` warning. Because the skill never reruns the script once init writes PRODUCT.md, whatever that first run withheld is gone for the whole session. A greenfield iOS project would be designed without reference/ios.md ever loading, and a project carrying DESIGN.md without PRODUCT.md never saw its own design system. The two halves need different fixes. DESIGN.md is authority in its own right and does not depend on PRODUCT.md existing, so context.mjs now emits it on both branches. Platform is unknowable before PRODUCT.md exists, so no change to the script can recover it; init.md, the one step that learns the answer, now loads ios.md / android.md / both right after recording a native platform, and SKILL.src.md says so where it tells the agent not to rerun. Verified end to end against a temp project on both branches. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
06d21dea7d |
Add first-class Grok Build harness support
Emit .grok skills, agents, and PostToolUse/Stop hooks; wire the CLI installer and downloads; fix the plugin install path to #plugin; and document Grok in HARNESSES.md and README. AI assistance: written with Grok Build. |
||
|
|
285ed7ef78 |
serve-question: schema discoverability and a non-blocking mode
Paul's two concerns with the blocking design. --schema prints the exact payload example so the model never guesses the shape (new-work.md points at it). And harnesses that cannot leave a shell blocked (or cannot open a browser while blocked) get a two-phase path: --start daemonizes the server and returns the URL plus a key immediately, --wait polls for the answer with exit 3 meaning ask again, exit 2 meaning the server died, and --stop for cleanup. The browser open happens from the detached server process, so it works even when the agent thread is short-lived. State lives under .impeccable/questions/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5612cdf45b |
Visual decisions and default visualization
Paul's design, three pieces: serve-question.mjs: the world decision presented as a themed page instead of a text prompt. The script serves an impeccable-styled option board (assigned direction leading with THE ROLL badge, dealt challengers as alternates carrying their QUALITY BAR cards, re-roll and steer built in), prints the URL, opens the browser, and blocks until the user chooses; the answer lands on stdout as ANSWER JSON, so the shell call itself is the wait and no harness machinery is needed. Local images are served by the ephemeral server; nothing leaves the machine. generate-image.mjs + context.mjs IMAGE_GEN_AVAILABLE: when an OpenAI key is in the environment, context reports that image generation works even without a harness-native tool (gpt-image-2, billed to the user's key, stated before first use; Google skipped by decision). Harness-native tools always win when present. new-work.md: visualize-before-build is now the default whenever any image generation exists, not a codex.md special case; the attended presentation prefers the visual decision page and falls back to the structured question tool. Evals keep the unattended path untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
36e3c05ca7 |
Restore dice assignment, fusion, and the commitment counterweights
The ship40 concept pipeline had reversed the proven a-series mechanisms: the seed's roll decayed into a shortlist nomination that taste functions (model ranking, candidate floor, simulated user) then argmaxed into the safest card; the costume check returned as the Translation veto and carrier-removal test; and the 07-15 rewrite deleted the calibration, reflex-font lanes, color strategies, and commit-every-atom language that had held off the cream-editorial default since the alpha era. Five of six frozen craft directions converged on the same warm-paper family and both builders obeyed them. This lands the repair on top of the in-progress simplification: - new-work.md: the script assigns the build index again on both scopes; catalog challengers are fused (challenger supplies form and grammar, product supplies every fact, clarity wins conflicts) and weighed on the two proven axes only; attended runs present one fully committed direction with re-roll and an optional steer instead of a ranked lineup; the color-strategy picker, reflex-face list, saturated-look calibration, first-viewport thesis and memory test, commit-every-atom, scroll pacing, and prove-don't-claim return; the direction contract returns as five lean blocks audited by the separate-agent finish. - concept-seed.mjs: PROMOTED INDEX becomes ASSIGNED INDEX with build-assignment semantics; self re-roll only on named factual grounds. - craft-floor.md: hook-active sessions act on findings instead of re-auditing; the Refuse list is framed as category defaults the brief can earn; a closing commitment line keeps a ban list from being the last word before code. - codex.md / shape.md: contract references restored for flow coherence. Adopts the concurrent session's ceremony cuts, softened challenger instruction, seed SOURCE IDs and --candidate-count, detector-ownership fix, and the removal of the hook-side contract audit (the audit now belongs to the separate reviewer at finish). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
153b416f2e |
Move the slop defects back into the craft floor
The detector-blind slop review existed because the AI-tell rules had been stripped out of SKILL.md and nothing carried them. The floor is a better home: it loads after concept ideation and immediately before editing UI, which is the placement that made stripping them necessary in the first place. Models tread lightly when a ban list is present during ideation; by the time the floor loads, the direction is already committed. - Rename build-floor.md to craft-floor.md and restore the absolute bans (side-stripes, gradient text, glassmorphism, hero-metric, identical card grids, eyebrow-on-every-section, numbered markers, text overflow), the codex and gemini defect lists, and the reflexes no scanner catches. Rule ids match the ones the ablation catalog already knows. - Delete lib/slop-review.mjs and both injections. The Stop hook is now purely a mechanical pass and stays silent with nothing to report. - context.mjs replaces AI_SLOP_REVIEW_REQUIRED with the narrower MANUAL_DETECTOR_REQUIRED, emitted only when a session has no hook at all. A per-edit hook already covers the mechanical gap, and the floor covers the judgment one either way. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
d7d10277d1 |
Merge main into oneshot-v4, keeping the service layer split out
main still carries the site, so every `site/` path resolves to deleted. `tests/docs-integrity.test.js` goes with it (it imports the site's demo renderer), and `package.json` keeps main's `@anthropic-ai/sdk` bump while dropping `@google/genai` and `@paper-design/shaders`, which nothing in the product layer imports. Real code merges: - hook-lib: main's #391 cache fix (sync the remembered set to the live scan so fixed findings stop being named and a reintroduced one fires again) now runs on the immediate tier rather than the whole filtered set. Remembering a deferred finding the per-edit pass never reported would let the Stop deep pass dedupe it away. main's `maxFileBytes` ceiling, `cleanAcked` once-per-file ack, and template-extensions re-export all land alongside the tiering work. - live-browser: main's `hasParams` gate on the Tune badge, keeping this branch's `C.ink` badge text so it stays legible on kinpaku gold. - detect-text: both the block-level codex-grid-background scan and main's inset-stripe CSS check. - test-suites: union of both trigger sets and file lists, minus the site-only entries (`shiki-theme`, `docs-integrity`). - Two hook tests moved off deferred-tier rules (`overused-font`, `side-tab`) onto immediate-tier ones. They assert cache bookkeeping, which the per-edit pass only reaches for the immediate tier. Also drops the site waivers from `.impeccable/config.json` and stops `build:browser` recreating a stray `site/` tree just to write a bundle the other repo builds itself. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
b1735015a9 |
Deal three staging inputs per roll instead of one
A single staging input was too weak a counterweight to the model's habitual page skeleton: beside six identity challengers it read as one optional flourish rather than a real search over composition. Roll three from distinct staging families so a roll tests materially different hierarchy, sequence, and interaction laws. selectApprovedStagings replaces the single-pick selector; the old selectApprovedStaging stays as a count-1 wrapper for smoke tests. Re-rolls exclude every earlier set, and an absent mode still returns nothing rather than falling back across modes. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
90f9eeb99b |
Split service layer into private impeccable-site repo
The public repo keeps the OSS promise surface: skill, CLI, extension, tests, and the provider build. The site, labs, concept/composition catalogs, image pipeline, Cloudflare functions, and authoring guide move to pbakaus/impeccable-site. concept-seed tests run against a synthetic fixture catalog; the plugin icon and skill categories moved in-repo; build validation narrows to README prose and non-site counts; release notes read from a sibling impeccable-site checkout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b5ec969c07 |
Add world roll API and seed telemetry client
/api/roll deals deterministic challenger rolls server-side (same salts and sha256 ranking as the local seed, verified bit-for-bit); the request log is the impression record. /api/chosen takes the anonymous choice ping. Events land in Workers Analytics Engine. concept-seed.mjs resolves data in order: local catalog dir, roll API, degraded promotion-only seed. --chosen sends the choice ping; DO_NOT_TRACK and IMPECCABLE_NO_TELEMETRY disable it. API-dealt seeds carry the telemetry instruction inline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7557935fdb |
Expand concept system: modes, ratings, re-roll, breadth strategy
Catalog: mode-aligned staging surfaces (persuade/operate/read/experience), star ratings on approvals feeding challenger draw weights, family retirements, authoring strategy and territory guide, rework and breadth authoring rounds, composition mining from rejected worlds. Seed: six challengers (two per tier), --reroll chains, --mode staging filter, rating-weighted draws. New-work: Present/visualize/re-roll flow, image-gen requirement, register-neutral vocabulary. Pipeline: per-mode staging prompts with split frames, hero-from-board reference generation, render-safety guards. Labs: ratings UI, unrated filter, mode chips, composition approve-guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a6957e5d4b |
Allow context roots to be declared in .impeccable/config.json (decoupled from package managers) (#307)
* Allow context roots to be declared in .impeccable/config.json Monorepo detection previously read workspace roots only from package managers (package.json workspaces, pnpm-workspace.yaml, lerna.json), coupling "where design context lives" to the dependency graph. Add a `contextRoots` glob list to .impeccable/config.json / config.local.json so non-JS repos -- and design-context boundaries that don't match packages -- can declare nested PRODUCT.md/DESIGN.md roots directly. The new source is folded into readWorkspacePatterns(), so detection, project resolution, and the app picker pick it up unchanged. Negation and config.local.json extension work for free. * Define projectRoots composition with package workspaces Address review feedback on #307: - Rename the config key contextRoots -> projectRoots: the globs establish project boundaries and app-picker targets, not just where context files live. - Make cross-source precedence explicit: a path matched by any impeccable pattern, positive or negated, is governed by the impeccable group alone; package-manager patterns fill in the paths it does not match, and `!` negations apply only within their own source. readWorkspacePatterns() becomes readProjectPatternGroups() / readProjectPatterns(), with package workspaces as one discovery source. - Drop app-picker candidates that would resolve elsewhere: a package workspace subsumed by a broader impeccable boundary is no longer listed, since choosing it would silently resolve to that boundary. - Add five composition tests and document the key in the config and context reference pages (path relativity, glob and negation syntax, shared/local merge, precedence). |
||
|
|
5d719a279a |
Fix Live accept for Elixir templates in lib/ (#374)
* Fix Live accept for Elixir templates in lib/ Wrap and accept search the repo for impeccable variant markers. That search skipped .ex files and the lib/ tree, so Phoenix LiveView markup inside ~H""" blocks never matched and browser Accept returned "Session markers not found". Extend the same EXTENSIONS and searchDirs in live-accept.mjs and live-wrap.mjs. Add a regression test that accepts from lib/my_app_web/components/layouts.ex. * Live: give the source search one owner for template extensions The #374 fix had to patch the same hardcoded EXTENSIONS array in two files because live-wrap.mjs and live-accept.mjs each carried their own copy of the project source walk. The copies had already drifted: same extension list twice, same searchDirs twice, and one realpathSync guarded by try/catch while the other was not. Meanwhile hook-lib.mjs had solved this properly for the design hook in #316/#347 with a configurable `detector.extensions` and suffix matching that handles .blade.php and .html.erb. Live never read it, so a project that taught the hook about .heex still got 'Session markers not found' on Accept. - lib/template-extensions.mjs is the single owner. It holds Live's built-in markup list, the suffix matcher, and the detector.extensions config reader. hook-lib.mjs now imports its normalize/merge/match helpers from here instead of duplicating them, and re-exports matchConfiguredExtension for its existing callers. - Live resolves built-ins PLUS detector.extensions, so teaching the hook about a server template teaches wrap and accept at the same time. - live/source-search.mjs holds the walk both scripts share. Callers pass the one thing that actually differs (skipDirs, fileFilter). Unifying gives live-wrap the guarded realpathSync, so a dangling symlink in the tree no longer throws out of the whole wrap, and makes it skip .impeccable artifacts the way accept already did. - Extensions are matched on filename suffix rather than path.extname, so root.html.heex and show.html.erb resolve. - Drop .exs. Those are Elixir scripts (mix.exs, config/*.exs), never markup, and including them only lets a wrap query match build config. - Fill the Elixir gap in the manual-edit paths, which kept their own allowlists and would have left Live half-working for Phoenix: live-commit-manual-edits.mjs and live-manual-edit-evidence.mjs. Verified the round trip by hand against a Phoenix layout: wrap injects markers into a ~H""" block in lib/**/*.ex, accept carbonizes the chosen variant back out. AI assistance: written with Claude Code. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Nils Kanevad <heliumbrain@users.noreply.github.com> Co-authored-by: Paul Bakaus <paul.bakaus@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d146d2084b |
Stop the design hook lying about findings it already reported (#391)
* Stop the design hook lying about findings it already reported
Three fixes, all aimed at the hook being trustworthy enough that an agent
keeps reading it.
1. The session cache was append-only, so the hook lied and then went blind.
`rememberFindings` unioned new keys into the remembered set and nothing ever
removed them, and the pending ack took its count from that set rather than
from the live scan. Fixing two of three findings produced:
Still has 3 finding(s) flagged earlier this session
(overused-font:1:inter, overused-font:2:roboto, overused-font:3:geist)
with roboto and geist already gone. Worse, a finding that was fixed and then
reintroduced was deduped against the stale memory and never re-reported, so
the hook was permanently blind to that regression for the rest of the session.
The cache now syncs to the complete current scan on every scan, so the count
shrinks as work lands and a reintroduced finding reads as fresh. Dedup within
a session still works, because it compares against the previous scan rather
than against all history. A detector failure leaves the remembered set alone
instead of recording an empty scan as truth.
2. The size ceiling, for generated files that do not live under dist/.
`GENERATED_PATH` covered dist, build, out, .next, .cache, coverage and
.min., but repos commit browser bundles and vendored detector copies next to
source. The hook was reading and scanning a 215KB generated bundle, and
reporting findings in it. Added `generated` as a path segment, matched with
separators on both sides so authored names such as generated-utils.ts and
CodeGenerator.tsx still get scanned, plus a `limits.maxFileBytes` ceiling
defaulting to 128KB. In this codebase authored files top out at 86KB while
the bundles start at 215KB, so the gap is comfortable.
3. The clean ack repeated on every clean edit.
It carries no finding, only the standing steer that a silent hook is not a
verdict on the design. That steer is worth saying, but not dozens of times
per session. It now fires once per file per session and reports
`clean-ack-deduped` in the audit log so suppressed noise stays visible. The
pending ack is deliberately untouched: it names real unresolved work, and the
comment explaining why it must repeat still holds.
Verified end-to-end against the built hook: three findings, fix two and the
count drops to one naming only the survivor, fix the last and it goes clean,
edit again and it stays silent, reintroduce and it fires as fresh.
Generated provider output is deliberately left out; the sync workflow owns it.
Prepared with AI assistance (Claude Code).
Co-Authored-By: Claude <noreply@anthropic.com>
* Address review: three clean-ack and audit bugs in the dedupe change
All three were introduced by this PR and all three are fair catches.
Quiet mode spent the ack (bugbot). A clean scan marked cleanAcked and
persisted it even when quiet suppressed all output, so a later non-quiet run
in the same session never got the steer. The quiet decision is now hoisted
above the scan loop and quiet leaves the ack unspent.
Multi-file events lost the ack (copilot). The first clean target became
cleanWinner unconditionally; if that file was already acked, cleanAckDeduped
went true and the `!cleanWinner` guard meant a later target that had never
been acked could never win. A raw apply_patch touching two files would drop
the second file's ack entirely. The loop now keeps looking for a target that
is actually owed an ack.
audit.bytes leaked across targets (copilot). It was set when a file was
skipped as too-large and never cleared, so in a multi-file event a later
emitted result carried the skipped file's byte count. Cleared per iteration.
The tests use a raw apply_patch payload rather than MultiEdit, because
MultiEdit in this harness is single-file ({ file_path, edits: [] }) and would
not have exercised the multi-target paths at all. Verified the three tests
fail against the pre-fix code and pass after, so they are not passing for the
wrong reason.
Prepared with AI assistance (Claude Code).
Co-Authored-By: Claude <noreply@anthropic.com>
* Address review: font-size waivers silently did nothing
Two more review findings, both real.
Specific-value font-size waivers were dead config (greptile). The rule emits an
ignoreValue, and the hook's own directive footer tells the agent to waive
value-specific findings with `hooks ignore-value <rule> <value>`, but
`design-system-font-size` was missing from the direct-value rule set in
`extractFindingIgnoreValue`. The extracted value came back empty, so any
waiver naming an actual size was compared against nothing and silently
dropped. Only the `*` wildcard worked, which is why the framework-viz waiver
earlier in this branch appeared to function.
Reproduced against the built hook: with a `0.82rem` waiver the finding still
fired; it now goes clean, while a waiver naming a different size correctly
still fires, so this is not over-matching.
Wrong audit skip reason (bugbot). In a mixed multi-target run, an earlier UI
file whose ack was already spent set `cleanAckDeduped`, and a later non-UI
clean file became the winner. The tail then reported `clean-ack-deduped` when
the honest reason was `non-ui-ack`. Audit-label only, no behavior change.
Reordered so the winner is described first and dedupe is reported only when it
is genuinely why nothing was emitted.
Prepared with AI assistance (Claude Code).
Co-Authored-By: Claude <noreply@anthropic.com>
* Mirror the font-size waiver fix into the CLI's config reader
Bugbot caught that the previous commit only fixed one of two copies.
`extractFindingIgnoreValue` exists twice, in skill/scripts/hook-lib.mjs and in
cli/lib/impeccable-config.mjs, and the direct-value rule list is duplicated in
both. Adding design-system-font-size to the hook alone meant the same
.impeccable/config.json filtered differently depending on the entry point: a
size waiver was honored by the hook and ignored by `npx impeccable detect`.
The two functions are otherwise byte-identical, so this restores parity rather
than changing CLI behavior independently. The new test notes the duplication so
the next person knows the pair has drifted once already.
Prepared with AI assistance (Claude Code).
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix the audit byte-count leak properly, not just one scan order
My earlier fix cleared audit.bytes at the top of each iteration, which was
wrong twice over, and bugbot caught both.
The clear sat below the sensitive, generated, extension, ignore-file and
file-missing continues, so a later target exiting through any of those never
reached it and kept the oversized file's size while audit.file pointed
somewhere else. It also only handled the bundle-scanned-first order; when the
oversized file came last, the byte count was set after the emitting file had
already been decided and rode along on its audit entry regardless.
The root problem was keeping per-file state on the shared audit object. The
size is now held in a local and attached only when the oversized skip is the
run's actual outcome, so it cannot describe a file other than the one being
reported. Tests cover both scan orders, an early-continue target after the
skip, and the single-oversized-file case where the count should still appear.
Prepared with AI assistance (Claude Code).
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
373039a837 |
Give DESIGN.md a real type ramp so the design hook stops crying wolf (#390)
* Give DESIGN.md a real type ramp so the design hook stops crying wolf The design hook fired on nearly every CSS file we touched. The cause was DESIGN.md's typography block: it declared seven named roles rather than a scale, and two of those roles used clamp(), which the extractor skipped outright. That left an allowlist of five sizes standing against the 86 distinct font sizes actually in use, so design-system-font-size flagged roughly 500 declarations. Editing any .astro page made it worse, because the companion-stylesheet scan re-reported the whole backlog. Extractor (cli/engine/design-system.mjs): - Read a typography.scale map as the enumerated ramp. - Read both clamp() endpoints as allowed sizes. These stay additive on purpose: clamp endpoints alone cannot switch the rule on, because a fully fluid system enumerates no discrete ramp and inferring one from its endpoints would flag every intermediate size. The existing abstention test still passes, and three new tests cover the added behavior. DESIGN.md: - Document a 19-step ramp, 8px through 72px at a 16px root. - Snap the five discrete role sizes onto ramp steps. This also fixes real drift. DESIGN.md claims to mirror kinpaku-tokens.css verbatim, but wordmark was 1.15rem in the CSS against 1.3rem documented, with tracking at 0.42em against 0.15em. Both are re-synced. Standardization, 64 declarations: - Six near-identical steps between 13.7px and 15.4px collapse onto 14 and 15. - .foundation-card-label, .designing-lane-mock-title and .designing-iterate-name each existed at two different sizes in two files. Now unified. - The wordmark rendered at four sizes (20.8, 18.4, 17, 16.8px). Now 18px, plus one deliberate smaller nav variant. Exemptions, for designs that are foreign on purpose: the antipattern-example fixtures, the neo-mirai case-study build, the periodic-table cell annotations in framework-viz.js (5 to 7px diagram geometry sitting at 2 to 3px offsets), and the .why-slop-* before-state card's Inter and gradient text. Verified by computed style across ten rendered pages: every element lands on a ramp step except clamp() values mid-interpolation, which is what fluid means. Full test suite and build validators pass. Generated provider output is deliberately left out; the sync workflow owns it. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> * Validate clamp() endpoints in usage, not just when reading DESIGN.md Reading clamp endpoints as documented steps without also checking them in source left an asymmetry: `isAllowedFontSizeRaw` returned true for anything failing the px/rem literal test, so `clamp(99rem, 1vw, 200rem)` passed. That is how `.ptable-symbol` at `clamp(1.45rem, 1.8vw, 1.8rem)` stayed invisible until someone measured computed styles, which is not a check the hook can run. Fluid values are now judged on their min and max. The viewport term interpolates between them and is never a fixed step, so it is left alone. Endpoints that cannot be resolved, such as var() or calc() or em, abstain rather than guess. Findings name the offending endpoint and use it as the ignore-value, because the whole clamp string is not actionable on its own. Turning the check on surfaced 22 fluid declarations that had never been looked at. Three used hero sizes above the ramp's 72px cap (80, 83.2 and 88px) alongside the display role's documented 89.6px max, so the top of the ramp was genuinely incomplete. Added the 80 and 88 steps, which gives the display end consistent 8px increments instead of 48/56/64/72 plus an orphan at 89.6, and fixes two declarations outright. The other 20 are snapped by a stated rule: nearest step, ties toward the smaller step, endpoints already matching a documented fluid role left as-is, and where nearest-step would make a breakpoint override meet or exceed its base, the next smaller step so the override still reduces. That last case applies once, to .designing-page-title. Also narrows the framework-viz.js waiver. The periodic-table cell annotations now carry two `impeccable-disable-line` comments naming the reason, instead of a config entry wildcarding the whole file for the rule. Inline waivers travel with the code and cannot silence future drift elsewhere in that file. Verified at 420px, 900px and 1600px across seven pages. The pinned ends are fully on-ramp; the only off-ramp values at 900px are the vw term mid-interpolation, which is what fluid means. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> * Address review: wordmark tracking picked the wrong side, stale ramp count Two review findings, both fair. Wordmark tracking (greptile, bugbot). This PR moved DESIGN.md's wordmark letterSpacing from 0.15em to 0.42em on the grounds that DESIGN.md claims to mirror kinpaku-tokens.css and the token read 0.42em. That was the wrong side to trust. `--ks-type-wordmark-track` has exactly one consumer, design-system.css:570, which is the specimen page. Every production lockup (.ks-wordmark, .kinpaku-chrome .site-header-brand-name, .footer-logo) hardcodes 0.15em, so 0.15em is what every visitor actually sees and what DESIGN.md already documented correctly before this PR touched it. Reverted the doc to 0.15em and moved the token to 0.15em as well, so the specimen now renders the same lockup as production instead of a wider one nothing else uses. Verified by computed style: header and specimen both report 18px with 2.7px tracking. No production visual change. Stale ramp count (copilot). The sidecar described an "18-step ramp, 8px through 72px". It went stale twice inside this PR, once when the 8 step was added and again when 80 and 88 were added for the hero display sizes. It is 21 steps, 8px through 88px. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> * Strip !important from the font-size ignore value Follow-on from the waiver wiring in the hook branch. The ignoreValue is what a `hooks ignore-value` waiver has to match, and `font-size: 1.4rem !important` emitted `1.4rem !important` while a plain declaration emitted `1.4rem`. Once font-size is a direct-value rule, that means the same size needs two different waivers depending on whether it carries a priority marker. font-family already strips the marker before matching, and there is a test for that. font-size now does the same. The snippet still shows the declaration as authored. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> * Have the wordmark rules consume their tokens instead of copying the values Follow-up to the tracking fix, and the residual half of what the reviewers were pointing at. `.ks-wordmark` and the kinpaku chrome lockup each repeated `1.125rem` and `0.15em` literally rather than reading `--ks-type-wordmark-size` and `--ks-type-wordmark-track`. That duplication is exactly how the token drifted to 0.42em while every production lockup stayed at 0.15em and nobody noticed, which is the confusion that started this thread. The values already agree, so this is a no-op visually and is verified as such: computed styles across the home, design-system, docs and changelog pages all still report 18px with 2.7px tracking. What changes is that there is now one place to edit, so the next tracking change cannot silently apply to the specimen page alone. Prepared with AI assistance (Claude Code). Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d0ac67c6e9 |
Live: polling rework, source locks, preflight scaffolding (#381)
* Improve Live polling responsiveness and reliability Restore foreground/background polling as the primary harness architecture, add progressive publication and framework-safe previews, and harden quality and regression coverage. The experimental app-server runtime is intentionally excluded.\n\nPrepared with AI assistance under maintainer direction. * Fix source-safety, detector, and lock defects in Live polling work Addresses the review findings on #371, plus several the bots did not catch. All fixes have regression coverage that fails on the prior code. Source corruption: - Vue accept dropped valueless root attrs (disabled, v-cloak) and, worse, rewrote @click="x" as a literal click="x" DOM attribute, because the attr parser was name-anchored and skipped the sigil. Tokenize the whole Vue attr grammar and normalize shorthands so accept round-trips directives. - --variant was interpolated unescaped into a RegExp, so --variant '.*' matched the original block first and reported a successful accept while silently restoring the original. Validate against the digits pattern the browser and the /events schema already enforce. - --id reached path.join unvalidated, so --id ../../../../etc/evil wrote and read receipts outside the project. Hoist the existing safeSessionId check into impeccable-paths and apply it at every id-to-path sink. Accept/lock correctness: - Plain HTML/JSX accept and discard did not catch SOURCE_LOCKED, so contention exited non-zero with empty stdout and the agent got no JSON to retry on. - Lock staleness was mtime-only and never read the pid it records: a holder whose critical section outran 60s had its live lock swept, admitting a second writer to the same file, while a crashed holder blocked accepts for a full 60s. Decide staleness by owner liveness, and release only our own lock. Detector: - isNeutralColor only parses computed color forms, so routing authored CSS through it reported inset 4px 0 0 #000 / black / #e5e7eb as chromatic side-tab stripes. Add an authored-color neutrality test covering hex and named neutrals; the fixture had no literal-color cases at all. - Rule line numbers were off by one for every rule after the first, and commented-out CSS was scanned as live rules. Server: - An error reply carries no sourceEventType, and inferSourceEventType returned undefined, which acknowledgePendingEvent treats as a wildcard: a stale generate worker's failure consumed the user's queued Accept, which then reached no agent and left the browser in SAVING forever. - The generate preflight spawned live-wrap.mjs synchronously inside the request handler, freezing the single-threaded server for the whole scaffold (~7.6s measured on this repo, 15s ceiling) and stalling Accept/Discard/SSE. Make it async, claiming the lease before the first await so no event double-delivers. - Every browser checkpoint was echoed back as variant_progress, so a Tune slider drag remounted the preview under the user's cursor and latched the *_reviewable phases from the wrong trigger. Gate on the reason. Cleanup: - Collapse four divergent benchmark argv parsers into scripts/lib/cli-args.mjs. Three silently misread flags: --iterations 20 benchmarked 5, --agent llm ran the fake agent, --median-target=0.4 used the default threshold. - Drop a snapshot cache this branch made write-only (it grew per session for the server's lifetime and was never read), a dead exported reconcile helper, and the unused deferReply branch. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Route the last two benchmark scripts through the shared argv parser Follow-up on review feedback. The previous commit consolidated four of the six Live benchmark parsers and left these two on their own hand-rolled `arg()`, which was the inconsistency the first pass was meant to remove. - benchmark-live-control.mjs and benchmark-live-init.mjs parsed --iterations with Number(), so a non-numeric value became NaN and `index < NaN` ran the benchmark zero times before failing on the metrics file. They also accepted only the space-separated form, so --iterations=20 silently measured the default. Both now use parseArgs + positiveIntFlag, which throws on a value that was clearly meant as a number. - benchmark-live-control.mjs read the metrics file with no handling for the case where the run produced nothing: a missing file surfaced as a raw ENOENT stack and a malformed line as a bare SyntaxError. Report both with a diagnostic naming the file and the env var that populates it. - summarize() now reports a `samples` count and nulls instead of letting percentile() read past an empty array, where the NaN serialized to null and a report of nothing measured looked like a real measurement. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Stop telling users a busy agent is disconnected The agent-poll indicator tracks whether a poll is parked, which is the right signal for "can steering reach the agent right now" and is why the flag itself is left alone. But it goes quiet for two different reasons, and both got the same copy: "Agent disconnected - run live-poll.mjs to connect". Under the one-shot foreground polling that live.md calls the primary contract, no poll is parked while the agent works, so the second reason is every normal generation. For its whole duration the bar told the user a healthy session was broken and advised them to start a poll loop that was already running. Pick the copy from the live state, which the browser already tracks: GENERATING and SAVING mean the agent holds work it was handed, so say it is working. Every other state with no parked poll keeps the original, actionable wording. The aria-label carries the same distinction, since the tooltip is mouse-only. The text is derived at read time rather than cached, because the live state moves between the 5s status polls and a finished generation would otherwise keep reading "Agent is working" until the next one landed. Deriving it also keeps the read out of setLiveState, which runs long before agentPollingConnected's declaration and would hit its temporal dead zone. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Scope design-system-font-size off the injected live overlay live-browser.js builds a self-contained UI that renders over arbitrary host pages, so its inline type scale is deliberately independent of DESIGN.md, which documents the impeccable website's ramp. The rule fired 32 times there and is the only rule that fires on that file. Suppress it as a file-scoped value wildcard rather than via ignoreFiles: an ignoreFiles glob would silence every rule for the file, and the overlay is real user-facing chrome where a future contrast or side-tab finding should still be heard. Scoped to this one file, so the rule keeps working everywhere else. Written by hand because hook-admin's ignore-value cannot emit the `files` array that detector.ignoreValues supports and existing entries already use. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Let hooks ignore-value scope a rule to files, and stop churning the config Fallout from suppressing the overlay's font-size findings: the narrowest exception detector.ignoreValues supports was unreachable from the path the hook tells the model to use, so the guidance steered to the blunt instrument instead. - hook-admin's ignore-value now takes --file / --files / --file= / --files=, matching `impeccable ignores add-value`, which already had them. Without it the only file-scoped option was ignore-file, which silences every rule for a path permanently, including rules not yet written. - A bare wildcard value is now refused with a message pointing at either --file or ignore-rule. Previously `ignore-value <rule> "*"` quietly wrote a project-wide suppression from a single file's finding. - ignore-value keyed entries on rule+value only, so a second scope for the same rule overwrote the first instead of coexisting. Key on the file scope too. - An unknown flag folded into the value: `ignore-value overused-font Inter --shard` stored "inter --shard", matched nothing, and reported success. Reject it, as the sibling command does. Config churn: normalizeIgnoreValueEntries runs on every write and emitted keys as rule, value, files, reason, createdAt while the config on disk uses createdAt before reason. Any edit therefore rewrote every untouched entry (35 churned lines for a one-line change). Pin the canonical order in both copies of the normalizer and in ignores.mjs, and add a test that the two copies cannot drift apart. Also point the hook's own footer and reference/hooks.md at the file-scoped form first, and say plainly what ignore-file costs. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Correct the prose-gate docs and write down the no-bump-in-a-PR rule CLAUDE.md said the prose validator "deliberately skips skill/", which is only half true and cost a build failure this week: validateProse skips it, but validateSkillProse then scans skill/**/*.md and fails the build on em dashes plus the phrases with no technical reading. Document both gates, which files each one reads, and the line that actually matters in practice: an em dash in skill/reference/*.md fails the build, one in a skill/scripts/*.mjs comment does not. Each claim was checked against a real `bun run build`. Also record that feature PRs do not bump manifest versions or add changelog entries. It was not written down anywhere: not CLAUDE.md, not AGENTS.md, not the PR template. CLAUDE.md's "Bump when: CLI code changes" reads as an instruction to bump inside the PR that touches cli/, so say plainly that it names which component a change belongs to rather than when to edit the manifest. Put the rule in AGENTS.md too. That is the guide the agents opening PRs here actually read, so a rule about PR hygiene living only in CLAUDE.md would not reach them. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Bring Live progressive delivery and the generator subagent to Claude Code Almost none of this branch's Live work was actually Codex-specific. The publisher, the fences, the source locks and the browser's partial-arrival UI are plain node and DOM with zero provider references, and the progressive E2E already passes on five frameworks driven by a non-Codex agent. The Codex-only part was policy prose and one frontmatter line, so Claude Code shipped the progressive browser UI it could never trigger. Progressive delivery, Codex and Claude Code: - Add a `live-progressive` capability tag and opt codex, agents, and claude-code in. A provider block takes one tag, so naming harnesses would have meant duplicating the recipe per tag; a capability reads better than a provider list anyway. Cursor and everyone else keep the atomic path until their poll loop is known not to stall on the extra publish calls. - Claude Code publishes variant 1 as soon as it validates rather than waiting to write the whole trio in one edit. Nothing about the arrival path needed changing: the publisher writes, framework HMR pushes, and the browser's MutationObserver counts variants. The parent conversation was never in that path, which is why Claude Code's lack of subagent progress streaming does not matter here. Generator subagent: - Drop `providers: codex` from impeccable-live-generator. The build already maps its frontmatter correctly for Claude Code, and impeccable-manual-edit-applier has shipped to .claude/agents/ this way all along. - The reason differs per harness, so the reference says so: Codex delegates to unblock a foreground poll, Claude Code delegates to keep a long session's screenshots and variant CSS out of the main context. Follows the existing manual-edit-applier convention: both agent names, and an inline fallback when native subagents are unavailable. Fixes found on the way: - The two publish commands hardcoded `.agents/skills/impeccable/scripts/` while the other thirteen commands in live.md use {{scripts_path}}. Correct only for the Codex repo-skills bundle; it would have pointed Claude Code at a directory its install never creates. The shipped .codex variant was already internally inconsistent. Now covered by a test. - `--agent=codex` resolved to the canned fake agent, because the flag parsed as `x === 'llm' ? 'llm' : 'fake'`. The private evals Live runner passes exactly that, so a real-harness run would have scored deterministic stub variants and reported them as Codex output. Unknown values for --agent, --scenario and --delivery now fail loudly. - live-reference tests now compile with each provider's real providerTags instead of hand-written lists, so a providers.js misconfiguration fails in tests rather than shipping. Verified: progressive E2E green on vite8-react-plain against a real Vite server and Chromium; every provider variant's publish and poll paths now agree; Cursor and Gemini still compile to atomic only. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Fix inset-order detection, the unlocked artifact discard, and stray boolean flags Three of the four open review findings. The fourth is declined below. - The inset-stripe scan only matched layers starting with `inset`, but the keyword is order-independent: `box-shadow: 4px 0 0 var(--brand-accent) inset` paints the same stripe and was silently missed. Strip the keyword wherever it sits, but only as a standalone token, so a color like var(--inset-accent) is not mangled into `var(-- -accent)` and quietly reclassified as neutral. The fixture now covers both orders plus that token, and a trailing-inset neutral still passes. - The source-artifact discard deleted the preview without the source lock, unlike every other discard path. Take the lock. Narrower than reported, though: the server journals `discard_requested` as a fenced phase before live-accept runs and the publisher checks it three times, so a publish could never land on a discarded session. What this actually prevents is deleting the artifact under a publisher mid-critical-section, turning a clean stale_generation_epoch into an ENOENT crash. - benchmark-live-providers.mjs still compared `--headed` and `--skip-cleanup-control` against a boolean sentinel, so the `=true` spelling silently did nothing. My gap: I introduced boolFlag and converted benchmark-live.mjs but not this one. skipCleanupControl is now read once rather than twice, so the two call sites cannot drift. Declined: tightening the selector guard that skips `active` / `current` / `selected` tokens. It does cause false negatives on names like `.selected-feature`, but the rule's contract makes selection and focus indicators its one exception, and `.active-tab` / `.current-step` / `.selected-row` are syntactically identical to `.selected-feature`. No regex separates them, so tightening the guard trades missed stripes for false positives on exactly the case the rule exempts. The conservative skip is the intended behavior. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Classify failed accepts as errors, and fix parallel lane race/all misuse Two of the three new findings, plus the bug that chasing them exposed in my own earlier fix. The third is mitigated rather than broken; details below. Failed accepts reported success: live/completion.mjs only classifies a result as `error` when it carries `mode: 'error'`. Everything else unhandled falls through to `agent_done` with an ok ack, which is deliberate for the documented fallback paths (two tests pin it) but wrong for a real failure. So `accept_receipt_conflict` reported success, and reference/live.md's `handled: false` without `mode` bullet told the agent to "read file, find markers, edit" — hand-applying a second accept on top of the one the receipt already recorded. The same hole swallowed `source_locked`, which is mine: the earlier commit made lock contention return clean JSON so the agent could retry, but the classifier turned that failure into agent_done/ok, so the accept was dequeued and silently lost. Mark genuine failures with `mode: 'error'` through one `operationFailure` helper, and give live.md a `mode: "error"` bullet with per-error guidance: retry the same command on `source_locked`, never hand-edit, and on a receipt conflict report what the session actually resolved to. The deliberate fallback and markers-not-found handoffs stay untouched. parallel-compact lane orchestration: `Promise.race` settles on the first *settlement*, so one lane failing fast rejected the whole first-variant step while two lanes were still on their way to succeeding. `Promise.any` now takes the first success and only a total wipeout is fatal, reporting every lane's reason. The tail step's `Promise.all` surfaced a raw lane error non-deterministically; `Promise.allSettled` now reports how many lanes failed and why. Added a `requestImpl` seam so lane orchestration is testable without a provider key. Not a defect: the browser releasing Accept before the source write. That is the intended optimistic design, and it is safe because poll-lanes ranks accept at priority 0 against generate at 2, so a queued accept is always leased before a generate the user queues afterwards, even if the generate arrived first. Its source write lands inside the poll script before the next generate preflights. That invariant is load-bearing and had no tests at all; poll-lanes.mjs now has a suite covering it plus lease and type filtering. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Finish the failed-accept classification my last commit only half did All three new findings are the same root cause, and it is my incomplete fix: operationFailure only covered results built from a *thrown* error. Two paths it missed: - Two catches wrote the failure result as a multi-line literal, so the single-line replace skipped them. The Vue accept catch was still bare, exactly as reported; the Svelte one too, though its failures happened to be caught by completion.mjs's Svelte-only special case. - The accept implementations also *return* `{handled: false, error}` for their own checks (variant missing, template empty, original text ambiguous). Those never throw, so no catch ran and no `mode` was set. Both layers now agree, because each is reachable on its own: - live-accept marks any unhandled preview-path result via markPreviewFailure, keyed on `previewMode` — a clean discriminator, since only the preview branches set it and a plain wrapper never does. This is what the agent reads: reference/live.md routes on `mode`, so without it the agent was told "read file, find markers, edit" for a preview that has no markers in source. - completion.mjs replaces its arbitrary svelte-component special case with the set of preview modes whose variants live outside the user's source. That case existed for precisely this reason; Vue and source-artifact were simply never added, so the identical failure on those paths acknowledged as success. The plain wrapper keeps its manual handoff, which is the one shape with editable markers in source. Both deliberate handoffs (mode: 'fallback' and markers not found) still classify as agent_done, now pinned by a test so the generalization cannot swallow them. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Stop the progressive benchmark agent inventing a second variant on count:1 `Math.max(1, event.count - 1)` floored the tail request at one variant, so a one-variant request fetched a second direction and assembled two. Ask for `count - 1` and return the first variant untouched when there is no tail. Latent rather than live: the only caller hardcodes `count: 3`. The reason it is worth fixing is the caller inconsistency it exposed. tests/live-e2e/agent.mjs gates its split-progressive path on `event.count > 1`; benchmark-live-providers.mjs had no such guard, so it would have run the tail for a one-variant request, and the parallel strategy would have assembled its three fixed lanes regardless of what was asked for. Guard the caller the same way. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Drop the live generator subagent; fix the artifact decoy that broke accept The first real Claude Code Live run failed, and the subagent was not the cause. Root cause: progressive publication stages each revision as `.impeccable/live/artifacts/<id>-r<n>.<source-ext>`, nothing ever deleted them, and findSessionFile's walker skipped only node_modules/.git/dist/build. It searches src, app, pages, ... then `.`; a project whose source is not under one of those (this repo's own site lives in site/pages/) falls through to the `.` walk, where dot-directories sort before letters. So accept found the artifact instead of the real file. Two outcomes, both reproduced: where isGeneratedFile returns true it declines with mode: 'fallback' (what the run hit, after which the agent hand-carbonized several hundred lines across three stylesheets, including unrequested drive-by edits); where it returns false, accept writes the variant into the throwaway artifact and reports handled: true while real source never changes. The E2E suite could not have caught this. Every fixture puts source under `src/`, which is searched before the `.` walk can reach `.impeccable`. Five framework fixtures and three progressive scenarios pass because of fixture layout, not because the path works. I read that as evidence and shouldn't have. - Never search `.impeccable`: it is Impeccable's own state, never project source. - Retire a session's staged artifacts on accept/discard, so they cannot outlive the session and become a decoy for anything else that walks the tree. - Regression tests use a site/pages layout with artifacts present. All three fail against the previous code. Generator subagent removed, on both harnesses: The parent must hand-compress the design system into the handoff, and compression is lossy. Measured on the real run: a 6,826-char handoff carrying exactly one token reference, after the parent had itself read kinpaku-tokens.css. The subagent then spent 3 of its first 9 turns hunting DESIGN.md, gave up, and emitted 0 var(--token) uses and 22 raw oklch literals — violating its own spec's "Never invent raw colors when tokens exist" — including a 1:1 gold-on-gold contrast bug. Isolation is not a benefit here; knowing the design system is the job. Generation stays in the main thread, which already holds the tokens and writes them from the first byte, so carbonize is a move rather than a translation. Copy edits keep their subagent: applying a known set of ops to a named file is self-contained, so an isolated context costs nothing. That is the line. Progressive delivery stays for Codex and Claude Code, main-thread driven. Claude Code keeps the full benefit because its poll is a background task. Codex's poll blocks the foreground, so with no subagent the user sees variant 1 early via HMR but cannot accept it until the trio finishes; that is the cost of the simplification and it is worth naming. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Rip out the dead isolated-preview mode and the private repo's job Comparing this branch's live against main's turned up two whole features that never made sense here. -2,466 lines. 1. The isolated source-artifact preview was never switched on. `scaffoldSourceArtifactSession` is only reachable via live-wrap's `--isolated`, and nothing passes it: not the server's preflight, not live.md, nothing. Proved it end-to-end — the default wrap writes markers straight into real source and creates no previews/ session. So the mode was wired through three modules, carried its own accept/discard branches, browser branches, server metadata resolution, preview-mode classifier entry, and test suites, and none of it could run. Worse, live.md documented it as the active path and told the agent "The true source is only the publisher's hash fence and must remain byte-identical until Accept." That is false: the wrapper lands in source at scaffold time and each revision rewrites it. An agent following that sentence believes source is protected when it isn't, and the leftover artifacts are what made accept resolve the wrong file in the first real run. live.md now describes what actually happens, including that markers are visible in source until Accept or Discard. Removed: source-artifact.mjs, --isolated, the preflight's isolated option, the accept/discard branches, four dead browser branches, the server's previews/ resolution, the classifier entry, and their tests. Kept the previews/ gitignore pattern: an ignore line for a directory that cannot exist is free, and a test pins it. 2. Quality judging belongs to the private evals repo, which says so. runner/live/README.md there is explicit: the public repo owns protocol correctness, framework coverage, timing, source commit, recovery, and a rubric-free evidence bundle; the private repo owns the task corpus, baselines, comparative judges, and release-quality decisions — "Do not add quality rubrics, competitor comparisons, or broad fixture corpora to the public Live benchmark." This branch added exactly those: an LLM judge scoring 1-10 on "off-brand, generic-AI" (live-rendered-quality.mjs, judge-live-rendered.mjs), a cross-provider comparison with a BRAND_CONTRACT rubric (live-provider-benchmark .mjs, benchmark-live-providers.mjs), and a brand-fidelity fixture corpus. All removed, with bench:live:providers and their suite entries. Also removed tests/framework-fixtures/README.md's "External quality-eval fixtures" section: it documented a bench:live workflow using --fixture-dir, --agent=codex, --action and --evidence-bundle, none of which benchmark-live.mjs implements, plus an evidenceCapture block nothing reads. Kept: timing benchmarks (the public repo's half of that boundary), progressive publication, the source lock, poll lanes, and Nuxt/Vue component previews. Coverage note: deleting the isolated suites took the only tests for `source_locked` classification with them, so the plain wrapper path — now the only non-component preview — gets equivalent accept and discard coverage. Both new tests fail if mode:'error' is removed. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Flag inset stripes written with the two-length box-shadow form box-shadow takes <length>{2,4}: only the two offsets are required, so `inset 4px 0 red` is valid and paints the same single-edge stripe as `inset 4px 0 0 red`. The scan demanded a third length, so the short form was silently missed. Blur and spread now default to 0 when omitted, which is exactly the stripe shape the rule looks for. The neutral-color and blur/spread exclusions still hold: `inset 4px 0 #000` and `inset 4px 0 5px var(--brand-accent)` both pass. Fixture covers both orders of the short form plus those two exclusions, and fails against the previous regex. Third false negative found in this rule (after trailing `inset` and literal neutral colors), all from the same cause: the scan was written against one spelling of the syntax rather than the grammar. Prepared with AI assistance under maintainer direction. Co-Authored-By: Claude <noreply@anthropic.com> * Live: polling rework, source locks, preflight scaffolding, Vue previews Carved out of #371, minus progressive publication. Everything here works against real project source the way main's Live already does: the agent writes variants into the file the browser loaded, HMR fires, Accept promotes and carbonizes. Nothing is staged anywhere. Poll lanes. Events now carry an explicit priority: accept/discard/exit ahead of manual_edit_apply/steer/carbonize_cleanup ahead of generate. A long generate can no longer sit in front of the Accept the user just clicked. leaseEvent claims its lease before awaiting, so a slow prepare cannot hand the same event to two pollers. Source locks. A per-file mutex around every accept and discard path, keyed on a digest of the absolute path. Staleness is decided by owner-pid liveness rather than mtime, so a wedged lock clears when its owner dies instead of after an arbitrary timeout, and a slow-but-live accept is never stolen from. Only the owning process can release a lock. Preflight scaffolding. The server runs live-wrap (or live-insert) before the poll returns and hands the result back as event.scaffold. That walk is measured at ~7.6s on a large repo; moving it off the agent's critical path removes a deterministic tool round trip without touching the generated design. Falls back cleanly to the agent running the helper itself. Vue previews. previewMode: "vue-component" for Nuxt/Vue targets, matching the existing Svelte component path: variants compile as real SFCs from a dev-only directory so the route is never rewritten during generation, and Vite mounts them without invalidating page state. Accept is the only route write. Includes a Vue attr tokenizer that normalizes shorthand bindings (@x, :x, #x) to their canonical forms. Accept hardening. Every thrown failure now returns mode: 'error' rather than an ambiguous unhandled result, so a real failure is never classified as a deliberate manual handoff and silently dropped. The marker search skips node_modules/.git/dist/build/.impeccable. Shared CLI arg parsing extracted to scripts/lib/cli-args.mjs. Assisted-by: Claude Code * Drop the progressive benchmark, remove dead wrap scaffolding Review fallout from removing progressive publication. The Live benchmark existed to compare atomic against progressive delivery: compareModelBackedReports measures goToFirstVariantMs improvement of one over the other. With progressive gone it measures nothing against nothing. Worse, benchmark-live.mjs still passed `progressive` to bootFixtureSession, which no longer accepts it, so `--delivery progressive` was silently ignored and would have emitted reports labeled progressive that actually ran atomic. Silent wrong data is worse than a crash. It was built for progressive, so it goes with progressive: benchmark-live.mjs, its lib, its test, and the bench:live script. If an atomic latency baseline is wanted later, that is a smaller thing built on purpose. live-wrap.mjs: sourceOriginalLines was assigned and never read. Both found by review bots on #381 (Copilot). Assisted-by: Claude Code * Drop the Vue preview mode; it never reached Svelte's accept path Cursor found that inlineVueComponentAccept never receives paramValues, while the Svelte equivalent uses them in 23 places: Accept on a tuned Vue variant silently persisted the default and threw the user's tuning away. Chasing that corrected something I had asserted the other way round. I said Vue's raw-CSS-append was inherited from the Svelte path. It is not. svelte-component.mjs calls sanitizeAcceptedSvelteCss before writing, which sanitizes the CSS and bakes tuned params into it. vue-component.mjs had no sanitize step at all — it appended the variant's <style scoped> body into whatever style block came last, so a variant could leak CSS site-wide when the last block was global, and brace CSS landed in a lang="sass" block. Both are the same defect: the Vue mode mirrored Svelte's preview path without its accept-side subsystem (bakeParamValuesInCss, sanitizeAcceptedSvelteCss, appendSanitizedCssRule, rewriteAcceptedSvelteSelector, rewriteParamSelectors — roughly 200 lines of CSS rewriting). Both were introduced here, not inherited. A shipped Vue session could leak styles and discard tuning without saying so. So it comes out. The poll lanes, source locks, preflight scaffolding, and accept hardening do not depend on it and are worth landing now. Vue returns when its accept path reaches parity. The nuxt-vite7 fixture goes back to main's plain-wrapper shape. Assisted-by: Claude Code * Stop the lease redelivery test racing the scheduler CI failed `does not drop polled events until the agent acknowledges them` on a commit whose content was byte-identical to one that passed, which is the signature of a flake rather than a regression. The test leased an event for 50ms, then asserted a second poll saw a timeout because the lease was still held. That gave the whole second HTTP round trip a 50ms real-time budget: cross it and the lease expires, the event is redelivered, and the assertion fails for a scheduling hiccup instead of a bookkeeping bug. Locally it passed 6/6; a loaded runner is where it bites. Hold the lease for 1000ms so a round trip cannot cross it, and wait LEASE_MS + 300 before asserting redelivery, so each half has headroom in the direction it asserts. Verified by injecting a 60ms stall before the second poll: the old test fails with exactly the CI message, the new one passes. Assisted-by: Claude Code * Recover live sessions that reload past the generation done broadcast The preflight scaffold write (new in this PR) triggers a framework full-reload — Astro reloads the page for any .astro edit. When the agent's variant write and its done SSE land while the browser is mid-reload, the resumed page misses both the second HMR reload and the done broadcast: it comes back up on the scaffold-only source and waits in GENERATING at 0/N forever, with the finished variants sitting in source. This is the astro-vite7 CI timeout; the failure artifacts show the full sequence (scaffold at 26.319s, done at 26.515s, the new page's browser_resumed checkpoint at 26.653s, DOM still scaffold-only). Three-part fix: - session-store: agent_done now stamps a monotone generationCompletedAt on the snapshot. Browser checkpoints legitimately regress phase and arrivedVariants (a resumed page reports what it sees), so completion needed a field checkpoints cannot un-set. - live-browser: on every SSE (re)connect, compare the session summary's generationCompletedAt against local progress; when behind while GENERATING, pull the finished variants from source (same settle delay as the done handler's HMR-first fallback). Covers both orderings of resumed-checkpoint vs agent_done. Also, the source-fallback empty- wrapper branch no longer tears the session down mid-generation — a scaffold-only wrapper is a legitimate in-flight state, so stay in GENERATING instead of destroying a session the agent is still filling. - live-server: a browser checkpoint reporting generating/behind for a session whose generation already completed re-broadcasts the stored done (idempotent for every other tab), and connected-payload summaries expose generationCompletedAt for the browser-side check. Coverage: live-server unit tests for redelivery, the no-redelivery guard, and marker durability across checkpoint regression; plus a deterministic live-e2e scenario on astro-vite7 that blocks the reloaded page's SSE stream and mocks its HMR websocket dead until after the agent finishes, forcing the missed-broadcast window every run. All new tests fail against the pre-fix code. The e2e harness additionally gains an IMPECCABLE_E2E_ATOMIC_DELAY_MS lever (widens the scaffold-to-write window) and env-gated console/nav tracing (IMPECCABLE_E2E_CONSOLE=1) used to diagnose this. The hypothesis that preflight opens a wrapper-with-no-variants window came from Copilot's review sketch in the follow-up WIP PR; the killing mechanism differs from that sketch (nothing calls recoverEmptyCycling in the CI trace — the session hangs precisely because no code path runs at all), but the window is real and the guard it suggested is folded into the source-fallback fix. Assisted-by: Claude Code Co-Authored-By: Claude Code <noreply@anthropic.com> * Retry a completion-driven source fallback that reads only the scaffold Greptile flagged a hole in the previous commit's empty-scaffold guard: when a `done` has already been delivered, the source fallback gets exactly one read. If that read returns the preflight-only scaffold (a stale source view, or an agent whose write lands in multiple steps), the guard's silent return left the tab in GENERATING with no further event ever coming — the same stuck state the previous commit fixed, reintroduced through a different door. Callers that know generation finished (the done handler's fallback and the SSE-reconnect self-heal) now pass generationCompleted, and an empty read on that path re-reads the source up to 3 times before surfacing recoverEmptyCycling instead of hanging. Mid-generation callers are unchanged and still wait indefinitely — a real agent can legitimately take minutes between scaffold and write, and tearing that down was the original #385 hazard. The missed-done e2e scenario now also serves a captured scaffold-only copy for the first post-reconnect /source read, forcing the retry path every run. Verified failing against the pre-retry code (tab stranded in GENERATING, test timeout) and passing with it. Assisted-by: Claude Code Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
331540ddec |
Scope a single rule to a file with ignore-value "*" --file (#379)
* Scope a single rule to a file with ignore-value "*" --file
`ignore-file <glob>` was the only file-scoped escape the hook offered, and
it is far blunter than most findings justify: it silences every rule for
that path forever, including rules not written yet. A real UI surface with
one noisy rule had no proportionate option.
Add a file scope to `ignore-value`, so one rule can be turned off in
matching files while staying active everywhere else:
hooks ignore-value design-system-font-size "*" --file "src/widget.js"
- Refuse a bare `"*"` with no `--file`. Suppressing a rule project-wide is
`ignore-rule`'s job, and the error says so.
- Reject unknown `--flags` instead of folding them into the value.
`ignore-value overused-font Inter --shard` stored the value
"inter --shard", matched no finding, and reported success.
- Key dedup on the file scope too. The same rule/value legitimately
appears more than once with different scopes; the old rule+value key
silently overwrote the earlier entry.
- Keep normalizer key order (rule, value, files, createdAt, reason) in
step across both copies. Normalizing runs on every write, so emitting a
different order than what is on disk rewrites untouched entries.
- Lead with the narrow form in the hook's directive footer and hooks.md;
`ignore-file` is now documented as the whole-file-out-of-scope case.
Dogfoods it on skill/scripts/live-browser.js, where all 32 findings are
design-system-font-size: the overlay is injected over arbitrary host pages
and builds a self-contained UI, so DESIGN.md's ramp does not describe it.
The other rules stay live for that file.
Assisted-by: Claude Code
* Show the file scope in hooks status, and stop the wildcard error misdirecting
Two findings from Cursor.
status formatted every ignore value as rule=value and dropped files. Now
that the primary hooks path writes file-scoped `"*"` entries, that rendered
`design-system-font-size=*` — which reads as exactly the project-wide
wildcard this command refuses, the opposite of what is on disk. Print the
scope, matching the `rule=value [files]` shape `impeccable ignores list`
already uses. This repo's own config already carries several scoped
wildcards written through the CLI path, so status has been under-reporting
them.
The bare-wildcard refusal always pointed at `ignore-rule <rule>`. For
overused-font that command refuses on its own without --all-values, so the
guidance handed the user a second error. Name the flag for that rule.
Assisted-by: Claude Code
* Refuse an empty --file glob, and store multi-file scopes in canonical order
Two Copilot findings, both the silent-no-op class this PR exists to remove.
An empty glob was dropped by filter(Boolean). So
`ignore-value overused-font Inter --file=` reported "Added
overused-font=inter" and wrote an entry with no files: the user asked to
scope a rule to one file and silently got the project-wide suppression
instead — broader than what they asked for, reported as success. Refuse an
empty or whitespace glob on every form (--file, --file=, --files, --files=)
in both the hook-admin and CLI paths.
Multi-file scopes were deduped but not ordered, and the dedup key compares
the files array, so `--file b.css --file a.css` stored a second entry
distinct from `--file a.css --file b.css`. Sort at parse so storage is
canonical, and sort inside the key so entries already on disk in another
order still compare equal.
Assisted-by: Claude Code
* Sort files in every dedup key, not just two of the four
My previous commit sorted the file scope at parse time and inside
ignoreValueFilesKey, and stopped there. Cursor pointed out ignoreValueKey
(CLI) and ignoreValueEntryKey (hook-admin) still joined `files` in stored
order, so add/remove dedup missed any on-disk scope whose glob order
differed from the sorted argv form: a re-add duplicated the entry and a
remove silently failed.
Four functions hash `files`; I had fixed two. All four sort now. The
remaining `files.join(', ')` call sites are display, not keys.
Verified against a config seeded in non-sorted order, as an older client
would have written it: the re-add updates the existing entry rather than
duplicating it, and remove-value finds it. Test covers that shape.
Assisted-by: Claude Code
* Refuse a following flag as a --file glob
Cursor again, same class as the last two. requireGlob checked non-empty but
not whether the argv it consumed was itself a flag, so
`ignore-value design-system-font-size "*" --file --reason "why"` took
`--reason` as the scope, left "why" to fold into the value, stored
value="* why" files=["--reason"], and reported success. Garbage, announced
as done.
Refuse a glob starting with `--`, in both the hook-admin and CLI paths.
Assisted-by: Claude Code
|
||
|
|
428b86b139 |
Detect single-edge stripes painted with an inset box-shadow (#378)
* Detect single-edge stripes painted with an inset box-shadow
The side-tab rule caught bordered stripes but not the inset box-shadow spelling of
the same anti-pattern, which is how it usually reaches an Astro/CSS source file.
Adds a structural CSS scan for `box-shadow: inset` layers whose shape is a 3-12px
stripe on exactly one edge with no blur or spread, reusing the existing `side-tab`
rule id, so the rule count is unchanged.
Scoped narrowly, because a stripe is correct design in some places. It skips
selection and focus indicators (the rule's one documented exception), interactive
and semantic elements, narrow artwork, and neutral colors: `inset 4px 0 0 #000` is
a hairline, not an AI tell. Chromatic intent is read from the color literal or from
a `var(--token)` name.
Grammar rather than one spelling, learned the hard way — three of the four
false-negative shapes below were found only after the first pass shipped:
- `inset` is order-independent, so `4px 0 0 red inset` is the same stripe. Only a
standalone keyword is stripped, so `var(--inset-accent)` is not mangled.
- box-shadow takes <length>{2,4}: `inset 4px 0 red` omits blur and spread, which
default to 0. That is exactly the stripe shape.
- Authored CSS spells neutrals as `#000` / `black`, and shared/color.mjs only
parses the computed function forms a browser emits, deliberately reporting
anything else as chromatic. Routing authored colors through it flagged plain
black hairlines, so hex and named neutrals are handled before deferring.
- Comment bodies are blanked before matching, preserving byte offsets so line
numbers stay right, and the selector's line is taken from its first
non-whitespace character rather than the greedy match start.
Fixture covers 8 flag shapes and 13 pass shapes, including a literal-color column
that the original had none of, which is why the neutral bug survived review.
Prepared with AI assistance under maintainer direction.
Co-Authored-By: Claude <noreply@anthropic.com>
* Parse box-shadow layers by grammar, not by one spelling
Three review-bot findings, two of them the same mistake I had already made
twice in this rule.
Color-first layers were missed (greptile). `box-shadow` orders `inset`,
the lengths, and the color freely, so `red 4px 0 inset` and
`var(--brand-accent) 4px 0 0 inset` paint the stripe the length-first
regex was looking for and were skipped. That is the third valid spelling
this rule has missed after trailing `inset` and the two-length form, all
from encoding one spelling instead of the grammar. Stop patching
spellings: tokenize the layer, pick out `inset` and the 2-4 lengths in any
order, and treat the single remaining token as the color. Tokenizing is
paren-aware because `rgb(0 0 0)` is one color value whose channels would
otherwise read as lengths.
Neutral `rgb()` with space-separated channels was flagged (cursor).
shared/color.mjs parses only the comma form that getComputedStyle emits,
so an authored `rgb(0 0 0)` fell through it and reported chromatic — the
exemption isNeutralAuthoredColor exists for, missed. Parse both separators
before delegating. Left shared/color.mjs alone: it reads computed styles,
where the comma form is all a browser produces.
Line numbers were derived by re-slicing the whole prefix per rule, O(n^2)
on a large stylesheet (Copilot). Matches arrive in source order, so carry
a monotonic cursor: one pass total.
Fixtures cover both flag shapes and the neutral pass shape; all three fail
against the previous parse ("expected Color First Edge to flag", and
Space Rgb Neutral Edge appearing in the old flag list).
Assisted-by: Claude Code
* Fix the !important regression my tokenizer introduced, plus two cascade bugs
Three findings from Cursor on the grammar rewrite. The first is mine, from
the commit that claimed to end this bug class.
`!important` stopped flagging. Tokenizing split it into its own token, so
the color count came out at two and the layer was skipped — a shape the
regex it replaced handled correctly. `!important` qualifies the
declaration, not the shadow value, so strip it before reading layers.
Style-block findings reported one line low. block.startLine is the first
line after the <style> tag, but block.content begins at the character right
after that tag, so content's own line 1 sits on the tag's line. Passing
startLine - 1 to a 1-based line lookup counted that line twice. It is
startLine - 2. runRegexMatchers is unaffected and stays at startLine - 1
because it indexes its split lines from zero — verified by a fixture where
bounce-easing and side-tab share one block and now both report correctly.
Repeated declarations read the first, not the last. The cascade paints the
last, so `box-shadow: inset 4px 0 red; box-shadow: none` was flagged
though it paints nothing, and the reverse order was missed. Same for a
width override deciding the narrow-artwork skip.
Fixtures cover !important, both cascade orders, and the line-accuracy
shapes (multi-line block, single-line block, plain .css); they fail against
the previous commit.
Assisted-by: Claude Code
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
144cee5c36 |
Fix detector coverage for generated UI tells
Remove provider gating, share grid-background detection across source and rendered scan paths, and update the detector catalog and tests.\n\nAI-assisted: prepared by Codex at Paul's request. |
||
|
|
2b1f36c43e |
Add concept world catalog and review workflow
AI-assisted: prepared by Codex at Paul's request. |
||
|
|
77c7d8e0fc | Refine product and visual work lifecycle | ||
|
|
79d5294765 |
Fix: honor --target for nested products in non-monorepo repos (#377)
* Fix: honor --target for nested products in non-monorepo repos Closes #376. Resolve projectRoot from the target path when no monorepo marker is present, and inherit missing context files from the repo root when the active project is nested below it. Co-authored-by: Cursor <cursoragent@cursor.com> * Recognize nested-product context in .agents/context/ and docs/ fallback dirs Addresses PR #377 review: nearestTargetContextRoot only matched canonical PRODUCT.md/DESIGN.md directly in a directory, so nested products keeping context in the documented fallback locations were never selected. Reuse resolveLocalContextDir so the walk honors the same lookup order. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
bbed6eef08 |
Refresh the Impeccable product experience
Rework the landing page proof, steering demo, feature grid, slop catalog, detector coverage, theming, Live workflow, and responsive behavior.\n\nAI-assisted implementation by OpenAI Codex. |
||
|
|
8682c85c57 |
Fix Live side-tab validation gaps
Scan Astro style blocks for inset-shadow stripes, recognize semantically chromatic external tokens without flagging neutral unknowns, and make the polling generator run advisory detector checks before publication. Sync the affected detector bundles and add a paired regression fixture.\n\nAI-assisted: Codex analyzed the failed Live task, implemented the detector and generator changes, and ran the validation suites under maintainer direction. |
||
|
|
0ac1ca6867 |
Restore polling as the primary Live architecture
Default Codex back to one-shot foreground polling, delegate generation to the existing low-effort agent, and keep the app-server worker available only through an explicit experimental opt-in. Preserve progressive publication and the shared safety and framework optimizations. Prepared with Codex assistance under maintainer direction. |
||
|
|
ead6ddabe5 |
Preserve experimental Live app-server workstream
Snapshot the current app-server implementation, shared Live optimizations, generated harness output, and in-progress site work before restoring polling as the primary runtime path. Prepared with Codex assistance under maintainer direction. |
||
|
|
ed7a6fbe4e |
detector: text-occlusion + first-viewport-column-overflow (57 -> 59)
Two browser-engine quality rules, both warning severity. text-occlusion / element-overlap fires on three shapes: an opaque decorated box painted over a text element (elementFromPoint confirms real coverage, box >= 30%), one text run buried under another when at least one side is a positioned layer (text >= 45%, so line-box leading bleed between stacked flow blocks does not count), and an inline element whose opaque fill leaks past its line onto a neighbour (the class-name collision bug). A large headline whose edge overhangs a bounded content card is caught as an element collision even when the text stays on top. Gradient scrims, decorative SVG emblems, fixed/sticky overlays, floats, and raw image backdrops (contrast territory, deduped against the pixel low-contrast rule) are exempt. first-viewport-column-overflow fires when a multi-column opening section runs one column past 140% of the viewport while a sibling fits inside one screen, the stretched-hero signature. Single-column pages and full-page heroes with no fitting sibling are exempt. Validated: fires on the diagnosed repros, clean across a 60-sample sweep. Fixtures + browser tests added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |