@oss-autopilot/core 0.43.0 → 0.43.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.
@@ -3,6 +3,7 @@
3
3
  * Shows or updates configuration
4
4
  */
5
5
  import { getStateManager } from '../core/index.js';
6
+ import { validateGitHubUsername } from './validation.js';
6
7
  export async function runConfig(options) {
7
8
  const stateManager = getStateManager();
8
9
  const currentConfig = stateManager.getState().config;
@@ -17,7 +18,7 @@ export async function runConfig(options) {
17
18
  // Handle specific config keys
18
19
  switch (options.key) {
19
20
  case 'username':
20
- stateManager.updateConfig({ githubUsername: value });
21
+ stateManager.updateConfig({ githubUsername: validateGitHubUsername(value) });
21
22
  break;
22
23
  case 'add-language':
23
24
  if (!currentConfig.languages.includes(value)) {
@@ -7,9 +7,22 @@
7
7
  * orchestration layer that wires up the phases and handles I/O.
8
8
  */
9
9
  import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
10
- import { errorMessage } from '../core/errors.js';
10
+ import { errorMessage, getHttpStatusCode } from '../core/errors.js';
11
+ import { warn } from '../core/logger.js';
11
12
  import { emptyPRCountsResult } from '../core/github-stats.js';
12
13
  import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '../formatters/json.js';
14
+ const MODULE = 'daily';
15
+ /** Return true for errors that should propagate (not degrade gracefully). */
16
+ function isRateLimitOrAuthError(err) {
17
+ const status = getHttpStatusCode(err);
18
+ if (status === 401 || status === 429)
19
+ return true;
20
+ if (status === 403) {
21
+ const msg = errorMessage(err).toLowerCase();
22
+ return msg.includes('rate limit') || msg.includes('abuse detection');
23
+ }
24
+ return false;
25
+ }
13
26
  // Re-export domain functions so existing consumers (tests, dashboard, startup)
14
27
  // can continue importing from './daily.js' without changes.
15
28
  export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
@@ -26,35 +39,45 @@ async function fetchPRData(prMonitor, token) {
26
39
  const { prs, failures } = await prMonitor.fetchUserOpenPRs();
27
40
  // Log any failures (but continue with successful checks)
28
41
  if (failures.length > 0) {
29
- console.error(`Warning: ${failures.length} PR fetch(es) failed`);
42
+ warn(MODULE, `${failures.length} PR fetch(es) failed`);
30
43
  }
31
44
  // Fetch merged PR counts, closed PR counts, recently closed PRs, recently merged PRs, and commented issues in parallel
32
45
  // All stats fetches are non-critical (cosmetic/scoring), so isolate their failure
33
46
  const issueMonitor = new IssueConversationMonitor(token);
34
47
  const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all([
35
48
  prMonitor.fetchUserMergedPRCounts().catch((err) => {
36
- console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
49
+ if (isRateLimitOrAuthError(err))
50
+ throw err;
51
+ warn(MODULE, `Failed to fetch merged PR counts: ${errorMessage(err)}`);
37
52
  return emptyPRCountsResult();
38
53
  }),
39
54
  prMonitor.fetchUserClosedPRCounts().catch((err) => {
40
- console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
55
+ if (isRateLimitOrAuthError(err))
56
+ throw err;
57
+ warn(MODULE, `Failed to fetch closed PR counts: ${errorMessage(err)}`);
41
58
  return emptyPRCountsResult();
42
59
  }),
43
60
  prMonitor.fetchRecentlyClosedPRs().catch((err) => {
44
- console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
61
+ if (isRateLimitOrAuthError(err))
62
+ throw err;
63
+ warn(MODULE, `Failed to fetch recently closed PRs: ${errorMessage(err)}`);
45
64
  return [];
46
65
  }),
47
66
  prMonitor.fetchRecentlyMergedPRs().catch((err) => {
48
- console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
67
+ if (isRateLimitOrAuthError(err))
68
+ throw err;
69
+ warn(MODULE, `Failed to fetch recently merged PRs: ${errorMessage(err)}`);
49
70
  return [];
50
71
  }),
51
72
  issueMonitor.fetchCommentedIssues().catch((error) => {
73
+ if (isRateLimitOrAuthError(error))
74
+ throw error;
52
75
  const msg = errorMessage(error);
53
76
  if (msg.includes('No GitHub username configured')) {
54
- console.error(`[DAILY] Issue conversation tracking requires setup: ${msg}`);
77
+ warn(MODULE, `Issue conversation tracking requires setup: ${msg}`);
55
78
  }
56
79
  else {
57
- console.error(`[DAILY] Issue conversation fetch failed: ${msg}`);
80
+ warn(MODULE, `Issue conversation fetch failed: ${msg}`);
58
81
  }
59
82
  return {
60
83
  issues: [],
@@ -64,7 +87,7 @@ async function fetchPRData(prMonitor, token) {
64
87
  ]);
65
88
  const commentedIssues = issueConversationResult.issues;
66
89
  if (issueConversationResult.failures.length > 0) {
67
- console.error(`[DAILY] ${issueConversationResult.failures.length} issue conversation check(s) failed`);
90
+ warn(MODULE, `${issueConversationResult.failures.length} issue conversation check(s) failed`);
68
91
  }
69
92
  const { repos: mergedCounts, monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
70
93
  const { repos: closedCounts, monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed, } = closedResult;
@@ -94,7 +117,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
94
117
  // skip the reset to avoid wiping scores due to transient API failures.
95
118
  const existingReposWithMerges = Object.values(stateManager.getState().repoScores).filter((s) => s.mergedPRCount > 0);
96
119
  if (mergedCounts.size === 0 && existingReposWithMerges.length > 0) {
97
- console.error(`[DAILY] Skipping stale repo reset: API returned 0 merged PR results but state has ${existingReposWithMerges.length} repo(s) with merges. Possible API issue.`);
120
+ warn(MODULE, `Skipping stale repo reset: API returned 0 merged PR results but state has ${existingReposWithMerges.length} repo(s) with merges. Possible API issue.`);
98
121
  }
99
122
  else {
100
123
  for (const score of Object.values(stateManager.getState().repoScores)) {
@@ -111,18 +134,18 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
111
134
  }
112
135
  catch (error) {
113
136
  mergedCountFailures++;
114
- console.error(`[DAILY] Failed to update merged count for ${repo}:`, errorMessage(error));
137
+ warn(MODULE, `Failed to update merged count for ${repo}: ${errorMessage(error)}`);
115
138
  }
116
139
  }
117
140
  if (mergedCountFailures === mergedCounts.size && mergedCounts.size > 0) {
118
- console.error(`[DAILY_ALL_MERGED_COUNT_UPDATES_FAILED] All ${mergedCounts.size} merged count update(s) failed.`);
141
+ warn(MODULE, `[ALL_MERGED_COUNT_UPDATES_FAILED] All ${mergedCounts.size} merged count update(s) failed.`);
119
142
  }
120
143
  // Populate closedWithoutMergeCount in repo scores.
121
144
  // Diagnostic: warn if API returned empty but we have known closed PRs (possible transient API failure).
122
145
  // Unlike merged counts above, there is no stale-reset loop for closed counts, so no skip is needed.
123
146
  const existingReposWithClosed = Object.values(stateManager.getState().repoScores).filter((s) => (s.closedWithoutMergeCount || 0) > 0);
124
147
  if (closedCounts.size === 0 && existingReposWithClosed.length > 0) {
125
- console.error(`[DAILY] Warning: API returned 0 closed PR results but state has ${existingReposWithClosed.length} repo(s) with closed PRs. Possible transient API issue.`);
148
+ warn(MODULE, `API returned 0 closed PR results but state has ${existingReposWithClosed.length} repo(s) with closed PRs. Possible transient API issue.`);
126
149
  }
127
150
  let closedCountFailures = 0;
128
151
  for (const [repo, count] of closedCounts) {
@@ -131,11 +154,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
131
154
  }
132
155
  catch (error) {
133
156
  closedCountFailures++;
134
- console.error(`[DAILY] Failed to update closed count for ${repo}:`, errorMessage(error));
157
+ warn(MODULE, `Failed to update closed count for ${repo}: ${errorMessage(error)}`);
135
158
  }
136
159
  }
137
160
  if (closedCountFailures === closedCounts.size && closedCounts.size > 0) {
138
- console.error(`[DAILY_ALL_CLOSED_COUNT_UPDATES_FAILED] All ${closedCounts.size} closed count update(s) failed.`);
161
+ warn(MODULE, `[ALL_CLOSED_COUNT_UPDATES_FAILED] All ${closedCounts.size} closed count update(s) failed.`);
139
162
  }
140
163
  // Update repo signals from observed open PR data (responsiveness, active maintainers).
141
164
  // Only repos with current open PRs get signal updates — repos with no open PRs
@@ -150,11 +173,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
150
173
  }
151
174
  catch (error) {
152
175
  signalUpdateFailures++;
153
- console.error(`[DAILY] Failed to update signals for ${repo}:`, errorMessage(error));
176
+ warn(MODULE, `Failed to update signals for ${repo}: ${errorMessage(error)}`);
154
177
  }
155
178
  }
156
179
  if (signalUpdateFailures === repoSignals.size && repoSignals.size > 0) {
157
- console.error(`[DAILY_ALL_SIGNAL_UPDATES_FAILED] All ${repoSignals.size} signal update(s) failed. This may indicate corrupted state.`);
180
+ warn(MODULE, `[ALL_SIGNAL_UPDATES_FAILED] All ${repoSignals.size} signal update(s) failed. This may indicate corrupted state.`);
158
181
  }
159
182
  // Fetch star counts for all scored repos (used by dashboard minStars filter, #216)
160
183
  const allRepos = Object.keys(stateManager.getState().repoScores);
@@ -163,8 +186,8 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
163
186
  starCounts = await prMonitor.fetchRepoStarCounts(allRepos);
164
187
  }
165
188
  catch (error) {
166
- console.error('[DAILY] Failed to fetch repo star counts:', errorMessage(error));
167
- console.error('[DAILY] Dashboard minStars filter will use cached star counts (or be skipped for repos without cached data).');
189
+ warn(MODULE, `Failed to fetch repo star counts: ${errorMessage(error)}`);
190
+ warn(MODULE, 'Dashboard minStars filter will use cached star counts (or be skipped for repos without cached data).');
168
191
  starCounts = new Map();
169
192
  }
170
193
  let starUpdateFailures = 0;
@@ -174,11 +197,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
174
197
  }
175
198
  catch (error) {
176
199
  starUpdateFailures++;
177
- console.error(`[DAILY] Failed to update star count for ${repo}:`, errorMessage(error));
200
+ warn(MODULE, `Failed to update star count for ${repo}: ${errorMessage(error)}`);
178
201
  }
179
202
  }
180
203
  if (starUpdateFailures === starCounts.size && starCounts.size > 0) {
181
- console.error(`[DAILY_ALL_STAR_COUNT_UPDATES_FAILED] All ${starCounts.size} star count update(s) failed.`);
204
+ warn(MODULE, `[ALL_STAR_COUNT_UPDATES_FAILED] All ${starCounts.size} star count update(s) failed.`);
182
205
  }
183
206
  // Auto-sync trustedProjects from repos with merged PRs
184
207
  let trustSyncFailures = 0;
@@ -188,11 +211,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
188
211
  }
189
212
  catch (error) {
190
213
  trustSyncFailures++;
191
- console.error(`[DAILY] Failed to sync trusted project ${repo}:`, errorMessage(error));
214
+ warn(MODULE, `Failed to sync trusted project ${repo}: ${errorMessage(error)}`);
192
215
  }
193
216
  }
194
217
  if (trustSyncFailures === mergedCounts.size && mergedCounts.size > 0) {
195
- console.error(`[DAILY_ALL_TRUST_SYNCS_FAILED] All ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`);
218
+ warn(MODULE, `[ALL_TRUST_SYNCS_FAILED] All ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`);
196
219
  }
197
220
  }
198
221
  /**
@@ -211,7 +234,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
211
234
  }
212
235
  }
213
236
  catch (error) {
214
- console.error('[DAILY] Failed to store monthly merged counts:', errorMessage(error));
237
+ warn(MODULE, `Failed to store monthly merged counts: ${errorMessage(error)}`);
215
238
  }
216
239
  try {
217
240
  if (Object.keys(monthlyClosedCounts).length > 0) {
@@ -219,7 +242,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
219
242
  }
220
243
  }
221
244
  catch (error) {
222
- console.error('[DAILY] Failed to store monthly closed counts:', errorMessage(error));
245
+ warn(MODULE, `Failed to store monthly closed counts: ${errorMessage(error)}`);
223
246
  }
224
247
  try {
225
248
  // Build combined monthly opened counts from merged + closed + currently-open PRs
@@ -239,7 +262,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
239
262
  }
240
263
  }
241
264
  catch (error) {
242
- console.error('[DAILY] Failed to compute/store monthly opened counts:', errorMessage(error));
265
+ warn(MODULE, `Failed to compute/store monthly opened counts: ${errorMessage(error)}`);
243
266
  }
244
267
  }
245
268
  /**
@@ -254,15 +277,13 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
254
277
  try {
255
278
  const expiredSnoozes = stateManager.expireSnoozes();
256
279
  if (expiredSnoozes.length > 0) {
257
- console.error(`[DAILY] ${expiredSnoozes.length} snoozed PR(s) expired and will resurface:`);
258
- for (const url of expiredSnoozes) {
259
- console.error(` - ${url}`);
260
- }
280
+ const urls = expiredSnoozes.map((url) => ` - ${url}`).join('\n');
281
+ warn(MODULE, `${expiredSnoozes.length} snoozed PR(s) expired and will resurface:\n${urls}`);
261
282
  stateManager.save();
262
283
  }
263
284
  }
264
285
  catch (error) {
265
- console.error('[DAILY] Failed to expire/persist snoozes:', errorMessage(error));
286
+ warn(MODULE, `Failed to expire/persist snoozes: ${errorMessage(error)}`);
266
287
  }
267
288
  // Partition PRs into active vs shelved, auto-unshelving when maintainers engage
268
289
  const shelvedPRs = [];
@@ -322,12 +343,12 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
322
343
  if (isNaN(responseTime) || isNaN(dismissTime)) {
323
344
  // Invalid timestamp — fail open (include issue to be safe) without
324
345
  // permanently removing dismiss record (may be a transient data issue)
325
- console.error(`[DAILY] Invalid timestamp in dismiss check for ${issue.url}, including issue`);
346
+ warn(MODULE, `Invalid timestamp in dismiss check for ${issue.url}, including issue`);
326
347
  return true;
327
348
  }
328
349
  if (responseTime > dismissTime) {
329
350
  // New activity after dismiss — auto-undismiss and resurface
330
- console.error(`[DAILY] Auto-undismissing issue ${issue.url}: new response at ${issue.lastResponseAt} after dismiss at ${dismissedAt}`);
351
+ warn(MODULE, `Auto-undismissing issue ${issue.url}: new response at ${issue.lastResponseAt} after dismiss at ${dismissedAt}`);
331
352
  stateManager.undismissIssue(issue.url);
332
353
  hasAutoUndismissed = true;
333
354
  return true;
@@ -349,12 +370,12 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
349
370
  if (isNaN(activityTime) || isNaN(dismissTime)) {
350
371
  // Invalid timestamp — fail open (include PR to be safe) without
351
372
  // permanently removing dismiss record (may be a transient data issue)
352
- console.error(`[DAILY] Invalid timestamp in PR dismiss check for ${pr.url}, including PR`);
373
+ warn(MODULE, `Invalid timestamp in PR dismiss check for ${pr.url}, including PR`);
353
374
  return true;
354
375
  }
355
376
  if (activityTime > dismissTime) {
356
377
  // New activity after dismiss — auto-undismiss and resurface
357
- console.error(`[DAILY] Auto-undismissing PR ${pr.url}: new activity at ${pr.updatedAt} after dismiss at ${dismissedAt}`);
378
+ warn(MODULE, `Auto-undismissing PR ${pr.url}: new activity at ${pr.updatedAt} after dismiss at ${dismissedAt}`);
358
379
  stateManager.undismissIssue(pr.url);
359
380
  hasAutoUndismissed = true;
360
381
  return true;
@@ -368,7 +389,7 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
368
389
  stateManager.save();
369
390
  }
370
391
  catch (error) {
371
- console.error('[DAILY] Failed to persist auto-undismissed state:', errorMessage(error));
392
+ warn(MODULE, `Failed to persist auto-undismissed state: ${errorMessage(error)}`);
372
393
  }
373
394
  }
374
395
  const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls);
@@ -114,7 +114,7 @@ export async function findRunningDashboardServer() {
114
114
  function buildDashboardJson(digest, state, commentedIssues) {
115
115
  const prsByRepo = computePRsByRepo(digest, state);
116
116
  const topRepos = computeTopRepos(prsByRepo);
117
- const { monthlyMerged } = getMonthlyData(state);
117
+ const { monthlyMerged, monthlyOpened, monthlyClosed } = getMonthlyData(state);
118
118
  const stats = buildDashboardStats(digest, state);
119
119
  const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
120
120
  return {
@@ -122,6 +122,8 @@ function buildDashboardJson(digest, state, commentedIssues) {
122
122
  prsByRepo,
123
123
  topRepos: topRepos.map(([repo, data]) => ({ repo, ...data })),
124
124
  monthlyMerged,
125
+ monthlyOpened,
126
+ monthlyClosed,
125
127
  activePRs: digest.openPRs || [],
126
128
  shelvedPRUrls: state.config.shelvedPRUrls || [],
127
129
  commentedIssues,
@@ -8,8 +8,9 @@
8
8
  */
9
9
  import * as fs from 'fs';
10
10
  import { execFile } from 'child_process';
11
- import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js';
11
+ import { getStateManager, getGitHubToken, getCLIVersion, getStatePath, getDashboardPath } 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 { writeDashboardFromState } from './dashboard.js';
15
16
  import { launchDashboardServer } from './dashboard-lifecycle.js';
@@ -116,6 +117,25 @@ function openInBrowser(filePath) {
116
117
  }
117
118
  });
118
119
  }
120
+ /**
121
+ * Check whether the dashboard HTML file is at least as recent as state.json.
122
+ * Returns true when the dashboard exists and its mtime >= state mtime,
123
+ * meaning there is no need to regenerate it.
124
+ */
125
+ function isDashboardFresh() {
126
+ try {
127
+ const dashPath = getDashboardPath();
128
+ if (!fs.existsSync(dashPath))
129
+ return false;
130
+ const dashMtime = fs.statSync(dashPath).mtimeMs;
131
+ const stateMtime = fs.statSync(getStatePath()).mtimeMs;
132
+ return dashMtime >= stateMtime;
133
+ }
134
+ catch (error) {
135
+ warn('startup', `Failed to check dashboard freshness, will regenerate: ${errorMessage(error)}`);
136
+ return false;
137
+ }
138
+ }
119
139
  /**
120
140
  * Run startup checks and return structured output.
121
141
  * Returns StartupOutput with one of three shapes:
@@ -143,10 +163,17 @@ export async function runStartup() {
143
163
  }
144
164
  // 3. Run daily check
145
165
  const daily = await executeDailyCheck(token);
146
- // 4. Generate static HTML dashboard (always — serves as fallback + snapshot)
166
+ // 4. Generate static HTML dashboard (serves as fallback + snapshot).
167
+ // Skip regeneration if the dashboard HTML is already newer than state.json.
147
168
  let dashboardPath;
148
169
  try {
149
- dashboardPath = writeDashboardFromState();
170
+ if (isDashboardFresh()) {
171
+ dashboardPath = getDashboardPath();
172
+ console.error('[STARTUP] Dashboard HTML is fresh, skipping regeneration');
173
+ }
174
+ else {
175
+ dashboardPath = writeDashboardFromState();
176
+ }
150
177
  }
151
178
  catch (error) {
152
179
  console.error('[STARTUP] Dashboard generation failed:', errorMessage(error));
@@ -3,9 +3,10 @@
3
3
  * Vets a specific issue before working on it
4
4
  */
5
5
  import { IssueDiscovery, requireGitHubToken } from '../core/index.js';
6
- import { validateUrl } from './validation.js';
6
+ import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
7
7
  export async function runVet(options) {
8
8
  validateUrl(options.issueUrl);
9
+ validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue');
9
10
  const token = requireGitHubToken();
10
11
  const discovery = new IssueDiscovery(token);
11
12
  const candidate = await discovery.vetIssue(options.issueUrl);
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Runs a worker pool that processes items with bounded concurrency.
3
- * N workers consume from a shared index simpler than Promise.race + splice.
3
+ * N workers consume from a shared index. On any worker error, remaining
4
+ * workers are aborted via a shared flag and the error is propagated.
4
5
  */
5
6
  export declare function runWorkerPool<T>(items: T[], worker: (item: T) => Promise<void>, concurrency: number): Promise<void>;
@@ -1,13 +1,23 @@
1
1
  /**
2
2
  * Runs a worker pool that processes items with bounded concurrency.
3
- * N workers consume from a shared index simpler than Promise.race + splice.
3
+ * N workers consume from a shared index. On any worker error, remaining
4
+ * workers are aborted via a shared flag and the error is propagated.
4
5
  */
5
6
  export async function runWorkerPool(items, worker, concurrency) {
6
7
  let index = 0;
8
+ let aborted = false;
7
9
  const poolWorker = async () => {
8
10
  while (index < items.length) {
11
+ if (aborted)
12
+ break;
9
13
  const item = items[index++];
10
- await worker(item);
14
+ try {
15
+ await worker(item);
16
+ }
17
+ catch (err) {
18
+ aborted = true;
19
+ throw err;
20
+ }
11
21
  }
12
22
  };
13
23
  const workerCount = Math.min(concurrency, items.length);
@@ -350,11 +350,7 @@ export class PRMonitor {
350
350
  * Check if PR has merge conflict
351
351
  */
352
352
  hasMergeConflict(mergeable, mergeableState) {
353
- if (mergeable === false)
354
- return true;
355
- if (mergeableState === 'dirty')
356
- return true;
357
- return false;
353
+ return mergeable === false || mergeableState === 'dirty';
358
354
  }
359
355
  /**
360
356
  * Get CI status from combined status API and check runs.
@@ -371,6 +367,14 @@ export class PRMonitor {
371
367
  // 404 is expected for repos without check runs configured; log other errors for debugging
372
368
  this.octokit.checks.listForRef({ owner, repo, ref: sha }).catch((err) => {
373
369
  const status = getHttpStatusCode(err);
370
+ // Rate limit errors must propagate — matches listReviewComments pattern (#481)
371
+ if (status === 429)
372
+ throw err;
373
+ if (status === 403) {
374
+ const msg = errorMessage(err).toLowerCase();
375
+ if (msg.includes('rate limit') || msg.includes('abuse detection'))
376
+ throw err;
377
+ }
374
378
  if (status === 404) {
375
379
  debug('pr-monitor', `Check runs 404 for ${owner}/${repo}@${sha.slice(0, 7)} (no checks configured)`);
376
380
  }
@@ -400,12 +404,8 @@ export class PRMonitor {
400
404
  }
401
405
  catch (error) {
402
406
  const statusCode = getHttpStatusCode(error);
403
- const errMsg = errorMessage(error);
404
- if (statusCode === 401) {
405
- warn('pr-monitor', `CI check failed for ${owner}/${repo}: Invalid token`);
406
- }
407
- else if (statusCode === 403) {
408
- warn('pr-monitor', `CI check failed for ${owner}/${repo}: Rate limit exceeded`);
407
+ if (statusCode === 401 || statusCode === 403 || statusCode === 429) {
408
+ throw error;
409
409
  }
410
410
  else if (statusCode === 404) {
411
411
  // Repo might not have CI configured, this is normal
@@ -413,7 +413,7 @@ export class PRMonitor {
413
413
  return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
414
414
  }
415
415
  else {
416
- warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errMsg}`);
416
+ warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errorMessage(error)}`);
417
417
  }
418
418
  return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
419
419
  }
@@ -144,10 +144,9 @@ export declare function daysBetween(from: Date, to?: Date): number;
144
144
  /**
145
145
  * Splits an `"owner/repo"` string into its owner and repo components.
146
146
  *
147
- * Does not validate the input format; if no `/` is present, `repo` will be `undefined`.
148
- *
149
147
  * @param repoFullName - Full repository name in `"owner/repo"` format
150
148
  * @returns Object with `owner` and `repo` string properties
149
+ * @throws {Error} If the input does not contain both an owner and repo separated by `/`
151
150
  *
152
151
  * @example
153
152
  * splitRepo('facebook/react')
@@ -215,10 +215,9 @@ export function daysBetween(from, to = new Date()) {
215
215
  /**
216
216
  * Splits an `"owner/repo"` string into its owner and repo components.
217
217
  *
218
- * Does not validate the input format; if no `/` is present, `repo` will be `undefined`.
219
- *
220
218
  * @param repoFullName - Full repository name in `"owner/repo"` format
221
219
  * @returns Object with `owner` and `repo` string properties
220
+ * @throws {Error} If the input does not contain both an owner and repo separated by `/`
222
221
  *
223
222
  * @example
224
223
  * splitRepo('facebook/react')
@@ -226,6 +225,9 @@ export function daysBetween(from, to = new Date()) {
226
225
  */
227
226
  export function splitRepo(repoFullName) {
228
227
  const [owner, repo] = repoFullName.split('/');
228
+ if (!owner || !repo) {
229
+ throw new Error(`Invalid repo format: expected "owner/repo", got "${repoFullName}"`);
230
+ }
229
231
  return { owner, repo };
230
232
  }
231
233
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.43.0",
3
+ "version": "0.43.1",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsc",
63
- "bundle": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/cli.bundle.cjs",
63
+ "bundle": "esbuild src/cli.ts --bundle --platform=node --target=node20 --format=cjs --minify --outfile=dist/cli.bundle.cjs",
64
64
  "start": "tsx src/cli.ts",
65
65
  "dev": "tsx watch src/cli.ts",
66
66
  "typecheck": "tsc --noEmit",