From 772aa73aa3f0566c33bb62a3b9c4f37ac0bd7035 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Thu, 28 May 2026 15:23:09 -0700 Subject: [PATCH] feat(skill): make bare /impeccable context-aware (re: #159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reshape of the "/impeccable suggest" proposal in #159. Instead of adding a 24th command (menu pollution + the command-add tax + its own discoverability problem), upgrade the path users already hit: bare `/impeccable` with no argument. - New skill/scripts/context-signals.mjs gathers cheap, deterministic signals (setup gaps, register, latest cached critique score, git change scope, a dev-server port probe, and a `scan.detectTarget` for the detector) and emits JSON. It does NOT score or rank, and it does NOT run the detector itself (the engine isn't importable in an installed skill, and shelling npx+jsdom would risk a hang) — the agent reasons over the raw signals. - SKILL.md routing rule 1 now leads with the 2-3 highest-value next commands, each with a reason from the signals, then the full menu. Never auto-runs; always confirms. Reuses init's "Recommend starting points" vocabulary. When a project has never been critiqued it offers critique; when scan.detectTarget is set it runs `npx impeccable detect --fast --json` and folds the hits in. - Export extractRegister from context.mjs for reuse. Stays 23 commands; no metadata/pin/site-data changes. Unit-tested, including a regression guard for porcelain leading-space path parsing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .agents/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .claude/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .cursor/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .gemini/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .github/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .kiro/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .opencode/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .pi/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .qoder/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .rovodev/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .trae-cn/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- .trae/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- plugin/skills/impeccable/SKILL.md | 3 ++ .../impeccable/scripts/context-signals.mjs | 30 ++++++++++++++++++- skill/SKILL.src.md | 3 ++ skill/scripts/context-signals.mjs | 30 ++++++++++++++++++- tests/context-signals.test.mjs | 21 +++++++++++++ 29 files changed, 469 insertions(+), 14 deletions(-) diff --git a/.agents/skills/impeccable/SKILL.md b/.agents/skills/impeccable/SKILL.md index ca57daafc..78a7a5b74 100644 --- a/.agents/skills/impeccable/SKILL.md +++ b/.agents/skills/impeccable/SKILL.md @@ -152,11 +152,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `$impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.agents/skills/impeccable/scripts/context-signals.mjs b/.agents/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.agents/skills/impeccable/scripts/context-signals.mjs +++ b/.agents/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.claude/skills/impeccable/SKILL.md b/.claude/skills/impeccable/SKILL.md index c61880528..589bee044 100644 --- a/.claude/skills/impeccable/SKILL.md +++ b/.claude/skills/impeccable/SKILL.md @@ -146,11 +146,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.claude/skills/impeccable/scripts/context-signals.mjs b/.claude/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.claude/skills/impeccable/scripts/context-signals.mjs +++ b/.claude/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.cursor/skills/impeccable/SKILL.md b/.cursor/skills/impeccable/SKILL.md index 83dcdfbd9..5b9c9f3df 100644 --- a/.cursor/skills/impeccable/SKILL.md +++ b/.cursor/skills/impeccable/SKILL.md @@ -142,11 +142,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.cursor/skills/impeccable/scripts/context-signals.mjs b/.cursor/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.cursor/skills/impeccable/scripts/context-signals.mjs +++ b/.cursor/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.gemini/skills/impeccable/SKILL.md b/.gemini/skills/impeccable/SKILL.md index 2e753039f..931e402b9 100644 --- a/.gemini/skills/impeccable/SKILL.md +++ b/.gemini/skills/impeccable/SKILL.md @@ -143,11 +143,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.gemini/skills/impeccable/scripts/context-signals.mjs b/.gemini/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.gemini/skills/impeccable/scripts/context-signals.mjs +++ b/.gemini/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.github/skills/impeccable/SKILL.md b/.github/skills/impeccable/SKILL.md index 6ee98f389..c107bc2a7 100644 --- a/.github/skills/impeccable/SKILL.md +++ b/.github/skills/impeccable/SKILL.md @@ -144,11 +144,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.github/skills/impeccable/scripts/context-signals.mjs b/.github/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.github/skills/impeccable/scripts/context-signals.mjs +++ b/.github/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.kiro/skills/impeccable/SKILL.md b/.kiro/skills/impeccable/SKILL.md index d0e6e0528..4aa5b2679 100644 --- a/.kiro/skills/impeccable/SKILL.md +++ b/.kiro/skills/impeccable/SKILL.md @@ -142,11 +142,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.kiro/skills/impeccable/scripts/context-signals.mjs b/.kiro/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.kiro/skills/impeccable/scripts/context-signals.mjs +++ b/.kiro/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.opencode/skills/impeccable/SKILL.md b/.opencode/skills/impeccable/SKILL.md index eea811708..7cc0abe66 100644 --- a/.opencode/skills/impeccable/SKILL.md +++ b/.opencode/skills/impeccable/SKILL.md @@ -146,11 +146,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.opencode/skills/impeccable/scripts/context-signals.mjs b/.opencode/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.opencode/skills/impeccable/scripts/context-signals.mjs +++ b/.opencode/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.pi/skills/impeccable/SKILL.md b/.pi/skills/impeccable/SKILL.md index e4833e0fd..8a066527d 100644 --- a/.pi/skills/impeccable/SKILL.md +++ b/.pi/skills/impeccable/SKILL.md @@ -144,11 +144,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.pi/skills/impeccable/scripts/context-signals.mjs b/.pi/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.pi/skills/impeccable/scripts/context-signals.mjs +++ b/.pi/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.qoder/skills/impeccable/SKILL.md b/.qoder/skills/impeccable/SKILL.md index a6fecfaf8..9afbff322 100644 --- a/.qoder/skills/impeccable/SKILL.md +++ b/.qoder/skills/impeccable/SKILL.md @@ -146,11 +146,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.qoder/skills/impeccable/scripts/context-signals.mjs b/.qoder/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.qoder/skills/impeccable/scripts/context-signals.mjs +++ b/.qoder/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.rovodev/skills/impeccable/SKILL.md b/.rovodev/skills/impeccable/SKILL.md index 6a6619645..ac7e279ee 100644 --- a/.rovodev/skills/impeccable/SKILL.md +++ b/.rovodev/skills/impeccable/SKILL.md @@ -146,11 +146,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.rovodev/skills/impeccable/scripts/context-signals.mjs b/.rovodev/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.rovodev/skills/impeccable/scripts/context-signals.mjs +++ b/.rovodev/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.trae-cn/skills/impeccable/SKILL.md b/.trae-cn/skills/impeccable/SKILL.md index e93bbdd3c..fd2c6d784 100644 --- a/.trae-cn/skills/impeccable/SKILL.md +++ b/.trae-cn/skills/impeccable/SKILL.md @@ -144,11 +144,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.trae-cn/skills/impeccable/scripts/context-signals.mjs b/.trae-cn/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.trae-cn/skills/impeccable/scripts/context-signals.mjs +++ b/.trae-cn/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/.trae/skills/impeccable/SKILL.md b/.trae/skills/impeccable/SKILL.md index d8316ae21..b2cf7deb4 100644 --- a/.trae/skills/impeccable/SKILL.md +++ b/.trae/skills/impeccable/SKILL.md @@ -144,11 +144,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/.trae/skills/impeccable/scripts/context-signals.mjs b/.trae/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/.trae/skills/impeccable/scripts/context-signals.mjs +++ b/.trae/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/plugin/skills/impeccable/SKILL.md b/plugin/skills/impeccable/SKILL.md index c61880528..589bee044 100644 --- a/plugin/skills/impeccable/SKILL.md +++ b/plugin/skills/impeccable/SKILL.md @@ -146,11 +146,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/plugin/skills/impeccable/scripts/context-signals.mjs b/plugin/skills/impeccable/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/plugin/skills/impeccable/scripts/context-signals.mjs +++ b/plugin/skills/impeccable/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/skill/SKILL.src.md b/skill/SKILL.src.md index f82f70b7b..08eb358e8 100644 --- a/skill/SKILL.src.md +++ b/skill/SKILL.src.md @@ -165,11 +165,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be Reason over the signals; there is no score to obey: - `setup.hasDesign` false while `setup.hasCode` true → `document` (capture the visual system). + - `critique.latest` is `null` → the project has never been critiqued; for a set-up project with a real surface, offering `/impeccable critique ` is a strong default. - `critique.latest` with a low `score` or non-zero `p0` / `p1` → `polish` (it reads that snapshot as its backlog), or re-run `critique` if the snapshot looks stale. - `git.changedFiles` pointing at one surface → scope `audit` or `polish` to those files specifically, naming them. - `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. + 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. 3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. diff --git a/skill/scripts/context-signals.mjs b/skill/scripts/context-signals.mjs index cd54793af..1792d1a19 100644 --- a/skill/scripts/context-signals.mjs +++ b/skill/scripts/context-signals.mjs @@ -150,8 +150,35 @@ async function devServerSignals() { return { running: open.length > 0, ports: open }; } +/** + * 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. + */ +function scanTarget(cwd, devServer) { + if (devServer.running && devServer.ports.length) { + return { detectTarget: `http://localhost:${devServer.ports[0]}`, via: 'dev-server' }; + } + 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 }; +} + export async function gatherSignals(cwd = process.cwd()) { const ctx = loadContext(cwd); + const devServer = await devServerSignals(); return { setup: { hasProduct: ctx.hasProduct, @@ -163,7 +190,8 @@ export async function gatherSignals(cwd = process.cwd()) { }, critique: { latest: latestCritique(cwd) }, git: gitSignals(cwd), - devServer: await devServerSignals(), + devServer, + scan: scanTarget(cwd, devServer), }; } diff --git a/tests/context-signals.test.mjs b/tests/context-signals.test.mjs index b44efb0c2..28f778b97 100644 --- a/tests/context-signals.test.mjs +++ b/tests/context-signals.test.mjs @@ -108,6 +108,27 @@ describe('gatherSignals', () => { assert.equal(typeof s.devServer.running, 'boolean'); assert.ok(Array.isArray(s.devServer.ports)); }); + + it('points scan.detectTarget at an HTML entry when one exists', async () => { + write('public/index.html', 'x'); + 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'); + } + }); + + it('has a null scan.detectTarget when there is nothing scannable', async () => { + const s = await gatherSignals(scratch); + if (!s.devServer.running) { + assert.equal(s.scan.detectTarget, null); + assert.equal(s.scan.via, null); + } + }); }); describe('context-signals CLI', () => {