@bastani/atomic 0.8.20-0 → 0.8.21-0

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 (127) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/CHANGELOG.md +5 -0
  4. package/dist/builtin/mcp/package.json +1 -1
  5. package/dist/builtin/subagents/CHANGELOG.md +5 -0
  6. package/dist/builtin/subagents/agents/code-simplifier.md +78 -22
  7. package/dist/builtin/subagents/agents/debugger.md +4 -3
  8. package/dist/builtin/subagents/package.json +1 -1
  9. package/dist/builtin/web-access/CHANGELOG.md +5 -0
  10. package/dist/builtin/web-access/package.json +1 -1
  11. package/dist/builtin/workflows/CHANGELOG.md +25 -0
  12. package/dist/builtin/workflows/package.json +1 -1
  13. package/dist/builtin/workflows/skills/create-spec/SKILL.md +169 -125
  14. package/dist/builtin/workflows/skills/impeccable/SKILL.md +89 -80
  15. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
  16. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
  17. package/dist/builtin/workflows/skills/impeccable/agents/openai.yaml +4 -0
  18. package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +122 -1
  19. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +38 -12
  20. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +5 -5
  21. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +7 -7
  22. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +4 -14
  23. package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +115 -1
  24. package/dist/builtin/workflows/skills/impeccable/reference/codex.md +3 -3
  25. package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +109 -6
  26. package/dist/builtin/workflows/skills/impeccable/reference/craft.md +7 -7
  27. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +623 -94
  28. package/dist/builtin/workflows/skills/impeccable/reference/delight.md +2 -2
  29. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
  30. package/dist/builtin/workflows/skills/impeccable/reference/document.md +16 -14
  31. package/dist/builtin/workflows/skills/impeccable/reference/extract.md +1 -1
  32. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -1
  33. package/dist/builtin/workflows/skills/impeccable/reference/init.md +172 -0
  34. package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -6
  35. package/dist/builtin/workflows/skills/impeccable/reference/layout.md +33 -13
  36. package/dist/builtin/workflows/skills/impeccable/reference/live.md +96 -19
  37. package/dist/builtin/workflows/skills/impeccable/reference/onboard.md +1 -1
  38. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +1 -1
  39. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -1
  40. package/dist/builtin/workflows/skills/impeccable/reference/polish.md +3 -4
  41. package/dist/builtin/workflows/skills/impeccable/reference/product.md +1 -3
  42. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +2 -2
  43. package/dist/builtin/workflows/skills/impeccable/reference/shape.md +5 -5
  44. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +158 -3
  45. package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +1 -1
  46. package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +2 -2
  47. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +225 -0
  48. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +266 -0
  49. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +17 -1
  50. package/dist/builtin/workflows/skills/impeccable/scripts/design-parser.mjs +16 -1
  51. package/dist/builtin/workflows/skills/impeccable/scripts/detect.mjs +21 -0
  52. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +1725 -0
  53. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
  54. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4543 -0
  55. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
  56. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
  57. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
  58. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
  59. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
  60. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  61. package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +12 -0
  62. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
  63. package/dist/builtin/workflows/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  64. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
  65. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2316 -0
  66. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  67. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
  68. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  69. package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +17 -1
  70. package/dist/builtin/workflows/skills/impeccable/scripts/is-generated.mjs +2 -2
  71. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +139 -96
  72. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +4491 -526
  73. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
  74. package/dist/builtin/workflows/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
  75. package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
  76. package/dist/builtin/workflows/skills/impeccable/scripts/live-event-validation.mjs +136 -0
  77. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +22 -9
  78. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
  79. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +232 -0
  80. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
  81. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
  82. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +288 -110
  83. package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +47 -1
  84. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +1443 -100
  85. package/dist/builtin/workflows/skills/impeccable/scripts/live-session-store.mjs +17 -0
  86. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +17 -3
  87. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +216 -6
  88. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +2 -3
  89. package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +633 -0
  90. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +1 -1
  91. package/dist/builtin/workflows/src/extension/index.ts +67 -3
  92. package/dist/builtin/workflows/src/extension/render-result.ts +26 -1
  93. package/dist/builtin/workflows/src/runs/foreground/executor.ts +227 -3
  94. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +94 -7
  95. package/dist/builtin/workflows/src/shared/stage-prompt.ts +326 -0
  96. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +62 -7
  97. package/dist/builtin/workflows/src/shared/store-types.ts +43 -0
  98. package/dist/builtin/workflows/src/shared/store.ts +37 -0
  99. package/dist/builtin/workflows/src/tui/chat-surface-message.ts +22 -4
  100. package/dist/builtin/workflows/src/tui/graph-view.ts +47 -0
  101. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +43 -1
  102. package/dist/builtin/workflows/src/tui/run-detail.ts +10 -4
  103. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +117 -15
  104. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +9 -0
  105. package/dist/core/skills.d.ts.map +1 -1
  106. package/dist/core/skills.js +2 -5
  107. package/dist/core/skills.js.map +1 -1
  108. package/dist/core/system-prompt.d.ts.map +1 -1
  109. package/dist/core/system-prompt.js +11 -29
  110. package/dist/core/system-prompt.js.map +1 -1
  111. package/dist/index.d.ts +1 -0
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +3 -0
  114. package/dist/index.js.map +1 -1
  115. package/docs/quickstart.md +1 -2
  116. package/package.json +4 -4
  117. package/dist/builtin/workflows/skills/impeccable/reference/cognitive-load.md +0 -106
  118. package/dist/builtin/workflows/skills/impeccable/reference/color-and-contrast.md +0 -105
  119. package/dist/builtin/workflows/skills/impeccable/reference/heuristics-scoring.md +0 -234
  120. package/dist/builtin/workflows/skills/impeccable/reference/motion-design.md +0 -109
  121. package/dist/builtin/workflows/skills/impeccable/reference/personas.md +0 -179
  122. package/dist/builtin/workflows/skills/impeccable/reference/responsive-design.md +0 -114
  123. package/dist/builtin/workflows/skills/impeccable/reference/spatial-design.md +0 -100
  124. package/dist/builtin/workflows/skills/impeccable/reference/teach.md +0 -156
  125. package/dist/builtin/workflows/skills/impeccable/reference/typography.md +0 -159
  126. package/dist/builtin/workflows/skills/impeccable/reference/ux-writing.md +0 -107
  127. package/dist/builtin/workflows/skills/impeccable/scripts/load-context.mjs +0 -141
@@ -209,6 +209,23 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
209
209
  next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
210
210
  next.pendingEvent = toPendingEvent(event);
211
211
  break;
212
+ case 'manual_edit_apply':
213
+ next.phase = 'manual_edit_apply_requested';
214
+ next.pageUrl = event.pageUrl ?? next.pageUrl;
215
+ next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
216
+ next.pendingEvent = toPendingEvent(event);
217
+ break;
218
+ case 'steer':
219
+ next.phase = 'steer_requested';
220
+ next.pageUrl = event.pageUrl ?? next.pageUrl;
221
+ next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
222
+ next.pendingEvent = toPendingEvent(event);
223
+ break;
224
+ case 'steer_done':
225
+ next.phase = 'steer_done';
226
+ next.pendingEventSeq = null;
227
+ next.pendingEvent = null;
228
+ break;
212
229
  case 'discard':
213
230
  next.phase = 'discard_requested';
214
231
  next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { createLiveSessionStore } from './live-session-store.mjs';
7
7
  import { readLiveServerInfo } from './impeccable-paths.mjs';
8
+ import { manualApplyResumeHint } from './live-resume.mjs';
8
9
 
9
10
  function readServerInfo() {
10
11
  return readLiveServerInfo(process.cwd())?.info || null;
@@ -26,21 +27,34 @@ export async function statusCli() {
26
27
  const server = await fetchServerStatus(info);
27
28
  const store = createLiveSessionStore({ cwd: process.cwd() });
28
29
  const activeSessions = store.listActiveSessions();
30
+ const manualApply = findPendingManualApply(server, activeSessions);
29
31
  const payload = {
30
32
  liveServer: server ? {
31
33
  status: server.status,
32
34
  port: server.port,
33
35
  connectedClients: server.connectedClients,
36
+ agentPolling: server.agentPolling,
34
37
  pendingEvents: server.pendingEvents,
35
38
  } : null,
36
39
  activeSessions: server?.activeSessions || activeSessions,
37
- recoveryHint: server
38
- ? 'Run live-poll.mjs to continue pending work, or live-complete.mjs --id <session> after manual cleanup.'
39
- : 'Start live-server.mjs to requeue pending durable events, then run live-poll.mjs.',
40
+ recoveryHint: manualApply
41
+ ? manualApplyResumeHint(manualApply)
42
+ : server
43
+ ? 'Run live-poll.mjs to continue pending work, or live-complete.mjs --id <session> after manual cleanup.'
44
+ : 'Start live-server.mjs to requeue pending durable events, then run live-poll.mjs.',
40
45
  };
41
46
  console.log(JSON.stringify(payload, null, 2));
42
47
  }
43
48
 
49
+ function findPendingManualApply(server, activeSessions) {
50
+ const fromServer = server?.pendingEvents?.find((event) => event?.type === 'manual_edit_apply');
51
+ if (fromServer) return fromServer;
52
+ const fromSession = activeSessions
53
+ ?.map((session) => session.pendingEvent)
54
+ .find((event) => event?.type === 'manual_edit_apply');
55
+ return fromSession || null;
56
+ }
57
+
44
58
  const _running = process.argv[1];
45
59
  if (_running?.endsWith('live-status.mjs') || _running?.endsWith('live-status.mjs/')) {
46
60
  statusCli();
@@ -14,6 +14,7 @@
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import { isGeneratedFile } from './is-generated.mjs';
17
+ import { readBuffer as readManualEditsBuffer } from './live-manual-edits-buffer.mjs';
17
18
 
18
19
  const EXTENSIONS = ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro'];
19
20
 
@@ -31,7 +32,7 @@ Required:
31
32
 
32
33
  Element identification (at least one required):
33
34
  --element-id ID HTML id attribute of the element
34
- --classes A,B,C Comma-separated CSS class names
35
+ --classes A,B,C Comma- or space-separated CSS class names
35
36
  --tag TAG Tag name (div, section, etc.)
36
37
  --query TEXT Fallback: raw text to search for
37
38
 
@@ -41,6 +42,9 @@ Optional:
41
42
  classes/tag match multiple sibling elements (e.g. a list
42
43
  of <Card>s with the same className). Pass the first ~80
43
44
  chars of event.element.textContent.
45
+ --page-url URL Current page URL. Required when pending manual edits may
46
+ affect the picked source block. Pending edits are filtered
47
+ to this page so an edit on /a doesn't bleed into /b.
44
48
  --help Show this help message
45
49
 
46
50
  Output (JSON):
@@ -58,6 +62,7 @@ The agent should insert variant HTML at insertLine.`);
58
62
  const query = argVal(args, '--query');
59
63
  const filePath = argVal(args, '--file');
60
64
  const text = argVal(args, '--text');
65
+ const pageUrl = argVal(args, '--page-url');
61
66
 
62
67
  if (!id) { console.error('Missing --id'); process.exit(1); }
63
68
  if (!elementId && !classes && !query) {
@@ -196,7 +201,62 @@ The agent should insert variant HTML at insertLine.`);
196
201
  // the inner element at its parent's depth instead of nested inside it.
197
202
  // Strip only the COMMON minimum leading whitespace across the picked lines;
198
203
  // `deindentContent` on the accept side already mirrors this convention.
199
- const originalLines = lines.slice(startLine, endLine + 1);
204
+ let originalLines = lines.slice(startLine, endLine + 1);
205
+
206
+ // Buffer-aware "original" content: if the user has pending manual edits for
207
+ // this page whose originalText appears in the picked source range, apply
208
+ // them so the wrap block's "original" variant reflects what the user was
209
+ // looking at (their edited DOM), not the raw source. Source itself stays
210
+ // untouched here — only the wrap block's embedded "original" copy is
211
+ // adjusted. The pending edits remain in the buffer until committed.
212
+ //
213
+ // Apply buffered edits only when the browser provided the current page URL.
214
+ // Without it, fail if pending edits plausibly touch this exact source range;
215
+ // otherwise skip buffer awareness so unrelated staged edits on another page
216
+ // do not block normal wrap work.
217
+ let pendingBuffer = { entries: [] };
218
+ try { pendingBuffer = readManualEditsBuffer(process.cwd()); } catch {}
219
+ const pendingEntriesForTarget = pageUrl
220
+ ? []
221
+ : pendingEntriesThatMayAffectWrap(pendingBuffer.entries, targetFile, originalLines, startLine, process.cwd());
222
+ if (pendingEntriesForTarget.length > 0) {
223
+ console.error(JSON.stringify({
224
+ error: 'missing_page_url_with_pending_edits',
225
+ pendingEntries: pendingEntriesForTarget.length,
226
+ hint: 'Pending manual edits may affect the selected source block. Pass --page-url=$event.pageUrl so the wrap block reflects the user\'s staged DOM.',
227
+ }));
228
+ process.exit(1);
229
+ }
230
+ if (pageUrl) {
231
+ const failedBufferedOps = [];
232
+ for (const entry of pendingBuffer.entries || []) {
233
+ if (entry.pageUrl !== pageUrl) continue;
234
+ for (const op of entry.ops || []) {
235
+ const mayAffectWrap = manualEditMayAffectWrap(op, targetFile, originalLines, startLine, process.cwd());
236
+ const result = applyBufferedManualEditToLines(originalLines, startLine, op);
237
+ if (result.changed) {
238
+ originalLines = result.lines;
239
+ continue;
240
+ }
241
+ if (!mayAffectWrap) continue;
242
+ failedBufferedOps.push({
243
+ entryId: entry.id,
244
+ ref: op?.ref || null,
245
+ originalText: op?.originalText || null,
246
+ reason: 'ambiguous_or_unmatched_pending_edit',
247
+ });
248
+ }
249
+ }
250
+ if (failedBufferedOps.length > 0) {
251
+ console.error(JSON.stringify({
252
+ error: 'manual_edit_buffer_apply_failed',
253
+ pendingOps: failedBufferedOps,
254
+ hint: 'A staged copy edit appears to affect the selected source block, but could not be applied unambiguously to the wrap original. Apply or discard copy edits first, or write the wrapper manually.',
255
+ }));
256
+ process.exit(1);
257
+ }
258
+ }
259
+
200
260
  const originalBaseIndent = minLeadingSpaces(originalLines);
201
261
  const reindentOriginal = (extra) => originalLines
202
262
  .map((l) => (l.trim() === '' ? '' : indent + extra + l.slice(originalBaseIndent)))
@@ -283,10 +343,140 @@ The agent should insert variant HTML at insertLine.`);
283
343
  // ---------------------------------------------------------------------------
284
344
 
285
345
  function argVal(args, flag) {
346
+ const prefix = flag + '=';
347
+ for (const arg of args) {
348
+ if (arg.startsWith(prefix)) return arg.slice(prefix.length);
349
+ }
286
350
  const idx = args.indexOf(flag);
287
351
  return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null;
288
352
  }
289
353
 
354
+ function pendingEntriesThatMayAffectWrap(entries, targetFile, originalLines, selectionStartLine, cwd) {
355
+ const targetAbs = path.resolve(cwd, targetFile);
356
+ return (entries || []).filter((entry) => {
357
+ return (entry.ops || []).some((op) => {
358
+ return manualEditMayAffectWrap(op, targetAbs, originalLines, selectionStartLine, cwd);
359
+ });
360
+ });
361
+ }
362
+
363
+ function manualEditMayAffectWrap(op, targetFile, originalLines, selectionStartLine, cwd) {
364
+ const targetAbs = path.resolve(cwd, targetFile);
365
+ if (manualEditHintFallsInsideSelection(op, targetAbs, originalLines, selectionStartLine, cwd)) return true;
366
+ if (manualEditLocatorMatchesSelection(op, originalLines)) return true;
367
+ if (typeof op?.originalText === 'string' && op.originalText.length > 0) {
368
+ return originalLines.join('\n').includes(op.originalText);
369
+ }
370
+ return false;
371
+ }
372
+
373
+ function manualEditHintFallsInsideSelection(op, targetAbs, originalLines, selectionStartLine, cwd) {
374
+ const hintFile = op?.sourceHint?.file;
375
+ const hintedLine = Number(op?.sourceHint?.line);
376
+ if (!hintFile || !Number.isFinite(hintedLine)) return false;
377
+ const hintAbs = path.isAbsolute(hintFile) ? hintFile : path.resolve(cwd, hintFile);
378
+ if (path.resolve(hintAbs) !== targetAbs) return false;
379
+ const hintedIndex = hintedLine - 1 - selectionStartLine;
380
+ return hintedIndex >= 0
381
+ && hintedIndex < originalLines.length
382
+ && typeof op?.originalText === 'string'
383
+ && originalLines[hintedIndex].includes(op.originalText);
384
+ }
385
+
386
+ function manualEditLocatorMatchesSelection(op, originalLines) {
387
+ if (!op || typeof op.originalText !== 'string' || op.originalText.length === 0) return false;
388
+ return originalLines.some((line) => (
389
+ line.includes(op.originalText) && lineMatchesManualEditLocator(line, op)
390
+ ));
391
+ }
392
+
393
+ function applyBufferedManualEditToLines(originalLines, selectionStartLine, op) {
394
+ if (
395
+ !op
396
+ || typeof op.originalText !== 'string'
397
+ || op.originalText.length === 0
398
+ || typeof op.newText !== 'string'
399
+ ) {
400
+ return { lines: originalLines, changed: false };
401
+ }
402
+
403
+ const replaceLine = (lineIndex) => ({
404
+ lines: originalLines.map((line, index) => (
405
+ index === lineIndex ? replaceOnce(line, op.originalText, op.newText) : line
406
+ )),
407
+ changed: true,
408
+ });
409
+
410
+ const hintedLine = Number(op.sourceHint?.line);
411
+ if (Number.isFinite(hintedLine)) {
412
+ const hintedIndex = hintedLine - 1 - selectionStartLine;
413
+ if (hintedIndex >= 0 && hintedIndex < originalLines.length && originalLines[hintedIndex].includes(op.originalText)) {
414
+ return replaceLine(hintedIndex);
415
+ }
416
+ }
417
+
418
+ const locatorMatches = [];
419
+ for (let index = 0; index < originalLines.length; index += 1) {
420
+ const line = originalLines[index];
421
+ if (!line.includes(op.originalText)) continue;
422
+ if (!lineMatchesManualEditLocator(line, op)) continue;
423
+ locatorMatches.push(index);
424
+ }
425
+ if (locatorMatches.length === 1) return replaceLine(locatorMatches[0]);
426
+
427
+ const originalBlock = originalLines.join('\n');
428
+ if (countOccurrences(originalBlock, op.originalText) === 1) {
429
+ return {
430
+ lines: replaceOnce(originalBlock, op.originalText, op.newText).split('\n'),
431
+ changed: true,
432
+ };
433
+ }
434
+
435
+ return { lines: originalLines, changed: false };
436
+ }
437
+
438
+ function lineMatchesManualEditLocator(line, op) {
439
+ if (op.tag) {
440
+ const tagRe = new RegExp('<\\s*' + escapeRegExp(op.tag) + '(?=[\\s>/]|$)', 'i');
441
+ if (!tagRe.test(line)) return false;
442
+ }
443
+
444
+ if (op.elementId) {
445
+ const id = escapeRegExp(op.elementId);
446
+ const idRe = new RegExp('\\bid\\s*=\\s*["\']' + id + '["\']');
447
+ if (!idRe.test(line)) return false;
448
+ }
449
+
450
+ const classes = Array.isArray(op.classes) ? op.classes.filter(Boolean) : [];
451
+ for (const className of classes) {
452
+ if (!line.includes(className)) return false;
453
+ }
454
+
455
+ return true;
456
+ }
457
+
458
+ function replaceOnce(value, needle, replacement) {
459
+ const index = value.indexOf(needle);
460
+ if (index === -1) return value;
461
+ return value.slice(0, index) + replacement + value.slice(index + needle.length);
462
+ }
463
+
464
+ function countOccurrences(value, needle) {
465
+ if (!needle) return 0;
466
+ let count = 0;
467
+ let index = 0;
468
+ while (true) {
469
+ index = value.indexOf(needle, index);
470
+ if (index === -1) return count;
471
+ count += 1;
472
+ index += needle.length;
473
+ }
474
+ }
475
+
476
+ function escapeRegExp(value) {
477
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
478
+ }
479
+
290
480
  /**
291
481
  * Build search query strings in priority order (most specific first).
292
482
  * ID is most reliable, then specific class combos, then single classes, then raw query.
@@ -303,13 +493,15 @@ function buildSearchQueries(elementId, classes, tag, query) {
303
493
  // Emit both class="..." (HTML) and className="..." (React/JSX) so whichever
304
494
  // convention the file uses will match.
305
495
  if (classes) {
306
- const classList = classes.split(',').map(c => c.trim()).filter(Boolean);
496
+ const classList = splitClassList(classes);
307
497
  if (classList.length > 1) {
308
498
  const joined = classList.join(' ');
309
499
  const sorted = [...classList].sort((a, b) => b.length - a.length);
310
500
  queries.push('class="' + joined + '"');
311
501
  queries.push('className="' + joined + '"');
312
- queries.push(sorted[0]); // most distinctive single class, fallback
502
+ for (const className of sorted) {
503
+ queries.push(className);
504
+ }
313
505
  } else if (classList.length === 1) {
314
506
  queries.push(classList[0]);
315
507
  }
@@ -318,7 +510,7 @@ function buildSearchQueries(elementId, classes, tag, query) {
318
510
  // 3. Tag + class combo (e.g., <section class="hero">).
319
511
  // Same dual-emit for JSX compatibility.
320
512
  if (tag && classes) {
321
- const firstClass = classes.split(',')[0].trim();
513
+ const firstClass = splitClassList(classes)[0];
322
514
  queries.push('<' + tag + ' class="' + firstClass);
323
515
  queries.push('<' + tag + ' className="' + firstClass);
324
516
  }
@@ -331,6 +523,10 @@ function buildSearchQueries(elementId, classes, tag, query) {
331
523
  return queries;
332
524
  }
333
525
 
526
+ function splitClassList(classes) {
527
+ return String(classes).split(/[,\s]+/).map(c => c.trim()).filter(Boolean);
528
+ }
529
+
334
530
  function detectCommentSyntax(filePath) {
335
531
  const ext = path.extname(filePath).toLowerCase();
336
532
  if (ext === '.jsx' || ext === '.tsx') {
@@ -370,11 +566,14 @@ function buildCssAuthoring(styleMode, count) {
370
566
  selectorExamples: variantNumbers.map((n) => `[data-impeccable-variant="${n}"] > .variant-class`),
371
567
  requirements: [
372
568
  'Use the styleTag exactly; the is:inline attribute is required for this file.',
569
+ 'Put raw CSS directly between the styleTag opening and a plain </style> close.',
373
570
  'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.',
374
571
  'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.',
375
572
  ],
376
573
  forbidden: [
377
574
  'Do not use @scope for this styleMode.',
575
+ 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.',
576
+ 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.',
378
577
  ],
379
578
  };
380
579
  }
@@ -629,4 +828,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/'))
629
828
  }
630
829
 
631
830
  // Test exports (used by tests/live-wrap.test.mjs)
632
- export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax };
831
+ export {
832
+ buildSearchQueries,
833
+ findElement,
834
+ findClosingLine,
835
+ detectCommentSyntax,
836
+ findAllElements,
837
+ filterByText,
838
+ findFileWithQuery,
839
+ detectStyleMode,
840
+ buildCssAuthoring,
841
+ buildCssSelectorPrefixExamples,
842
+ };
@@ -21,7 +21,7 @@ import { execSync } from 'node:child_process';
21
21
  import fs from 'node:fs';
22
22
  import path from 'node:path';
23
23
  import { fileURLToPath } from 'node:url';
24
- import { loadContext } from './load-context.mjs';
24
+ import { loadContext } from './context.mjs';
25
25
  import { resolveFiles } from './live-inject.mjs';
26
26
  import { readLiveServerInfo } from './impeccable-paths.mjs';
27
27
 
@@ -80,7 +80,7 @@ The agent should then:
80
80
  process.exit(1);
81
81
  }
82
82
 
83
- // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md)
83
+ // 4. Load PRODUCT.md + DESIGN.md context.
84
84
  const ctx = loadContext(process.cwd());
85
85
 
86
86
  // 5. Compute drift-heal: compare resolved inject targets against the
@@ -102,7 +102,6 @@ The agent should then:
102
102
  hasDesign: ctx.hasDesign,
103
103
  design: ctx.design,
104
104
  designPath: ctx.designPath,
105
- migrated: ctx.migrated,
106
105
  }, null, 2));
107
106
  }
108
107