@oss-autopilot/core 0.42.0 → 0.42.2

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 (50) hide show
  1. package/dist/cli.bundle.cjs +1026 -1018
  2. package/dist/cli.js +18 -30
  3. package/dist/commands/check-integration.js +5 -4
  4. package/dist/commands/comments.js +24 -24
  5. package/dist/commands/daily.d.ts +0 -1
  6. package/dist/commands/daily.js +18 -16
  7. package/dist/commands/dashboard-components.d.ts +33 -0
  8. package/dist/commands/dashboard-components.js +57 -0
  9. package/dist/commands/dashboard-data.js +7 -6
  10. package/dist/commands/dashboard-formatters.d.ts +20 -0
  11. package/dist/commands/dashboard-formatters.js +33 -0
  12. package/dist/commands/dashboard-scripts.d.ts +7 -0
  13. package/dist/commands/dashboard-scripts.js +281 -0
  14. package/dist/commands/dashboard-server.js +3 -2
  15. package/dist/commands/dashboard-styles.d.ts +5 -0
  16. package/dist/commands/dashboard-styles.js +765 -0
  17. package/dist/commands/dashboard-templates.d.ts +6 -18
  18. package/dist/commands/dashboard-templates.js +30 -1134
  19. package/dist/commands/dashboard.js +2 -1
  20. package/dist/commands/dismiss.d.ts +6 -6
  21. package/dist/commands/dismiss.js +13 -13
  22. package/dist/commands/local-repos.js +2 -1
  23. package/dist/commands/parse-list.js +2 -1
  24. package/dist/commands/startup.js +6 -16
  25. package/dist/commands/validation.d.ts +3 -1
  26. package/dist/commands/validation.js +12 -6
  27. package/dist/core/errors.d.ts +9 -0
  28. package/dist/core/errors.js +17 -0
  29. package/dist/core/github-stats.d.ts +14 -21
  30. package/dist/core/github-stats.js +84 -138
  31. package/dist/core/http-cache.d.ts +6 -0
  32. package/dist/core/http-cache.js +16 -4
  33. package/dist/core/index.d.ts +3 -2
  34. package/dist/core/index.js +3 -2
  35. package/dist/core/issue-conversation.js +4 -4
  36. package/dist/core/issue-discovery.d.ts +5 -0
  37. package/dist/core/issue-discovery.js +70 -93
  38. package/dist/core/issue-vetting.js +17 -17
  39. package/dist/core/logger.d.ts +5 -0
  40. package/dist/core/logger.js +8 -0
  41. package/dist/core/pr-monitor.d.ts +6 -20
  42. package/dist/core/pr-monitor.js +16 -52
  43. package/dist/core/review-analysis.js +8 -6
  44. package/dist/core/state.js +4 -5
  45. package/dist/core/test-utils.d.ts +14 -0
  46. package/dist/core/test-utils.js +125 -0
  47. package/dist/core/utils.d.ts +11 -0
  48. package/dist/core/utils.js +21 -0
  49. package/dist/formatters/json.d.ts +0 -1
  50. package/package.json +1 -1
@@ -15,7 +15,7 @@ import { getOctokit } from './github.js';
15
15
  import { getStateManager } from './state.js';
16
16
  import { daysBetween, parseGitHubUrl, extractOwnerRepo } from './utils.js';
17
17
  import { runWorkerPool } from './concurrency.js';
18
- import { ConfigurationError, ValidationError } from './errors.js';
18
+ import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode } from './errors.js';
19
19
  import { paginateAll } from './pagination.js';
20
20
  import { debug, warn, timed } from './logger.js';
21
21
  import { getHttpCache, cachedRequest } from './http-cache.js';
@@ -114,9 +114,9 @@ export class PRMonitor {
114
114
  prs.push(pr);
115
115
  }
116
116
  catch (error) {
117
- const errorMessage = error instanceof Error ? error.message : String(error);
118
- warn('pr-monitor', `Error fetching ${item.html_url}: ${errorMessage}`);
119
- failures.push({ prUrl: item.html_url, error: errorMessage });
117
+ const errMsg = errorMessage(error);
118
+ warn('pr-monitor', `Error fetching ${item.html_url}: ${errMsg}`);
119
+ failures.push({ prUrl: item.html_url, error: errMsg });
120
120
  }
121
121
  }, MAX_CONCURRENT_REQUESTS);
122
122
  });
@@ -162,14 +162,14 @@ export class PRMonitor {
162
162
  paginateAll((page) => this.octokit.issues.listComments({ owner, repo, issue_number: number, per_page: 100, page })),
163
163
  this.octokit.pulls.listReviews({ owner, repo, pull_number: number }),
164
164
  paginateAll((page) => this.octokit.pulls.listReviewComments({ owner, repo, pull_number: number, per_page: 100, page })).catch((err) => {
165
- const status = err?.status;
165
+ const status = getHttpStatusCode(err);
166
166
  // Rate limit errors must propagate — silently swallowing them hides
167
167
  // a systemic problem and produces misleading results (#229).
168
168
  if (status === 429) {
169
169
  throw err;
170
170
  }
171
171
  if (status === 403) {
172
- const msg = (err?.message ?? '').toLowerCase();
172
+ const msg = errorMessage(err).toLowerCase();
173
173
  if (msg.includes('rate limit') || msg.includes('abuse detection')) {
174
174
  throw err;
175
175
  }
@@ -283,6 +283,11 @@ export class PRMonitor {
283
283
  // If the contributor pushed a commit after the maintainer's comment,
284
284
  // the changes have been addressed — waiting for maintainer re-review
285
285
  if (latestCommitDate && lastMaintainerCommentDate && latestCommitDate > lastMaintainerCommentDate) {
286
+ // Safety net (#431): if a CHANGES_REQUESTED review was submitted after
287
+ // the commit, the maintainer still expects changes — don't mask it
288
+ if (latestChangesRequestedDate && latestCommitDate < latestChangesRequestedDate) {
289
+ return 'needs_response';
290
+ }
286
291
  if (ciStatus === 'failing')
287
292
  return 'failing_ci';
288
293
  return 'changes_addressed';
@@ -349,7 +354,7 @@ export class PRMonitor {
349
354
  this.octokit.repos.getCombinedStatusForRef({ owner, repo, ref: sha }),
350
355
  // 404 is expected for repos without check runs configured; log other errors for debugging
351
356
  this.octokit.checks.listForRef({ owner, repo, ref: sha }).catch((err) => {
352
- const status = err?.status;
357
+ const status = getHttpStatusCode(err);
353
358
  if (status === 404) {
354
359
  debug('pr-monitor', `Check runs 404 for ${owner}/${repo}@${sha.slice(0, 7)} (no checks configured)`);
355
360
  }
@@ -378,8 +383,8 @@ export class PRMonitor {
378
383
  return mergeStatuses(checkRunAnalysis, combinedAnalysis, checkRuns.length);
379
384
  }
380
385
  catch (error) {
381
- const statusCode = error.status;
382
- const errorMessage = error instanceof Error ? error.message : String(error);
386
+ const statusCode = getHttpStatusCode(error);
387
+ const errMsg = errorMessage(error);
383
388
  if (statusCode === 401) {
384
389
  warn('pr-monitor', `CI check failed for ${owner}/${repo}: Invalid token`);
385
390
  }
@@ -392,7 +397,7 @@ export class PRMonitor {
392
397
  return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
393
398
  }
394
399
  else {
395
- warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errorMessage}`);
400
+ warn('pr-monitor', `Failed to check CI for ${owner}/${repo}@${sha.slice(0, 7)}: ${errMsg}`);
396
401
  }
397
402
  return { status: 'unknown', failingCheckNames: [], failingCheckConclusions: new Map() };
398
403
  }
@@ -451,7 +456,7 @@ export class PRMonitor {
451
456
  }
452
457
  else {
453
458
  chunkFailures++;
454
- warn(MODULE, `Failed to fetch stars for ${chunk[j]}: ${result.reason instanceof Error ? result.reason.message : result.reason}`);
459
+ warn(MODULE, `Failed to fetch stars for ${chunk[j]}: ${errorMessage(result.reason)}`);
455
460
  }
456
461
  }
457
462
  // If entire chunk failed, likely a systemic issue (rate limit, auth, outage) — abort remaining
@@ -466,47 +471,6 @@ export class PRMonitor {
466
471
  debug(MODULE, `Fetched star counts for ${results.size}/${repos.length} repos`);
467
472
  return results;
468
473
  }
469
- /**
470
- * Shared helper: search for recent PRs and filter out own repos, excluded repos/orgs.
471
- * Returns parsed search results that pass all filters.
472
- */
473
- async fetchRecentPRs(query, label, days, mapItem) {
474
- const config = this.stateManager.getState().config;
475
- if (!config.githubUsername) {
476
- warn(MODULE, `Skipping recently ${label} PRs fetch: no githubUsername configured. Run /setup-oss to configure.`);
477
- return [];
478
- }
479
- const sinceDate = new Date();
480
- sinceDate.setDate(sinceDate.getDate() - days);
481
- const since = sinceDate.toISOString().split('T')[0]; // YYYY-MM-DD
482
- debug(MODULE, `Fetching recently ${label} PRs for @${config.githubUsername} (since ${since})...`);
483
- const { data } = await this.octokit.search.issuesAndPullRequests({
484
- q: query.replace('{username}', config.githubUsername).replace('{since}', since),
485
- sort: 'updated',
486
- order: 'desc',
487
- per_page: 100,
488
- });
489
- const results = [];
490
- for (const item of data.items) {
491
- const parsed = parseGitHubUrl(item.html_url);
492
- if (!parsed) {
493
- warn(MODULE, `Could not parse GitHub URL from API response: ${item.html_url}`);
494
- continue;
495
- }
496
- const repo = `${parsed.owner}/${parsed.repo}`;
497
- // Skip own repos
498
- if (parsed.owner.toLowerCase() === config.githubUsername.toLowerCase())
499
- continue;
500
- // Skip excluded repos and orgs
501
- if (config.excludeRepos.includes(repo))
502
- continue;
503
- if (config.excludeOrgs?.some((org) => parsed.owner.toLowerCase() === org.toLowerCase()))
504
- continue;
505
- results.push(mapItem(item, { owner: parsed.owner, repo, number: parsed.number }));
506
- }
507
- debug(MODULE, `Found ${results.length} recently ${label} PRs`);
508
- return results;
509
- }
510
474
  /**
511
475
  * Fetch PRs closed without merge in the last N days.
512
476
  * Delegates to github-stats module.
@@ -101,11 +101,11 @@ export function checkUnrespondedComments(comments, reviews, reviewComments, user
101
101
  if (!review.submitted_at)
102
102
  continue;
103
103
  const body = (review.body || '').trim();
104
- // Include COMMENTED reviews even without body text — they indicate
105
- // inline review comments were posted and may need a response (#151).
106
- // Skip other empty-body reviews (APPROVED, CHANGES_REQUESTED, DISMISSED)
107
- // as those are state changes without comment text.
108
- if (!body && review.state !== 'COMMENTED')
104
+ // Include COMMENTED and CHANGES_REQUESTED reviews even without body text —
105
+ // they indicate inline review comments were posted and need a response (#151, #431).
106
+ // CHANGES_REQUESTED with only inline comments is actionable maintainer feedback.
107
+ // Skip other empty-body reviews (APPROVED, DISMISSED) as those are state changes.
108
+ if (!body && review.state !== 'COMMENTED' && review.state !== 'CHANGES_REQUESTED')
109
109
  continue;
110
110
  const author = review.user?.login || 'unknown';
111
111
  // For inline-only COMMENTED reviews, skip pure self-replies (#199)
@@ -117,7 +117,9 @@ export function checkUnrespondedComments(comments, reviews, reviewComments, user
117
117
  // Resolve body: prefer actual text, then inline comment text, then synthetic placeholder
118
118
  const resolvedBody = body ||
119
119
  (review.id != null ? getInlineCommentBody(review.id, reviewComments) : undefined) ||
120
- '(posted inline review comments)';
120
+ (review.state === 'CHANGES_REQUESTED'
121
+ ? '(requested changes via inline review comments)'
122
+ : '(posted inline review comments)');
121
123
  timeline.push({
122
124
  author,
123
125
  body: resolvedBody,
@@ -6,7 +6,7 @@ import * as fs from 'fs';
6
6
  import * as path from 'path';
7
7
  import { INITIAL_STATE, } from './types.js';
8
8
  import { getStatePath, getBackupDir, getDataDir } from './utils.js';
9
- import { ValidationError } from './errors.js';
9
+ import { ValidationError, errorMessage } from './errors.js';
10
10
  import { debug, warn } from './logger.js';
11
11
  const MODULE = 'state';
12
12
  // Current state version
@@ -272,8 +272,7 @@ export class StateManager {
272
272
  return true;
273
273
  }
274
274
  catch (error) {
275
- const errorMessage = error instanceof Error ? error.message : String(error);
276
- warn(MODULE, `Failed to migrate state: ${errorMessage}`);
275
+ warn(MODULE, `Failed to migrate state: ${errorMessage(error)}`);
277
276
  // Clean up partial migration to avoid inconsistent state
278
277
  const newStatePath = getStatePath();
279
278
  if (fs.existsSync(newStatePath) && fs.existsSync(LEGACY_STATE_FILE)) {
@@ -471,12 +470,12 @@ export class StateManager {
471
470
  fs.unlinkSync(path.join(backupDir, file));
472
471
  }
473
472
  catch (error) {
474
- warn(MODULE, `Could not delete old backup ${file}:`, error instanceof Error ? error.message : error);
473
+ warn(MODULE, `Could not delete old backup ${file}:`, errorMessage(error));
475
474
  }
476
475
  }
477
476
  }
478
477
  catch (error) {
479
- warn(MODULE, 'Could not clean up backups:', error instanceof Error ? error.message : error);
478
+ warn(MODULE, 'Could not clean up backups:', errorMessage(error));
480
479
  }
481
480
  }
482
481
  /**
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared test factories for oss-autopilot.
3
+ *
4
+ * Centralises mock object construction so that when types gain new required
5
+ * fields we only update one place. Every factory accepts a `Partial<T>`
6
+ * override bag — callers only specify the fields relevant to their test.
7
+ */
8
+ import type { FetchedPR, DailyDigest, ShelvedPRRef, AgentState } from './types.js';
9
+ import type { CapacityAssessment } from '../formatters/json.js';
10
+ export declare function makeFetchedPR(overrides?: Partial<FetchedPR>): FetchedPR;
11
+ export declare function makeDailyDigest(overrides?: Partial<DailyDigest>): DailyDigest;
12
+ export declare function makeShelvedPRRef(overrides?: Partial<ShelvedPRRef>): ShelvedPRRef;
13
+ export declare function makeCapacityAssessment(overrides?: Partial<CapacityAssessment>): CapacityAssessment;
14
+ export declare function makeAgentState(overrides?: Partial<AgentState>): AgentState;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shared test factories for oss-autopilot.
3
+ *
4
+ * Centralises mock object construction so that when types gain new required
5
+ * fields we only update one place. Every factory accepts a `Partial<T>`
6
+ * override bag — callers only specify the fields relevant to their test.
7
+ */
8
+ // ---------------------------------------------------------------------------
9
+ // FetchedPR
10
+ // ---------------------------------------------------------------------------
11
+ export function makeFetchedPR(overrides = {}) {
12
+ const repo = overrides.repo ?? 'owner/repo';
13
+ const number = overrides.number ?? 1;
14
+ return {
15
+ id: 1,
16
+ url: `https://github.com/${repo}/pull/${number}`,
17
+ repo,
18
+ number,
19
+ title: 'Test PR',
20
+ status: 'healthy',
21
+ displayLabel: '[Healthy]',
22
+ displayDescription: 'Everything looks good',
23
+ createdAt: '2025-06-01T00:00:00Z',
24
+ updatedAt: '2025-06-15T00:00:00Z',
25
+ daysSinceActivity: 2,
26
+ ciStatus: 'passing',
27
+ failingCheckNames: [],
28
+ classifiedChecks: [],
29
+ hasMergeConflict: false,
30
+ reviewDecision: 'approved',
31
+ hasUnrespondedComment: false,
32
+ hasIncompleteChecklist: false,
33
+ maintainerActionHints: [],
34
+ ...overrides,
35
+ };
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // DailyDigest
39
+ // ---------------------------------------------------------------------------
40
+ export function makeDailyDigest(overrides = {}) {
41
+ return {
42
+ generatedAt: '2025-06-20T00:00:00Z',
43
+ openPRs: [],
44
+ prsNeedingResponse: [],
45
+ ciFailingPRs: [],
46
+ ciBlockedPRs: [],
47
+ ciNotRunningPRs: [],
48
+ mergeConflictPRs: [],
49
+ needsRebasePRs: [],
50
+ missingRequiredFilesPRs: [],
51
+ incompleteChecklistPRs: [],
52
+ needsChangesPRs: [],
53
+ changesAddressedPRs: [],
54
+ waitingOnMaintainerPRs: [],
55
+ approachingDormant: [],
56
+ dormantPRs: [],
57
+ healthyPRs: [],
58
+ recentlyClosedPRs: [],
59
+ recentlyMergedPRs: [],
60
+ shelvedPRs: [],
61
+ autoUnshelvedPRs: [],
62
+ summary: {
63
+ totalActivePRs: 0,
64
+ totalNeedingAttention: 0,
65
+ totalMergedAllTime: 0,
66
+ mergeRate: 0,
67
+ },
68
+ ...overrides,
69
+ };
70
+ }
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
+ // CapacityAssessment
87
+ // ---------------------------------------------------------------------------
88
+ export function makeCapacityAssessment(overrides = {}) {
89
+ return {
90
+ hasCapacity: true,
91
+ activePRCount: 3,
92
+ maxActivePRs: 10,
93
+ shelvedPRCount: 0,
94
+ criticalIssueCount: 0,
95
+ reason: 'You have capacity',
96
+ ...overrides,
97
+ };
98
+ }
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
+ }
@@ -157,6 +157,17 @@ export declare function splitRepo(repoFullName: string): {
157
157
  owner: string;
158
158
  repo: string;
159
159
  };
160
+ /**
161
+ * Case-insensitive check whether a repo owner matches the given GitHub username.
162
+ * Used to skip a user's own repos (PRs to your own repos aren't OSS contributions).
163
+ */
164
+ export declare function isOwnRepo(owner: string, username: string): boolean;
165
+ /**
166
+ * Read the CLI package version from package.json relative to the running CLI bundle.
167
+ * Resolves `../package.json` from `process.argv[1]` (the bundle entry point).
168
+ * Falls back to '0.0.0' if the file is unreadable.
169
+ */
170
+ export declare function getCLIVersion(): string;
160
171
  /**
161
172
  * Formats a timestamp as a human-readable relative time string.
162
173
  *
@@ -228,6 +228,27 @@ export function splitRepo(repoFullName) {
228
228
  const [owner, repo] = repoFullName.split('/');
229
229
  return { owner, repo };
230
230
  }
231
+ /**
232
+ * Case-insensitive check whether a repo owner matches the given GitHub username.
233
+ * Used to skip a user's own repos (PRs to your own repos aren't OSS contributions).
234
+ */
235
+ export function isOwnRepo(owner, username) {
236
+ return owner.toLowerCase() === username.toLowerCase();
237
+ }
238
+ /**
239
+ * Read the CLI package version from package.json relative to the running CLI bundle.
240
+ * Resolves `../package.json` from `process.argv[1]` (the bundle entry point).
241
+ * Falls back to '0.0.0' if the file is unreadable.
242
+ */
243
+ export function getCLIVersion() {
244
+ try {
245
+ const pkgPath = path.join(path.dirname(process.argv[1]), '..', 'package.json');
246
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
247
+ }
248
+ catch {
249
+ return '0.0.0';
250
+ }
251
+ }
231
252
  /**
232
253
  * Formats a timestamp as a human-readable relative time string.
233
254
  *
@@ -108,7 +108,6 @@ export interface CompactRepoGroup {
108
108
  }
109
109
  export interface DailyOutput {
110
110
  digest: DailyDigestCompact;
111
- updates: unknown[];
112
111
  capacity: CapacityAssessment;
113
112
  summary: string;
114
113
  briefSummary: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.42.0",
3
+ "version": "0.42.2",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {