@oss-autopilot/core 0.53.1 → 0.55.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 (43) hide show
  1. package/dist/cli.bundle.cjs +63 -63
  2. package/dist/commands/comments.js +0 -1
  3. package/dist/commands/config.js +45 -5
  4. package/dist/commands/daily.js +197 -162
  5. package/dist/commands/dashboard-data.js +37 -30
  6. package/dist/commands/dashboard-server.js +8 -1
  7. package/dist/commands/dismiss.js +0 -6
  8. package/dist/commands/init.js +0 -1
  9. package/dist/commands/local-repos.js +1 -2
  10. package/dist/commands/move.js +12 -11
  11. package/dist/commands/setup.d.ts +2 -1
  12. package/dist/commands/setup.js +166 -130
  13. package/dist/commands/shelve.js +10 -10
  14. package/dist/commands/startup.js +30 -14
  15. package/dist/core/ci-analysis.d.ts +6 -0
  16. package/dist/core/ci-analysis.js +91 -12
  17. package/dist/core/daily-logic.js +24 -33
  18. package/dist/core/display-utils.js +22 -2
  19. package/dist/core/github-stats.d.ts +1 -1
  20. package/dist/core/github-stats.js +1 -1
  21. package/dist/core/index.d.ts +2 -1
  22. package/dist/core/index.js +2 -1
  23. package/dist/core/issue-discovery.d.ts +7 -44
  24. package/dist/core/issue-discovery.js +83 -188
  25. package/dist/core/issue-eligibility.d.ts +35 -0
  26. package/dist/core/issue-eligibility.js +126 -0
  27. package/dist/core/issue-vetting.d.ts +6 -21
  28. package/dist/core/issue-vetting.js +15 -279
  29. package/dist/core/pr-monitor.d.ts +14 -16
  30. package/dist/core/pr-monitor.js +26 -90
  31. package/dist/core/repo-health.d.ts +24 -0
  32. package/dist/core/repo-health.js +193 -0
  33. package/dist/core/repo-score-manager.js +2 -0
  34. package/dist/core/search-phases.d.ts +55 -0
  35. package/dist/core/search-phases.js +155 -0
  36. package/dist/core/state.d.ts +11 -0
  37. package/dist/core/state.js +63 -4
  38. package/dist/core/status-determination.d.ts +2 -0
  39. package/dist/core/status-determination.js +82 -22
  40. package/dist/core/types.d.ts +23 -2
  41. package/dist/core/types.js +7 -0
  42. package/dist/formatters/json.d.ts +1 -1
  43. package/package.json +1 -1
@@ -9,7 +9,6 @@ export async function runInit(options) {
9
9
  const stateManager = getStateManager();
10
10
  // Set username in config
11
11
  stateManager.updateConfig({ githubUsername: options.username });
12
- stateManager.save();
13
12
  return {
14
13
  username: options.username,
15
14
  message: 'Username saved. Run `daily` to fetch your open PRs from GitHub.',
@@ -113,11 +113,10 @@ export async function runLocalRepos(options) {
113
113
  const cachedAt = new Date().toISOString();
114
114
  try {
115
115
  stateManager.setLocalRepoCache({ repos, scanPaths, cachedAt });
116
- stateManager.save();
117
116
  }
118
117
  catch (error) {
119
118
  const msg = errorMessage(error);
120
- console.error(`Warning: Failed to cache scan results: ${msg}`);
119
+ console.error(`Warning: Failed to cache scan results to disk: ${msg}`);
121
120
  }
122
121
  return {
123
122
  repos,
@@ -21,15 +21,17 @@ export async function runMove(options) {
21
21
  // Use current time — the CLI doesn't have cached PR data. The override
22
22
  // will auto-clear on the next daily run if the PR has new activity after this.
23
23
  const lastActivityAt = new Date().toISOString();
24
- stateManager.setStatusOverride(options.prUrl, status, lastActivityAt);
25
- stateManager.unshelvePR(options.prUrl);
26
- stateManager.save();
24
+ stateManager.batch(() => {
25
+ stateManager.setStatusOverride(options.prUrl, status, lastActivityAt);
26
+ stateManager.unshelvePR(options.prUrl);
27
+ });
27
28
  return { url: options.prUrl, target, description: `Moved to ${label}` };
28
29
  }
29
30
  case 'shelved': {
30
- stateManager.shelvePR(options.prUrl);
31
- stateManager.clearStatusOverride(options.prUrl);
32
- stateManager.save();
31
+ stateManager.batch(() => {
32
+ stateManager.shelvePR(options.prUrl);
33
+ stateManager.clearStatusOverride(options.prUrl);
34
+ });
33
35
  return {
34
36
  url: options.prUrl,
35
37
  target,
@@ -37,11 +39,10 @@ export async function runMove(options) {
37
39
  };
38
40
  }
39
41
  case 'auto': {
40
- const clearedOverride = stateManager.clearStatusOverride(options.prUrl);
41
- const unshelved = stateManager.unshelvePR(options.prUrl);
42
- if (clearedOverride || unshelved) {
43
- stateManager.save();
44
- }
42
+ stateManager.batch(() => {
43
+ stateManager.clearStatusOverride(options.prUrl);
44
+ stateManager.unshelvePR(options.prUrl);
45
+ });
45
46
  return {
46
47
  url: options.prUrl,
47
48
  target,
@@ -2,7 +2,7 @@
2
2
  * Setup command
3
3
  * Interactive setup / configuration
4
4
  */
5
- import { type ProjectCategory } from '../core/types.js';
5
+ import { type ProjectCategory, type IssueScope } from '../core/types.js';
6
6
  interface SetupOptions {
7
7
  reset?: boolean;
8
8
  set?: string[];
@@ -23,6 +23,7 @@ export interface SetupCompleteOutput {
23
23
  labels: string[];
24
24
  projectCategories: ProjectCategory[];
25
25
  preferredOrgs: string[];
26
+ scope: IssueScope[];
26
27
  };
27
28
  }
28
29
  export interface SetupPrompt {
@@ -5,7 +5,7 @@
5
5
  import { getStateManager, DEFAULT_CONFIG } from '../core/index.js';
6
6
  import { ValidationError } from '../core/errors.js';
7
7
  import { validateGitHubUsername } from './validation.js';
8
- import { PROJECT_CATEGORIES } from '../core/types.js';
8
+ import { PROJECT_CATEGORIES, ISSUE_SCOPES } from '../core/types.js';
9
9
  /** Parse and validate a positive integer setting value. */
10
10
  function parsePositiveInt(value, settingName) {
11
11
  const parsed = Number(value);
@@ -21,153 +21,181 @@ export async function runSetup(options) {
21
21
  if (options.set && options.set.length > 0) {
22
22
  const results = {};
23
23
  const warnings = [];
24
- for (const setting of options.set) {
25
- const [key, ...valueParts] = setting.split('=');
26
- const value = valueParts.join('=');
27
- switch (key) {
28
- case 'username':
29
- validateGitHubUsername(value);
30
- stateManager.updateConfig({ githubUsername: value });
31
- results[key] = value;
32
- break;
33
- case 'maxActivePRs': {
34
- const maxPRs = parsePositiveInt(value, 'maxActivePRs');
35
- stateManager.updateConfig({ maxActivePRs: maxPRs });
36
- results[key] = String(maxPRs);
37
- break;
38
- }
39
- case 'dormantDays': {
40
- const dormant = parsePositiveInt(value, 'dormantDays');
41
- stateManager.updateConfig({ dormantThresholdDays: dormant });
42
- results[key] = String(dormant);
43
- break;
44
- }
45
- case 'approachingDays': {
46
- const approaching = parsePositiveInt(value, 'approachingDays');
47
- stateManager.updateConfig({ approachingDormantDays: approaching });
48
- results[key] = String(approaching);
49
- break;
50
- }
51
- case 'languages':
52
- stateManager.updateConfig({ languages: value.split(',').map((l) => l.trim()) });
53
- results[key] = value;
54
- break;
55
- case 'labels':
56
- stateManager.updateConfig({ labels: value.split(',').map((l) => l.trim()) });
57
- results[key] = value;
58
- break;
59
- case 'showHealthCheck':
60
- stateManager.updateConfig({ showHealthCheck: value !== 'false' });
61
- results[key] = value !== 'false' ? 'true' : 'false';
62
- break;
63
- case 'squashByDefault':
64
- if (value === 'ask') {
65
- stateManager.updateConfig({ squashByDefault: 'ask' });
66
- results[key] = 'ask';
24
+ stateManager.batch(() => {
25
+ for (const setting of options.set) {
26
+ const [key, ...valueParts] = setting.split('=');
27
+ const value = valueParts.join('=');
28
+ switch (key) {
29
+ case 'username':
30
+ validateGitHubUsername(value);
31
+ stateManager.updateConfig({ githubUsername: value });
32
+ results[key] = value;
33
+ break;
34
+ case 'maxActivePRs': {
35
+ const maxPRs = parsePositiveInt(value, 'maxActivePRs');
36
+ stateManager.updateConfig({ maxActivePRs: maxPRs });
37
+ results[key] = String(maxPRs);
38
+ break;
67
39
  }
68
- else {
69
- stateManager.updateConfig({ squashByDefault: value !== 'false' });
70
- results[key] = value !== 'false' ? 'true' : 'false';
40
+ case 'dormantDays': {
41
+ const dormant = parsePositiveInt(value, 'dormantDays');
42
+ stateManager.updateConfig({ dormantThresholdDays: dormant });
43
+ results[key] = String(dormant);
44
+ break;
71
45
  }
72
- break;
73
- case 'minStars': {
74
- const stars = Number(value);
75
- if (!Number.isFinite(stars) || !Number.isInteger(stars) || stars < 0) {
76
- throw new ValidationError(`Invalid value for minStars: "${value}". Must be a non-negative integer.`);
46
+ case 'approachingDays': {
47
+ const approaching = parsePositiveInt(value, 'approachingDays');
48
+ stateManager.updateConfig({ approachingDormantDays: approaching });
49
+ results[key] = String(approaching);
50
+ break;
77
51
  }
78
- stateManager.updateConfig({ minStars: stars });
79
- results[key] = String(stars);
80
- break;
81
- }
82
- case 'includeDocIssues':
83
- stateManager.updateConfig({ includeDocIssues: value === 'true' });
84
- results[key] = value === 'true' ? 'true' : 'false';
85
- break;
86
- case 'aiPolicyBlocklist': {
87
- const entries = value
88
- .split(',')
89
- .map((r) => r.trim())
90
- .filter(Boolean);
91
- const valid = [];
92
- const invalid = [];
93
- for (const entry of entries) {
94
- const normalized = entry.replace(/\s+/g, '');
95
- if (/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(normalized)) {
96
- valid.push(normalized);
52
+ case 'languages':
53
+ stateManager.updateConfig({ languages: value.split(',').map((l) => l.trim()) });
54
+ results[key] = value;
55
+ break;
56
+ case 'labels':
57
+ stateManager.updateConfig({ labels: value.split(',').map((l) => l.trim()) });
58
+ results[key] = value;
59
+ break;
60
+ case 'showHealthCheck':
61
+ stateManager.updateConfig({ showHealthCheck: value !== 'false' });
62
+ results[key] = value !== 'false' ? 'true' : 'false';
63
+ break;
64
+ case 'squashByDefault':
65
+ if (value === 'ask') {
66
+ stateManager.updateConfig({ squashByDefault: 'ask' });
67
+ results[key] = 'ask';
97
68
  }
98
69
  else {
99
- invalid.push(entry);
70
+ stateManager.updateConfig({ squashByDefault: value !== 'false' });
71
+ results[key] = value !== 'false' ? 'true' : 'false';
100
72
  }
101
- }
102
- if (invalid.length > 0) {
103
- warnings.push(`Warning: Skipping invalid entries (expected "owner/repo" format): ${invalid.join(', ')}`);
104
- results['aiPolicyBlocklist_invalidEntries'] = invalid.join(', ');
105
- }
106
- if (valid.length === 0 && entries.length > 0) {
107
- warnings.push('Warning: All entries were invalid. Blocklist not updated.');
108
- results[key] = '(all entries invalid)';
73
+ break;
74
+ case 'minStars': {
75
+ const stars = Number(value);
76
+ if (!Number.isFinite(stars) || !Number.isInteger(stars) || stars < 0) {
77
+ throw new ValidationError(`Invalid value for minStars: "${value}". Must be a non-negative integer.`);
78
+ }
79
+ stateManager.updateConfig({ minStars: stars });
80
+ results[key] = String(stars);
109
81
  break;
110
82
  }
111
- stateManager.updateConfig({ aiPolicyBlocklist: valid });
112
- results[key] = valid.length > 0 ? valid.join(', ') : '(empty)';
113
- break;
114
- }
115
- case 'projectCategories': {
116
- const categories = value
117
- .split(',')
118
- .map((c) => c.trim())
119
- .filter(Boolean);
120
- const validCategories = [];
121
- const invalidCategories = [];
122
- for (const cat of categories) {
123
- if (PROJECT_CATEGORIES.includes(cat)) {
124
- validCategories.push(cat);
83
+ case 'includeDocIssues':
84
+ stateManager.updateConfig({ includeDocIssues: value === 'true' });
85
+ results[key] = value === 'true' ? 'true' : 'false';
86
+ break;
87
+ case 'aiPolicyBlocklist': {
88
+ const entries = value
89
+ .split(',')
90
+ .map((r) => r.trim())
91
+ .filter(Boolean);
92
+ const valid = [];
93
+ const invalid = [];
94
+ for (const entry of entries) {
95
+ const normalized = entry.replace(/\s+/g, '');
96
+ if (/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(normalized)) {
97
+ valid.push(normalized);
98
+ }
99
+ else {
100
+ invalid.push(entry);
101
+ }
125
102
  }
126
- else {
127
- invalidCategories.push(cat);
103
+ if (invalid.length > 0) {
104
+ warnings.push(`Warning: Skipping invalid entries (expected "owner/repo" format): ${invalid.join(', ')}`);
105
+ results['aiPolicyBlocklist_invalidEntries'] = invalid.join(', ');
106
+ }
107
+ if (valid.length === 0 && entries.length > 0) {
108
+ warnings.push('Warning: All entries were invalid. Blocklist not updated.');
109
+ results[key] = '(all entries invalid)';
110
+ break;
128
111
  }
112
+ stateManager.updateConfig({ aiPolicyBlocklist: valid });
113
+ results[key] = valid.length > 0 ? valid.join(', ') : '(empty)';
114
+ break;
129
115
  }
130
- if (invalidCategories.length > 0) {
131
- warnings.push(`Unknown project categories: ${invalidCategories.join(', ')}. Valid: ${PROJECT_CATEGORIES.join(', ')}`);
116
+ case 'projectCategories': {
117
+ const categories = value
118
+ .split(',')
119
+ .map((c) => c.trim())
120
+ .filter(Boolean);
121
+ const validCategories = [];
122
+ const invalidCategories = [];
123
+ for (const cat of categories) {
124
+ if (PROJECT_CATEGORIES.includes(cat)) {
125
+ validCategories.push(cat);
126
+ }
127
+ else {
128
+ invalidCategories.push(cat);
129
+ }
130
+ }
131
+ if (invalidCategories.length > 0) {
132
+ warnings.push(`Unknown project categories: ${invalidCategories.join(', ')}. Valid: ${PROJECT_CATEGORIES.join(', ')}`);
133
+ }
134
+ const dedupedCategories = [...new Set(validCategories)];
135
+ stateManager.updateConfig({ projectCategories: dedupedCategories });
136
+ results[key] = dedupedCategories.length > 0 ? dedupedCategories.join(', ') : '(empty)';
137
+ break;
132
138
  }
133
- const dedupedCategories = [...new Set(validCategories)];
134
- stateManager.updateConfig({ projectCategories: dedupedCategories });
135
- results[key] = dedupedCategories.length > 0 ? dedupedCategories.join(', ') : '(empty)';
136
- break;
137
- }
138
- case 'preferredOrgs': {
139
- const orgs = value
140
- .split(',')
141
- .map((o) => o.trim())
142
- .filter(Boolean);
143
- const validOrgs = [];
144
- for (const org of orgs) {
145
- if (org.includes('/')) {
146
- warnings.push(`"${org}" looks like a repo path. Use org name only (e.g., "vercel" not "vercel/next.js").`);
139
+ case 'preferredOrgs': {
140
+ const orgs = value
141
+ .split(',')
142
+ .map((o) => o.trim())
143
+ .filter(Boolean);
144
+ const validOrgs = [];
145
+ for (const org of orgs) {
146
+ if (org.includes('/')) {
147
+ warnings.push(`"${org}" looks like a repo path. Use org name only (e.g., "vercel" not "vercel/next.js").`);
148
+ }
149
+ else if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(org)) {
150
+ warnings.push(`"${org}" is not a valid GitHub organization name. Skipping.`);
151
+ }
152
+ else {
153
+ validOrgs.push(org.toLowerCase());
154
+ }
147
155
  }
148
- else if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(org)) {
149
- warnings.push(`"${org}" is not a valid GitHub organization name. Skipping.`);
156
+ const dedupedOrgs = [...new Set(validOrgs)];
157
+ stateManager.updateConfig({ preferredOrgs: dedupedOrgs });
158
+ results[key] = dedupedOrgs.length > 0 ? dedupedOrgs.join(', ') : '(empty)';
159
+ break;
160
+ }
161
+ case 'scope': {
162
+ const scopeValues = value
163
+ .split(',')
164
+ .map((s) => s.trim())
165
+ .filter(Boolean);
166
+ const validScopes = [];
167
+ const invalidScopes = [];
168
+ for (const s of scopeValues) {
169
+ if (ISSUE_SCOPES.includes(s)) {
170
+ validScopes.push(s);
171
+ }
172
+ else {
173
+ invalidScopes.push(s);
174
+ }
150
175
  }
151
- else {
152
- validOrgs.push(org.toLowerCase());
176
+ if (invalidScopes.length > 0) {
177
+ warnings.push(`Unknown issue scopes: ${invalidScopes.join(', ')}. Valid: ${ISSUE_SCOPES.join(', ')}`);
153
178
  }
179
+ const dedupedScopes = [...new Set(validScopes)];
180
+ stateManager.updateConfig({ scope: dedupedScopes.length > 0 ? dedupedScopes : undefined });
181
+ results[key] = dedupedScopes.length > 0 ? dedupedScopes.join(', ') : '(empty — using labels only)';
182
+ break;
154
183
  }
155
- const dedupedOrgs = [...new Set(validOrgs)];
156
- stateManager.updateConfig({ preferredOrgs: dedupedOrgs });
157
- results[key] = dedupedOrgs.length > 0 ? dedupedOrgs.join(', ') : '(empty)';
158
- break;
184
+ case 'issueListPath':
185
+ stateManager.updateConfig({ issueListPath: value || undefined });
186
+ results[key] = value || '(cleared)';
187
+ break;
188
+ case 'complete':
189
+ if (value === 'true') {
190
+ stateManager.markSetupComplete();
191
+ results[key] = 'true';
192
+ }
193
+ break;
194
+ default:
195
+ warnings.push(`Unknown setting: ${key}`);
159
196
  }
160
- case 'complete':
161
- if (value === 'true') {
162
- stateManager.markSetupComplete();
163
- results[key] = 'true';
164
- }
165
- break;
166
- default:
167
- warnings.push(`Unknown setting: ${key}`);
168
197
  }
169
- }
170
- stateManager.save();
198
+ });
171
199
  return { success: true, settings: results, warnings: warnings.length > 0 ? warnings : undefined };
172
200
  }
173
201
  // Show setup status
@@ -183,6 +211,7 @@ export async function runSetup(options) {
183
211
  labels: config.labels,
184
212
  projectCategories: config.projectCategories ?? [],
185
213
  preferredOrgs: config.preferredOrgs ?? [],
214
+ scope: config.scope ?? [],
186
215
  },
187
216
  };
188
217
  }
@@ -232,6 +261,13 @@ export async function runSetup(options) {
232
261
  default: ['good first issue', 'help wanted'],
233
262
  type: 'list',
234
263
  },
264
+ {
265
+ setting: 'scope',
266
+ prompt: 'What scope of issues do you want to discover? (beginner, intermediate, advanced — leave empty for default labels only)',
267
+ current: config.scope ?? [],
268
+ default: [],
269
+ type: 'list',
270
+ },
235
271
  {
236
272
  setting: 'aiPolicyBlocklist',
237
273
  prompt: 'Repos with anti-AI contribution policies to block (owner/repo, comma-separated)?',
@@ -15,21 +15,21 @@ export async function runShelve(options) {
15
15
  validateUrl(options.prUrl);
16
16
  validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
17
17
  const stateManager = getStateManager();
18
- const added = stateManager.shelvePR(options.prUrl);
19
- const clearedOverride = stateManager.clearStatusOverride(options.prUrl);
20
- if (added || clearedOverride) {
21
- stateManager.save();
22
- }
18
+ let added = false;
19
+ stateManager.batch(() => {
20
+ added = stateManager.shelvePR(options.prUrl);
21
+ stateManager.clearStatusOverride(options.prUrl);
22
+ });
23
23
  return { shelved: added, url: options.prUrl };
24
24
  }
25
25
  export async function runUnshelve(options) {
26
26
  validateUrl(options.prUrl);
27
27
  validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
28
28
  const stateManager = getStateManager();
29
- const removed = stateManager.unshelvePR(options.prUrl);
30
- const clearedOverride = stateManager.clearStatusOverride(options.prUrl);
31
- if (removed || clearedOverride) {
32
- stateManager.save();
33
- }
29
+ let removed = false;
30
+ stateManager.batch(() => {
31
+ removed = stateManager.unshelvePR(options.prUrl);
32
+ stateManager.clearStatusOverride(options.prUrl);
33
+ });
34
34
  return { unshelved: removed, url: options.prUrl };
35
35
  }
@@ -10,6 +10,7 @@ import * as fs from 'fs';
10
10
  import { execFile } from 'child_process';
11
11
  import { getStateManager, getGitHubToken, getCLIVersion, detectGitHubUsername } from '../core/index.js';
12
12
  import { errorMessage } from '../core/errors.js';
13
+ import { warn } from '../core/logger.js';
13
14
  import { executeDailyCheck } from './daily.js';
14
15
  import { launchDashboardServer } from './dashboard-lifecycle.js';
15
16
  /**
@@ -53,22 +54,37 @@ export function countIssueListItems(content) {
53
54
  export function detectIssueList() {
54
55
  let issueListPath = '';
55
56
  let source = 'auto-detected';
56
- // 1. Check config file for configured path
57
- const configPath = '.claude/oss-autopilot/config.md';
58
- if (fs.existsSync(configPath)) {
59
- try {
60
- const configContent = fs.readFileSync(configPath, 'utf-8');
61
- const configuredPath = parseIssueListPathFromConfig(configContent);
62
- if (configuredPath && fs.existsSync(configuredPath)) {
63
- issueListPath = configuredPath;
64
- source = 'configured';
65
- }
57
+ // 1. Check state.json config (primary)
58
+ try {
59
+ const stateManager = getStateManager();
60
+ const configuredPath = stateManager.getState().config.issueListPath;
61
+ if (configuredPath && fs.existsSync(configuredPath)) {
62
+ issueListPath = configuredPath;
63
+ source = 'configured';
66
64
  }
67
- catch (error) {
68
- console.error('[STARTUP] Failed to read config:', errorMessage(error));
65
+ }
66
+ catch (error) {
67
+ // State manager may not be initialized yet — fall through to legacy config.md
68
+ warn('startup', `Could not read issueListPath from state: ${errorMessage(error)}`);
69
+ }
70
+ // 2. Fallback: config.md (legacy — will be removed in future)
71
+ if (!issueListPath) {
72
+ const configPath = '.claude/oss-autopilot/config.md';
73
+ if (fs.existsSync(configPath)) {
74
+ try {
75
+ const configContent = fs.readFileSync(configPath, 'utf-8');
76
+ const configuredPath = parseIssueListPathFromConfig(configContent);
77
+ if (configuredPath && fs.existsSync(configuredPath)) {
78
+ issueListPath = configuredPath;
79
+ source = 'configured';
80
+ }
81
+ }
82
+ catch (error) {
83
+ console.error('[STARTUP] Failed to read config:', errorMessage(error));
84
+ }
69
85
  }
70
86
  }
71
- // 2. Probe known paths
87
+ // 3. Probe known paths
72
88
  if (!issueListPath) {
73
89
  const probes = ['open-source/potential-issue-list.md', 'oss/issue-list.md', 'issues.md'];
74
90
  for (const probe of probes) {
@@ -81,7 +97,7 @@ export function detectIssueList() {
81
97
  }
82
98
  if (!issueListPath)
83
99
  return undefined;
84
- // 3. Count available/completed items
100
+ // 4. Count available/completed items
85
101
  try {
86
102
  const content = fs.readFileSync(issueListPath, 'utf-8');
87
103
  const { availableCount, completedCount } = countIssueListItems(content);
@@ -2,6 +2,7 @@
2
2
  * CI Analysis - Classification and analysis of CI check runs and combined statuses.
3
3
  * Extracted from PRMonitor to isolate CI-related logic (#263).
4
4
  */
5
+ import type { Octokit } from '@octokit/rest';
5
6
  import { CIFailureCategory, ClassifiedCheck, CIStatusResult } from './types.js';
6
7
  /**
7
8
  * Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145).
@@ -61,4 +62,9 @@ export declare function analyzeCombinedStatus(combinedStatus: {
61
62
  * Priority: failing > pending > passing > unknown.
62
63
  */
63
64
  export declare function mergeStatuses(checkRunAnalysis: CheckRunAnalysis, combinedAnalysis: CombinedStatusAnalysis, checkRunCount: number): CIStatusResult;
65
+ /**
66
+ * Get CI status for a commit SHA by querying both the combined status API and check runs API.
67
+ * Returns the merged status and names of any failing checks for diagnostics.
68
+ */
69
+ export declare function getCIStatus(octokit: Octokit, owner: string, repo: string, sha: string): Promise<CIStatusResult>;
64
70
  export {};