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
+29 -1
View File
@@ -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),
};
}