fix(skill): target local files for detect, never a URL (#159)

Rework context-signals' detect target after review: a URL meant a costly
Puppeteer render (and a probed port might not even be this project), and the
index.html-or-bail fallback failed most real apps (no root index.html).

New priority: (1) the scannable markup/style files in the dirty git tree
(what the user is working on, small and local); (2) a local source dir
(src / app / components / pages / public — the detector walks these and skips
node_modules / dist / build); (3) a root index.html, else the project root as
a last resort when there's code. Emits `scan.targets` (a list) + `scan.via`.
Never a URL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-05-28 15:50:53 -07:00
co-authored by Claude Opus 4.8
parent f7f2bfc800
commit 0047981a95
29 changed files with 530 additions and 393 deletions
+1 -2
View File
@@ -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 h1h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans.
#### Layout
@@ -152,7 +151,7 @@ Plus two management commands: `pin <command>` and `unpin <command>`, 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 <scan.detectTarget>` 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 <scan.targets joined by spaces>` 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.
@@ -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 <scripts>/detect.mjs --json <target>` (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 <scripts>/detect.mjs --json <targets>` 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),
};
}