@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
@@ -29,6 +29,9 @@ const ROLLBACK_EXTENSIONS = new Set([
29
29
  '.astro',
30
30
  '.cjs',
31
31
  '.css',
32
+ '.eex',
33
+ '.ex',
34
+ '.heex',
32
35
  '.htm',
33
36
  '.html',
34
37
  '.js',
@@ -8,9 +8,14 @@
8
8
  * with zero LLM involvement.
9
9
  *
10
10
  * Usage:
11
- * node live-inject.mjs --port PORT # Insert the live script tag
12
- * node live-inject.mjs --remove # Remove the live script tag
13
- * node live-inject.mjs --check # Check whether live config exists
11
+ * node live-inject.mjs --port PORT [--token TOKEN] # Insert the live script tag
12
+ * node live-inject.mjs --remove # Remove the live script tag
13
+ * node live-inject.mjs --check # Check whether live config exists
14
+ *
15
+ * When --token is supplied, it is appended to the /live.js src as `?token=...`
16
+ * so the server's token-gated /live.js handler will serve the bundle. Omitting
17
+ * the token yields a bare `/live.js` src (legacy behavior; the server returns
18
+ * 401 for it under the current gate).
14
19
  */
15
20
 
16
21
  import fs from 'node:fs';
@@ -22,11 +27,18 @@ import {
22
27
  detectSvelteKitProject,
23
28
  removeSvelteKitLiveAdapter,
24
29
  } from './live/sveltekit-adapter.mjs';
30
+ import {
31
+ applyTanStackLiveAdapter,
32
+ detectTanStackStartProject,
33
+ removeTanStackLiveAdapter,
34
+ } from './live/tanstack-adapter.mjs';
25
35
 
26
36
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
37
  const CONFIG_PATH = resolveLiveConfigPath({ cwd: process.cwd(), scriptsDir: __dirname });
28
38
  const MARKER_OPEN_TEXT = 'impeccable-live-start';
29
39
  const MARKER_CLOSE_TEXT = 'impeccable-live-end';
40
+ const NUXT_PLUGIN_MARKER = 'impeccable-live-nuxt-plugin';
41
+ const NUXT_PLUGIN_NAME = 'impeccable-live.client.ts';
30
42
  const IGNORE_MARKER_OPEN = '# impeccable-live-ignore-start';
31
43
  const IGNORE_MARKER_CLOSE = '# impeccable-live-ignore-end';
32
44
 
@@ -37,6 +49,9 @@ export const LIVE_IGNORE_PATTERNS = Object.freeze([
37
49
  '.impeccable/live/sessions/',
38
50
  '.impeccable/live/previews/',
39
51
  '.impeccable/live/annotations/',
52
+ '.impeccable/live/artifacts/',
53
+ '.impeccable/live/accept-receipts/',
54
+ '.impeccable/live/locks/',
40
55
  '.impeccable/live/cache/',
41
56
  '.impeccable/live/manual-edit-apply-transaction.json',
42
57
  '.impeccable/live/manual-edit-events.jsonl',
@@ -45,10 +60,15 @@ export const LIVE_IGNORE_PATTERNS = Object.freeze([
45
60
  '.impeccable/live/deferred-svelte-component-accepts.json',
46
61
  '.impeccable-live.json',
47
62
  '.impeccable-live/',
63
+ 'app/.impeccable-live/',
64
+ 'src/.impeccable-live/',
48
65
  'node_modules/.impeccable-live/',
49
66
  'src/lib/impeccable/ImpeccableLiveRoot.svelte',
50
67
  'src/lib/impeccable/__runtime.js',
51
68
  'src/lib/impeccable/[0-9a-f]*/',
69
+ 'plugins/impeccable-live.client.ts',
70
+ 'app/plugins/impeccable-live.client.ts',
71
+ 'src/plugins/impeccable-live.client.ts',
52
72
  ]);
53
73
 
54
74
  /**
@@ -112,6 +132,8 @@ Output (JSON):
112
132
 
113
133
  const resolvedFiles = resolveFiles(process.cwd(), config);
114
134
  const svelteKit = detectSvelteKitProject(process.cwd(), config);
135
+ const nuxt = detectNuxtProject(process.cwd());
136
+ const tanstack = svelteKit || nuxt ? null : detectTanStackStartProject(process.cwd());
115
137
 
116
138
  if (args.includes('--remove')) {
117
139
  if (svelteKit) {
@@ -119,6 +141,18 @@ Output (JSON):
119
141
  console.log(JSON.stringify({ ok: true, adapter: 'sveltekit', results: [adapterResult] }));
120
142
  return;
121
143
  }
144
+ if (tanstack) {
145
+ const adapterResult = removeTanStackLiveAdapter({ cwd: process.cwd(), project: tanstack });
146
+ console.log(JSON.stringify({ ok: !adapterResult.error, adapter: 'tanstack-start', results: [adapterResult] }));
147
+ if (adapterResult.error) process.exitCode = 1;
148
+ return;
149
+ }
150
+ if (nuxt) {
151
+ const adapterResult = removeNuxtLiveAdapter({ cwd: process.cwd(), project: nuxt });
152
+ console.log(JSON.stringify({ ok: !adapterResult.error, adapter: 'nuxt', results: [adapterResult] }));
153
+ if (adapterResult.error) process.exitCode = 1;
154
+ return;
155
+ }
122
156
  const results = resolvedFiles.map((relFile) => {
123
157
  const absFile = path.resolve(process.cwd(), relFile);
124
158
  if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
@@ -144,20 +178,51 @@ Output (JSON):
144
178
  console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
145
179
  process.exit(1);
146
180
  }
147
- const gitIgnore = ensureLiveGitIgnores(process.cwd());
181
+ // Optional server token: appended to the /live.js src so the token-gated
182
+ // /live.js handler authorizes the browser fetch. `live.mjs` always passes it.
183
+ const tokenIdx = args.indexOf('--token');
184
+ const token = tokenIdx !== -1 ? args[tokenIdx + 1] : undefined;
185
+ const gitIgnore = ensureLiveGitIgnores(
186
+ process.cwd(),
187
+ nuxt ? [nuxt.pluginFile] : tanstack ? [tanstack.componentFile] : [],
188
+ );
148
189
 
149
190
  if (svelteKit) {
150
- const adapterResult = applySvelteKitLiveAdapter({ cwd: process.cwd(), port, config });
191
+ const adapterResult = applySvelteKitLiveAdapter({ cwd: process.cwd(), port, token, config });
151
192
  console.log(JSON.stringify({ ok: true, port, adapter: 'sveltekit', gitIgnore, results: [adapterResult] }));
152
193
  return;
153
194
  }
195
+ if (tanstack) {
196
+ const adapterResult = applyTanStackLiveAdapter({ cwd: process.cwd(), port, token, project: tanstack });
197
+ console.log(JSON.stringify({
198
+ ok: !adapterResult.error,
199
+ port,
200
+ adapter: 'tanstack-start',
201
+ gitIgnore,
202
+ results: [adapterResult],
203
+ }));
204
+ if (adapterResult.error) process.exitCode = 1;
205
+ return;
206
+ }
207
+ if (nuxt) {
208
+ const adapterResult = applyNuxtLiveAdapter({ cwd: process.cwd(), port, token, project: nuxt });
209
+ console.log(JSON.stringify({
210
+ ok: !adapterResult.error,
211
+ port,
212
+ adapter: 'nuxt',
213
+ gitIgnore,
214
+ results: [adapterResult],
215
+ }));
216
+ if (adapterResult.error) process.exitCode = 1;
217
+ return;
218
+ }
154
219
 
155
220
  const results = resolvedFiles.map((relFile) => {
156
221
  const absFile = path.resolve(process.cwd(), relFile);
157
222
  if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
158
223
  const content = fs.readFileSync(absFile, 'utf-8');
159
224
  const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax));
160
- const withTag = insertTag(withoutOld, config, port, relFile);
225
+ const withTag = insertTag(withoutOld, config, port, relFile, token);
161
226
  if (withTag === withoutOld) {
162
227
  return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter };
163
228
  }
@@ -174,12 +239,12 @@ Output (JSON):
174
239
  if (!anyInserted) process.exit(1);
175
240
  }
176
241
 
177
- export function ensureLiveGitIgnores(cwd = process.cwd()) {
242
+ export function ensureLiveGitIgnores(cwd = process.cwd(), extraPatterns = []) {
178
243
  const target = resolveIgnoreTarget(cwd);
179
244
  const existing = fs.existsSync(target.path) ? fs.readFileSync(target.path, 'utf-8') : '';
180
245
  const block = [
181
246
  IGNORE_MARKER_OPEN,
182
- ...LIVE_IGNORE_PATTERNS,
247
+ ...new Set([...LIVE_IGNORE_PATTERNS, ...extraPatterns]),
183
248
  IGNORE_MARKER_CLOSE,
184
249
  ].join('\n');
185
250
  const markerRe = new RegExp(`${escapeRegExp(IGNORE_MARKER_OPEN)}[\\s\\S]*?${escapeRegExp(IGNORE_MARKER_CLOSE)}`);
@@ -201,10 +266,119 @@ export function ensureLiveGitIgnores(cwd = process.cwd()) {
201
266
  file: path.relative(cwd, target.path).split(path.sep).join('/'),
202
267
  mode: target.mode,
203
268
  changed: updated !== existing,
204
- patterns: [...LIVE_IGNORE_PATTERNS],
269
+ patterns: [...new Set([...LIVE_IGNORE_PATTERNS, ...extraPatterns])],
205
270
  };
206
271
  }
207
272
 
273
+ // ---------------------------------------------------------------------------
274
+ // Nuxt adapter
275
+ //
276
+ // A script element placed in app.vue is compiled as Vue-rendered DOM and is
277
+ // not executed. Nuxt instead auto-discovers client plugins. Keep the adapter
278
+ // generated, dev-only, and outside user-authored source: Live creates one
279
+ // marked .client.ts plugin on start and removes it on stop.
280
+ // ---------------------------------------------------------------------------
281
+
282
+ export function detectNuxtProject(cwd = process.cwd()) {
283
+ const configFile = fs.readdirSync(cwd, { withFileTypes: true })
284
+ .find((entry) => entry.isFile() && /^nuxt\.config\.(?:js|mjs|cjs|ts|mts|cts)$/.test(entry.name))
285
+ ?.name;
286
+ if (!configFile) return null;
287
+
288
+ const config = fs.readFileSync(path.join(cwd, configFile), 'utf-8');
289
+ const literalSrcDir = config.match(/\bsrcDir\s*:\s*(['"])([^'"]+)\1/);
290
+ let appDir = '';
291
+ if (literalSrcDir) {
292
+ const candidate = literalSrcDir[2]
293
+ .replace(/\\/g, '/')
294
+ .replace(/^\.\//, '')
295
+ .replace(/\/+$/, '');
296
+ const normalized = path.posix.normalize(candidate);
297
+ if (normalized !== '..' && !normalized.startsWith('../') && !path.isAbsolute(normalized)) {
298
+ appDir = normalized === '.' ? '' : normalized;
299
+ }
300
+ } else if (
301
+ fs.existsSync(path.join(cwd, 'app', 'app.vue'))
302
+ || fs.existsSync(path.join(cwd, 'app', 'pages'))
303
+ ) {
304
+ appDir = 'app';
305
+ }
306
+
307
+ const pluginFile = [appDir, 'plugins', NUXT_PLUGIN_NAME].filter(Boolean).join('/');
308
+ return { configFile, appDir, pluginFile };
309
+ }
310
+
311
+ export function buildNuxtPlugin(port, token) {
312
+ return `/* ${NUXT_PLUGIN_MARKER} */
313
+ const liveSrc = '${buildLiveScriptSrc(port, token)}';
314
+ const liveSelector = 'script[data-impeccable-live-nuxt]';
315
+
316
+ export default defineNuxtPlugin(() => {
317
+ if (!import.meta.dev || typeof document === 'undefined') return;
318
+
319
+ const expectedSrc = new URL(liveSrc, window.location.href).href;
320
+ let script = document.querySelector(liveSelector);
321
+ if (script?.src === expectedSrc) return;
322
+ script?.remove();
323
+
324
+ script = document.createElement('script');
325
+ script.src = liveSrc;
326
+ script.async = true;
327
+ script.dataset.impeccableLiveNuxt = '';
328
+ document.head.appendChild(script);
329
+
330
+ import.meta.hot?.dispose(() => {
331
+ if (script?.isConnected) script.remove();
332
+ });
333
+ });
334
+ /* /${NUXT_PLUGIN_MARKER} */
335
+ `;
336
+ }
337
+
338
+ export function applyNuxtLiveAdapter({ cwd = process.cwd(), port, token, project = detectNuxtProject(cwd) }) {
339
+ if (!project) return { error: 'nuxt_not_detected' };
340
+ const absFile = path.join(cwd, project.pluginFile);
341
+ const existing = fs.existsSync(absFile) ? fs.readFileSync(absFile, 'utf-8') : null;
342
+ if (existing !== null && !existing.includes(NUXT_PLUGIN_MARKER)) {
343
+ return {
344
+ file: project.pluginFile,
345
+ error: 'nuxt_plugin_conflict',
346
+ hint: `${project.pluginFile} already exists and is not managed by Impeccable Live`,
347
+ };
348
+ }
349
+
350
+ const content = buildNuxtPlugin(port, token);
351
+ fs.mkdirSync(path.dirname(absFile), { recursive: true });
352
+ if (content !== existing) fs.writeFileSync(absFile, content, 'utf-8');
353
+ return {
354
+ file: project.pluginFile,
355
+ inserted: true,
356
+ changed: content !== existing,
357
+ devOnly: true,
358
+ };
359
+ }
360
+
361
+ export function removeNuxtLiveAdapter({ cwd = process.cwd(), project = detectNuxtProject(cwd) }) {
362
+ if (!project) return { error: 'nuxt_not_detected' };
363
+ const absFile = path.join(cwd, project.pluginFile);
364
+ if (!fs.existsSync(absFile)) {
365
+ return { file: project.pluginFile, removed: false, note: 'no adapter present' };
366
+ }
367
+ const content = fs.readFileSync(absFile, 'utf-8');
368
+ if (!content.includes(NUXT_PLUGIN_MARKER)) {
369
+ return {
370
+ file: project.pluginFile,
371
+ removed: false,
372
+ error: 'nuxt_plugin_conflict',
373
+ hint: `${project.pluginFile} is not managed by Impeccable Live`,
374
+ };
375
+ }
376
+ fs.unlinkSync(absFile);
377
+ const pluginDir = path.dirname(absFile);
378
+ if (fs.readdirSync(pluginDir).length === 0) fs.rmdirSync(pluginDir);
379
+ return { file: project.pluginFile, removed: true };
380
+ }
381
+
208
382
  function resolveIgnoreTarget(cwd) {
209
383
  const gitExcludePath = resolveGitInfoExcludePath(cwd);
210
384
  if (gitExcludePath) {
@@ -355,7 +529,18 @@ function validateConfig(cfg) {
355
529
  function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : '<!--'; }
356
530
  function commentClose(syntax) { return syntax === 'jsx' ? '*/}' : '-->'; }
357
531
 
358
- function buildTagBlock(syntax, port, filePath) {
532
+ /**
533
+ * Build the /live.js src the browser loads. When a token is supplied it rides
534
+ * as a `?token=...` query param so the server's token-gated /live.js handler
535
+ * authorizes the fetch. Shared by every injection path (HTML/JSX script tag,
536
+ * the Nuxt plugin, the SvelteKit root component) so they stay in sync.
537
+ */
538
+ export function buildLiveScriptSrc(port, token) {
539
+ const base = 'http://localhost:' + port + '/live.js';
540
+ return token ? base + '?token=' + encodeURIComponent(token) : base;
541
+ }
542
+
543
+ function buildTagBlock(syntax, port, filePath, token) {
359
544
  const open = commentOpen(syntax);
360
545
  const close = commentClose(syntax);
361
546
  // Astro processes <script> tags by default and rewrites src to its own
@@ -364,7 +549,7 @@ function buildTagBlock(syntax, port, filePath) {
364
549
  const scriptAttrs = isAstro ? 'is:inline ' : '';
365
550
  return (
366
551
  open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
367
- '<script ' + scriptAttrs + 'src="http://localhost:' + port + '/live.js"></script>\n' +
552
+ '<script ' + scriptAttrs + 'src="' + buildLiveScriptSrc(port, token) + '"></script>\n' +
368
553
  open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n'
369
554
  );
370
555
  }
@@ -386,9 +571,9 @@ function readLineEndingAt(content, index) {
386
571
  return '';
387
572
  }
388
573
 
389
- function insertTag(content, config, port, filePath) {
574
+ function insertTag(content, config, port, filePath, token) {
390
575
  const lineEnding = detectLineEnding(content);
391
- const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding);
576
+ const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding);
392
577
  // insertBefore: match the LAST occurrence. Anchors like `</body>` naturally
393
578
  // belong at the end, and the same literal can appear earlier in code blocks
394
579
  // within rendered documentation pages.
@@ -131,6 +131,9 @@ Output (JSON):
131
131
  const query = argVal(args, '--query');
132
132
  const filePath = argVal(args, '--file');
133
133
  const text = argVal(args, '--text');
134
+ // See live-wrap.mjs: preflight computes the scaffold but leaves source
135
+ // untouched so the agent's single edit is the only framework reload.
136
+ const deferSourceWrite = args.includes('--defer-source-write');
134
137
 
135
138
  if (!id) { console.error('Missing --id'); process.exit(1); }
136
139
  if (!position) { console.error('Missing --position (before | after)'); process.exit(1); }
@@ -244,12 +247,23 @@ Output (JSON):
244
247
  isJsx,
245
248
  });
246
249
 
247
- const newLines = [
248
- ...lines.slice(0, spliceIndex),
249
- ...wrapperLines,
250
- ...lines.slice(spliceIndex),
251
- ];
252
- fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
250
+ let deferredWrapper = null;
251
+ if (deferSourceWrite) {
252
+ // Insert-as-empty-range: the agent inserts `wrapperBlock` (variants spliced
253
+ // at the marker) at spliceIndex without removing any source line.
254
+ deferredWrapper = {
255
+ block: wrapperLines.join('\n'),
256
+ replaceStartLine: spliceIndex + 1,
257
+ replaceEndLine: spliceIndex, // empty range (endLine < startLine) => insertion
258
+ };
259
+ } else {
260
+ const newLines = [
261
+ ...lines.slice(0, spliceIndex),
262
+ ...wrapperLines,
263
+ ...lines.slice(spliceIndex),
264
+ ];
265
+ fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8');
266
+ }
253
267
 
254
268
  const insertLine = spliceIndex + 3;
255
269
 
@@ -257,6 +271,10 @@ Output (JSON):
257
271
  mode: 'insert',
258
272
  position,
259
273
  file: relTargetFile,
274
+ sourceWritten: deferredWrapper ? false : undefined,
275
+ wrapperBlock: deferredWrapper ? deferredWrapper.block : undefined,
276
+ replaceStartLine: deferredWrapper ? deferredWrapper.replaceStartLine : undefined,
277
+ replaceEndLine: deferredWrapper ? deferredWrapper.replaceEndLine : undefined,
260
278
  insertLine: insertLine + 1,
261
279
  commentSyntax,
262
280
  styleMode: styleMode.mode,
@@ -14,7 +14,12 @@ import { isGeneratedFile } from './lib/is-generated.mjs';
14
14
  import { readBuffer, getBufferPath } from './live/manual-edits-buffer.mjs';
15
15
 
16
16
  const EVIDENCE_VERSION = 1;
17
- const TEXT_EXTENSIONS = new Set(['.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro', '.js', '.mjs', '.ts']);
17
+ const TEXT_EXTENSIONS = new Set([
18
+ '.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro', '.js', '.mjs', '.ts',
19
+ // Phoenix keeps `~H"""` markup in .ex alongside standalone .heex/.eex
20
+ // templates, so copy edits land in all three.
21
+ '.ex', '.heex', '.eex',
22
+ ]);
18
23
  const SEARCH_DIRS = ['src', 'app', 'pages', 'components', 'public', 'views', 'templates', 'site', 'lib', 'data'];
19
24
  const STRONG_LITERAL_MATCH_LIMIT = 8;
20
25
  const WEAK_LITERAL_MATCH_LIMIT = 4;
@@ -27,7 +27,7 @@ const scriptCmd = (name) => `node "${path.join(SELF_DIR, name)}"`;
27
27
  export const PER_REQUEST_TIMEOUT_MS = 270_000;
28
28
  export const DEFAULT_EVENT_LEASE_MS = 600_000;
29
29
 
30
- const EVENT_TYPES_NEEDING_AGENT_REPLY = new Set(['generate', 'steer', 'manual_edit_apply']);
30
+ const EVENT_TYPES_NEEDING_AGENT_REPLY = new Set(['generate', 'steer', 'manual_edit_apply', 'carbonize_cleanup']);
31
31
 
32
32
  function readServerInfo() {
33
33
  const record = readLiveServerInfo(process.cwd());
@@ -38,8 +38,8 @@ function readServerInfo() {
38
38
  return record.info;
39
39
  }
40
40
 
41
- export function buildPollReplyPayload(token, { id, type, message, file, data }) {
42
- return { token, id, type, message, file, data };
41
+ export function buildPollReplyPayload(token, { id, type, message, file, data, sourceEventType }) {
42
+ return { token, id, type, message, file, data, sourceEventType };
43
43
  }
44
44
 
45
45
  export function manualApplyPollBanner(event = {}) {
@@ -152,7 +152,14 @@ export async function waitForEventAck(base, token, eventId, {
152
152
  return false;
153
153
  }
154
154
 
155
- export async function fetchNextEvent(base, token, { totalDeadline } = {}) {
155
+ export async function fetchNextEvent(base, token, {
156
+ totalDeadline,
157
+ types,
158
+ resolveTypes,
159
+ perRequestTimeoutMs = PER_REQUEST_TIMEOUT_MS,
160
+ leaseMs = DEFAULT_EVENT_LEASE_MS,
161
+ signal,
162
+ } = {}) {
156
163
  while (true) {
157
164
  if (totalDeadline && Date.now() >= totalDeadline) {
158
165
  return { type: 'timeout' };
@@ -161,8 +168,15 @@ export async function fetchNextEvent(base, token, { totalDeadline } = {}) {
161
168
  const remaining = totalDeadline
162
169
  ? totalDeadline - Date.now()
163
170
  : PER_REQUEST_TIMEOUT_MS;
164
- const slice = Math.min(Math.max(remaining, 1000), PER_REQUEST_TIMEOUT_MS);
165
- const res = await fetch(`${base}/poll?token=${token}&timeout=${slice}&leaseMs=${DEFAULT_EVENT_LEASE_MS}`);
171
+ const slice = Math.min(Math.max(remaining, 1000), perRequestTimeoutMs);
172
+ const query = new URLSearchParams({
173
+ token,
174
+ timeout: String(slice),
175
+ leaseMs: String(leaseMs),
176
+ });
177
+ const normalizedTypes = normalizePollTypes(resolveTypes ? await resolveTypes() : types);
178
+ if (normalizedTypes.length > 0) query.set('types', normalizedTypes.join(','));
179
+ const res = await fetch(`${base}/poll?${query}`, { signal });
166
180
 
167
181
  if (res.status === 401) {
168
182
  const err = new Error('Authentication failed. The server token may have changed.');
@@ -202,11 +216,17 @@ export async function augmentEventWithAcceptHandling(event, base, token) {
202
216
  event._acceptResult = { handled: false, mode: 'error', error: err.message };
203
217
  }
204
218
 
219
+ await completeAcceptHandling(event, base, token);
220
+ return event;
221
+ }
222
+
223
+ export async function completeAcceptHandling(event, base, token) {
205
224
  const completionType = completionTypeForAcceptResult(event.type, event._acceptResult);
206
225
  try {
207
226
  await postReply(base, token, {
208
227
  id: event.id,
209
228
  type: completionType,
229
+ sourceEventType: event.type,
210
230
  message: event._acceptResult?.error,
211
231
  file: event._acceptResult?.file,
212
232
  data: event._acceptResult?.carbonize === true ? { carbonize: true } : undefined,
@@ -217,7 +237,6 @@ export async function augmentEventWithAcceptHandling(event, base, token) {
217
237
  if (!event._completionAck) {
218
238
  event._completionAck = completionAckForAcceptResult(event.id, completionType, event._acceptResult);
219
239
  }
220
-
221
240
  return event;
222
241
  }
223
242
 
@@ -245,9 +264,9 @@ export function printPollEvent(event) {
245
264
  console.log(JSON.stringify(event));
246
265
  }
247
266
 
248
- export async function runPollOnce(base, token, { totalTimeout = 600_000 } = {}) {
267
+ export async function runPollOnce(base, token, { totalTimeout = 600_000, types, resolveTypes, perRequestTimeoutMs } = {}) {
249
268
  const deadline = Date.now() + totalTimeout;
250
- const event = await fetchNextEvent(base, token, { totalDeadline: deadline });
269
+ const event = await fetchNextEvent(base, token, { totalDeadline: deadline, types, resolveTypes, perRequestTimeoutMs });
251
270
  await augmentEventWithAcceptHandling(event, base, token);
252
271
  writeCarbonizeBanner(event);
253
272
  printPollEvent(event);
@@ -258,11 +277,14 @@ export async function runPollStream(base, token, {
258
277
  ackTimeoutMs = 600_000,
259
278
  ackPollIntervalMs = 400,
260
279
  shouldContinue = () => true,
280
+ types,
281
+ resolveTypes,
282
+ perRequestTimeoutMs,
261
283
  } = {}) {
262
284
  process.stderr.write('[impeccable-poll] stream mode: one JSON object per line on stdout; use --reply while this process stays running\n');
263
285
 
264
286
  while (shouldContinue()) {
265
- const event = await fetchNextEvent(base, token);
287
+ const event = await fetchNextEvent(base, token, { types, resolveTypes, perRequestTimeoutMs });
266
288
  await augmentEventWithAcceptHandling(event, base, token);
267
289
  writeCarbonizeBanner(event);
268
290
  printPollEvent(event);
@@ -322,14 +344,15 @@ Modes:
322
344
 
323
345
  Options:
324
346
  --timeout=MS One-shot poll timeout in ms (default: 600000). Ignored in --stream mode
347
+ --types=A,B Lease only these event types
325
348
  --ack-timeout=MS Stream mode: max wait for --reply after generate/steer (default: 600000)
326
349
  --file PATH Attach a source file path to the reply (generate/steer flow)
327
350
  --data JSON Attach a JSON result object to the reply (manual_edit_apply flow). Must be valid JSON
328
351
  --help Show this help message
329
352
 
330
353
  Harness note:
331
- Default one-shot mode is the portable contract.
332
- --stream is experimental for harnesses with fast incremental stdout.`);
354
+ Default one-shot mode is the primary contract, including Codex foreground polling.
355
+ --stream is retained for harnesses with measured, reliable incremental stdout.`);
333
356
  process.exit(0);
334
357
  }
335
358
 
@@ -360,23 +383,30 @@ Harness note:
360
383
  }
361
384
 
362
385
  const streamMode = args.includes('--stream');
386
+ const typesArg = args.find((a) => a.startsWith('--types='));
387
+ const types = normalizePollTypes(typesArg ? typesArg.slice('--types='.length) : null);
363
388
  const ackTimeoutArg = args.find((a) => a.startsWith('--ack-timeout='));
364
389
  const ackTimeoutMs = ackTimeoutArg ? parseInt(ackTimeoutArg.split('=')[1], 10) : 600_000;
365
390
 
366
391
  try {
367
392
  if (streamMode) {
368
- await runPollStream(base, info.token, { ackTimeoutMs });
393
+ await runPollStream(base, info.token, { ackTimeoutMs, types });
369
394
  return;
370
395
  }
371
396
 
372
397
  const timeoutArg = args.find((a) => a.startsWith('--timeout='));
373
398
  const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600_000;
374
- await runPollOnce(base, info.token, { totalTimeout });
399
+ await runPollOnce(base, info.token, { totalTimeout, types });
375
400
  } catch (err) {
376
401
  handlePollError(err);
377
402
  }
378
403
  }
379
404
 
405
+ export function normalizePollTypes(value) {
406
+ const values = Array.isArray(value) ? value : String(value || '').split(',');
407
+ return [...new Set(values.map((type) => String(type).trim()).filter(Boolean))];
408
+ }
409
+
380
410
  // Auto-execute when run directly
381
411
  const _running = process.argv[1];
382
412
  if (_running?.endsWith('live-poll.mjs') || _running?.endsWith('live-poll.mjs/')) {