vigthoria-cli 1.13.25 → 1.13.29

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 (54) hide show
  1. package/dist/commands/chat.js +83 -27
  2. package/dist/commands/config.js +4 -4
  3. package/dist/commands/fork.d.ts +3 -2
  4. package/dist/commands/fork.js +124 -123
  5. package/dist/commands/game.d.ts +8 -0
  6. package/dist/commands/game.js +118 -10
  7. package/dist/commands/history.d.ts +0 -1
  8. package/dist/commands/history.js +8 -22
  9. package/dist/commands/hub.d.ts +20 -0
  10. package/dist/commands/hub.js +17 -3
  11. package/dist/commands/preview.d.ts +1 -0
  12. package/dist/commands/preview.js +36 -15
  13. package/dist/commands/product-run-registration.js +1 -1
  14. package/dist/commands/replay.d.ts +0 -1
  15. package/dist/commands/replay.js +10 -19
  16. package/dist/commands/repo.d.ts +35 -2
  17. package/dist/commands/repo.js +76 -34
  18. package/dist/commands/update-registration.js +4 -3
  19. package/dist/commands/workflow.d.ts +4 -0
  20. package/dist/commands/workflow.js +27 -0
  21. package/dist/index.js +6 -4
  22. package/dist/utils/agentRunOutcome.d.ts +7 -0
  23. package/dist/utils/agentRunOutcome.js +13 -0
  24. package/dist/utils/api.d.ts +20 -5
  25. package/dist/utils/api.js +440 -54
  26. package/dist/utils/command-policy.js +1 -1
  27. package/dist/utils/config.d.ts +2 -0
  28. package/dist/utils/config.js +8 -3
  29. package/dist/utils/frontend-preview-service.d.ts +1 -0
  30. package/dist/utils/frontend-preview-service.js +54 -5
  31. package/dist/utils/model-governance.js +23 -14
  32. package/dist/utils/model-transport-service.js +1 -1
  33. package/dist/utils/network-policy.js +16 -4
  34. package/dist/utils/operator-client.js +23 -4
  35. package/dist/utils/post-write-validator.js +7 -3
  36. package/dist/utils/preview-screenshot-adapter.d.ts +19 -3
  37. package/dist/utils/preview-screenshot-adapter.js +289 -28
  38. package/dist/utils/runtime-capability.d.ts +7 -0
  39. package/dist/utils/runtime-capability.js +11 -0
  40. package/dist/utils/runtime-temp.d.ts +6 -2
  41. package/dist/utils/runtime-temp.js +143 -27
  42. package/dist/utils/tools.js +1 -1
  43. package/dist/utils/v3-stream-events.d.ts +8 -0
  44. package/dist/utils/v3-stream-events.js +70 -0
  45. package/dist/utils/v3-workspace-service.d.ts +1 -0
  46. package/dist/utils/v3-workspace-service.js +38 -1
  47. package/dist/utils/vigflow-client.d.ts +9 -0
  48. package/dist/utils/vigflow-client.js +48 -2
  49. package/dist/utils/workspace-reference.d.ts +8 -0
  50. package/dist/utils/workspace-reference.js +21 -0
  51. package/package.json +4 -6
  52. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +2 -2
  53. package/scripts/release/validate-live-service-gates.sh +3 -3
  54. package/scripts/release/validate-no-go-gates.sh +2 -0
package/dist/utils/api.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Connects to coder.vigthoria.io API endpoints
4
4
  */
5
5
  import axios from 'axios';
6
- import { randomUUID } from 'crypto';
6
+ import { createHash, randomUUID } from 'crypto';
7
7
  import fs from 'fs';
8
8
  import https from 'https';
9
9
  import net from 'net';
@@ -15,29 +15,31 @@ import { buildSemanticContext } from './context-ranker.js';
15
15
  import { getChangedFiles } from './workspace-cache.js';
16
16
  import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOutcome.js';
17
17
  import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
18
- import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
18
+ import { assertValidAgentPlanEvent, isV3StreamKeepaliveEvent } from './v3-stream-events.js';
19
19
  import { resolveV3ContextCharLimit, WORKSPACE_FILE_CHAR_CAP, } from './contextBudget.js';
20
20
  import { isLocalTestfarmMode, fetchWithServiceTimeout } from './localTestMode.js';
21
21
  import { buildClientManifest } from './clientManifest.js';
22
22
  import { assertTrustedEndpoint, guardedFetch, installAxiosNetworkPolicy, installGlobalFetchPolicy } from './network-policy.js';
23
23
  import { assertSafeRelativePath, resolveWorkspacePath } from './workspace-boundary.js';
24
24
  import { parseSupportedProcess } from './process-policy.js';
25
- import { OptionalPuppeteerScreenshotAdapter } from './preview-screenshot-adapter.js';
25
+ import { SystemBrowserScreenshotAdapter } from './preview-screenshot-adapter.js';
26
26
  import { normalizeSubscriptionResponse } from './subscription-policy.js';
27
27
  import { ModelGovernance } from './model-governance.js';
28
28
  import { OperatorClient, OperatorClientError } from './operator-client.js';
29
29
  import { CodeOperationsService } from './code-operations-service.js';
30
30
  import { McpContextClient } from './mcp-context-client.js';
31
- import { FrontendPreviewService } from './frontend-preview-service.js';
31
+ import { FrontendPreviewService, isConstrainedStaticFrontendRequest, } from './frontend-preview-service.js';
32
32
  import { V3AgentClient } from './v3-agent-client.js';
33
33
  import { V3WorkspaceService } from './v3-workspace-service.js';
34
34
  import { CapabilityHealthService, } from './capability-health-service.js';
35
35
  import { ModelTransportService } from './model-transport-service.js';
36
36
  import { VigFlowClient, } from './vigflow-client.js';
37
37
  import { MutationJournal, finalizeMutationTransaction, } from './mutation-journal.js';
38
- import { isSensitivePath, redactSensitiveText, safeChildProcessEnv, scanOutboundContext, } from './secret-policy.js';
38
+ import { containsHighConfidenceSecret, isSensitivePath, redactSensitiveText, safeChildProcessEnv, scanOutboundContext, } from './secret-policy.js';
39
+ import { buildLocalWorkspaceReference } from './workspace-reference.js';
39
40
  export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
40
41
  export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
42
+ const MAX_V3_CLIENT_MUTATION_BYTES = 512 * 1024;
41
43
  export class CLIError extends Error {
42
44
  category;
43
45
  statusCode;
@@ -424,7 +426,7 @@ export class APIClient {
424
426
  extractExpectedFiles: (message, context) => this.extractExpectedWorkspaceFiles(message, context),
425
427
  isAnalysisOnlyTask: (message, context) => this.isAnalysisOnlyTask(message, context),
426
428
  sanitizeError: sanitizeUserFacingErrorText,
427
- screenshotAdapter: dependencies.screenshotAdapter || new OptionalPuppeteerScreenshotAdapter(),
429
+ screenshotAdapter: dependencies.screenshotAdapter || new SystemBrowserScreenshotAdapter(),
428
430
  });
429
431
  const vigFlowTransport = axios.create({ timeout: 30_000 });
430
432
  installAxiosNetworkPolicy(vigFlowTransport, 'vigflow');
@@ -710,13 +712,10 @@ export class APIClient {
710
712
  return this.v3AgentClient.approvalUrl(baseUrl);
711
713
  }
712
714
  getOperatorBaseUrls() {
713
- const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
714
715
  const urls = [
715
716
  process.env.VIGTHORIA_OPERATOR_URL,
716
717
  process.env.OPERATOR_URL,
717
- 'http://127.0.0.1:4009',
718
- configuredModelsApiUrl,
719
- 'https://api.vigthoria.io',
718
+ 'https://agent.vigthoria.io',
720
719
  ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
721
720
  return [...new Set(urls)];
722
721
  }
@@ -805,6 +804,9 @@ export class APIClient {
805
804
  getVigFlowExecutionStatus(executionId) {
806
805
  return this.vigFlowClient.executionStatus(executionId);
807
806
  }
807
+ deleteVigFlowWorkflow(selector) {
808
+ return this.vigFlowClient.deleteWorkflow(selector);
809
+ }
808
810
  async getV3AgentHeaders() {
809
811
  return this.v3AgentClient.headers();
810
812
  }
@@ -876,20 +878,12 @@ export class APIClient {
876
878
  const resolvedContext = this.ensureExecutionContext(context);
877
879
  const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
878
880
  const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
879
- const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
880
- const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
881
- if (resolvedContext.workspaceFilesOutOfBand === true && localWorkspaceSummary?.workspaceFiles) {
882
- localWorkspaceSummary.workspaceHydration = {
883
- delivery: 'out-of-band',
884
- fileCount: Object.keys(localWorkspaceSummary.workspaceFiles).length,
885
- complete: true,
886
- };
887
- delete localWorkspaceSummary.workspaceFiles;
888
- }
889
881
  const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
890
882
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
891
883
  const localWorkspaceName = this.getDisplayWorkspaceName(localWorkspacePath);
892
- const localWorkspaceRef = localWorkspaceName ? `vigthoria://local-workspace/${localWorkspaceName}` : null;
884
+ const localWorkspaceRef = localWorkspacePath
885
+ ? buildLocalWorkspaceReference(localWorkspacePath, String(this.config.get('userId') || this.config.get('email') || ''))
886
+ : null;
893
887
  const executionSurface = String(resolvedContext.executionSurface || resolvedContext.clientSurface || 'cli');
894
888
  const localMachineCapable = resolvedContext.localMachineCapable !== false;
895
889
  const clientToolExecution = resolvedContext.clientToolExecution === false
@@ -898,15 +892,34 @@ export class APIClient {
898
892
  || (!serverWorkspacePath
899
893
  && localMachineCapable
900
894
  && ['cli', 'fork', 'local-ide', 'desktop', 'local'].includes(executionSurface)));
895
+ const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
896
+ const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus, !clientToolExecution);
897
+ if (clientToolExecution && localWorkspaceSummary) {
898
+ localWorkspaceSummary.workspaceHydration = {
899
+ delivery: 'client-tool-bridge',
900
+ fileCount: Number(localWorkspaceSummary.fileCount || 0),
901
+ complete: false,
902
+ authoritativeLocation: 'client',
903
+ };
904
+ delete localWorkspaceSummary.workspaceFiles;
905
+ }
906
+ else if (resolvedContext.workspaceFilesOutOfBand === true && localWorkspaceSummary?.workspaceFiles) {
907
+ localWorkspaceSummary.workspaceHydration = {
908
+ delivery: 'out-of-band',
909
+ fileCount: Object.keys(localWorkspaceSummary.workspaceFiles).length,
910
+ complete: true,
911
+ authoritativeLocation: 'server',
912
+ };
913
+ delete localWorkspaceSummary.workspaceFiles;
914
+ }
901
915
  const publicRuntimeEnvironment = this.buildPublicRuntimeEnvironment(resolvedContext.agentRuntime, {
902
916
  localWorkspacePath,
903
917
  serverWorkspacePath,
904
918
  });
905
- // When the server cannot directly access the workspace (e.g. Windows
906
- // client), use the local path as a hint and flag that the workspace
907
- // files are provided inline in localWorkspaceSummary.workspaceFiles.
919
+ // A local CLI/Workbench workspace remains authoritative on that machine.
920
+ // Only server-owned/background runs may hydrate a bounded server copy.
908
921
  const effectiveWorkspacePath = serverWorkspacePath || null;
909
- const needsHydration = !serverWorkspacePath && !!localWorkspacePath;
922
+ const needsHydration = !clientToolExecution && !serverWorkspacePath && !!localWorkspacePath;
910
923
  const agentTaskType = resolvedContext.agentTaskType || 'general';
911
924
  const rawPrompt = resolvedContext.rawPrompt || resolvedContext.prompt || '';
912
925
  const executionHints = (resolvedContext.executionHints && typeof resolvedContext.executionHints === 'object')
@@ -960,15 +973,13 @@ export class APIClient {
960
973
  projectPath: effectiveWorkspacePath,
961
974
  targetPath: effectiveWorkspacePath,
962
975
  // Never send a client-local absolute path to the V3 model boundary.
963
- // The server only needs a stable label; file contents are carried by
964
- // localWorkspaceSummary.workspaceFiles and hydrated into a temp workspace.
976
+ // The server only receives a stable label. Local bridge runs retrieve
977
+ // file contents on demand through authenticated client tool requests.
965
978
  localWorkspacePath: serverWorkspacePath ? serverWorkspacePath : localWorkspaceRef,
966
979
  localWorkspaceRef,
967
980
  localWorkspaceName,
968
981
  localWorkspaceSummary,
969
- // Signal to the server that the workspace filesystem is not locally
970
- // accessible — it must hydrate a temp directory from the provided
971
- // workspaceFiles before the agent starts using tools.
982
+ // This is true only for explicit server-owned/background execution.
972
983
  workspaceHydrationRequired: needsHydration,
973
984
  contextId: resolvedContext.contextId,
974
985
  traceId: resolvedContext.traceId,
@@ -1895,7 +1906,7 @@ menu {
1895
1906
  cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
1896
1907
  };
1897
1908
  }
1898
- buildLocalWorkspaceSummary(rootPath, requestFocus = '') {
1909
+ buildLocalWorkspaceSummary(rootPath, requestFocus = '', includeWorkspaceFiles = true) {
1899
1910
  if (!rootPath || !fs.existsSync(rootPath)) {
1900
1911
  return null;
1901
1912
  }
@@ -1981,9 +1992,12 @@ menu {
1981
1992
  if (fs.existsSync(readmePath)) {
1982
1993
  summary.readmeExcerpt = fs.readFileSync(readmePath, 'utf8').slice(0, 2500);
1983
1994
  }
1984
- // Hydrate workspace: include actual file contents so the V3 server
1985
- // can populate the remote workspace before the agent starts.
1986
- summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, orderedPaths);
1995
+ 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.
1999
+ summary.workspaceFiles = this.collectWorkspaceFileContents(rootPath, orderedPaths);
2000
+ }
1987
2001
  return summary;
1988
2002
  }
1989
2003
  catch (error) {
@@ -1999,7 +2013,15 @@ menu {
1999
2013
  const resolvedContext = this.ensureExecutionContext(context);
2000
2014
  const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
2001
2015
  const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
2002
- if (serverWorkspacePath || !localWorkspacePath)
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)
2003
2025
  return undefined;
2004
2026
  const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
2005
2027
  const summary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
@@ -2034,6 +2056,29 @@ menu {
2034
2056
  resolveV3ClientToolPath(rootPath, rawPath, sourceRoots = []) {
2035
2057
  return this.v3WorkspaceService.resolveClientToolPath(rootPath, rawPath, sourceRoots);
2036
2058
  }
2059
+ describeMissingV3ClientToolPath(rootPath, relativePath) {
2060
+ const normalized = String(relativePath || '').replace(/\\/g, '/');
2061
+ const requestedDirectory = path.posix.dirname(normalized);
2062
+ const requestedExtension = path.posix.extname(normalized).toLowerCase();
2063
+ let candidates = [];
2064
+ try {
2065
+ const paths = this.getAgentWorkspaceSnapshot(rootPath).paths;
2066
+ const sameDirectory = paths.filter((entry) => path.posix.dirname(entry) === requestedDirectory);
2067
+ const sameExtension = paths.filter((entry) => (requestedExtension && path.posix.extname(entry).toLowerCase() === requestedExtension));
2068
+ candidates = Array.from(new Set([
2069
+ ...sameDirectory,
2070
+ ...sameExtension,
2071
+ ...paths,
2072
+ ])).slice(0, 16);
2073
+ }
2074
+ catch {
2075
+ // The primary not-found error remains actionable even if inventory fails.
2076
+ }
2077
+ const inventory = candidates.length > 0
2078
+ ? ` Existing workspace files include: ${candidates.join(', ')}.`
2079
+ : '';
2080
+ return `File not found in the local workspace: ${normalized}.${inventory} Use list_directory or glob and retry with an existing workspace-relative path.`;
2081
+ }
2037
2082
  recordV3ClientToolMutation(event, context, result) {
2038
2083
  if (!result.success) {
2039
2084
  return;
@@ -2068,6 +2113,23 @@ menu {
2068
2113
  }
2069
2114
  }
2070
2115
  }
2116
+ buildV3ClientToolMutation(relativePath, content) {
2117
+ if (isSensitivePath(relativePath)) {
2118
+ return { error: 'Sensitive paths cannot be synchronized to the Agent service.' };
2119
+ }
2120
+ const byteLength = Buffer.byteLength(content, 'utf8');
2121
+ if (byteLength > MAX_V3_CLIENT_MUTATION_BYTES) {
2122
+ return { error: `Client mutation exceeds the ${MAX_V3_CLIENT_MUTATION_BYTES}-byte synchronization limit.` };
2123
+ }
2124
+ if (containsHighConfidenceSecret(content)) {
2125
+ return { error: 'Client mutation contains credential-like material and was not synchronized.' };
2126
+ }
2127
+ return {
2128
+ path: relativePath,
2129
+ content,
2130
+ sha256: createHash('sha256').update(content, 'utf8').digest('hex'),
2131
+ };
2132
+ }
2071
2133
  async executeV3ClientToolRequest(event, context = {}) {
2072
2134
  const rootPath = this.resolveAgentTargetPath(context);
2073
2135
  if (!rootPath) {
@@ -2087,13 +2149,121 @@ menu {
2087
2149
  if (!target) {
2088
2150
  return { success: false, output: '', error: 'Tool path is outside the workspace.' };
2089
2151
  }
2152
+ const existingPathTools = new Set([
2153
+ 'read_file', 'edit_file', 'list_directory', 'glob', 'search_files', 'grep',
2154
+ 'syntax_check', 'preview_check', 'runtime_check',
2155
+ ]);
2156
+ if (existingPathTools.has(name) && !fs.existsSync(target.absolutePath)) {
2157
+ return {
2158
+ success: false,
2159
+ output: '',
2160
+ error: this.describeMissingV3ClientToolPath(rootPath, target.relativePath),
2161
+ };
2162
+ }
2090
2163
  try {
2164
+ if (name === 'preview_check') {
2165
+ const entryPath = fs.statSync(target.absolutePath).isDirectory()
2166
+ ? path.join(target.absolutePath, 'index.html')
2167
+ : target.absolutePath;
2168
+ if (!fs.existsSync(entryPath) || !fs.statSync(entryPath).isFile()) {
2169
+ return { success: false, output: '', error: 'Preview entry file not found.' };
2170
+ }
2171
+ const htmlBytes = fs.statSync(entryPath).size;
2172
+ if (htmlBytes > 2 * 1024 * 1024) {
2173
+ return { success: false, output: '', error: 'Preview entry exceeds the 2 MiB local validation limit.' };
2174
+ }
2175
+ const html = fs.readFileSync(entryPath, 'utf8');
2176
+ const expectedText = String(args.expect_text || '').trim();
2177
+ if (expectedText && !html.includes(expectedText)) {
2178
+ return { success: false, output: '', error: `Expected text was not found in ${path.basename(entryPath)}.` };
2179
+ }
2180
+ const missingAssets = [];
2181
+ const checkedAssets = [];
2182
+ const assetPattern = /<(?:script|link|img|source|video|audio)\b[^>]+(?:src|href)=["']([^"']+)["']/gi;
2183
+ let match;
2184
+ while ((match = assetPattern.exec(html)) !== null && checkedAssets.length < 200) {
2185
+ const rawAsset = String(match[1] || '').trim();
2186
+ if (!rawAsset || /^(?:https?:|data:|blob:|mailto:|tel:|javascript:|#)/i.test(rawAsset))
2187
+ continue;
2188
+ let decoded;
2189
+ try {
2190
+ decoded = decodeURIComponent(rawAsset.split(/[?#]/, 1)[0]);
2191
+ }
2192
+ catch {
2193
+ return { success: false, output: '', error: 'Preview contains a malformed local asset URL.' };
2194
+ }
2195
+ const entryRelative = path.relative(rootPath, entryPath).replace(/\\/g, '/');
2196
+ const entryDirectory = path.posix.dirname(entryRelative);
2197
+ const assetRelative = decoded.startsWith('/')
2198
+ ? decoded.replace(/^\/+/, '')
2199
+ : path.posix.normalize(path.posix.join(entryDirectory, decoded));
2200
+ let assetAbsolute;
2201
+ try {
2202
+ assetAbsolute = resolveWorkspacePath(rootPath, assetRelative);
2203
+ }
2204
+ catch {
2205
+ return { success: false, output: '', error: 'Preview asset path escapes the local workspace.' };
2206
+ }
2207
+ checkedAssets.push(assetRelative);
2208
+ if (!fs.existsSync(assetAbsolute) || !fs.statSync(assetAbsolute).isFile())
2209
+ missingAssets.push(assetRelative);
2210
+ }
2211
+ if (missingAssets.length > 0) {
2212
+ return {
2213
+ success: false,
2214
+ output: '',
2215
+ error: `Missing local preview assets: ${missingAssets.slice(0, 12).join(', ')}`,
2216
+ };
2217
+ }
2218
+ return {
2219
+ success: true,
2220
+ output: JSON.stringify({
2221
+ entry: path.relative(rootPath, entryPath).replace(/\\/g, '/'),
2222
+ checkedAssets: checkedAssets.length,
2223
+ expectedTextMatched: Boolean(expectedText),
2224
+ }),
2225
+ };
2226
+ }
2227
+ if (name === 'runtime_check') {
2228
+ const previewResult = await this.executeV3ClientToolRequest({ ...event, name: 'preview_check', arguments: args }, context);
2229
+ if (!previewResult.success)
2230
+ return previewResult;
2231
+ const relativeTarget = target.relativePath && target.relativePath !== '.' ? target.relativePath : 'index.html';
2232
+ const gate = await this.frontendPreviewService.runTemplateServicePreviewGate(`Verify the interactive frontend runtime at ${relativeTarget}.`, {
2233
+ ...context,
2234
+ targetPath: rootPath,
2235
+ workspacePath: rootPath,
2236
+ projectPath: rootPath,
2237
+ rawPrompt: `Verify the interactive frontend runtime at ${relativeTarget}.`,
2238
+ forceFrontendPreview: true,
2239
+ localScreenshotProof: true,
2240
+ requireScreenshot: true,
2241
+ });
2242
+ if (!gate.passed || gate.artifacts?.screenshotCaptured !== true) {
2243
+ return {
2244
+ success: false,
2245
+ output: '',
2246
+ error: gate.error || gate.artifacts?.screenshotError || 'Local browser runtime proof failed closed.',
2247
+ };
2248
+ }
2249
+ return {
2250
+ success: true,
2251
+ output: JSON.stringify({
2252
+ entry: gate.entryPath || relativeTarget,
2253
+ screenshotCaptured: true,
2254
+ proof: 'local-system-browser',
2255
+ }),
2256
+ };
2257
+ }
2091
2258
  if (name === 'write_file') {
2092
2259
  if (typeof args.content !== 'string')
2093
2260
  return { success: false, output: '', error: 'write_file requires string content.' };
2261
+ const mutation = this.buildV3ClientToolMutation(target.relativePath, args.content);
2262
+ if ('error' in mutation)
2263
+ return { success: false, output: '', error: mutation.error };
2094
2264
  fs.mkdirSync(path.dirname(target.absolutePath), { recursive: true });
2095
2265
  fs.writeFileSync(target.absolutePath, args.content, 'utf8');
2096
- const result = { success: true, output: `Updated ${target.relativePath}` };
2266
+ const result = { success: true, output: `Updated ${target.relativePath}`, mutation };
2097
2267
  this.recordV3ClientToolMutation(event, context, result);
2098
2268
  return result;
2099
2269
  }
@@ -2104,10 +2274,31 @@ menu {
2104
2274
  return { success: false, output: '', error: 'edit_file requires old_string.' };
2105
2275
  const existing = fs.readFileSync(target.absolutePath, 'utf8');
2106
2276
  const count = existing.split(oldString).length - 1;
2107
- if (count !== 1)
2108
- return { success: false, output: '', error: `old_string found ${count} times; expected exactly once.` };
2109
- fs.writeFileSync(target.absolutePath, existing.replace(oldString, newString), 'utf8');
2110
- const result = { success: true, output: `Edited ${target.relativePath}` };
2277
+ const replaceAll = args.replace_all === true;
2278
+ if ((!replaceAll && count !== 1) || (replaceAll && count < 1)) {
2279
+ return {
2280
+ success: false,
2281
+ output: '',
2282
+ error: replaceAll
2283
+ ? `old_string found ${count} times; expected at least once.`
2284
+ : `old_string found ${count} times; expected exactly once.`,
2285
+ };
2286
+ }
2287
+ if (replaceAll && (oldString.includes('\n') || newString.includes('\n') || oldString.length > 120 || count > 1000)) {
2288
+ return {
2289
+ success: false,
2290
+ output: '',
2291
+ error: 'replace_all is restricted to short, single-line token migrations with at most 1000 matches.',
2292
+ };
2293
+ }
2294
+ const nextContent = replaceAll
2295
+ ? existing.split(oldString).join(newString)
2296
+ : existing.replace(oldString, newString);
2297
+ const mutation = this.buildV3ClientToolMutation(target.relativePath, nextContent);
2298
+ if ('error' in mutation)
2299
+ return { success: false, output: '', error: mutation.error };
2300
+ fs.writeFileSync(target.absolutePath, nextContent, 'utf8');
2301
+ const result = { success: true, output: `Edited ${target.relativePath}`, mutation };
2111
2302
  this.recordV3ClientToolMutation(event, context, result);
2112
2303
  return result;
2113
2304
  }
@@ -2357,6 +2548,19 @@ menu {
2357
2548
  normalizeAgentWorkspaceRelativePath(rawPath, rootPath) {
2358
2549
  return this.v3WorkspaceService.normalizeRelativePath(rawPath, rootPath);
2359
2550
  }
2551
+ isExplicitSingleFileFrontendRequest(prompt, expectedFiles, rootPath) {
2552
+ const normalizedFiles = Array.from(new Set(expectedFiles
2553
+ .map((filePath) => this.normalizeAgentWorkspaceRelativePath(filePath, rootPath))
2554
+ .filter((filePath) => /\.html?$/i.test(filePath))));
2555
+ if (normalizedFiles.length !== 1) {
2556
+ return false;
2557
+ }
2558
+ const request = String(prompt || '').toLowerCase();
2559
+ const explicitOneFileLanguage = /\b(?:single|one)[ -]?file\b/i.test(request)
2560
+ || /\b(?:in\s+)?(?:exactly|only)\s+[`'\"]?[a-z0-9_./-]+\.html?\b/i.test(request)
2561
+ || /\b[a-z0-9_./-]+\.html?\b[^.\n]{0,100}\b(?:only|alone)\b/i.test(request);
2562
+ return explicitOneFileLanguage;
2563
+ }
2360
2564
  async ensureAgentFrontendPolish(message = '', context = {}) {
2361
2565
  const rootPath = this.resolveAgentTargetPath(context);
2362
2566
  if (!rootPath || !fs.existsSync(rootPath)) {
@@ -2364,16 +2568,66 @@ menu {
2364
2568
  }
2365
2569
  const prompt = String(message || '');
2366
2570
  const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
2367
- const looksLikeFrontendTask = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase)/i.test(prompt)
2571
+ const looksLikeFrontendTask = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase|responsive|interactive|game|canvas)/i.test(prompt)
2368
2572
  || expectedFiles.some((filePath) => /\.(html|css|js)$/i.test(filePath));
2369
2573
  if (!looksLikeFrontendTask) {
2370
2574
  return;
2371
2575
  }
2372
- const htmlPath = path.join(rootPath, 'index.html');
2373
- if (!fs.existsSync(htmlPath)) {
2576
+ const expectedHtmlPaths = expectedFiles
2577
+ .map((filePath) => this.normalizeAgentWorkspaceRelativePath(filePath, rootPath))
2578
+ .filter((filePath) => /\.html?$/i.test(filePath));
2579
+ const selectedHtmlRelativePath = [
2580
+ ...expectedHtmlPaths,
2581
+ 'index.html',
2582
+ ].find((filePath) => {
2583
+ try {
2584
+ return Boolean(filePath) && fs.existsSync(resolveWorkspacePath(rootPath, filePath));
2585
+ }
2586
+ catch {
2587
+ return false;
2588
+ }
2589
+ });
2590
+ if (!selectedHtmlRelativePath) {
2591
+ return;
2592
+ }
2593
+ const htmlPath = resolveWorkspacePath(rootPath, selectedHtmlRelativePath);
2594
+ let html = fs.readFileSync(htmlPath, 'utf8');
2595
+ // The Template Service correctly fails motion-bearing frontend artifacts
2596
+ // without a reduced-motion path. V3 may produce a valid single-file app
2597
+ // whose only motion is inline CSS, so enforce this small accessibility
2598
+ // contract on the actual requested HTML entry before preview validation.
2599
+ // This remains inside the surrounding mutation journal and is rolled back
2600
+ // with every other client-side mutation when preview later fails.
2601
+ const usesMotion = /@keyframes|\banimation(?:-name)?\s*:|\btransition(?:-property)?\s*:/i.test(html);
2602
+ if (usesMotion && !/prefers-reduced-motion/i.test(html)) {
2603
+ const reducedMotionCss = `
2604
+
2605
+ @media (prefers-reduced-motion: reduce) {
2606
+ html { scroll-behavior: auto !important; }
2607
+ *, *::before, *::after {
2608
+ animation-duration: 0.01ms !important;
2609
+ animation-iteration-count: 1 !important;
2610
+ transition-duration: 0.01ms !important;
2611
+ }
2612
+ }`;
2613
+ if (/<\/style>/i.test(html)) {
2614
+ html = html.replace(/<\/style>/i, `${reducedMotionCss}\n</style>`);
2615
+ }
2616
+ else if (/<\/head>/i.test(html)) {
2617
+ html = html.replace(/<\/head>/i, `<style>${reducedMotionCss}\n</style>\n</head>`);
2618
+ }
2619
+ fs.writeFileSync(htmlPath, html, 'utf8');
2620
+ }
2621
+ if (isConstrainedStaticFrontendRequest(prompt)
2622
+ || this.isExplicitSingleFileFrontendRequest(prompt, expectedFiles, rootPath)) {
2623
+ return;
2624
+ }
2625
+ // The broader legacy polish recovery owns only the conventional root
2626
+ // index entry. Arbitrary explicitly named applications retain their
2627
+ // architecture; only the accessibility contract above is normalized.
2628
+ if (selectedHtmlRelativePath !== 'index.html') {
2374
2629
  return;
2375
2630
  }
2376
- const html = fs.readFileSync(htmlPath, 'utf8');
2377
2631
  const ensuredAssets = this.ensureReferencedFrontendAssets(rootPath, html, prompt, 'Vigthoria CLI');
2378
2632
  const cssPath = ensuredAssets.cssPath;
2379
2633
  const jsPath = ensuredAssets.jsPath;
@@ -2383,6 +2637,24 @@ menu {
2383
2637
  }
2384
2638
  let css = fs.readFileSync(cssPath, 'utf8');
2385
2639
  let js = fs.existsSync(jsPath) ? fs.readFileSync(jsPath, 'utf8') : '';
2640
+ // Production preview evaluates the linked stylesheet as part of the final
2641
+ // artifact. The earlier inline-only guard cannot see animation and
2642
+ // transition rules that live in that stylesheet, which allowed the Agent's
2643
+ // client-side runtime check to pass and the final Template Service gate to
2644
+ // reject the same workspace. Normalize the two deterministic production
2645
+ // contracts before any of the legacy early-return paths below.
2646
+ const linkedFrontend = `${html}\n${css}`;
2647
+ if (!/\*\s*,\s*\*::before\s*,\s*\*::after\s*\{[^}]*box-sizing\s*:\s*border-box/i.test(linkedFrontend)) {
2648
+ css = `*, *::before, *::after {\n box-sizing: border-box;\n}\n\n${css}`;
2649
+ }
2650
+ const linkedFrontendWithReset = `${html}\n${css}`;
2651
+ const linkedStylesUseMotion = /@keyframes|\banimation(?:-name)?\s*:|\btransition(?:-property)?\s*:/i.test(linkedFrontendWithReset);
2652
+ if (linkedStylesUseMotion && !/prefers-reduced-motion/i.test(linkedFrontendWithReset)) {
2653
+ css = `${css.trimEnd()}\n\n/* Vigthoria CLI Reduced Motion Baseline */\n@media (prefers-reduced-motion: reduce) {\n html { scroll-behavior: auto !important; }\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}\n`;
2654
+ }
2655
+ if (css !== fs.readFileSync(cssPath, 'utf8')) {
2656
+ fs.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
2657
+ }
2386
2658
  const keyframesBlocks = Array.from(js.matchAll(/@keyframes[\s\S]*?\n\}/g)).map((match) => match[0]);
2387
2659
  if (keyframesBlocks.length > 0) {
2388
2660
  const migrated = keyframesBlocks.filter((block) => !css.includes(block));
@@ -2460,7 +2732,15 @@ menu {
2460
2732
  const cssMarker = '/* Vigthoria CLI Motion Enhancement */';
2461
2733
  const jsMarker = '/* Vigthoria CLI Motion Enhancement */';
2462
2734
  if (!css.includes(cssMarker)) {
2463
- fs.appendFileSync(cssPath, `\n\n${cssMarker}\n.hero, section {\n opacity: 0;\n transform: translateY(24px);\n animation: vigCliFadeIn 0.8s ease forwards;\n}\n\nsection {\n animation-delay: 0.12s;\n}\n\nbutton, .cta, a {\n transition: transform 0.25s ease, opacity 0.25s ease;\n}\n\nbutton:hover, .cta:hover, a:hover {\n transform: translateY(-2px);\n}\n\n.motion-reveal {\n opacity: 0;\n transform: translateY(24px);\n transition: opacity 0.7s ease, transform 0.7s ease;\n}\n\n.motion-reveal.is-visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n@keyframes vigCliFadeIn {\n from {\n opacity: 0;\n transform: translateY(24px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n`, 'utf8');
2735
+ // This enhancement introduces motion after the earlier linked-asset
2736
+ // normalization pass. Add the accessibility counterpart in the same
2737
+ // atomic append so the final Template Service gate never observes
2738
+ // CLI-created animation without a reduced-motion contract.
2739
+ const currentCss = fs.readFileSync(cssPath, 'utf8');
2740
+ const reducedMotionBaseline = /prefers-reduced-motion/i.test(currentCss)
2741
+ ? ''
2742
+ : `\n\n/* Vigthoria CLI Reduced Motion Baseline */\n@media (prefers-reduced-motion: reduce) {\n html { scroll-behavior: auto !important; }\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}`;
2743
+ fs.appendFileSync(cssPath, `\n\n${cssMarker}\n.hero, section {\n opacity: 0;\n transform: translateY(24px);\n animation: vigCliFadeIn 0.8s ease forwards;\n}\n\nsection {\n animation-delay: 0.12s;\n}\n\nbutton, .cta, a {\n transition: transform 0.25s ease, opacity 0.25s ease;\n}\n\nbutton:hover, .cta:hover, a:hover {\n transform: translateY(-2px);\n}\n\n.motion-reveal {\n opacity: 0;\n transform: translateY(24px);\n transition: opacity 0.7s ease, transform 0.7s ease;\n}\n\n.motion-reveal.is-visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n@keyframes vigCliFadeIn {\n from {\n opacity: 0;\n transform: translateY(24px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}${reducedMotionBaseline}\n`, 'utf8');
2464
2744
  }
2465
2745
  if (!js.includes(jsMarker)) {
2466
2746
  fs.appendFileSync(jsPath, `\n\n${jsMarker}\ndocument.addEventListener('DOMContentLoaded', () => {\n const revealTargets = document.querySelectorAll('section, .hero, .project-grid > *, .journal-preview > *');\n if (typeof IntersectionObserver !== 'function') {\n revealTargets.forEach((element) => element.classList.add('is-visible'));\n return;\n }\n\n const observer = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n entry.target.classList.add('is-visible');\n observer.unobserve(entry.target);\n }\n });\n }, { threshold: 0.16 });\n\n revealTargets.forEach((element, index) => {\n element.classList.add('motion-reveal');\n element.style.transitionDelay = String(Math.min(index * 60, 320)) + 'ms';\n observer.observe(element);\n });\n});\n`, 'utf8');
@@ -3109,6 +3389,7 @@ document.addEventListener('DOMContentLoaded', () => {
3109
3389
  if (explicitEventType && (!event.type || event.type === 'message')) {
3110
3390
  event.type = explicitEventType;
3111
3391
  }
3392
+ assertValidAgentPlanEvent(event);
3112
3393
  const userEvent = this.sanitizeV3AgentEventForUser(event);
3113
3394
  events.push(userEvent);
3114
3395
  if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
@@ -3360,12 +3641,15 @@ document.addEventListener('DOMContentLoaded', () => {
3360
3641
  // A pre-existing local project is not proof that this run produced
3361
3642
  // valid output. A terminal planner/executor error fails proof and
3362
3643
  // causes the operation journal to roll back streamed mutations.
3363
- const failedPreviewGate = {
3364
- required: true,
3365
- passed: false,
3366
- skipped: true,
3367
- error: 'Agent execution failed before preview validation.',
3368
- };
3644
+ const analysisOnly = this.isAnalysisOnlyTask(message, executionContext);
3645
+ const failedPreviewGate = analysisOnly
3646
+ ? { required: false, passed: true, skipped: false }
3647
+ : {
3648
+ required: true,
3649
+ passed: false,
3650
+ skipped: true,
3651
+ error: 'Agent execution failed before preview validation.',
3652
+ };
3369
3653
  return this.finalizeV3AgentWorkflowResponse(data, {
3370
3654
  content: '',
3371
3655
  taskId: data.task_id || null,
@@ -3394,6 +3678,23 @@ document.addEventListener('DOMContentLoaded', () => {
3394
3678
  }, mutationJournal);
3395
3679
  }
3396
3680
  catch (error) {
3681
+ if (error?.code === 'AGENT_PLAN_INVALID') {
3682
+ if (mutationJournal?.hasMutations()) {
3683
+ const mutation = mutationJournal.rollback();
3684
+ mutationJournal = null;
3685
+ error.operationId = mutation.operationId;
3686
+ error.mutation = mutation;
3687
+ error.partialMutation = mutation.partialMutation;
3688
+ }
3689
+ else {
3690
+ error.partialMutation = false;
3691
+ }
3692
+ this.activeV3StreamedFiles = null;
3693
+ // A malformed graph is deterministic and backend-independent.
3694
+ // Never retry it against another service or erase its typed code
3695
+ // while folding endpoint diagnostics into a string.
3696
+ throw error;
3697
+ }
3397
3698
  if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
3398
3699
  this.recoverAgentWorkspaceFiles(executionContext, error.partialData.files || {}, expectedFiles);
3399
3700
  await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
@@ -3606,14 +3907,50 @@ document.addEventListener('DOMContentLoaded', () => {
3606
3907
  ? requestedOperatorTimeoutMs
3607
3908
  : 0;
3608
3909
  const workspacePath = executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd();
3609
- const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
3910
+ const serverWorkspacePath = this.resolveServerBindableWorkspacePath(executionContext);
3911
+ const clientWorkspaceAuthoritative = Boolean(workspacePath
3912
+ && !serverWorkspacePath
3913
+ && executionContext.localMachineCapable !== false);
3914
+ let workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath, message, !clientWorkspaceAuthoritative);
3610
3915
  const operatorWorkspaceAlias = `vigthoria://workspace/${encodeURIComponent(path.basename(workspacePath))}`;
3611
3916
  const requestedModel = String(executionContext.model || 'code');
3612
3917
  const resolvedModel = this.resolveModelId(requestedModel);
3613
3918
  try {
3614
- return await this.operatorClient.run({
3919
+ let groundedAnalysis = null;
3920
+ let operatorExecutionContext = executionContext;
3921
+ if (clientWorkspaceAuthoritative) {
3922
+ groundedAnalysis = await this.runV3AgentWorkflow([
3923
+ 'Inspect the client workspace through the authenticated local tool bridge.',
3924
+ 'Produce a grounded, read-only technical report for the BMAD Operator.',
3925
+ 'Do not modify files. Identify relevant files, constraints, risks, and exact evidence for this request:',
3926
+ message,
3927
+ ].join('\n'), {
3928
+ ...executionContext,
3929
+ rawPrompt: message,
3930
+ contextualPrompt: message,
3931
+ agentTaskType: 'analysis',
3932
+ workflowType: 'analysis_only',
3933
+ clientToolExecution: true,
3934
+ localMachineCapable: true,
3935
+ });
3936
+ const groundingReport = String(groundedAnalysis.content || '').trim();
3937
+ if (!groundingReport || groundedAnalysis.partial === true) {
3938
+ throw new CLIError('Operator could not obtain a complete grounded report from the client workspace bridge.', 'model_backend');
3939
+ }
3940
+ workspaceSummary = workspaceSummary || { name: path.basename(workspacePath), files: [] };
3941
+ workspaceSummary.workspaceAuthority = 'client-tool-bridge';
3942
+ workspaceSummary.groundedBy = 'v3-client-tools';
3943
+ delete workspaceSummary.workspaceFiles;
3944
+ operatorExecutionContext = {
3945
+ ...executionContext,
3946
+ workspaceAuthority: 'client-tool-bridge',
3947
+ clientGroundingReport: groundingReport.slice(0, 64 * 1024),
3948
+ workflowType: executionContext.workflowType === 'full' ? 'planning_only' : executionContext.workflowType,
3949
+ };
3950
+ }
3951
+ const operatorResult = await this.operatorClient.run({
3615
3952
  message,
3616
- executionContext,
3953
+ executionContext: operatorExecutionContext,
3617
3954
  workspaceAlias: operatorWorkspaceAlias,
3618
3955
  workspaceSummary,
3619
3956
  requestedModel,
@@ -3621,6 +3958,54 @@ document.addEventListener('DOMContentLoaded', () => {
3621
3958
  timeoutMs,
3622
3959
  onStreamEvent: typeof context.onStreamEvent === 'function' ? context.onStreamEvent : undefined,
3623
3960
  });
3961
+ if (!clientWorkspaceAuthoritative || executionContext.workflowType !== 'full') {
3962
+ return {
3963
+ ...operatorResult,
3964
+ metadata: {
3965
+ ...(operatorResult.metadata || {}),
3966
+ ...(clientWorkspaceAuthoritative ? {
3967
+ workspaceAuthority: 'client-tool-bridge',
3968
+ groundingTaskId: groundedAnalysis?.taskId || null,
3969
+ } : {}),
3970
+ },
3971
+ };
3972
+ }
3973
+ const plan = String(operatorResult.content || '').trim();
3974
+ if (!plan) {
3975
+ throw new CLIError('BMAD Operator returned no governed implementation plan.', 'model_backend');
3976
+ }
3977
+ const implementation = await this.runV3AgentWorkflow([
3978
+ message,
3979
+ '',
3980
+ 'Execute this governed BMAD plan against the authoritative client workspace using client tools:',
3981
+ plan.slice(0, 64 * 1024),
3982
+ ].join('\n'), {
3983
+ ...executionContext,
3984
+ rawPrompt: message,
3985
+ contextualPrompt: message,
3986
+ workflowType: 'full',
3987
+ clientToolExecution: true,
3988
+ localMachineCapable: true,
3989
+ });
3990
+ if (implementation.partial === true) {
3991
+ throw new CLIError('Local implementation ended partially and was not reported as Operator success.', 'model_backend');
3992
+ }
3993
+ return {
3994
+ content: implementation.content,
3995
+ workflowId: operatorResult.workflowId,
3996
+ contextId: implementation.contextId || operatorResult.contextId,
3997
+ backendUrl: operatorResult.backendUrl,
3998
+ savedWorkflow: operatorResult.savedWorkflow,
3999
+ changedFiles: implementation.changedFiles,
4000
+ metadata: {
4001
+ ...(operatorResult.metadata || {}),
4002
+ workspaceAuthority: 'client-tool-bridge',
4003
+ groundingTaskId: groundedAnalysis?.taskId || null,
4004
+ implementationTaskId: implementation.taskId,
4005
+ implementationBackendUrl: implementation.backendUrl,
4006
+ bmadPlan: true,
4007
+ },
4008
+ };
3624
4009
  }
3625
4010
  catch (error) {
3626
4011
  if (error instanceof OperatorClientError) {
@@ -3746,6 +4131,7 @@ document.addEventListener('DOMContentLoaded', () => {
3746
4131
  success: result.success === true,
3747
4132
  output: String(result.output || ''),
3748
4133
  error: String(result.error || ''),
4134
+ mutation: result.mutation || null,
3749
4135
  });
3750
4136
  for (let attempt = 0; attempt < 3; attempt += 1) {
3751
4137
  const response = await this.fetch(endpoint, {