* feat(site): scaffold Astro migration, convert 3 pages
Phase 1+2 of the Astro migration:
- Astro v6.2.1 installed, srcDir: 'site', static output to build/
- Shared layout: Base.astro (head, fonts, meta, slots), Header.astro
(star count in one place: 23k), Footer.astro
- CSS moved from public/css/ to site/styles/ (9 files, @import chains
resolve via Vite)
- Three pages converted: privacy, cases/neo-mirai, live-mode
(all return 200 on astro dev)
Remaining: designing, slop, homepage, content collections (docs),
JS migration, server/index.js deletion, build.js cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(site): migrate all 6 main pages to Astro
Converts the remaining pages:
- designing/index.html → site/pages/designing/index.astro (551 lines)
- slop/index.html → site/pages/slop/index.astro (909 lines)
- index.html → site/pages/index.astro (1278 lines, the homepage)
Base.astro gains OG meta tag props, before-header/after-header
slots (for grain overlay and section nav), and configurable mainId.
Homepage uses link tags to public/css/ instead of frontmatter CSS
imports to avoid esbuild choking on :has() in main.css. Curly
braces inside <code> elements (CSS snippets in changelog) escaped
with HTML entities to prevent Astro JSX expression parsing.
All 6 pages return 200 on astro dev. Branch: feat/astro-migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(site): content collections for docs and tutorials
Replaces the 1532-line build-sub-pages.js generator with Astro v6
content collections:
- 24 skill editorial files move to site/content/skills/
- 4 tutorial files move to site/content/tutorials/
- site/content.config.ts defines both collections with glob loaders
- site/pages/docs/[...slug].astro reads skills collection + command
metadata from source/skills/ at build time
- site/pages/docs/index.astro renders the command grid grouped by
category (create, evaluate, refine, simplify, harden, system)
- site/pages/tutorials/ mirrors the pattern with ordered index
- Doc.astro layout provides sidebar nav, breadcrumbs, and related-
command chips from the COMMAND_RELATIONSHIPS data
- Category/relationship data extracted to site/data/sub-pages-data.ts
All 15 tested pages return 200: 6 main pages + 5 docs + 2 tutorials
+ 2 index pages. The old generator is not yet deleted (Task #6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(site): move JS source from public/js/ to site/scripts/
Moves all 49 JS files (app.js + 48 in js/) into site/scripts/.
Vite now processes them through its module bundler instead of
serving them raw from public/.
app.js import paths updated from ./js/X to ./X (the js/ nesting
is gone since app.js now lives alongside the subdirectories).
Homepage and live-mode page switch from <script is:inline src="/app.js">
to Vite-processed <script> imports, so tree-shaking, bundling,
and minification happen automatically at build time.
public/js/ still exists for now (cleanup in Task #6) and the
generated/counts.js build output path needs updating there too.
@paper-design/shaders added to npm dependencies (was missing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(site): delete old Bun server, generator, and duplicated files
Cleanup after the Astro migration:
Deleted:
- server/index.js (233 lines, replaced by `astro dev`)
- scripts/build-sub-pages.js (1532 lines, replaced by content collections)
- scripts/lib/render-page.js (247 lines, replaced by Base.astro layout)
- content/site/partials/header.html (replaced by Header.astro component)
- public/index.html, privacy.html, designing/, live-mode/, cases/
(replaced by .astro pages in site/pages/)
- public/css/ (moved to site/styles/)
- public/js/ old source files (moved to site/scripts/)
- public/app.js (moved to site/scripts/app.js)
Kept in public/:
- antipattern-examples/ (standalone HTML demos, not Astro pages)
- antipattern-images/, assets/, neo-mirai/ (static assets)
- js/detect-antipatterns-browser.js (referenced by antipattern examples)
- js/generated/counts.js (build output from scripts/build.js)
- _data/api/ (generated API data, now written to public/ so Astro
passes it through to build/)
Updated:
- astro.config.mjs: added redirects (skills->docs, cheatsheet->docs,
gallery->slop, neon-mirai->neo-mirai, etc.)
- package.json: dev->astro dev, build->build:skills+build:site,
preview->astro preview
- scripts/build.js: removed buildStaticSite(), generateSubPages(),
static-asset copying. API data writes to public/_data/ instead of
build/_data/. Site-header validator is a no-op (shared component).
Em-dash validator scans site/components + site/layouts, not pages
(pages contain content from other sources like detector descriptions).
- .gitignore: removed public/slop/ entry
Tests: 186/186 pass. Skills build: clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(site): fix redirect config for Astro compatibility
Move the dynamic /skills/:id -> /docs/:id redirect to public/_redirects
(Cloudflare Pages native format) since Astro's redirect config can't
handle dynamic routes that don't match existing page patterns.
Remove duplicate trailing-slash redirect entries that caused warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(site): switch remaining pages from /css/ link tags to frontmatter imports
Doc.astro, docs/index, tutorials/index, and tutorials/[slug] were
still using <link href="/css/sub-pages.css"> which pointed at the
deleted public/css/ directory. Switched to frontmatter CSS imports
(import '../../styles/sub-pages.css') which Vite resolves from
site/styles/.
Homepage also switches from link tags to frontmatter imports for
main.css and sub-pages.css — the esbuild error that originally
forced the link-tag workaround was caused by unescaped curly braces
in the HTML content (since fixed), not by the CSS itself.
All pages verified visually in Chrome: homepage hero, foundation
grid, docs index (card grid with categories), docs detail (sidebar +
editorial content + visual mockups), designing (core loop diagram),
privacy, tutorials. Header renders with 23k stars on every page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(site): fix edge-to-edge sections, broken API paths, CSS links
Three fixes:
1. Homepage sections sat on the viewport edge because Base.astro's
<main> lacked the site-content class (provides max-width + padding).
Added mainClass prop to Base.astro; homepage sets mainClass="site-content".
2. "Failed to load commands" because app.js fetched /api/commands
which only existed in the old Bun server's routing. Updated to
fetch from /_data/api/commands.json (the static JSON files that
build:skills writes to public/_data/).
3. CSS reference fix (previous commit was incomplete): Doc.astro,
docs/index, tutorials pages all used <link href="/css/sub-pages.css">
pointing at deleted public/css/. Switched to frontmatter imports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(site): add sidebar to docs index page
The docs index was using Base.astro directly without the skills-layout
grid, so it rendered without a sidebar. Added the same sidebar structure
from Doc.astro (category-grouped command list) and wrapped the content
in the skills-layout grid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(site): extract footer CSS to shared file, import in Base.astro
Footer was unstyled on sub-pages because footer CSS lived only in
main.css (loaded by the homepage) not in sub-pages.css. Extracted
the 95 lines of footer rules into site/styles/footer.css and
imported it in Base.astro so every page gets footer styles regardless
of which page-specific CSS it loads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(demos): move landing-demo into repo, add as slop specimens
Moves ~/code/landing-demo/ into demos/landing-demo/ (without
node_modules or the redundant .claude/.agents skill copies — the
repo root's skill is found by walking up). PRODUCT.md, DESIGN.md,
DESIGN.json, PROMPT.md, and SCRIPT.md stay in place so running
Claude from demos/landing-demo/ picks up the project context.
Also copies both pages as slop specimens to public/antipattern-examples/
with the detector script baked in:
- new-slop-2026.html (Fraunces + warm cream editorial monoculture)
- old-slop-2022.html (purple gradient + glassmorphism + neon glow)
These can be linked from the slop page gallery alongside the
existing 11 synthetic specimens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(slop): replace single demo iframe with Then vs Now comparison
The "See it" section (01) on the slop page now shows two side-by-side
browser frames: 2022 slop (purple gradients, glassmorphism, neon glow)
and 2026 slop (Fraunces, warm cream, editorial restraint). Both run
the detector overlay live — hover either to see which rules fire.
Replaces the single visual-mode-demo.html iframe. Responsive: stacks
vertically on viewports below 900px.
Caption: "Same engine, different decade, both flagged."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(slop): switch to single-frame era toggle, center the section
Replaces the side-by-side dual-iframe layout with a single large
frame and a segmented 2022/2026 toggle. Clicking the toggle swaps
which iframe is visible (both pre-loaded, instant switch). Browser
chrome title updates to match the active era.
Centers the lede text and toggle above the frame for visual
cohesion with the full-width iframe below.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(slop): left-align See It section, toggle inline with lede
Moves the era toggle to the right of the lede paragraph using a
flex row (align-items: flex-end). Left-aligned text + right-docked
toggle matches the rest of the page's flow instead of standing out
as a centered island. Stacks vertically on narrow viewports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(slop): left-align iframe, remove max-width and auto margin
The visual-mode-preview had max-width: 1040px + margin: 0 auto
which centered it within the column. Override both in the
.slop-then-now context so the frame fills the full content width
flush with the text above. Caption left-aligned to match.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(site): update star count to 24k (24,062)
One file, one edit. The Astro migration working as intended.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): regenerate pnpm-lock.yaml for astro + shaders deps
Cloudflare Pages uses pnpm with frozen-lockfile. The lockfile was
stale after adding astro, @astrojs/cloudflare, and
@paper-design/shaders via npm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): resolve 3 bugbot review issues
1. Restore public/slop/ to .gitignore — prevents accidental legacy
generator output from conflicting with the Astro page.
2. Move astro and @paper-design/shaders to devDependencies — these
are site-build tools, not CLI runtime deps. Removes @astrojs/cloudflare
entirely (unused; static output mode needs no adapter).
3. Fix Astro wiping build:skills output — CF config (_headers,
_redirects, _routes.json) and API data now write to public/ so
Astro copies them through. Dist ZIPs copy to build/_data/dist/
as a post-build step (after Astro finishes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): merge duplicate devDependencies, use npx for astro CLI
The previous commit created a second devDependencies key in
package.json. JSON doesn't support duplicate keys — pnpm ignored
the first block (with astro), so `astro build` wasn't found.
Merged astro and @paper-design/shaders into the existing
devDependencies block. Changed `astro build/dev/preview` to
`npx astro build/dev/preview` so pnpm finds the local binary
on Cloudflare Pages (which doesn't add node_modules/.bin to PATH
by default).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(demos): remove private demo script and prompt from public repo
SCRIPT.md contained a detailed conference talk script with personal
delivery strategies, rehearsed Q&A answers, and venue details.
PROMPT.md contained the origin brief for the demo page. Neither
belongs in a public repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(build): gitignore generated public/ artifacts, consolidate redirects
1. Generated files written to public/ by build:skills (API data,
CF config, browser detector, counts.js) are now gitignored.
Prevents noisy diffs and merge conflicts from committed build
artifacts.
2. Removed duplicate redirects from astro.config.mjs. All redirects
now live in one place: the _redirects file generated by
scripts/build.js (which Cloudflare Pages processes natively).
Eliminates the dual-maintenance risk where the two sources
could drift apart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Star count was 21k on sub-pages and the header partial, 22k on the
homepage. Updated all seven source files to 23k (actual: 23,692).
Changelog section gains a curated "Highlights since v3.0" block
above the full version history, which now collapses behind a
disclosure toggle. Fixes the vertical bloat from 9 entries in
three weeks while keeping v3.0's anchor content visible.
Also fixes a nesting bug where an orphan </div> from the old
changelog-list wrapper prematurely closed the changelog-section,
breaking the two-column changelog+FAQ grid layout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The seven-lane list in Phase C departure mode was acting as a menu:
the model ran "furthest from editorial" as its selection criterion and
converged on Swiss-grid / Terminal / Industrial-signage every time.
Replaced with a brand-voice derivation process (read personality
words, imagine physical experiences, derive visual directions).
Explicitly names the failure mode so the model can't fall into it.
Phase D family-pass labels are now open-ended nouns, not a fixed
vocabulary list that re-anchored the same categories.
Reinforced parameter generation: Phase C (both modes) now requires
naming 2-3 parameter knobs alongside each variant during planning,
not as an afterthought. The freeform bias paragraph aligns with
the budget table (2-3 for large compositions, not 1-2) and frames
0-param heroes as mistakes, not judgment calls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SAFE_TAGS skipped <a> and <button> categorically to avoid noise on
inline links and unstyled controls. The blanket skip overshot: a
pill-style anchor or styled button with its own opaque background
was silently exempted from the contrast check, so a "Get started"
button with charcoal text on near-black background (~2:1) read as
fine to both the CLI and the browser overlay.
The bail in checkColors now permits <a> and <button> when they have
their own opaque background AND direct text. Inline links and bare
controls keep skipping. checkElementColorsDOM no longer short-circuits
before reaching checkColors so the exception fires on the browser path.
Adds readOwnBackgroundColor() helper to handle jsdom's missing
shorthand decomposition; falls back to parsing the inline style attr
when getComputedStyle returns empty (real browsers always decompose,
so the fallback is a no-op there).
Fixture gains four cases: pill-style <a> low-contrast (flag),
<button> low-contrast (flag), inline <a> with no own bg (pass),
pill-style <a> with high contrast (pass). Three new tests assert
the right flags fire and the no-regression cases stay clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The service worker's webNavigation.onCompleted handler only cleared
csInjected when DevTools was open. The popup-only flow never
registered the tab in devtoolsTabs, so a page reload left a stale
csInjected: true even though the content script had been destroyed.
The next popup scan saw the stale flag, skipped re-injection, and
silently sent its scan request to a tab with no listener — popup
UI got stuck on "Scanning..." indefinitely.
Reset is now unconditional (page reload always destroys the content
script regardless of which UI is open). Auto-rescan stays gated to
DevTools, since the popup is strictly user-driven.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructures live.md Step 4 into four phases: identity extraction
(Phase A, non-skippable, falls back to CSS variables and computed
styles when DESIGN.md is absent), default vs departure mode pick
(Phase B), variant planning by primary axis or aesthetic lane
(Phase C), and a family-level squint test before the sentence
pass (Phase D). Default mode preserves identity and varies
expression; departure mode only triggers on explicit signals
(PRODUCT.md anti-references calling out the current surface, or
the user prompt asking for departure).
Adds reflex-reject aesthetic lanes to brand.md as a parallel to
the existing font reflex-reject list. Editorial-typographic is
the first entry. Expands SKILL.md's category-reflex check to two
altitudes (theme + palette from category, then aesthetic family
from category + anti-references).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Fraunces, Geist (Sans/Mono), Mona Sans, Plus Jakarta Sans,
Space Grotesk, Recoleta, and Instrument Sans to OVERUSED_FONTS.
Brand-domain exceptions for Geist on vercel.com / nextjs.org /
v0.app and Mona Sans on github.com / githubnext.com.
Updates four should-pass fixtures that used the now-flagged faces
as "good examples"; switches to Newsreader + Karla. Adds positive
assertions for Fraunces and Geist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls the bold lead text from each <li><strong>...</strong> in the
changelog entry as a tweet-grade summary, fits as many bullets as
possible under the 280-char limit (first highlight always wins since
it's already the most user-facing line), and prints inside a labeled box
with a live char count so the user can copy-paste into @impeccable_ai.
Adds tweetHeader and tweetCta to each component config (skill / cli /
extension); skill uses the npx skills install line, CLI uses npm i -g,
extension drops the CTA entirely (link to the release page is enough
since the user has to upload to Chrome Web Store separately).
Falls back to header + URL only if even the first highlight overflows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a configurable lookup path for PRODUCT.md / DESIGN.md / DESIGN.json so
they don't have to live at the project root. Resolution order (first match
wins):
1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd)
2. cwd, when canonical or legacy files are at the root (back-compat)
3. Auto-fallback subdirs of cwd: .agents/context/ then docs/
4. cwd as a default "no context found" location
Existing layouts (PRODUCT.md / DESIGN.md at repo root) keep working unchanged
- step 2 preserves the current behaviour. The auto-fallback covers the two
most common conventions seen in the wild (.agents/context/ for AGENTS.md
auto-import setups, docs/ for the request in the issue) without needing any
configuration.
Changes:
- load-context.mjs: export resolveContextDir() and use it inside
loadContext(); add contextDir to the JSON output
- live-server.mjs: import resolveContextDir and read PRODUCT.md /
DESIGN.md / DESIGN.json from the resolved dir instead of process.cwd()
- SKILL.md: short note on the env var and fallback dirs in Setup -> Context
- tests/load-context.test.mjs: 19 cases covering env var, fallbacks,
legacy migration scope, and back-compat
Legacy .impeccable.md -> PRODUCT.md auto-migration stays scoped to cwd root;
fallback dirs are read-only as far as auto-rename is concerned.
Closes#119
* fix(live): switch live-poll to execFileSync, validate ids strictly
live-poll.mjs built the live-accept invocation with execSync and string
interpolation of event.id and event.variantId. Both fields originate in
the browser; validateEvent only checked truthiness, so shell metacharacters
in either field would land in the shell-parsed command.
Real exploitability is gated by the per-session token (loopback only,
unguessable UUID), so risk is low. The construction itself is structurally
unsafe though, and the fix is small.
- live-poll.mjs: execSync(string) → execFileSync('node', argv). Drops the
hand-rolled single-quote wrap for --param-values; execFileSync passes
each arg as a discrete argv slot, no shell parsing.
- live-server.mjs validateEvent: tighten id and variantId to match the
actual generator shapes (8 hex chars and 1-3 digit numeric strings).
Defense in depth so any value reaching downstream code is inert by
construction.
- live-server.test.mjs: add three regression tests covering accept/discard
rejection of shell-metachar ids and non-numeric variantIds. Update the
three existing fixture ids to match the new pattern.
Reported in #122.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: refresh pnpm-lock.yaml to match package.json
Cloudflare Pages runs pnpm install --frozen-lockfile and was failing on
ERR_PNPM_OUTDATED_LOCKFILE: the lockfile was missing entries for
@ai-sdk/anthropic, @ai-sdk/openai, @anthropic-ai/claude-agent-sdk,
@anthropic-ai/sdk, @google/genai, ai, modern-screenshot, zod, and had
stale specifiers for jsdom, marked, playwright, wrangler, puppeteer.
Drift was introduced when package.json was last edited without a lockfile
regen. Running pnpm install --lockfile-only resolves it; verified with
pnpm install --frozen-lockfile (clean install succeeds).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Paul Bakaus <paulbakaus@pauls-mbp-3.lan>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use tesslio/skill-review to run tessl skill review on changed skills and
post results on the PR. No Tessl account required; only GITHUB_TOKEN.
Made-with: Cursor
Co-authored-by: rohan-tessl <rohan-tessl@users.noreply.github.com>
The inspectElement function previously used manual replace() chains to
escape backslashes and single quotes in CSS selectors before passing
them to chrome.devtools.inspectedWindow.eval(). This escaping was
incomplete: selectors containing crafted sequences of special characters
(backticks, newlines, Unicode escapes) could break out of the string
literal and inject arbitrary JS into the inspected page context.
JSON.stringify produces a properly escaped JS string literal that handles
all special characters, eliminating the injection surface entirely.
Live-mode bug-fix release. Closes#114, with thanks again to @dergachoff.
- Live mode lands valid TSX through the wrap → preview → accept →
carbonize loop on Vite/Next React/TSX projects, including repeated
sibling branches and JSX `asChild` parents (Radix `<Title>` etc.).
- Wrap correctly disambiguates repeated identical-class siblings via
the picked element's textContent (`--text` flag).
- Carbonize CSS no longer double-wraps in nested template literals on
TSX targets.
- JSX accept/discard restores at the picked element's original indent;
relative depth between lines preserved through the round trip.
- Screenshot overlay during loading no longer flashes solid black on
default-background pages.
- live-inject's CSP-meta patch+revert byte-for-byte preserves
self-closing tag whitespace.
- live.md gained explicit guidance on `:scope` descendant combinators
(authoring trap), the new `--text` flag, JSX `<style>` template-
literal wrapping, and the agent-side abort path.
Cursor Bugbot review on 8660d3a flagged a real corruption bug:
> Multi-line self-closing div breaks depth tracking in expandReplaceRange.
> The forward div-depth walk applies openRe / selfCloseRe / closeRe
> per-line. A multi-line `<div\n className="spacer"\n/>` causes openRe
> to match the opener line but selfCloseRe fails on both lines because
> `/<div\b[^>]*\/\s*>/` requires the full tag on one line. Depth is
> permanently over-counted by 1, so the walk overshoots.
Trace on the JSX-marker-inside-wrapper layout:
- Inside the wrapped element, a multi-line `<div … />` increments depth
at the `<div` line and never decrements.
- Forward walk's depth never returns to 0 → end stays at block.end (the
inner marker comment) → replace range stops there.
- Wrapper's outer `</div>` is left orphaned in the file after
accept/discard, breaking the JSX. Worse: an unrelated subsequent
`<div className="next-card">…</div>` sibling gets its `</div>`
mis-counted as the wrapper close, and the depth walk corrupts further.
Fix: rewrite the forward walk on JOINED text instead of per-line. A
single regex `/<div\b[^>]*?(\/?)>|<\/div\s*>/g` spans newlines (because
`[^>]` matches `\n`), so it correctly identifies multi-line opens,
closes, AND self-closes. Convert the match offset back to a file line
index to set `end`. Walk-back logic for the wrapper opener is
unchanged.
Test coverage:
- New `expandReplaceRange handles multi-line self-closing <div />` test
in live-accept.test.mjs constructs the exact Bugbot scenario: a
multi-line `<div\n className="spacer"\n/>` inside the picked
element AND an unrelated `<div className="next-card">After</div>`
sibling right after. Asserts the discard removes ALL impeccable
markers / wrapper attrs, preserves the next-card sibling intact, and
the multi-line `<div />` survives inside the restored content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more Cursor Bugbot findings on commit 11dfad81:
1. `filterByText` short-text returned the wrong sentinel value.
When the trimmed snippet was shorter than 8 chars, the function
returned `candidates.slice()` (all candidates). The caller then
sees `filtered.length > 1` and fires `element_ambiguous` — exactly
the opposite of the documented short-text fallback ("caller falls
back to first-match," which corresponds to `filtered.length === 0`).
So any picker event with a short textContent on a page with multiple
matching siblings spuriously errored.
Fix: return `[]` for short text, matching the JSDoc.
2. `endLine` in the wrap output was wrong for multi-line picked elements.
`wrapperLines.length` counts ARRAY elements, but one element is a
`\n`-joined multi-line string (originalIndented). The actual
wrapper-region row count is `wrapperLines.length + (originalLines.length
- 1)`. Reporting `endLine = startLine + wrapperLines.length` placed
the boundary inside the wrapper for any multi-line pick, giving
downstream agents an incorrect range.
Fix: add the originalLines offset (matching what `insertLine` already
does after the prior commit).
Test coverage:
- `short --text falls back to first-match instead of erroneously firing
element_ambiguous` covers fix#1.
- `returns endLine that includes the multi-line original content offset`
covers fix#2 by wrapping a 5-line <section> in a real HTML file and
asserting the reported endLine points at the variants-end marker (and
the next line is </main>, proving no rows were missed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sanity-check on the live-inject unwrap path turned up a real round-trip
bug on HTML files that ship a `<meta http-equiv="Content-Security-Policy"
content="..." />` tag (the leading space before `/>` is the canonical
self-closing form).
Trace:
- The tag-finder regex (`<meta\s+([^>]*?)\/?>`) captures any whitespace
between the last attribute and the closing `/>` as part of `attrs`.
- patchCspMeta did `attrs.replace(content, newContent) + ' ' + marker`,
appending the marker AFTER that captured trailing whitespace. Result:
`...content="..." data-...="..."` — a double space inside attrs and
the original space-before-slash gone.
- revertCspMeta then strips the marker via `\s*${origAttr.full}`, which
greedily eats both spaces — so the round trip leaves `"/>` with no
space, even though the original was `" />`.
Fix: split off the trailing whitespace from `attrs` before patching,
splice the marker into the attribute body with a single leading space,
and re-append the original trailing whitespace. The marker-removal
regex then consumes exactly one space and the trailing space rides
through unchanged.
Test coverage:
- New `round-trips through CSP-meta patch and revert` test in
live-inject.test.mjs covers the canonical Vite shape (CSP meta with
` />`).
- Plus a `round-trips with insertAfter` test for symmetry — the existing
suite only covered insertBefore.
- Existing 4 round-trip tests (HTML, JSX layout, multi-file, column-0)
all still pass byte-for-byte.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Companion to the prior outer-indent fix. live-wrap.mjs's
`originalLines.map(l => indent + ' ' + l.trimStart())` calls
`trimStart()` on every line, which strips ALL leading whitespace and
collapses multi-line picked elements to a uniform indent. So a 6/8/6
shape like
<aside className="card">
<h1 className="hero-title">Hero</h1>
</aside>
was being reindented to 10/10/10 inside the wrapper, and on
accept/discard the round-trip restored 6/6/6 — the <h1> ended up at
its parent's depth instead of nested inside it.
Fix: extract `minLeadingSpaces(lines)` and strip only the COMMON
minimum across the picked lines before reindenting under the wrapper.
That mirrors how `deindentContent` on the accept side already works,
so wrap+accept now form a clean round-trip.
Test coverage:
- Expanded the indent regression test in live-accept.test.mjs to
also assert the inner `<h1>` at 8-space indent and the closing
`</aside>` at 6 — proving the relative depth survives wrap and
discard end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cursor Bugbot caught this on PR #118 review:
> JSX discard/accept restores content with wrong indentation. In the JSX
> path, `indent` is captured from `lines[block.start]` — the marker comment
> line inside the wrapper div, which is indented 2 extra spaces relative
> to the original element. But `expandReplaceRange` expands the replacement
> to include the outer `<div data-impeccable-variants>` wrapper, which sits
> at the original element's indent level. `deindentContent(original, indent)`
> restores content to the marker's deeper indent, so all restored lines end
> up 2 spaces deeper than the original element was.
I'd actually noticed the symptom during the live testing session ("some
odd indentation in card-2 after discard") and dismissed it as cosmetic.
Bugbot's analysis matches exactly.
Fix: anchor the deindent base on `replaceRange.start` instead of
`block.start`. For HTML the two are identical (markers sit outside the
wrapper), so HTML is unchanged. For JSX `replaceRange.start` is the
outer `<div>` at the original element's indent — correct base.
Also dropped a duplicate `expandReplaceRange` call in handleAccept that
the earlier edit left orphaned.
Test coverage:
- Two new regression tests in live-accept.test.mjs:
- `discard restores JSX content at the original indent` runs the
real wrap CLI and asserts the restored <aside> opener lands at
its original 6-space indent (was 8 before the fix).
- `accept (no carbonize, raw HTML) restores at the original indent
on JSX` exercises the same anchor on the accept path.
- Inner-element indent loss inside the wrapped content (`<h1>` ending
up at the same indent as its parent `<aside>`) is a separate,
pre-existing wrap behavior — left for a follow-up; explicitly
noted in the test comments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same alpha-string trap pattern as the recent detectPageTheme fix, on a
different code path. resolveCanvasBackground walks parents looking for
an opaque background; on a page that doesn't set its own bg the loop
runs out and fell through to:
return getComputedStyle(document.body).backgroundColor
|| getComputedStyle(document.documentElement).backgroundColor
|| '#ffffff';
`getComputedStyle(body).backgroundColor` for a default-bg page returns
the literal string "rgba(0, 0, 0, 0)" — non-empty, truthy — so the `||`
chain short-circuits to transparent-black instead of falling through to
'#ffffff'. modern-screenshot then composites the capture onto a black
canvas; the WebGL shader overlay flashes solid black until the shader
finishes loading.
Fix: drop the buggy fallback. The while-loop already covered <body> and
<html>; if neither is opaque the only sensible answer is the browser's
default canvas color (white).
Test coverage:
- New tests/live-browser-regression.test.mjs pins the anti-pattern
with a static-source check (live-browser.js is an IIFE with no module
exports, so this is the cheapest reliable regression guard). Also
pins the equivalent guard for detectPageTheme's readOpaque helper
added in the prior commit.
- Wired the new test file into `bun run test`'s explicit list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
While driving the new live loop end-to-end against the repeated-aside
fixture, --text disambiguation silently fell back to first-match instead
of landing on the picked card.
Root cause: `el.textContent` concatenates child text nodes without
inserting whitespace, so `<h1>Hero Two</h1><p>Second card body copy.</p>`
reads as "Hero TwoSecond card body copy." — but the source has whitespace
between </h1> and <p>. The single-space normalization on both sides
missed the join boundary; substring comparison failed; filterByText
returned [] and the caller fell through to first-match.
Fix: filterByText now compares both single-space AND no-whitespace
normalizations on each side, accepting the candidate if EITHER matches.
Bumped the minimum-target-length threshold from 6 to 8 to compensate
for the slightly looser comparison.
Plus two doc clarifications surfaced during the same session:
- live.md now warns that variant CSS using bare `:scope { ... }` styles
the variant wrapper div, not the picked element. Always use a
descendant combinator (`:scope > .card`, `:scope .hero-title`, etc.) —
the fake test agent's CSS is the canonical template.
- live.md documents the agent-side abort path. Aborting an in-flight
generate via `live-accept --discard` only mutates source — the browser
bar stays in GENERATING forever. Use `live-poll --reply EVENT_ID error
"msg"` instead so the browser receives the error SSE and resets.
Test coverage:
- New unit test in live-wrap.test.mjs covering the textContent-without-
inter-element-whitespace shape (three identical <aside> branches each
with <h1> + <p>, picks the second by --text).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#114.
Three orthogonal bugs that surfaced together when live mode picked an
element inside a Vite React/TSX component with sibling branches:
1. JSX wrapper insertion produced invalid TSX
- Replacing a single picked JSX child with [comment, <div>, comment]
yields three adjacent siblings, which oxc rejects with "Adjacent
JSX elements must be wrapped in an enclosing tag."
- A Fragment `<></>` solves the adjacency case but breaks
`cloneElement`-using parents (Radix `asChild`, Headless UI, etc.)
with "Invalid prop supplied to React.Fragment."
- Fix: keep the wrapper `<div data-impeccable-variants="ID">` as the
single JSX-slot child and tuck both marker comments INSIDE it.
accept/discard now expands its replacement range to include the
wrapper's `<div>` open/close lines via div-depth tracking.
2. carbonize produced nested template literals in TSX `<style>`
- extractCss captured `{` / `` `} `` lines from the agent's existing
`<style>{`…`}</style>` template, then handleAccept re-wrapped with
another pair, producing `<style>{`{`@scope…`}`}</style>` which oxc
rejects with "Expected `}` but found `@`".
- Fix: extractCss now strips a leading `{` and trailing `` `} ``
wherever they appear in the captured content (own line OR attached
to the first/last CSS line), so re-wrapping always yields exactly
one `{` ` … ` `}` pair.
3. Ambiguous source matching for repeated JSX branches
- `findElement` returned the first substring match. Multiple
`<aside className="card">` siblings all matched the same query, so
wrap silently landed on the first regardless of which one the user
picked.
- Fix: live-wrap accepts `--text TEXT` (the picked element's
textContent), collects ALL candidates via `findAllElements`, and
narrows by a tag-stripped, JSX-expression-stripped substring match.
Returns `element_ambiguous + candidates[]` when multiple branches
match equally; falls back to first-match when source uses dynamic
content (`<h1>{title}</h1>`) so existing flows aren't broken.
- The fake e2e agent now forwards `event.element.textContent` to
wrap, and live.md tells the agent to do the same.
Test coverage:
- New `vite8-react-tsx-repeated-aside` e2e fixture: three identical
`<aside>` branches, picks the second card's <h1>, runs the full
wrap → Go → cycle → accept → carbonize cycle on a real Vite + TSX
dev server, asserts that Hero One and Hero Three survive untouched
(proving wrap landed on the correct branch).
- Six new unit tests across live-wrap.test.mjs and live-accept.test.mjs
covering the Fragment-replacement design, both leading/trailing
template-literal placements, --text disambiguation, the dynamic-
content fallback, and the element_ambiguous error shape.
- New `runtime.assertSourceContains` fixture hook so other regression
fixtures can assert sibling-branch survivability cheaply.
All 186 unit + static-fixture tests pass; all 21 live e2e fixtures
(20 prior + new TSX) pass with no console errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the per-component tag prefixes, the changelog-label convention
that the release script matches against, the cleanliness gates, the
attached artifacts, and the manual post-release steps for the CLI
(npm publish) and the extension (Chrome Web Store upload).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The HTML changelog source lives 12 spaces deep inside its containers,
so list items emitted by htmlToMarkdown carried that indentation. Four
or more leading spaces in markdown is a code block, so all bullets
after the first (which the final .trim() rescued) rendered as code on
the GitHub release page. Strip leading whitespace per line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v3.0.3 was never installable as a distinct version: the manifest jumped
3.0.2 → 3.0.4 in a single commit (5f5e2b0), so plugin users picked up
the craft/shape hardening and the modal-host live-picker fix together
with the 3.0.4 work. Merging the changelog matches what actually
shipped and keeps a single GitHub release for that batch of changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- scripts/release.mjs tags and publishes GitHub releases for the three
independently versioned components (skill, cli, extension). Refuses on
dirty tree, unpushed HEAD, missing changelog entry, or stale build
outputs. Skill release attaches dist/universal.zip; extension release
runs build:extension and attaches dist/extension.zip. Prints a manual
next-step hint for npm publish (CLI) and Chrome Web Store upload.
- package.json: bump CLI to 2.1.8, add release:{skill,cli,ext} scripts.
- public/index.html: add CLI v2.1.8 changelog entry covering the
Windows path fix (#95) and border-radius detector hardening. Adopt
"CLI v" / "Extension v" prefix convention to disambiguate components
in the shared changelog timeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add qoder to FILE_DOWNLOAD_PROVIDER_CONFIG_DIRS so the download endpoint
accepts /api/download/skill/qoder/* and resolves to dist/qoder/.qoder/.
Without this, the website install surface returned 400 Invalid provider
even though qoder was a first-class harness everywhere else.
Cover the new provider with two assertions in download-validation.test.js
(allowlist + path resolution).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Windows checkouts, SKILL.md may have CRLF line endings, which caused
parseFrontmatter to fall back to {} and the build to prepend a fresh
frontmatter block while leaving the original one in the body. The result
was the dist Qoder SKILL.md shipping with two frontmatter blocks and
losing user-invocable / argument-hint / license / allowed-tools metadata.
Make the regex and YAML line split CRLF-tolerant. Regenerated tracked
.qoder/skills/impeccable/SKILL.md is now a single, well-formed
frontmatter block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PROVIDER_DIRS got the .qoder entry but the path-normalization regex in
normalizeForHash() did not. Without it, .qoder/skills/ paths in SKILL.md
are not collapsed to the synthetic .PROVIDER/skills/ token during
update-detection hashing, so a Qoder install can hash differently from
the upstream bundle and trigger spurious "update available" results
when symlinked provider dirs are involved.
Caught by Cursor Bugbot review on c6ca721.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the project-and-global cp install snippet (matches Pi/Rovo Dev
shape since Qoder supports both .qoder/skills/ and ~/.qoder/skills/
per docs.qoder.com/extensions/skills) and adds qoder.com to the
Supported Tools list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Qoder ships an Agent Skills system at .qoder/skills/{name}/SKILL.md with
slash-command invocation, mapping cleanly onto the existing transformer
pipeline. Adds Qoder as a 13th first-class harness:
- PROVIDER_PLACEHOLDERS entry in scripts/lib/utils.js (model, config_file,
ask_instruction, command_prefix) mirroring the Pi/Rovo Dev shape.
- PROVIDERS entry in scripts/lib/transformers/providers.js with
configDir=.qoder and the OpenCode/Claude Code frontmatter field set
(user-invocable, argument-hint, license, compatibility, metadata,
allowed-tools), since Qoder docs explicitly support those.
- transformQoder named export in scripts/lib/transformers/index.js for
test-spy parity (kept per CLAUDE.md guidance, even though build.js uses
PROVIDERS directly).
- .qoder added to PROVIDER_DIRS in bin/commands/skills.mjs so the CLI
detects existing Qoder installs.
- HARNESSES.md updated: official docs row, frontmatter support column,
directory structure row, and "Last verified" date bumped.
- DEVELOP.md reference link added.
- .github/ISSUE_TEMPLATE/feature_request.md and PULL_REQUEST_TEMPLATE.md
extended with Qoder in the provider checklists.
- Built .qoder/skills/impeccable/ tree committed (per CLAUDE.md harness
output dirs are tracked so npx skills can read them at install time).
The dynamic providers.test.js loop picks up Qoder automatically; all
non-prefix Qoder cases pass. The pre-existing Windows-only prefix-test
flake affects every provider equally and is out of scope for this PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#113.
Picker chrome could become unclickable inside Radix Dialog portals, and
clicking it dismissed the host dialog. Three orthogonal issues surfaced
during manual verification:
1. Modal-aware chrome
- Add `defangOutsideHandlers` and apply it to bar, picker, params
panel, annotation overlay, global bar, and design panel host.
- Sets `pointer-events: auto !important` on interactive chrome so
Radix's `body { pointer-events: none }` modal scroll-lock can't
silence our UI.
- Stops `pointerdown` / `mousedown` / `focusin` propagation at the
chrome boundary so DismissableLayer / FocusScope outside-handlers
never fire for clicks that land on us.
2. detectPageTheme: misread transparent body as black
- `getComputedStyle(body).backgroundColor` returns `rgba(0,0,0,0)`
when no bg is set; the prior regex captured (0,0,0) and ignored
alpha, calling every default-bg page "dark."
- Honor alpha, walk body → html, fall back to
`prefers-color-scheme` only when both are transparent.
3. Exit X invisible on host pages with `button { padding: ... }`
- Every other chrome button sets padding inline; exitBtn didn't.
Host resets like `button { padding: 0.5rem 1rem }` (in the new
fixture, common in the wild) inflated the 24x24 button into 56x40
and pushed the SVG into a non-rendering region — DevTools showed
the right styles, the X just didn't paint.
- Pin `padding: 0` + `box-sizing: border-box`, match the toggle
icon spec (14 / stroke 1.5 / textDim → text on hover).
4. Toast no longer obscures the global bar
- Position the toast above globalBarEl's actual rect instead of a
fixed bottom: 16px that overlapped the bar's bottom: 14px.
Test coverage: new `vite8-react-radix-dialog` fixture exercises the
full pick → Go → cycle → Accept loop with `@radix-ui/react-dialog`
+ `Portal` + `Overlay` + `Content`. Without the fix, clicking Go
dismisses the dialog and unmounts the picked element. All 20 live
e2e fixtures pass; all 180 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Added tests/windows-path-fix.test.js to package.json's test script so
the regression suite actually runs in CI; without this the file lived
on disk but no command picked it up. Verified with bun run test:
170 bun tests / 3 files, all green.
- Rebased onto current main. The PR's second hunk (live-mode browser
script load) no longer applies because that code path was extracted
into source/skills/impeccable/scripts/live-*.mjs during the live-mode
rewrite. The remaining puppeteer site at line 2700 still had the bug
and now uses fileURLToPath, matching the PR's intent.
- The added test file's mix of ESM imports + require/__dirname runs
cleanly under Bun's test runner; left as-is to preserve the PR's
authorship.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Windows, `new URL(import.meta.url).pathname` returns `/C:/...`
(with a leading slash). Passing that to `path.resolve()` or
`path.join()` causes Node to prepend the drive letter again, producing
doubled paths like `C:\C:\Users\...\detect-antipatterns-browser.js`.
Replace both occurrences (puppeteer scan at ~L2690 and live detect at
~L3506) with `fileURLToPath(import.meta.url)` from `node:url`, which
correctly strips the leading slash on Windows while remaining a no-op
on POSIX.
Add regression tests verifying the source no longer uses the raw
`.pathname` accessor for local path construction and that
`fileURLToPath` handles both Windows and POSIX file URLs correctly.
Closes#95
parseRadiusToPx("50%", 0) used to return 0, and resolveBorderRadiusPx's
"if (fromComputed !== null) return fromComputed" guard short-circuited
with that 0 before ever consulting longhand / inline / stylesheet
fallbacks. Callers that gate on `> 0` (border-accent-on-rounded and
isCardLike's hasRadius) silently lost findings the old
parseFloat(style.borderRadius) === 50 heuristic happened to keep.
In jsdom this is reachable any time style.width resolves to "auto" or
an empty string — parseFloat yields NaN, the `|| 0` fallback turns it
into 0, and any percent radius collapses to nothing. Real-world cards
with `width: 100%` hit this on every load.
Fix: when widthPx is 0 / missing, return the raw percentage number
instead. The percent-to-px conversion only makes sense with a width
reference; without one, the value still serves as a positive presence
signal for boolean checks. The icon-tile circle exclusion is
unaffected because that rule already gates on `siblingWidth >= 32`.
Caught by Cursor Bugbot on PR #115.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds resolveBorderRadiusPx(el, style, widthPx, win), a helper that walks
computed style → longhand → inline DOM → raw style attribute → matching
stylesheet rules to recover a pixel value, converting % to px when
needed.
Three jsdom adapter sites now use it: checkElementBorders (via a new
optional resolvedRadius param threaded from detectHtml), the icon-tile
sibling check in checkElementIconTile, and isCardLike's hasRadius gate.
Browser DOM adapters hit the fast path on the first line since real
getComputedStyle resolves both shorthand and percentages.
Background: from jsdom 29.0.2 onward, getComputedStyle(el).borderRadius
returns "" for the shorthand and "0" for longhand reads when the rule
used the shorthand. checkIconTile relied on parseFloat(borderRadius) >=
width/2 to exclude circular avatars; that comparison broke and circles
got false-flagged as icon-tile-stack. jsdom 29.1.0 has a separate
parser crash on <h*> + linear-gradient inline style which keeps the
pin at exactly 29.0.0 for now, but landing the helper means we can
move forward as soon as the gradient crash is fixed upstream without
touching detector code again.
The change is also strictly more correct than the old parseFloat
approach: percentage values now convert to actual pixel sizes, so
checkIconTile no longer relies on parseFloat("50%") == 50 happening
to satisfy `>= width/2` only for elements <= 100px wide.
bun run test passes (174/174); bun run build:browser and
bun run build:extension regenerated to mirror the helper into
bundled artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- @ai-sdk/anthropic 3.0.69 → 3.0.71
- @anthropic-ai/claude-agent-sdk 0.2.110 → 0.2.119
- ai 6.0.162 → 6.0.168
- playwright 1.58.2 → 1.59.1
- wrangler 4.75.0 → 4.85.0
- puppeteer 24.39.1 → 24.42.0 (optional)
- marked range floor bumped to 16.4.2 (already installed)
jsdom is intentionally pinned to exact 29.0.0. From 29.0.2 onward,
getComputedStyle(el).borderRadius returns "" (empty string) instead
of "50%" for percentage values that the engine can't resolve to px
without layout. checkIconTile relies on parseFloat(borderRadius) ≥
width/2 to exclude circular avatars; with the empty string, all
circles get re-flagged as icon-tile-stack. Real browsers resolve
the percentage so the public-site overlay and Chrome extension are
unaffected — only the Node/jsdom path used by `npx impeccable detect`
on HTML files breaks. Hardening the detector to read raw stylesheet
rules as a fallback is a follow-up; pinning is the safe move today.
Skipped: marked 16 → 18 (major bump, unrelated to this work).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
package.json was updated to ^0.91.1 in d26ccac (live-mode E2E LLM
agent), but the lockfile was not committed alongside. The next
bun install bumped @anthropic-ai/sdk from 0.81.0 to 0.91.1 to match
the declared range. The nested resolution under
@anthropic-ai/claude-agent-sdk stays at 0.81.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Marketplace source moved from "./" to "./plugin", a thin generated
subtree containing only the plugin manifest and the impeccable skill.
Per-version plugin cache shrinks ~378× (~770 KB instead of ~291 MB),
and the lockfile is no longer included in the source path so the
cache extraction never runs bun install. (#107)
- skills field in plugin.json now ends with a trailing slash to match
the documented schema (code.claude.com/docs/en/plugins-reference,
every directory example uses ./path/). Three reporters converged on
this fix because Claude Code's plugin loader skips command
registration on some setups when the slash is missing. (#86)
- Anti-patterns maintenance agent moved out of .claude/agents/ into
CLAUDE.md / AGENTS.md as concise inline guidance, since it is
repo-internal dev workflow, not user-facing. The plugin was also
the only place this agent was exposed to install users.
- Skills version bumped to 3.0.2 so existing users pick up the new
install path on next /plugin update.
- Top-level harness directories (.claude/skills/, .cursor/skills/, ...)
intentionally stay where they are; npx skills add reads them
directly from the GitHub repo and that path is unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>