feat(skill): make bare /impeccable context-aware (re: #159)

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) <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 06eabc144a
commit 772aa73aa3
29 changed files with 469 additions and 14 deletions
+3
View File
@@ -144,11 +144,14 @@ Plus two management commands: `pin <command>` and `unpin <command>`, 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 <surface>` 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 <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.
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.
@@ -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 <scripts>/detect.mjs --json <target>` (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),
};
}