@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.
- package/CHANGELOG.md +14 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +33 -129
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_asset_producer.toml +11 -3
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_documenter.toml +26 -0
- package/dist/builtin/workflows/skills/impeccable/agents/impeccable_finish_reviewer.toml +35 -0
- package/dist/builtin/workflows/skills/impeccable/reference/android.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/animate.md +72 -189
- package/dist/builtin/workflows/skills/impeccable/reference/audit.md +10 -9
- package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +19 -108
- package/dist/builtin/workflows/skills/impeccable/reference/clarify.md +59 -253
- package/dist/builtin/workflows/skills/impeccable/reference/colorize.md +51 -222
- package/dist/builtin/workflows/skills/impeccable/reference/craft-floor.md +45 -0
- package/dist/builtin/workflows/skills/impeccable/reference/craft.md +3 -121
- package/dist/builtin/workflows/skills/impeccable/reference/critique.md +28 -20
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/asset-producer.md +97 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/documenter.md +23 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/finish-reviewer.md +32 -0
- package/dist/builtin/workflows/skills/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/dist/builtin/workflows/skills/impeccable/reference/delight.md +47 -279
- package/dist/builtin/workflows/skills/impeccable/reference/distill.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/doctor.md +53 -0
- package/dist/builtin/workflows/skills/impeccable/reference/document.md +60 -73
- package/dist/builtin/workflows/skills/impeccable/reference/harden.md +1 -12
- package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +17 -4
- package/dist/builtin/workflows/skills/impeccable/reference/init.md +64 -163
- package/dist/builtin/workflows/skills/impeccable/reference/ios.md +1 -1
- package/dist/builtin/workflows/skills/impeccable/reference/layout.md +52 -153
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +44 -32
- package/dist/builtin/workflows/skills/impeccable/reference/new-work.md +105 -0
- package/dist/builtin/workflows/skills/impeccable/reference/{product.md → operate.md} +6 -5
- package/dist/builtin/workflows/skills/impeccable/reference/optimize.md +4 -4
- package/dist/builtin/workflows/skills/impeccable/reference/overdrive.md +1 -4
- package/dist/builtin/workflows/skills/impeccable/reference/polish.md +68 -212
- package/dist/builtin/workflows/skills/impeccable/reference/quieter.md +3 -3
- package/dist/builtin/workflows/skills/impeccable/reference/routing.md +18 -0
- package/dist/builtin/workflows/skills/impeccable/reference/shape.md +38 -144
- package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +48 -269
- package/dist/builtin/workflows/skills/impeccable/reference/visualize.md +38 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/command-metadata.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/concept-seed.mjs +584 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +117 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +486 -59
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +16 -45
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +143 -26
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +181 -12
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +3187 -182
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +282 -70
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +183 -15
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +22 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/findings.mjs +7 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +16 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +155 -42
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +2988 -155
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/constants.mjs +11 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/doctor.mjs +336 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/generate-image.mjs +232 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +90 -10
- package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +410 -78
- package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +25 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/artifact-schema.mjs +93 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/composition-catalog.mjs +165 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/concept-catalog.mjs +329 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +20 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +16 -8
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/provider.mjs +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-deep.mjs +455 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness-notice.mjs +169 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/staleness.mjs +457 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/surface-briefs.mjs +151 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-slug.mjs +33 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/template-extensions.mjs +146 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/completion.mjs +10 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live/event-validation.mjs +15 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/generation-preflight.mjs +149 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/poll-lanes.mjs +14 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/session-store.mjs +109 -32
- package/dist/builtin/workflows/skills/impeccable/scripts/live/source-lock.mjs +105 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/source-search.mjs +105 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +8 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +208 -68
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +563 -112
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +198 -13
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +24 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +44 -14
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +398 -33
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +9 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +81 -67
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +7 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/palette.mjs +76 -81
- package/dist/builtin/workflows/skills/impeccable/scripts/pin.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/serve-question.mjs +890 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/surface-brief.mjs +74 -0
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +201 -22
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +23 -9
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
- package/dist/builtin/workflows/src/runs/foreground/postmortem-stage-chat.ts +20 -5
- package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +10 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-delivery-activity.ts +86 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +6 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +19 -13
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +8 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +16 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-delivery-activity.ts +121 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +42 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +16 -0
- package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +4 -0
- package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-actions.js +2 -14
- package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.js +9 -0
- package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +25 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-runtime.js +53 -0
- package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts +13 -0
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +22 -1
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.js +9 -3
- package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
- package/docs/tui.md +7 -2
- package/docs/workflows.md +1 -1
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +0 -108
- package/dist/builtin/workflows/skills/impeccable/reference/codex.md +0 -105
- 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
|
-
|
|
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 (
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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 = '
|
|
152
|
+
const LIVE_URL = '${liveUrl}';
|
|
151
153
|
const HOST_ID = 'impeccable-live-root';
|
|
152
154
|
|
|
153
155
|
onMount(() => {
|