From 153b416f2ee2d62e9ec67d4d2948dd51af55fbdc Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 21 Jul 2026 02:04:36 -0700 Subject: [PATCH] Move the slop defects back into the craft floor The detector-blind slop review existed because the AI-tell rules had been stripped out of SKILL.md and nothing carried them. The floor is a better home: it loads after concept ideation and immediately before editing UI, which is the placement that made stripping them necessary in the first place. Models tread lightly when a ban list is present during ideation; by the time the floor loads, the direction is already committed. - Rename build-floor.md to craft-floor.md and restore the absolute bans (side-stripes, gradient text, glassmorphism, hero-metric, identical card grids, eyebrow-on-every-section, numbered markers, text overflow), the codex and gemini defect lists, and the reflexes no scanner catches. Rule ids match the ones the ablation catalog already knows. - Delete lib/slop-review.mjs and both injections. The Stop hook is now purely a mechanical pass and stays silent with nothing to report. - context.mjs replaces AI_SLOP_REVIEW_REQUIRED with the narrower MANUAL_DETECTOR_REQUIRED, emitted only when a session has no hook at all. A per-edit hook already covers the mechanical gap, and the floor covers the judgment one either way. Co-Authored-By: Claude --- skill/SKILL.src.md | 2 +- skill/reference/build-floor.md | 24 --------------- skill/reference/craft-floor.md | 51 +++++++++++++++++++++++++++++++ skill/reference/hooks.md | 4 ++- skill/scripts/context.mjs | 27 ++++++++-------- skill/scripts/hook-lib.mjs | 28 ++--------------- skill/scripts/lib/slop-review.mjs | 30 ------------------ tests/context.test.mjs | 20 ++++++------ tests/hook.test.mjs | 19 ++++-------- 9 files changed, 87 insertions(+), 118 deletions(-) delete mode 100644 skill/reference/build-floor.md create mode 100644 skill/reference/craft-floor.md delete mode 100644 skill/scripts/lib/slop-review.mjs diff --git a/skill/SKILL.src.md b/skill/SKILL.src.md index 2043ba19e..488025f8d 100644 --- a/skill/SKILL.src.md +++ b/skill/SKILL.src.md @@ -20,7 +20,7 @@ Core principles: 1. Run `node {{scripts_path}}/context.mjs` once per session (if the runtime shows this skill's loaded base directory, run `node /scripts/context.mjs`; keep cwd at the user's project). Pass a named source file or route as `--target `. It loads PRODUCT.md, DESIGN.md, the matching surface brief, and native-platform guidance when applicable; follow its directives and do not rerun it. 2. Before acting, load the one playbook that owns the request: the Commands table's reference for an explicit or clearly implied sub-command, or [reference/new-work.md](reference/new-work.md) for a new surface or replacement visual world. Then inspect the target and at least one representative source of incumbent visual truth (tokens, theme, CSS, component, or asset) before editing. -3. After analysis and direction are resolved, load [reference/build-floor.md](reference/build-floor.md) immediately before editing UI. Do not load it for planning-only work. +3. After analysis and direction are resolved, load [reference/craft-floor.md](reference/craft-floor.md) immediately before editing UI. It carries the quality floor, the absolute bans, and the reflexes no detector catches. Do not load it for planning-only work. ## How to design diff --git a/skill/reference/build-floor.md b/skill/reference/build-floor.md deleted file mode 100644 index 6bee088e1..000000000 --- a/skill/reference/build-floor.md +++ /dev/null @@ -1,24 +0,0 @@ -# Build floor - -Apply this only after analysis and direction are settled. Build without announcing the checklist. - -- **Contrast:** body and placeholder text ≥4.5:1; large text ≥3:1. On colored surfaces, tint secondary text from that hue or the foreground instead of using gray. -- **Depth:** shadows describe light with offset and soft blur; zero-offset colored halos are decoration. -- **Spacing:** tight groups, generous separation, no cramped containers; space above a heading exceeds space below. Verify computed values. -- **Type:** body measure 65–75ch; display max 6rem and tracking floor -0.04em; balance headings; use clear scale/weight contrast; test overflow at every breakpoint. -- **Motion:** author one coherent moment instead of scattered effects. Use exponential ease-out and an already-visible default. Premium motion may add focus, depth, masks, light, or material change through blur/filter, backdrop-filter, clip-path/masks, or shadow when smooth; do not rely on transform/opacity alone. -- **Shipping:** real content, working controls, responsive composition, keyboard focus, and the states users hit: hover, disabled, loading, error, and empty. -- **Copy:** use the product's language; controls name their action, errors name the problem and recovery. -- **Coverage:** every brief requirement must exist and be findable within seconds. - -Before finishing changed UI, follow the quality guidance supplied by `context.mjs` and hooks. Context requests a manual scan only when no automatic detector is active; never add a second detector pass. - - -- Display tracking stops at -0.04em; -0.02 to -0.03em is usually enough. -- Declare elevation once: border or shadow, not both as decoration. Keep container radii modest; reserve pills for small controls. -- Use real illustration or none. Treat backgrounds as surfaces and add texture only from the subject's world. Claims, evidence, and configuration come from supplied truth; label illustrative behavior and unresolved values honestly. - - - -Never animate `` elements on hover, directly or through a parent. Give the card itself feedback instead. - diff --git a/skill/reference/craft-floor.md b/skill/reference/craft-floor.md new file mode 100644 index 000000000..ca3584c38 --- /dev/null +++ b/skill/reference/craft-floor.md @@ -0,0 +1,51 @@ +# Craft floor + +Apply this only after analysis and direction are settled. Build without announcing the checklist. A pinned brief or the committed visual world beats every line below; your own reflex does not. + +## Floor + +- **Contrast:** body and placeholder text ≥4.5:1; large text ≥3:1. On colored surfaces, tint secondary text from that hue or the foreground instead of using gray. +- **Depth:** shadows describe light with offset and soft blur; zero-offset colored halos are decoration. +- **Spacing:** tight groups, generous separation, no cramped containers; space above a heading exceeds space below. Verify computed values. +- **Type:** body measure 65–75ch; display max 6rem and tracking floor -0.04em; balance headings; use clear scale/weight contrast; test overflow at every breakpoint. +- **Motion:** author one coherent moment instead of scattered effects. Use exponential ease-out and an already-visible default. Premium motion may add focus, depth, masks, light, or material change through blur/filter, backdrop-filter, clip-path/masks, or shadow when smooth; do not rely on transform/opacity alone. +- **Shipping:** real content, working controls, responsive composition, keyboard focus, and the states users hit: hover, disabled, loading, error, and empty. +- **Copy:** use the product's language; controls name their action, errors name the problem and recovery. +- **Coverage:** every brief requirement must exist and be findable within seconds. + +## Absolute bans + +Match and refuse. If you are about to write one of these, rewrite the element with different structure. + +- **Side-stripe borders.** `border-left` or `border-right` above 1px as a colored accent on cards, list items, callouts, or alerts. Never intentional. Use full borders, background tints, leading numbers or icons, or nothing. +- **Gradient text.** `background-clip: text` over a gradient. Decorative, never meaningful. One solid color; emphasis through weight or size. +- **Glassmorphism as default.** Blur and glass panels used as decoration. Rare and purposeful, or nothing. +- **The hero-metric template.** Big number, small label, supporting stats, accent treatment. +- **Identical card grids.** Same-sized cards carrying icon plus heading plus text, repeated as the page structure. Cards are the lazy answer; nested cards are always wrong. +- **A tiny tracked uppercase eyebrow above every section.** Small all-caps with wide tracking ("ABOUT", "PROCESS", "PRICING") shows up on most generations regardless of brief, which is what makes it a tell. One named kicker as a deliberate system is voice; an eyebrow on every section is AI grammar. +- **Numbered section markers as scaffolding (01 / 02 / 03).** The eyebrow trope one tier deeper. Numbers earn their place when the section really is a sequence and the order carries information the reader needs. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids overflow on tablet and mobile. Test the real copy at every breakpoint, then reduce the clamp max or rewrite the line. The viewport is part of the design. + +## Detector-blind reflexes + +No scanner catches these, and they are where an otherwise competent build still reads as generated. Inspect the rendered result for them. + +- Monospace used to signal "technical" or "developer" rather than to show code, data, or measurement. +- Light or dark chosen by category habit instead of the real use scene: who uses this, where, under what ambient light. +- Decorative sparklines, meaningless progress rings, and rounded rectangles with soft shadows standing in for content. +- A modal reached for when the task needs neither interruption nor protected focus. +- One uniform entrance animation applied to every section. Staggering items within a single list is legitimate; the reflex is the identical reveal, not motion itself. + +Run the mechanical detector once over the changed web UI, and only when this session has no automatic hook: `node {{scripts_path}}/detect.mjs --json `. Never add a second detector pass, and never run it during concept selection. + + +- Display tracking stops at -0.04em; -0.02 to -0.03em is usually enough. +- Declare elevation once: border or shadow, not both as decoration. A 1px border paired with a soft shadow past 16px blur is the ghost-card tell. Keep container radii modest, 12–16px on cards; reserve pills for small controls. +- Use real illustration or none. Hand-drawn SVG scenes, `loose-sketch` / `doodle` class names, and `feTurbulence` paper-grain filters read as amateurish rather than whimsical. +- Treat backgrounds as surfaces and add texture only from the subject's world. `repeating-linear-gradient` stripes and two-axis CSS grid overlays are decoration unless the surface really is a canvas, map, blueprint, or measurement tool. +- Claims, evidence, and configuration come from supplied truth; label illustrative behavior and unresolved values honestly. Naming a concept and then layering an ironic modifier is not a claim. + + + +Never animate `` elements on hover, directly or through a parent. That includes Tailwind's `.group:hover .group-hover\:scale` / `:rotate` / `:translate` reaching a child image. The image is not an action target, so the motion carries nothing. Give the card itself feedback instead. + diff --git a/skill/reference/hooks.md b/skill/reference/hooks.md index 068b04bc2..fa6222274 100644 --- a/skill/reference/hooks.md +++ b/skill/reference/hooks.md @@ -4,7 +4,9 @@ Manage the **design detector hook** for the current project. The hook runs the impeccable design detector on direct file edits to design-relevant files (`.tsx`, `.jsx`, `.html`, `.vue`, `.svelte`, `.astro`, `.css`, `.scss`, `.sass`, `.less`, `.ts`, `.js`). Claude Code, Codex, and GitHub Copilot use a post-tool-use hook and push a short system reminder into the agent's context after the edit; findings get a correction prompt, pending issues get a re-nudge, and clean UI-ish files get a short ack unless quiet mode is on (`hook.quiet` in config). Plain `.ts` and `.js` files are still scanned, but stay quiet unless the detector finds something. Cursor uses `preToolUse` to block bad proposed writes before they land and stays silent when it allows a clean write. -The detector rules run in two tiers. The per-edit hook surfaces only the immediate tier: mechanical, unambiguous problems worth interrupting an edit for, such as broken images, overflowing or clipped content, contrast and legibility failures, gradient text, glow shadows, and design-system drift. Everything else (copy cadence, palette and typography taste, layout rhythm) is deferred to a deep pass on the `Stop` hook event, which runs the full rule set over every UI file touched in the session and surfaces the remaining findings once, deduplicated against what the per-edit pass already reported. That Stop message also asks for one authored review of detector-blind model reflexes. A session that touched no UI files stops silently. Set `hook.perEditRules` to `"all"` in `.impeccable/config.json` to restore the full rule set on every edit. The Stop deep pass is wired for Claude Code and Codex, which both dispatch a native `Stop` hook event. Cursor does not get one (its stop hook is not consistently dispatched; the pre-write gate covers it), and GitHub Copilot's stop-style events do not feed context back to the model, so they keep the full detector per edit while `context.mjs` supplies the detector-blind review. When no automatic hook is active, `context.mjs` instead supplies that review plus one manual detector command. +The detector rules run in two tiers. The per-edit hook surfaces only the immediate tier: mechanical, unambiguous problems worth interrupting an edit for, such as broken images, overflowing or clipped content, contrast and legibility failures, gradient text, glow shadows, and design-system drift. Everything else (copy cadence, palette and typography taste, layout rhythm) is deferred to a deep pass on the `Stop` hook event, which runs the full rule set over every UI file touched in the session and surfaces the remaining findings once, deduplicated against what the per-edit pass already reported. A session with nothing left to report stops silently. Set `hook.perEditRules` to `"all"` in `.impeccable/config.json` to restore the full rule set on every edit. The Stop deep pass is wired for Claude Code and Codex, which both dispatch a native `Stop` hook event. Cursor does not get one (its stop hook is not consistently dispatched; the pre-write gate covers it), and GitHub Copilot's stop-style events do not feed context back to the model, so they keep the full detector per edit. + +Every hook is a mechanical pass. The reflexes no scanner catches live in [craft-floor.md](craft-floor.md), which the skill loads before it edits UI, so they apply whether or not a hook is wired. A session with no automatic hook gets one `MANUAL_DETECTOR_REQUIRED` directive from `context.mjs` asking for a single detector run at the end. This command toggles the hook **per project** by editing `.impeccable/config.json` (the unified Impeccable config; hook runtime settings live under its `hook` key, and shared detector ignores live under `detector`). Per-developer overrides, including the install consent decision (`hook.consent`) the CLI records, live in the gitignored `.impeccable/config.local.json`. Set `hook.enabled: false` to turn the hook off, `hook.quiet: true` to silence the clean/pending acks, or `hook.auditLog` to a file path for an NDJSON log. The legacy `IMPECCABLE_HOOK_DISABLED`, `IMPECCABLE_HOOK_QUIET`, and `IMPECCABLE_HOOK_LOG` env vars are still honored and override these config values when set. diff --git a/skill/scripts/context.mjs b/skill/scripts/context.mjs index 184599a3b..598871f94 100644 --- a/skill/scripts/context.mjs +++ b/skill/scripts/context.mjs @@ -32,7 +32,6 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseTargetOptions } from './lib/target-args.mjs'; import { IMPECCABLE_COMMAND, IMPECCABLE_PROVIDER_ID } from './lib/provider.mjs'; -import { renderLlmOnlySlopReview } from './lib/slop-review.mjs'; import { resolveSurfaceBrief } from './lib/surface-briefs.mjs'; const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; @@ -1127,7 +1126,7 @@ async function cli() { ]; appendSurfaceBriefContext(parts, ctx); parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); - appendHookFallback(parts, ctx); + appendDetectorFallback(parts, ctx); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { parts.push(buildMissingTargetDirective()); } @@ -1141,7 +1140,7 @@ async function cli() { } appendSurfaceBriefContext(parts, ctx); parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists })); - appendHookFallback(parts, ctx); + appendDetectorFallback(parts, ctx); if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) { parts.push(buildMissingTargetDirective()); } @@ -1244,15 +1243,19 @@ function automaticHookMode(ctx) { return 'none'; } -function appendHookFallback(parts, ctx) { - const hookMode = automaticHookMode(ctx); - if (hookMode === 'stop') return; - const native = ctx.platform === 'ios' || ctx.platform === 'android' || ctx.platform === 'adaptive'; - parts.push(renderLlmOnlySlopReview({ - automaticDetector: hookMode === 'per-edit', - manualDetector: hookMode === 'none' && !native, - scriptsPath: path.dirname(fileURLToPath(import.meta.url)), - })); +// reference/craft-floor.md carries the detector-blind reflexes on every build, +// so the only gap left here is the mechanical pass. A hook covers it, per-edit +// or Stop; a session without one has to run the detector by hand. The detector +// reads HTML and CSS, so native projects get nothing. +function appendDetectorFallback(parts, ctx) { + if (automaticHookMode(ctx) !== 'none') return; + if (ctx.platform === 'ios' || ctx.platform === 'android' || ctx.platform === 'adaptive') return; + const scriptsPath = path.dirname(fileURLToPath(import.meta.url)); + parts.push([ + 'MANUAL_DETECTOR_REQUIRED: No automatic Impeccable design hook is active this session.', + `Once the changed web UI is finished, run the mechanical detector over it: \`node ${scriptsPath}/detect.mjs --json \`.`, + 'Run it once, and not earlier during concept selection.', + ].join(' ')); } function buildResolvedContextDirective(ctx, options, { targetExists = null } = {}) { diff --git a/skill/scripts/hook-lib.mjs b/skill/scripts/hook-lib.mjs index 71439212f..aafe32890 100644 --- a/skill/scripts/hook-lib.mjs +++ b/skill/scripts/hook-lib.mjs @@ -46,7 +46,6 @@ import path from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { extractPlatform, loadContext } from './context.mjs'; import { IMPECCABLE_COMMAND } from './lib/provider.mjs'; -import { renderStopSlopReview } from './lib/slop-review.mjs'; // `detector.extensions` (issue #316) is shared with Live's source search, which // needs the same answer for `.heex` / `.blade.php` when it hunts for session // markers. lib/template-extensions.mjs owns the shape; re-exported here because @@ -2054,22 +2053,9 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no return result({ skipped: 'native-platform', platform, durationMs: Date.now() - started }); } - const session = ensureSession(cache, sessionId); - const needsSlopReview = session.llmSlopReviewed !== true; - const det = detector || await loadDetector(); if (!det || typeof det.detectText !== 'function') { - if (!needsSlopReview) return result({ skipped: 'detector-missing', durationMs: Date.now() - started }); - session.llmSlopReviewed = true; - session.updatedAt = Date.now(); - persistCache(projectCwd, cache); - const text = renderStopSlopReview(); - return { - exitCode: 0, - stdout: payload(text, 'Stop', harness), - emission: { kind: 'stop-llm-slop-review', llmSlopReview: true }, - audit: { ...audit, emitted: true, detectorMissing: true, llmSlopReview: true, chars: text.length, durationMs: Date.now() - started }, - }; + return result({ skipped: 'detector-missing', durationMs: Date.now() - started }); } const scanOptions = designSystemOptions(config, det, projectCwd); @@ -2130,15 +2116,10 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no } audit.scannedFiles = scanned; - if (freshGroups.length === 0 && contractEntries.length === 0 && !needsSlopReview) { + if (freshGroups.length === 0 && contractEntries.length === 0) { return result({ emitted: false, skipped: 'stop-clean', durationMs: Date.now() - started }); } - if (needsSlopReview) { - session.llmSlopReviewed = true; - session.updatedAt = Date.now(); - } - // Fresh findings and first-time contract audits earn the cache write; // both mark this batch as surfaced so the next Stop fire is silent // unless new issues appear. @@ -2153,9 +2134,6 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no if (contractEntries.length > 0) { parts.push(renderContractAudit(contractEntries, { cwd: projectCwd })); } - if (needsSlopReview) { - parts.push(renderStopSlopReview()); - } const text = appendDesignSystemNote(parts.join('\n\n'), scanOptions); return { exitCode: 0, @@ -2166,7 +2144,6 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no ...(contractEntries.length > 0 ? { contractFiles: contractEntries.map((entry) => entry.filePath) } : {}), - ...(needsSlopReview ? { llmSlopReview: true } : {}), }, audit: { ...audit, @@ -2174,7 +2151,6 @@ export async function runStopHook({ stdinJson, env = {}, cwd = process.cwd(), no freshFiles: freshGroups.length, freshFindings: freshGroups.reduce((sum, group) => sum + group.findings.length, 0), ...(contractEntries.length > 0 ? { contractAudits: contractEntries.length } : {}), - ...(needsSlopReview ? { llmSlopReview: true } : {}), chars: text.length, durationMs: Date.now() - started, }, diff --git a/skill/scripts/lib/slop-review.mjs b/skill/scripts/lib/slop-review.mjs deleted file mode 100644 index 30f03e34d..000000000 --- a/skill/scripts/lib/slop-review.mjs +++ /dev/null @@ -1,30 +0,0 @@ -export const LLM_ONLY_SLOP_ITEMS = Object.freeze([ - 'Monospace used merely to signal “technical” or “developer.”', - 'Light or dark mode chosen by category habit rather than the actual use scene.', - 'Everything wrapped in cards, or identical icon-heading-text cards repeated as the page structure.', - 'Hero-metric scaffolds: one oversized number, a small label, supporting stats, and an accent treatment.', - 'Decorative glassmorphism, meaningless sparklines, or generic rounded rectangles with drop shadows.', - 'A modal chosen by reflex when the task does not require interruption or protected focus.', -]); - -export function renderLlmOnlySlopReview({ manualDetector = false, automaticDetector = false, scriptsPath = null } = {}) { - const lines = [ - automaticDetector - ? 'AI_SLOP_REVIEW_REQUIRED: The automatic detector covers mechanical rules, but this harness has no reliable late review. Before finishing changed UI, inspect the authored result for detector-blind model reflexes:' - : 'AI_SLOP_REVIEW_REQUIRED: The automatic Impeccable design hook is not available for this session. Before finishing changed UI, inspect the authored result for detector-blind model reflexes:', - ...LLM_ONLY_SLOP_ITEMS.map((item) => `- ${item}`), - 'Fix reflexes, not intentional choices required by the brief or established visual authority.', - ]; - if (manualDetector && scriptsPath) { - lines.push(`Then run the mechanical detector once over the changed web UI: \`node ${scriptsPath}/detect.mjs --json \`. Do not run it earlier during concept selection.`); - } - return lines.join('\n'); -} - -export function renderStopSlopReview() { - return [ - '[impeccable@1] Detector-blind AI-slop review. The mechanical pass is complete; now inspect the rendered result for model reflexes it cannot reliably detect:', - ...LLM_ONLY_SLOP_ITEMS.map((item) => `- ${item}`), - 'Fix reflexes, not intentional choices required by the brief or established visual authority. Do not rerun the detector; this is the authored judgment layer.', - ].join('\n'); -} diff --git a/tests/context.test.mjs b/tests/context.test.mjs index b589a2b09..ed4641721 100644 --- a/tests/context.test.mjs +++ b/tests/context.test.mjs @@ -1035,17 +1035,16 @@ describe('context.mjs CLI', () => { // routes to new-work rather than back through product init. assert.match(res.stdout, /\n---\n\n/); assert.match(res.stdout, /WORLD_DISCOVERY_REQUIRED: PRODUCT\.md exists but no DESIGN\.md/); - assert.match(res.stdout, /AI_SLOP_REVIEW_REQUIRED:/); - assert.match(res.stdout, /Monospace used merely/); + assert.match(res.stdout, /MANUAL_DETECTOR_REQUIRED:/); assert.match(res.stdout, /detect\.mjs --json /); }); - it('keeps LLM-only fallback guidance out of early context when the current provider hook is active', () => { + it('keeps the manual-detector directive out of early context when the current provider hook is active', () => { const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts'); const lib = path.join(scripts, 'lib'); fs.mkdirSync(lib, { recursive: true }); fs.copyFileSync(SCRIPT_PATH, path.join(scripts, 'context.mjs')); - for (const helper of ['target-args.mjs', 'surface-briefs.mjs', 'target-slug.mjs', 'slop-review.mjs']) { + for (const helper of ['target-args.mjs', 'surface-briefs.mjs', 'target-slug.mjs']) { fs.copyFileSync(path.join(path.dirname(SCRIPT_PATH), 'lib', helper), path.join(lib, helper)); } const provider = fs.readFileSync(path.join(path.dirname(SCRIPT_PATH), 'lib', 'provider.mjs'), 'utf8') @@ -1065,7 +1064,7 @@ describe('context.mjs CLI', () => { env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' }, }); assert.equal(res.status, 0, res.stderr); - assert.doesNotMatch(res.stdout, /AI_SLOP_REVIEW_REQUIRED:/); + assert.doesNotMatch(res.stdout, /MANUAL_DETECTOR_REQUIRED:/); fs.mkdirSync(path.join(project, '.impeccable'), { recursive: true }); fs.writeFileSync(path.join(project, '.impeccable', 'config.json'), JSON.stringify({ hook: { enabled: false } })); @@ -1075,16 +1074,16 @@ describe('context.mjs CLI', () => { env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' }, }); assert.equal(disabled.status, 0, disabled.stderr); - assert.match(disabled.stdout, /AI_SLOP_REVIEW_REQUIRED:/); + assert.match(disabled.stdout, /MANUAL_DETECTOR_REQUIRED:/); assert.match(disabled.stdout, /detect\.mjs --json /); }); - it('injects only detector-blind guidance when a per-edit-only hook is active', () => { + it('adds no detector directive when a per-edit-only hook is active', () => { const scripts = path.join(scratch, 'bundle', 'skills', 'impeccable', 'scripts'); const lib = path.join(scripts, 'lib'); fs.mkdirSync(lib, { recursive: true }); fs.copyFileSync(SCRIPT_PATH, path.join(scripts, 'context.mjs')); - for (const helper of ['target-args.mjs', 'surface-briefs.mjs', 'target-slug.mjs', 'slop-review.mjs']) { + for (const helper of ['target-args.mjs', 'surface-briefs.mjs', 'target-slug.mjs']) { fs.copyFileSync(path.join(path.dirname(SCRIPT_PATH), 'lib', helper), path.join(lib, helper)); } const provider = fs.readFileSync(path.join(path.dirname(SCRIPT_PATH), 'lib', 'provider.mjs'), 'utf8') @@ -1104,8 +1103,7 @@ describe('context.mjs CLI', () => { env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' }, }); assert.equal(res.status, 0, res.stderr); - assert.match(res.stdout, /AI_SLOP_REVIEW_REQUIRED:/); - assert.match(res.stdout, /automatic detector covers mechanical rules/); + assert.doesNotMatch(res.stdout, /MANUAL_DETECTOR_REQUIRED:/); assert.doesNotMatch(res.stdout, /detect\.mjs --json /); }); @@ -1354,7 +1352,7 @@ describe('context.mjs update check', () => { const providerSrc = path.join(path.dirname(SCRIPT_PATH), 'lib', 'provider.mjs'); const providerDest = path.join(path.dirname(skillScript), 'lib', 'provider.mjs'); fs.copyFileSync(providerSrc, providerDest); - for (const helper of ['surface-briefs.mjs', 'target-slug.mjs', 'slop-review.mjs']) { + for (const helper of ['surface-briefs.mjs', 'target-slug.mjs']) { fs.copyFileSync( path.join(path.dirname(SCRIPT_PATH), 'lib', helper), path.join(path.dirname(skillScript), 'lib', helper), diff --git a/tests/hook.test.mjs b/tests/hook.test.mjs index fc25c9100..b284c4f75 100644 --- a/tests/hook.test.mjs +++ b/tests/hook.test.mjs @@ -3281,8 +3281,6 @@ describe('runStopHook()', () => { assert.match(out.hookSpecificOutput.additionalContext, /em-dash-overuse/); assert.match(out.hookSpecificOutput.additionalContext, /side-tab/); assert.doesNotMatch(out.hookSpecificOutput.additionalContext, /dark-glow/); - assert.match(out.hookSpecificOutput.additionalContext, /Detector-blind AI-slop review/); - assert.match(out.hookSpecificOutput.additionalContext, /Monospace used merely/); assert.equal(stop.emission.kind, 'stop-deep-pass'); }); @@ -3307,7 +3305,7 @@ describe('runStopHook()', () => { assert.equal(second.audit.skipped, 'stop-clean'); }); - it('respects detector.ignoreRules while still delivering the one-time LLM-only review', async () => { + it('stays silent when detector.ignoreRules filters away every touched finding', async () => { const sid = 'stop-ignored'; fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true }); fs.writeFileSync(getConfigPath(cwd), JSON.stringify({ @@ -3318,12 +3316,8 @@ describe('runStopHook()', () => { await runHook({ stdinJson: JSON.stringify(editEvent(file, sid)), env: {}, cwd, detector: det }); const stop = await runStopHook({ stdinJson: JSON.stringify(stopEvent(sid)), env: {}, cwd, detector: det }); - const text = JSON.parse(stop.stdout).hookSpecificOutput.additionalContext; - assert.doesNotMatch(text, /em-dash-overuse/); - assert.match(text, /Detector-blind AI-slop review/); - const second = await runStopHook({ stdinJson: JSON.stringify(stopEvent(sid)), env: {}, cwd, detector: det }); - assert.equal(second.stdout, ''); - assert.equal(second.audit.skipped, 'stop-clean'); + assert.equal(stop.stdout, ''); + assert.equal(stop.audit.skipped, 'stop-clean'); }); it('honors kill switches and the re-entrancy guard', async () => { @@ -3586,15 +3580,14 @@ describe('runStopHook() — direction-contract audit', () => { assert.doesNotMatch(text, /Direction-contract audit/); }); - it('does not audit a malformed contract but still emits the one-time LLM-only review', async () => { + it('stays silent on a malformed contract with nothing else to report', async () => { const sid = 'contract-malformed'; const file = write('index.html', '