vigthoria-cli 1.13.31 → 1.13.42

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 (51) hide show
  1. package/README.md +11 -56
  2. package/SECURITY_HARDENING.md +2 -4
  3. package/dist/commands/auth.js +16 -14
  4. package/dist/commands/cancel.d.ts +1 -0
  5. package/dist/commands/cancel.js +21 -4
  6. package/dist/commands/chat.d.ts +13 -0
  7. package/dist/commands/chat.js +170 -14
  8. package/dist/commands/fork.js +8 -3
  9. package/dist/commands/history.js +8 -3
  10. package/dist/commands/legion.js +1 -1
  11. package/dist/commands/replay.js +8 -3
  12. package/dist/commands/update-registration.js +63 -206
  13. package/dist/commands/v4-menu.d.ts +0 -1
  14. package/dist/commands/v4-menu.js +8 -6
  15. package/dist/commands/v4-registration.js +0 -2
  16. package/dist/commands/v4.js +9 -4
  17. package/dist/commands/wallet.d.ts +1 -1
  18. package/dist/commands/wallet.js +3 -3
  19. package/dist/index.js +2 -3
  20. package/dist/utils/agent-stream-state.d.ts +7 -0
  21. package/dist/utils/agent-stream-state.js +31 -0
  22. package/dist/utils/agentRunOutcome.js +5 -1
  23. package/dist/utils/api.d.ts +21 -3
  24. package/dist/utils/api.js +379 -550
  25. package/dist/utils/frontend-preview-service.d.ts +12 -2
  26. package/dist/utils/frontend-preview-service.js +214 -24
  27. package/dist/utils/javascript-syntax.d.ts +19 -0
  28. package/dist/utils/javascript-syntax.js +35 -0
  29. package/dist/utils/localTestMode.js +1 -1
  30. package/dist/utils/mutation-journal.d.ts +10 -0
  31. package/dist/utils/mutation-journal.js +104 -76
  32. package/dist/utils/network-policy.js +7 -10
  33. package/dist/utils/preview-screenshot-adapter.d.ts +111 -1
  34. package/dist/utils/preview-screenshot-adapter.js +990 -7
  35. package/dist/utils/release-install.d.ts +0 -1
  36. package/dist/utils/release-install.js +44 -7
  37. package/dist/utils/requestIntent.d.ts +1 -1
  38. package/dist/utils/requestIntent.js +17 -3
  39. package/dist/utils/tools.js +21 -15
  40. package/dist/utils/update-policy.d.ts +0 -4
  41. package/dist/utils/update-policy.js +5 -9
  42. package/dist/utils/v3-agent-client.js +25 -11
  43. package/install.ps1 +30 -50
  44. package/install.sh +12 -23
  45. package/package.json +4 -2
  46. package/release-policy.json +1 -5
  47. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +6 -7
  48. package/scripts/release/install-release.mjs +5 -4
  49. package/scripts/release/publish-cli-release.mjs +21 -8
  50. package/scripts/release/test-balanced-model-live.sh +4 -1
  51. package/scripts/release/validate-live-service-gates.sh +27 -5
@@ -1,5 +1,5 @@
1
1
  import type { MutationJournalReport } from './mutation-journal.js';
2
- import type { PreviewScreenshotPort } from './preview-screenshot-adapter.js';
2
+ import type { PreviewRuntimeInteractionResult, PreviewScreenshotPort } from './preview-screenshot-adapter.js';
3
3
  export interface FrontendPreviewGateResult {
4
4
  required: boolean;
5
5
  passed: boolean;
@@ -19,6 +19,7 @@ export interface FrontendPreviewGateResult {
19
19
  previewFileUrl?: string;
20
20
  screenshotCaptured?: boolean;
21
21
  screenshotError?: string;
22
+ runtimeInteraction?: PreviewRuntimeInteractionResult;
22
23
  };
23
24
  modes?: {
24
25
  design?: {
@@ -43,6 +44,7 @@ export interface FrontendPreviewGateResult {
43
44
  export interface FrontendPreviewServiceDependencies {
44
45
  getBaseUrls(): string[];
45
46
  getAccessToken(): string | null;
47
+ getServiceKey?(): string | null;
46
48
  fetch(input: string, init: RequestInit): Promise<Response>;
47
49
  resolveTargetPath(context: Record<string, any>): string;
48
50
  extractExpectedFiles(message: string, context: Record<string, any>): string[];
@@ -51,10 +53,17 @@ export interface FrontendPreviewServiceDependencies {
51
53
  screenshotAdapter: PreviewScreenshotPort;
52
54
  }
53
55
  export declare function isConstrainedStaticFrontendRequest(message?: string): boolean;
56
+ /**
57
+ * A code artifact can belong to a browser product without itself being a
58
+ * previewable application. Requiring an HTML entry for an explicitly scoped
59
+ * module/library request turns a successful native verification into a false
60
+ * failure and rolls the requested file back.
61
+ */
62
+ export declare function isFocusedNonPreviewableCodeArtifactRequest(message?: string, expectedFiles?: string[]): boolean;
54
63
  export declare class FrontendPreviewService {
55
64
  private readonly dependencies;
56
65
  constructor(dependencies: FrontendPreviewServiceDependencies);
57
- private isFrontendTask;
66
+ isFrontendTask(message?: string, context?: Record<string, any>): boolean;
58
67
  /**
59
68
  * Returns true when the prompt describes a read-only / analysis task.
60
69
  * Used to suppress preview gate and other write-oriented side effects.
@@ -69,6 +78,7 @@ export declare class FrontendPreviewService {
69
78
  js: string[];
70
79
  assets: string[];
71
80
  };
81
+ private expandLinkedFrontendDependencyGraph;
72
82
  private buildFrontendForPreview;
73
83
  private gatherFrontendPreviewArtifacts;
74
84
  private evaluateFrontendVisualProof;
@@ -11,6 +11,47 @@ export function isConstrainedStaticFrontendRequest(message = '') {
11
11
  && /\b(?:single[- ]file|one\s+paragraph|exactly\s+one|current\s+directory\s+only|write\s+the\s+file\b[^.\n]{0,80}\bonly)\b/.test(prompt)
12
12
  && !/\b(?:premium|signature|showcase|redesign|overhaul|dashboard|portal|saas|interactive|animation|motion|form|checkout|booking|commerce)\b/.test(prompt);
13
13
  }
14
+ /**
15
+ * A code artifact can belong to a browser product without itself being a
16
+ * previewable application. Requiring an HTML entry for an explicitly scoped
17
+ * module/library request turns a successful native verification into a false
18
+ * failure and rolls the requested file back.
19
+ */
20
+ export function isFocusedNonPreviewableCodeArtifactRequest(message = '', expectedFiles = []) {
21
+ const prompt = String(message || '').toLowerCase();
22
+ const normalizedFiles = Array.from(new Set(expectedFiles
23
+ .map((filePath) => String(filePath || '').trim().replace(/\\/g, '/').replace(/^\.\//, ''))
24
+ .filter(Boolean)));
25
+ if (normalizedFiles.length === 0 || normalizedFiles.length > 4)
26
+ return false;
27
+ if (normalizedFiles.some((filePath) => /\.(?:html?|css|scss|sass|less)$/i.test(filePath)))
28
+ return false;
29
+ if (!normalizedFiles.every((filePath) => /\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|swift|rb|php|cs|cpp|cc|c|h|hpp)$/i.test(filePath))) {
30
+ return false;
31
+ }
32
+ const explicitArtifactScope = /\b(?:es\s+module|module|library|utility|helper|class|function|service|adapter|middleware|route|schema)\b/i.test(prompt)
33
+ || /\bexport\s+(?:exactly|only)\b/i.test(prompt)
34
+ || /\b(?:create|write|implement|update|fix|repair)\b[^.\n]{0,120}\b(?:file|files)\b/i.test(prompt);
35
+ if (!explicitArtifactScope)
36
+ return false;
37
+ const wholeProductScope = /\b(?:build|create|implement|deliver|make)\b[^.\n]{0,100}\b(?:complete|full|entire|end-to-end|from scratch|production-ready)\b[^.\n]{0,80}\b(?:website|site|page|dashboard|portal|app|application|game)\b/i.test(prompt)
38
+ || /\b(?:complete|full|entire|end-to-end|playable)\s+(?:website|site|page|dashboard|portal|app|application|game)\b/i.test(prompt);
39
+ return !wholeProductScope;
40
+ }
41
+ function htmlAttributeValue(openingTag, name) {
42
+ const match = String(openingTag || '').match(new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s'"\\x60=<>]+))`, 'i'));
43
+ return match ? String(match[1] ?? match[2] ?? match[3] ?? '') : '';
44
+ }
45
+ function hasViewportMeta(html) {
46
+ return Array.from(String(html || '').matchAll(/<meta\b[^>]*>/gi)).some((match) => htmlAttributeValue(match[0], 'name').toLowerCase() === 'viewport');
47
+ }
48
+ function canonicalizePreviewHtmlAttributes(html) {
49
+ // The browser accepts valid unquoted attributes. Some older preview-service
50
+ // deployments still parse only quoted name/src/href/type tokens. Send a
51
+ // semantically equivalent canonical proof document without rewriting the
52
+ // user's source file, so standards-compliant markup is not falsely rejected.
53
+ return String(html || '').replace(/\b(name|href|src|rel|type)\s*=\s*([^\s'"\x60=<>]+)/gi, (_match, name, value) => `${name}="${value}"`);
54
+ }
14
55
  export class FrontendPreviewService {
15
56
  dependencies;
16
57
  constructor(dependencies) {
@@ -28,6 +69,9 @@ export class FrontendPreviewService {
28
69
  const prompt = String(message || '');
29
70
  const expectedFiles = this.dependencies.extractExpectedFiles(message, context);
30
71
  const hasHtmlEntry = expectedFiles.some((filePath) => /\.html?$/i.test(filePath));
72
+ if (isFocusedNonPreviewableCodeArtifactRequest(prompt, expectedFiles)) {
73
+ return false;
74
+ }
31
75
  return /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase|hero|responsive|interactive|game|canvas)/i.test(prompt)
32
76
  || hasHtmlEntry;
33
77
  }
@@ -156,24 +200,30 @@ export class FrontendPreviewService {
156
200
  }
157
201
  return this.normalizeWorkspaceRelativePath(path.posix.normalize(path.posix.join(entryDir, withoutQuery)));
158
202
  };
159
- const cssPattern = /<link[^>]+href=["']([^"']+\.css(?:[?#][^"']*)?)["'][^>]*>/gi;
160
- const jsPattern = /<script[^>]+src=["']([^"']+\.(?:js|mjs|cjs)(?:[?#][^"']*)?)["'][^>]*>/gi;
161
203
  let match;
162
- while ((match = cssPattern.exec(String(html || ''))) !== null) {
163
- const resolved = normalizeAsset(match[1]);
204
+ const linkPattern = /<link\b[^>]*>/gi;
205
+ while ((match = linkPattern.exec(String(html || ''))) !== null) {
206
+ const href = htmlAttributeValue(match[0], 'href');
207
+ if (!/\.css(?:[?#]|$)/i.test(href))
208
+ continue;
209
+ const resolved = normalizeAsset(href);
164
210
  if (resolved) {
165
211
  css.add(resolved);
166
212
  }
167
213
  }
168
- while ((match = jsPattern.exec(String(html || ''))) !== null) {
169
- const resolved = normalizeAsset(match[1]);
214
+ const scriptPattern = /<script\b[^>]*>/gi;
215
+ while ((match = scriptPattern.exec(String(html || ''))) !== null) {
216
+ const src = htmlAttributeValue(match[0], 'src');
217
+ if (!/\.(?:js|mjs|cjs)(?:[?#]|$)/i.test(src))
218
+ continue;
219
+ const resolved = normalizeAsset(src);
170
220
  if (resolved) {
171
221
  js.add(resolved);
172
222
  }
173
223
  }
174
- const assetPattern = /<(?:link|img|source|video|audio)\b[^>]+(?:href|src)=["']([^"']+)["'][^>]*>/gi;
224
+ const assetPattern = /<(?:link|img|source|video|audio)\b[^>]*>/gi;
175
225
  while ((match = assetPattern.exec(String(html || ''))) !== null) {
176
- const resolved = normalizeAsset(match[1]);
226
+ const resolved = normalizeAsset(htmlAttributeValue(match[0], 'href') || htmlAttributeValue(match[0], 'src'));
177
227
  if (resolved)
178
228
  assets.add(resolved);
179
229
  }
@@ -183,6 +233,93 @@ export class FrontendPreviewService {
183
233
  assets: Array.from(assets),
184
234
  };
185
235
  }
236
+ expandLinkedFrontendDependencyGraph(rootPath, entryFiles, kind) {
237
+ const selected = [];
238
+ const seen = new Set();
239
+ const queued = new Set();
240
+ const queue = entryFiles
241
+ .map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
242
+ .filter(Boolean);
243
+ for (const filePath of queue)
244
+ queued.add(filePath);
245
+ const extensions = kind === 'css' ? ['.css'] : ['.js', '.mjs', '.cjs'];
246
+ const resolveImport = (ownerPath, specifier) => {
247
+ const clean = String(specifier || '').trim().split('?')[0].split('#')[0];
248
+ if (!clean
249
+ || clean.startsWith('#')
250
+ || clean.startsWith('//')
251
+ || /^(?:https?:|data:|node:)/i.test(clean)
252
+ || (!clean.startsWith('.') && !clean.startsWith('/'))) {
253
+ return null;
254
+ }
255
+ const ownerDir = path.posix.dirname(this.normalizeWorkspaceRelativePath(ownerPath) || '.');
256
+ const base = this.normalizeWorkspaceRelativePath(path.posix.normalize(clean.startsWith('/')
257
+ ? clean.slice(1)
258
+ : path.posix.join(ownerDir, clean)));
259
+ const candidates = [base];
260
+ if (!path.posix.extname(base)) {
261
+ for (const extension of extensions)
262
+ candidates.push(`${base}${extension}`);
263
+ for (const extension of extensions)
264
+ candidates.push(path.posix.join(base, `index${extension}`));
265
+ }
266
+ for (const candidate of candidates) {
267
+ if (!extensions.includes(path.posix.extname(candidate).toLowerCase()))
268
+ continue;
269
+ try {
270
+ const absolute = resolveWorkspacePath(rootPath, candidate);
271
+ if (fs.statSync(absolute).isFile())
272
+ return candidate;
273
+ }
274
+ catch {
275
+ // The candidate is absent or outside the workspace boundary.
276
+ }
277
+ }
278
+ return null;
279
+ };
280
+ // A dependency graph is authoritative because every admitted file is
281
+ // reachable from the HTML entry. The cap prevents cyclic or adversarial
282
+ // graphs from inflating a preview payload; the final payload has its own
283
+ // byte limit as a second boundary.
284
+ while (queue.length > 0 && selected.length < 64) {
285
+ const ownerPath = queue.shift();
286
+ if (!ownerPath || seen.has(ownerPath))
287
+ continue;
288
+ seen.add(ownerPath);
289
+ let source = '';
290
+ try {
291
+ source = fs.readFileSync(resolveWorkspacePath(rootPath, ownerPath), 'utf8');
292
+ }
293
+ catch {
294
+ continue;
295
+ }
296
+ selected.push(ownerPath);
297
+ const specifiers = [];
298
+ if (kind === 'css') {
299
+ const importPattern = /@import\s+(?:url\(\s*)?(?:["']([^"']+)["']|([^\s)'";]+))\s*\)?/gi;
300
+ let match;
301
+ while ((match = importPattern.exec(source)) !== null)
302
+ specifiers.push(match[1] || match[2]);
303
+ }
304
+ else {
305
+ const staticPattern = /\b(?:import|export)\s+(?:[^'";]*?\sfrom\s*)?["']([^"']+)["']/g;
306
+ const dynamicPattern = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
307
+ let match;
308
+ while ((match = staticPattern.exec(source)) !== null)
309
+ specifiers.push(match[1]);
310
+ while ((match = dynamicPattern.exec(source)) !== null)
311
+ specifiers.push(match[1]);
312
+ }
313
+ for (const specifier of specifiers) {
314
+ const dependency = resolveImport(ownerPath, specifier);
315
+ if (dependency && !seen.has(dependency) && !queued.has(dependency)) {
316
+ queue.push(dependency);
317
+ queued.add(dependency);
318
+ }
319
+ }
320
+ }
321
+ return selected;
322
+ }
186
323
  async buildFrontendForPreview(rootPath) {
187
324
  const packagePath = path.join(rootPath, 'package.json');
188
325
  if (!fs.existsSync(packagePath)) {
@@ -256,12 +393,14 @@ export class FrontendPreviewService {
256
393
  // Pulling every CSS/JS file from the workspace also ingested audit copies,
257
394
  // rejected model drafts, and test harnesses into the production proof body.
258
395
  // Fall back to expected/workspace files only for documents without links.
259
- const selectedCssFiles = (linkedAssets.css.length > 0
396
+ const directCssFiles = (linkedAssets.css.length > 0
260
397
  ? chooseExisting(linkedAssets.css, [])
261
398
  : chooseExisting(expectedCss, cssFiles)).slice(0, 12);
262
- const selectedJsFiles = (linkedAssets.js.length > 0
399
+ const directJsFiles = (linkedAssets.js.length > 0
263
400
  ? chooseExisting(linkedAssets.js, [])
264
401
  : chooseExisting(expectedJs, jsFiles)).slice(0, 12);
402
+ const selectedCssFiles = this.expandLinkedFrontendDependencyGraph(rootPath, directCssFiles, 'css');
403
+ const selectedJsFiles = this.expandLinkedFrontendDependencyGraph(rootPath, directJsFiles, 'js');
265
404
  const css = selectedCssFiles.map((filePath) => fs.readFileSync(resolveWorkspacePath(rootPath, filePath), 'utf8')).join('\n\n');
266
405
  const js = selectedJsFiles.map((filePath) => fs.readFileSync(resolveWorkspacePath(rootPath, filePath), 'utf8')).join('\n\n');
267
406
  return { htmlPath, html, css, js, cssPaths: selectedCssFiles, jsPaths: selectedJsFiles };
@@ -272,6 +411,14 @@ export class FrontendPreviewService {
272
411
  const html = String(artifacts.html || '');
273
412
  const css = String(artifacts.css || '');
274
413
  const js = String(artifacts.js || '');
414
+ // runtime_check proves that an existing page can be opened by a real local
415
+ // browser. It is not a responsive-design audit. Requiring viewport metadata,
416
+ // stylesheets, and responsive CSS made focused JavaScript/CORS repairs fail
417
+ // even after Chromium produced the required screenshot. The screenshot and
418
+ // artifact persistence remain fail-closed in persistFrontendPreviewArtifacts.
419
+ if (options.functionalRuntime) {
420
+ return { ok: true, reasons: [] };
421
+ }
275
422
  if (!summary.hasViewportMeta) {
276
423
  reasons.push('missing viewport metadata');
277
424
  }
@@ -305,13 +452,30 @@ export class FrontendPreviewService {
305
452
  const screenshotPath = resolveWorkspacePath(rootPath, `.vigthoria/proof/preview/${baseName}.png`, { allowMissing: true });
306
453
  const entryAbsolutePath = resolveWorkspacePath(rootPath, previewGate.entryPath);
307
454
  const previewFileUrl = `file://${entryAbsolutePath}`;
308
- const screenshot = await this.dependencies.screenshotAdapter.capture(entryAbsolutePath, screenshotPath);
455
+ const runtimeOptions = context.runtimeInteractionProof && typeof context.runtimeInteractionProof === 'object'
456
+ ? context.runtimeInteractionProof
457
+ : null;
458
+ const runtimeInteraction = runtimeOptions
459
+ ? (typeof this.dependencies.screenshotAdapter.captureRuntime === 'function'
460
+ ? await this.dependencies.screenshotAdapter.captureRuntime(entryAbsolutePath, screenshotPath, { ...runtimeOptions, workspaceRoot: rootPath })
461
+ : {
462
+ captured: false,
463
+ passed: false,
464
+ error: 'required loopback browser interaction adapter is unavailable',
465
+ })
466
+ : null;
467
+ const screenshot = runtimeInteraction
468
+ || await this.dependencies.screenshotAdapter.capture(entryAbsolutePath, screenshotPath);
309
469
  const screenshotRequired = context.requireScreenshot === true;
310
- const effectivePreviewGate = screenshotRequired && !screenshot.captured
470
+ const screenshotFailure = screenshotRequired && !screenshot.captured;
471
+ const runtimeFailure = runtimeInteraction !== null && !runtimeInteraction.passed;
472
+ const effectivePreviewGate = screenshotFailure || runtimeFailure
311
473
  ? {
312
474
  ...previewGate,
313
475
  passed: false,
314
- error: `${previewGate.error ? `${previewGate.error} ` : ''}Required screenshot proof is unavailable: ${screenshot.error || 'capture failed'}.`,
476
+ error: `${previewGate.error ? `${previewGate.error} ` : ''}${runtimeFailure
477
+ ? (runtimeInteraction.error || 'Required loopback browser interaction proof failed.')
478
+ : `Required screenshot proof is unavailable: ${screenshot.error || 'capture failed'}.`}`,
315
479
  }
316
480
  : previewGate;
317
481
  const manifest = {
@@ -335,6 +499,16 @@ export class FrontendPreviewService {
335
499
  captured: screenshot.captured,
336
500
  error: screenshot.error || null,
337
501
  },
502
+ runtimeInteraction: runtimeInteraction ? {
503
+ passed: runtimeInteraction.passed,
504
+ action: runtimeInteraction.action || null,
505
+ initialBodyText: runtimeInteraction.initialBodyText || '',
506
+ finalBodyText: runtimeInteraction.finalBodyText || '',
507
+ bodyChanged: runtimeInteraction.bodyChanged === true,
508
+ visualChanged: runtimeInteraction.visualChanged === true,
509
+ runtimeErrors: runtimeInteraction.runtimeErrors || [],
510
+ error: runtimeInteraction.error || null,
511
+ } : null,
338
512
  };
339
513
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
340
514
  return {
@@ -345,6 +519,7 @@ export class FrontendPreviewService {
345
519
  previewFileUrl,
346
520
  screenshotCaptured: screenshot.captured,
347
521
  screenshotError: screenshot.error,
522
+ ...(runtimeInteraction ? { runtimeInteraction } : {}),
348
523
  },
349
524
  };
350
525
  }
@@ -408,14 +583,17 @@ export class FrontendPreviewService {
408
583
  const errors = [];
409
584
  if (context.localScreenshotProof === true) {
410
585
  const summary = {
411
- hasViewportMeta: /<meta\b[^>]*name=["']viewport["'][^>]*>/i.test(html),
586
+ hasViewportMeta: hasViewportMeta(html),
412
587
  hasResponsiveSignals: /@media\b/i.test(`${html}\n${css}`),
413
588
  hasInteractiveSignals: /<script[\s>]/i.test(html) || js.trim().length > 0,
414
589
  sectionCount: (html.match(/<section\b/gi) || []).length,
415
590
  proofSource: 'local-screenshot-adapter',
416
591
  };
417
592
  const constrainedStatic = isConstrainedStaticFrontendRequest(String(context.rawPrompt || message || ''));
418
- const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, { constrainedStatic });
593
+ const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, {
594
+ constrainedStatic,
595
+ functionalRuntime: context.functionalRuntimeProof === true,
596
+ });
419
597
  const localGate = {
420
598
  required: true,
421
599
  passed: visualProof.ok,
@@ -447,7 +625,7 @@ export class FrontendPreviewService {
447
625
  qualityProfile: isConstrainedStaticFrontendRequest(String(context.rawPrompt || message || ''))
448
626
  ? 'constrained-static'
449
627
  : 'auto',
450
- html,
628
+ html: canonicalizePreviewHtmlAttributes(html),
451
629
  css,
452
630
  js,
453
631
  entryPath: artifacts.htmlPath,
@@ -466,8 +644,14 @@ export class FrontendPreviewService {
466
644
  error: 'Preview proof skipped: payload exceeds size limit.',
467
645
  };
468
646
  }
469
- const internalServiceKey = process.env.VIGTHORIA_TEMPLATE_SERVICE_KEY
470
- || process.env.V3_SERVICE_KEY;
647
+ const internalServiceKeyCandidate = process.env.VIGTHORIA_TEMPLATE_SERVICE_KEY
648
+ || process.env.VIGTHORIA_V3_SERVICE_KEY
649
+ || process.env.V3_SERVICE_KEY
650
+ || this.dependencies.getServiceKey?.();
651
+ const internalServiceKey = String(internalServiceKeyCandidate || '').trim();
652
+ if (internalServiceKey && /[\r\n\0]/.test(internalServiceKey)) {
653
+ throw new Error('Stored Template Service capability contains invalid header characters.');
654
+ }
471
655
  const authToken = this.dependencies.getAccessToken();
472
656
  const response = await this.dependencies.fetch(`${baseUrl}/preview-proof`, {
473
657
  method: 'POST',
@@ -491,12 +675,18 @@ export class FrontendPreviewService {
491
675
  const summary = payload?.summary || {};
492
676
  const constrainedStatic = isConstrainedStaticFrontendRequest(String(context.rawPrompt || message || ''))
493
677
  && modes?.production?.qualityTier === 'scope-complete';
494
- const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, { constrainedStatic });
495
- const passed = payload?.success === true
496
- && modes?.design?.ready === true
497
- && modes?.live?.ready === true
498
- && modes?.production?.ready === true
499
- && visualProof.ok;
678
+ const functionalRuntime = context.functionalRuntimeProof === true;
679
+ const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, {
680
+ constrainedStatic,
681
+ functionalRuntime,
682
+ });
683
+ const modeProofReady = functionalRuntime
684
+ ? payload?.success === true && modes?.live?.ready === true
685
+ : payload?.success === true
686
+ && modes?.design?.ready === true
687
+ && modes?.live?.ready === true
688
+ && modes?.production?.ready === true;
689
+ const passed = modeProofReady && visualProof.ok;
500
690
  const previewGate = {
501
691
  required: true,
502
692
  passed,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Detect static ESM syntax even when a compact generator places exports after
3
+ * declarations on the same line. Dynamic import() alone remains valid in
4
+ * CommonJS syntax mode and does not require ESM parsing.
5
+ */
6
+ export declare function usesJavaScriptModuleSyntax(source: string, filePath?: string): boolean;
7
+ export type JavaScriptSyntaxMode = 'script' | 'module';
8
+ /**
9
+ * Return the grammars that may legitimately own a JavaScript source file.
10
+ *
11
+ * A plain `.js` file is ambiguous until its package or HTML loader is known.
12
+ * In particular, top-level await can make otherwise valid browser-module code
13
+ * fail a CommonJS-only preflight without leaving a static import/export token
14
+ * for the lightweight detector above. Trying both grammars keeps the gate
15
+ * strict (one parser must accept the complete source) without rejecting valid
16
+ * browser modules or classic scripts. Explicit `.mjs` and `.cjs` identities
17
+ * remain single-grammar and therefore fail closed.
18
+ */
19
+ export declare function resolveJavaScriptSyntaxModes(source: string, filePath?: string): JavaScriptSyntaxMode[];
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * Detect static ESM syntax even when a compact generator places exports after
4
+ * declarations on the same line. Dynamic import() alone remains valid in
5
+ * CommonJS syntax mode and does not require ESM parsing.
6
+ */
7
+ export function usesJavaScriptModuleSyntax(source, filePath = '.js') {
8
+ const extension = path.extname(filePath).toLowerCase();
9
+ if (extension === '.mjs')
10
+ return true;
11
+ if (extension === '.cjs')
12
+ return false;
13
+ return /(?:^|[;}\n]|\*\/)[ \t]*(?:import(?!\s*\()|export\b)/m.test(source);
14
+ }
15
+ /**
16
+ * Return the grammars that may legitimately own a JavaScript source file.
17
+ *
18
+ * A plain `.js` file is ambiguous until its package or HTML loader is known.
19
+ * In particular, top-level await can make otherwise valid browser-module code
20
+ * fail a CommonJS-only preflight without leaving a static import/export token
21
+ * for the lightweight detector above. Trying both grammars keeps the gate
22
+ * strict (one parser must accept the complete source) without rejecting valid
23
+ * browser modules or classic scripts. Explicit `.mjs` and `.cjs` identities
24
+ * remain single-grammar and therefore fail closed.
25
+ */
26
+ export function resolveJavaScriptSyntaxModes(source, filePath = '.js') {
27
+ const extension = path.extname(filePath).toLowerCase();
28
+ if (extension === '.mjs')
29
+ return ['module'];
30
+ if (extension === '.cjs')
31
+ return ['script'];
32
+ if (usesJavaScriptModuleSyntax(source, filePath))
33
+ return ['module'];
34
+ return ['script', 'module'];
35
+ }
@@ -20,7 +20,7 @@ export function applyLocalTestfarmDefaults() {
20
20
  process.env.VIGTHORIA_HYPERLOOP_URL ??= 'http://127.0.0.1:8020/api/hyperloop/health';
21
21
  process.env.VIGTHORIA_HYPERLOOP_EXECUTE_URL ??= 'http://127.0.0.1:8020/api/hyperloop/execute';
22
22
  process.env.VIGTHORIA_HYPERLOOP_MODULES_URL ??= 'http://127.0.0.1:8020/api/hyperloop/modules';
23
- process.env.VIGTHORIA_V3_AGENT_URL ??= 'http://127.0.0.1:8030';
23
+ process.env.VIGTHORIA_V3_AGENT_URL ??= 'http://127.0.0.1:8032';
24
24
  process.env.VIGTHORIA_TEMPLATE_SERVICE_URL ??= 'http://127.0.0.1:4011';
25
25
  process.env.VIGTHORIA_HYPERLOOP_TIMEOUT_MS ??= '180000';
26
26
  const testToken = String(process.env.VIGTHORIA_LOCAL_TEST_TOKEN || '').trim();
@@ -39,4 +39,14 @@ export declare class MutationJournal {
39
39
  commit(): MutationJournalReport;
40
40
  rollback(): MutationJournalReport;
41
41
  }
42
+ /**
43
+ * Last-resort synchronous rollback for process signals.
44
+ *
45
+ * APIClient normally owns the foreground journal, but command teardown or a
46
+ * second lifecycle wrapper must never be able to detach an open transaction
47
+ * from SIGINT/SIGTERM protection. Journals register at construction and
48
+ * unregister only after commit/rollback, so this fallback closes every still
49
+ * open workspace transaction before the original signal is re-raised.
50
+ */
51
+ export declare function rollbackActiveMutationJournals(): MutationJournalReport[];
42
52
  export declare function finalizeMutationTransaction(journal: MutationJournal, proofSucceeded: boolean): MutationJournalReport;