@oss-autopilot/core 0.58.0 → 0.59.0

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/cli-registry.js +54 -0
  2. package/dist/cli.bundle.cjs +49 -45
  3. package/dist/commands/comments.d.ts +28 -0
  4. package/dist/commands/comments.js +28 -0
  5. package/dist/commands/config.d.ts +11 -0
  6. package/dist/commands/config.js +11 -0
  7. package/dist/commands/daily.d.ts +26 -2
  8. package/dist/commands/daily.js +26 -2
  9. package/dist/commands/detect-formatters.d.ts +11 -0
  10. package/dist/commands/detect-formatters.js +24 -0
  11. package/dist/commands/dismiss.d.ts +17 -0
  12. package/dist/commands/dismiss.js +17 -0
  13. package/dist/commands/index.d.ts +3 -1
  14. package/dist/commands/index.js +2 -0
  15. package/dist/commands/init.d.ts +8 -0
  16. package/dist/commands/init.js +8 -0
  17. package/dist/commands/move.d.ts +10 -0
  18. package/dist/commands/move.js +10 -0
  19. package/dist/commands/search.d.ts +18 -0
  20. package/dist/commands/search.js +18 -0
  21. package/dist/commands/setup.d.ts +17 -0
  22. package/dist/commands/setup.js +17 -0
  23. package/dist/commands/shelve.d.ts +16 -0
  24. package/dist/commands/shelve.js +16 -0
  25. package/dist/commands/startup.d.ts +16 -7
  26. package/dist/commands/startup.js +16 -7
  27. package/dist/commands/status.d.ts +8 -0
  28. package/dist/commands/status.js +8 -0
  29. package/dist/commands/track.d.ts +16 -0
  30. package/dist/commands/track.js +16 -0
  31. package/dist/commands/vet.d.ts +8 -0
  32. package/dist/commands/vet.js +8 -0
  33. package/dist/core/daily-logic.d.ts +60 -7
  34. package/dist/core/daily-logic.js +52 -7
  35. package/dist/core/formatter-detection.d.ts +61 -0
  36. package/dist/core/formatter-detection.js +360 -0
  37. package/dist/core/github.d.ts +25 -2
  38. package/dist/core/github.js +25 -2
  39. package/dist/core/index.d.ts +1 -0
  40. package/dist/core/index.js +1 -0
  41. package/dist/core/issue-discovery.d.ts +46 -6
  42. package/dist/core/issue-discovery.js +46 -6
  43. package/dist/core/logger.d.ts +13 -0
  44. package/dist/core/logger.js +13 -0
  45. package/dist/core/pr-monitor.d.ts +43 -8
  46. package/dist/core/pr-monitor.js +43 -8
  47. package/dist/core/state.d.ts +167 -0
  48. package/dist/core/state.js +167 -0
  49. package/dist/core/types.d.ts +2 -8
  50. package/dist/formatters/json.d.ts +5 -0
  51. package/package.json +6 -3
@@ -0,0 +1,360 @@
1
+ /**
2
+ * Formatter Detection Module (#703)
3
+ *
4
+ * Programmatically detects formatters/linters configured in a local repo directory
5
+ * and diagnoses CI formatting failures from log output.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { debug } from './logger.js';
10
+ const MODULE = 'formatter-detection';
11
+ // ── Prettier config file patterns ──────────────────────────────────────────
12
+ const PRETTIER_CONFIG_PATTERNS = [
13
+ '.prettierrc',
14
+ '.prettierrc.json',
15
+ '.prettierrc.yml',
16
+ '.prettierrc.yaml',
17
+ '.prettierrc.json5',
18
+ '.prettierrc.js',
19
+ '.prettierrc.cjs',
20
+ '.prettierrc.mjs',
21
+ '.prettierrc.toml',
22
+ 'prettier.config.js',
23
+ 'prettier.config.cjs',
24
+ 'prettier.config.mjs',
25
+ ];
26
+ // ── ESLint config file patterns ────────────────────────────────────────────
27
+ const ESLINT_CONFIG_PATTERNS = [
28
+ '.eslintrc',
29
+ '.eslintrc.js',
30
+ '.eslintrc.cjs',
31
+ '.eslintrc.yml',
32
+ '.eslintrc.yaml',
33
+ '.eslintrc.json',
34
+ 'eslint.config.js',
35
+ 'eslint.config.cjs',
36
+ 'eslint.config.mjs',
37
+ 'eslint.config.ts',
38
+ ];
39
+ // ── package.json script names that indicate formatting ─────────────────────
40
+ const FORMAT_SCRIPT_NAMES = ['lint:fix', 'format', 'fmt', 'lint', 'format:check', 'format:fix'];
41
+ // ── CI log patterns for each formatter ────────────────────────────────────
42
+ const CI_PATTERNS = [
43
+ {
44
+ formatter: 'prettier',
45
+ patterns: [/Code style issues found/i, /Forgot to run Prettier/i, /prettier --check/i],
46
+ },
47
+ {
48
+ formatter: 'ruff',
49
+ patterns: [/ruff format.*--check/i, /ruff format.*would reformat/i],
50
+ },
51
+ {
52
+ formatter: 'black',
53
+ patterns: [/Oh no! .* files? would be reformatted/i, /black --check/i],
54
+ },
55
+ {
56
+ formatter: 'rustfmt',
57
+ patterns: [/Diff in .*\.rs/i, /rustfmt --check/i, /cargo fmt.*--check/i],
58
+ },
59
+ {
60
+ formatter: 'biome',
61
+ patterns: [/biome check/i, /biome ci/i, /Found \d+ fixable diagnostics?/i],
62
+ },
63
+ {
64
+ formatter: 'eslint',
65
+ patterns: [/eslint.*--fix/i, /eslint.*\d+ problems?/i],
66
+ },
67
+ {
68
+ formatter: 'gofmt',
69
+ patterns: [/gofmt -d/i, /goimports/i],
70
+ },
71
+ {
72
+ formatter: 'clang-format',
73
+ patterns: [/clang-format/i],
74
+ },
75
+ {
76
+ formatter: 'rubocop',
77
+ patterns: [/rubocop.*offense/i, /rubocop -a/i],
78
+ },
79
+ ];
80
+ /**
81
+ * Safely read and parse a JSON file. Returns undefined on failure.
82
+ */
83
+ function readJsonFile(filePath) {
84
+ try {
85
+ const content = fs.readFileSync(filePath, 'utf-8');
86
+ return JSON.parse(content);
87
+ }
88
+ catch (err) {
89
+ debug(MODULE, `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
90
+ return undefined;
91
+ }
92
+ }
93
+ /**
94
+ * Safely read a text file. Returns undefined on failure.
95
+ */
96
+ function readTextFile(filePath) {
97
+ try {
98
+ return fs.readFileSync(filePath, 'utf-8');
99
+ }
100
+ catch (err) {
101
+ debug(MODULE, `Failed to read ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
102
+ return undefined;
103
+ }
104
+ }
105
+ /**
106
+ * Find the first existing file from a list of candidates in a directory.
107
+ */
108
+ function findFirstExisting(dir, candidates) {
109
+ for (const candidate of candidates) {
110
+ if (fs.existsSync(path.join(dir, candidate))) {
111
+ return candidate;
112
+ }
113
+ }
114
+ return undefined;
115
+ }
116
+ /**
117
+ * Read and parse package.json once. Returns undefined if not found or invalid.
118
+ */
119
+ function readPackageJson(repoPath) {
120
+ return readJsonFile(path.join(repoPath, 'package.json'));
121
+ }
122
+ /**
123
+ * Extract formatting-related scripts from a parsed package.json.
124
+ */
125
+ function extractPackageJsonScripts(pkg) {
126
+ if (!pkg?.scripts)
127
+ return [];
128
+ const results = [];
129
+ for (const scriptName of FORMAT_SCRIPT_NAMES) {
130
+ const command = pkg.scripts[scriptName];
131
+ if (command) {
132
+ results.push({ name: scriptName, command });
133
+ }
134
+ }
135
+ return results;
136
+ }
137
+ /**
138
+ * Check if prettier is listed in devDependencies or dependencies.
139
+ */
140
+ function hasPrettierDependency(pkg) {
141
+ if (!pkg)
142
+ return false;
143
+ return !!(pkg.devDependencies?.['prettier'] || pkg.dependencies?.['prettier']);
144
+ }
145
+ /**
146
+ * Check if a TOML file contains a specific section header.
147
+ */
148
+ function tomlHasSection(repoPath, fileName, sectionPattern) {
149
+ const content = readTextFile(path.join(repoPath, fileName));
150
+ if (!content)
151
+ return false;
152
+ return content.includes(sectionPattern);
153
+ }
154
+ /**
155
+ * Detect formatters and linters configured in a repository.
156
+ *
157
+ * Checks config files in priority order using fs.existsSync() / fs.readFileSync().
158
+ * Returns all detected formatters, plus any formatting-related package.json scripts.
159
+ *
160
+ * @param repoPath - Absolute path to the repository root directory
161
+ * @returns Detection result with formatters ordered by priority and extracted package.json scripts
162
+ * @throws {Error} If repoPath does not exist or is not a directory
163
+ */
164
+ export function detectFormatters(repoPath) {
165
+ if (!fs.existsSync(repoPath) || !fs.statSync(repoPath).isDirectory()) {
166
+ throw new Error(`Repository path does not exist or is not a directory: ${repoPath}`);
167
+ }
168
+ const formatters = [];
169
+ const pkg = readPackageJson(repoPath);
170
+ // 1. Biome (highest priority for JS/TS — replaces prettier + eslint)
171
+ const biomeConfig = findFirstExisting(repoPath, ['biome.json', 'biome.jsonc']);
172
+ if (biomeConfig) {
173
+ formatters.push({
174
+ name: 'biome',
175
+ configPath: biomeConfig,
176
+ fixCommand: 'npx @biomejs/biome check --write',
177
+ checkCommand: 'npx @biomejs/biome check',
178
+ supportsFileArgs: true,
179
+ });
180
+ }
181
+ // 2. Prettier
182
+ const prettierConfig = findFirstExisting(repoPath, PRETTIER_CONFIG_PATTERNS);
183
+ if (prettierConfig) {
184
+ formatters.push({
185
+ name: 'prettier',
186
+ configPath: prettierConfig,
187
+ fixCommand: 'npx prettier --write .',
188
+ checkCommand: 'npx prettier --check .',
189
+ supportsFileArgs: true,
190
+ });
191
+ }
192
+ else if (hasPrettierDependency(pkg)) {
193
+ formatters.push({
194
+ name: 'prettier',
195
+ configPath: 'package.json',
196
+ fixCommand: 'npx prettier --write .',
197
+ checkCommand: 'npx prettier --check .',
198
+ supportsFileArgs: true,
199
+ });
200
+ }
201
+ // 3. ESLint
202
+ const eslintConfig = findFirstExisting(repoPath, ESLINT_CONFIG_PATTERNS);
203
+ if (eslintConfig) {
204
+ formatters.push({
205
+ name: 'eslint',
206
+ configPath: eslintConfig,
207
+ fixCommand: 'npx eslint --fix .',
208
+ checkCommand: 'npx eslint .',
209
+ supportsFileArgs: true,
210
+ });
211
+ }
212
+ // 4. Rust (Cargo.toml → rustfmt)
213
+ if (fs.existsSync(path.join(repoPath, 'Cargo.toml'))) {
214
+ formatters.push({
215
+ name: 'rustfmt',
216
+ configPath: 'Cargo.toml',
217
+ fixCommand: 'cargo fmt',
218
+ checkCommand: 'cargo fmt --check',
219
+ supportsFileArgs: false,
220
+ });
221
+ }
222
+ // 5. Python — ruff takes priority over black
223
+ const hasPyproject = fs.existsSync(path.join(repoPath, 'pyproject.toml'));
224
+ if (hasPyproject && tomlHasSection(repoPath, 'pyproject.toml', '[tool.ruff]')) {
225
+ formatters.push({
226
+ name: 'ruff',
227
+ configPath: 'pyproject.toml',
228
+ fixCommand: 'ruff format .',
229
+ checkCommand: 'ruff format --check .',
230
+ supportsFileArgs: true,
231
+ });
232
+ }
233
+ else if (hasPyproject && tomlHasSection(repoPath, 'pyproject.toml', '[tool.black]')) {
234
+ formatters.push({
235
+ name: 'black',
236
+ configPath: 'pyproject.toml',
237
+ fixCommand: 'black .',
238
+ checkCommand: 'black --check .',
239
+ supportsFileArgs: true,
240
+ });
241
+ }
242
+ else if (fs.existsSync(path.join(repoPath, 'ruff.toml'))) {
243
+ formatters.push({
244
+ name: 'ruff',
245
+ configPath: 'ruff.toml',
246
+ fixCommand: 'ruff format .',
247
+ checkCommand: 'ruff format --check .',
248
+ supportsFileArgs: true,
249
+ });
250
+ }
251
+ // 6. Go
252
+ if (fs.existsSync(path.join(repoPath, 'go.mod'))) {
253
+ formatters.push({
254
+ name: 'gofmt',
255
+ configPath: 'go.mod',
256
+ fixCommand: 'gofmt -w .',
257
+ checkCommand: 'gofmt -d .',
258
+ supportsFileArgs: true,
259
+ });
260
+ }
261
+ // 7. Clang-format
262
+ if (fs.existsSync(path.join(repoPath, '.clang-format'))) {
263
+ formatters.push({
264
+ name: 'clang-format',
265
+ configPath: '.clang-format',
266
+ fixCommand: 'clang-format -i',
267
+ checkCommand: 'clang-format --dry-run --Werror',
268
+ supportsFileArgs: true,
269
+ });
270
+ }
271
+ // 8. RuboCop
272
+ if (fs.existsSync(path.join(repoPath, '.rubocop.yml'))) {
273
+ formatters.push({
274
+ name: 'rubocop',
275
+ configPath: '.rubocop.yml',
276
+ fixCommand: 'rubocop -a',
277
+ checkCommand: 'rubocop',
278
+ supportsFileArgs: true,
279
+ });
280
+ }
281
+ // Extract package.json scripts
282
+ const packageJsonScripts = extractPackageJsonScripts(pkg);
283
+ debug(MODULE, `Detected ${formatters.length} formatters in ${repoPath}`);
284
+ return { formatters, packageJsonScripts, repoPath };
285
+ }
286
+ /**
287
+ * Diagnose whether CI log output indicates a formatting failure.
288
+ *
289
+ * Pattern-matches known formatter error strings. When repoPath is provided,
290
+ * cross-references with {@link detectFormatters} to provide a targeted fix command.
291
+ *
292
+ * @param logOutput - Raw CI log output to analyze
293
+ * @param repoPath - Optional repo path for cross-referencing with local formatter config
294
+ * @returns Diagnosis with matched formatter, fix command, and evidence strings
295
+ */
296
+ export function diagnoseCIFormatterFailure(logOutput, repoPath) {
297
+ if (!logOutput.trim()) {
298
+ return { isFormattingFailure: false, evidence: [] };
299
+ }
300
+ const evidence = [];
301
+ let matchedFormatter;
302
+ for (const { formatter, patterns } of CI_PATTERNS) {
303
+ for (const pattern of patterns) {
304
+ const match = logOutput.match(pattern);
305
+ if (match) {
306
+ evidence.push(match[0]);
307
+ if (!matchedFormatter) {
308
+ matchedFormatter = formatter;
309
+ }
310
+ }
311
+ }
312
+ }
313
+ if (!matchedFormatter) {
314
+ return { isFormattingFailure: false, evidence: [] };
315
+ }
316
+ // Cross-reference with local detection to get the fix command
317
+ let fixCommand;
318
+ if (repoPath) {
319
+ try {
320
+ const detected = detectFormatters(repoPath);
321
+ const localMatch = detected.formatters.find((f) => f.name === matchedFormatter);
322
+ if (localMatch) {
323
+ fixCommand = localMatch.fixCommand;
324
+ }
325
+ }
326
+ catch (err) {
327
+ debug(MODULE, `Cross-reference failed for ${repoPath}: ${err instanceof Error ? err.message : String(err)}`);
328
+ }
329
+ }
330
+ // Fallback fix commands when CI-matched formatter wasn't found locally or no repoPath provided
331
+ if (!fixCommand) {
332
+ const fallbackCommands = {
333
+ prettier: 'npx prettier --write .',
334
+ eslint: 'npx eslint --fix .',
335
+ biome: 'npx @biomejs/biome check --write',
336
+ black: 'black .',
337
+ ruff: 'ruff format .',
338
+ rustfmt: 'cargo fmt',
339
+ gofmt: 'gofmt -w .',
340
+ 'clang-format': 'clang-format -i',
341
+ rubocop: 'rubocop -a',
342
+ };
343
+ fixCommand = fallbackCommands[matchedFormatter];
344
+ }
345
+ return {
346
+ isFormattingFailure: true,
347
+ formatter: matchedFormatter,
348
+ fixCommand,
349
+ evidence,
350
+ };
351
+ }
352
+ /**
353
+ * Return the first (highest-priority) detected formatter, or undefined if none found.
354
+ *
355
+ * @param result - Detection result from {@link detectFormatters}
356
+ * @returns The highest-priority formatter, or undefined if none detected
357
+ */
358
+ export function getPreferredFormatter(result) {
359
+ return result.formatters[0];
360
+ }
@@ -11,14 +11,37 @@ export interface RateLimitInfo {
11
11
  /** ISO timestamp when the rate limit window resets. */
12
12
  resetAt: string;
13
13
  }
14
- /** Throttle callbacks used by the Octokit client. Exported for testability. */
14
+ /**
15
+ * Throttle callbacks used by the Octokit client. Exported for testability.
16
+ * @returns Rate limit and secondary rate limit handler callbacks
17
+ */
15
18
  export declare function getRateLimitCallbacks(): {
16
19
  onRateLimit: (retryAfter: number, options: unknown, _octokit: unknown, retryCount: number) => boolean;
17
20
  onSecondaryRateLimit: (retryAfter: number, options: unknown, _octokit: unknown, retryCount: number) => boolean;
18
21
  };
22
+ /**
23
+ * Get a shared, throttled Octokit instance for the given token.
24
+ * Returns a cached instance if the token matches, otherwise creates a new one.
25
+ *
26
+ * The client retries on primary rate limits (up to 2 retries) and
27
+ * secondary rate limits (1 retry).
28
+ *
29
+ * @param token - GitHub personal access token
30
+ * @returns Authenticated Octokit instance with rate limit throttling
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import { getOctokit, requireGitHubToken } from '@oss-autopilot/core';
35
+ *
36
+ * const octokit = getOctokit(requireGitHubToken());
37
+ * const { data } = await octokit.repos.get({ owner: 'facebook', repo: 'react' });
38
+ * ```
39
+ */
19
40
  export declare function getOctokit(token: string): Octokit;
20
41
  /**
21
42
  * Check the GitHub Search API rate limit quota.
22
- * Returns the remaining requests, total limit, and reset time for the search endpoint.
43
+ *
44
+ * @param token - GitHub personal access token
45
+ * @returns Remaining requests, total limit, and reset time for the search endpoint
23
46
  */
24
47
  export declare function checkRateLimit(token: string): Promise<RateLimitInfo>;
@@ -12,7 +12,10 @@ let _currentToken = null;
12
12
  function formatResetTime(date) {
13
13
  return date.toLocaleTimeString('en-US', { hour12: false });
14
14
  }
15
- /** Throttle callbacks used by the Octokit client. Exported for testability. */
15
+ /**
16
+ * Throttle callbacks used by the Octokit client. Exported for testability.
17
+ * @returns Rate limit and secondary rate limit handler callbacks
18
+ */
16
19
  export function getRateLimitCallbacks() {
17
20
  return {
18
21
  onRateLimit: (retryAfter, options, _octokit, retryCount) => {
@@ -37,6 +40,24 @@ export function getRateLimitCallbacks() {
37
40
  },
38
41
  };
39
42
  }
43
+ /**
44
+ * Get a shared, throttled Octokit instance for the given token.
45
+ * Returns a cached instance if the token matches, otherwise creates a new one.
46
+ *
47
+ * The client retries on primary rate limits (up to 2 retries) and
48
+ * secondary rate limits (1 retry).
49
+ *
50
+ * @param token - GitHub personal access token
51
+ * @returns Authenticated Octokit instance with rate limit throttling
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * import { getOctokit, requireGitHubToken } from '@oss-autopilot/core';
56
+ *
57
+ * const octokit = getOctokit(requireGitHubToken());
58
+ * const { data } = await octokit.repos.get({ owner: 'facebook', repo: 'react' });
59
+ * ```
60
+ */
40
61
  export function getOctokit(token) {
41
62
  // Return cached instance only if token matches
42
63
  if (_octokit && _currentToken === token)
@@ -51,7 +72,9 @@ export function getOctokit(token) {
51
72
  }
52
73
  /**
53
74
  * Check the GitHub Search API rate limit quota.
54
- * Returns the remaining requests, total limit, and reset time for the search endpoint.
75
+ *
76
+ * @param token - GitHub personal access token
77
+ * @returns Remaining requests, total limit, and reset time for the search endpoint
55
78
  */
56
79
  export async function checkRateLimit(token) {
57
80
  const octokit = getOctokit(token);
@@ -16,4 +16,5 @@ export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-
16
16
  export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
17
17
  export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
18
18
  export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
19
+ export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, type DetectedFormatter, type FormatterDetectionResult, type CIFormatterDiagnosis, type FormatterName, } from './formatter-detection.js';
19
20
  export * from './types.js';
@@ -16,4 +16,5 @@ export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
16
16
  export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
17
17
  export { computeContributionStats } from './stats.js';
18
18
  export { fetchPRTemplate } from './pr-template.js';
19
+ export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, } from './formatter-detection.js';
19
20
  export * from './types.js';
@@ -10,6 +10,18 @@
10
10
  * - search-phases.ts — search helpers, caching, batched repo search
11
11
  */
12
12
  import { type IssueCandidate } from './types.js';
13
+ /**
14
+ * Multi-phase issue discovery engine that searches GitHub for contributable issues.
15
+ *
16
+ * Search phases (in priority order):
17
+ * 1. Repos where user has merged PRs (highest merge probability)
18
+ * 2. Preferred organizations
19
+ * 3. Starred repos
20
+ * 4. General label-filtered search
21
+ * 5. Actively maintained repos
22
+ *
23
+ * Each candidate is vetted for claimability and scored 0-100 for viability.
24
+ */
13
25
  export declare class IssueDiscovery {
14
26
  private octokit;
15
27
  private stateManager;
@@ -17,21 +29,42 @@ export declare class IssueDiscovery {
17
29
  private vetter;
18
30
  /** Set after searchIssues() runs if rate limits affected the search (low pre-flight quota or mid-search rate limit hits). */
19
31
  rateLimitWarning: string | null;
32
+ /** @param githubToken - GitHub personal access token or token from `gh auth token` */
20
33
  constructor(githubToken: string);
21
34
  /**
22
35
  * Fetch the authenticated user's starred repositories from GitHub.
23
36
  * Updates the state manager with the list and timestamp.
37
+ * @returns Array of starred repo names in "owner/repo" format
24
38
  */
25
39
  fetchStarredRepos(): Promise<string[]>;
26
40
  /**
27
- * Get starred repos, fetching from GitHub if cache is stale
41
+ * Get starred repos, fetching from GitHub if cache is stale.
42
+ * @returns Array of starred repo names in "owner/repo" format
28
43
  */
29
44
  getStarredReposWithRefresh(): Promise<string[]>;
30
45
  /**
31
46
  * Search for issues matching our criteria.
32
47
  * Searches in priority order: merged-PR repos first (no label filter), then starred repos,
33
- * then general search, then actively maintained repos (#349).
48
+ * then general search, then actively maintained repos.
34
49
  * Filters out issues from low-scoring and excluded repos.
50
+ *
51
+ * @param options - Search configuration
52
+ * @param options.languages - Programming languages to filter by
53
+ * @param options.labels - Issue labels to search for
54
+ * @param options.maxResults - Maximum candidates to return (default: 10)
55
+ * @returns Scored and sorted issue candidates
56
+ * @throws {ValidationError} If no candidates found and no rate limits prevented the search
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * import { IssueDiscovery, requireGitHubToken } from '@oss-autopilot/core';
61
+ *
62
+ * const discovery = new IssueDiscovery(requireGitHubToken());
63
+ * const candidates = await discovery.searchIssues({ maxResults: 5 });
64
+ * for (const c of candidates) {
65
+ * console.log(`${c.issue.repo}#${c.issue.number}: ${c.viabilityScore}/100`);
66
+ * }
67
+ * ```
35
68
  */
36
69
  searchIssues(options?: {
37
70
  languages?: string[];
@@ -39,16 +72,23 @@ export declare class IssueDiscovery {
39
72
  maxResults?: number;
40
73
  }): Promise<IssueCandidate[]>;
41
74
  /**
42
- * Vet a specific issue (delegates to IssueVetter).
75
+ * Vet a specific issue for claimability and project health.
76
+ * @param issueUrl - Full GitHub issue URL
77
+ * @returns The vetted issue candidate with recommendation and scores
78
+ * @throws {ValidationError} If the URL is invalid or the issue cannot be fetched
43
79
  */
44
80
  vetIssue(issueUrl: string): Promise<IssueCandidate>;
45
81
  /**
46
- * Save search results to ~/.oss-autopilot/found-issues.md
47
- * Results are sorted by viability score (highest first)
82
+ * Save search results to ~/.oss-autopilot/found-issues.md.
83
+ * Results are sorted by viability score (highest first).
84
+ * @param candidates - Issue candidates to save
85
+ * @returns Absolute path to the written file
48
86
  */
49
87
  saveSearchResults(candidates: IssueCandidate[]): string;
50
88
  /**
51
- * Format issue candidate for display
89
+ * Format issue candidate as a markdown display string.
90
+ * @param candidate - The issue candidate to format
91
+ * @returns Multi-line markdown string with vetting details
52
92
  */
53
93
  formatCandidate(candidate: IssueCandidate): string;
54
94
  }
@@ -22,6 +22,18 @@ import { IssueVetter } from './issue-vetting.js';
22
22
  import { getTopicsForCategories } from './category-mapping.js';
23
23
  import { buildLabelQuery, buildEffectiveLabels, interleaveArrays, cachedSearchIssues, filterVetAndScore, searchInRepos, } from './search-phases.js';
24
24
  const MODULE = 'issue-discovery';
25
+ /**
26
+ * Multi-phase issue discovery engine that searches GitHub for contributable issues.
27
+ *
28
+ * Search phases (in priority order):
29
+ * 1. Repos where user has merged PRs (highest merge probability)
30
+ * 2. Preferred organizations
31
+ * 3. Starred repos
32
+ * 4. General label-filtered search
33
+ * 5. Actively maintained repos
34
+ *
35
+ * Each candidate is vetted for claimability and scored 0-100 for viability.
36
+ */
25
37
  export class IssueDiscovery {
26
38
  octokit;
27
39
  stateManager;
@@ -29,6 +41,7 @@ export class IssueDiscovery {
29
41
  vetter;
30
42
  /** Set after searchIssues() runs if rate limits affected the search (low pre-flight quota or mid-search rate limit hits). */
31
43
  rateLimitWarning = null;
44
+ /** @param githubToken - GitHub personal access token or token from `gh auth token` */
32
45
  constructor(githubToken) {
33
46
  this.githubToken = githubToken;
34
47
  this.octokit = getOctokit(githubToken);
@@ -38,6 +51,7 @@ export class IssueDiscovery {
38
51
  /**
39
52
  * Fetch the authenticated user's starred repositories from GitHub.
40
53
  * Updates the state manager with the list and timestamp.
54
+ * @returns Array of starred repo names in "owner/repo" format
41
55
  */
42
56
  async fetchStarredRepos() {
43
57
  info(MODULE, 'Fetching starred repositories...');
@@ -93,7 +107,8 @@ export class IssueDiscovery {
93
107
  }
94
108
  }
95
109
  /**
96
- * Get starred repos, fetching from GitHub if cache is stale
110
+ * Get starred repos, fetching from GitHub if cache is stale.
111
+ * @returns Array of starred repo names in "owner/repo" format
97
112
  */
98
113
  async getStarredReposWithRefresh() {
99
114
  if (this.stateManager.isStarredReposStale()) {
@@ -104,8 +119,26 @@ export class IssueDiscovery {
104
119
  /**
105
120
  * Search for issues matching our criteria.
106
121
  * Searches in priority order: merged-PR repos first (no label filter), then starred repos,
107
- * then general search, then actively maintained repos (#349).
122
+ * then general search, then actively maintained repos.
108
123
  * Filters out issues from low-scoring and excluded repos.
124
+ *
125
+ * @param options - Search configuration
126
+ * @param options.languages - Programming languages to filter by
127
+ * @param options.labels - Issue labels to search for
128
+ * @param options.maxResults - Maximum candidates to return (default: 10)
129
+ * @returns Scored and sorted issue candidates
130
+ * @throws {ValidationError} If no candidates found and no rate limits prevented the search
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * import { IssueDiscovery, requireGitHubToken } from '@oss-autopilot/core';
135
+ *
136
+ * const discovery = new IssueDiscovery(requireGitHubToken());
137
+ * const candidates = await discovery.searchIssues({ maxResults: 5 });
138
+ * for (const c of candidates) {
139
+ * console.log(`${c.issue.repo}#${c.issue.number}: ${c.viabilityScore}/100`);
140
+ * }
141
+ * ```
109
142
  */
110
143
  async searchIssues(options = {}) {
111
144
  const config = this.stateManager.getState().config;
@@ -453,14 +486,19 @@ export class IssueDiscovery {
453
486
  return capped.slice(0, maxResults);
454
487
  }
455
488
  /**
456
- * Vet a specific issue (delegates to IssueVetter).
489
+ * Vet a specific issue for claimability and project health.
490
+ * @param issueUrl - Full GitHub issue URL
491
+ * @returns The vetted issue candidate with recommendation and scores
492
+ * @throws {ValidationError} If the URL is invalid or the issue cannot be fetched
457
493
  */
458
494
  async vetIssue(issueUrl) {
459
495
  return this.vetter.vetIssue(issueUrl);
460
496
  }
461
497
  /**
462
- * Save search results to ~/.oss-autopilot/found-issues.md
463
- * Results are sorted by viability score (highest first)
498
+ * Save search results to ~/.oss-autopilot/found-issues.md.
499
+ * Results are sorted by viability score (highest first).
500
+ * @param candidates - Issue candidates to save
501
+ * @returns Absolute path to the written file
464
502
  */
465
503
  saveSearchResults(candidates) {
466
504
  // Sort by viability score descending
@@ -489,7 +527,9 @@ export class IssueDiscovery {
489
527
  return outputFile;
490
528
  }
491
529
  /**
492
- * Format issue candidate for display
530
+ * Format issue candidate as a markdown display string.
531
+ * @param candidate - The issue candidate to format
532
+ * @returns Multi-line markdown string with vetting details
493
533
  */
494
534
  formatCandidate(candidate) {
495
535
  const { issue, vettingResult, projectHealth, recommendation, reasonsToApprove, reasonsToSkip } = candidate;