vigthoria-cli 1.13.34 → 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 +19 -4
  24. package/dist/utils/api.js +322 -550
  25. package/dist/utils/frontend-preview-service.d.ts +10 -1
  26. package/dist/utils/frontend-preview-service.js +162 -17
  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 +81 -0
  34. package/dist/utils/preview-screenshot-adapter.js +615 -38
  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 +13 -24
  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
package/dist/utils/api.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import axios from 'axios';
6
6
  import { createHash, randomUUID } from 'crypto';
7
+ import { execFileSync } from 'child_process';
7
8
  import fs from 'fs';
8
9
  import https from 'https';
9
10
  import net from 'net';
@@ -22,7 +23,7 @@ import { buildClientManifest } from './clientManifest.js';
22
23
  import { assertTrustedEndpoint, guardedFetch, installAxiosNetworkPolicy, installGlobalFetchPolicy } from './network-policy.js';
23
24
  import { assertSafeRelativePath, resolveWorkspacePath } from './workspace-boundary.js';
24
25
  import { parseSupportedProcess } from './process-policy.js';
25
- import { SystemBrowserScreenshotAdapter } from './preview-screenshot-adapter.js';
26
+ import { SystemBrowserScreenshotAdapter, } from './preview-screenshot-adapter.js';
26
27
  import { normalizeSubscriptionResponse } from './subscription-policy.js';
27
28
  import { ModelGovernance } from './model-governance.js';
28
29
  import { OperatorClient, OperatorClientError } from './operator-client.js';
@@ -37,9 +38,22 @@ import { VigFlowClient, } from './vigflow-client.js';
37
38
  import { MutationJournal, finalizeMutationTransaction, } from './mutation-journal.js';
38
39
  import { containsHighConfidenceSecret, isSensitivePath, redactSensitiveText, safeChildProcessEnv, scanOutboundContext, } from './secret-policy.js';
39
40
  import { buildLocalWorkspaceReference } from './workspace-reference.js';
41
+ import { resolveJavaScriptSyntaxModes } from './javascript-syntax.js';
40
42
  export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
41
43
  export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
44
+ export function resolveSelfHostedModelAuthToken(environment = process.env, userToken = '') {
45
+ const serviceToken = String(environment.VIGTHORIA_INFERENCE_SERVICE_TOKEN || '').trim();
46
+ return serviceToken || String(userToken || '').trim();
47
+ }
42
48
  const MAX_V3_CLIENT_MUTATION_BYTES = 512 * 1024;
49
+ // These are the authoritative V3 request-schema ceilings. A local tool can
50
+ // produce a very long parser diagnostic (for example when a generated module
51
+ // contains one large unterminated line). Sending that diagnostic verbatim
52
+ // makes FastAPI reject the result with HTTP 422 while the server-side tool
53
+ // future continues waiting. Normalize before the first submission so every
54
+ // local outcome, including failure, is deliverable.
55
+ const MAX_V3_CLIENT_RESULT_OUTPUT_CHARS = 500_000;
56
+ const MAX_V3_CLIENT_RESULT_ERROR_CHARS = 10_000;
43
57
  export class CLIError extends Error {
44
58
  category;
45
59
  statusCode;
@@ -152,7 +166,7 @@ export function sanitizeUserFacingPathText(input) {
152
166
  [/\bvig-remote(?:-server)?-[A-Za-z0-9._-]+/gi, 'workspace'],
153
167
  [/\bvig-fork-[A-Za-z0-9._-]+/gi, 'workspace'],
154
168
  // ── V3 Code Agent installation tree and sibling services
155
- [new RegExp(String.raw `/var/www/V3-Code-Agent(?:/${pathToken})?`, 'gi'), '[Vigthoria Agent]'],
169
+ [new RegExp(String.raw `/var/www/V3-Code-Agent(?:-(?:IDE|CLI|VIBE))?(?:/${pathToken})?`, 'gi'), '[Vigthoria Agent]'],
156
170
  [new RegExp(String.raw `/var/www/(?:agent-vigthoria|vigthoria-model-router|vigthoria-devtools-bridge|vigthoria-hyper-loop|vigthoria-mcp-server|vigthoria-template-service|vigthoria-security-devops|vigthoria-coder|vigthoria-code|Vigthoria-Code-2|vigthoria-asset-storage|vigthoria-storage-service|vigthoria-hosted)(?:/${pathToken})?`, 'gi'), '[Vigthoria service]'],
157
171
  // ── User workspace mount + long-term storage backing path
158
172
  [new RegExp(String.raw `/var/www/(?:vigthoria-user-workspaces|\.vigthoria)(?:/${pathToken})?`, 'gi'), 'workspace'],
@@ -320,6 +334,13 @@ export class APIClient {
320
334
  pendingV3ClientToolTasks = new Set();
321
335
  /** Tracks files mutated via client_tool_request during an active V3 SSE stream. */
322
336
  activeV3StreamedFiles = null;
337
+ /**
338
+ * Owns the currently open local workspace transaction for a foreground V3
339
+ * run. This must live on the API lifecycle rather than only in the async
340
+ * call stack: SIGINT/SIGTERM can otherwise terminate Node before the
341
+ * workflow catch block gets a chance to restore the workspace.
342
+ */
343
+ activeMutationJournal = null;
323
344
  lifecycleAbortController = new AbortController();
324
345
  frontendPreviewService;
325
346
  withLifecycleSignal(signal) {
@@ -421,6 +442,7 @@ export class APIClient {
421
442
  this.frontendPreviewService = new FrontendPreviewService({
422
443
  getBaseUrls: () => this.getTemplateServiceBaseUrls(),
423
444
  getAccessToken: () => this.getAccessToken(),
445
+ getServiceKey: () => this.config.get('v3ServiceKey'),
424
446
  fetch: (input, init) => guardedFetch(input, { ...init, signal: this.withLifecycleSignal(init.signal) }, { audience: 'template' }),
425
447
  resolveTargetPath: (context) => this.resolveAgentTargetPath(context),
426
448
  extractExpectedFiles: (message, context) => this.extractExpectedWorkspaceFiles(message, context),
@@ -510,7 +532,7 @@ export class APIClient {
510
532
  return req;
511
533
  });
512
534
  this.selfHostedModelRouterClient?.interceptors.request.use((req) => {
513
- const token = this.getAccessToken();
535
+ const token = resolveSelfHostedModelAuthToken(process.env, this.getAccessToken() || '');
514
536
  const destination = resolveAxiosRequestUrl(req);
515
537
  if (token) {
516
538
  assertTrustedEndpoint(destination, { audience: 'models', credentialBearing: true });
@@ -519,9 +541,9 @@ export class APIClient {
519
541
  return req;
520
542
  });
521
543
  // Add response interceptors for token refresh + structured errors
522
- const createAuthRetryInterceptor = (client) => {
544
+ const createAuthRetryInterceptor = (client, refreshableCredential = true) => {
523
545
  client.interceptors.response.use((res) => res, async (error) => {
524
- if (error.response?.status === 401) {
546
+ if (error.response?.status === 401 && refreshableCredential) {
525
547
  const refreshed = await this.refreshToken();
526
548
  if (refreshed && error.config) {
527
549
  return client.request(error.config);
@@ -534,7 +556,8 @@ export class APIClient {
534
556
  createAuthRetryInterceptor(this.client);
535
557
  createAuthRetryInterceptor(this.modelRouterClient);
536
558
  if (this.selfHostedModelRouterClient) {
537
- createAuthRetryInterceptor(this.selfHostedModelRouterClient);
559
+ const usesServiceIdentity = Boolean(String(process.env.VIGTHORIA_INFERENCE_SERVICE_TOKEN || '').trim());
560
+ createAuthRetryInterceptor(this.selfHostedModelRouterClient, !usesServiceIdentity);
538
561
  }
539
562
  }
540
563
  /**
@@ -544,6 +567,7 @@ export class APIClient {
544
567
  * on Windows / Node 25+.
545
568
  */
546
569
  destroy() {
570
+ const mutation = this.rollbackActiveMutationTransaction();
547
571
  this.lifecycleAbortController.abort();
548
572
  this.unsubscribeAuthInvalidation?.();
549
573
  this.unsubscribeAuthInvalidation = null;
@@ -551,6 +575,45 @@ export class APIClient {
551
575
  this._httpsAgent.destroy();
552
576
  this._httpsAgent = null;
553
577
  }
578
+ return mutation;
579
+ }
580
+ trackMutationTransaction(journal) {
581
+ if (this.activeMutationJournal && this.activeMutationJournal !== journal) {
582
+ throw new Error('A V3 workspace mutation transaction is already active.');
583
+ }
584
+ this.activeMutationJournal = journal;
585
+ }
586
+ releaseMutationTransaction(journal) {
587
+ if (this.activeMutationJournal === journal) {
588
+ this.activeMutationJournal = null;
589
+ }
590
+ }
591
+ rollbackMutationTransaction(journal) {
592
+ try {
593
+ return journal.rollback();
594
+ }
595
+ finally {
596
+ this.releaseMutationTransaction(journal);
597
+ }
598
+ }
599
+ rollbackActiveMutationTransaction() {
600
+ const journal = this.activeMutationJournal;
601
+ if (!journal)
602
+ return null;
603
+ try {
604
+ const mutation = this.rollbackMutationTransaction(journal);
605
+ if (mutation.partialMutation) {
606
+ this.logger.error(`Interrupted V3 operation ${mutation.operationId} left a partial workspace mutation: ${mutation.rollbackFailures.join('; ')}`);
607
+ }
608
+ else if (mutation.affectedPaths.length > 0) {
609
+ this.logger.warn(`Interrupted V3 operation ${mutation.operationId}; restored ${mutation.affectedPaths.length} workspace path(s).`);
610
+ }
611
+ return mutation;
612
+ }
613
+ catch (error) {
614
+ this.logger.error(`Interrupted V3 workspace rollback failed: ${error.message}`);
615
+ return null;
616
+ }
554
617
  }
555
618
  /** Exposed for Balanced 4B agent routing (local inference :8016). */
556
619
  getSelfHostedModelsApiUrl() {
@@ -896,9 +959,11 @@ export class APIClient {
896
959
  const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus, !clientToolExecution);
897
960
  if (clientToolExecution && localWorkspaceSummary) {
898
961
  localWorkspaceSummary.workspaceHydration = {
899
- delivery: 'client-tool-bridge',
962
+ delivery: resolvedContext.workspaceFilesOutOfBand === true
963
+ ? 'client-tool-bridge+validation-mirror'
964
+ : 'client-tool-bridge',
900
965
  fileCount: Number(localWorkspaceSummary.fileCount || 0),
901
- complete: false,
966
+ complete: resolvedContext.workspaceFilesOutOfBand === true,
902
967
  authoritativeLocation: 'client',
903
968
  };
904
969
  delete localWorkspaceSummary.workspaceFiles;
@@ -917,9 +982,11 @@ export class APIClient {
917
982
  serverWorkspacePath,
918
983
  });
919
984
  // A local CLI/Workbench workspace remains authoritative on that machine.
920
- // Only server-owned/background runs may hydrate a bounded server copy.
985
+ // Its bounded server scratch copy is validation-only and is hydrated from
986
+ // the out-of-band request payload so established cross-file interfaces are
987
+ // available before the first delegated mutation is mirrored.
921
988
  const effectiveWorkspacePath = serverWorkspacePath || null;
922
- const needsHydration = !clientToolExecution && !serverWorkspacePath && !!localWorkspacePath;
989
+ const needsHydration = !serverWorkspacePath && !!localWorkspacePath;
923
990
  const agentTaskType = resolvedContext.agentTaskType || 'general';
924
991
  const rawPrompt = resolvedContext.rawPrompt || resolvedContext.prompt || '';
925
992
  const executionHints = (resolvedContext.executionHints && typeof resolvedContext.executionHints === 'object')
@@ -973,13 +1040,15 @@ export class APIClient {
973
1040
  projectPath: effectiveWorkspacePath,
974
1041
  targetPath: effectiveWorkspacePath,
975
1042
  // Never send a client-local absolute path to the V3 model boundary.
976
- // The server only receives a stable label. Local bridge runs retrieve
977
- // file contents on demand through authenticated client tool requests.
1043
+ // The server only receives a stable label. Local bridge runs still
1044
+ // execute reads and writes on the client; the separate bounded file map
1045
+ // exists only to keep the server's validation mirror graph-complete.
978
1046
  localWorkspacePath: serverWorkspacePath ? serverWorkspacePath : localWorkspaceRef,
979
1047
  localWorkspaceRef,
980
1048
  localWorkspaceName,
981
1049
  localWorkspaceSummary,
982
- // This is true only for explicit server-owned/background execution.
1050
+ // A remote local-workspace run also requires its validation-only mirror
1051
+ // to be hydrated before execution starts.
983
1052
  workspaceHydrationRequired: needsHydration,
984
1053
  contextId: resolvedContext.contextId,
985
1054
  traceId: resolvedContext.traceId,
@@ -1288,462 +1357,6 @@ export class APIClient {
1288
1357
  contextState: resolvedContext.contextState || null,
1289
1358
  });
1290
1359
  }
1291
- extractEmergencyAppName(message = '', fallback = 'Signal Desk') {
1292
- const match = String(message || '').match(/called\s+([A-Z][A-Za-z0-9&\- ]{2,40})/i);
1293
- return match?.[1]?.trim() || fallback;
1294
- }
1295
- materializeEmergencySaaSWorkspace(message = '', context = {}) {
1296
- const rootPath = this.resolveAgentTargetPath(context);
1297
- if (!rootPath) {
1298
- return null;
1299
- }
1300
- fs.mkdirSync(rootPath, { recursive: true });
1301
- const appName = this.extractEmergencyAppName(message);
1302
- const html = `<!DOCTYPE html>
1303
- <html lang="en">
1304
- <head>
1305
- <meta charset="UTF-8">
1306
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1307
- <title>${appName}</title>
1308
- <link rel="stylesheet" href="styles.css">
1309
- </head>
1310
- <body>
1311
- <div class="app-shell">
1312
- <aside class="sidebar">
1313
- <div class="brand">${appName}</div>
1314
- <button class="menu-toggle" id="menu-toggle" aria-label="Toggle navigation">Menu</button>
1315
- <nav>
1316
- <a href="#dashboard" class="nav-link active">Dashboard</a>
1317
- <a href="#team" class="nav-link">Team</a>
1318
- <a href="#billing" class="nav-link">Billing</a>
1319
- <a href="#settings" class="nav-link">Settings</a>
1320
- </nav>
1321
- </aside>
1322
- <main class="content">
1323
- <section class="hero-card panel active-panel" id="dashboard">
1324
- <div class="hero-copy">
1325
- <p class="eyebrow">Dashboard</p>
1326
- <h1>${appName} revenue command center</h1>
1327
- <p>Track login activity, campaign velocity, billing state, and team performance from one responsive SaaS workspace.</p>
1328
- </div>
1329
- <form class="login-card">
1330
- <h2>Login</h2>
1331
- <label>Email<input type="email" placeholder="ops@${appName.toLowerCase().replace(/[^a-z0-9]+/g, '') || 'signaldesk'}.io"></label>
1332
- <label>Password<input type="password" placeholder="Enter password"></label>
1333
- <button type="submit">Enter dashboard</button>
1334
- </form>
1335
- </section>
1336
-
1337
- <section class="stats-grid">
1338
- <article class="stat-card"><span>MRR</span><strong>$284K</strong><em>+12.4%</em></article>
1339
- <article class="stat-card"><span>Activation</span><strong>74%</strong><em>+6.1%</em></article>
1340
- <article class="stat-card"><span>Team Seats</span><strong>128</strong><em>8 pending</em></article>
1341
- <article class="stat-card"><span>Churn Risk</span><strong>2.1%</strong><em>Low</em></article>
1342
- </section>
1343
-
1344
- <section class="workspace-grid">
1345
- <article class="panel chart-panel">
1346
- <div class="panel-header">
1347
- <h2>Analytics</h2>
1348
- <button id="open-modal" type="button">Add campaign</button>
1349
- </div>
1350
- <div class="chart-bars" aria-label="Revenue chart">
1351
- <div class="bar" style="--value: 52%"><span>Mon</span></div>
1352
- <div class="bar" style="--value: 68%"><span>Tue</span></div>
1353
- <div class="bar" style="--value: 74%"><span>Wed</span></div>
1354
- <div class="bar" style="--value: 59%"><span>Thu</span></div>
1355
- <div class="bar" style="--value: 88%"><span>Fri</span></div>
1356
- </div>
1357
- </article>
1358
-
1359
- <article class="panel activity-panel">
1360
- <div class="panel-header"><h2>Activity Feed</h2><span>Live</span></div>
1361
- <ul class="activity-feed">
1362
- <li><strong>Billing</strong><span>Enterprise invoice paid</span></li>
1363
- <li><strong>Team</strong><span>New strategist invited to workspace</span></li>
1364
- <li><strong>Dashboard</strong><span>KPI threshold updated for activation alerts</span></li>
1365
- </ul>
1366
- </article>
1367
-
1368
- <article class="panel" id="team">
1369
- <div class="panel-header"><h2>Team Management</h2><span>Owners and operators</span></div>
1370
- <div class="team-list">
1371
- <div><strong>Ana</strong><span>Growth lead</span></div>
1372
- <div><strong>Marcus</strong><span>Billing admin</span></div>
1373
- <div><strong>Lina</strong><span>Lifecycle analyst</span></div>
1374
- </div>
1375
- </article>
1376
-
1377
- <article class="panel" id="billing">
1378
- <div class="panel-header"><h2>Billing</h2><span>Current plan</span></div>
1379
- <div class="billing-card">
1380
- <strong>Scale Annual</strong>
1381
- <p>Renews on 12 Oct with usage-based analytics overages.</p>
1382
- <button type="button" class="secondary-action">Update payment method</button>
1383
- </div>
1384
- </article>
1385
-
1386
- <article class="panel" id="settings">
1387
- <div class="panel-header"><h2>Settings</h2><span>Automation and alerts</span></div>
1388
- <form class="settings-form">
1389
- <label>Alert threshold<input type="number" value="18"></label>
1390
- <label>Weekly digest<select><option>Enabled</option><option>Paused</option></select></label>
1391
- <button type="submit">Save settings</button>
1392
- </form>
1393
- </article>
1394
- </section>
1395
- </main>
1396
- </div>
1397
-
1398
- <dialog id="campaign-modal">
1399
- <form method="dialog" class="modal-form">
1400
- <h2>Launch campaign</h2>
1401
- <label>Name<input type="text" placeholder="Retention push"></label>
1402
- <label>Owner<input type="text" placeholder="Lina"></label>
1403
- <menu>
1404
- <button value="cancel">Cancel</button>
1405
- <button value="confirm">Create</button>
1406
- </menu>
1407
- </form>
1408
- </dialog>
1409
-
1410
- <script src="scripts.js"></script>
1411
- </body>
1412
- </html>
1413
- `;
1414
- const css = `:root {
1415
- --bg: #f2ede4;
1416
- --ink: #18222f;
1417
- --muted: #5c6674;
1418
- --panel: rgba(255, 255, 255, 0.82);
1419
- --line: rgba(24, 34, 47, 0.08);
1420
- --accent: #b6542c;
1421
- --accent-strong: #7f3417;
1422
- --shadow: 0 24px 60px rgba(24, 34, 47, 0.12);
1423
- }
1424
-
1425
- * { box-sizing: border-box; }
1426
-
1427
- body {
1428
- margin: 0;
1429
- font-family: "Georgia", "Times New Roman", serif;
1430
- color: var(--ink);
1431
- background:
1432
- radial-gradient(circle at top left, rgba(182, 84, 44, 0.18), transparent 28%),
1433
- radial-gradient(circle at bottom right, rgba(24, 34, 47, 0.14), transparent 30%),
1434
- var(--bg);
1435
- }
1436
-
1437
- .app-shell {
1438
- min-height: 100vh;
1439
- display: grid;
1440
- grid-template-columns: 260px 1fr;
1441
- }
1442
-
1443
- .sidebar {
1444
- padding: 2rem 1.25rem;
1445
- background: rgba(24, 34, 47, 0.94);
1446
- color: #f7f2eb;
1447
- position: sticky;
1448
- top: 0;
1449
- min-height: 100vh;
1450
- }
1451
-
1452
- .brand {
1453
- font-size: 1.6rem;
1454
- font-weight: 700;
1455
- margin-bottom: 1.5rem;
1456
- }
1457
-
1458
- .menu-toggle {
1459
- display: none;
1460
- margin-bottom: 1rem;
1461
- }
1462
-
1463
- nav {
1464
- display: grid;
1465
- gap: 0.6rem;
1466
- }
1467
-
1468
- .nav-link {
1469
- color: inherit;
1470
- text-decoration: none;
1471
- padding: 0.8rem 0.95rem;
1472
- border-radius: 999px;
1473
- transition: transform 0.25s ease, background-color 0.25s ease;
1474
- }
1475
-
1476
- .nav-link:hover,
1477
- .nav-link.active {
1478
- background: rgba(255, 255, 255, 0.12);
1479
- transform: translateX(4px);
1480
- }
1481
-
1482
- .content {
1483
- padding: 2rem;
1484
- }
1485
-
1486
- .hero-card,
1487
- .panel,
1488
- .stat-card,
1489
- .login-card,
1490
- dialog {
1491
- background: var(--panel);
1492
- backdrop-filter: blur(16px);
1493
- border: 1px solid var(--line);
1494
- box-shadow: var(--shadow);
1495
- }
1496
-
1497
- .hero-card {
1498
- display: grid;
1499
- grid-template-columns: 1.3fr 0.9fr;
1500
- gap: 1.5rem;
1501
- border-radius: 32px;
1502
- padding: 2rem;
1503
- margin-bottom: 1.5rem;
1504
- }
1505
-
1506
- .eyebrow {
1507
- text-transform: uppercase;
1508
- letter-spacing: 0.14em;
1509
- color: var(--accent-strong);
1510
- font-size: 0.78rem;
1511
- }
1512
-
1513
- .hero-card h1,
1514
- .panel h2,
1515
- .login-card h2 {
1516
- margin: 0 0 0.75rem;
1517
- }
1518
-
1519
- .login-card,
1520
- .panel,
1521
- .stat-card {
1522
- border-radius: 24px;
1523
- }
1524
-
1525
- .login-card,
1526
- .settings-form,
1527
- .modal-form {
1528
- display: grid;
1529
- gap: 0.85rem;
1530
- }
1531
-
1532
- .stats-grid,
1533
- .workspace-grid {
1534
- display: grid;
1535
- gap: 1rem;
1536
- }
1537
-
1538
- .stats-grid {
1539
- grid-template-columns: repeat(4, minmax(0, 1fr));
1540
- margin-bottom: 1rem;
1541
- }
1542
-
1543
- .workspace-grid {
1544
- grid-template-columns: repeat(2, minmax(0, 1fr));
1545
- }
1546
-
1547
- .stat-card,
1548
- .panel {
1549
- padding: 1.2rem;
1550
- animation: riseIn 0.7s ease forwards;
1551
- }
1552
-
1553
- .stat-card span,
1554
- .panel-header span,
1555
- .activity-feed span,
1556
- .team-list span,
1557
- .billing-card p {
1558
- color: var(--muted);
1559
- }
1560
-
1561
- .panel-header {
1562
- display: flex;
1563
- align-items: center;
1564
- justify-content: space-between;
1565
- gap: 1rem;
1566
- margin-bottom: 1rem;
1567
- }
1568
-
1569
- .chart-bars {
1570
- display: grid;
1571
- grid-template-columns: repeat(5, minmax(0, 1fr));
1572
- gap: 0.9rem;
1573
- align-items: end;
1574
- min-height: 220px;
1575
- }
1576
-
1577
- .bar {
1578
- position: relative;
1579
- min-height: 180px;
1580
- border-radius: 20px 20px 8px 8px;
1581
- background: linear-gradient(180deg, rgba(182, 84, 44, 0.92), rgba(127, 52, 23, 0.68));
1582
- transform-origin: bottom;
1583
- transform: scaleY(calc(var(--value) / 100));
1584
- transition: transform 0.6s ease;
1585
- }
1586
-
1587
- .bar span {
1588
- position: absolute;
1589
- left: 50%;
1590
- bottom: -1.6rem;
1591
- transform: translateX(-50%);
1592
- }
1593
-
1594
- .activity-feed,
1595
- .team-list {
1596
- display: grid;
1597
- gap: 0.8rem;
1598
- padding: 0;
1599
- margin: 0;
1600
- list-style: none;
1601
- }
1602
-
1603
- .activity-feed li,
1604
- .team-list div,
1605
- .billing-card {
1606
- padding: 0.9rem 1rem;
1607
- border-radius: 18px;
1608
- background: rgba(255, 255, 255, 0.7);
1609
- border: 1px solid var(--line);
1610
- }
1611
-
1612
- label {
1613
- display: grid;
1614
- gap: 0.35rem;
1615
- font-size: 0.95rem;
1616
- }
1617
-
1618
- input,
1619
- select,
1620
- button {
1621
- font: inherit;
1622
- }
1623
-
1624
- input,
1625
- select {
1626
- width: 100%;
1627
- padding: 0.85rem 1rem;
1628
- border-radius: 14px;
1629
- border: 1px solid var(--line);
1630
- background: rgba(255, 255, 255, 0.92);
1631
- }
1632
-
1633
- button {
1634
- border: none;
1635
- border-radius: 999px;
1636
- padding: 0.85rem 1.2rem;
1637
- background: var(--accent);
1638
- color: #fff9f3;
1639
- cursor: pointer;
1640
- transition: transform 0.25s ease, background-color 0.25s ease;
1641
- }
1642
-
1643
- button:hover {
1644
- background: var(--accent-strong);
1645
- transform: translateY(-2px);
1646
- }
1647
-
1648
- .secondary-action,
1649
- menu button:first-child {
1650
- background: rgba(24, 34, 47, 0.12);
1651
- color: var(--ink);
1652
- }
1653
-
1654
- dialog {
1655
- border-radius: 28px;
1656
- padding: 0;
1657
- width: min(420px, calc(100% - 2rem));
1658
- }
1659
-
1660
- dialog::backdrop {
1661
- background: rgba(24, 34, 47, 0.3);
1662
- }
1663
-
1664
- .modal-form {
1665
- padding: 1.4rem;
1666
- }
1667
-
1668
- menu {
1669
- display: flex;
1670
- justify-content: flex-end;
1671
- gap: 0.75rem;
1672
- padding: 0;
1673
- margin: 0.5rem 0 0;
1674
- }
1675
-
1676
- @keyframes riseIn {
1677
- from {
1678
- opacity: 0;
1679
- transform: translateY(18px);
1680
- }
1681
- to {
1682
- opacity: 1;
1683
- transform: translateY(0);
1684
- }
1685
- }
1686
-
1687
- @media (max-width: 980px) {
1688
- .app-shell,
1689
- .hero-card,
1690
- .stats-grid,
1691
- .workspace-grid {
1692
- grid-template-columns: 1fr;
1693
- }
1694
-
1695
- .sidebar {
1696
- position: static;
1697
- min-height: auto;
1698
- }
1699
-
1700
- .menu-toggle {
1701
- display: inline-flex;
1702
- }
1703
-
1704
- nav {
1705
- display: none;
1706
- }
1707
-
1708
- nav.is-open {
1709
- display: grid;
1710
- }
1711
- }
1712
- `;
1713
- const js = `document.addEventListener('DOMContentLoaded', () => {
1714
- const menuToggle = document.getElementById('menu-toggle');
1715
- const nav = document.querySelector('nav');
1716
- const modal = document.getElementById('campaign-modal');
1717
- const openModal = document.getElementById('open-modal');
1718
- const navLinks = document.querySelectorAll('.nav-link');
1719
-
1720
- menuToggle?.addEventListener('click', () => nav?.classList.toggle('is-open'));
1721
- openModal?.addEventListener('click', () => modal?.showModal());
1722
- modal?.addEventListener('close', () => document.body.classList.remove('modal-open'));
1723
-
1724
- navLinks.forEach((link) => {
1725
- link.addEventListener('click', (event) => {
1726
- event.preventDefault();
1727
- navLinks.forEach((entry) => entry.classList.remove('active'));
1728
- link.classList.add('active');
1729
- document.querySelector(link.getAttribute('href'))?.scrollIntoView({ behavior: 'smooth', block: 'start' });
1730
- nav?.classList.remove('is-open');
1731
- });
1732
- });
1733
-
1734
- document.querySelectorAll('.bar').forEach((bar, index) => {
1735
- bar.animate([
1736
- { transform: 'scaleY(0.15)' },
1737
- { transform: getComputedStyle(bar).transform || 'scaleY(1)' }
1738
- ], { duration: 600 + index * 80, fill: 'forwards', easing: 'ease-out' });
1739
- });
1740
- });
1741
- `;
1742
- fs.writeFileSync(path.join(rootPath, 'index.html'), `${html.trimEnd()}\n`, 'utf8');
1743
- fs.writeFileSync(path.join(rootPath, 'styles.css'), `${css.trimEnd()}\n`, 'utf8');
1744
- fs.writeFileSync(path.join(rootPath, 'scripts.js'), `${js.trimEnd()}\n`, 'utf8');
1745
- return appName;
1746
- }
1747
1360
  ensureExecutionContext(context = {}) {
1748
1361
  const existingId = String(context.contextId || context.traceId || '').trim();
1749
1362
  const contextId = existingId || `vig-${Date.now()}-${randomUUID().slice(0, 8)}`;
@@ -1993,9 +1606,9 @@ menu {
1993
1606
  summary.readmeExcerpt = fs.readFileSync(readmePath, 'utf8').slice(0, 2500);
1994
1607
  }
1995
1608
  if (includeWorkspaceFiles) {
1996
- // Explicit server-owned/background execution may hydrate a bounded
1997
- // copy. Interactive CLI/Workbench runs retrieve files on demand via
1998
- // the local tool bridge and never upload a workspace mirror.
1609
+ // The context-embedded form is reserved for server-owned/background
1610
+ // execution. Interactive CLI/Workbench hydration is sent separately
1611
+ // from model context and remains a bounded validation-only mirror.
1999
1612
  summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, orderedPaths);
2000
1613
  }
2001
1614
  return summary;
@@ -2013,15 +1626,7 @@ menu {
2013
1626
  const resolvedContext = this.ensureExecutionContext(context);
2014
1627
  const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
2015
1628
  const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
2016
- const executionSurface = String(resolvedContext.executionSurface || resolvedContext.clientSurface || 'cli');
2017
- const localMachineCapable = resolvedContext.localMachineCapable !== false;
2018
- const clientToolExecution = resolvedContext.clientToolExecution === false
2019
- ? false
2020
- : (resolvedContext.clientToolExecution === true
2021
- || (!serverWorkspacePath
2022
- && localMachineCapable
2023
- && ['cli', 'fork', 'local-ide', 'desktop', 'local'].includes(executionSurface)));
2024
- if (clientToolExecution || serverWorkspacePath || !localWorkspacePath)
1629
+ if (serverWorkspacePath || !localWorkspacePath)
2025
1630
  return undefined;
2026
1631
  const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
2027
1632
  const summary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
@@ -2084,7 +1689,7 @@ menu {
2084
1689
  return;
2085
1690
  }
2086
1691
  const name = String(event.name || event.tool || '').trim();
2087
- if (name !== 'write_file' && name !== 'edit_file') {
1692
+ if (name !== 'write_file' && name !== 'edit_file' && name !== 'delete_file') {
2088
1693
  return;
2089
1694
  }
2090
1695
  const args = event.arguments || {};
@@ -2100,6 +1705,10 @@ menu {
2100
1705
  if (!bucket) {
2101
1706
  return;
2102
1707
  }
1708
+ if (name === 'delete_file') {
1709
+ delete bucket[target.relativePath];
1710
+ return;
1711
+ }
2103
1712
  if (name === 'write_file' && typeof args.content === 'string') {
2104
1713
  bucket[target.relativePath] = args.content;
2105
1714
  return;
@@ -2130,6 +1739,71 @@ menu {
2130
1739
  sha256: createHash('sha256').update(content, 'utf8').digest('hex'),
2131
1740
  };
2132
1741
  }
1742
+ validateV3ClientJavaScript(relativePath, source) {
1743
+ const extension = path.extname(relativePath).toLowerCase();
1744
+ if (!['.js', '.mjs', '.cjs'].includes(extension)) {
1745
+ return null;
1746
+ }
1747
+ let firstError = null;
1748
+ for (const mode of resolveJavaScriptSyntaxModes(source, relativePath)) {
1749
+ const argv = mode === 'module'
1750
+ ? ['--input-type=module', '--check', '-']
1751
+ : ['--check', '-'];
1752
+ try {
1753
+ execFileSync(process.execPath, argv, {
1754
+ input: source,
1755
+ stdio: ['pipe', 'pipe', 'pipe'],
1756
+ timeout: 10_000,
1757
+ windowsHide: true,
1758
+ env: safeChildProcessEnv(),
1759
+ });
1760
+ return null;
1761
+ }
1762
+ catch (error) {
1763
+ const detail = error?.stderr?.toString?.().trim() || error?.message || 'JavaScript syntax check failed.';
1764
+ firstError ||= this.compactV3ClientJavaScriptSyntaxError(relativePath, String(detail));
1765
+ }
1766
+ }
1767
+ return firstError || 'JavaScript syntax check failed.';
1768
+ }
1769
+ compactV3ClientJavaScriptSyntaxError(relativePath, detail) {
1770
+ const normalized = String(detail || '').replaceAll('[stdin]', relativePath).replaceAll('\r\n', '\n');
1771
+ const lines = normalized.split('\n').map((line) => line.trim()).filter(Boolean);
1772
+ const escapedPath = relativePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1773
+ const location = lines.find((line) => new RegExp(`^${escapedPath}:\\d+(?::\\d+)?$`).test(line))
1774
+ || lines.find((line) => /:\d+(?::\d+)?$/.test(line));
1775
+ const syntax = [...lines].reverse().find((line) => /^(?:[A-Za-z]*SyntaxError|SyntaxError):/i.test(line))
1776
+ || [...lines].reverse().find((line) => /(?:unexpected|missing|unterminated|invalid)/i.test(line));
1777
+ if (location || syntax) {
1778
+ return [location, syntax].filter(Boolean).join(' ').slice(0, 900);
1779
+ }
1780
+ const nonStack = lines.find((line) => !/^at\s/.test(line) && !/^node:/.test(line));
1781
+ return (nonStack || 'JavaScript syntax check failed.').slice(0, 900);
1782
+ }
1783
+ isV3ReleaseVerificationRequest(event) {
1784
+ const taskId = String(event?.task_id || '').trim().toUpperCase();
1785
+ const phaseKind = String(event?.phase_kind || '').trim().toLowerCase();
1786
+ const taskKind = String(event?.task_kind || '').trim().toLowerCase();
1787
+ return taskId === 'TUX'
1788
+ || taskId.startsWith('HEAL-FIX-')
1789
+ || phaseKind === 'verify'
1790
+ || phaseKind === 'verification'
1791
+ || taskKind === 'verify'
1792
+ || taskKind === 'verification';
1793
+ }
1794
+ describeV3AuthoritativePreviewFailure(gate) {
1795
+ const production = (gate.modes?.production || {});
1796
+ const summary = (gate.summary || {});
1797
+ const blockers = [
1798
+ ...(Array.isArray(production.blockers) ? production.blockers : []),
1799
+ ...(Array.isArray(summary.blockers) ? summary.blockers : []),
1800
+ ].map((value) => String(value || '').trim()).filter(Boolean);
1801
+ const details = [...new Set([
1802
+ String(gate.error || '').trim(),
1803
+ ...blockers,
1804
+ ].filter(Boolean))];
1805
+ return `Authoritative Template Service production preview failed${details.length ? `: ${details.join('; ')}` : '.'}`;
1806
+ }
2133
1807
  async executeV3ClientToolRequest(event, context = {}) {
2134
1808
  const rootPath = this.resolveAgentTargetPath(context);
2135
1809
  if (!rootPath) {
@@ -2150,7 +1824,7 @@ menu {
2150
1824
  return { success: false, output: '', error: 'Tool path is outside the workspace.' };
2151
1825
  }
2152
1826
  const existingPathTools = new Set([
2153
- 'read_file', 'edit_file', 'list_directory', 'glob', 'search_files', 'grep',
1827
+ 'read_file', 'edit_file', 'delete_file', 'list_directory', 'glob', 'search_files', 'grep',
2154
1828
  'syntax_check', 'preview_check', 'runtime_check',
2155
1829
  ]);
2156
1830
  if (existingPathTools.has(name) && !fs.existsSync(target.absolutePath)) {
@@ -2179,10 +1853,10 @@ menu {
2179
1853
  }
2180
1854
  const missingAssets = [];
2181
1855
  const checkedAssets = [];
2182
- const assetPattern = /<(?:script|link|img|source|video|audio)\b[^>]+(?:src|href)=["']([^"']+)["']/gi;
1856
+ const assetPattern = /<(?:script|link|img|source|video|audio)\b[^>]+(?:src|href)\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'"`=<>]+))/gi;
2183
1857
  let match;
2184
1858
  while ((match = assetPattern.exec(html)) !== null && checkedAssets.length < 200) {
2185
- const rawAsset = String(match[1] || '').trim();
1859
+ const rawAsset = String(match[1] || match[2] || match[3] || '').trim();
2186
1860
  if (!rawAsset || /^(?:https?:|data:|blob:|mailto:|tel:|javascript:|#)/i.test(rawAsset))
2187
1861
  continue;
2188
1862
  let decoded;
@@ -2215,6 +1889,37 @@ menu {
2215
1889
  error: `Missing local preview assets: ${missingAssets.slice(0, 12).join(', ')}`,
2216
1890
  };
2217
1891
  }
1892
+ if (this.isV3ReleaseVerificationRequest(event)) {
1893
+ const releasePrompt = String(context.rawPrompt || context.contextualPrompt || context.prompt
1894
+ || 'Verify the complete responsive frontend application for production release.');
1895
+ const gate = await this.frontendPreviewService.runTemplateServicePreviewGate(releasePrompt, {
1896
+ ...context,
1897
+ targetPath: rootPath,
1898
+ workspacePath: rootPath,
1899
+ projectPath: rootPath,
1900
+ rawPrompt: releasePrompt,
1901
+ forceFrontendPreview: true,
1902
+ functionalRuntimeProof: false,
1903
+ requireScreenshot: false,
1904
+ });
1905
+ if (!gate.passed || gate.skipped === true) {
1906
+ return {
1907
+ success: false,
1908
+ output: '',
1909
+ error: this.describeV3AuthoritativePreviewFailure(gate),
1910
+ };
1911
+ }
1912
+ return {
1913
+ success: true,
1914
+ output: JSON.stringify({
1915
+ entry: gate.entryPath || path.relative(rootPath, entryPath).replace(/\\/g, '/'),
1916
+ checkedAssets: checkedAssets.length,
1917
+ expectedTextMatched: Boolean(expectedText),
1918
+ authoritativeProductionPreview: true,
1919
+ qualityTier: gate.modes?.production?.qualityTier || null,
1920
+ }),
1921
+ };
1922
+ }
2218
1923
  return {
2219
1924
  success: true,
2220
1925
  output: JSON.stringify({
@@ -2243,7 +1948,7 @@ menu {
2243
1948
  localScreenshotProof: true,
2244
1949
  functionalRuntimeProof: true,
2245
1950
  requireScreenshot: true,
2246
- runtimeInteractionProof: this.buildDirectFileRuntimeInteraction(runtimePrompt, args),
1951
+ runtimeInteractionProof: this.buildLoopbackRuntimeInteraction(runtimePrompt, args),
2247
1952
  });
2248
1953
  if (!gate.passed || gate.artifacts?.screenshotCaptured !== true) {
2249
1954
  return {
@@ -2257,14 +1962,35 @@ menu {
2257
1962
  output: JSON.stringify({
2258
1963
  entry: gate.entryPath || relativeTarget,
2259
1964
  screenshotCaptured: true,
2260
- proof: 'direct-file-system-browser-interaction',
1965
+ proof: 'loopback-http-browser-interaction',
2261
1966
  runtimeInteraction: gate.artifacts?.runtimeInteraction || null,
2262
1967
  }),
2263
1968
  };
2264
1969
  }
1970
+ if (name === 'delete_file') {
1971
+ const stat = fs.statSync(target.absolutePath);
1972
+ if (!stat.isFile()) {
1973
+ return {
1974
+ success: false,
1975
+ output: '',
1976
+ error: `delete_file only removes a single workspace file: ${target.relativePath}`,
1977
+ };
1978
+ }
1979
+ fs.unlinkSync(target.absolutePath);
1980
+ const result = {
1981
+ success: true,
1982
+ output: `Removed ${target.relativePath}`,
1983
+ };
1984
+ this.recordV3ClientToolMutation(event, context, result);
1985
+ return result;
1986
+ }
2265
1987
  if (name === 'write_file') {
2266
1988
  if (typeof args.content !== 'string')
2267
1989
  return { success: false, output: '', error: 'write_file requires string content.' };
1990
+ const syntaxError = this.validateV3ClientJavaScript(target.relativePath, args.content);
1991
+ if (syntaxError) {
1992
+ return { success: false, output: '', error: `JavaScript mutation rejected before write: ${syntaxError}` };
1993
+ }
2268
1994
  const mutation = this.buildV3ClientToolMutation(target.relativePath, args.content);
2269
1995
  if ('error' in mutation)
2270
1996
  return { success: false, output: '', error: mutation.error };
@@ -2301,6 +2027,10 @@ menu {
2301
2027
  const nextContent = replaceAll
2302
2028
  ? existing.split(oldString).join(newString)
2303
2029
  : existing.replace(oldString, newString);
2030
+ const syntaxError = this.validateV3ClientJavaScript(target.relativePath, nextContent);
2031
+ if (syntaxError) {
2032
+ return { success: false, output: '', error: `JavaScript mutation rejected before edit: ${syntaxError}` };
2033
+ }
2304
2034
  const mutation = this.buildV3ClientToolMutation(target.relativePath, nextContent);
2305
2035
  if ('error' in mutation)
2306
2036
  return { success: false, output: '', error: mutation.error };
@@ -2424,15 +2154,11 @@ menu {
2424
2154
  return { success: true, output: `Syntax check passed: ${target.relativePath}` };
2425
2155
  }
2426
2156
  if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
2427
- const { execFileSync } = await import('child_process');
2428
- try {
2429
- execFileSync(process.execPath, ['--check', target.absolutePath], { stdio: 'pipe', timeout: 10_000, windowsHide: true, env: safeChildProcessEnv() });
2430
- return { success: true, output: `Syntax check passed: ${target.relativePath}` };
2431
- }
2432
- catch (error) {
2433
- const stderr = error?.stderr?.toString?.() || error?.message || String(error);
2434
- return { success: false, output: '', error: stderr.trim() || `Syntax check failed: ${target.relativePath}` };
2435
- }
2157
+ const source = fs.readFileSync(target.absolutePath, 'utf8');
2158
+ const syntaxError = this.validateV3ClientJavaScript(target.relativePath, source);
2159
+ return syntaxError
2160
+ ? { success: false, output: '', error: syntaxError }
2161
+ : { success: true, output: `Syntax check passed: ${target.relativePath}` };
2436
2162
  }
2437
2163
  fs.readFileSync(target.absolutePath, 'utf8');
2438
2164
  return { success: true, output: `Syntax check passed: ${target.relativePath}` };
@@ -2576,7 +2302,7 @@ menu {
2576
2302
  ].map((value) => String(value || '').trim().toLowerCase());
2577
2303
  return taskKinds.includes('repair');
2578
2304
  }
2579
- buildDirectFileRuntimeInteraction(prompt, args = {}) {
2305
+ buildLoopbackRuntimeInteraction(prompt, args = {}) {
2580
2306
  const request = String(prompt || '');
2581
2307
  const keyMatch = request.match(/\b(?:press|hit|tap)\s+(?:the\s+)?(?:key\s+)?(space(?:bar)?|enter|return|esc(?:ape)?|arrow\s*(?:up|down|left|right)|[a-z0-9])\b/i);
2582
2308
  const rawKey = String(keyMatch?.[1] || '').replace(/\s+/g, '');
@@ -2589,15 +2315,31 @@ menu {
2589
2315
  ? (keyAliases[rawKey.toLowerCase()] || rawKey)
2590
2316
  : undefined;
2591
2317
  const expectText = String(args.expect_text || '').trim() || undefined;
2318
+ const supportedActions = new Set(['assert', 'click', 'fill', 'select', 'press', 'focus', 'wait', 'reload']);
2319
+ const steps = Array.isArray(args.steps)
2320
+ ? args.steps.slice(0, 64).filter((step) => (Boolean(step) && typeof step === 'object' && !Array.isArray(step))).map((step) => ({
2321
+ ...step,
2322
+ action: supportedActions.has(String(step.action || '').trim().toLowerCase())
2323
+ ? String(step.action || '').trim().toLowerCase()
2324
+ : 'assert',
2325
+ }))
2326
+ : [];
2327
+ const requestedTimeoutSeconds = Math.max(1, Math.min(Number(args.timeout) || 30, 120));
2592
2328
  return {
2593
- ...(key ? { key } : {}),
2329
+ ...(steps.length > 0 ? { steps } : (key ? { key } : {})),
2594
2330
  ...(expectText ? { expectText } : {}),
2595
- requireStateChange: Boolean(key),
2331
+ requireStateChange: steps.length === 0 && Boolean(key),
2596
2332
  waitMs: 1_750,
2333
+ timeoutMs: requestedTimeoutSeconds * 1_000,
2597
2334
  };
2598
2335
  }
2599
2336
  buildAgentFinalPreviewContext(message, context = {}) {
2600
- if (!this.isFocusedAgentRepair(context)) {
2337
+ const classificationContext = {
2338
+ ...context,
2339
+ forceFrontendPreview: false,
2340
+ };
2341
+ if (!this.isFocusedAgentRepair(context)
2342
+ || !this.frontendPreviewService.isFrontendTask(message, classificationContext)) {
2601
2343
  return context;
2602
2344
  }
2603
2345
  return {
@@ -2607,10 +2349,16 @@ menu {
2607
2349
  localScreenshotProof: true,
2608
2350
  functionalRuntimeProof: true,
2609
2351
  requireScreenshot: true,
2610
- runtimeInteractionProof: this.buildDirectFileRuntimeInteraction(String(context.rawPrompt || message || '')),
2352
+ runtimeInteractionProof: this.buildLoopbackRuntimeInteraction(String(context.rawPrompt || message || '')),
2611
2353
  };
2612
2354
  }
2613
2355
  async ensureAgentFrontendPolish(message = '', context = {}) {
2356
+ // V3 owns application source. Legacy post-processing is retained only as
2357
+ // an explicit compatibility hook; normal Agent runs must never mutate a
2358
+ // successful or read-only result behind the model's back.
2359
+ if (context.allowLegacyClientFrontendPolish !== true) {
2360
+ return;
2361
+ }
2614
2362
  const rootPath = this.resolveAgentTargetPath(context);
2615
2363
  if (!rootPath || !fs.existsSync(rootPath)) {
2616
2364
  return;
@@ -3252,7 +3000,12 @@ document.addEventListener('DOMContentLoaded', () => {
3252
3000
  const proofSucceeded = previewGate?.required === true
3253
3001
  ? previewGate.passed === true && previewGate.skipped !== true
3254
3002
  : true;
3255
- mutation = finalizeMutationTransaction(journal, proofSucceeded);
3003
+ try {
3004
+ mutation = finalizeMutationTransaction(journal, proofSucceeded);
3005
+ }
3006
+ finally {
3007
+ this.releaseMutationTransaction(journal);
3008
+ }
3256
3009
  if (!proofSucceeded)
3257
3010
  changedFiles = {};
3258
3011
  }
@@ -3488,7 +3241,11 @@ document.addEventListener('DOMContentLoaded', () => {
3488
3241
  }
3489
3242
  }
3490
3243
  }
3491
- if (typeof context.onStreamEvent === 'function' && !isV3StreamKeepaliveEvent(userEvent)) {
3244
+ const userVisibleEvent = userEvent?.user_visible !== false
3245
+ && String(userEvent?.visibility || '').toLowerCase() !== 'internal';
3246
+ if (typeof context.onStreamEvent === 'function'
3247
+ && !isV3StreamKeepaliveEvent(userEvent)
3248
+ && userVisibleEvent) {
3492
3249
  try {
3493
3250
  context.onStreamEvent(userEvent);
3494
3251
  }
@@ -3510,21 +3267,24 @@ document.addEventListener('DOMContentLoaded', () => {
3510
3267
  checkpointed_task_id: event.task_id,
3511
3268
  };
3512
3269
  }
3513
- if (this.hasAgentWorkspaceOutput(context)) {
3514
- return {
3515
- task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
3516
- context_id: contextId,
3517
- result: final || event,
3518
- events,
3519
- files: streamedFiles,
3520
- partial: true,
3521
- terminal_error: {
3522
- message: event.message || 'V3 agent returned an error',
3523
- code: event.code || event.error_code || null,
3524
- },
3525
- };
3526
- }
3527
- throw new Error(event.message || 'V3 agent returned an error');
3270
+ // A parsed terminal SSE error is an authoritative server outcome,
3271
+ // including when an empty-workspace build failed before its first
3272
+ // accepted mutation. Return typed workflow data so the caller can
3273
+ // report the real executor failure. Throwing here made the outer
3274
+ // accepted-stream safety guard misclassify a known clean failure as
3275
+ // AGENT_OUTCOME_UNKNOWN and misleadingly recommend a resume.
3276
+ return {
3277
+ task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
3278
+ context_id: contextId,
3279
+ result: final || event,
3280
+ events,
3281
+ files: streamedFiles,
3282
+ partial: true,
3283
+ terminal_error: {
3284
+ message: event.message || 'V3 agent returned an error',
3285
+ code: event.code || event.error_code || null,
3286
+ },
3287
+ };
3528
3288
  }
3529
3289
  if (event.type === 'complete' || event.type === 'message') {
3530
3290
  final = event;
@@ -3555,6 +3315,7 @@ document.addEventListener('DOMContentLoaded', () => {
3555
3315
  let mutationJournal = null;
3556
3316
  if (mutationRoot && fs.existsSync(mutationRoot) && fs.statSync(mutationRoot).isDirectory()) {
3557
3317
  mutationJournal = new MutationJournal(mutationRoot, executionContext.operationId);
3318
+ this.trackMutationTransaction(mutationJournal);
3558
3319
  executionContext.operationId = mutationJournal.operationId;
3559
3320
  executionContext.__mutationJournal = mutationJournal;
3560
3321
  }
@@ -3611,20 +3372,30 @@ document.addEventListener('DOMContentLoaded', () => {
3611
3372
  const requestBody = buildRequestBody(contextIdOverride);
3612
3373
  const controller = new AbortController();
3613
3374
  const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
3375
+ let runRequestDispatched = false;
3376
+ let runResponseReceived = false;
3377
+ let runResponseAccepted = false;
3614
3378
  try {
3379
+ // POST /run is a mutating operation. A transport failure after the
3380
+ // request leaves this process is ambiguous even when no streamed
3381
+ // file has reached the local mirror yet: the server can already be
3382
+ // planning or writing its managed workspace. Never turn that
3383
+ // ambiguity into a second whole-agent execution.
3384
+ runRequestDispatched = true;
3615
3385
  const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
3386
+ runResponseReceived = true;
3616
3387
  if (!response.ok) {
3617
3388
  const errorText = await response.text().catch(() => '');
3618
3389
  const sanitized = sanitizeUserFacingErrorText(errorText).slice(0, 200);
3619
- const isContextCollision = response.status === 409 && /already in progress/i.test(errorText);
3620
3390
  const isTransientWorkspaceHydration = /remote workspace is empty|workspace sync failure/i.test(errorText);
3621
- if ((isContextCollision || isTransientWorkspaceHydration) && contextRetry === 0) {
3391
+ if (isTransientWorkspaceHydration && contextRetry === 0) {
3622
3392
  contextIdOverride = `${requestExecutionContext.contextId || 'vig'}-retry-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3623
3393
  this.logger?.debug?.(`V3 transient agent error on ${baseUrl}; retrying with fresh context id.`);
3624
3394
  continue;
3625
3395
  }
3626
3396
  throw new Error(`V3 agent ${response.status}: ${sanitized}`);
3627
3397
  }
3398
+ runResponseAccepted = true;
3628
3399
  const data = await this.collectV3AgentStream(response, requestExecutionContext);
3629
3400
  // Auto-continuation: if the agent checkpointed (budget exceeded), continue automatically
3630
3401
  if (data.checkpointed && data.checkpointed_task_id) {
@@ -3699,7 +3470,11 @@ document.addEventListener('DOMContentLoaded', () => {
3699
3470
  // valid output. A terminal planner/executor error fails proof and
3700
3471
  // causes the operation journal to roll back streamed mutations.
3701
3472
  const analysisOnly = this.isAnalysisOnlyTask(message, executionContext);
3702
- const failedPreviewGate = analysisOnly
3473
+ const frontendTask = this.frontendPreviewService.isFrontendTask(message, {
3474
+ ...executionContext,
3475
+ forceFrontendPreview: false,
3476
+ });
3477
+ const failedPreviewGate = analysisOnly || !frontendTask
3703
3478
  ? { required: false, passed: true, skipped: false }
3704
3479
  : {
3705
3480
  required: true,
@@ -3736,8 +3511,8 @@ document.addEventListener('DOMContentLoaded', () => {
3736
3511
  }
3737
3512
  catch (error) {
3738
3513
  if (error?.code === 'AGENT_PLAN_INVALID') {
3739
- if (mutationJournal?.hasMutations()) {
3740
- const mutation = mutationJournal.rollback();
3514
+ if (mutationJournal) {
3515
+ const mutation = this.rollbackMutationTransaction(mutationJournal);
3741
3516
  mutationJournal = null;
3742
3517
  error.operationId = mutation.operationId;
3743
3518
  error.mutation = mutation;
@@ -3767,7 +3542,7 @@ document.addEventListener('DOMContentLoaded', () => {
3767
3542
  }, mutationJournal);
3768
3543
  }
3769
3544
  if (mutationJournal?.hasMutations()) {
3770
- const mutation = mutationJournal.rollback();
3545
+ const mutation = this.rollbackMutationTransaction(mutationJournal);
3771
3546
  mutationJournal = null;
3772
3547
  this.activeV3StreamedFiles = null;
3773
3548
  const ambiguousError = new Error(`V3 mutation outcome is ambiguous for operation ${mutation.operationId}; workspace rollback was attempted and no alternate backend retry was allowed.`);
@@ -3776,6 +3551,22 @@ document.addEventListener('DOMContentLoaded', () => {
3776
3551
  ambiguousError.partialMutation = mutation.partialMutation;
3777
3552
  throw ambiguousError;
3778
3553
  }
3554
+ if (runResponseAccepted || (runRequestDispatched && !runResponseReceived)) {
3555
+ const mutation = mutationJournal
3556
+ ? this.rollbackMutationTransaction(mutationJournal)
3557
+ : null;
3558
+ mutationJournal = null;
3559
+ this.activeV3StreamedFiles = null;
3560
+ const ambiguousError = new Error(`Agent connection was interrupted after operation ${executionContext.operationId || 'unknown'} started. `
3561
+ + 'The task was not submitted again because that could duplicate changes. Use /continue to resume the saved run.');
3562
+ ambiguousError.name = 'AgentOutcomeUnknownError';
3563
+ ambiguousError.code = 'AGENT_OUTCOME_UNKNOWN';
3564
+ ambiguousError.operationId = executionContext.operationId || null;
3565
+ ambiguousError.contextId = requestBody.context_id || requestExecutionContext.contextId || null;
3566
+ ambiguousError.mutation = mutation || undefined;
3567
+ ambiguousError.partialMutation = mutation?.partialMutation || false;
3568
+ throw ambiguousError;
3569
+ }
3779
3570
  errors.push(`${baseUrl}: ${error?.message || String(error)}`);
3780
3571
  }
3781
3572
  finally {
@@ -3816,27 +3607,9 @@ document.addEventListener('DOMContentLoaded', () => {
3816
3607
  : '';
3817
3608
  throw new Error(`V3 agent authentication failed at the V3 service layer while your gateway login token is still valid.${endpointSummary} Please retry shortly.`);
3818
3609
  }
3819
- if (preferLocalV3
3820
- && !this.hasAgentWorkspaceOutput(executionContext)
3821
- && /(saas|dashboard|analytics|billing|team management|activity feed)/i.test(message)) {
3822
- const appName = this.materializeEmergencySaaSWorkspace(message, executionContext);
3823
- if (appName) {
3824
- await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles: ['index.html', 'styles.css', 'scripts.js'] });
3825
- await this.ensureAgentFrontendPolish(message, executionContext);
3826
- const previewGate = await this.runTemplateServicePreviewGate(message, this.buildAgentFinalPreviewContext(message, executionContext));
3827
- return this.finalizeV3AgentWorkflowResponse({}, {
3828
- content: `Recovered a local SaaS workspace scaffold for ${appName} after repeated V3 materialization failures.`,
3829
- taskId: null,
3830
- contextId: executionContext.contextId || null,
3831
- backendUrl: 'local-emergency-scaffold',
3832
- partial: true,
3833
- metadata: { source: 'v3-agent-emergency-scaffold', mode: 'agent', previewGate, emergencyScaffold: true },
3834
- }, mutationJournal);
3835
- }
3836
- }
3837
3610
  const finalError = new Error(errors.join(' | '));
3838
3611
  if (mutationJournal) {
3839
- const mutation = mutationJournal.rollback();
3612
+ const mutation = this.rollbackMutationTransaction(mutationJournal);
3840
3613
  mutationJournal = null;
3841
3614
  finalError.operationId = mutation.operationId;
3842
3615
  finalError.mutation = mutation;
@@ -4181,13 +3954,13 @@ document.addEventListener('DOMContentLoaded', () => {
4181
3954
  const headers = await this.getV3AgentHeaders();
4182
3955
  const endpoint = this.isDirectV3AgentBaseUrl(baseUrl)
4183
3956
  ? `${baseUrl}/api/agent/client-tool-result`
4184
- : `${baseUrl}/api/v3-agent/client-tool-result`;
3957
+ : `${baseUrl}/api/v3-agent-cli/client-tool-result`;
4185
3958
  const body = JSON.stringify({
4186
3959
  context_id: trimmedContextId,
4187
3960
  call_id: trimmedCallId,
4188
3961
  success: result.success === true,
4189
- output: String(result.output || ''),
4190
- error: String(result.error || ''),
3962
+ output: String(result.output || '').slice(0, MAX_V3_CLIENT_RESULT_OUTPUT_CHARS),
3963
+ error: String(result.error || '').slice(0, MAX_V3_CLIENT_RESULT_ERROR_CHARS),
4191
3964
  mutation: result.mutation || null,
4192
3965
  });
4193
3966
  for (let attempt = 0; attempt < 3; attempt += 1) {
@@ -4311,12 +4084,11 @@ document.addEventListener('DOMContentLoaded', () => {
4311
4084
  }
4312
4085
  async getV3AgentHealth() {
4313
4086
  const baseUrl = this.getV3AgentBaseUrls()[0];
4314
- // Try multiple health endpoint patterns — the V3 backend may expose
4315
- // different paths depending on whether it's local (8030) or remote.
4087
+ // Direct requests use the isolated CLI runtime on 8032; authenticated
4088
+ // gateway requests use the named CLI lane.
4316
4089
  const candidates = [
4317
- `${baseUrl}/api/v3-agent/health`,
4090
+ `${baseUrl}${this.isDirectV3AgentBaseUrl(baseUrl) ? '/health' : '/api/v3-agent-cli/health'}`,
4318
4091
  `${baseUrl}/api/health`,
4319
- `${baseUrl}/health`,
4320
4092
  ];
4321
4093
  const headers = await this.getV3AgentHeaders();
4322
4094
  for (const endpoint of candidates) {