@oss-autopilot/core 0.54.0 → 0.56.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 (37) 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 +190 -157
  5. package/dist/commands/dashboard-data.js +37 -30
  6. package/dist/commands/dashboard-server.js +0 -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 +89 -12
  17. package/dist/core/daily-logic.js +24 -33
  18. package/dist/core/index.d.ts +2 -1
  19. package/dist/core/index.js +2 -1
  20. package/dist/core/issue-discovery.d.ts +7 -44
  21. package/dist/core/issue-discovery.js +83 -188
  22. package/dist/core/issue-eligibility.d.ts +35 -0
  23. package/dist/core/issue-eligibility.js +126 -0
  24. package/dist/core/issue-vetting.d.ts +6 -21
  25. package/dist/core/issue-vetting.js +15 -279
  26. package/dist/core/pr-monitor.d.ts +7 -12
  27. package/dist/core/pr-monitor.js +14 -80
  28. package/dist/core/repo-health.d.ts +24 -0
  29. package/dist/core/repo-health.js +193 -0
  30. package/dist/core/search-phases.d.ts +55 -0
  31. package/dist/core/search-phases.js +155 -0
  32. package/dist/core/state.d.ts +11 -0
  33. package/dist/core/state.js +63 -4
  34. package/dist/core/types.d.ts +8 -1
  35. package/dist/core/types.js +7 -0
  36. package/dist/formatters/json.d.ts +1 -1
  37. package/package.json +1 -1
@@ -21,6 +21,8 @@ export class StateManager {
21
21
  state;
22
22
  inMemoryOnly;
23
23
  lastLoadedMtimeMs = 0;
24
+ _batching = false;
25
+ _batchDirty = false;
24
26
  /**
25
27
  * Create a new StateManager instance.
26
28
  * @param inMemoryOnly - When true, state is held only in memory and never read from or
@@ -38,6 +40,37 @@ export class StateManager {
38
40
  this.lastLoadedMtimeMs = result.mtimeMs;
39
41
  }
40
42
  }
43
+ /**
44
+ * Execute multiple mutations as a single batch, deferring disk I/O until the
45
+ * batch completes. Nested `batch()` calls are flattened — only the outermost saves.
46
+ */
47
+ batch(fn) {
48
+ if (this._batching) {
49
+ fn();
50
+ return;
51
+ }
52
+ this._batching = true;
53
+ this._batchDirty = false;
54
+ try {
55
+ fn();
56
+ if (this._batchDirty)
57
+ this.save();
58
+ }
59
+ finally {
60
+ this._batching = false;
61
+ this._batchDirty = false;
62
+ }
63
+ }
64
+ /**
65
+ * Auto-persist after a mutation. Inside a `batch()`, defers to the batch boundary.
66
+ */
67
+ autoSave() {
68
+ if (this._batching) {
69
+ this._batchDirty = true;
70
+ return;
71
+ }
72
+ this.save();
73
+ }
41
74
  /**
42
75
  * Check if initial setup has been completed.
43
76
  */
@@ -50,6 +83,7 @@ export class StateManager {
50
83
  markSetupComplete() {
51
84
  this.state.config.setupComplete = true;
52
85
  this.state.config.setupCompletedAt = new Date().toISOString();
86
+ this.autoSave();
53
87
  }
54
88
  /**
55
89
  * Initialize state with sensible defaults for zero-config onboarding.
@@ -60,10 +94,11 @@ export class StateManager {
60
94
  debug(MODULE, `Setup already complete, skipping initializeWithDefaults for "${username}"`);
61
95
  return;
62
96
  }
63
- this.state.config.githubUsername = username;
64
- this.markSetupComplete();
65
- debug(MODULE, `Initialized with defaults for user "${username}"`);
66
- this.save();
97
+ this.batch(() => {
98
+ this.updateConfig({ githubUsername: username });
99
+ this.markSetupComplete();
100
+ debug(MODULE, `Initialized with defaults for user "${username}"`);
101
+ });
67
102
  }
68
103
  /**
69
104
  * Persist the current state to disk, creating a timestamped backup of the previous
@@ -100,21 +135,27 @@ export class StateManager {
100
135
  setLastDigest(digest) {
101
136
  this.state.lastDigest = digest;
102
137
  this.state.lastDigestAt = digest.generatedAt;
138
+ this.autoSave();
103
139
  }
104
140
  setMonthlyMergedCounts(counts) {
105
141
  this.state.monthlyMergedCounts = counts;
142
+ this.autoSave();
106
143
  }
107
144
  setMonthlyClosedCounts(counts) {
108
145
  this.state.monthlyClosedCounts = counts;
146
+ this.autoSave();
109
147
  }
110
148
  setMonthlyOpenedCounts(counts) {
111
149
  this.state.monthlyOpenedCounts = counts;
150
+ this.autoSave();
112
151
  }
113
152
  setDailyActivityCounts(counts) {
114
153
  this.state.dailyActivityCounts = counts;
154
+ this.autoSave();
115
155
  }
116
156
  setLocalRepoCache(cache) {
117
157
  this.state.localRepoCache = cache;
158
+ this.autoSave();
118
159
  }
119
160
  // === Merged PR Storage ===
120
161
  getMergedPRs() {
@@ -132,6 +173,7 @@ export class StateManager {
132
173
  this.state.mergedPRs.push(...newPRs);
133
174
  this.state.mergedPRs.sort((a, b) => b.mergedAt.localeCompare(a.mergedAt));
134
175
  debug(MODULE, `Added ${newPRs.length} merged PRs (total: ${this.state.mergedPRs.length})`);
176
+ this.autoSave();
135
177
  }
136
178
  getMergedPRWatermark() {
137
179
  return this.state.mergedPRs?.[0]?.mergedAt || undefined;
@@ -152,6 +194,7 @@ export class StateManager {
152
194
  this.state.closedPRs.push(...newPRs);
153
195
  this.state.closedPRs.sort((a, b) => b.closedAt.localeCompare(a.closedAt));
154
196
  debug(MODULE, `Added ${newPRs.length} closed PRs (total: ${this.state.closedPRs.length})`);
197
+ this.autoSave();
155
198
  }
156
199
  getClosedPRWatermark() {
157
200
  return this.state.closedPRs?.[0]?.closedAt || undefined;
@@ -159,6 +202,7 @@ export class StateManager {
159
202
  // === Configuration ===
160
203
  updateConfig(config) {
161
204
  this.state.config = { ...this.state.config, ...config };
205
+ this.autoSave();
162
206
  }
163
207
  // === Event Logging ===
164
208
  appendEvent(type, data) {
@@ -173,6 +217,7 @@ export class StateManager {
173
217
  if (this.state.events.length > MAX_EVENTS) {
174
218
  this.state.events = this.state.events.slice(-MAX_EVENTS);
175
219
  }
220
+ this.autoSave();
176
221
  }
177
222
  getEventsByType(type) {
178
223
  return this.state.events.filter((e) => e.type === type);
@@ -192,12 +237,14 @@ export class StateManager {
192
237
  }
193
238
  this.state.activeIssues.push(issue);
194
239
  debug(MODULE, `Added issue: ${issue.repo}#${issue.number}`);
240
+ this.autoSave();
195
241
  }
196
242
  // === Trusted Projects ===
197
243
  addTrustedProject(repo) {
198
244
  if (!this.state.config.trustedProjects.includes(repo)) {
199
245
  this.state.config.trustedProjects.push(repo);
200
246
  debug(MODULE, `Added trusted project: ${repo}`);
247
+ this.autoSave();
201
248
  }
202
249
  }
203
250
  static matchesExclusion(repo, repos, orgs) {
@@ -215,6 +262,7 @@ export class StateManager {
215
262
  const removedTrusted = beforeTrusted - this.state.config.trustedProjects.length;
216
263
  if (removedTrusted > 0) {
217
264
  debug(MODULE, `Removed ${removedTrusted} trusted project(s) for excluded repos/orgs`);
265
+ this.autoSave();
218
266
  }
219
267
  }
220
268
  // === Starred Repos Management ===
@@ -225,6 +273,7 @@ export class StateManager {
225
273
  this.state.config.starredRepos = repos;
226
274
  this.state.config.starredReposLastFetched = new Date().toISOString();
227
275
  debug(MODULE, `Updated starred repos: ${repos.length} repositories`);
276
+ this.autoSave();
228
277
  }
229
278
  isStarredReposStale() {
230
279
  const lastFetched = this.state.config.starredReposLastFetched;
@@ -245,6 +294,7 @@ export class StateManager {
245
294
  return false;
246
295
  }
247
296
  this.state.config.shelvedPRUrls.push(url);
297
+ this.autoSave();
248
298
  return true;
249
299
  }
250
300
  unshelvePR(url) {
@@ -256,6 +306,7 @@ export class StateManager {
256
306
  return false;
257
307
  }
258
308
  this.state.config.shelvedPRUrls.splice(index, 1);
309
+ this.autoSave();
259
310
  return true;
260
311
  }
261
312
  isPRShelved(url) {
@@ -270,6 +321,7 @@ export class StateManager {
270
321
  return false;
271
322
  }
272
323
  this.state.config.dismissedIssues[url] = timestamp;
324
+ this.autoSave();
273
325
  return true;
274
326
  }
275
327
  undismissIssue(url) {
@@ -277,6 +329,7 @@ export class StateManager {
277
329
  return false;
278
330
  }
279
331
  delete this.state.config.dismissedIssues[url];
332
+ this.autoSave();
280
333
  return true;
281
334
  }
282
335
  getIssueDismissedAt(url) {
@@ -292,12 +345,14 @@ export class StateManager {
292
345
  setAt: new Date().toISOString(),
293
346
  lastActivityAt,
294
347
  };
348
+ this.autoSave();
295
349
  }
296
350
  clearStatusOverride(url) {
297
351
  if (!this.state.config.statusOverrides || !(url in this.state.config.statusOverrides)) {
298
352
  return false;
299
353
  }
300
354
  delete this.state.config.statusOverrides[url];
355
+ this.autoSave();
301
356
  return true;
302
357
  }
303
358
  getStatusOverride(url, currentUpdatedAt) {
@@ -317,15 +372,19 @@ export class StateManager {
317
372
  }
318
373
  updateRepoScore(repo, updates) {
319
374
  repoScoring.updateRepoScore(this.state, repo, updates);
375
+ this.autoSave();
320
376
  }
321
377
  incrementMergedCount(repo) {
322
378
  repoScoring.incrementMergedCount(this.state, repo);
379
+ this.autoSave();
323
380
  }
324
381
  incrementClosedCount(repo) {
325
382
  repoScoring.incrementClosedCount(this.state, repo);
383
+ this.autoSave();
326
384
  }
327
385
  markRepoHostile(repo) {
328
386
  repoScoring.markRepoHostile(this.state, repo);
387
+ this.autoSave();
329
388
  }
330
389
  getReposWithMergedPRs() {
331
390
  return repoScoring.getReposWithMergedPRs(this.state);
@@ -25,7 +25,7 @@ export interface ClassifiedCheck {
25
25
  category: CIFailureCategory;
26
26
  conclusion?: string;
27
27
  }
28
- /** Return type for PRMonitor.getCIStatus(). */
28
+ /** CI status result returned by getCIStatus(). */
29
29
  export interface CIStatusResult {
30
30
  status: CIStatus;
31
31
  failingCheckNames: string[];
@@ -468,6 +468,8 @@ export interface AgentConfig {
468
468
  languages: string[];
469
469
  /** GitHub labels to filter issues by (e.g., `["good first issue", "help wanted"]`). */
470
470
  labels: string[];
471
+ /** Issue scope tiers to search (e.g., `["beginner", "intermediate"]`). When set, scope tier labels are merged with custom `labels`. When absent, only `labels` is used (legacy behavior). */
472
+ scope?: IssueScope[];
471
473
  /** Repos to exclude from issue discovery/search, in `"owner/repo"` format. */
472
474
  excludeRepos: string[];
473
475
  /** Organizations to exclude from issue discovery/search (case-insensitive match on owner segment). */
@@ -498,6 +500,8 @@ export interface AgentConfig {
498
500
  dismissedIssues?: Record<string, string>;
499
501
  /** Manual status overrides for PRs. Maps PR URL to override metadata. Auto-clears when the PR has new activity. */
500
502
  statusOverrides?: Record<string, StatusOverride>;
503
+ /** Path to the user's curated issue list file. Replaces config.md as the primary source for detectIssueList(). */
504
+ issueListPath?: string;
501
505
  /** Project categories the user is interested in (e.g., devtools, nonprofit). Used to prioritize search results. */
502
506
  projectCategories?: ProjectCategory[];
503
507
  /** GitHub organizations the user wants to prioritize in issue search. Org names only (not owner/repo). */
@@ -542,6 +546,9 @@ export declare const DEFAULT_CONFIG: AgentConfig;
542
546
  export declare const INITIAL_STATE: AgentState;
543
547
  export declare const PROJECT_CATEGORIES: readonly ["nonprofit", "devtools", "infrastructure", "web-frameworks", "data-ml", "education"];
544
548
  export type ProjectCategory = (typeof PROJECT_CATEGORIES)[number];
549
+ export declare const ISSUE_SCOPES: readonly ["beginner", "intermediate", "advanced"];
550
+ export type IssueScope = (typeof ISSUE_SCOPES)[number];
551
+ export declare const SCOPE_LABELS: Record<IssueScope, string[]>;
545
552
  /** Priority tier for issue search results. Ordered: merged_pr > preferred_org > starred > normal. */
546
553
  export type SearchPriority = 'merged_pr' | 'preferred_org' | 'starred' | 'normal';
547
554
  export interface IssueCandidate {
@@ -49,3 +49,10 @@ export const PROJECT_CATEGORIES = [
49
49
  'data-ml',
50
50
  'education',
51
51
  ];
52
+ // -- Issue scope types --
53
+ export const ISSUE_SCOPES = ['beginner', 'intermediate', 'advanced'];
54
+ export const SCOPE_LABELS = {
55
+ beginner: ['good first issue', 'help wanted', 'easy', 'up-for-grabs', 'first-timers-only', 'beginner'],
56
+ intermediate: ['enhancement', 'feature', 'feature-request', 'contributions welcome'],
57
+ advanced: ['proposal', 'RFC', 'accepted', 'design'],
58
+ };
@@ -5,7 +5,7 @@
5
5
  import type { FetchedPR, DailyDigest, AgentState, RepoGroup, CommentedIssue, ShelvedPRRef } from '../core/types.js';
6
6
  import type { ContributionStats } from '../core/stats.js';
7
7
  import type { PRCheckFailure } from '../core/pr-monitor.js';
8
- import type { SearchPriority } from '../core/issue-discovery.js';
8
+ import type { SearchPriority } from '../core/types.js';
9
9
  export interface JsonOutput<T = unknown> {
10
10
  success: boolean;
11
11
  data?: T;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.54.0",
3
+ "version": "0.56.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {