@vibe-validate/cli 0.17.6 → 0.18.0-rc.1

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 (81) hide show
  1. package/dist/bin/vibe-validate +4 -0
  2. package/dist/bin/vibe-validate.js +4 -0
  3. package/dist/bin/vibe-validate.js.map +1 -1
  4. package/dist/bin/vv +4 -0
  5. package/dist/commands/cleanup.d.ts.map +1 -1
  6. package/dist/commands/cleanup.js +5 -2
  7. package/dist/commands/cleanup.js.map +1 -1
  8. package/dist/commands/config.d.ts.map +1 -1
  9. package/dist/commands/config.js +3 -1
  10. package/dist/commands/config.js.map +1 -1
  11. package/dist/commands/create-extractor.d.ts.map +1 -1
  12. package/dist/commands/create-extractor.js +3 -1
  13. package/dist/commands/create-extractor.js.map +1 -1
  14. package/dist/commands/doctor.d.ts.map +1 -1
  15. package/dist/commands/doctor.js +11 -5
  16. package/dist/commands/doctor.js.map +1 -1
  17. package/dist/commands/generate-workflow.d.ts.map +1 -1
  18. package/dist/commands/generate-workflow.js +6 -2
  19. package/dist/commands/generate-workflow.js.map +1 -1
  20. package/dist/commands/history.d.ts.map +1 -1
  21. package/dist/commands/history.js +19 -9
  22. package/dist/commands/history.js.map +1 -1
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +3 -1
  25. package/dist/commands/init.js.map +1 -1
  26. package/dist/commands/pre-commit.d.ts.map +1 -1
  27. package/dist/commands/pre-commit.js +3 -1
  28. package/dist/commands/pre-commit.js.map +1 -1
  29. package/dist/commands/run.d.ts.map +1 -1
  30. package/dist/commands/run.js +11 -9
  31. package/dist/commands/run.js.map +1 -1
  32. package/dist/commands/state.d.ts.map +1 -1
  33. package/dist/commands/state.js +11 -6
  34. package/dist/commands/state.js.map +1 -1
  35. package/dist/commands/watch-pr.d.ts +16 -0
  36. package/dist/commands/watch-pr.d.ts.map +1 -1
  37. package/dist/commands/watch-pr.js +574 -320
  38. package/dist/commands/watch-pr.js.map +1 -1
  39. package/dist/schemas/watch-pr-result.schema.d.ts +2010 -0
  40. package/dist/schemas/watch-pr-result.schema.d.ts.map +1 -0
  41. package/dist/schemas/watch-pr-result.schema.js +335 -0
  42. package/dist/schemas/watch-pr-result.schema.js.map +1 -0
  43. package/dist/schemas/watch-pr-schema.d.ts +6 -6
  44. package/dist/services/cache-manager.d.ts +113 -0
  45. package/dist/services/cache-manager.d.ts.map +1 -0
  46. package/dist/services/cache-manager.js +211 -0
  47. package/dist/services/cache-manager.js.map +1 -0
  48. package/dist/services/ci-providers/github-actions.d.ts.map +1 -1
  49. package/dist/services/ci-providers/github-actions.js +10 -16
  50. package/dist/services/ci-providers/github-actions.js.map +1 -1
  51. package/dist/services/external-check-extractor.d.ts +66 -0
  52. package/dist/services/external-check-extractor.d.ts.map +1 -0
  53. package/dist/services/external-check-extractor.js +114 -0
  54. package/dist/services/external-check-extractor.js.map +1 -0
  55. package/dist/services/extraction-mode-detector.d.ts +85 -0
  56. package/dist/services/extraction-mode-detector.d.ts.map +1 -0
  57. package/dist/services/extraction-mode-detector.js +200 -0
  58. package/dist/services/extraction-mode-detector.js.map +1 -0
  59. package/dist/services/github-fetcher.d.ts +210 -0
  60. package/dist/services/github-fetcher.d.ts.map +1 -0
  61. package/dist/services/github-fetcher.js +412 -0
  62. package/dist/services/github-fetcher.js.map +1 -0
  63. package/dist/services/history-summary-builder.d.ts +74 -0
  64. package/dist/services/history-summary-builder.d.ts.map +1 -0
  65. package/dist/services/history-summary-builder.js +199 -0
  66. package/dist/services/history-summary-builder.js.map +1 -0
  67. package/dist/services/watch-pr-orchestrator.d.ts +119 -0
  68. package/dist/services/watch-pr-orchestrator.d.ts.map +1 -0
  69. package/dist/services/watch-pr-orchestrator.js +420 -0
  70. package/dist/services/watch-pr-orchestrator.js.map +1 -0
  71. package/dist/utils/command-name.d.ts +21 -0
  72. package/dist/utils/command-name.d.ts.map +1 -0
  73. package/dist/utils/command-name.js +45 -0
  74. package/dist/utils/command-name.js.map +1 -0
  75. package/dist/utils/pid-lock.d.ts.map +1 -1
  76. package/dist/utils/pid-lock.js +3 -3
  77. package/dist/utils/pid-lock.js.map +1 -1
  78. package/dist/utils/temp-files.d.ts.map +1 -1
  79. package/dist/utils/temp-files.js +2 -2
  80. package/dist/utils/temp-files.js.map +1 -1
  81. package/package.json +7 -7
@@ -1,33 +1,46 @@
1
+ import { getCurrentBranch, getCurrentPR, getRemoteUrl, listPullRequests } from '@vibe-validate/git';
1
2
  import { stringify as stringifyYaml } from 'yaml';
2
- import { CIProviderRegistry } from '../services/ci-provider-registry.js';
3
+ import { WatchPROrchestrator } from '../services/watch-pr-orchestrator.js';
4
+ import { getCommandName } from '../utils/command-name.js';
3
5
  /**
4
6
  * Register the watch-pr command
5
7
  */
6
8
  export function registerWatchPRCommand(program) {
7
9
  program
8
10
  .command('watch-pr [pr-number]')
9
- .description('Watch CI checks for a pull/merge request in real-time')
10
- .option('--provider <name>', 'Force specific CI provider (github-actions, gitlab-ci)')
11
- .option('--yaml', 'Output YAML only (no interactive display)')
12
- .option('--timeout <seconds>', 'Maximum time to wait in seconds (default: 3600)', '3600')
11
+ .description('Monitor PR checks with auto-polling, error extraction, and flaky test detection (use after creating PR, run after each push)')
12
+ .option('--yaml', 'Force YAML output (auto-enabled on failure)')
13
+ .option('--repo <owner/repo>', 'Repository (default: auto-detect from git remote)')
14
+ .option('--history', 'Show historical runs for the PR with pass/fail summary')
15
+ .option('--run-id <id>', 'Watch specific run ID instead of latest (useful for testing failed runs)')
16
+ .option('--timeout <seconds>', 'Maximum polling time in seconds (default: 1800 = 30 min)', '1800')
13
17
  .option('--poll-interval <seconds>', 'Polling frequency in seconds (default: 10)', '10')
14
- .option('--fail-fast', 'Exit immediately on first check failure')
18
+ .option('--fail-fast', 'Exit immediately on first check failure (no polling)')
15
19
  .action(async (prNumber, options) => {
16
20
  try {
17
21
  const exitCode = await watchPRCommand(prNumber, options);
18
22
  process.exit(exitCode);
19
23
  }
20
24
  catch (error) {
21
- if (options.yaml) {
22
- // YAML mode: Output error to stdout
25
+ // Only output YAML for PR failures, not for usage/argument errors
26
+ // Check if this is a usage error (no PR detected, invalid args, etc.)
27
+ const errorMessage = error instanceof Error ? error.message : String(error);
28
+ const isUsageError = errorMessage.includes('Could not auto-detect') ||
29
+ errorMessage.includes('Invalid PR number') ||
30
+ errorMessage.includes('Invalid run ID') ||
31
+ errorMessage.includes('Invalid --repo format') ||
32
+ errorMessage.includes('Could not detect repository');
33
+ if (isUsageError) {
34
+ // Output as plain text for better UX
35
+ process.stderr.write(`Error: ${errorMessage}\n`);
36
+ }
37
+ else {
38
+ // Actual PR/API errors - output as YAML for parseability
23
39
  process.stdout.write('---\n');
24
40
  process.stdout.write(stringifyYaml({
25
- error: error instanceof Error ? error.message : String(error),
41
+ error: errorMessage,
26
42
  }));
27
43
  }
28
- else {
29
- console.error('❌ Error:', error instanceof Error ? error.message : String(error));
30
- }
31
44
  process.exit(1);
32
45
  }
33
46
  });
@@ -35,295 +48,464 @@ export function registerWatchPRCommand(program) {
35
48
  /**
36
49
  * Execute watch-pr command
37
50
  *
38
- * @returns Exit code (0 = success, 1 = failure, 2 = timeout)
51
+ * @returns Exit code (0 = success, 1 = failure)
39
52
  */
40
- async function watchPRCommand(prNumber, options) {
41
- const registry = new CIProviderRegistry();
42
- // Auto-detect or use specified provider
43
- const provider = options.provider
44
- ? registry.getProvider(options.provider)
45
- : await registry.detectProvider();
46
- if (!provider) {
47
- const availableProviders = registry.getProviderNames().join(', ');
48
- throw new Error(`No supported CI provider detected. Available: ${availableProviders}\n` +
49
- 'GitHub Actions requires: gh CLI installed and github.com remote');
53
+ export async function watchPRCommand(prNumber, options) {
54
+ // Detect owner/repo from git remote or --repo flag (do this early for auto-detection)
55
+ const { owner, repo } = options.repo
56
+ ? parseRepoFlag(options.repo)
57
+ : detectOwnerRepo();
58
+ // Auto-detect PR number if not provided
59
+ let prNum;
60
+ if (prNumber) {
61
+ // Explicit PR number provided - parse and validate
62
+ prNum = Number.parseInt(prNumber, 10);
63
+ if (Number.isNaN(prNum) || prNum <= 0) {
64
+ throw new Error(`Invalid PR number: ${prNumber}`);
65
+ }
50
66
  }
51
- // If no PR number, try to detect from current branch
52
- let prId = prNumber;
53
- if (!prId) {
54
- const pr = await provider.detectPullRequest();
55
- if (!pr) {
56
- throw new Error('Could not detect PR from current branch.\n' +
57
- 'Usage: vibe-validate watch-pr <pr-number>');
67
+ else {
68
+ // No PR number - try to auto-detect from current branch
69
+ prNum = await autoDetectPR(owner, repo);
70
+ }
71
+ // Create orchestrator
72
+ const orchestrator = new WatchPROrchestrator(owner, repo);
73
+ // Handle --history flag (list historical runs)
74
+ if (options.history) {
75
+ await displayHistoricalRuns(orchestrator, prNum, options.yaml ?? false);
76
+ return 0; // Success exit code
77
+ }
78
+ // Handle --run-id mode (single fetch, no polling)
79
+ if (options.runId) {
80
+ const runId = Number.parseInt(options.runId, 10);
81
+ if (Number.isNaN(runId) || runId <= 0) {
82
+ throw new Error(`Invalid run ID: ${options.runId}. Must be a positive integer.`);
58
83
  }
59
- prId = pr.id.toString();
84
+ const result = await orchestrator.buildResultForRun(prNum, runId, { useCache: true });
85
+ return displayFinalResult(result, orchestrator, options.yaml ?? false);
60
86
  }
61
- // Watch the PR
62
- return await watchPR(provider, prId, options);
87
+ // Normal mode: poll until complete (or timeout/fail-fast)
88
+ return await pollUntilComplete(orchestrator, prNum, options);
63
89
  }
64
90
  /**
65
- * Handle timeout scenario
91
+ * Display final result and return exit code
92
+ *
93
+ * @param result - Result to display
94
+ * @param orchestrator - WatchPROrchestrator instance
95
+ * @param yaml - Force YAML output
96
+ * @returns Exit code (0 = passed, 1 = failed)
66
97
  */
67
- function handleTimeout(lastStatus, elapsed, yaml) {
68
- if (yaml && lastStatus) {
69
- const result = {
70
- pr: lastStatus.pr,
71
- status: 'timeout',
72
- result: 'unknown',
73
- duration: formatDuration(elapsed),
74
- summary: 'Timed out waiting for checks to complete',
75
- checks: lastStatus.checks.map((c) => ({
76
- name: c.name,
77
- status: c.status,
78
- conclusion: c.conclusion,
79
- duration: c.duration,
80
- url: c.url,
81
- })),
82
- };
83
- // YAML mode: Output timeout result to stdout
98
+ function displayFinalResult(result, orchestrator, yaml) {
99
+ const shouldYAML = orchestrator.shouldOutputYAML(result.status, yaml);
100
+ if (shouldYAML) {
84
101
  process.stdout.write('---\n');
85
102
  process.stdout.write(stringifyYaml(result));
86
103
  }
87
104
  else {
88
- console.log('\nâąī¸ Timeout reached. Checks still pending.');
105
+ displayHumanResult(result);
89
106
  }
90
- return 2;
107
+ return result.status === 'passed' ? 0 : 1;
91
108
  }
92
109
  /**
93
- * Watch PR until completion or timeout
110
+ * Check if polling has timed out
111
+ *
112
+ * @param startTime - Polling start time
113
+ * @param timeoutMs - Timeout in milliseconds
114
+ * @param previousResult - Last result (for displaying on timeout)
115
+ * @returns Exit code (2) if timeout, null otherwise
94
116
  */
95
- async function watchPR(provider, prId, options) {
96
- const timeoutMs = Number.parseInt(options.timeout ?? '3600') * 1000;
97
- const pollIntervalMs = Number.parseInt(options.pollInterval ?? '10') * 1000;
117
+ function checkTimeout(startTime, timeoutMs, previousResult) {
118
+ const elapsed = Date.now() - startTime;
119
+ if (elapsed >= timeoutMs) {
120
+ if (previousResult) {
121
+ process.stdout.write('\nâąī¸ Timeout reached. Checks still pending.\n\n');
122
+ process.stdout.write('---\n');
123
+ process.stdout.write(stringifyYaml(previousResult));
124
+ }
125
+ return 2;
126
+ }
127
+ return null;
128
+ }
129
+ /**
130
+ * Check exit conditions (fail-fast or completion)
131
+ *
132
+ * @param result - Current result
133
+ * @param orchestrator - WatchPROrchestrator instance
134
+ * @param options - Command options
135
+ * @returns Exit code if should exit, null to continue polling
136
+ */
137
+ function checkExitConditions(result, orchestrator, options) {
138
+ // Check if should fail fast
139
+ if (options.failFast && result.status === 'failed') {
140
+ process.stdout.write('\n⚡ Failing fast (--fail-fast enabled)\n\n');
141
+ process.stdout.write('---\n');
142
+ process.stdout.write(stringifyYaml(result));
143
+ return 1;
144
+ }
145
+ // Check if all checks are complete
146
+ if (result.status !== 'pending') {
147
+ process.stdout.write('\n');
148
+ return displayFinalResult(result, orchestrator, options.yaml ?? false);
149
+ }
150
+ return null; // Continue polling
151
+ }
152
+ /**
153
+ * Poll until PR checks complete (or timeout/fail-fast)
154
+ *
155
+ * @param orchestrator - WatchPROrchestrator instance
156
+ * @param prNumber - PR number
157
+ * @param options - Command options
158
+ * @returns Exit code
159
+ */
160
+ async function pollUntilComplete(orchestrator, prNumber, options) {
161
+ const timeoutMs = Number.parseInt(options.timeout ?? '1800', 10) * 1000;
162
+ const pollIntervalMs = Number.parseInt(options.pollInterval ?? '10', 10) * 1000;
98
163
  const startTime = Date.now();
99
- let lastStatus = null;
164
+ let previousResult = null;
100
165
  let iteration = 0;
101
166
  while (true) {
102
- const elapsed = Date.now() - startTime;
103
167
  // Check timeout
104
- if (elapsed >= timeoutMs) {
105
- return handleTimeout(lastStatus, elapsed, options.yaml ?? false);
168
+ const timeoutCode = checkTimeout(startTime, timeoutMs, previousResult);
169
+ if (timeoutCode !== null) {
170
+ return timeoutCode;
106
171
  }
107
172
  // Fetch current status
108
- const status = await provider.fetchCheckStatus(prId);
109
- lastStatus = status;
110
- // Display current status
111
- if (!options.yaml) {
112
- displayHumanStatus(status, iteration === 0);
173
+ const result = await orchestrator.buildResult(prNumber);
174
+ // Display changes (first iteration shows everything)
175
+ const changes = detectChanges(previousResult, result);
176
+ if (iteration === 0 || changes.length > 0) {
177
+ displayChanges(changes, result, iteration === 0);
113
178
  }
114
- // Check if we should fail fast
115
- const shouldFailFast = options.failFast && status.checks.some((c) => c.conclusion === 'failure');
116
- if (shouldFailFast) {
117
- return await handleCompletion(provider, status, options, elapsed);
118
- }
119
- // Check if all checks are complete
120
- if (status.status === 'completed') {
121
- return await handleCompletion(provider, status, options, elapsed);
122
- }
123
- // Wait before next poll
179
+ previousResult = result;
124
180
  iteration++;
181
+ // Check exit conditions (fail-fast or completion)
182
+ const exitCode = checkExitConditions(result, orchestrator, options);
183
+ if (exitCode !== null) {
184
+ return exitCode;
185
+ }
186
+ // Sleep before next poll
125
187
  await sleep(pollIntervalMs);
126
188
  }
127
189
  }
128
190
  /**
129
- * Handle completion - fetch failure details if needed and output result
191
+ * Auto-detect PR number from current branch
192
+ *
193
+ * Tries two approaches:
194
+ * 1. Use `gh pr view` (fast, but can fail in some scenarios)
195
+ * 2. Fall back to branch name matching (more reliable)
196
+ *
197
+ * @param owner - Repository owner
198
+ * @param repo - Repository name
199
+ * @returns PR number
130
200
  */
131
- async function handleCompletion(provider, status, options, elapsedMs) {
132
- const failures = status.checks.filter((c) => c.conclusion === 'failure');
133
- // Fetch detailed failure information
134
- const failureDetails = await Promise.all(failures.map(async (check) => {
201
+ async function autoDetectPR(owner, repo) {
202
+ try {
203
+ // Approach 1: Try gh pr view (fast path)
204
+ // Uses getCurrentPR from @vibe-validate/git (architectural compliance)
205
+ const prNumber = getCurrentPR(owner, repo);
206
+ if (prNumber !== null) {
207
+ return prNumber;
208
+ }
209
+ throw new Error('No PR number found');
210
+ }
211
+ catch {
212
+ // Approach 2: Fall back to branch name matching (Issue #5 fix)
213
+ // This handles cases where gh pr view fails (detached HEAD, etc.)
135
214
  try {
136
- const logs = await provider.fetchFailureLogs(check.id);
137
- return {
138
- name: check.name,
139
- checkId: check.id,
140
- errorSummary: logs.errorSummary,
141
- validationResult: logs.validationResult,
142
- nextSteps: generateNextSteps(check.id, logs.validationResult),
143
- };
215
+ // Get current branch name using getCurrentBranch from @vibe-validate/git
216
+ const currentBranch = getCurrentBranch();
217
+ if (!currentBranch || currentBranch === 'HEAD') {
218
+ throw new Error('Not on a branch (detached HEAD?)');
219
+ }
220
+ // Get all open PRs with branch names
221
+ const prsData = listPullRequests(owner, repo, 20, ['number', 'headRefName']);
222
+ // Find PR matching current branch
223
+ const matchingPR = prsData.find(pr => pr.headRefName === currentBranch);
224
+ if (matchingPR) {
225
+ return matchingPR.number;
226
+ }
227
+ throw new Error(`No PR found for branch: ${currentBranch}`);
144
228
  }
145
- catch (error) {
146
- return {
147
- name: check.name,
148
- checkId: check.id,
149
- errorSummary: `Failed to fetch logs: ${error}`,
150
- nextSteps: [`gh run view ${check.id} --log-failed`],
151
- };
229
+ catch {
230
+ // Both approaches failed - show suggestions
231
+ const suggestions = await suggestOpenPRs(owner, repo);
232
+ const cmd = getCommandName();
233
+ throw new Error(`Could not auto-detect PR from current branch.\n\n` +
234
+ `${suggestions}\n\n` +
235
+ `Usage: ${cmd} watch-pr <pr-number>\n` +
236
+ `Example: ${cmd} watch-pr 90`);
152
237
  }
153
- }));
154
- // Output final result
155
- if (options.yaml) {
156
- const result = {
157
- pr: {
158
- id: status.pr.id,
159
- title: status.pr.title,
160
- url: status.pr.url,
161
- },
162
- status: status.status,
163
- result: status.result,
164
- duration: formatDuration(elapsedMs),
165
- summary: `${status.checks.filter((c) => c.conclusion === 'success').length}/${status.checks.length} checks passed`,
166
- checks: status.checks.map((c) => ({
167
- name: c.name,
168
- status: c.status,
169
- conclusion: c.conclusion,
170
- duration: c.duration,
171
- url: c.url,
172
- })),
173
- failures: failureDetails.length > 0 ? failureDetails : undefined,
174
- };
175
- // YAML mode: Output completion result to stdout
176
- process.stdout.write('---\n');
177
- process.stdout.write(stringifyYaml(result));
178
238
  }
179
- else {
180
- displayHumanCompletion(status, failureDetails, elapsedMs);
239
+ }
240
+ /**
241
+ * Suggest open PRs to help user choose
242
+ *
243
+ * @param owner - Repository owner
244
+ * @param repo - Repository name
245
+ * @returns Formatted suggestion text
246
+ */
247
+ async function suggestOpenPRs(owner, repo) {
248
+ try {
249
+ const prsData = listPullRequests(owner, repo, 5, ['number', 'title', 'author', 'headRefName']);
250
+ if (prsData.length === 0) {
251
+ return 'No open PRs found in this repository.';
252
+ }
253
+ const prList = prsData
254
+ .map((pr) => ` #${pr.number} - ${pr.title}\n` +
255
+ ` (${pr.headRefName} by ${pr.author.login})`)
256
+ .join('\n');
257
+ return `Open PRs in ${owner}/${repo}:\n${prList}`;
258
+ }
259
+ catch {
260
+ return 'Could not fetch open PRs.';
181
261
  }
182
- // Return appropriate exit code
183
- return status.result === 'success' ? 0 : 1;
184
262
  }
185
263
  /**
186
- * Display human-friendly status update
264
+ * Detect owner/repo from git remote
265
+ *
266
+ * @returns Owner and repo from GitHub remote
187
267
  */
188
- function displayHumanStatus(status, isFirst) {
189
- if (isFirst) {
190
- console.log(`🔍 Watching PR #${status.pr.id}: ${status.pr.title}`);
191
- console.log(` ${status.pr.url}\n`);
268
+ function detectOwnerRepo() {
269
+ try {
270
+ // Use getRemoteUrl from @vibe-validate/git (architectural compliance)
271
+ const remote = getRemoteUrl('origin');
272
+ // Parse GitHub URL (supports HTTPS, SSH, and SSH with custom host aliases)
273
+ // HTTPS: https://github.com/owner/repo.git
274
+ // SSH: git@github.com:owner/repo.git
275
+ // SSH with alias: git@github.com-personal:owner/repo.git
276
+ // SSH with alias: git@github.com-work:owner/repo.git
277
+ const regex = /github\.com[^/:]*[/:]([\w-]+)\/([\w-]+)/;
278
+ const match = regex.exec(remote);
279
+ if (!match) {
280
+ throw new Error('Could not parse GitHub owner/repo from remote URL');
281
+ }
282
+ const owner = match[1];
283
+ const repo = match[2].replace(/\.git$/, '');
284
+ return { owner, repo };
192
285
  }
193
- // Clear previous output (for live updating)
194
- if (!isFirst) {
195
- process.stdout.write('\x1B[2J\x1B[H'); // Clear screen and move cursor to top
196
- console.log(`🔍 Watching PR #${status.pr.id}: ${status.pr.title}\n`);
286
+ catch {
287
+ // Error occurred - could not detect repo from git remote
288
+ throw new Error('Could not detect repository from git remote.\n' +
289
+ 'Ensure you are in a git repository with a GitHub remote.\n' +
290
+ 'Or specify --repo <owner/repo> explicitly.');
197
291
  }
198
- // Display checks
199
- for (const check of status.checks) {
200
- const icon = getCheckIcon(check);
201
- const statusStr = check.conclusion ?? check.status;
202
- const duration = check.duration ?? '';
203
- console.log(`${icon} ${check.name.padEnd(40)} ${statusStr.padEnd(12)} ${duration}`);
292
+ }
293
+ /**
294
+ * Parse --repo flag (owner/repo format)
295
+ *
296
+ * @param repoFlag - Repository in owner/repo format
297
+ * @returns Owner and repo
298
+ */
299
+ function parseRepoFlag(repoFlag) {
300
+ const parts = repoFlag.split('/');
301
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
302
+ throw new Error(`Invalid --repo format: ${repoFlag}\n` +
303
+ 'Expected format: --repo owner/repo\n' +
304
+ 'Example: --repo jdutton/vibe-validate');
204
305
  }
205
- // Display summary
206
- const completed = status.checks.filter((c) => c.status === 'completed').length;
207
- const total = status.checks.length;
208
- const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
209
- console.log(`\n${completed}/${total} checks complete (${percentage}%)`);
306
+ return { owner: parts[0], repo: parts[1] };
210
307
  }
211
308
  /**
212
- * Display human-friendly completion message
309
+ * Display historical runs for a PR
310
+ *
311
+ * @param orchestrator - WatchPROrchestrator instance
312
+ * @param prNumber - PR number
313
+ * @param yaml - Output in YAML format
213
314
  */
214
- // eslint-disable-next-line sonarjs/cognitive-complexity -- Complex display logic, documented technical debt
215
- function displayHumanCompletion(status, failures, elapsedMs) {
216
- console.log('\n' + '='.repeat(60));
217
- if (status.result === 'success') {
218
- console.log('✅ All checks passed!');
219
- console.log(` Duration: ${formatDuration(elapsedMs)}`);
220
- console.log(` Ready to merge: ${status.pr.url}`);
315
+ async function displayHistoricalRuns(orchestrator, prNumber, yaml) {
316
+ const runs = await orchestrator.fetchRunsForPR(prNumber);
317
+ if (runs.length === 0) {
318
+ console.log(`No workflow runs found for PR #${prNumber}`);
319
+ return;
320
+ }
321
+ if (yaml) {
322
+ // YAML output
323
+ process.stdout.write('---\n');
324
+ process.stdout.write(stringifyYaml({ runs }));
221
325
  }
222
326
  else {
223
- console.log('❌ Some checks failed');
224
- console.log(` Duration: ${formatDuration(elapsedMs)}`);
225
- for (const failure of failures) {
226
- console.log(`\n📋 ${failure.name}:`);
227
- if (failure.validationResult) {
228
- const validationResult = failure.validationResult;
229
- console.log(` Failed step: ${validationResult.failedStep}`);
230
- // Find the failed step to get command and extraction data
231
- const failedStep = validationResult.phases
232
- ?.flatMap(phase => phase.steps ?? [])
233
- .find(step => step && step.name === validationResult.failedStep);
234
- if (failedStep?.command) {
235
- console.log(` Re-run locally: ${failedStep.command}`);
236
- }
237
- // Show extracted errors from failed step (v0.15.0+)
238
- if (failedStep?.extraction?.errors && failedStep.extraction.errors.length > 0) {
239
- console.log(`\n Failed tests/errors:`);
240
- for (const error of failedStep.extraction.errors.slice(0, 10)) {
241
- let location = 'Unknown location';
242
- if (error.file) {
243
- if (error.line) {
244
- const columnPart = error.column ? `:${error.column}` : '';
245
- location = `${error.file}:${error.line}${columnPart}`;
246
- }
247
- else {
248
- location = error.file;
249
- }
250
- }
251
- const message = error.message ?? 'Error';
252
- console.log(` ❌ ${location} - ${message}`);
253
- }
254
- if (failedStep.extraction.errors.length > 10) {
255
- console.log(` ... and ${failedStep.extraction.errors.length - 10} more`);
256
- }
257
- }
258
- else if (failedStep?.extraction?.summary) {
259
- // Show summary if no specific errors extracted
260
- console.log(`\n Error summary: ${failedStep.extraction.summary}`);
261
- }
327
+ // Human-friendly table
328
+ console.log(`\n📋 Workflow Runs for PR #${prNumber}\n`);
329
+ console.log(' RUN ID CONCLUSION DURATION WORKFLOW STARTED');
330
+ console.log(' ' + '─'.repeat(95));
331
+ for (const run of runs) {
332
+ const runId = run.run_id.toString().padEnd(12);
333
+ const conclusion = (run.conclusion ?? 'pending').padEnd(11);
334
+ const duration = (run.duration ?? '?').padEnd(9);
335
+ const workflow = run.workflow_name.slice(0, 29).padEnd(29);
336
+ const startedAt = new Date(run.started_at).toLocaleString();
337
+ // Color code by conclusion
338
+ let icon = 'âŗ'; // pending
339
+ if (run.conclusion === 'success') {
340
+ icon = '✅';
262
341
  }
263
- else if (failure.errorSummary) {
264
- console.log(` ${failure.errorSummary}`);
342
+ else if (run.conclusion === 'failure') {
343
+ icon = '❌';
344
+ }
345
+ console.log(`${icon} ${runId} ${conclusion} ${duration} ${workflow} ${startedAt}`);
346
+ }
347
+ console.log('\n💡 Tip: Use --run-id <id> to drill into a specific run for extraction testing');
348
+ const cmd = getCommandName();
349
+ console.log(` Example: ${cmd} watch-pr ${prNumber} --run-id ${runs[0].run_id}`);
350
+ }
351
+ }
352
+ /**
353
+ * Display human-friendly result
354
+ *
355
+ * @param result - WatchPRResult
356
+ */
357
+ function displayHumanResult(result) {
358
+ console.log(`\n🔍 PR #${result.pr.number}: ${result.pr.title}`);
359
+ console.log(` ${result.pr.url}\n`);
360
+ // Display checks
361
+ const allChecks = [...result.checks.github_actions, ...result.checks.external_checks];
362
+ for (const check of allChecks) {
363
+ const icon = getCheckIcon(check.conclusion);
364
+ const statusStr = check.conclusion ?? check.status;
365
+ console.log(`${icon} ${check.name.padEnd(40)} ${statusStr}`);
366
+ }
367
+ // Display summary
368
+ const failedSuffix = result.checks.failed > 0 ? ` (${result.checks.failed} failed)` : '';
369
+ console.log(`\n${result.checks.passed}/${result.checks.total} checks passed${failedSuffix}`);
370
+ // Display guidance
371
+ if (result.guidance) {
372
+ console.log(`\n${result.guidance.summary}`);
373
+ if (result.guidance.next_steps) {
374
+ console.log('\nNext steps:');
375
+ for (const step of result.guidance.next_steps) {
376
+ const icon = getStepIcon(step.severity);
377
+ console.log(`${icon} ${step.action}`);
378
+ if (step.reason) {
379
+ console.log(` ${step.reason}`);
380
+ }
265
381
  }
266
- console.log(`\n Next steps:`);
267
- for (const step of failure.nextSteps)
268
- console.log(` - ${step}`);
269
382
  }
270
- // Suggest reporting extractor issues if extraction quality is poor
271
- console.log('\n💡 Error output unclear or missing details?');
272
- console.log(' Help improve extraction: https://github.com/jdutton/vibe-validate/issues/new?template=extractor-improvement.yml');
273
383
  }
274
- console.log('='.repeat(60));
275
384
  }
276
385
  /**
277
- * Get icon for check status
386
+ * Get icon for step severity
387
+ *
388
+ * @param severity - Step severity
389
+ * @returns Icon emoji
390
+ */
391
+ function getStepIcon(severity) {
392
+ if (severity === 'error')
393
+ return '❌';
394
+ if (severity === 'warning')
395
+ return 'âš ī¸';
396
+ return 'â„šī¸';
397
+ }
398
+ /**
399
+ * Get icon for check conclusion
400
+ *
401
+ * @param conclusion - Check conclusion
402
+ * @returns Icon emoji
278
403
  */
279
- function getCheckIcon(check) {
280
- if (check.conclusion === 'success')
404
+ function getCheckIcon(conclusion) {
405
+ if (conclusion === 'success')
281
406
  return '✅';
282
- if (check.conclusion === 'failure')
407
+ if (conclusion === 'failure')
283
408
  return '❌';
284
- if (check.conclusion === 'cancelled')
409
+ if (conclusion === 'neutral')
410
+ return 'â„šī¸';
411
+ if (conclusion === 'cancelled')
285
412
  return 'đŸšĢ';
286
- if (check.conclusion === 'skipped')
287
- return 'â­ī¸ ';
288
- if (check.status === 'in_progress')
289
- return 'âŗ';
290
- return 'â¸ī¸ ';
413
+ if (conclusion === 'skipped')
414
+ return 'â­ī¸';
415
+ if (conclusion === 'timed_out')
416
+ return 'âąī¸';
417
+ if (conclusion === 'action_required')
418
+ return 'âš ī¸';
419
+ return 'â¸ī¸';
291
420
  }
292
421
  /**
293
- * Generate next steps for a failure
422
+ * Sleep for specified milliseconds
423
+ *
424
+ * @param ms - Milliseconds to sleep
294
425
  */
295
- function generateNextSteps(checkId, validationResult) {
296
- const steps = [];
297
- // Find the failed step's command (v0.15.0+: rerunCommand removed, use step.command)
298
- if (validationResult) {
299
- const failedStep = validationResult.phases
300
- ?.flatMap(phase => phase.steps ?? [])
301
- .find(step => step && step.name === validationResult.failedStep);
302
- if (failedStep?.command) {
303
- steps.push(`Run locally: ${failedStep.command}`);
304
- }
305
- }
306
- steps.push(`View logs: gh run view ${checkId} --log-failed`);
307
- steps.push(`Re-run check: gh run rerun ${checkId} --failed`);
308
- return steps;
426
+ function sleep(ms) {
427
+ return new Promise((resolve) => setTimeout(resolve, ms));
309
428
  }
310
429
  /**
311
- * Format duration in human-readable format
430
+ * Detect changes between two results
431
+ *
432
+ * @param previous - Previous result (null on first iteration)
433
+ * @param current - Current result
434
+ * @returns Array of check changes
312
435
  */
313
- function formatDuration(ms) {
314
- const seconds = Math.floor(ms / 1000);
315
- const minutes = Math.floor(seconds / 60);
316
- const remainingSeconds = seconds % 60;
317
- if (minutes > 0) {
318
- return `${minutes}m ${remainingSeconds}s`;
436
+ function detectChanges(previous, current) {
437
+ if (!previous) {
438
+ // First iteration - everything is a change
439
+ return [
440
+ ...current.checks.github_actions.map((c) => ({
441
+ name: c.name,
442
+ previousStatus: null,
443
+ newStatus: c.status,
444
+ conclusion: c.conclusion,
445
+ })),
446
+ ...current.checks.external_checks.map((c) => ({
447
+ name: c.name,
448
+ previousStatus: null,
449
+ newStatus: c.status,
450
+ conclusion: c.conclusion,
451
+ })),
452
+ ];
319
453
  }
320
- return `${seconds}s`;
454
+ const changes = [];
455
+ // Build map of previous checks
456
+ const previousChecks = new Map();
457
+ for (const check of [...previous.checks.github_actions, ...previous.checks.external_checks]) {
458
+ previousChecks.set(check.name, { status: check.status, conclusion: check.conclusion });
459
+ }
460
+ // Compare current checks to previous
461
+ for (const check of [...current.checks.github_actions, ...current.checks.external_checks]) {
462
+ const prev = previousChecks.get(check.name);
463
+ if (!prev || prev.status !== check.status || prev.conclusion !== check.conclusion) {
464
+ changes.push({
465
+ name: check.name,
466
+ previousStatus: prev?.status ?? null,
467
+ newStatus: check.status,
468
+ conclusion: check.conclusion,
469
+ });
470
+ }
471
+ }
472
+ return changes;
321
473
  }
322
474
  /**
323
- * Sleep for specified milliseconds
475
+ * Display changes incrementally during polling
476
+ *
477
+ * @param changes - Array of check changes
478
+ * @param result - Current result
479
+ * @param isFirstIteration - Whether this is the first poll
324
480
  */
325
- function sleep(ms) {
326
- return new Promise((resolve) => setTimeout(resolve, ms));
481
+ function displayChanges(changes, result, isFirstIteration) {
482
+ if (isFirstIteration) {
483
+ // First iteration - show PR info and all checks
484
+ process.stdout.write(`\n🔍 Monitoring PR #${result.pr.number}: ${result.pr.title}\n`);
485
+ process.stdout.write(` ${result.pr.url}\n\n`);
486
+ }
487
+ // Display each change
488
+ for (const change of changes) {
489
+ const icon = getCheckIcon(change.conclusion);
490
+ const statusStr = change.conclusion ?? change.newStatus;
491
+ if (change.previousStatus === null) {
492
+ // New check started
493
+ process.stdout.write(`${icon} ${change.name} - ${statusStr}\n`);
494
+ }
495
+ else if (change.newStatus === 'completed') {
496
+ // Check completed
497
+ process.stdout.write(`${icon} ${change.name} - ${statusStr}\n`);
498
+ }
499
+ else {
500
+ // Status changed
501
+ process.stdout.write(`${icon} ${change.name} - ${change.previousStatus} → ${statusStr}\n`);
502
+ }
503
+ }
504
+ // Show progress summary
505
+ const { passed, failed, pending } = result.checks;
506
+ if (changes.length > 0) {
507
+ process.stdout.write(`\nâ¸ī¸ ${pending} running, ${passed} passed, ${failed} failed\n`);
508
+ }
327
509
  }
328
510
  /**
329
511
  * Show verbose help with detailed documentation
@@ -331,142 +513,214 @@ function sleep(ms) {
331
513
  export function showWatchPRVerboseHelp() {
332
514
  console.log(`# watch-pr Command Reference
333
515
 
334
- > Watch CI checks for a pull/merge request in real-time
516
+ > Monitor PR checks with auto-polling, error extraction, and flaky test detection
335
517
 
336
518
  ## Overview
337
519
 
338
- The \`watch-pr\` command monitors CI provider (GitHub Actions) check status in real-time after pushing to a PR. It provides live progress updates, extracts validation results from CI logs on failure, and provides actionable recovery commands. This is especially useful for AI agents waiting for CI completion.
520
+ The \`watch-pr\` command monitors pull request CI checks with **automatic polling** until completion. It provides:
521
+ - **Auto-polling**: Waits for checks to complete (no manual refresh)
522
+ - **Error extraction**: Extracts file:line:message from failed GitHub Actions logs
523
+ - **Flaky test detection**: Tracks history to identify unstable tests (e.g., "Failed last 2 runs", 60% success rate)
524
+ - **External check summaries**: codecov coverage %, SonarCloud quality gates
525
+ - **PR metadata**: branch, labels, linked issues, mergeable state
526
+ - **File change context**: What files changed, insertions/deletions
527
+ - **Intelligent guidance**: Severity-based next steps
528
+
529
+ **YAML output is auto-enabled on failure** (consistent with validate command).
530
+
531
+ ## When to Use
532
+
533
+ Use \`watch-pr\` after creating a PR and after each push:
534
+
535
+ \`\`\`bash
536
+ # Workflow:
537
+ git push # Push commits to PR branch
538
+ vibe-validate watch-pr 90 # Monitor CI (auto-polls until complete)
539
+ # → Returns structured result when checks finish
540
+ # → YAML output if failed (with extracted errors)
541
+ # → Text output if passed (human-friendly)
542
+ \`\`\`
543
+
544
+ **Note**: PR must exist on GitHub (watch-pr fetches data from GitHub API).
339
545
 
340
546
  ## How It Works
341
547
 
342
- 1. Detects PR from current branch (or uses provided PR number)
343
- 2. Polls CI provider (GitHub Actions) for check status
344
- 3. Shows real-time progress of all matrix jobs
345
- 4. On failure: fetches logs and extracts vibe-validate state file
346
- 5. Provides actionable recovery commands
347
- 6. Exits when all checks complete or timeout reached
548
+ 1. **Fetches PR metadata** and check results from GitHub
549
+ 2. **Auto-polls** until checks complete (no manual refresh needed)
550
+ 3. **Classifies checks** (GitHub Actions vs external)
551
+ 4. **Extracts errors** from failed GitHub Actions logs (matrix + non-matrix mode)
552
+ 5. **Extracts summaries** from external checks (codecov, SonarCloud)
553
+ 6. **Builds history** (last 10 runs, success rate, patterns like "flaky")
554
+ 7. **Outputs YAML on failure**, text on success (unless --yaml forced)
348
555
 
349
556
  ## Options
350
557
 
351
- - \`--provider <name>\` - Force specific CI provider (github-actions, gitlab-ci)
352
- - \`--yaml\` - Output YAML only (no interactive display)
353
- - \`--timeout <seconds>\` - Maximum time to wait in seconds (default: 3600)
354
- - \`--poll-interval <seconds>\` - Polling frequency in seconds (default: 10)
355
- - \`--fail-fast\` - Exit immediately on first check failure
558
+ - \`--yaml\` - Force YAML output (auto-enabled on failure)
559
+ - \`--repo <owner/repo>\` - Repository (default: auto-detect from git remote)
560
+ - \`--run-id <id>\` - Watch specific run ID instead of latest (useful for testing failed runs)
356
561
 
357
562
  ## Exit Codes
358
563
 
359
564
  - \`0\` - All checks passed
360
565
  - \`1\` - One or more checks failed
361
- - \`2\` - Timeout reached before completion
362
566
 
363
567
  ## Examples
364
568
 
365
569
  \`\`\`bash
366
- # Push to PR and watch
367
- git push origin my-branch
368
- vibe-validate watch-pr # Auto-detect PR
369
-
370
- # Watch specific PR
371
- vibe-validate watch-pr 42
570
+ # Watch PR (auto-detect repo from git remote)
571
+ vibe-validate watch-pr 90
372
572
 
373
- # YAML output only
374
- vibe-validate watch-pr --yaml
573
+ # Force YAML output (even on success)
574
+ vibe-validate watch-pr 90 --yaml
375
575
 
376
- # Exit on first failure
377
- vibe-validate watch-pr --fail-fast
576
+ # Watch PR in different repo
577
+ vibe-validate watch-pr 42 --repo jdutton/vibe-validate
378
578
 
379
- # Custom timeout (10 minutes)
380
- vibe-validate watch-pr --timeout 600
579
+ # Watch specific failed run (useful for testing extraction with failures)
580
+ vibe-validate watch-pr 104 --run-id 19754182675 --repo jdutton/mcp-typescript-simple --yaml
381
581
  \`\`\`
382
582
 
383
- ## Common Workflows
583
+ ## Output Format
584
+
585
+ ### YAML (auto on failure, or with --yaml)
586
+
587
+ \`\`\`yaml
588
+ pr:
589
+ number: 90
590
+ title: "Enhancement: Add watch-pr improvements"
591
+ branch: "feature/watch-pr"
592
+ mergeable: true
593
+ labels: ["enhancement"]
594
+ linked_issues:
595
+ - number: 42
596
+ title: "Improve watch-pr"
597
+
598
+ status: failed
599
+
600
+ checks:
601
+ total: 3
602
+ passed: 2
603
+ failed: 1
604
+ history_summary:
605
+ total_runs: 5
606
+ recent_pattern: "Failed last 2 runs"
607
+ success_rate: "60%"
608
+
609
+ github_actions:
610
+ - name: "Test"
611
+ conclusion: failure
612
+ run_id: 123
613
+ extraction:
614
+ errors:
615
+ - file: "test.ts"
616
+ line: 42
617
+ message: "Expected success"
618
+ summary: "1 test failure"
619
+ totalErrors: 1
620
+
621
+ external_checks:
622
+ - name: "codecov/patch"
623
+ conclusion: success
624
+ extracted:
625
+ summary: "Coverage: 85%"
626
+
627
+ guidance:
628
+ status: failed
629
+ severity: error
630
+ summary: "1 check(s) failed"
631
+ next_steps:
632
+ - action: "Fix Test failure"
633
+ url: "https://github.com/.../runs/123"
634
+ severity: error
635
+ \`\`\`
384
636
 
385
- ### Standard workflow after pushing PR
637
+ ### Text (on success, unless --yaml)
386
638
 
387
- \`\`\`bash
388
- # Push changes
389
- git push origin feature/my-work
639
+ \`\`\`
640
+ 🔍 PR #90: Enhancement: Add watch-pr improvements
641
+ https://github.com/jdutton/vibe-validate/pull/90
390
642
 
391
- # Create PR (if not exists)
392
- gh pr create
643
+ ✅ Test success
644
+ ✅ Lint success
645
+ ✅ codecov/patch success
393
646
 
394
- # Watch CI checks
395
- vibe-validate watch-pr
647
+ 3/3 checks passed
396
648
 
397
- # If passes: merge
398
- gh pr merge
649
+ All checks passed
399
650
 
400
- # Cleanup
401
- vibe-validate cleanup
651
+ Next steps:
652
+ â„šī¸ Ready to merge
402
653
  \`\`\`
403
654
 
404
- ### CI dogfooding workflow (for vibe-validate developers)
655
+ ## Extraction Modes
405
656
 
406
- \`\`\`bash
407
- # Push changes
408
- git push origin my-branch
409
-
410
- # Watch with fail-fast (exit on first failure for quick feedback)
411
- vibe-validate watch-pr --fail-fast
657
+ ### Matrix Mode (vibe-validate repos)
412
658
 
413
- # If fails: view validation result from YAML
414
- vibe-validate watch-pr 42 --yaml | yq '.failures[0].validationResult'
659
+ If check uses \`vv run\` or \`vv validate\`, YAML output is parsed and extraction passed through:
415
660
 
416
- # Fix errors and re-run
417
- gh run rerun <run-id> --failed
661
+ \`\`\`yaml
662
+ extraction:
663
+ errors:
664
+ - file: "test.ts"
665
+ line: 42
666
+ summary: "1 test failure"
667
+ totalErrors: 1
418
668
  \`\`\`
419
669
 
420
- ### AI agent workflow
421
-
422
- \`\`\`bash
423
- # AI agent pushes code
424
- git push origin feature/ai-generated
425
-
426
- # AI agent watches PR (YAML mode for parsing)
427
- vibe-validate watch-pr --yaml --timeout 600
428
-
429
- # AI agent parses YAML result
430
- # - If passed: proceed with merge
431
- # - If failed: extract error details and fix
670
+ ### Non-Matrix Mode (other repos)
671
+
672
+ For raw test output, extractors detect tool (vitest, jest, eslint) and extract errors:
673
+
674
+ \`\`\`yaml
675
+ extraction:
676
+ errors:
677
+ - file: "test.integration.test.ts"
678
+ line: 10
679
+ message: "Connection refused"
680
+ summary: "2 test failures"
681
+ totalErrors: 2
682
+ metadata:
683
+ detection:
684
+ extractor: vitest
432
685
  \`\`\`
433
686
 
434
- ## Error Recovery
687
+ ## Common Workflows
435
688
 
436
- ### If check fails
689
+ ### Standard PR workflow
437
690
 
438
- **View validation result from YAML output:**
439
691
  \`\`\`bash
440
- # View validation result from YAML output
441
- vibe-validate watch-pr 42 --yaml | yq '.failures[0].validationResult'
692
+ # Check PR status
693
+ vibe-validate watch-pr 90
694
+
695
+ # If failed, view extraction
696
+ vibe-validate watch-pr 90 --yaml | yq '.checks.github_actions[0].extraction'
442
697
 
443
698
  # Re-run failed check
444
699
  gh run rerun <run-id> --failed
445
700
  \`\`\`
446
701
 
447
- ### If no PR found
702
+ ### AI agent workflow
448
703
 
449
- **Create PR first:**
450
704
  \`\`\`bash
451
- # Create PR first
452
- gh pr create
705
+ # AI agent checks PR (always YAML for parsing)
706
+ vibe-validate watch-pr 90 --yaml
453
707
 
454
- # Or specify PR number explicitly
455
- vibe-validate watch-pr 42
708
+ # Parse result
709
+ # - If passed: proceed with merge
710
+ # - If failed: extract errors and fix
456
711
  \`\`\`
457
712
 
458
- ## CI Provider Support
713
+ ## Caching
459
714
 
460
- ### GitHub Actions (default)
461
-
462
- - **Requirements**: \`gh\` CLI installed and github.com remote
463
- - **Auto-detection**: Checks for .github/workflows and github.com remote
464
- - **Features**: Matrix job support, log extraction, vibe-validate state parsing
465
-
466
- ### GitLab CI (planned)
715
+ Results are cached locally (5 minute TTL):
716
+ \`\`\`
717
+ /tmp/vibe-validate/watch-pr-cache/<repo>/<pr-number>/
718
+ metadata.json # Complete WatchPRResult
719
+ logs/<run-id>.log # Raw logs from GitHub Actions
720
+ extractions/ # Extracted errors
721
+ \`\`\`
467
722
 
468
- - **Status**: Not yet implemented
469
- - **Tracking**: https://github.com/jdutton/vibe-validate/issues
723
+ Cache location is included in YAML output (\`cache.location\` field).
470
724
  `);
471
725
  }
472
726
  //# sourceMappingURL=watch-pr.js.map