@bastani/atomic 0.9.7 → 0.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +12 -0
  3. package/dist/builtin/cursor/package.json +2 -2
  4. package/dist/builtin/intercom/CHANGELOG.md +12 -0
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/mcp/CHANGELOG.md +12 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +14 -0
  9. package/dist/builtin/subagents/package.json +1 -1
  10. package/dist/builtin/subagents/skills/{effective-liteparse → liteparse}/SKILL.md +4 -4
  11. package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +21 -5
  12. package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +311 -12
  13. package/dist/builtin/web-access/CHANGELOG.md +12 -0
  14. package/dist/builtin/web-access/package.json +1 -1
  15. package/dist/builtin/workflows/CHANGELOG.md +24 -0
  16. package/dist/builtin/workflows/README.md +2 -2
  17. package/dist/builtin/workflows/builtin/goal-prompts.ts +9 -7
  18. package/dist/builtin/workflows/builtin/goal-reducer.ts +9 -24
  19. package/dist/builtin/workflows/builtin/goal-review.ts +17 -36
  20. package/dist/builtin/workflows/builtin/ralph-core.ts +1 -3
  21. package/dist/builtin/workflows/builtin/ralph-review-gate.ts +31 -53
  22. package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +10 -4
  23. package/dist/builtin/workflows/builtin/shared-prompts.ts +5 -4
  24. package/dist/builtin/workflows/package.json +1 -1
  25. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +2 -2
  26. package/dist/builtin/workflows/skills/impeccable/reference/init.md +37 -10
  27. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +1 -1
  28. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +2 -1
  29. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +2 -2
  30. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +20 -4
  31. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +3 -6
  32. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2 -4
  33. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +31 -8
  34. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +7 -6
  35. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +11 -10
  36. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +1 -0
  37. package/dist/builtin/workflows/skills/impeccable/scripts/lib/is-generated.mjs +1 -1
  38. package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +4 -0
  39. package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +2 -1
  40. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +38 -16
  41. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +33 -19
  42. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +9 -9
  43. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +2 -0
  44. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +16 -9
  45. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +59 -14
  46. package/docs/quickstart.md +1 -1
  47. package/docs/workflows.md +4 -4
  48. package/npm-shrinkwrap.json +23 -23
  49. package/package.json +2 -2
  50. package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +0 -305
  51. /package/dist/builtin/subagents/skills/{effective-liteparse → liteparse}/scripts/search.py +0 -0
@@ -23,6 +23,7 @@ import os from 'node:os';
23
23
  import path from 'node:path';
24
24
  import { fileURLToPath } from 'node:url';
25
25
  import { parseTargetOptions } from './lib/target-args.mjs';
26
+ import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
26
27
 
27
28
  const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md'];
28
29
  const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md'];
@@ -902,7 +903,7 @@ async function cli() {
902
903
  'or wording that clearly maps to a from-scratch build/shape flow, load ' +
903
904
  'reference/init.md and write PRODUCT.md first; for any other (scoped) ' +
904
905
  'command against existing code, proceed using the code as context and ' +
905
- 'offer `/impeccable init` as a suggestion (do not block).',
906
+ `offer \`${IMPECCABLE_COMMAND} init\` as a suggestion (do not block).`,
906
907
  ];
907
908
  parts.push(buildResolvedContextDirective(ctx, cliOptions, { targetExists }));
908
909
  if (shouldWarnMissingTarget(ctx, targetProvided, targetExists)) {
@@ -2,11 +2,11 @@
2
2
  /**
3
3
  * Critique persistence helper.
4
4
  *
5
- * Each run of /impeccable critique writes a per-target snapshot to
5
+ * Each critique run writes a per-target snapshot to
6
6
  * .impeccable/critique/<timestamp>__<slug>.md
7
7
  * with a small YAML frontmatter carrying the score + P0/P1 counts.
8
8
  *
9
- * /impeccable polish reads the latest matching snapshot at start as its
9
+ * The polish workflow reads the latest matching snapshot at start as its
10
10
  * fix backlog. No other skill auto-reads critique output.
11
11
  *
12
12
  * The slug is derived mechanically from the *resolved* primary artifact
@@ -1284,14 +1284,30 @@ function checkHtmlPatterns(html) {
1284
1284
  }
1285
1285
  }
1286
1286
 
1287
+ function stripHtmlBlocks(content, tagNames) {
1288
+ let out = String(content);
1289
+ for (const tagName of tagNames) {
1290
+ const opener = '<' + tagName;
1291
+ const closer = '</' + tagName;
1292
+ while (true) {
1293
+ const lower = out.toLowerCase();
1294
+ const start = lower.indexOf(opener);
1295
+ if (start === -1) break;
1296
+ const openEnd = lower.indexOf('>', start + opener.length);
1297
+ const closeStart = openEnd === -1 ? -1 : lower.indexOf(closer, openEnd + 1);
1298
+ const closeEnd = closeStart === -1 ? -1 : lower.indexOf('>', closeStart + closer.length);
1299
+ if (openEnd === -1 || closeStart === -1 || closeEnd === -1) break;
1300
+ out = out.slice(0, start) + ' ' + out.slice(closeEnd + 1);
1301
+ }
1302
+ }
1303
+ return out;
1304
+ }
1305
+
1287
1306
  // --- Provider tells (gated): "X theater" framing copy (GPT) ---
1288
1307
  // Lives here (regex-on-HTML) rather than in the text-content analyzers so it
1289
1308
  // runs in the bundled browser path too, not just the CLI/static path.
1290
1309
  {
1291
- const bodyText = html
1292
- .replace(/<script\b[^>]*>[\s\S]*?<\/script\b[^>]*>/gi, ' ')
1293
- .replace(/<style\b[^>]*>[\s\S]*?<\/style\b[^>]*>/gi, ' ')
1294
- .replace(/<[^>]+>/g, ' ');
1310
+ const bodyText = stripHtmlBlocks(html, ['script', 'style']).replace(/<[^>]+>/g, ' ');
1295
1311
  const tm = /\b(\w+)\s+theater\b/i.exec(bodyText);
1296
1312
  if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` });
1297
1313
  }
@@ -2,7 +2,7 @@ import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
2
2
  import { isNeutralColor } from '../../shared/color.mjs';
3
3
  import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
4
4
  import { checkSourceDesignSystem } from '../../design-system.mjs';
5
- import { isFullPage } from '../../shared/page.mjs';
5
+ import { isFullPage, stripHtmlBlocks } from '../../shared/page.mjs';
6
6
  import { applyInlineIgnores } from '../../shared/inline-ignores.mjs';
7
7
  import { finding } from '../../findings.mjs';
8
8
  import { filterByProviders } from '../../registry/antipatterns.mjs';
@@ -19,10 +19,7 @@ const isSafeElement = (line) => /<(?:blockquote|nav[\s>]|pre[\s>]|code[\s>]|a\s|
19
19
  /** Strip HTML to plain text — drops script/style/comments/tags so
20
20
  * content-text analyzers don't false-positive on code or CSS. */
21
21
  function stripHtmlToText(html) {
22
- return html
23
- .replace(/<script\b[^>]*>[\s\S]*?<\/script\b[^>]*>/gi, ' ')
24
- .replace(/<style\b[^>]*>[\s\S]*?<\/style\b[^>]*>/gi, ' ')
25
- .replace(/<!--[\s\S]*?-->/g, ' ')
22
+ return stripHtmlBlocks(html, ['script', 'style'])
26
23
  .replace(/<[^>]+>/g, ' ')
27
24
  .replace(/\s+/g, ' ');
28
25
  }
@@ -352,7 +349,7 @@ function extractStyleBlocks(content, ext) {
352
349
  ext = ext.toLowerCase();
353
350
  if (ext !== '.vue' && ext !== '.svelte') return [];
354
351
  const blocks = [];
355
- const re = /<style[^>]*>([\s\S]*?)<\/style\b[^>]*>/gi;
352
+ const re = /<style[^>]*>([\s\S]*?)<\/style>/gi;
356
353
  let m;
357
354
  while ((m = re.exec(content)) !== null) {
358
355
  const before = content.substring(0, m.index);
@@ -19,6 +19,7 @@ import {
19
19
  relativeLuminance,
20
20
  } from '../shared/color.mjs';
21
21
  import { extractGoogleFontFamilies } from '../shared/fonts.mjs';
22
+ import { stripHtmlBlocks } from '../shared/page.mjs';
22
23
 
23
24
  const DETECTOR_IS_BROWSER = typeof window !== 'undefined';
24
25
 
@@ -614,10 +615,7 @@ function checkHtmlPatterns(html) {
614
615
  // Lives here (regex-on-HTML) rather than in the text-content analyzers so it
615
616
  // runs in the bundled browser path too, not just the CLI/static path.
616
617
  {
617
- const bodyText = html
618
- .replace(/<script\b[^>]*>[\s\S]*?<\/script\b[^>]*>/gi, ' ')
619
- .replace(/<style\b[^>]*>[\s\S]*?<\/style\b[^>]*>/gi, ' ')
620
- .replace(/<[^>]+>/g, ' ');
618
+ const bodyText = stripHtmlBlocks(html, ['script', 'style']).replace(/<[^>]+>/g, ' ');
621
619
  const tm = /\b(\w+)\s+theater\b/i.exec(bodyText);
622
620
  if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` });
623
621
  }
@@ -1,12 +1,35 @@
1
+ /** Remove complete script/style elements and comments without regex-based HTML sanitization. */
2
+ function stripHtmlBlocks(content, tagNames = []) {
3
+ let out = String(content);
4
+ for (const tagName of tagNames) {
5
+ const opener = `<${tagName}`;
6
+ const closer = `</${tagName}`;
7
+ while (true) {
8
+ const lower = out.toLowerCase();
9
+ const start = lower.indexOf(opener);
10
+ if (start === -1) break;
11
+ const openEnd = lower.indexOf('>', start + opener.length);
12
+ const closeStart = openEnd === -1 ? -1 : lower.indexOf(closer, openEnd + 1);
13
+ const closeEnd = closeStart === -1 ? -1 : lower.indexOf('>', closeStart + closer.length);
14
+ if (openEnd === -1 || closeStart === -1 || closeEnd === -1) break;
15
+ out = out.slice(0, start) + ' ' + out.slice(closeEnd + 1);
16
+ }
17
+ }
18
+ while (true) {
19
+ const start = out.indexOf('<!--');
20
+ if (start === -1) break;
21
+ const normalEnd = out.indexOf('-->', start + 4);
22
+ const bangEnd = out.indexOf('--!>', start + 4);
23
+ const end = normalEnd === -1 ? bangEnd : bangEnd === -1 ? normalEnd : Math.min(normalEnd, bangEnd);
24
+ if (end === -1) break;
25
+ out = out.slice(0, start) + out.slice(end + (end === bangEnd ? 4 : 3));
26
+ }
27
+ return out;
28
+ }
29
+
1
30
  /** Check if content looks like a full page (not a component/partial) */
2
31
  function isFullPage(content) {
3
- let stripped = String(content || '');
4
- let previous;
5
- do {
6
- previous = stripped;
7
- stripped = stripped.replace(/<!--[\s\S]*?-->/g, '');
8
- } while (stripped !== previous);
9
- return /<!doctype\s|<html[\s>]|<head[\s>]/i.test(stripped);
32
+ return /<!doctype\s|<html[\s>]|<head[\s>]/i.test(stripHtmlBlocks(content));
10
33
  }
11
34
 
12
- export { isFullPage };
35
+ export { isFullPage, stripHtmlBlocks };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `/impeccable hooks <on|off|status|reset>` manage the design hook runtime
3
+ * The Impeccable hooks command manages the design hook runtime
4
4
  * via the `hook` key and shared detector ignores via the `detector` key in
5
5
  * .impeccable/config.json / .impeccable/config.local.json.
6
6
  *
@@ -21,6 +21,7 @@
21
21
 
22
22
  import fs from 'node:fs';
23
23
  import path from 'node:path';
24
+ import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
24
25
 
25
26
  import {
26
27
  getConfigPath,
@@ -184,7 +185,7 @@ function writeHookConfig(cwd, hookConfig, opts = {}) {
184
185
  const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {};
185
186
  const existingHook = stripDetectorKeys(hookSection(existing));
186
187
  // Merge over the existing hook object so fields the merge helpers don't manage
187
- // (consent, quiet, auditLog) survive a `/impeccable hooks` edit.
188
+ // (consent, quiet, auditLog) survive an Impeccable hooks edit.
188
189
  const next = { ...existing, hook: { ...existingHook, ...hookConfig } };
189
190
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
190
191
  fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n');
@@ -513,9 +514,9 @@ function parseIgnoreRuleArgs(args) {
513
514
  function addIgnoreRule(cwd, args) {
514
515
  const parsed = parseIgnoreRuleArgs(args);
515
516
  const rule = parsed.rule;
516
- if (!rule) throw new Error('Pass a rule id, e.g. /impeccable hooks ignore-rule side-tab');
517
+ if (!rule) throw new Error(`Pass a rule id, e.g. ${IMPECCABLE_COMMAND} hooks ignore-rule side-tab`);
517
518
  if (rule === 'overused-font' && !parsed.allValues) {
518
- throw new Error('overused-font is value-specific by default. Use /impeccable hooks ignore-value overused-font <font> for a confirmed font, or /impeccable hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.');
519
+ throw new Error(`overused-font is value-specific by default. Use ${IMPECCABLE_COMMAND} hooks ignore-value overused-font <font> for a confirmed font, or ${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`);
519
520
  }
520
521
  const config = mergeDetectorConfig(readRawDetectorConfig(cwd));
521
522
  if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule);
@@ -524,7 +525,7 @@ function addIgnoreRule(cwd, args) {
524
525
  }
525
526
 
526
527
  function addIgnoreFile(cwd, glob) {
527
- if (!glob) throw new Error('Pass a glob, e.g. /impeccable hooks ignore-file "src/legacy/**"');
528
+ if (!glob) throw new Error(`Pass a glob, e.g. ${IMPECCABLE_COMMAND} hooks ignore-file "src/legacy/**"`);
528
529
  const config = mergeDetectorConfig(readRawDetectorConfig(cwd));
529
530
  if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob);
530
531
  writeDetectorConfig(cwd, config);
@@ -569,7 +570,7 @@ function parseIgnoreValueArgs(args) {
569
570
  function addIgnoreValue(cwd, args) {
570
571
  const parsed = parseIgnoreValueArgs(args);
571
572
  if (!parsed.rule || !parsed.value) {
572
- throw new Error('Pass a rule id and value, e.g. /impeccable hooks ignore-value overused-font Inter');
573
+ throw new Error(`Pass a rule id and value, e.g. ${IMPECCABLE_COMMAND} hooks ignore-value overused-font Inter`);
573
574
  }
574
575
 
575
576
  if (parsed.shared && parsed.local) {
@@ -41,6 +41,7 @@ import os from 'node:os';
41
41
  import path from 'node:path';
42
42
  import { pathToFileURL, fileURLToPath } from 'node:url';
43
43
  import { extractPlatform, loadContext } from './context.mjs';
44
+ import { IMPECCABLE_COMMAND } from './lib/provider.mjs';
44
45
 
45
46
  const __filename = fileURLToPath(import.meta.url);
46
47
  const __dirname = path.dirname(__filename);
@@ -661,7 +662,7 @@ export function bumpEditCount(cache, sessionId, filePath) {
661
662
  }
662
663
 
663
664
  export function suppressionNotice(filePath) {
664
- return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run /impeccable audit to revisit.`;
665
+ return `${ENVELOPE_PREFIX} Suppressing further design hints on ${filePath}. More than ${EDIT_COUNT_THRESHOLD} edits in this session reached. Run ${IMPECCABLE_COMMAND} audit to revisit.`;
665
666
  }
666
667
 
667
668
  // Glob → RegExp. Supports `**`, `*`, `?`, and `{a,b}` alternation.
@@ -877,7 +878,7 @@ export function renderTemplate(findings, filePath, config, opts = {}) {
877
878
  const header = `${ENVELOPE_PREFIX} Design hook findings requiring review in ${display} (${total} issue(s)):`;
878
879
  const lines = shown.map((f) => formatFindingLine(f));
879
880
  const more = remaining > 0
880
- ? `... and ${remaining} more (see /impeccable audit).`
881
+ ? `... and ${remaining} more (see ${IMPECCABLE_COMMAND} audit).`
881
882
  : null;
882
883
  const footer = directiveFooter(display);
883
884
 
@@ -921,7 +922,7 @@ function renderGroupedTemplate(groups, config, opts = {}) {
921
922
  shownCount += shown.length;
922
923
  const hidden = group.findings.length - shown.length;
923
924
  if (hidden > 0) {
924
- lines.push(`- ... ${hidden} more in ${display} (see /impeccable audit).`);
925
+ lines.push(`- ... ${hidden} more in ${display} (see ${IMPECCABLE_COMMAND} audit).`);
925
926
  }
926
927
  }
927
928
 
@@ -937,7 +938,7 @@ function clampGroupedToBudget(header, lines, footer, maxChars) {
937
938
  const assemble = (linesArr, omitted) => [
938
939
  header,
939
940
  ...linesArr,
940
- ...(omitted ? ['... and more (see /impeccable audit).'] : []),
941
+ ...(omitted ? [`... and more (see ${IMPECCABLE_COMMAND} audit).`] : []),
941
942
  '',
942
943
  footer,
943
944
  ].join('\n');
@@ -970,7 +971,7 @@ function clampToBudget(header, lines, more, footer, maxChars) {
970
971
  let assembled = assemble(working, moreText);
971
972
  while (assembled.length > maxChars && working.length > 1) {
972
973
  working.pop();
973
- moreText = '... and more (see /impeccable audit).';
974
+ moreText = `... and more (see ${IMPECCABLE_COMMAND} audit).`;
974
975
  assembled = assemble(working, moreText);
975
976
  }
976
977
  if (assembled.length > maxChars) {
@@ -1002,7 +1003,7 @@ function formatFindingIgnoreCommand(finding) {
1002
1003
  const value = extractFindingIgnoreValueRaw(finding);
1003
1004
  const valueArg = quoteCommandArg(value);
1004
1005
  const reason = quoteCommandArg(`User confirmed ${value} is intentional`);
1005
- return `/impeccable hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`;
1006
+ return `${IMPECCABLE_COMMAND} hooks ignore-value ${rule} ${valueArg} --shared --reason ${reason}`;
1006
1007
  }
1007
1008
 
1008
1009
  function quoteCommandArg(value) {
@@ -1447,7 +1448,7 @@ export function designSystemOptions(config, detector, projectCwd) {
1447
1448
 
1448
1449
  export function appendDesignSystemNote(text, scanOptions) {
1449
1450
  if (!text || !scanOptions?.designSystem?.mdNewerThanJson) return text;
1450
- return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run /impeccable document to refresh the design-system sidecar.`;
1451
+ return `${text}\n\n${ENVELOPE_PREFIX} DESIGN.md is newer than .impeccable/design.json. Run ${IMPECCABLE_COMMAND} document to refresh the design-system sidecar.`;
1451
1452
  }
1452
1453
 
1453
1454
  // The directive footer is the part of the hook output that steers model
@@ -1464,16 +1465,16 @@ export function appendDesignSystemNote(text, scanOptions) {
1464
1465
  // raw envelope. Asking the model to surface the resolution in its
1465
1466
  // reply is the cheapest way to make the feedback loop visible.
1466
1467
  function directiveFooter(display, opts = {}) {
1467
- const ignoreFileCommand = `/impeccable hooks ignore-file ${quoteCommandArg(display)}`;
1468
+ const ignoreFileCommand = `${IMPECCABLE_COMMAND} hooks ignore-file ${quoteCommandArg(display)}`;
1468
1469
  const fileIgnoreGuidance = opts.grouped
1469
- ? 'run `/impeccable hooks ignore-file <path>` for the specific file'
1470
+ ? `run \`${IMPECCABLE_COMMAND} hooks ignore-file <path>\` for the specific file`
1470
1471
  : `run \`${ignoreFileCommand}\``;
1471
1472
  return [
1472
1473
  'Handle these before finalizing: fix findings that are real design problems, or explicitly classify contextually intentional findings as false positives. Acknowledge what you changed or why you are leaving a finding unchanged.',
1473
1474
  '',
1474
1475
  'Use context judgment before editing. A finding is not automatically a defect; literal or domain-appropriate motion, intentional demos or fixtures, documentation of bad design, and user-confirmed choices can be valid as-is.',
1475
1476
  '',
1476
- `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable <rule>\` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`/impeccable hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`/impeccable hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`/impeccable hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run /impeccable audit for the full pass.`,
1477
+ `Do not change intentional design just to satisfy the hook, and do not silence a real finding with an inline ignore comment to skip fixing it. Suppress a finding only after the user explicitly confirms it is intentional. Prefer a config ignore (one reviewable place, the commands below); reach for an inline \`impeccable-disable <rule>\` comment only when the waiver must travel with a file that leaves the repo, such as an exported or standalone document. Prefer the narrowest persisted exception: run the exact \`${IMPECCABLE_COMMAND} hooks ignore-value ... --shared\` command shown next to a value-specific finding. For \`overused-font\`, use \`ignore-value\` for a specific font and use \`${IMPECCABLE_COMMAND} hooks ignore-rule overused-font --all-values\` only when the user asks to ignore overused fonts generally. For file-specific findings without an ignore-value command, ${fileIgnoreGuidance}; use \`${IMPECCABLE_COMMAND} hooks ignore-rule <id>\` only when the user asks to suppress the whole non-value-specific rule. Run ${IMPECCABLE_COMMAND} audit for the full pass.`,
1477
1478
  ].join('\n');
1478
1479
  }
1479
1480
 
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { resolveProjectRoot } from '../context.mjs';
4
+ export { IMPECCABLE_COMMAND_PREFIX } from './provider.mjs';
4
5
 
5
6
  export const IMPECCABLE_DIR = '.impeccable';
6
7
  export const LIVE_DIR = 'live';
@@ -46,7 +46,7 @@ function isGitIgnored(absPath, cwd) {
46
46
  stdio: 'ignore',
47
47
  });
48
48
  return true; // exit 0 = ignored
49
- } catch {
49
+ } catch (err) {
50
50
  // Exit code 1 = not ignored. Exit code 128 = not a git repo or other error.
51
51
  // In both cases, treat as "not known to be ignored."
52
52
  return false;
@@ -0,0 +1,4 @@
1
+ // Source scripts default to slash commands. The provider build replaces only
2
+ // this exact declaration, avoiding heuristic rewrites across executable code.
3
+ export const IMPECCABLE_COMMAND_PREFIX = "$";
4
+ export const IMPECCABLE_COMMAND = `${IMPECCABLE_COMMAND_PREFIX}impeccable`;
@@ -32,10 +32,11 @@ export function readLiveBrowserScriptParts(parts, readFile = (filePath) => fs.re
32
32
  }));
33
33
  }
34
34
 
35
- export function assembleLiveBrowserScript({ token, port, vocabulary, parts }) {
35
+ export function assembleLiveBrowserScript({ token, port, vocabulary, commandPrefix = '/', parts }) {
36
36
  const prelude =
37
37
  `window.__IMPECCABLE_TOKEN__ = '${token}';\n` +
38
38
  `window.__IMPECCABLE_PORT__ = ${port};\n` +
39
+ `window.__IMPECCABLE_COMMAND_PREFIX__ = ${JSON.stringify(commandPrefix)};\n` +
39
40
  // Canonical command vocabulary (values + labels + icons). live-browser.js
40
41
  // builds its action picker from this instead of an inline copy.
41
42
  `window.__IMPECCABLE_VOCAB__ = ${JSON.stringify(vocabulary)};\n`;
@@ -95,11 +95,42 @@ export function substitutePropsWithExprs(markup, contract) {
95
95
  return out;
96
96
  }
97
97
 
98
+ function stripNamedBlocks(content, names) {
99
+ let out = String(content || '');
100
+ for (const name of names) {
101
+ const opener = `<${name}`;
102
+ const closer = `</${name}`;
103
+ while (true) {
104
+ const lower = out.toLowerCase();
105
+ const start = lower.indexOf(opener);
106
+ if (start === -1) break;
107
+ const openEnd = lower.indexOf('>', start + opener.length);
108
+ const closeStart = openEnd === -1 ? -1 : lower.indexOf(closer, openEnd + 1);
109
+ const closeEnd = closeStart === -1 ? -1 : lower.indexOf('>', closeStart + closer.length);
110
+ if (openEnd === -1 || closeStart === -1 || closeEnd === -1) break;
111
+ out = out.slice(0, start) + out.slice(closeEnd + 1);
112
+ }
113
+ }
114
+ while (true) {
115
+ const start = out.indexOf('<!--');
116
+ if (start === -1) break;
117
+ const normalEnd = out.indexOf('-->', start + 4);
118
+ const bangEnd = out.indexOf('--!>', start + 4);
119
+ const end = normalEnd === -1 ? bangEnd : bangEnd === -1 ? normalEnd : Math.min(normalEnd, bangEnd);
120
+ if (end === -1) break;
121
+ out = out.slice(0, start) + out.slice(end + (end === bangEnd ? 4 : 3));
122
+ }
123
+ return out;
124
+ }
125
+
98
126
  export function parseSvelteComponentFile(content) {
99
127
  const text = String(content || '');
100
- const scriptMatch = text.match(/^([\s\S]*?)<script\b[^>]*>[\s\S]*?<\/script\b[^>]*>/i);
101
- const withoutScript = scriptMatch ? text.slice(scriptMatch[0].length) : text;
102
- const styleMatch = withoutScript.match(/<style\b[^>]*>[\s\S]*?<\/style\b[^>]*>/i);
128
+ const lower = text.toLowerCase();
129
+ const scriptStart = lower.indexOf('<script');
130
+ const scriptClose = scriptStart === -1 ? -1 : lower.indexOf('</script', scriptStart + 7);
131
+ const scriptEnd = scriptClose === -1 ? -1 : lower.indexOf('>', scriptClose + 8);
132
+ const withoutScript = scriptEnd === -1 ? text : text.slice(scriptEnd + 1);
133
+ const styleMatch = withoutScript.match(/<style\b[^>]*>[\s\S]*?<\/style\s*>/i);
103
134
  const styleBlock = styleMatch ? styleMatch[0] : '';
104
135
  const markup = styleMatch
105
136
  ? withoutScript.slice(0, styleMatch.index).trim()
@@ -107,7 +138,7 @@ export function parseSvelteComponentFile(content) {
107
138
  const cssLines = styleBlock
108
139
  ? styleBlock
109
140
  .replace(/^<style\b[^>]*>/i, '')
110
- .replace(/<\/style\b[^>]*>$/i, '')
141
+ .replace(/<\/style\s*>$/i, '')
111
142
  .split('\n')
112
143
  .map((line) => line.trimEnd())
113
144
  : [];
@@ -290,7 +321,7 @@ function appendCssToSvelteStyle(lines, cssLines) {
290
321
 
291
322
  function findLastStyleCloseLine(lines) {
292
323
  for (let i = lines.length - 1; i >= 0; i--) {
293
- if (/<\/style\b[^>]*>/.test(lines[i])) return i;
324
+ if (/<\/style\s*>/.test(lines[i])) return i;
294
325
  }
295
326
  return -1;
296
327
  }
@@ -631,17 +662,8 @@ function inlineSvelteComponentInsertAccept({
631
662
  };
632
663
  }
633
664
 
634
- function svelteMarkupHasVisibleContent(markup) {
635
- let stripped = String(markup || '');
636
- let previous;
637
- do {
638
- previous = stripped;
639
- stripped = stripped
640
- .replace(/<script[\s\S]*?<\/script\b[^>]*>/gi, ' ')
641
- .replace(/<style[\s\S]*?<\/style\b[^>]*>/gi, ' ')
642
- .replace(/<!--[\s\S]*?-->/g, '');
643
- } while (stripped !== previous);
644
- const text = stripped
665
+ export function svelteMarkupHasVisibleContent(markup) {
666
+ const text = stripNamedBlocks(markup, ['script', 'style'])
645
667
  .replace(/<[^>]+>/g, ' ')
646
668
  .replace(/\s+/g, ' ')
647
669
  .trim();
@@ -400,18 +400,12 @@ function readHtmlAttr(tag, name) {
400
400
  return decodeHtmlAttr(match[2]);
401
401
  }
402
402
 
403
- const HTML_ATTR_ENTITIES = {
404
- '&quot;': '"',
405
- '&lt;': '<',
406
- '&gt;': '>',
407
- '&amp;': '&',
408
- };
409
-
410
403
  function decodeHtmlAttr(value) {
411
- return String(value || '').replace(
412
- /&(?:quot|lt|gt|amp);/g,
413
- (entity) => HTML_ATTR_ENTITIES[entity] || entity,
414
- );
404
+ return String(value || '')
405
+ .replace(/&quot;/g, '"')
406
+ .replace(/&lt;/g, '<')
407
+ .replace(/&gt;/g, '>')
408
+ .replace(/&amp;/g, '&');
415
409
  }
416
410
 
417
411
  // ---------------------------------------------------------------------------
@@ -527,6 +521,26 @@ function hasVariantWrapperAttr(line, id) {
527
521
  * - Same-line `<style>…</style>` blocks
528
522
  * - Multi-line `<style>\n…\n</style>` blocks
529
523
  */
524
+ function removeCompleteStyleElements(value) {
525
+ let out = value;
526
+ while (true) {
527
+ const lower = out.toLowerCase();
528
+ const start = lower.indexOf('<style');
529
+ if (start === -1) break;
530
+ const openEnd = lower.indexOf('>', start + 6);
531
+ if (openEnd === -1) break;
532
+ if (lower.slice(start, openEnd).trimEnd().endsWith('/')) {
533
+ out = out.slice(0, start) + out.slice(openEnd + 1);
534
+ continue;
535
+ }
536
+ const closeStart = lower.indexOf('</style', openEnd + 1);
537
+ const closeEnd = closeStart === -1 ? -1 : lower.indexOf('>', closeStart + 7);
538
+ if (closeStart === -1 || closeEnd === -1) break;
539
+ out = out.slice(0, start) + out.slice(closeEnd + 1);
540
+ }
541
+ return out;
542
+ }
543
+
530
544
  function stripStyleAndJoin(lines, block) {
531
545
  const out = [];
532
546
  let inStyle = false;
@@ -536,13 +550,11 @@ function stripStyleAndJoin(lines, block) {
536
550
  if (!inStyle) {
537
551
  // Strip any complete <style> elements on this line (self-closed or
538
552
  // same-line-closed), including their body content.
539
- line = line
540
- .replace(/<style\b[^>]*>[\s\S]*?<\/style\b[^>]*>/g, ' ')
541
- .replace(/<style\b[^>]*\/\s*>/g, ' ');
553
+ line = removeCompleteStyleElements(line);
542
554
 
543
555
  // If a <style> opener remains (multi-line body starts here), strip from
544
556
  // the opener to end-of-line and flip into skip mode.
545
- const openerIdx = line.search(/<style\b/);
557
+ const openerIdx = line.toLowerCase().indexOf('<style');
546
558
  if (openerIdx !== -1) {
547
559
  line = line.slice(0, openerIdx);
548
560
  inStyle = true;
@@ -550,10 +562,12 @@ function stripStyleAndJoin(lines, block) {
550
562
  out.push(line);
551
563
  } else {
552
564
  // In multi-line style body; drop everything until we see </style>.
553
- const closeIdx = line.search(/<\/style\b[^>]*>/);
565
+ const closeIdx = line.toLowerCase().indexOf('</style');
554
566
  if (closeIdx !== -1) {
567
+ const closeEnd = line.indexOf('>', closeIdx + 7);
568
+ if (closeEnd === -1) continue;
555
569
  inStyle = false;
556
- out.push(line.slice(closeIdx).replace(/<\/style\b[^>]*>/, ' '));
570
+ out.push(line.slice(closeEnd + 1));
557
571
  }
558
572
  // else: skip line entirely
559
573
  }
@@ -643,7 +657,7 @@ function extractCss(lines, block, id) {
643
657
  // Self-closing: nothing to carbonize.
644
658
  if (/<style\b[^>]*\/\s*>/.test(line)) return null;
645
659
  // Same-line open + close: extract inner text.
646
- const sameLine = line.match(/<style\b[^>]*>([\s\S]*?)<\/style\b[^>]*>/);
660
+ const sameLine = line.match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/);
647
661
  if (sameLine) {
648
662
  const inner = stripJsxTemplateWrap(sameLine[1]);
649
663
  return inner.length > 0 ? inner.split('\n') : null;
@@ -656,7 +670,7 @@ function extractCss(lines, block, id) {
656
670
  // Detect </style> anywhere on the line — JSX template-literal closes
657
671
  // (`}</style>`) put the close mid-line, and we don't want to absorb the
658
672
  // template-literal punctuation as CSS content.
659
- const closeIdx = line.search(/<\/style\b[^>]*>/);
673
+ const closeIdx = line.indexOf('</style>');
660
674
  if (closeIdx !== -1) break;
661
675
  content.push(line);
662
676
  }
@@ -57,6 +57,7 @@
57
57
  const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 };
58
58
  const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint
59
59
  const PREFIX = 'impeccable-live';
60
+ const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable';
60
61
  const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style';
61
62
  const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
62
63
  const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
@@ -5076,7 +5077,7 @@
5076
5077
  }
5077
5078
 
5078
5079
  function extractSvelteComponentStyle(source) {
5079
- const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\b[^>]*>/i);
5080
+ const match = String(source || '').match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i);
5080
5081
  return match ? match[1].trim() : '';
5081
5082
  }
5082
5083
 
@@ -5104,8 +5105,7 @@
5104
5105
  }
5105
5106
 
5106
5107
  function scopeCssToSveltePreview(css, sessionId) {
5107
- const escapedSessionId = String(sessionId).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
5108
- const prefix = '[data-impeccable-variants="' + escapedSessionId + '"] ';
5108
+ const prefix = '[data-impeccable-variants="' + String(sessionId).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"] ';
5109
5109
  return scopeCssBlock(String(css || ''), prefix).trim();
5110
5110
  }
5111
5111
 
@@ -5606,7 +5606,7 @@
5606
5606
  if (!/\.[cm]?[jt]sx$/i.test(String(filePath || ''))) return block;
5607
5607
  return String(block)
5608
5608
  .replace(
5609
- /<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style\b[^>]*>/g,
5609
+ /<style\b([^>]*)>\s*\{\s*`([\s\S]*?)`\s*\}\s*<\/style>/g,
5610
5610
  (_match, attrs, css) => '<style' + attrs + '>' + css + '</style>',
5611
5611
  )
5612
5612
  .replace(/\bclassName\s*=\s*\{\s*`([^`]*?)`\s*\}/g, (_match, value) => {
@@ -5637,7 +5637,7 @@
5637
5637
  let out = String(prop || '').trim().replace(/^["']|["']$/g, '');
5638
5638
  if (!out) return '';
5639
5639
  if (out.startsWith('--')) return out;
5640
- return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase()).replace(/^ms-/, '-ms-');
5640
+ return out.replace(/[A-Z]/g, (ch) => '-' + ch.toLowerCase());
5641
5641
  }
5642
5642
 
5643
5643
  function buildSvelteExpressionTextMap(sourceOriginal, liveOriginal) {
@@ -6146,7 +6146,7 @@
6146
6146
  switch (msg.type) {
6147
6147
  case 'connected':
6148
6148
  hasProjectContext = !!msg.hasProjectContext;
6149
- if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000);
6149
+ if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
6150
6150
  console.log('[impeccable] Live mode connected.');
6151
6151
  syncAgentPollingUi(!!msg.agentPolling);
6152
6152
  startAgentStatusPoll();
@@ -10539,7 +10539,7 @@ void main() {
10539
10539
  if (designState.present === false) {
10540
10540
  const empty = document.createElement('div');
10541
10541
  empty.className = 'empty';
10542
- empty.innerHTML = `<strong>No DESIGN.md yet</strong>Create one by running <code>/impeccable document</code> in your terminal, then re-open this panel.`;
10542
+ empty.innerHTML = `<strong>No DESIGN.md yet</strong>Create one by running <code>${IMPECCABLE_COMMAND} document</code> in your terminal, then re-open this panel.`;
10543
10543
  body.appendChild(empty);
10544
10544
  return;
10545
10545
  }
@@ -10569,7 +10569,7 @@ void main() {
10569
10569
  box.className = 'stale';
10570
10570
  box.innerHTML = `
10571
10571
  <span class="stale-dot"></span>
10572
- <span class="stale-text"><strong>DESIGN.md is newer than .impeccable/design.json.</strong> Run <code>/impeccable document</code> to refresh the sidecar.</span>
10572
+ <span class="stale-text"><strong>DESIGN.md is newer than .impeccable/design.json.</strong> Run <code>${IMPECCABLE_COMMAND} document</code> to refresh the sidecar.</span>
10573
10573
  `;
10574
10574
  return box;
10575
10575
  }
@@ -10577,7 +10577,7 @@ void main() {
10577
10577
  function renderParsedMdCta() {
10578
10578
  const box = document.createElement('div');
10579
10579
  box.className = 'parsed-md-cta';
10580
- box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code>/impeccable document</code> also generates a <code>.impeccable/design.json</code> sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`;
10580
+ box.innerHTML = `<strong>Basic view</strong>This panel reads the tokens in your <code>DESIGN.md</code> frontmatter. Running <code>${IMPECCABLE_COMMAND} document</code> also generates a <code>.impeccable/design.json</code> sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`;
10581
10581
  return box;
10582
10582
  }
10583
10583
 
@@ -36,6 +36,7 @@ import {
36
36
  getDesignSidecarPath,
37
37
  getLiveDir,
38
38
  getLiveAnnotationsDir,
39
+ IMPECCABLE_COMMAND_PREFIX,
39
40
  readLiveServerInfo,
40
41
  removeLiveServerInfo,
41
42
  resolveDesignSidecarPath,
@@ -413,6 +414,7 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
413
414
  token: state.token,
414
415
  port: state.port,
415
416
  vocabulary: LIVE_COMMANDS,
417
+ commandPrefix: IMPECCABLE_COMMAND_PREFIX,
416
418
  parts,
417
419
  });
418
420
  res.writeHead(200, {