@bastani/atomic 0.9.11-alpha.6 → 0.9.11-alpha.7

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 (140) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/package.json +1 -1
  4. package/dist/builtin/subagents/package.json +1 -1
  5. package/dist/builtin/web-access/package.json +1 -1
  6. package/dist/builtin/workflows/package.json +1 -1
  7. package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
  8. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
  9. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
  10. package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
  11. package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
  12. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
  13. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
  14. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
  15. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
  16. package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
  17. package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
  18. package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
  19. package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
  20. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
  21. package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
  22. package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
  23. package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
  24. package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  25. package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
  26. package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
  27. package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
  28. package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
  29. package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
  30. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
  31. package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
  32. package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
  33. package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
  34. package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
  35. package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
  36. package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
  37. package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
  38. package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
  39. package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
  40. package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
  41. package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
  42. package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
  43. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
  44. package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
  45. package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
  46. package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
  47. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
  48. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
  49. package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
  50. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
  51. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
  52. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
  53. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
  54. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
  55. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
  56. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
  57. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
  58. package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
  59. package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
  60. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
  61. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
  62. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
  63. package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
  64. package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
  65. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
  66. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
  67. package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
  68. package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  69. package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
  70. package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
  71. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
  72. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
  73. package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
  74. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
  75. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  76. package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
  77. package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
  79. package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
  80. package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
  81. package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
  82. package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
  83. package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
  84. package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
  85. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
  86. package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
  87. package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
  88. package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  89. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
  90. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
  91. package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
  92. package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
  93. package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
  94. package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
  95. package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
  96. package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
  97. package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
  98. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
  99. package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
  100. package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
  101. package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
  102. package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
  103. package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
  104. package/dist/builtin/workflows/src/extension/index.bundle.mjs +201 -22
  105. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
  106. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  107. package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
  108. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
  109. package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
  110. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
  111. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +19 -13
  112. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
  113. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
  114. package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
  115. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
  116. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
  117. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
  118. package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
  120. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  121. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  122. package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
  123. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  124. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
  125. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  126. package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
  127. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  128. package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
  129. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/chat-session-host.js +22 -1
  131. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  132. package/dist/modes/interactive/interactive-agent-events.js +9 -3
  133. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  134. package/docs/tui.md +7 -2
  135. package/docs/workflows.md +1 -1
  136. package/npm-shrinkwrap.json +23 -23
  137. package/package.json +2 -2
  138. package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
  139. package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
  140. package/dist/builtin/workflows/skills/impeccable/reference/interaction-design.md +0 -189
@@ -0,0 +1,151 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { slugFromTarget } from './target-slug.mjs';
4
+
5
+ export const SURFACE_BRIEF_VERSION = 1;
6
+
7
+ export function getSurfaceBriefDir(projectRoot) {
8
+ return path.join(projectRoot, '.impeccable', 'surfaces');
9
+ }
10
+
11
+ export function normalizeSurfaceTarget(target, { projectRoot = process.cwd() } = {}) {
12
+ if (!target || typeof target !== 'string' || !target.trim()) return null;
13
+ const trimmed = target.trim();
14
+ if (/^https?:\/\//i.test(trimmed)) {
15
+ try {
16
+ const url = new URL(trimmed);
17
+ url.hash = '';
18
+ url.search = '';
19
+ return url.toString().replace(/\/$/, '') || url.origin;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ if (/^route:/i.test(trimmed)) {
25
+ const route = trimmed.slice(trimmed.indexOf(':') + 1).trim();
26
+ if (!route.startsWith('/') || route.includes('..')) return null;
27
+ const normalizedRoute = route.split(/[?#]/, 1)[0].replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
28
+ return `route:${normalizedRoute}`;
29
+ }
30
+ if (trimmed === '/') return 'route:/';
31
+ if (trimmed.startsWith('/')) {
32
+ const absolute = path.resolve(trimmed);
33
+ const relativeToProject = path.relative(projectRoot, absolute);
34
+ const isProjectFile = relativeToProject && !relativeToProject.startsWith('..') && !path.isAbsolute(relativeToProject);
35
+ if (!isProjectFile && !fs.existsSync(absolute) && !trimmed.includes('..')) {
36
+ const normalizedRoute = trimmed.split(/[?#]/, 1)[0].replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
37
+ return `route:${normalizedRoute}`;
38
+ }
39
+ }
40
+ const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(projectRoot, trimmed);
41
+ const rel = path.relative(projectRoot, abs);
42
+ if (!rel || rel === '.' || rel.startsWith('..') || path.isAbsolute(rel)) return null;
43
+ return rel.split(path.sep).join('/');
44
+ }
45
+
46
+ export function surfaceBriefPathForTarget(target, { projectRoot = process.cwd() } = {}) {
47
+ const normalized = normalizeSurfaceTarget(target, { projectRoot });
48
+ if (!normalized) return null;
49
+ const slugInput = normalized.startsWith('route:') ? `route${normalized.slice('route:'.length)}` : normalized;
50
+ const slug = slugFromTarget(slugInput, { cwd: projectRoot });
51
+ return slug ? path.join(getSurfaceBriefDir(projectRoot), `${slug}.md`) : null;
52
+ }
53
+
54
+ export function parseSurfaceBrief(text, filePath = null) {
55
+ const match = String(text || '').match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
56
+ const meta = {};
57
+ if (match) {
58
+ for (const line of match[1].split(/\r?\n/)) {
59
+ const colon = line.indexOf(':');
60
+ if (colon < 0) continue;
61
+ const key = line.slice(0, colon).trim();
62
+ const raw = line.slice(colon + 1).trim();
63
+ if (!key) continue;
64
+ if (/^(?:\[|\{|\")/.test(raw) || /^(?:true|false|null|-?\d+(?:\.\d+)?)$/.test(raw)) {
65
+ try { meta[key] = JSON.parse(raw); continue; } catch { /* keep string */ }
66
+ }
67
+ meta[key] = raw.replace(/^['"]|['"]$/g, '');
68
+ }
69
+ }
70
+ const primaryTarget = typeof meta.primary_target === 'string' ? meta.primary_target : null;
71
+ const relatedTargets = Array.isArray(meta.related_targets)
72
+ ? meta.related_targets.filter((value) => typeof value === 'string')
73
+ : [];
74
+ return {
75
+ path: filePath,
76
+ text: String(text || ''),
77
+ body: match ? String(text || '').slice(match[0].length).trim() : String(text || '').trim(),
78
+ meta,
79
+ slug: typeof meta.slug === 'string' ? meta.slug : filePath ? path.basename(filePath, '.md') : null,
80
+ primaryTarget,
81
+ relatedTargets,
82
+ targets: [primaryTarget, ...relatedTargets].filter(Boolean),
83
+ };
84
+ }
85
+
86
+ export function listSurfaceBriefs(projectRoot = process.cwd()) {
87
+ const dir = getSurfaceBriefDir(projectRoot);
88
+ let names;
89
+ try {
90
+ names = fs.readdirSync(dir).filter((name) => name.endsWith('.md')).sort();
91
+ } catch {
92
+ return [];
93
+ }
94
+ return names.flatMap((name) => {
95
+ const filePath = path.join(dir, name);
96
+ try {
97
+ return [parseSurfaceBrief(fs.readFileSync(filePath, 'utf-8'), filePath)];
98
+ } catch {
99
+ return [];
100
+ }
101
+ });
102
+ }
103
+
104
+ export function resolveSurfaceBrief(projectRoot = process.cwd(), target = null) {
105
+ const briefs = listSurfaceBriefs(projectRoot);
106
+ if (!target) {
107
+ return {
108
+ brief: briefs.length === 1 ? briefs[0] : null,
109
+ candidates: briefs,
110
+ reason: briefs.length === 1 ? 'only-brief' : briefs.length > 1 ? 'ambiguous' : 'none',
111
+ };
112
+ }
113
+
114
+ const normalized = normalizeSurfaceTarget(target, { projectRoot });
115
+ if (!normalized) return { brief: null, candidates: briefs, reason: 'invalid-target' };
116
+ const exactPath = surfaceBriefPathForTarget(normalized, { projectRoot });
117
+ const exact = briefs.find((brief) => brief.path === exactPath && (!brief.targets.length || brief.targets.includes(normalized)));
118
+ if (exact) return { brief: exact, candidates: briefs, reason: 'slug' };
119
+ const mapped = briefs.filter((brief) => brief.targets.includes(normalized));
120
+ return {
121
+ brief: mapped.length === 1 ? mapped[0] : null,
122
+ candidates: mapped.length > 1 ? mapped : briefs,
123
+ reason: mapped.length === 1 ? 'mapping' : mapped.length > 1 ? 'ambiguous-target' : 'not-found',
124
+ };
125
+ }
126
+
127
+ export function writeSurfaceBrief({
128
+ projectRoot = process.cwd(),
129
+ primaryTarget,
130
+ relatedTargets = [],
131
+ body,
132
+ }) {
133
+ const normalizedPrimary = normalizeSurfaceTarget(primaryTarget, { projectRoot });
134
+ if (!normalizedPrimary) throw new Error('surface brief requires a concrete project-relative primary target or URL');
135
+ const normalizedRelated = [...new Set(relatedTargets
136
+ .map((target) => normalizeSurfaceTarget(target, { projectRoot }))
137
+ .filter((target) => target && target !== normalizedPrimary))];
138
+ const slug = slugFromTarget(normalizedPrimary, { cwd: projectRoot });
139
+ const filePath = surfaceBriefPathForTarget(normalizedPrimary, { projectRoot });
140
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
141
+ const frontmatter = [
142
+ '---',
143
+ `version: ${SURFACE_BRIEF_VERSION}`,
144
+ `slug: ${JSON.stringify(slug)}`,
145
+ `primary_target: ${JSON.stringify(normalizedPrimary)}`,
146
+ `related_targets: ${JSON.stringify(normalizedRelated)}`,
147
+ '---',
148
+ ].join('\n');
149
+ fs.writeFileSync(filePath, `${frontmatter}\n\n${String(body || '').trim()}\n`, 'utf-8');
150
+ return filePath;
151
+ }
@@ -0,0 +1,33 @@
1
+ import path from 'node:path';
2
+
3
+ const SLUG_MAX = 50;
4
+
5
+ /** Derive one clone-stable slug from a concrete file path or URL. */
6
+ export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
7
+ if (!resolved || typeof resolved !== 'string') return null;
8
+ const trimmed = resolved.trim();
9
+ if (!trimmed) return null;
10
+
11
+ if (/^https?:\/\//i.test(trimmed)) {
12
+ let url;
13
+ try { url = new URL(trimmed); } catch { return null; }
14
+ return kebab(`${url.hostname}${url.pathname}`);
15
+ }
16
+
17
+ const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
18
+ let rel = path.relative(cwd, abs);
19
+ if (rel.startsWith('..') || path.isAbsolute(rel)) rel = path.basename(abs);
20
+ if (!rel || rel === '.') return null;
21
+ return kebab(rel);
22
+ }
23
+
24
+ export function kebab(value) {
25
+ const slug = String(value || '')
26
+ .toLowerCase()
27
+ .replace(/[/\\.]+/g, '-')
28
+ .replace(/[^a-z0-9-]+/g, '-')
29
+ .replace(/-+/g, '-')
30
+ .replace(/^-|-$/g, '');
31
+ if (!slug) return null;
32
+ return slug.length <= SLUG_MAX ? slug : slug.slice(slug.length - SLUG_MAX).replace(/^-/, '');
33
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * One owner for "which file extensions hold UI markup".
3
+ *
4
+ * Before this module the answer was spelled out separately in hook-lib.mjs
5
+ * (`detector.extensions` config, issue #316) and in live-wrap.mjs /
6
+ * live-accept.mjs (a hardcoded `EXTENSIONS` array, duplicated verbatim in both).
7
+ * The lists drifted: the hook learned configurable server-template extensions
8
+ * while Live kept its six frontend defaults, so a Phoenix project got design
9
+ * findings on `.heex` files but `Session markers not found` on Accept (#374).
10
+ *
11
+ * Extensions are matched against the END OF THE FILENAME, not `path.extname`,
12
+ * so double extensions like `.blade.php`, `.html.erb`, and `.html.heex` work.
13
+ */
14
+
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+
18
+ /**
19
+ * Built-in markup extensions for Live's wrap/accept source search.
20
+ *
21
+ * Elixir's `.ex` is here because Phoenix function components put `~H"""`
22
+ * templates directly in `lib/**\/*.ex`; `.heex` and `.eex` cover standalone
23
+ * templates. `.exs` is deliberately absent: those are Elixir *scripts*
24
+ * (`mix.exs`, `config/*.exs`, tests) and never hold markup, so including them
25
+ * only gives the wrap query a chance to match build config by accident.
26
+ */
27
+ export const LIVE_TEMPLATE_EXTENSIONS = Object.freeze([
28
+ '.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro',
29
+ '.ex', '.heex', '.eex',
30
+ ]);
31
+
32
+ /**
33
+ * Normalize `detector.extensions` entries to `{ ext, engine }`.
34
+ *
35
+ * Accepts `{ ext, engine }` objects (engine 'html' | 'text', default 'html' —
36
+ * the common case for server-side templates) or bare strings as shorthand.
37
+ */
38
+ export function normalizeExtensionEntries(entries) {
39
+ if (!Array.isArray(entries)) return [];
40
+ const out = [];
41
+ for (const entry of entries) {
42
+ const raw = typeof entry === 'string' ? entry : entry?.ext;
43
+ if (typeof raw !== 'string') continue;
44
+ let ext = raw.trim().toLowerCase();
45
+ if (!ext) continue;
46
+ if (!ext.startsWith('.')) ext = `.${ext}`;
47
+ const engine = (!(typeof entry === 'string') && entry?.engine === 'text') ? 'text' : 'html';
48
+ out.push({ ext, engine });
49
+ }
50
+ return out;
51
+ }
52
+
53
+ export function mergeExtensions(existing, incoming) {
54
+ const map = new Map();
55
+ for (const entry of normalizeExtensionEntries(existing)) map.set(entry.ext, entry);
56
+ for (const entry of normalizeExtensionEntries(incoming)) map.set(entry.ext, entry);
57
+ return Array.from(map.values());
58
+ }
59
+
60
+ export function matchConfiguredExtension(filePath, extensions) {
61
+ if (!Array.isArray(extensions) || extensions.length === 0) return null;
62
+ const name = path.basename(String(filePath || '')).toLowerCase();
63
+ if (!name) return null;
64
+ // The longest matching suffix wins, so `.blade.php` beats a broader `.php`
65
+ // entry regardless of config order.
66
+ let best = null;
67
+ for (const entry of normalizeExtensionEntries(extensions)) {
68
+ if (name.length > entry.ext.length && name.endsWith(entry.ext)
69
+ && (!best || entry.ext.length > best.ext.length)) {
70
+ best = entry;
71
+ }
72
+ }
73
+ return best;
74
+ }
75
+
76
+ /**
77
+ * Does this filename end in one of `extensions`?
78
+ *
79
+ * Suffix matching rather than `path.extname` equality, so a configured
80
+ * `.html.erb` matches `show.html.erb` (whose extname is only `.erb`). The
81
+ * `name.length > ext.length` guard keeps a file literally named `.heex` from
82
+ * counting as a template.
83
+ */
84
+ export function matchesTemplateExtension(filePath, extensions) {
85
+ const name = path.basename(String(filePath || '')).toLowerCase();
86
+ if (!name) return false;
87
+ for (const ext of extensions) {
88
+ if (name.length > ext.length && name.endsWith(ext)) return true;
89
+ }
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * Built-in Live extensions plus any the project configured for the detector.
95
+ *
96
+ * Reading `detector.extensions` here is the point: a user who taught the design
97
+ * hook about `.blade.php` should not have to teach Live separately. Config
98
+ * parsing is intentionally minimal (own the shape, not the whole hook config)
99
+ * so this module stays importable from the Live CLI without pulling in
100
+ * hook-lib.mjs.
101
+ */
102
+ export function resolveLiveTemplateExtensions(cwd = process.cwd()) {
103
+ const cached = extensionCache.get(cwd);
104
+ if (cached) return cached;
105
+ const resolved = readLiveTemplateExtensions(cwd);
106
+ extensionCache.set(cwd, resolved);
107
+ return resolved;
108
+ }
109
+
110
+ // live-wrap calls the resolver once per candidate query per pass (up to eight
111
+ // times in one CLI run), and every call would otherwise re-read and re-parse
112
+ // both config files. Keyed by cwd; a single CLI process never rewrites its own
113
+ // config mid-run.
114
+ const extensionCache = new Map();
115
+
116
+ /** Test seam: drop the memoized config so a fixture can rewrite config.json. */
117
+ export function clearTemplateExtensionCache() {
118
+ extensionCache.clear();
119
+ }
120
+
121
+ function readLiveTemplateExtensions(cwd) {
122
+ const configured = [];
123
+ for (const name of ['config.json', 'config.local.json']) {
124
+ const raw = safeReadJson(path.join(cwd, '.impeccable', name));
125
+ const detector = raw?.detector;
126
+ if (detector && typeof detector === 'object' && !Array.isArray(detector)) {
127
+ configured.push(...normalizeExtensionEntries(detector.extensions));
128
+ }
129
+ }
130
+ const seen = new Set(LIVE_TEMPLATE_EXTENSIONS);
131
+ const out = [...LIVE_TEMPLATE_EXTENSIONS];
132
+ for (const { ext } of configured) {
133
+ if (seen.has(ext)) continue;
134
+ seen.add(ext);
135
+ out.push(ext);
136
+ }
137
+ return out;
138
+ }
139
+
140
+ function safeReadJson(filePath) {
141
+ try {
142
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
@@ -1,9 +1,18 @@
1
+ // A preview whose variants live in component modules rather than in the user's
2
+ // source. These leave no markers in the real file, so a failed accept gives the
3
+ // agent nothing to hand-edit and must be reported as a failure rather than
4
+ // reference/live.md's manual-cleanup handoff. Kept as a set: any future
5
+ // component-module preview mode belongs here the day it lands.
6
+ const PREVIEW_MODES_WITHOUT_SOURCE_MARKERS = new Set([
7
+ 'svelte-component',
8
+ ]);
9
+
1
10
  export function completionTypeForAcceptResult(eventType, acceptResult) {
2
11
  if (eventType === 'discard') return acceptResult?.handled === true ? 'discarded' : 'error';
3
12
  if (acceptResult?.handled === true && acceptResult?.carbonize === true) return 'agent_done';
4
13
  if (acceptResult?.handled === true) return 'complete';
5
14
  if (acceptResult?.mode === 'error') return 'error';
6
- if (eventType === 'accept' && acceptResult?.previewMode === 'svelte-component') return 'error';
15
+ if (eventType === 'accept' && PREVIEW_MODES_WITHOUT_SOURCE_MARKERS.has(acceptResult?.previewMode)) return 'error';
7
16
  return 'agent_done';
8
17
  }
9
18
 
@@ -118,6 +118,15 @@ export function validateEvent(msg) {
118
118
  return 'checkpoint: paramValues must be an object';
119
119
  }
120
120
  return null;
121
+ case 'agent_phase':
122
+ if (!isValidId(msg.id)) return 'agent_phase: missing or malformed id';
123
+ if (typeof msg.phase !== 'string' || !/^[a-z][a-z0-9_]{1,63}$/.test(msg.phase)) {
124
+ return 'agent_phase: missing or malformed phase';
125
+ }
126
+ if (msg.durationMs !== undefined && (!Number.isFinite(msg.durationMs) || msg.durationMs < 0)) {
127
+ return 'agent_phase: durationMs must be a non-negative number';
128
+ }
129
+ return null;
121
130
  case 'exit':
122
131
  return null;
123
132
  case 'prefetch':
@@ -131,6 +140,12 @@ export function validateEvent(msg) {
131
140
  if (msg.message.length > 4000) return 'steer: message too long';
132
141
  if (msg.pageUrl !== undefined && typeof msg.pageUrl !== 'string') return 'steer: pageUrl must be string';
133
142
  return null;
143
+ case 'carbonize_cleanup':
144
+ if (!isValidId(msg.id)) return 'carbonize_cleanup: missing or malformed id';
145
+ if (!isValidId(msg.sessionId)) return 'carbonize_cleanup: missing or malformed sessionId';
146
+ if (!msg.file || typeof msg.file !== 'string') return 'carbonize_cleanup: missing file';
147
+ if (!isValidVariantId(String(msg.variantId))) return 'carbonize_cleanup: missing or malformed variantId';
148
+ return null;
134
149
  default:
135
150
  return 'Unknown event type: ' + msg.type;
136
151
  }
@@ -0,0 +1,149 @@
1
+ import { execFile } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { promisify } from 'node:util';
4
+
5
+ const execFileAsync = promisify(execFile);
6
+ const PREFLIGHT_TIMEOUT_MS = 15_000;
7
+
8
+ // Per-target cache of the resolved source file. The wrap search walks the whole
9
+ // project tree and was measured at ~7.6s on a large repo; it re-ran on every
10
+ // generate for the same picked element (re-rolls, param passes). Keyed by the
11
+ // target signature (locator + route), so it invalidates automatically when the
12
+ // element or route changes; a failed resolution evicts its entry (see below).
13
+ const sourceResolutionCache = new Map();
14
+
15
+ /** Test/lifecycle hook: drop all cached source resolutions. */
16
+ export function clearSourceResolutionCache() {
17
+ sourceResolutionCache.clear();
18
+ }
19
+
20
+ function targetSignature(event) {
21
+ const isInsert = event.mode === 'insert';
22
+ const target = isInsert ? insertTarget(event) : replaceTarget(event);
23
+ return JSON.stringify({
24
+ mode: isInsert ? 'insert' : 'replace',
25
+ position: isInsert ? target.position : null,
26
+ elementId: target.elementId || null,
27
+ classes: target.classes || null,
28
+ tag: target.tag || null,
29
+ pageUrl: event.pageUrl || null,
30
+ });
31
+ }
32
+
33
+ export function buildGenerationPreflight(event, scriptsDir, { cache = null } = {}) {
34
+ if (!event || event.type !== 'generate' || !event.id) return null;
35
+
36
+ const isInsert = event.mode === 'insert';
37
+ const target = isInsert ? insertTarget(event) : replaceTarget(event);
38
+ if (!target.elementId && !target.classes) return null;
39
+
40
+ const script = path.join(scriptsDir, isInsert ? 'live-insert.mjs' : 'live-wrap.mjs');
41
+ const args = [script, '--id', event.id, '--count', String(event.count || 3)];
42
+ // Compute the scaffold but do not write it into source for source-preview
43
+ // targets. The agent writes wrapper + variants atomically; a premature
44
+ // server-side write reloads the framework and strands the browser at 0/N.
45
+ // No-op on the svelte-component path, which never writes the route source.
46
+ args.push('--defer-source-write');
47
+ if (isInsert) args.push('--position', target.position);
48
+ if (target.elementId) args.push('--element-id', target.elementId);
49
+ if (target.classes) args.push('--classes', target.classes);
50
+ if (target.tag) args.push('--tag', target.tag);
51
+ if (target.text) args.push('--text', target.text);
52
+ if (!isInsert && event.pageUrl) args.push('--page-url', event.pageUrl);
53
+ const signature = targetSignature(event);
54
+ // A cached resolution points the helper straight at the file, skipping the
55
+ // tree search. The helper still reads current content, so line ranges stay
56
+ // fresh; only discovery is cached.
57
+ const cachedFile = cache ? cache.get(signature) : null;
58
+ if (cachedFile) args.push('--file', cachedFile);
59
+ return { script, args, mode: isInsert ? 'insert' : 'replace', signature };
60
+ }
61
+
62
+ /**
63
+ * Scaffold the source for a generate event before handing it to an agent.
64
+ *
65
+ * Async on purpose. This spawns `live-wrap.mjs`, which walks the project's
66
+ * source tree and can take seconds (measured at ~7.6s on a large repo when the
67
+ * element is not found, with a 15s ceiling). The live server is single-threaded
68
+ * and calls this while leasing a poll, so a synchronous spawn froze the whole
69
+ * server for that entire window: Accept and Discard POSTs, SSE progress
70
+ * broadcasts, and every other poll stalled behind it.
71
+ */
72
+ export async function runGenerationPreflight(event, {
73
+ cwd = process.cwd(),
74
+ scriptsDir,
75
+ execFileImpl = execFileAsync,
76
+ timeoutMs = PREFLIGHT_TIMEOUT_MS,
77
+ cache = sourceResolutionCache,
78
+ } = {}) {
79
+ const command = buildGenerationPreflight(event, scriptsDir, { cache });
80
+ if (!command) {
81
+ return { ok: false, skipped: true, reason: 'insufficient_locator' };
82
+ }
83
+
84
+ const startedAt = performance.now();
85
+ try {
86
+ const { stdout } = await execFileImpl(process.execPath, command.args, {
87
+ cwd,
88
+ encoding: 'utf-8',
89
+ timeout: timeoutMs,
90
+ });
91
+ const line = String(stdout).trim().split('\n').filter(Boolean).pop();
92
+ if (!line) throw new Error('preflight returned no scaffold metadata');
93
+ const scaffold = JSON.parse(line);
94
+ // Cache the resolved SOURCE file (route source, not the svelte manifest) so
95
+ // the next generate on this target skips the tree search.
96
+ const resolvedSource = scaffold.sourceFile || scaffold.file;
97
+ if (cache && command.signature && typeof resolvedSource === 'string') {
98
+ cache.set(command.signature, resolvedSource);
99
+ }
100
+ return {
101
+ ok: true,
102
+ mode: command.mode,
103
+ durationMs: performance.now() - startedAt,
104
+ scaffold,
105
+ };
106
+ } catch (error) {
107
+ // Evict a stale/failed resolution so the next attempt does a full search
108
+ // (the element may have moved out of the previously cached file).
109
+ if (cache && command.signature) cache.delete(command.signature);
110
+ return {
111
+ ok: false,
112
+ mode: command.mode,
113
+ durationMs: performance.now() - startedAt,
114
+ error: compactError(error),
115
+ };
116
+ }
117
+ }
118
+
119
+ function replaceTarget(event) {
120
+ return normalizeTarget(event.element || {});
121
+ }
122
+
123
+ function insertTarget(event) {
124
+ return {
125
+ ...normalizeTarget(event.insert?.anchor || {}),
126
+ position: event.insert?.position === 'before' ? 'before' : 'after',
127
+ };
128
+ }
129
+
130
+ function normalizeTarget(target) {
131
+ const classes = Array.isArray(target.classes)
132
+ ? target.classes.join(' ')
133
+ : String(target.classes || '').trim();
134
+ const text = typeof target.textContent === 'string'
135
+ ? target.textContent.trim().slice(0, 80)
136
+ : '';
137
+ return {
138
+ elementId: target.id || target.elementId || undefined,
139
+ classes: classes || undefined,
140
+ tag: target.tagName || target.tag || undefined,
141
+ text: text || undefined,
142
+ };
143
+ }
144
+
145
+ function compactError(error) {
146
+ const stderr = error?.stderr ? String(error.stderr).trim() : '';
147
+ const message = stderr.split('\n').filter(Boolean).pop() || error?.message || 'preflight failed';
148
+ return String(message).slice(0, 500);
149
+ }
@@ -0,0 +1,14 @@
1
+ export function eventPriority(event = {}) {
2
+ if (event.type === 'accept' || event.type === 'discard' || event.type === 'exit') return 0;
3
+ if (event.type === 'manual_edit_apply' || event.type === 'steer' || event.type === 'carbonize_cleanup') return 1;
4
+ if (event.type === 'generate') return 2;
5
+ return 3;
6
+ }
7
+
8
+ export function selectAvailablePendingEvent(entries, { now = Date.now(), types = null } = {}) {
9
+ const allowed = types instanceof Set ? types : (Array.isArray(types) ? new Set(types) : null);
10
+ return entries
11
+ .filter((entry) => !(entry.leaseUntil && entry.leaseUntil > now))
12
+ .filter((entry) => !allowed || allowed.has(entry.event?.type))
13
+ .sort((a, b) => eventPriority(a.event) - eventPriority(b.event) || a.seq - b.seq)[0] || null;
14
+ }