@oss-autopilot/core 0.44.1 → 0.44.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,10 @@
2
2
  * Custom error type hierarchy for oss-autopilot.
3
3
  * Provides structured error codes and specific error classes
4
4
  * for different failure categories.
5
+ *
6
+ * Error strategy: Rate-limit and auth errors (429, 401, 403+rate-limit) always
7
+ * propagate to the caller via isRateLimitError/isRateLimitOrAuthError.
8
+ * Other errors degrade gracefully — modules return partial results and log warnings.
5
9
  */
6
10
  /**
7
11
  * Base error for all oss-autopilot errors.
@@ -31,3 +35,7 @@ export declare function errorMessage(e: unknown): string;
31
35
  * Returns undefined if the error doesn't have a numeric `status` property.
32
36
  */
33
37
  export declare function getHttpStatusCode(error: unknown): number | undefined;
38
+ /** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
39
+ export declare function isRateLimitError(error: unknown): boolean;
40
+ /** Return true for errors that should propagate (not degrade gracefully): rate limits, auth failures, abuse detection. */
41
+ export declare function isRateLimitOrAuthError(err: unknown): boolean;
@@ -2,6 +2,10 @@
2
2
  * Custom error type hierarchy for oss-autopilot.
3
3
  * Provides structured error codes and specific error classes
4
4
  * for different failure categories.
5
+ *
6
+ * Error strategy: Rate-limit and auth errors (429, 401, 403+rate-limit) always
7
+ * propagate to the caller via isRateLimitError/isRateLimitOrAuthError.
8
+ * Other errors degrade gracefully — modules return partial results and log warnings.
5
9
  */
6
10
  /**
7
11
  * Base error for all oss-autopilot errors.
@@ -49,3 +53,25 @@ export function getHttpStatusCode(error) {
49
53
  }
50
54
  return undefined;
51
55
  }
56
+ /** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
57
+ export function isRateLimitError(error) {
58
+ const status = getHttpStatusCode(error);
59
+ if (status === 429)
60
+ return true;
61
+ if (status === 403) {
62
+ const msg = errorMessage(error).toLowerCase();
63
+ return msg.includes('rate limit');
64
+ }
65
+ return false;
66
+ }
67
+ /** Return true for errors that should propagate (not degrade gracefully): rate limits, auth failures, abuse detection. */
68
+ export function isRateLimitOrAuthError(err) {
69
+ const status = getHttpStatusCode(err);
70
+ if (status === 401 || status === 429)
71
+ return true;
72
+ if (status === 403) {
73
+ const msg = errorMessage(err).toLowerCase();
74
+ return msg.includes('rate limit') || msg.includes('abuse detection');
75
+ }
76
+ return false;
77
+ }
@@ -28,26 +28,6 @@ export declare function fetchUserMergedPRCounts(octokit: Octokit, githubUsername
28
28
  * Used to populate closedWithoutMergeCount in repo scores for accurate merge rate.
29
29
  */
30
30
  export declare function fetchUserClosedPRCounts(octokit: Octokit, githubUsername: string): Promise<PRCountsResult<number>>;
31
- /**
32
- * Shared helper: search for recent PRs and filter out own repos, excluded repos/orgs.
33
- * Returns parsed search results that pass all filters.
34
- */
35
- export declare function fetchRecentPRs<T>(octokit: Octokit, config: {
36
- githubUsername: string;
37
- excludeRepos: string[];
38
- excludeOrgs?: string[];
39
- }, query: string, label: string, days: number, mapItem: (item: {
40
- html_url: string;
41
- title: string;
42
- closed_at: string | null;
43
- pull_request?: {
44
- merged_at?: string | null;
45
- };
46
- }, parsed: {
47
- owner: string;
48
- repo: string;
49
- number: number;
50
- }) => T): Promise<T[]>;
51
31
  /**
52
32
  * Fetch PRs closed without merge in the last N days.
53
33
  * Returns lightweight ClosedPR objects for surfacing in the daily digest.
@@ -168,7 +168,7 @@ export function fetchUserClosedPRCounts(octokit, githubUsername) {
168
168
  * Shared helper: search for recent PRs and filter out own repos, excluded repos/orgs.
169
169
  * Returns parsed search results that pass all filters.
170
170
  */
171
- export async function fetchRecentPRs(octokit, config, query, label, days, mapItem) {
171
+ async function fetchRecentPRs(octokit, config, query, label, days, mapItem) {
172
172
  if (!config.githubUsername) {
173
173
  warn(MODULE, `Skipping recently ${label} PRs fetch: no githubUsername configured. Run /setup-oss to configure.`);
174
174
  return [];
@@ -19,7 +19,7 @@ export function getOctokit(token) {
19
19
  _octokit = new ThrottledOctokit({
20
20
  auth: token,
21
21
  throttle: {
22
- onRateLimit: (retryAfter, options, octokit, retryCount) => {
22
+ onRateLimit: (retryAfter, options, _octokit, retryCount) => {
23
23
  const opts = options;
24
24
  const resetAt = new Date(Date.now() + retryAfter * 1000);
25
25
  if (retryCount < 2) {
@@ -29,7 +29,7 @@ export function getOctokit(token) {
29
29
  warn(MODULE, `Rate limit exceeded, not retrying — ${opts.method} ${opts.url} (resets at ${formatResetTime(resetAt)})`);
30
30
  return false;
31
31
  },
32
- onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => {
32
+ onSecondaryRateLimit: (retryAfter, options, _octokit, retryCount) => {
33
33
  const opts = options;
34
34
  const resetAt = new Date(Date.now() + retryAfter * 1000);
35
35
  if (retryCount < 1) {
@@ -78,8 +78,6 @@ export declare class HttpCache {
78
78
  * The singleton is lazily initialized on first access.
79
79
  */
80
80
  export declare function getHttpCache(): HttpCache;
81
- /** Reset the singleton (for tests). */
82
- export declare function resetHttpCache(): void;
83
81
  /**
84
82
  * Wraps an Octokit `repos.get`-style call with ETag caching and request
85
83
  * deduplication.
@@ -204,10 +204,6 @@ export function getHttpCache() {
204
204
  }
205
205
  return _httpCache;
206
206
  }
207
- /** Reset the singleton (for tests). */
208
- export function resetHttpCache() {
209
- _httpCache = null;
210
- }
211
207
  // ---------------------------------------------------------------------------
212
208
  // Octokit integration helpers
213
209
  // ---------------------------------------------------------------------------
@@ -8,9 +8,9 @@ export { IssueDiscovery, type IssueCandidate, type SearchPriority, isDocOnlyIssu
8
8
  export { IssueConversationMonitor } from './issue-conversation.js';
9
9
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
10
  export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
11
- export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
12
- export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
11
+ export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, DEFAULT_CONCURRENCY, } from './utils.js';
12
+ export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
13
13
  export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
14
- export { HttpCache, getHttpCache, resetHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
14
+ export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
15
15
  export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
16
16
  export * from './types.js';
@@ -8,9 +8,9 @@ export { IssueDiscovery, isDocOnlyIssue, applyPerRepoCap, DOC_ONLY_LABELS, } fro
8
8
  export { IssueConversationMonitor } from './issue-conversation.js';
9
9
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
10
  export { getOctokit, checkRateLimit } from './github.js';
11
- export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, } from './utils.js';
12
- export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
11
+ export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, getDashboardPath, formatRelativeTime, byDateDescending, getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, DEFAULT_CONCURRENCY, } from './utils.js';
12
+ export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
13
13
  export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
14
- export { HttpCache, getHttpCache, resetHttpCache, cachedRequest } from './http-cache.js';
14
+ export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
15
15
  export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
16
16
  export * from './types.js';
@@ -9,12 +9,12 @@ import { getOctokit } from './github.js';
9
9
  import { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
10
  import { paginateAll } from './pagination.js';
11
11
  import { getStateManager } from './state.js';
12
- import { daysBetween, splitRepo, extractOwnerRepo, isOwnRepo } from './utils.js';
12
+ import { daysBetween, splitRepo, extractOwnerRepo, isOwnRepo, DEFAULT_CONCURRENCY } from './utils.js';
13
13
  import { runWorkerPool } from './concurrency.js';
14
14
  import { ConfigurationError, errorMessage } from './errors.js';
15
15
  import { debug, warn } from './logger.js';
16
16
  const MODULE = 'issue-conversation';
17
- const MAX_CONCURRENT_REQUESTS = 5;
17
+ const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
18
18
  /** Associations that indicate someone with repo-level permissions. */
19
19
  const MAINTAINER_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
20
20
  export class IssueConversationMonitor {
@@ -65,11 +65,6 @@ export declare class IssueDiscovery {
65
65
  * Split repos into batches of the specified size.
66
66
  */
67
67
  private batchRepos;
68
- /**
69
- * Check if an error is a GitHub rate limit error (429 or rate-limit 403).
70
- * Static proxy kept for backward compatibility with tests.
71
- */
72
- static isRateLimitError(error: unknown): boolean;
73
68
  /**
74
69
  * Vet a specific issue (delegates to IssueVetter).
75
70
  */
@@ -12,7 +12,7 @@ import { getOctokit, checkRateLimit } from './github.js';
12
12
  import { getStateManager } from './state.js';
13
13
  import { daysBetween, getDataDir } from './utils.js';
14
14
  import { DEFAULT_CONFIG } from './types.js';
15
- import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
15
+ import { ValidationError, errorMessage, getHttpStatusCode, isRateLimitError } from './errors.js';
16
16
  import { debug, info, warn } from './logger.js';
17
17
  import { getHttpCache, cachedTimeBased } from './http-cache.js';
18
18
  import { isDocOnlyIssue, detectLabelFarmingRepos, applyPerRepoCap } from './issue-filtering.js';
@@ -328,7 +328,7 @@ export class IssueDiscovery {
328
328
  catch (error) {
329
329
  const errMsg = errorMessage(error);
330
330
  phase2Error = errMsg;
331
- if (IssueVetter.isRateLimitError(error)) {
331
+ if (isRateLimitError(error)) {
332
332
  rateLimitHitDuringSearch = true;
333
333
  }
334
334
  warn(MODULE, `Error in general issue search: ${errMsg}`);
@@ -370,7 +370,7 @@ export class IssueDiscovery {
370
370
  catch (error) {
371
371
  const errMsg = errorMessage(error);
372
372
  phase3Error = errMsg;
373
- if (IssueVetter.isRateLimitError(error)) {
373
+ if (isRateLimitError(error)) {
374
374
  rateLimitHitDuringSearch = true;
375
375
  }
376
376
  warn(MODULE, `Error in maintained-repo search: ${errMsg}`);
@@ -464,7 +464,7 @@ export class IssueDiscovery {
464
464
  }
465
465
  catch (error) {
466
466
  failedBatches++;
467
- if (IssueVetter.isRateLimitError(error)) {
467
+ if (isRateLimitError(error)) {
468
468
  rateLimitFailures++;
469
469
  }
470
470
  const batchRepos = batch.join(', ');
@@ -489,13 +489,6 @@ export class IssueDiscovery {
489
489
  }
490
490
  return batches;
491
491
  }
492
- /**
493
- * Check if an error is a GitHub rate limit error (429 or rate-limit 403).
494
- * Static proxy kept for backward compatibility with tests.
495
- */
496
- static isRateLimitError(error) {
497
- return IssueVetter.isRateLimitError(error);
498
- }
499
492
  /**
500
493
  * Vet a specific issue (delegates to IssueVetter).
501
494
  */
@@ -29,8 +29,6 @@ export declare class IssueVetter {
29
29
  allFailed: boolean;
30
30
  rateLimitHit: boolean;
31
31
  }>;
32
- /** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
33
- static isRateLimitError(error: unknown): boolean;
34
32
  checkNoExistingPR(owner: string, repo: string, issueNumber: number): Promise<CheckResult>;
35
33
  /**
36
34
  * Check how many merged PRs the authenticated user has in a repo.
@@ -5,14 +5,13 @@
5
5
  * Extracted from issue-discovery.ts (#356) to isolate vetting logic.
6
6
  */
7
7
  import { paginateAll } from './pagination.js';
8
- import { parseGitHubUrl, daysBetween } from './utils.js';
9
- import { ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
8
+ import { parseGitHubUrl, daysBetween, DEFAULT_CONCURRENCY } from './utils.js';
9
+ import { ValidationError, errorMessage, isRateLimitError } from './errors.js';
10
10
  import { warn } from './logger.js';
11
11
  import { getHttpCache, cachedRequest, cachedTimeBased } from './http-cache.js';
12
12
  import { calculateRepoQualityBonus, calculateViabilityScore } from './issue-scoring.js';
13
13
  const MODULE = 'issue-vetting';
14
- // Concurrency limit for parallel API calls
15
- const MAX_CONCURRENT_REQUESTS = 5;
14
+ const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
16
15
  // Cache for contribution guidelines (expires after 1 hour, max 100 entries)
17
16
  const guidelinesCache = new Map();
18
17
  const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
@@ -233,7 +232,7 @@ export class IssueVetter {
233
232
  */
234
233
  async vetIssuesParallel(urls, maxResults, priority) {
235
234
  const candidates = [];
236
- const pending = [];
235
+ const pending = new Map();
237
236
  let failedVettingCount = 0;
238
237
  let rateLimitFailures = 0;
239
238
  let attemptedCount = 0;
@@ -253,21 +252,20 @@ export class IssueVetter {
253
252
  })
254
253
  .catch((error) => {
255
254
  failedVettingCount++;
256
- if (IssueVetter.isRateLimitError(error)) {
255
+ if (isRateLimitError(error)) {
257
256
  rateLimitFailures++;
258
257
  }
259
258
  warn(MODULE, `Error vetting issue ${url}:`, errorMessage(error));
260
- });
261
- pending.push(task);
262
- // Limit concurrency
263
- if (pending.length >= MAX_CONCURRENT_REQUESTS) {
264
- // Wait for at least one to complete, then remove it
265
- const completed = await Promise.race(pending.map((p, i) => p.then(() => i)));
266
- pending.splice(completed, 1);
259
+ })
260
+ .finally(() => pending.delete(url));
261
+ pending.set(url, task);
262
+ // Limit concurrency wait for at least one to complete before launching more
263
+ if (pending.size >= MAX_CONCURRENT_REQUESTS) {
264
+ await Promise.race(pending.values());
267
265
  }
268
266
  }
269
267
  // Wait for remaining
270
- await Promise.allSettled(pending);
268
+ await Promise.allSettled(pending.values());
271
269
  const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
272
270
  if (allFailed) {
273
271
  warn(MODULE, `All ${attemptedCount} issue(s) failed vetting. ` +
@@ -275,17 +273,6 @@ export class IssueVetter {
275
273
  }
276
274
  return { candidates: candidates.slice(0, maxResults), allFailed, rateLimitHit: rateLimitFailures > 0 };
277
275
  }
278
- /** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
279
- static isRateLimitError(error) {
280
- const status = getHttpStatusCode(error);
281
- if (status === 429)
282
- return true;
283
- if (status === 403) {
284
- const msg = errorMessage(error).toLowerCase();
285
- return msg.includes('rate limit');
286
- }
287
- return false;
288
- }
289
276
  async checkNoExistingPR(owner, repo, issueNumber) {
290
277
  try {
291
278
  // Search for PRs that mention this issue
@@ -447,26 +434,25 @@ export class IssueVetter {
447
434
  return cached.guidelines;
448
435
  }
449
436
  const filesToCheck = ['CONTRIBUTING.md', '.github/CONTRIBUTING.md', 'docs/CONTRIBUTING.md', 'contributing.md'];
450
- for (const file of filesToCheck) {
451
- try {
452
- const { data } = await this.octokit.repos.getContent({
453
- owner,
454
- repo,
455
- path: file,
456
- });
457
- if ('content' in data) {
458
- const content = Buffer.from(data.content, 'base64').toString('utf-8');
459
- const guidelines = this.parseContributionGuidelines(content);
460
- // Cache the result and prune if needed
461
- guidelinesCache.set(cacheKey, { guidelines, fetchedAt: Date.now() });
462
- pruneCache();
463
- return guidelines;
464
- }
437
+ // Probe all paths in parallel — take the first success in priority order
438
+ const results = await Promise.allSettled(filesToCheck.map((file) => this.octokit.repos.getContent({ owner, repo, path: file }).then(({ data }) => {
439
+ if ('content' in data) {
440
+ return Buffer.from(data.content, 'base64').toString('utf-8');
441
+ }
442
+ return null;
443
+ })));
444
+ for (let i = 0; i < results.length; i++) {
445
+ const result = results[i];
446
+ if (result.status === 'fulfilled' && result.value) {
447
+ const guidelines = this.parseContributionGuidelines(result.value);
448
+ guidelinesCache.set(cacheKey, { guidelines, fetchedAt: Date.now() });
449
+ pruneCache();
450
+ return guidelines;
465
451
  }
466
- catch (error) {
467
- // File not found is expected; only log unexpected errors
468
- if (error instanceof Error && !error.message.includes('404') && !error.message.includes('Not Found')) {
469
- warn(MODULE, `Unexpected error fetching ${file} from ${owner}/${repo}: ${error.message}`);
452
+ if (result.status === 'rejected') {
453
+ const msg = result.reason instanceof Error ? result.reason.message : String(result.reason);
454
+ if (!msg.includes('404') && !msg.includes('Not Found')) {
455
+ warn(MODULE, `Unexpected error fetching ${filesToCheck[i]} from ${owner}/${repo}: ${msg}`);
470
456
  }
471
457
  }
472
458
  }
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { getOctokit } from './github.js';
15
15
  import { getStateManager } from './state.js';
16
- import { daysBetween, parseGitHubUrl, extractOwnerRepo } from './utils.js';
16
+ import { daysBetween, parseGitHubUrl, extractOwnerRepo, DEFAULT_CONCURRENCY } from './utils.js';
17
17
  import { runWorkerPool } from './concurrency.js';
18
18
  import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
19
19
  import { paginateAll } from './pagination.js';
@@ -31,8 +31,7 @@ export { computeDisplayLabel } from './display-utils.js';
31
31
  export { classifyCICheck, classifyFailingChecks } from './ci-analysis.js';
32
32
  export { isConditionalChecklistItem } from './checklist-analysis.js';
33
33
  const MODULE = 'pr-monitor';
34
- // Concurrency limit for parallel API calls
35
- const MAX_CONCURRENT_REQUESTS = 5;
34
+ const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
36
35
  export class PRMonitor {
37
36
  octokit;
38
37
  stateManager;
@@ -482,7 +481,7 @@ export class PRMonitor {
482
481
  }
483
482
  // If entire chunk failed, likely a systemic issue (rate limit, auth, outage) — abort remaining
484
483
  if (chunkFailures === chunk.length && chunk.length > 0) {
485
- const remaining = repos.length - i - chunkSize;
484
+ const remaining = uniqueRepos.length - i - chunkSize;
486
485
  if (remaining > 0) {
487
486
  warn(MODULE, `Entire chunk failed, aborting remaining ${remaining} repos`);
488
487
  }
@@ -5,10 +5,8 @@
5
5
  * fields we only update one place. Every factory accepts a `Partial<T>`
6
6
  * override bag — callers only specify the fields relevant to their test.
7
7
  */
8
- import type { FetchedPR, DailyDigest, ShelvedPRRef, AgentState } from './types.js';
8
+ import type { FetchedPR, DailyDigest } from './types.js';
9
9
  import type { CapacityAssessment } from '../formatters/json.js';
10
10
  export declare function makeFetchedPR(overrides?: Partial<FetchedPR>): FetchedPR;
11
11
  export declare function makeDailyDigest(overrides?: Partial<DailyDigest>): DailyDigest;
12
- export declare function makeShelvedPRRef(overrides?: Partial<ShelvedPRRef>): ShelvedPRRef;
13
12
  export declare function makeCapacityAssessment(overrides?: Partial<CapacityAssessment>): CapacityAssessment;
14
- export declare function makeAgentState(overrides?: Partial<AgentState>): AgentState;
@@ -69,20 +69,6 @@ export function makeDailyDigest(overrides = {}) {
69
69
  };
70
70
  }
71
71
  // ---------------------------------------------------------------------------
72
- // ShelvedPRRef
73
- // ---------------------------------------------------------------------------
74
- export function makeShelvedPRRef(overrides = {}) {
75
- return {
76
- number: 1,
77
- url: 'https://github.com/owner/repo/pull/1',
78
- title: 'Shelved PR',
79
- repo: 'owner/repo',
80
- daysSinceActivity: 45,
81
- status: 'healthy',
82
- ...overrides,
83
- };
84
- }
85
- // ---------------------------------------------------------------------------
86
72
  // CapacityAssessment
87
73
  // ---------------------------------------------------------------------------
88
74
  export function makeCapacityAssessment(overrides = {}) {
@@ -96,30 +82,3 @@ export function makeCapacityAssessment(overrides = {}) {
96
82
  ...overrides,
97
83
  };
98
84
  }
99
- // ---------------------------------------------------------------------------
100
- // AgentState (partial — for tests that need a state object)
101
- // ---------------------------------------------------------------------------
102
- export function makeAgentState(overrides = {}) {
103
- return {
104
- version: 2,
105
- repoScores: {},
106
- config: {
107
- setupComplete: false,
108
- githubUsername: 'testuser',
109
- excludeRepos: [],
110
- maxActivePRs: 10,
111
- dormantThresholdDays: 30,
112
- approachingDormantDays: 25,
113
- maxIssueAgeDays: 90,
114
- languages: [],
115
- labels: [],
116
- trustedProjects: [],
117
- minRepoScoreThreshold: 4,
118
- starredRepos: [],
119
- },
120
- events: [],
121
- lastRunAt: '2025-06-20T00:00:00Z',
122
- activeIssues: [],
123
- ...overrides,
124
- };
125
- }
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Shared utility functions
3
3
  */
4
+ /** Default concurrency limit for parallel GitHub API requests. */
5
+ export declare const DEFAULT_CONCURRENCY = 5;
4
6
  /**
5
7
  * Returns the oss-autopilot data directory path, creating it if it does not exist.
6
8
  *
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Shared utility functions
3
3
  */
4
+ /** Default concurrency limit for parallel GitHub API requests. */
5
+ export const DEFAULT_CONCURRENCY = 5;
4
6
  import * as fs from 'fs';
5
7
  import * as path from 'path';
6
8
  import * as os from 'os';
@@ -207,7 +209,7 @@ export function extractOwnerRepo(url) {
207
209
  * // -9
208
210
  */
209
211
  export function daysBetween(from, to = new Date()) {
210
- return Math.floor((to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24));
212
+ return Math.max(0, Math.floor((to.getTime() - from.getTime()) / (1000 * 60 * 60 * 24)));
211
213
  }
212
214
  /**
213
215
  * Splits an `"owner/repo"` string into its owner and repo components.
@@ -268,6 +270,8 @@ export function getCLIVersion() {
268
270
  export function formatRelativeTime(dateStr) {
269
271
  const date = new Date(dateStr);
270
272
  const diffMs = Date.now() - date.getTime();
273
+ if (diffMs < 0)
274
+ return 'just now';
271
275
  const diffMins = Math.floor(diffMs / 60000);
272
276
  const diffHours = Math.floor(diffMs / 3600000);
273
277
  const diffDays = Math.floor(diffMs / 86400000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.44.1",
3
+ "version": "0.44.3",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {