diff --git a/.agents/skills/impeccable/SKILL.md b/.agents/skills/impeccable/SKILL.md index 78a7a5b74..962b0405c 100644 --- a/.agents/skills/impeccable/SKILL.md +++ b/.agents/skills/impeccable/SKILL.md @@ -35,7 +35,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. Two hard typographic ceilings you currently miss: - Hero clamp() max ≤ 6rem. 8–11rem (128–176px) reads as comically loud, not bold. @@ -158,7 +157,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .agents/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .agents/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.agents/skills/impeccable/scripts/context-signals.mjs b/.agents/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.agents/skills/impeccable/scripts/context-signals.mjs +++ b/.agents/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.claude/skills/impeccable/SKILL.md b/.claude/skills/impeccable/SKILL.md index 589bee044..435478717 100644 --- a/.claude/skills/impeccable/SKILL.md +++ b/.claude/skills/impeccable/SKILL.md @@ -41,7 +41,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -152,7 +151,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .claude/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .claude/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.claude/skills/impeccable/scripts/context-signals.mjs b/.claude/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.claude/skills/impeccable/scripts/context-signals.mjs +++ b/.claude/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.cursor/skills/impeccable/SKILL.md b/.cursor/skills/impeccable/SKILL.md index 5b9c9f3df..5231e4cf6 100644 --- a/.cursor/skills/impeccable/SKILL.md +++ b/.cursor/skills/impeccable/SKILL.md @@ -37,7 +37,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -148,7 +147,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .cursor/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .cursor/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.cursor/skills/impeccable/scripts/context-signals.mjs b/.cursor/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.cursor/skills/impeccable/scripts/context-signals.mjs +++ b/.cursor/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.gemini/skills/impeccable/SKILL.md b/.gemini/skills/impeccable/SKILL.md index 931e402b9..3eb3f8ad7 100644 --- a/.gemini/skills/impeccable/SKILL.md +++ b/.gemini/skills/impeccable/SKILL.md @@ -36,7 +36,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -149,7 +148,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .gemini/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .gemini/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.gemini/skills/impeccable/scripts/context-signals.mjs b/.gemini/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.gemini/skills/impeccable/scripts/context-signals.mjs +++ b/.gemini/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.github/skills/impeccable/SKILL.md b/.github/skills/impeccable/SKILL.md index c107bc2a7..6bb7d8d49 100644 --- a/.github/skills/impeccable/SKILL.md +++ b/.github/skills/impeccable/SKILL.md @@ -39,7 +39,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -150,7 +149,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .github/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .github/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.github/skills/impeccable/scripts/context-signals.mjs b/.github/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.github/skills/impeccable/scripts/context-signals.mjs +++ b/.github/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.kiro/skills/impeccable/SKILL.md b/.kiro/skills/impeccable/SKILL.md index 4aa5b2679..1effd6acf 100644 --- a/.kiro/skills/impeccable/SKILL.md +++ b/.kiro/skills/impeccable/SKILL.md @@ -37,7 +37,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -148,7 +147,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .kiro/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .kiro/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.kiro/skills/impeccable/scripts/context-signals.mjs b/.kiro/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.kiro/skills/impeccable/scripts/context-signals.mjs +++ b/.kiro/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.opencode/skills/impeccable/SKILL.md b/.opencode/skills/impeccable/SKILL.md index 7cc0abe66..c0b1c10df 100644 --- a/.opencode/skills/impeccable/SKILL.md +++ b/.opencode/skills/impeccable/SKILL.md @@ -41,7 +41,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -152,7 +151,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .opencode/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .opencode/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.opencode/skills/impeccable/scripts/context-signals.mjs b/.opencode/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.opencode/skills/impeccable/scripts/context-signals.mjs +++ b/.opencode/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.pi/skills/impeccable/SKILL.md b/.pi/skills/impeccable/SKILL.md index 8a066527d..524189233 100644 --- a/.pi/skills/impeccable/SKILL.md +++ b/.pi/skills/impeccable/SKILL.md @@ -39,7 +39,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -150,7 +149,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .pi/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .pi/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.pi/skills/impeccable/scripts/context-signals.mjs b/.pi/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.pi/skills/impeccable/scripts/context-signals.mjs +++ b/.pi/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.qoder/skills/impeccable/SKILL.md b/.qoder/skills/impeccable/SKILL.md index 9afbff322..0c72b0b23 100644 --- a/.qoder/skills/impeccable/SKILL.md +++ b/.qoder/skills/impeccable/SKILL.md @@ -41,7 +41,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -152,7 +151,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .qoder/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .qoder/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.qoder/skills/impeccable/scripts/context-signals.mjs b/.qoder/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.qoder/skills/impeccable/scripts/context-signals.mjs +++ b/.qoder/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.rovodev/skills/impeccable/SKILL.md b/.rovodev/skills/impeccable/SKILL.md index ac7e279ee..7472c7ad1 100644 --- a/.rovodev/skills/impeccable/SKILL.md +++ b/.rovodev/skills/impeccable/SKILL.md @@ -41,7 +41,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -152,7 +151,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .rovodev/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .rovodev/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.rovodev/skills/impeccable/scripts/context-signals.mjs b/.rovodev/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.rovodev/skills/impeccable/scripts/context-signals.mjs +++ b/.rovodev/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.trae-cn/skills/impeccable/SKILL.md b/.trae-cn/skills/impeccable/SKILL.md index fd2c6d784..14c898041 100644 --- a/.trae-cn/skills/impeccable/SKILL.md +++ b/.trae-cn/skills/impeccable/SKILL.md @@ -39,7 +39,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -150,7 +149,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .trae-cn/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .trae-cn/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.trae-cn/skills/impeccable/scripts/context-signals.mjs b/.trae-cn/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.trae-cn/skills/impeccable/scripts/context-signals.mjs +++ b/.trae-cn/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/.trae/skills/impeccable/SKILL.md b/.trae/skills/impeccable/SKILL.md index b2cf7deb4..a2cc9f87a 100644 --- a/.trae/skills/impeccable/SKILL.md +++ b/.trae/skills/impeccable/SKILL.md @@ -39,7 +39,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -150,7 +149,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .trae/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .trae/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/.trae/skills/impeccable/scripts/context-signals.mjs b/.trae/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/.trae/skills/impeccable/scripts/context-signals.mjs +++ b/.trae/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/plugin/skills/impeccable/SKILL.md b/plugin/skills/impeccable/SKILL.md index 589bee044..435478717 100644 --- a/plugin/skills/impeccable/SKILL.md +++ b/plugin/skills/impeccable/SKILL.md @@ -41,7 +41,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. #### Layout @@ -152,7 +151,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node .claude/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node .claude/skills/impeccable/scripts/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/plugin/skills/impeccable/scripts/context-signals.mjs b/plugin/skills/impeccable/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/plugin/skills/impeccable/scripts/context-signals.mjs +++ b/plugin/skills/impeccable/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/skill/SKILL.src.md b/skill/SKILL.src.md index 08eb358e8..f78f889c8 100644 --- a/skill/SKILL.src.md +++ b/skill/SKILL.src.md @@ -40,7 +40,6 @@ Produce ready-to-ship, production-grade code, not prototypes or starting points. - No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. - Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. - Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". -- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. Two hard typographic ceilings you currently miss: @@ -171,7 +170,7 @@ Plus two management commands: `pin ` and `unpin `, detailed be - `devServer.running` true → `live` is available for in-browser iteration; if false, don't lead with `live`. - Otherwise group by intent exactly as init's "Recommend starting points" step does (build new / improve what's there / iterate visually), tailored to `setup.register`. - **If `scan.detectTarget` is set, run `node {{scripts_path}}/detect.mjs --json ` once** (the bundled detector: no network, no npx, full coverage) and fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors, skip it and recommend the user run `audit` themselves; never block the suggestion on it. + **If `scan.targets` is non-empty, run `node {{scripts_path}}/detect.mjs --json ` once** (the bundled detector over local files: no network, no npx). `scan.via` tells you what they are: `git-changes` (the markup/style files in your dirty tree, the most relevant set), `source-dir` (e.g. `src`, `app`), `html`, or `root`. Fold the hits into your picks: many quality / contrast hits → `audit` or `polish`; a specific slop family → the matching command (gradient text or eyebrows → `quieter` / `typeset`, flat or gray palette → `colorize`, and so on). It's a real, current signal that beats guessing. If detect errors or the tree is large and slow, skip it and recommend the user run `audit` themselves; never block the suggestion on it. Keep it to 2-3 pointed picks with the exact command to type. The menu stays the fallback; the recommendation is the lede. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. diff --git a/skill/scripts/context-signals.mjs b/skill/scripts/context-signals.mjs index 1792d1a19..c03843812 100644 --- a/skill/scripts/context-signals.mjs +++ b/skill/scripts/context-signals.mjs @@ -150,35 +150,45 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +// Extensions the detector scans (mirrors the engine's walkDir set + HTML). +const SCANNABLE_EXT = new Set([ + '.html', '.htm', '.css', '.scss', + '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro', +]); +// Where UI source typically lives. The detector walks these and skips +// node_modules / dist / build / .next / .nuxt automatically. +const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public']; + /** - * What the agent could point the bundled detector (`detect.mjs`) at. The - * detector is HTML/CSS oriented, so a rendered page (dev server) or a static - * HTML entry is a far better target than a raw source tree. This script does - * NOT run the detector itself — it just surfaces the target so the agent can - * run `node /detect.mjs --json ` (bundled, dep-free, fast) - * and fold the hits into its recommendation. + * Local paths the agent should point the bundled detector at — never a URL. + * A URL means a costly Puppeteer browser render, and a probed dev-server port + * may not even belong to this project. An HTML *file* or a source tree is + * scanned by the cheap, jsdom-free static engine. This script does NOT run the + * detector; it just surfaces the target(s) so the agent can run + * `node /detect.mjs --json ` and fold the hits in. */ -function scanTarget(cwd, devServer) { - if (devServer.running && devServer.ports.length) { - return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; +function scanTargets(cwd, git) { + // 1. Dirty tree wins: scan exactly the markup/style files in flight. It's + // what the user is working on, it's a small set, and it's local. + if (git.isRepo && git.changedFiles.length) { + const changed = git.changedFiles + .filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase())) + .filter((f) => fs.existsSync(path.join(cwd, f))); + if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' }; } - for (const c of ['index.html', 'public/index.html', 'dist/index.html', 'build/index.html']) { - if (fs.existsSync(path.join(cwd, c))) return { detectTarget: c, via: 'html' }; - } - for (const dir of ['.', 'public', 'dist', 'build']) { - try { - const abs = path.join(cwd, dir); - if (!fs.existsSync(abs)) continue; - const html = fs.readdirSync(abs).find((f) => f.endsWith('.html')); - if (html) return { detectTarget: path.join(dir, html), via: 'html' }; - } catch { /* ignore unreadable dir */ } - } - return { detectTarget: null, via: null }; + // 2. Otherwise scan the local source dirs that exist. + const dirs = SOURCE_DIRS.filter((d) => fs.existsSync(path.join(cwd, d))); + if (dirs.length) return { targets: dirs, via: 'source-dir' }; + // 3. A root HTML entry, or the project root as a last resort when there's + // code but no conventional source dir (walkDir still skips heavy dirs). + if (fs.existsSync(path.join(cwd, 'index.html'))) return { targets: ['index.html'], via: 'html' }; + if (hasCode(cwd)) return { targets: ['.'], via: 'root' }; + return { targets: [], via: null }; } export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); - const devServer = await devServerSignals(); + const git = gitSignals(cwd); return { setup: { hasProduct: ctx.hasProduct, @@ -189,9 +199,9 @@ export async function gatherSignals(cwd = process.cwd()) { register: extractRegister(ctx.product), }, critique: { latest: latestCritique(cwd) }, - git: gitSignals(cwd), - devServer, - scan: scanTarget(cwd, devServer), + git, + devServer: await devServerSignals(), + scan: scanTargets(cwd, git), }; } diff --git a/tests/context-signals.test.mjs b/tests/context-signals.test.mjs index 28f778b97..030c443dc 100644 --- a/tests/context-signals.test.mjs +++ b/tests/context-signals.test.mjs @@ -109,25 +109,36 @@ describe('gatherSignals', () => { assert.ok(Array.isArray(s.devServer.ports)); }); - it('points scan.detectTarget at an HTML entry when one exists', async () => { - write('public/index.html', 'x'); + it('targets a local source dir (never a URL), even with a dev server up', async () => { + write('src/App.tsx', 'export default 1;'); const s = await gatherSignals(scratch); - // A live dev server (if one happens to run on the host) wins; otherwise - // the static HTML entry is the target. - if (!s.devServer.running) { - assert.equal(s.scan.detectTarget, 'public/index.html'); - assert.equal(s.scan.via, 'html'); - } else { - assert.equal(s.scan.via, 'dev-server'); - } + assert.equal(s.scan.via, 'source-dir'); + assert.deepEqual(s.scan.targets, ['src']); + // No target is ever an http(s) URL. + assert.ok(s.scan.targets.every((t) => !/^https?:/.test(t))); }); - it('has a null scan.detectTarget when there is nothing scannable', async () => { + it('prefers the dirty tree: scans changed markup/style files', async () => { + const { execFileSync } = await import('node:child_process'); + const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' }); + git('init', '-q'); + git('config', 'user.email', 't@example.com'); + git('config', 'user.name', 'Test'); + write('src/Hero.tsx', 'export const Hero = () => null;\n'); + write('README.md', 'x\n'); + git('add', '.'); + git('commit', '-qm', 'init'); + write('src/Hero.tsx', 'export const Hero = () => 2;\n'); // dirty + write('README.md', 'y\n'); // dirty but not scannable const s = await gatherSignals(scratch); - if (!s.devServer.running) { - assert.equal(s.scan.detectTarget, null); - assert.equal(s.scan.via, null); - } + assert.equal(s.scan.via, 'git-changes'); + assert.deepEqual(s.scan.targets, ['src/Hero.tsx']); // README.md filtered out + }); + + it('has empty scan.targets only when there is no code at all', async () => { + const s = await gatherSignals(scratch); + assert.deepEqual(s.scan.targets, []); + assert.equal(s.scan.via, null); }); });