vigthoria-cli 1.13.26 → 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 (51) hide show
  1. package/dist/commands/chat.js +73 -26
  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 +113 -9
  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.js +7 -2
  12. package/dist/commands/product-run-registration.js +1 -1
  13. package/dist/commands/replay.d.ts +0 -1
  14. package/dist/commands/replay.js +10 -19
  15. package/dist/commands/repo.js +16 -4
  16. package/dist/commands/update-registration.js +2 -2
  17. package/dist/commands/workflow.d.ts +4 -0
  18. package/dist/commands/workflow.js +27 -0
  19. package/dist/index.js +6 -4
  20. package/dist/utils/agentRunOutcome.d.ts +7 -0
  21. package/dist/utils/agentRunOutcome.js +13 -0
  22. package/dist/utils/api.d.ts +20 -5
  23. package/dist/utils/api.js +428 -43
  24. package/dist/utils/command-policy.js +1 -1
  25. package/dist/utils/config.d.ts +2 -0
  26. package/dist/utils/config.js +8 -3
  27. package/dist/utils/frontend-preview-service.d.ts +1 -0
  28. package/dist/utils/frontend-preview-service.js +54 -5
  29. package/dist/utils/model-governance.js +23 -14
  30. package/dist/utils/model-transport-service.js +1 -1
  31. package/dist/utils/network-policy.js +15 -3
  32. package/dist/utils/operator-client.js +23 -4
  33. package/dist/utils/post-write-validator.js +7 -3
  34. package/dist/utils/preview-screenshot-adapter.d.ts +16 -41
  35. package/dist/utils/preview-screenshot-adapter.js +273 -64
  36. package/dist/utils/runtime-capability.d.ts +7 -0
  37. package/dist/utils/runtime-capability.js +11 -0
  38. package/dist/utils/runtime-temp.d.ts +5 -2
  39. package/dist/utils/runtime-temp.js +125 -29
  40. package/dist/utils/tools.js +1 -1
  41. package/dist/utils/v3-stream-events.js +10 -2
  42. package/dist/utils/v3-workspace-service.d.ts +1 -0
  43. package/dist/utils/v3-workspace-service.js +38 -1
  44. package/dist/utils/vigflow-client.d.ts +9 -0
  45. package/dist/utils/vigflow-client.js +48 -2
  46. package/dist/utils/workspace-reference.d.ts +8 -0
  47. package/dist/utils/workspace-reference.js +21 -0
  48. package/package.json +4 -6
  49. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +2 -2
  50. package/scripts/release/validate-live-service-gates.sh +3 -3
  51. 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';
@@ -22,22 +22,24 @@ 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');
@@ -802,6 +804,9 @@ export class APIClient {
802
804
  getVigFlowExecutionStatus(executionId) {
803
805
  return this.vigFlowClient.executionStatus(executionId);
804
806
  }
807
+ deleteVigFlowWorkflow(selector) {
808
+ return this.vigFlowClient.deleteWorkflow(selector);
809
+ }
805
810
  async getV3AgentHeaders() {
806
811
  return this.v3AgentClient.headers();
807
812
  }
@@ -873,20 +878,12 @@ export class APIClient {
873
878
  const resolvedContext = this.ensureExecutionContext(context);
874
879
  const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
875
880
  const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
876
- const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
877
- const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
878
- if (resolvedContext.workspaceFilesOutOfBand === true && localWorkspaceSummary?.workspaceFiles) {
879
- localWorkspaceSummary.workspaceHydration = {
880
- delivery: 'out-of-band',
881
- fileCount: Object.keys(localWorkspaceSummary.workspaceFiles).length,
882
- complete: true,
883
- };
884
- delete localWorkspaceSummary.workspaceFiles;
885
- }
886
881
  const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
887
882
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
888
883
  const localWorkspaceName = this.getDisplayWorkspaceName(localWorkspacePath);
889
- 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;
890
887
  const executionSurface = String(resolvedContext.executionSurface || resolvedContext.clientSurface || 'cli');
891
888
  const localMachineCapable = resolvedContext.localMachineCapable !== false;
892
889
  const clientToolExecution = resolvedContext.clientToolExecution === false
@@ -895,15 +892,34 @@ export class APIClient {
895
892
  || (!serverWorkspacePath
896
893
  && localMachineCapable
897
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
+ }
898
915
  const publicRuntimeEnvironment = this.buildPublicRuntimeEnvironment(resolvedContext.agentRuntime, {
899
916
  localWorkspacePath,
900
917
  serverWorkspacePath,
901
918
  });
902
- // When the server cannot directly access the workspace (e.g. Windows
903
- // client), use the local path as a hint and flag that the workspace
904
- // 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.
905
921
  const effectiveWorkspacePath = serverWorkspacePath || null;
906
- const needsHydration = !serverWorkspacePath && !!localWorkspacePath;
922
+ const needsHydration = !clientToolExecution && !serverWorkspacePath && !!localWorkspacePath;
907
923
  const agentTaskType = resolvedContext.agentTaskType || 'general';
908
924
  const rawPrompt = resolvedContext.rawPrompt || resolvedContext.prompt || '';
909
925
  const executionHints = (resolvedContext.executionHints && typeof resolvedContext.executionHints === 'object')
@@ -957,15 +973,13 @@ export class APIClient {
957
973
  projectPath: effectiveWorkspacePath,
958
974
  targetPath: effectiveWorkspacePath,
959
975
  // Never send a client-local absolute path to the V3 model boundary.
960
- // The server only needs a stable label; file contents are carried by
961
- // 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.
962
978
  localWorkspacePath: serverWorkspacePath ? serverWorkspacePath : localWorkspaceRef,
963
979
  localWorkspaceRef,
964
980
  localWorkspaceName,
965
981
  localWorkspaceSummary,
966
- // Signal to the server that the workspace filesystem is not locally
967
- // accessible — it must hydrate a temp directory from the provided
968
- // workspaceFiles before the agent starts using tools.
982
+ // This is true only for explicit server-owned/background execution.
969
983
  workspaceHydrationRequired: needsHydration,
970
984
  contextId: resolvedContext.contextId,
971
985
  traceId: resolvedContext.traceId,
@@ -1892,7 +1906,7 @@ menu {
1892
1906
  cwd: serverBindableWorkspace ? paths.serverWorkspacePath || null : (localName ? `vigthoria://local-workspace/${localName}` : null),
1893
1907
  };
1894
1908
  }
1895
- buildLocalWorkspaceSummary(rootPath, requestFocus = '') {
1909
+ buildLocalWorkspaceSummary(rootPath, requestFocus = '', includeWorkspaceFiles = true) {
1896
1910
  if (!rootPath || !fs.existsSync(rootPath)) {
1897
1911
  return null;
1898
1912
  }
@@ -1978,9 +1992,12 @@ menu {
1978
1992
  if (fs.existsSync(readmePath)) {
1979
1993
  summary.readmeExcerpt = fs.readFileSync(readmePath, 'utf8').slice(0, 2500);
1980
1994
  }
1981
- // Hydrate workspace: include actual file contents so the V3 server
1982
- // can populate the remote workspace before the agent starts.
1983
- 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
+ }
1984
2001
  return summary;
1985
2002
  }
1986
2003
  catch (error) {
@@ -1996,7 +2013,15 @@ menu {
1996
2013
  const resolvedContext = this.ensureExecutionContext(context);
1997
2014
  const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
1998
2015
  const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
1999
- 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)
2000
2025
  return undefined;
2001
2026
  const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
2002
2027
  const summary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
@@ -2031,6 +2056,29 @@ menu {
2031
2056
  resolveV3ClientToolPath(rootPath, rawPath, sourceRoots = []) {
2032
2057
  return this.v3WorkspaceService.resolveClientToolPath(rootPath, rawPath, sourceRoots);
2033
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
+ }
2034
2082
  recordV3ClientToolMutation(event, context, result) {
2035
2083
  if (!result.success) {
2036
2084
  return;
@@ -2065,6 +2113,23 @@ menu {
2065
2113
  }
2066
2114
  }
2067
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
+ }
2068
2133
  async executeV3ClientToolRequest(event, context = {}) {
2069
2134
  const rootPath = this.resolveAgentTargetPath(context);
2070
2135
  if (!rootPath) {
@@ -2084,13 +2149,121 @@ menu {
2084
2149
  if (!target) {
2085
2150
  return { success: false, output: '', error: 'Tool path is outside the workspace.' };
2086
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
+ }
2087
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
+ }
2088
2258
  if (name === 'write_file') {
2089
2259
  if (typeof args.content !== 'string')
2090
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 };
2091
2264
  fs.mkdirSync(path.dirname(target.absolutePath), { recursive: true });
2092
2265
  fs.writeFileSync(target.absolutePath, args.content, 'utf8');
2093
- const result = { success: true, output: `Updated ${target.relativePath}` };
2266
+ const result = { success: true, output: `Updated ${target.relativePath}`, mutation };
2094
2267
  this.recordV3ClientToolMutation(event, context, result);
2095
2268
  return result;
2096
2269
  }
@@ -2101,10 +2274,31 @@ menu {
2101
2274
  return { success: false, output: '', error: 'edit_file requires old_string.' };
2102
2275
  const existing = fs.readFileSync(target.absolutePath, 'utf8');
2103
2276
  const count = existing.split(oldString).length - 1;
2104
- if (count !== 1)
2105
- return { success: false, output: '', error: `old_string found ${count} times; expected exactly once.` };
2106
- fs.writeFileSync(target.absolutePath, existing.replace(oldString, newString), 'utf8');
2107
- 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 };
2108
2302
  this.recordV3ClientToolMutation(event, context, result);
2109
2303
  return result;
2110
2304
  }
@@ -2354,6 +2548,19 @@ menu {
2354
2548
  normalizeAgentWorkspaceRelativePath(rawPath, rootPath) {
2355
2549
  return this.v3WorkspaceService.normalizeRelativePath(rawPath, rootPath);
2356
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
+ }
2357
2564
  async ensureAgentFrontendPolish(message = '', context = {}) {
2358
2565
  const rootPath = this.resolveAgentTargetPath(context);
2359
2566
  if (!rootPath || !fs.existsSync(rootPath)) {
@@ -2361,16 +2568,66 @@ menu {
2361
2568
  }
2362
2569
  const prompt = String(message || '');
2363
2570
  const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
2364
- 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)
2365
2572
  || expectedFiles.some((filePath) => /\.(html|css|js)$/i.test(filePath));
2366
2573
  if (!looksLikeFrontendTask) {
2367
2574
  return;
2368
2575
  }
2369
- const htmlPath = path.join(rootPath, 'index.html');
2370
- 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') {
2371
2629
  return;
2372
2630
  }
2373
- const html = fs.readFileSync(htmlPath, 'utf8');
2374
2631
  const ensuredAssets = this.ensureReferencedFrontendAssets(rootPath, html, prompt, 'Vigthoria CLI');
2375
2632
  const cssPath = ensuredAssets.cssPath;
2376
2633
  const jsPath = ensuredAssets.jsPath;
@@ -2380,6 +2637,24 @@ menu {
2380
2637
  }
2381
2638
  let css = fs.readFileSync(cssPath, 'utf8');
2382
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
+ }
2383
2658
  const keyframesBlocks = Array.from(js.matchAll(/@keyframes[\s\S]*?\n\}/g)).map((match) => match[0]);
2384
2659
  if (keyframesBlocks.length > 0) {
2385
2660
  const migrated = keyframesBlocks.filter((block) => !css.includes(block));
@@ -2457,7 +2732,15 @@ menu {
2457
2732
  const cssMarker = '/* Vigthoria CLI Motion Enhancement */';
2458
2733
  const jsMarker = '/* Vigthoria CLI Motion Enhancement */';
2459
2734
  if (!css.includes(cssMarker)) {
2460
- 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');
2461
2744
  }
2462
2745
  if (!js.includes(jsMarker)) {
2463
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');
@@ -3395,6 +3678,23 @@ document.addEventListener('DOMContentLoaded', () => {
3395
3678
  }, mutationJournal);
3396
3679
  }
3397
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
+ }
3398
3698
  if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
3399
3699
  this.recoverAgentWorkspaceFiles(executionContext, error.partialData.files || {}, expectedFiles);
3400
3700
  await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
@@ -3607,14 +3907,50 @@ document.addEventListener('DOMContentLoaded', () => {
3607
3907
  ? requestedOperatorTimeoutMs
3608
3908
  : 0;
3609
3909
  const workspacePath = executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd();
3610
- 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);
3611
3915
  const operatorWorkspaceAlias = `vigthoria://workspace/${encodeURIComponent(path.basename(workspacePath))}`;
3612
3916
  const requestedModel = String(executionContext.model || 'code');
3613
3917
  const resolvedModel = this.resolveModelId(requestedModel);
3614
3918
  try {
3615
- 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({
3616
3952
  message,
3617
- executionContext,
3953
+ executionContext: operatorExecutionContext,
3618
3954
  workspaceAlias: operatorWorkspaceAlias,
3619
3955
  workspaceSummary,
3620
3956
  requestedModel,
@@ -3622,6 +3958,54 @@ document.addEventListener('DOMContentLoaded', () => {
3622
3958
  timeoutMs,
3623
3959
  onStreamEvent: typeof context.onStreamEvent === 'function' ? context.onStreamEvent : undefined,
3624
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
+ };
3625
4009
  }
3626
4010
  catch (error) {
3627
4011
  if (error instanceof OperatorClientError) {
@@ -3747,6 +4131,7 @@ document.addEventListener('DOMContentLoaded', () => {
3747
4131
  success: result.success === true,
3748
4132
  output: String(result.output || ''),
3749
4133
  error: String(result.error || ''),
4134
+ mutation: result.mutation || null,
3750
4135
  });
3751
4136
  for (let attempt = 0; attempt < 3; attempt += 1) {
3752
4137
  const response = await this.fetch(endpoint, {