@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
@@ -1,24 +1,26 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { getLegacyLiveSessionsDir, getLiveSessionsDir } from '../lib/impeccable-paths.mjs';
3
+ import { getLegacyLiveSessionsDir, getLiveSessionsDir, safeSessionId } from '../lib/impeccable-paths.mjs';
4
4
 
5
5
  const COMPLETED_PHASES = new Set(['completed', 'discarded']);
6
+ export const GENERATION_FENCED_PHASES = new Set([
7
+ 'accept_requested',
8
+ 'discard_requested',
9
+ 'carbonize_required',
10
+ 'completed',
11
+ 'discarded',
12
+ ]);
6
13
 
7
14
  export function createLiveSessionStore({ cwd = process.cwd(), sessionId } = {}) {
8
15
  const rootDir = getLiveSessionsDir(cwd);
9
16
  const legacyRootDir = getLegacyLiveSessionsDir(cwd);
10
17
  fs.mkdirSync(rootDir, { recursive: true });
11
- const snapshotCache = new Map();
12
-
13
- function loadCachedOrRebuild(id) {
14
- const cached = snapshotCache.get(id);
15
- if (cached) return cached;
16
- const journalPath = getReadableJournalPath(id);
17
- const rebuilt = rebuildSnapshotFromJournal(journalPath, id);
18
- snapshotCache.set(id, rebuilt);
19
- return rebuilt;
20
- }
21
18
 
19
+ // No snapshot cache on purpose: appendEvent and getSnapshot both rebuild from
20
+ // the journal so sequence numbers and phase fences never come from a stale
21
+ // in-memory copy when the publisher/complete helpers append from another
22
+ // process. A cache written but never read would grow per session for the
23
+ // lifetime of the server without ever saving a rebuild.
22
24
  function getReadableJournalPath(id) {
23
25
  const primary = getJournalPath(rootDir, id);
24
26
  if (fs.existsSync(primary)) return primary;
@@ -38,7 +40,10 @@ export function createLiveSessionStore({ cwd = process.cwd(), sessionId } = {})
38
40
  if (!fs.existsSync(journalPath) && fs.existsSync(legacyJournalPath)) {
39
41
  fs.copyFileSync(legacyJournalPath, journalPath);
40
42
  }
41
- const prior = loadCachedOrRebuild(normalized.id);
43
+ // Publisher/complete helpers can append from a separate process while
44
+ // the server is alive. Rebuild here so sequence numbers and phase
45
+ // fences never come from a stale in-memory cache.
46
+ const prior = rebuildSnapshotFromJournal(getReadableJournalPath(normalized.id), normalized.id);
42
47
  const seq = prior.nextSeq;
43
48
  const entry = {
44
49
  seq,
@@ -49,7 +54,6 @@ export function createLiveSessionStore({ cwd = process.cwd(), sessionId } = {})
49
54
  };
50
55
  fs.appendFileSync(journalPath, JSON.stringify(entry) + '\n');
51
56
  const next = applyEvent(prior.snapshot, entry, prior.diagnostics);
52
- snapshotCache.set(normalized.id, { snapshot: next, diagnostics: next.diagnostics || [], nextSeq: seq + 1 });
53
57
  writeSnapshot(snapshotPath, next);
54
58
  return next;
55
59
  },
@@ -58,7 +62,6 @@ export function createLiveSessionStore({ cwd = process.cwd(), sessionId } = {})
58
62
  const journalPath = getReadableJournalPath(id);
59
63
  const snapshotPath = getSnapshotPath(rootDir, id);
60
64
  const rebuilt = rebuildSnapshotFromJournal(journalPath, id);
61
- snapshotCache.set(id, rebuilt);
62
65
  writeSnapshot(snapshotPath, rebuilt.snapshot);
63
66
  if (!opts.includeCompleted && COMPLETED_PHASES.has(rebuilt.snapshot.phase)) return null;
64
67
  return rebuilt.snapshot;
@@ -95,11 +98,6 @@ function getSnapshotPath(rootDir, id) {
95
98
  return path.join(rootDir, safeSessionId(id) + '.snapshot.json');
96
99
  }
97
100
 
98
- function safeSessionId(id) {
99
- if (!/^[A-Za-z0-9_-]{1,128}$/.test(id)) throw new Error('invalid session id: ' + id);
100
- return id;
101
- }
102
-
103
101
  function baseSnapshot(id) {
104
102
  return {
105
103
  id,
@@ -116,9 +114,18 @@ function baseSnapshot(id) {
116
114
  pendingEvent: null,
117
115
  deliveryLease: null,
118
116
  checkpointRevision: 0,
117
+ browserCheckpointRevision: 0,
118
+ publicationCheckpointRevision: 0,
119
119
  activeOwner: null,
120
120
  sourceMarkers: {},
121
121
  fallbackMode: null,
122
+ generationPhase: null,
123
+ generationCompletedAt: null,
124
+ generationTimings: {},
125
+ variantPlan: null,
126
+ generationCanceled: false,
127
+ generationCanceledAt: null,
128
+ cancelReason: null,
122
129
  annotationArtifacts: [],
123
130
  diagnostics: [],
124
131
  updatedAt: null,
@@ -158,6 +165,8 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
158
165
  ...snapshot,
159
166
  paramValues: { ...(snapshot.paramValues || {}) },
160
167
  sourceMarkers: { ...(snapshot.sourceMarkers || {}) },
168
+ generationTimings: { ...(snapshot.generationTimings || {}) },
169
+ variantPlan: snapshot.variantPlan || null,
161
170
  annotationArtifacts: [...(snapshot.annotationArtifacts || [])],
162
171
  diagnostics: [...(snapshot.diagnostics || [])],
163
172
  updatedAt: entry.ts || new Date().toISOString(),
@@ -174,11 +183,48 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
174
183
  next.expectedVariants = event.count ?? next.expectedVariants;
175
184
  next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
176
185
  next.pendingEvent = toPendingEvent(event);
186
+ next.variantPlan = null;
177
187
  if (event.screenshotPath) upsertArtifact(next.annotationArtifacts, { type: 'screenshot', path: event.screenshotPath });
178
188
  break;
189
+ case 'variant_plan':
190
+ if (!next.generationCanceled && !GENERATION_FENCED_PHASES.has(next.phase)) {
191
+ next.variantPlan = event.plan ?? next.variantPlan;
192
+ }
193
+ break;
194
+ case 'detector_waivers':
195
+ if (!next.generationCanceled && !GENERATION_FENCED_PHASES.has(next.phase)) {
196
+ next.detectorWaivers = [
197
+ ...(next.detectorWaivers || []),
198
+ ...(Array.isArray(event.waivers) ? event.waivers : []),
199
+ ];
200
+ }
201
+ break;
202
+ case 'agent_phase':
203
+ next.generationPhase = event.phase ?? next.generationPhase;
204
+ if (event.phase) {
205
+ next.generationTimings[event.phase] = {
206
+ at: event.at ?? (Date.parse(entry.ts || '') || null),
207
+ durationMs: event.durationMs ?? null,
208
+ };
209
+ }
210
+ break;
179
211
  case 'variants_ready':
180
212
  case 'agent_done':
213
+ if ((next.generationCanceled || GENERATION_FENCED_PHASES.has(next.phase))
214
+ && !(event.type === 'agent_done' && event.carbonize === true && next.phase === 'accept_requested')) {
215
+ next.diagnostics.push({
216
+ error: 'late_generation_event_ignored',
217
+ type: event.type,
218
+ phase: next.phase,
219
+ });
220
+ break;
221
+ }
181
222
  next.phase = event.carbonize === true ? 'carbonize_required' : 'variants_ready';
223
+ // Durable completion marker: later browser checkpoints (a resumed page
224
+ // reporting phase "generating") regress `phase`, but generation staying
225
+ // finished is monotone — the live server keys missed-`done` redelivery
226
+ // on this field.
227
+ next.generationCompletedAt = event.at ?? (Date.parse(entry.ts || '') || Date.now());
182
228
  next.sourceFile = event.sourceFile ?? event.file ?? next.sourceFile;
183
229
  next.previewFile = event.previewFile ?? next.previewFile;
184
230
  next.previewMode = event.previewMode ?? next.previewMode;
@@ -194,27 +240,45 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
194
240
  }
195
241
  break;
196
242
  case 'checkpoint':
197
- if (COMPLETED_PHASES.has(next.phase)) {
243
+ if (next.generationCanceled || GENERATION_FENCED_PHASES.has(next.phase)) {
198
244
  next.diagnostics.push({ error: 'checkpoint_after_terminal_ignored', phase: event.phase ?? null, revision: event.revision ?? null });
199
245
  break;
200
246
  }
201
- if ((event.revision ?? 0) >= (next.checkpointRevision ?? 0)) {
202
- next.phase = event.phase ?? next.phase;
203
- next.checkpointRevision = event.revision ?? next.checkpointRevision;
204
- next.activeOwner = event.owner ?? next.activeOwner;
205
- next.arrivedVariants = event.arrivedVariants ?? next.arrivedVariants;
206
- next.visibleVariant = event.visibleVariant ?? next.visibleVariant;
207
- next.sourceFile = event.sourceFile ?? next.sourceFile;
208
- next.previewFile = event.previewFile ?? next.previewFile;
209
- next.previewMode = event.previewMode ?? next.previewMode;
210
- if (event.paramValues) next.paramValues = { ...event.paramValues };
211
- } else {
212
- next.diagnostics.push({ error: 'stale_checkpoint_ignored', revision: event.revision });
247
+ {
248
+ const revisionDomain = event.revisionDomain === 'publication'
249
+ || (event.reason === 'variants_progress' && !event.owner)
250
+ ? 'publication'
251
+ : 'browser';
252
+ const revisionField = revisionDomain === 'publication'
253
+ ? 'publicationCheckpointRevision'
254
+ : 'browserCheckpointRevision';
255
+ const currentRevision = next[revisionField]
256
+ ?? (revisionDomain === 'browser' ? next.checkpointRevision : 0)
257
+ ?? 0;
258
+ if ((event.revision ?? 0) >= currentRevision) {
259
+ next.phase = event.phase ?? next.phase;
260
+ next[revisionField] = event.revision ?? currentRevision;
261
+ if (revisionDomain === 'browser') {
262
+ next.checkpointRevision = event.revision ?? next.checkpointRevision;
263
+ next.activeOwner = event.owner ?? next.activeOwner;
264
+ }
265
+ next.arrivedVariants = event.arrivedVariants ?? next.arrivedVariants;
266
+ if (revisionDomain === 'browser') next.visibleVariant = event.visibleVariant ?? next.visibleVariant;
267
+ next.sourceFile = event.sourceFile ?? next.sourceFile;
268
+ next.previewFile = event.previewFile ?? next.previewFile;
269
+ next.previewMode = event.previewMode ?? next.previewMode;
270
+ if (revisionDomain === 'browser' && event.paramValues) next.paramValues = { ...event.paramValues };
271
+ } else {
272
+ next.diagnostics.push({ error: 'stale_checkpoint_ignored', revision: event.revision, revisionDomain });
273
+ }
213
274
  }
214
275
  break;
215
276
  case 'accept':
216
277
  case 'accept_intent':
217
278
  next.phase = 'accept_requested';
279
+ next.generationCanceled = true;
280
+ next.generationCanceledAt = event.at ?? (Date.parse(entry.ts || '') || Date.now());
281
+ next.cancelReason = 'accept';
218
282
  next.visibleVariant = Number(event.variantId ?? next.visibleVariant);
219
283
  if (event.paramValues) next.paramValues = { ...event.paramValues };
220
284
  next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
@@ -232,6 +296,12 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
232
296
  next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
233
297
  next.pendingEvent = toPendingEvent(event);
234
298
  break;
299
+ case 'carbonize_cleanup':
300
+ next.phase = 'carbonize_cleanup_requested';
301
+ next.sourceFile = event.file ?? next.sourceFile;
302
+ next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
303
+ next.pendingEvent = toPendingEvent(event);
304
+ break;
235
305
  case 'steer_done':
236
306
  next.phase = 'steer_done';
237
307
  next.sourceFile = event.sourceFile ?? event.file ?? next.sourceFile;
@@ -243,6 +313,9 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
243
313
  break;
244
314
  case 'discard':
245
315
  next.phase = 'discard_requested';
316
+ next.generationCanceled = true;
317
+ next.generationCanceledAt = event.at ?? (Date.parse(entry.ts || '') || Date.now());
318
+ next.cancelReason = 'discard';
246
319
  next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
247
320
  next.pendingEvent = toPendingEvent(event);
248
321
  break;
@@ -260,6 +333,10 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
260
333
  next.pendingEvent = null;
261
334
  break;
262
335
  case 'agent_error':
336
+ if (next.generationCanceled && event.sourceEventType === 'generate') {
337
+ next.diagnostics.push({ error: 'late_generation_event_ignored', type: event.type, phase: next.phase });
338
+ break;
339
+ }
263
340
  next.phase = 'agent_error';
264
341
  next.pendingEventSeq = null;
265
342
  next.pendingEvent = null;
@@ -0,0 +1,105 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { getLiveDir, isLiveServerPidReachable } from '../lib/impeccable-paths.mjs';
5
+
6
+ // Only used to retire a lock whose contents we cannot read (empty or truncated
7
+ // by a crash mid-write). A readable lock's fate is decided by its owner's
8
+ // liveness instead, so a slow critical section is never swept.
9
+ const UNREADABLE_LOCK_STALE_MS = 60_000;
10
+
11
+ export function sourceLockPath(file, cwd = process.cwd()) {
12
+ const digest = createHash('sha256').update(path.resolve(cwd, file)).digest('hex').slice(0, 24);
13
+ return path.join(getLiveDir(cwd), 'locks', digest + '.lock');
14
+ }
15
+
16
+ export function withSourceLockSync(file, owner, fn, {
17
+ cwd = process.cwd(),
18
+ waitMs = 0,
19
+ retryMs = 5,
20
+ } = {}) {
21
+ const lockPath = sourceLockPath(file, cwd);
22
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
23
+ const deadline = Date.now() + Math.max(0, Number(waitMs) || 0);
24
+ // Identifies this acquisition specifically, so release can tell our own lock
25
+ // from a replacement that some other writer created.
26
+ const token = randomUUID();
27
+ let acquired = false;
28
+
29
+ while (!acquired) {
30
+ clearStaleLock(lockPath);
31
+ let fd;
32
+ try {
33
+ fd = fs.openSync(lockPath, 'wx');
34
+ fs.writeFileSync(fd, JSON.stringify({
35
+ owner,
36
+ token,
37
+ pid: process.pid,
38
+ at: Date.now(),
39
+ file: path.resolve(cwd, file),
40
+ }) + '\n');
41
+ acquired = true;
42
+ } catch (error) {
43
+ if (error?.code !== 'EEXIST') throw error;
44
+ if (Date.now() >= deadline) {
45
+ const locked = new Error('source_locked');
46
+ locked.code = 'SOURCE_LOCKED';
47
+ locked.lockPath = lockPath;
48
+ throw locked;
49
+ }
50
+ sleepSync(Math.max(1, Math.min(Number(retryMs) || 5, deadline - Date.now())));
51
+ } finally {
52
+ try { if (fd !== undefined) fs.closeSync(fd); } catch {}
53
+ }
54
+ }
55
+
56
+ try {
57
+ return fn();
58
+ } finally {
59
+ releaseOwnLock(lockPath, token);
60
+ }
61
+ }
62
+
63
+ function sleepSync(ms) {
64
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
65
+ }
66
+
67
+ function readLock(lockPath) {
68
+ try { return JSON.parse(fs.readFileSync(lockPath, 'utf-8')); } catch { return null; }
69
+ }
70
+
71
+ /**
72
+ * Remove the lock only if it is still the one this call created. If a sweeper
73
+ * judged our lock stale and another writer replaced it, unlinking here would
74
+ * end *their* critical section and admit a third writer to the same file.
75
+ */
76
+ function releaseOwnLock(lockPath, token) {
77
+ const held = readLock(lockPath);
78
+ if (held && held.token !== token) return;
79
+ try { fs.unlinkSync(lockPath); } catch {}
80
+ }
81
+
82
+ /**
83
+ * A lock is stale when its owner is gone, not when it is old.
84
+ *
85
+ * Age alone cuts both ways: it sweeps a live holder whose critical section
86
+ * outran the timeout (a suspended laptop, a stopped process), letting two
87
+ * writers into the same source file, while still making every accept on a
88
+ * crashed holder's file wait out the full timeout. Asking the OS whether the
89
+ * recorded pid is alive answers both correctly: a dead owner releases at once,
90
+ * and a live owner keeps its lock however long it needs.
91
+ */
92
+ function clearStaleLock(lockPath) {
93
+ const held = readLock(lockPath);
94
+ if (!held) {
95
+ // Unreadable: either a crash truncated it, or we caught the brief window
96
+ // between create and write in a live acquisition. mtime distinguishes them.
97
+ try {
98
+ const stat = fs.statSync(lockPath);
99
+ if (Date.now() - stat.mtimeMs > UNREADABLE_LOCK_STALE_MS) fs.unlinkSync(lockPath);
100
+ } catch { /* gone already */ }
101
+ return;
102
+ }
103
+ if (typeof held.pid === 'number' && isLiveServerPidReachable(held.pid)) return;
104
+ try { fs.unlinkSync(lockPath); } catch {}
105
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The project-source walk shared by live-wrap.mjs and live-accept.mjs.
3
+ *
4
+ * Both scripts need the same thing: find the one project file containing a
5
+ * string (wrap looks for the element's class/id/text, accept looks for the
6
+ * session's `impeccable-variants-start` marker). They had two near-identical
7
+ * copies of the walk, and the copies drifted — same `EXTENSIONS` array declared
8
+ * twice, same `searchDirs` array declared twice, one `realpathSync` guarded by
9
+ * try/catch and the other not. That drift is what #374 had to patch in two
10
+ * places at once.
11
+ *
12
+ * Callers differ only in how they reject a candidate, so that is the one thing
13
+ * this module takes as options (`skipDirs`, `fileFilter`).
14
+ */
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { IMPECCABLE_DIR } from '../lib/impeccable-paths.mjs';
19
+ import { matchesTemplateExtension } from '../lib/template-extensions.mjs';
20
+
21
+ /**
22
+ * Privileged roots, searched in order, before the catch-all `.` walk.
23
+ *
24
+ * `lib` is here for Phoenix, whose templates live in `lib/my_app_web/`. It is
25
+ * an ordering preference rather than a reachability fix: `.` already recurses
26
+ * into `lib`, so the real #374 bug was the extension list, not this array.
27
+ */
28
+ export const SOURCE_SEARCH_DIRS = Object.freeze([
29
+ 'src', 'app', 'pages', 'components', 'public', 'views', 'templates', 'lib', '.',
30
+ ]);
31
+
32
+ /**
33
+ * Directories that are never project source.
34
+ *
35
+ * `.impeccable` is the critical entry, and it is not cosmetic. Progressive
36
+ * publication stages each revision as `.impeccable/live/artifacts/
37
+ * <id>-r<n>.<source-ext>`, and those artifacts carry the very marker accept
38
+ * searches for. The walk reaches `.` for any project whose source is not under
39
+ * one of the privileged roots above (this repo's own site lives in
40
+ * `site/pages/`), and dot-directories sort before letters, so the artifact was
41
+ * found *before* the real file. isGeneratedFile then declined the accept, and
42
+ * the agent fell back to carbonizing several hundred lines of stylesheet by
43
+ * hand.
44
+ */
45
+ export const NEVER_SOURCE_DIRS = Object.freeze(['node_modules', '.git', IMPECCABLE_DIR]);
46
+
47
+ const MAX_DEPTH = 5;
48
+
49
+ /**
50
+ * Walk the project for the first template file whose contents include `query`.
51
+ *
52
+ * @param {object} opts
53
+ * @param {string} opts.query substring to find in file contents
54
+ * @param {string} opts.cwd project root
55
+ * @param {string[]} opts.extensions filename suffixes that count as templates
56
+ * @param {Iterable<string>} [opts.skipDirs] directory names never to descend into
57
+ * @param {(filePath: string) => boolean} [opts.fileFilter] return false to reject a candidate
58
+ * @returns {string|null} absolute path of the first match
59
+ */
60
+ export function findSourceFile({ query, cwd, extensions, skipDirs = NEVER_SOURCE_DIRS, fileFilter }) {
61
+ const skip = new Set(skipDirs);
62
+ const seen = new Set();
63
+ for (const dir of SOURCE_SEARCH_DIRS) {
64
+ const absDir = path.join(cwd, dir);
65
+ if (!fs.existsSync(absDir)) continue;
66
+ const result = walk(absDir, query, extensions, skip, fileFilter, seen, 0);
67
+ if (result) return result;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function walk(dir, query, extensions, skip, fileFilter, seen, depth) {
73
+ if (depth > MAX_DEPTH) return null;
74
+ // A broken symlink anywhere in the tree used to throw straight out of
75
+ // live-wrap's copy of this walk, killing the whole wrap.
76
+ let realDir;
77
+ try { realDir = fs.realpathSync(dir); } catch { return null; }
78
+ if (seen.has(realDir)) return null;
79
+ seen.add(realDir);
80
+
81
+ let entries;
82
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
83
+ catch { return null; }
84
+
85
+ // Files before directories: a match in the current directory beats one
86
+ // nested deeper.
87
+ for (const entry of entries) {
88
+ if (!entry.isFile()) continue;
89
+ if (!matchesTemplateExtension(entry.name, extensions)) continue;
90
+ const filePath = path.join(dir, entry.name);
91
+ if (fileFilter && !fileFilter(filePath)) continue;
92
+ try {
93
+ if (fs.readFileSync(filePath, 'utf-8').includes(query)) return filePath;
94
+ } catch { /* unreadable, skip */ }
95
+ }
96
+
97
+ for (const entry of entries) {
98
+ if (!entry.isDirectory()) continue;
99
+ if (skip.has(entry.name)) continue;
100
+ const result = walk(path.join(dir, entry.name), query, extensions, skip, fileFilter, seen, depth + 1);
101
+ if (result) return result;
102
+ }
103
+
104
+ return null;
105
+ }
@@ -36,14 +36,14 @@ export function detectSvelteKitProject(cwd = process.cwd(), config = null) {
36
36
  };
37
37
  }
38
38
 
39
- export function applySvelteKitLiveAdapter({ cwd = process.cwd(), port, config = null } = {}) {
39
+ export function applySvelteKitLiveAdapter({ cwd = process.cwd(), port, token, config = null } = {}) {
40
40
  if (!Number.isFinite(Number(port))) {
41
41
  throw new Error('SvelteKit live adapter requires a numeric port');
42
42
  }
43
43
  const detected = detectSvelteKitProject(cwd, config);
44
44
  if (!detected) return null;
45
45
 
46
- ensureSvelteLiveRootComponent(cwd, Number(port));
46
+ ensureSvelteLiveRootComponent(cwd, Number(port), token);
47
47
 
48
48
  const layoutRel = detected.layoutFile;
49
49
  const layoutAbs = path.join(cwd, layoutRel);
@@ -136,18 +136,20 @@ export function unpatchSvelteLayout(content) {
136
136
  return out.replace(/\n{3,}/g, '\n\n');
137
137
  }
138
138
 
139
- export function ensureSvelteLiveRootComponent(cwd, port) {
139
+ export function ensureSvelteLiveRootComponent(cwd, port, token) {
140
140
  const file = path.join(cwd, SVELTE_LIVE_ROOT_COMPONENT);
141
141
  fs.mkdirSync(path.dirname(file), { recursive: true });
142
- fs.writeFileSync(file, buildSvelteLiveRootComponent(port), 'utf-8');
142
+ fs.writeFileSync(file, buildSvelteLiveRootComponent(port, token), 'utf-8');
143
143
  return file;
144
144
  }
145
145
 
146
- export function buildSvelteLiveRootComponent(port) {
146
+ export function buildSvelteLiveRootComponent(port, token) {
147
+ const liveUrl = 'http://localhost:' + Number(port) + '/live.js'
148
+ + (token ? '?token=' + encodeURIComponent(token) : '');
147
149
  return `<script>
148
150
  import { onMount } from 'svelte';
149
151
 
150
- const LIVE_URL = 'http://localhost:${Number(port)}/live.js';
152
+ const LIVE_URL = '${liveUrl}';
151
153
  const HOST_ID = 'impeccable-live-root';
152
154
 
153
155
  onMount(() => {