@vizzly-testing/cli 0.35.3-beta.1 → 0.35.3-beta.3

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 (34) hide show
  1. package/README.md +8 -3
  2. package/dist/api/endpoints.js +1 -46
  3. package/dist/api/index.js +1 -1
  4. package/dist/cli.js +5 -5
  5. package/dist/commands/api.js +3 -3
  6. package/dist/commands/builds.js +12 -15
  7. package/dist/commands/comparisons.js +7 -9
  8. package/dist/commands/context.js +10 -20
  9. package/dist/commands/init.js +62 -13
  10. package/dist/commands/review.js +122 -35
  11. package/dist/commands/run.js +1 -3
  12. package/dist/commands/status.js +35 -79
  13. package/dist/commands/upload.js +1 -1
  14. package/dist/context/local-workspace-provider.js +36 -126
  15. package/dist/reporter/reporter-bundle.iife.js +49 -81
  16. package/dist/server/handlers/tdd-handler.js +2 -7
  17. package/dist/server/routers/dashboard.js +5 -1
  18. package/dist/tdd/runtime-context.js +0 -2
  19. package/dist/tdd/services/comparison-service.js +36 -76
  20. package/dist/tdd/tdd-service.js +7 -205
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/uploader/core.js +7 -8
  23. package/dist/utils/visual-context-normalizers.js +86 -139
  24. package/package.json +4 -4
  25. package/skills/vizzly/SKILL.md +42 -52
  26. package/skills/vizzly/references/cli-context.md +49 -52
  27. package/skills/vizzly/references/dynamic-content.md +18 -29
  28. package/skills/vizzly/references/sdks.md +20 -28
  29. package/skills/vizzly/references/setup-ci.md +18 -42
  30. package/dist/tdd/core/hotspot-coverage.js +0 -112
  31. package/dist/tdd/core/region-coverage.js +0 -203
  32. package/dist/tdd/metadata/hotspot-metadata.js +0 -93
  33. package/dist/tdd/metadata/region-metadata.js +0 -56
  34. package/skills/vizzly/agents/openai.yaml +0 -4
package/README.md CHANGED
@@ -35,7 +35,12 @@ For agent-friendly repos, install the Vizzly skill and add a short project
35
35
  vizzly init --agent-guidance
36
36
  ```
37
37
 
38
- Use `vizzly init --agent-skill` to install only the local skill, or
38
+ This installs the portable Agent Skills bundle in `.agents/skills/vizzly` and
39
+ adds a small managed block to `AGENTS.md`. Run the same command again after a
40
+ CLI upgrade to refresh both without replacing the rest of `AGENTS.md` or an
41
+ existing Vizzly config.
42
+
43
+ Use `vizzly init --agent-skill` to install or refresh only the local skill, or
39
44
  `vizzly init --skip-agent-skill` when you want config without the agent prompt.
40
45
 
41
46
  ### Start Local TDD
@@ -202,8 +207,8 @@ Generate a config file:
202
207
  vizzly init
203
208
  ```
204
209
 
205
- To teach project agents about Vizzly screenshot memory and the local visual TDD
206
- loop, add the repo-local skill and AGENTS.md guidance:
210
+ To teach project agents the Vizzly evidence workflow, add the repo-local skill
211
+ and AGENTS.md guidance:
207
212
 
208
213
  ```bash
209
214
  vizzly init --agent-guidance
@@ -38,7 +38,7 @@ export async function getBuild(client, buildId, include = null) {
38
38
  *
39
39
  * @param {Object} client - API client
40
40
  * @param {string} buildId - Build ID
41
- * @returns {Promise<Object>} Canonical build status bundle
41
+ * @returns {Promise<Object>} Build status response
42
42
  */
43
43
  export async function getBuildStatus(client, buildId) {
44
44
  return client.request(`/api/sdk/builds/${buildId}/status`);
@@ -343,51 +343,6 @@ export async function searchComparisons(client, name, filters = {}) {
343
343
  return client.request(endpoint);
344
344
  }
345
345
 
346
- // ============================================================================
347
- // Hotspot Endpoints
348
- // ============================================================================
349
-
350
- /**
351
- * Get hotspot analysis for a single screenshot
352
- * @param {Object} client - API client
353
- * @param {string} screenshotName - Screenshot name
354
- * @param {Object} options - Optional settings
355
- * @returns {Promise<Object>} Hotspot analysis data
356
- */
357
- export async function getScreenshotHotspots(client, screenshotName, options = {}) {
358
- let {
359
- windowSize = 20
360
- } = options;
361
- let encodedName = encodeURIComponent(screenshotName);
362
- let endpoint = buildEndpointWithParams(`/api/sdk/screenshots/${encodedName}/hotspots`, {
363
- windowSize: String(windowSize)
364
- });
365
- return client.request(endpoint);
366
- }
367
-
368
- /**
369
- * Batch get hotspot analysis for multiple screenshots
370
- * @param {Object} client - API client
371
- * @param {string[]} screenshotNames - Array of screenshot names
372
- * @param {Object} options - Optional settings
373
- * @returns {Promise<Object>} Hotspots keyed by screenshot name
374
- */
375
- export async function getBatchHotspots(client, screenshotNames, options = {}) {
376
- let {
377
- windowSize = 20
378
- } = options;
379
- return client.request('/api/sdk/screenshots/hotspots', {
380
- method: 'POST',
381
- headers: {
382
- 'Content-Type': 'application/json'
383
- },
384
- body: JSON.stringify({
385
- screenshot_names: screenshotNames,
386
- windowSize
387
- })
388
- });
389
- }
390
-
391
346
  // ============================================================================
392
347
  // Auth/Token Endpoints
393
348
  // ============================================================================
package/dist/api/index.js CHANGED
@@ -16,4 +16,4 @@ export { createApiClient, DEFAULT_API_URL } from './client.js';
16
16
  export { buildApiUrl, buildAuthHeader, buildBuildPayload, buildEndpointWithParams, buildQueryParams, buildRequestHeaders, buildScreenshotCheckObject, buildScreenshotPayload, buildScreenshotResolvePayload, buildShaCheckPayload, buildUserAgent, computeSha256, extractErrorBody, findScreenshotBySha, isAuthError, isRateLimited, parseApiError, partitionByShaExistence, shaExists, shouldRetryWithRefresh } from './core.js';
17
17
 
18
18
  // Endpoint functions
19
- export { checkShas, createBuild, finalizeBuild, finalizeParallelBuild, getBatchHotspots, getBuild, getBuildContext, getBuildStatus, getBuilds, getComparison, getComparisonContext, getPreviewInfo, getReviewQueueContext, getScreenshotContext, getScreenshotHotspots, getSimilarFingerprintContext, getTddBaselines, getTokenContext, searchComparisons, updateBuildStatus, uploadPreviewZip, uploadScreenshot } from './endpoints.js';
19
+ export { checkShas, createBuild, finalizeBuild, finalizeParallelBuild, getBuild, getBuildContext, getBuildStatus, getBuilds, getComparison, getComparisonContext, getPreviewInfo, getReviewQueueContext, getScreenshotContext, getSimilarFingerprintContext, getTddBaselines, getTokenContext, searchComparisons, updateBuildStatus, uploadPreviewZip, uploadScreenshot } from './endpoints.js';
package/dist/cli.js CHANGED
@@ -450,7 +450,7 @@ try {
450
450
  } catch (error) {
451
451
  output.debug(`Plugin loading failed: ${error.message}`);
452
452
  }
453
- program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').option('--agent-skill', 'Install the repo-local Vizzly agent skill').option('--agent-guidance', 'Add Vizzly guidance to this project AGENTS.md and install the agent skill').option('--skip-agent-skill', 'Skip the Vizzly agent skill prompt').action(async options => {
453
+ program.command('init').description('Initialize Vizzly in your project').option('--force', 'Overwrite existing configuration').option('--agent-skill', 'Install or refresh the repo-local Vizzly agent skill').option('--agent-guidance', 'Install or refresh the repo-local Vizzly skill and AGENTS.md guidance').option('--skip-agent-skill', 'Skip the Vizzly agent skill prompt').action(async options => {
454
454
  let globalOptions = getGlobalOptions();
455
455
  await init({
456
456
  ...globalOptions,
@@ -681,7 +681,7 @@ Examples:
681
681
  }
682
682
  await contextComparisonCommand(comparisonId, options, globalOptions);
683
683
  });
684
- contextCmd.command('screenshot').description('Fetch screenshot context and historical memory').argument('<name>', 'Screenshot name').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('-p, --project <slug-or-id>', 'Project scope for user auth lookups').option('--org <slug>', 'Organization slug when project slug is ambiguous').option('--recent-limit <n>', 'Maximum recent comparisons to return (1-50)', Number).option('--window-size <n>', 'Historical hotspot analysis window size (1-50)', Number).addHelpText('after', `
684
+ contextCmd.command('screenshot').description('Fetch screenshot context and historical memory').argument('<name>', 'Screenshot name').option('--source <source>', 'Context source: auto, cloud, or local', 'auto').option('-p, --project <slug-or-id>', 'Project scope for user auth lookups').option('--org <slug>', 'Organization slug when project slug is ambiguous').option('--recent-limit <n>', 'Maximum recent comparisons to return (1-50)', Number).addHelpText('after', `
685
685
  Examples:
686
686
  $ vizzly context screenshot Dashboard --source cloud
687
687
  $ vizzly context screenshot Dashboard --source local
@@ -738,15 +738,15 @@ Note: Baselines are stored locally in .vizzly/baselines/ during TDD mode.
738
738
  let globalOptions = getGlobalOptions();
739
739
  await baselinesCommand(options, globalOptions);
740
740
  });
741
- program.command('api').description('Make raw API requests (for power users)').argument('<endpoint>', 'API endpoint (e.g., /api/sdk/builds)').option('-X, --method <method>', 'HTTP method (GET or POST for approve/reject/comment)', 'GET').option('-d, --data <json>', 'Request body (JSON)').option('-H, --header <header>', 'Add header (key:value), can be repeated', (val, prev) => prev ? [...prev, val] : [val]).option('-q, --query <param>', 'Add query param (key=value), can be repeated', (val, prev) => prev ? [...prev, val] : [val]).addHelpText('after', `
741
+ program.command('api').description('Make raw API requests (for power users)').argument('<endpoint>', 'API endpoint (e.g., /api/sdk/builds)').option('-X, --method <method>', 'HTTP method (GET or POST for build comments)', 'GET').option('-d, --data <json>', 'Request body (JSON)').option('-H, --header <header>', 'Add header (key:value), can be repeated', (val, prev) => prev ? [...prev, val] : [val]).option('-q, --query <param>', 'Add query param (key=value), can be repeated', (val, prev) => prev ? [...prev, val] : [val]).addHelpText('after', `
742
742
  Examples:
743
743
  $ vizzly api /api/sdk/builds # List builds
744
744
  $ vizzly api /api/sdk/builds -q limit=5 # With query params
745
745
  $ vizzly api /api/sdk/builds/abc123 # Get specific build
746
- $ vizzly api /api/sdk/comparisons/abc123/approve -X POST
746
+ $ vizzly api /api/sdk/builds/abc123/comments -X POST -d '{"content":"Looks good"}'
747
747
  $ vizzly api /api/sdk/builds/abc123/comments -X POST -d '{"content":"Nice!"}'
748
748
 
749
- Note: POST is restricted to approve, reject, and comment endpoints.
749
+ Note: POST is restricted to build comment endpoints. Use dedicated approve/reject commands for review decisions.
750
750
  Most operations have dedicated commands (builds, comparisons, approve, etc.).
751
751
  `).action(async (endpoint, options) => {
752
752
  let globalOptions = getGlobalOptions();
@@ -5,7 +5,7 @@
5
5
  import { createApiClient as defaultCreateApiClient } from '../api/index.js';
6
6
  import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
7
7
  import * as defaultOutput from '../utils/output.js';
8
- let ALLOWED_POST_ENDPOINTS = [/^\/api\/sdk\/comparisons\/[^/]+\/approve$/, /^\/api\/sdk\/comparisons\/[^/]+\/reject$/, /^\/api\/sdk\/builds\/[^/]+\/comments$/];
8
+ let ALLOWED_POST_ENDPOINTS = [/^\/api\/sdk\/builds\/[^/]+\/comments$/];
9
9
  function createApiCommandDeps(deps = {}) {
10
10
  return {
11
11
  loadConfig: deps.loadConfig || defaultLoadConfig,
@@ -70,11 +70,11 @@ export function validateApiRequest({
70
70
  }) {
71
71
  let errors = [];
72
72
  if (method !== 'GET' && method !== 'POST') {
73
- errors.push(`Method ${method} not allowed. Use GET for queries or POST for approve/reject/comment.`);
73
+ errors.push(`Method ${method} not allowed. Use GET for queries or POST for build comments.`);
74
74
  return errors;
75
75
  }
76
76
  if (method === 'POST' && !isAllowedPostEndpoint(endpoint)) {
77
- errors.push(`POST not allowed for ${endpoint}. Only approve, reject, and comment endpoints support POST.`);
77
+ errors.push(`POST not allowed for ${endpoint}. Only build comment endpoints support POST.`);
78
78
  }
79
79
  if (hasData && method !== 'POST') {
80
80
  errors.push('Request data requires --method POST.');
@@ -112,13 +112,11 @@ export async function buildsCommand(options = {}, globalOptions = {}, deps = {})
112
112
  for (let build of builds) {
113
113
  let statusColor = getStatusColor(colors, build.status);
114
114
  let statusBadge = statusColor(build.status.toUpperCase());
115
-
116
- // Approval badge
117
- let approvalBadge = '';
118
- if (build.approval_status && build.status === 'completed') {
119
- approvalBadge = ` ${getApprovalBadge(colors, build.approval_status)}`;
115
+ let reviewBadge = '';
116
+ if (build.visual_review?.state && build.status === 'completed') {
117
+ reviewBadge = ` ${getReviewBadge(colors, build.visual_review.state)}`;
120
118
  }
121
- output.print(` ${colors.bold(build.name || build.id)} ${statusBadge}${approvalBadge}`);
119
+ output.print(` ${colors.bold(build.name || build.id)} ${statusBadge}${reviewBadge}`);
122
120
  let details = [];
123
121
  if (build.branch) details.push(build.branch);
124
122
  if (build.commit_sha) details.push(build.commit_sha.substring(0, 7));
@@ -174,7 +172,7 @@ function formatBuildForJson(build, includeComparisons = false) {
174
172
  changed: build.changed_comparisons || 0,
175
173
  identical: build.identical_comparisons || 0
176
174
  },
177
- approvalStatus: build.approval_status,
175
+ visual_review: build.visual_review || null,
178
176
  createdAt: build.created_at,
179
177
  completedAt: build.completed_at
180
178
  };
@@ -192,7 +190,7 @@ function formatBuildForJson(build, includeComparisons = false) {
192
190
  name: c.name || c.current_name,
193
191
  status: c.status,
194
192
  diffPercentage: c.diff_percentage,
195
- approvalStatus: c.approval_status,
193
+ visual_review: c.visual_review || null,
196
194
  urls: {
197
195
  baseline: c.baseline_screenshot?.original_url || c.baseline_original_url || c.baseline_screenshot_url || null,
198
196
  current: c.current_screenshot?.original_url || c.current_original_url || c.current_screenshot_url || null,
@@ -238,8 +236,8 @@ function displayBuild(output, build, verbose) {
238
236
  if (stats.length > 0) {
239
237
  output.labelValue('Comparisons', stats.join(colors.dim(' · ')));
240
238
  }
241
- if (build.approval_status) {
242
- output.labelValue('Approval', build.approval_status);
239
+ if (build.visual_review?.state) {
240
+ output.labelValue('Review', build.visual_review.state);
243
241
  }
244
242
  output.blank();
245
243
  if (build.created_at) {
@@ -290,19 +288,18 @@ function getStatusColor(colors, status) {
290
288
  }
291
289
 
292
290
  /**
293
- * Get colored approval badge
291
+ * Get colored review badge
294
292
  */
295
- function getApprovalBadge(colors, approvalStatus) {
296
- switch (approvalStatus) {
293
+ function getReviewBadge(colors, reviewState) {
294
+ switch (reviewState) {
297
295
  case 'approved':
298
- case 'auto_approved':
299
296
  return colors.brand.success('APPROVED');
300
297
  case 'rejected':
301
298
  return colors.brand.error('REJECTED');
302
299
  case 'pending':
303
300
  return colors.brand.warning('PENDING');
304
301
  default:
305
- return colors.dim(approvalStatus?.toUpperCase() || '');
302
+ return colors.dim(reviewState?.toUpperCase() || '');
306
303
  }
307
304
  }
308
305
 
@@ -6,7 +6,7 @@ import { createApiClient as defaultCreateApiClient, getBuild as defaultGetBuild,
6
6
  import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
7
7
  import * as defaultOutput from '../utils/output.js';
8
8
  import { createWildcardMatcher } from '../utils/patterns.js';
9
- import { getComparisonBrowser, getComparisonName, getComparisonResult, getComparisonViewport, getVisualReviewState } from '../utils/visual-context-normalizers.js';
9
+ import { getComparisonBrowser, getComparisonName, getComparisonResult, getComparisonViewport } from '../utils/visual-context-normalizers.js';
10
10
 
11
11
  /**
12
12
  * Comparisons command - query comparisons with various filters
@@ -165,19 +165,17 @@ function formatComparisonForJson(comparison) {
165
165
  let ssimScore = comparison.ssim_score ?? diffImage.ssim_score ?? null;
166
166
  let gmsdScore = comparison.gmsd_score ?? diffImage.gmsd_score ?? null;
167
167
  let fingerprintHash = comparison.fingerprint_hash || diffImage.fingerprint_hash || null;
168
- let projection = comparison.analysis_projection || comparison.projection || diffImage.analysis_projection || diffImage.projection || null;
168
+ let analysisDetails = comparison.analysis_details || diffImage.analysis_details || null;
169
169
  let diffRegions = comparison.diff_regions ?? diffImage.diff_regions ?? null;
170
- let regionCount = comparison.region_count ?? diffImage.region_count ?? projection?.clusters?.count ?? (Array.isArray(diffRegions) ? diffRegions.length : null);
171
- let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash || projection || regionCount != null;
170
+ let regionCount = comparison.region_count ?? diffImage.region_count ?? analysisDetails?.clusters?.count ?? (Array.isArray(diffRegions) ? diffRegions.length : null);
171
+ let hasHoneydiff = clusterMetadata || ssimScore != null || gmsdScore != null || fingerprintHash || analysisDetails || regionCount != null;
172
172
  return {
173
173
  id: comparison.id,
174
174
  name: getComparisonName(comparison),
175
175
  status: comparison.status,
176
176
  result: getComparisonResult(comparison),
177
177
  diffPercentage: comparison.diff_percentage ?? null,
178
- approvalStatus: comparison.approval_status,
179
- reviewState: getVisualReviewState(comparison),
180
- visualReview: comparison.visual_review || null,
178
+ visual_review: comparison.visual_review || null,
181
179
  viewport: getComparisonViewport(comparison),
182
180
  browser: getComparisonBrowser(comparison),
183
181
  urls: {
@@ -192,7 +190,7 @@ function formatComparisonForJson(comparison) {
192
190
  clusterMetadata,
193
191
  fingerprintHash,
194
192
  regionCount,
195
- projection,
193
+ details: analysisDetails,
196
194
  diffRegions,
197
195
  diffLines: comparison.diff_lines ?? diffImage.diff_lines ?? null,
198
196
  fingerprintData: comparison.fingerprint_data ?? diffImage.fingerprint_data ?? null
@@ -214,7 +212,7 @@ function displayComparison(output, comparison, verbose) {
214
212
  Name: formatted.name,
215
213
  Status: formatted.result?.toUpperCase(),
216
214
  'Diff %': comparison.diff_percentage != null ? `${(comparison.diff_percentage * 100).toFixed(2)}%` : 'N/A',
217
- Review: formatted.reviewState || 'unknown'
215
+ Review: formatted.visual_review?.state || 'unknown'
218
216
  });
219
217
  output.blank();
220
218
  if (formatted.viewport) {
@@ -393,7 +393,7 @@ function buildCompactBuildCommands(context = {}, include = [], cursor = null) {
393
393
  command: appendContextSource(`vizzly --json context screenshot ${quoteCommandArgument(screenshotName)}`, context)
394
394
  });
395
395
  }
396
- if (buildTarget && evidence.length > 0) {
396
+ if (buildTarget && evidence.length > 0 && !include.includes('diffs')) {
397
397
  let cursorFlag = cursor ? ` --cursor ${quoteCommandArgument(cursor)}` : '';
398
398
  commands.push({
399
399
  label: 'Load raw diff diagnostics',
@@ -427,6 +427,12 @@ function buildCompactComparisonCommands(context = {}, include = []) {
427
427
  let comparisonId = context.comparison?.id;
428
428
  let streams = [['similar_by_fingerprint', 'similar history'], ['recent_by_name', 'recent history']];
429
429
  let includeFlag = include.length > 0 ? ` --include ${include.join(',')}` : '';
430
+ if (comparisonId && !include.includes('diffs')) {
431
+ commands.push({
432
+ label: 'Load raw diff diagnostics',
433
+ command: appendContextSource(`vizzly --json context comparison ${quoteCommandArgument(comparisonId)} --agent --include diffs`, context)
434
+ });
435
+ }
430
436
  for (let [stream, label] of streams) {
431
437
  let page = context.history?.[stream]?.page;
432
438
  if (!comparisonId || !page?.has_more || !page.next_cursor) {
@@ -565,9 +571,6 @@ function formatNeedsReview(status = {}) {
565
571
  }
566
572
  return details.length > 0 ? `yes · ${details.join(' · ')}` : 'yes';
567
573
  }
568
- function formatConfirmedRegionLabels(regions = []) {
569
- return regions.map(region => region.label).filter(Boolean).slice(0, 3).join(' · ');
570
- }
571
574
  function printComparisonList(output, comparisons = [], {
572
575
  limit = 5
573
576
  } = {}) {
@@ -735,19 +738,15 @@ function displayComparisonContext(output, context) {
735
738
  let statusTone = getStatusTone(colors, displayState);
736
739
  let screenshotName = context.comparison.screenshot?.name || context.comparison.id;
737
740
  let analysis = context.comparison.analysis || {};
738
- let confirmedRegionLabels = formatConfirmedRegionLabels(context.history.confirmed_regions);
739
741
  output.print(` ${colors.bold(screenshotName)} ${statusTone(displayState.toUpperCase())}`);
740
742
  output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
741
743
  output.blank();
742
744
  output.labelValue('Eyes', `${analysis.diff_image_url ? 'baseline/current/diff' : 'comparison metadata only'}`);
743
- output.labelValue('Memory', `${context.history.similar_by_fingerprint.length} similar · ${context.history.recent_by_name.length} recent · ${context.history.confirmed_regions.length} confirmed regions`);
745
+ output.labelValue('Memory', `${context.history.similar_by_fingerprint.length} similar · ${context.history.recent_by_name.length} recent`);
744
746
  output.labelValue('Review', `${context.review.build_comments.length} build comments · ${countScreenshotCommentEntries(context.review.screenshot_comments)} screenshot comments`);
745
747
  if (analysis.fingerprint_hash) {
746
748
  output.labelValue('Fingerprint', analysis.fingerprint_hash);
747
749
  }
748
- if (confirmedRegionLabels) {
749
- output.labelValue('Known Regions', confirmedRegionLabels);
750
- }
751
750
  if (context.links?.comparison_url) {
752
751
  output.labelValue('Comparison URL', context.links.comparison_url);
753
752
  }
@@ -760,15 +759,10 @@ function displayComparisonContext(output, context) {
760
759
  function displayScreenshotContext(output, context) {
761
760
  output.header('context', 'screenshot');
762
761
  let colors = output.getColors();
763
- let confirmedRegionLabels = formatConfirmedRegionLabels(context.confirmed_regions);
764
762
  output.print(` ${colors.bold(context.screenshot.name)}`);
765
763
  output.print(` ${colors.dim(`@${context.scope.organization.slug}/${context.scope.project.slug}`)}`);
766
764
  output.blank();
767
- output.labelValue('Memory', `${context.history.recent_comparisons.length} recent comparisons · ${context.confirmed_regions.length} confirmed regions`);
768
- output.labelValue('Hotspots', `${context.hotspot_analysis.total_builds_analyzed} builds analyzed · ${context.hotspot_analysis.confidence}`);
769
- if (confirmedRegionLabels) {
770
- output.labelValue('Known Regions', confirmedRegionLabels);
771
- }
765
+ output.labelValue('Memory', `${context.history.recent_comparisons.length} recent comparisons`);
772
766
  if (context.history.recent_comparisons.length > 0) {
773
767
  output.blank();
774
768
  output.print(' Recent Comparisons');
@@ -934,8 +928,7 @@ export async function contextScreenshotCommand(screenshotName, options = {}, glo
934
928
  return;
935
929
  }
936
930
  let query = buildScopeQuery(options, {
937
- recentLimit: options.recentLimit,
938
- windowSize: options.windowSize
931
+ recentLimit: options.recentLimit
939
932
  });
940
933
  output.startSpinner('Fetching screenshot context...');
941
934
  let context = await runtime.provider.getScreenshotContext(screenshotName, query);
@@ -1052,9 +1045,6 @@ export function validateContextScreenshotOptions(options = {}) {
1052
1045
  errors.push(...validateLimitRange(options.recentLimit, '--recent-limit', {
1053
1046
  max: 50
1054
1047
  }));
1055
- errors.push(...validateLimitRange(options.windowSize, '--window-size', {
1056
- max: 50
1057
- }));
1058
1048
  return errors;
1059
1049
  }
1060
1050
  export function validateContextSimilarOptions(options = {}) {
@@ -23,6 +23,7 @@ function createInitDeps(deps = {}) {
23
23
  output: deps.output || output,
24
24
  promptAgentSkill: deps.promptAgentSkill || promptAgentSkill,
25
25
  readFile: deps.readFile || fs.readFile,
26
+ remove: deps.remove || fs.rm,
26
27
  skillSourcePath: deps.skillSourcePath || getPackagedAgentSkillPath(),
27
28
  writeFile: deps.writeFile || fs.writeFile
28
29
  };
@@ -213,16 +214,20 @@ async function shouldInstallAgentSkill(options, deps) {
213
214
  let agentGuidanceStart = '<!-- vizzly-agent-guidance -->';
214
215
  let agentGuidanceEnd = '<!-- /vizzly-agent-guidance -->';
215
216
  function createAgentGuidanceContent() {
217
+ let skillPath = '.agents/skills/vizzly/SKILL.md';
216
218
  return `${agentGuidanceStart}
217
219
  ## Visual Testing With Vizzly
218
220
 
219
- Use Vizzly for user-facing UI changes when this repo has Vizzly configured.
221
+ For user-facing changes, use the repo-local Vizzly skill at
222
+ \`${skillPath}\`.
220
223
 
221
- - Read visual history before changing UI: \`npx vizzly context build current --source local --agent\`
222
- - Inspect named screenshot history when useful: \`npx vizzly context screenshot "<screenshot-name>" --source local --json\`
223
- - Verify visual changes through the existing user workflow that owns the surface: \`npx vizzly tdd run "<test command>" --no-open\`
224
+ - Inspect existing visual evidence before and after the change.
225
+ - Use the repository's established Vizzly command and owning user workflow.
226
+ - Read bounded JSON with \`--agent --json\`, then inspect baseline, current, and
227
+ diff images together.
224
228
 
225
- Prefer existing E2E/user journeys over narrow screenshot-only specs. Treat Vizzly diffs as review evidence; do not approve or reject changes unless asked.
229
+ Treat Vizzly diffs as review evidence. Do not approve, reject, or replace
230
+ evidence unless the task explicitly asks for that mutation.
226
231
  ${agentGuidanceEnd}
227
232
  `;
228
233
  }
@@ -248,9 +253,28 @@ async function upsertProjectAgentGuidance({
248
253
  };
249
254
  }
250
255
  let existingContent = await readFile(agentsPath, 'utf8');
251
- if (existingContent.includes(agentGuidanceStart)) {
256
+ let startIndex = existingContent.indexOf(agentGuidanceStart);
257
+ let endIndex = existingContent.indexOf(agentGuidanceEnd);
258
+ let startCount = existingContent.split(agentGuidanceStart).length - 1;
259
+ let endCount = existingContent.split(agentGuidanceEnd).length - 1;
260
+ if (startCount !== endCount || startCount > 1 || startCount === 1 && endIndex < startIndex) {
252
261
  return {
253
- status: 'exists',
262
+ status: 'malformed',
263
+ agentsPath
264
+ };
265
+ }
266
+ if (startIndex >= 0 && endIndex >= startIndex) {
267
+ let blockEnd = endIndex + agentGuidanceEnd.length;
268
+ let refreshedContent = `${existingContent.slice(0, startIndex)}${guidance.trimEnd()}${existingContent.slice(blockEnd)}`;
269
+ if (refreshedContent === existingContent) {
270
+ return {
271
+ status: 'exists',
272
+ agentsPath
273
+ };
274
+ }
275
+ await writeFile(agentsPath, refreshedContent, 'utf8');
276
+ return {
277
+ status: 'refreshed',
254
278
  agentsPath
255
279
  };
256
280
  }
@@ -263,9 +287,11 @@ async function upsertProjectAgentGuidance({
263
287
  export async function installProjectAgentSkill({
264
288
  cwd,
265
289
  sourcePath,
290
+ refresh = false,
266
291
  access = fs.access,
267
292
  copy = fs.cp,
268
- mkdir = fs.mkdir
293
+ mkdir = fs.mkdir,
294
+ remove = fs.rm
269
295
  }) {
270
296
  let targetPath = getProjectAgentSkillPath(cwd);
271
297
  if (!(await fileExists(path.join(sourcePath, 'SKILL.md'), access))) {
@@ -275,7 +301,8 @@ export async function installProjectAgentSkill({
275
301
  targetPath
276
302
  };
277
303
  }
278
- if (await fileExists(targetPath, access)) {
304
+ let targetExists = await fileExists(targetPath, access);
305
+ if (targetExists && !refresh) {
279
306
  return {
280
307
  status: 'exists',
281
308
  sourcePath,
@@ -287,11 +314,16 @@ export async function installProjectAgentSkill({
287
314
  });
288
315
  await copy(sourcePath, targetPath, {
289
316
  recursive: true,
290
- errorOnExist: true,
291
- force: false
317
+ errorOnExist: !targetExists,
318
+ force: targetExists
292
319
  });
320
+ if (targetExists) {
321
+ await remove(path.join(targetPath, 'agents', 'openai.yaml'), {
322
+ force: true
323
+ });
324
+ }
293
325
  return {
294
- status: 'installed',
326
+ status: targetExists ? 'refreshed' : 'installed',
295
327
  sourcePath,
296
328
  targetPath
297
329
  };
@@ -305,6 +337,11 @@ function writeAgentSkillOutput(output, result) {
305
337
  output.hint(`Installed at ${result.targetPath}`);
306
338
  return;
307
339
  }
340
+ if (result.status === 'refreshed') {
341
+ output.complete('Refreshed Vizzly agent skill');
342
+ output.hint(`Updated ${result.targetPath}`);
343
+ return;
344
+ }
308
345
  if (result.status === 'exists') {
309
346
  output.hint('Vizzly agent skill already exists in this repo');
310
347
  return;
@@ -327,8 +364,18 @@ function writeAgentGuidanceOutput(output, result) {
327
364
  output.hint(`Updated ${result.agentsPath}`);
328
365
  return;
329
366
  }
367
+ if (result.status === 'refreshed') {
368
+ output.complete('Refreshed Vizzly guidance in AGENTS.md');
369
+ output.hint(`Updated ${result.agentsPath}`);
370
+ return;
371
+ }
330
372
  if (result.status === 'exists') {
331
373
  output.hint('Vizzly guidance already exists in AGENTS.md');
374
+ return;
375
+ }
376
+ if (result.status === 'malformed') {
377
+ output.warn('Vizzly guidance markers in AGENTS.md are incomplete');
378
+ output.hint(`Repair the managed block in ${result.agentsPath}`);
332
379
  }
333
380
  }
334
381
  function withAgentSetupResults(payload, {
@@ -381,7 +428,9 @@ export async function init(options = {}, deps = {}) {
381
428
  sourcePath: resolvedDeps.skillSourcePath,
382
429
  access: resolvedDeps.access,
383
430
  copy: resolvedDeps.copy,
384
- mkdir: resolvedDeps.mkdir
431
+ mkdir: resolvedDeps.mkdir,
432
+ refresh: options.agentGuidance || options.agentSkill,
433
+ remove: resolvedDeps.remove
385
434
  });
386
435
  }
387
436
  if (options.agentGuidance) {