@oss-autopilot/core 0.51.1 → 0.53.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.
@@ -1,32 +1,18 @@
1
1
  /**
2
- * State management for the OSS Contribution Agent
3
- * Persists state to a JSON file in ~/.oss-autopilot/
2
+ * State management for the OSS Contribution Agent.
3
+ * Thin coordinator that delegates persistence to state-persistence.ts
4
+ * and scoring logic to repo-score-manager.ts.
4
5
  */
5
6
  import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, StatusOverride, FetchedPRStatus, StoredMergedPR, StoredClosedPR } from './types.js';
6
- /**
7
- * Acquire an advisory file lock using exclusive-create (`wx` flag).
8
- * If the lock file already exists but is stale (older than LOCK_TIMEOUT_MS or corrupt),
9
- * it is removed and re-acquired.
10
- * @throws Error if the lock is held by another active process.
11
- */
12
- export declare function acquireLock(lockPath: string): void;
13
- /**
14
- * Release an advisory file lock, but only if this process owns it.
15
- * Silently ignores missing lock files or locks owned by other processes.
16
- */
17
- export declare function releaseLock(lockPath: string): void;
18
- /**
19
- * Write data to `filePath` atomically by first writing to a temporary file
20
- * in the same directory and then renaming. Rename is atomic on POSIX filesystems,
21
- * preventing partial/corrupt state files if the process crashes mid-write.
22
- */
23
- export declare function atomicWriteFileSync(filePath: string, data: string, mode?: number): void;
7
+ import type { Stats } from './repo-score-manager.js';
8
+ export { acquireLock, releaseLock, atomicWriteFileSync } from './state-persistence.js';
9
+ export type { Stats } from './repo-score-manager.js';
24
10
  /**
25
11
  * Singleton manager for persistent agent state stored in ~/.oss-autopilot/state.json.
26
12
  *
27
- * Handles loading, saving, backup/restore, and v1-to-v2 migration of state. Supports
28
- * an in-memory mode (no disk I/O) for use in tests. In v2 architecture, PR arrays are
29
- * legacy -- open PRs are fetched fresh from GitHub on each run rather than stored locally.
13
+ * Delegates file I/O to state-persistence.ts and scoring logic to repo-score-manager.ts.
14
+ * Retains lightweight CRUD operations for config, events, issues, shelving, dismissal,
15
+ * and status overrides.
30
16
  */
31
17
  export declare class StateManager {
32
18
  private state;
@@ -39,13 +25,8 @@ export declare class StateManager {
39
25
  * Defaults to false (normal persistent mode).
40
26
  */
41
27
  constructor(inMemoryOnly?: boolean);
42
- /**
43
- * Create a fresh state (v2: fresh GitHub fetching)
44
- */
45
- private createFreshState;
46
28
  /**
47
29
  * Check if initial setup has been completed.
48
- * @returns true if the user has run `/setup-oss` and completed configuration.
49
30
  */
50
31
  isSetupComplete(): boolean;
51
32
  /**
@@ -54,349 +35,71 @@ export declare class StateManager {
54
35
  markSetupComplete(): void;
55
36
  /**
56
37
  * Initialize state with sensible defaults for zero-config onboarding.
57
- * Sets the GitHub username, marks setup as complete, and persists.
58
- * No-op if setup is already complete (prevents overwriting existing config).
59
- * @param username - The GitHub username to configure.
38
+ * No-op if setup is already complete.
60
39
  */
61
40
  initializeWithDefaults(username: string): void;
62
- /**
63
- * Migrate state from legacy ./data/ location to ~/.oss-autopilot/
64
- * Returns true if migration was performed
65
- */
66
- private migrateFromLegacyLocation;
67
- /**
68
- * Load state from file, or create initial state if none exists.
69
- * If the main state file is corrupted, attempts to restore from the most recent backup.
70
- * Performs migration from legacy ./data/ location if needed.
71
- */
72
- private load;
73
- /**
74
- * Attempt to restore state from the most recent valid backup.
75
- * Returns the restored state if successful, or null if no valid backup is found.
76
- */
77
- private tryRestoreFromBackup;
78
- /**
79
- * Validate that a loaded state has the required structure
80
- * Handles both v1 (with PR arrays) and v2 (without)
81
- */
82
- private isValidState;
83
41
  /**
84
42
  * Persist the current state to disk, creating a timestamped backup of the previous
85
- * state file first. Updates `lastRunAt` to the current time. In in-memory mode,
86
- * only updates `lastRunAt` without any file I/O. Retains at most 10 backup files.
43
+ * state file first. In in-memory mode, only updates `lastRunAt` without any file I/O.
87
44
  */
88
45
  save(): void;
89
- private cleanupBackups;
90
46
  /**
91
47
  * Get the current state as a read-only snapshot.
92
- * @returns The full agent state. Callers should not mutate the returned object;
93
- * use the StateManager methods to make changes.
94
48
  */
95
49
  getState(): Readonly<AgentState>;
96
50
  /**
97
51
  * Re-read state from disk if the file has been modified since the last load/save.
98
- * Uses mtime comparison (single statSync call) to avoid unnecessary JSON parsing.
99
52
  * Returns true if state was reloaded, false if unchanged or in-memory mode.
100
53
  */
101
54
  reloadIfChanged(): boolean;
102
- /**
103
- * Store the latest daily digest for dashboard rendering.
104
- * @param digest - The freshly generated digest from the current daily run.
105
- */
106
55
  setLastDigest(digest: DailyDigest): void;
107
- /**
108
- * Store monthly merged PR counts for the contribution timeline chart.
109
- * @param counts - Map of "YYYY-MM" strings to merged PR counts for that month.
110
- */
111
56
  setMonthlyMergedCounts(counts: Record<string, number>): void;
112
- /**
113
- * Store monthly closed (without merge) PR counts for the contribution timeline and success rate charts.
114
- * @param counts - Map of "YYYY-MM" strings to closed PR counts for that month.
115
- */
116
57
  setMonthlyClosedCounts(counts: Record<string, number>): void;
117
- /**
118
- * Store monthly opened PR counts for the contribution timeline chart.
119
- * @param counts - Map of "YYYY-MM" strings to opened PR counts for that month.
120
- */
121
58
  setMonthlyOpenedCounts(counts: Record<string, number>): void;
122
59
  setDailyActivityCounts(counts: Record<string, number>): void;
123
- /**
124
- * Get all stored merged PRs.
125
- * @returns Array of stored merged PRs, sorted by mergedAt desc.
126
- */
60
+ setLocalRepoCache(cache: LocalRepoCache): void;
127
61
  getMergedPRs(): StoredMergedPR[];
128
- /**
129
- * Add new merged PRs to the stored list. Deduplicates by URL and sorts by mergedAt desc.
130
- * @param prs - New merged PRs to add.
131
- */
132
62
  addMergedPRs(prs: StoredMergedPR[]): void;
133
- /**
134
- * Get the most recent mergedAt timestamp from stored merged PRs.
135
- * Used as the watermark for incremental fetching.
136
- * @returns ISO date string of the most recent merge, or undefined if no stored PRs.
137
- */
138
63
  getMergedPRWatermark(): string | undefined;
139
- /**
140
- * Get all stored closed PRs.
141
- * @returns Array of stored closed PRs, sorted by closedAt desc.
142
- */
143
64
  getClosedPRs(): StoredClosedPR[];
144
- /**
145
- * Add new closed PRs to the stored list. Deduplicates by URL and sorts by closedAt desc.
146
- * @param prs - New closed PRs to add.
147
- */
148
65
  addClosedPRs(prs: StoredClosedPR[]): void;
149
- /**
150
- * Get the most recent closedAt timestamp from stored closed PRs.
151
- * Used as the watermark for incremental fetching.
152
- * @returns ISO date string of the most recent close, or undefined if no stored PRs.
153
- */
154
66
  getClosedPRWatermark(): string | undefined;
155
- /**
156
- * Store cached local repo scan results (#84).
157
- * @param cache - The scan results, paths scanned, and timestamp.
158
- */
159
- setLocalRepoCache(cache: LocalRepoCache): void;
160
- /**
161
- * Shallow-merge partial configuration updates into the current config.
162
- * @param config - Partial config object whose properties override existing values.
163
- */
164
67
  updateConfig(config: Partial<AgentState['config']>): void;
165
- /**
166
- * Append an event to the event log. Events are capped at {@link MAX_EVENTS} (1000);
167
- * when the cap is exceeded, the oldest events are trimmed to stay within the limit.
168
- * @param type - The event type (e.g. 'pr_tracked').
169
- * @param data - Arbitrary key-value payload for the event.
170
- */
171
68
  appendEvent(type: StateEventType, data: Record<string, unknown>): void;
172
- /**
173
- * Filter the event log to events of a specific type.
174
- * @param type - The event type to filter by.
175
- * @returns All events matching the given type, in chronological order.
176
- */
177
69
  getEventsByType(type: StateEventType): StateEvent[];
178
- /**
179
- * Filter the event log to events within an inclusive time range.
180
- * @param since - Start of the range (inclusive).
181
- * @param until - End of the range (inclusive). Defaults to now.
182
- * @returns Events whose timestamps fall within [since, until].
183
- */
184
70
  getEventsInRange(since: Date, until?: Date): StateEvent[];
185
- /**
186
- * Add an issue to the active tracking list. If an issue with the same URL is
187
- * already tracked, the call is a no-op.
188
- * @param issue - The issue to begin tracking.
189
- */
190
71
  addIssue(issue: TrackedIssue): void;
191
- /**
192
- * Add a repository to the trusted projects list. Trusted projects are prioritized
193
- * in issue search results. No-op if the repo is already trusted.
194
- * @param repo - Repository in "owner/repo" format.
195
- */
196
72
  addTrustedProject(repo: string): void;
197
- /**
198
- * Test whether a repo matches any of the given exclusion lists.
199
- * Both repo and org comparisons are case-insensitive (GitHub names are case-insensitive).
200
- * @param repo - Repository in "owner/repo" format.
201
- * @param repos - Full "owner/repo" strings (case-insensitive match).
202
- * @param orgs - Org names (case-insensitive match against the owner segment of the repo).
203
- */
204
73
  private static matchesExclusion;
205
- /**
206
- * Remove repositories matching the given exclusion lists from `trustedProjects`.
207
- * Called when a repo or org is newly excluded.
208
- *
209
- * Note: `repoScores` are intentionally preserved so historical stats (merge rate,
210
- * total merged) remain accurate. Exclusion only affects issue discovery (#591).
211
- * @param repos - Full "owner/repo" strings to exclude (case-insensitive match).
212
- * @param orgs - Org names to exclude (case-insensitive match against owner segment).
213
- */
214
74
  cleanupExcludedData(repos: string[], orgs: string[]): void;
215
- /**
216
- * Get the cached list of the user's GitHub starred repositories.
217
- * @returns Array of "owner/repo" strings, or an empty array if never fetched.
218
- */
219
75
  getStarredRepos(): string[];
220
- /**
221
- * Replace the cached starred repositories list and update the fetch timestamp.
222
- * @param repos - Array of "owner/repo" strings from the user's GitHub stars.
223
- */
224
76
  setStarredRepos(repos: string[]): void;
225
- /**
226
- * Check if the starred repos cache is stale (older than 24 hours) or has never been fetched.
227
- * @returns true if the cache should be refreshed.
228
- */
229
77
  isStarredReposStale(): boolean;
230
- /**
231
- * Shelve a PR by URL. Shelved PRs are excluded from capacity and actionable issues.
232
- * They are auto-unshelved when a maintainer engages (needs_response, needs_changes, etc.).
233
- * @param url - The full GitHub PR URL.
234
- * @returns true if newly added, false if already shelved.
235
- */
236
78
  shelvePR(url: string): boolean;
237
- /**
238
- * Unshelve a PR by URL.
239
- * @param url - The full GitHub PR URL.
240
- * @returns true if found and removed, false if not shelved.
241
- */
242
79
  unshelvePR(url: string): boolean;
243
- /**
244
- * Check if a PR is shelved.
245
- * @param url - The full GitHub PR URL.
246
- * @returns true if the URL is in the shelved list.
247
- */
248
80
  isPRShelved(url: string): boolean;
249
- /**
250
- * Dismiss an issue by URL. Dismissed issues are excluded from `new_response` notifications
251
- * until new activity occurs after the dismiss timestamp.
252
- * @param url - The full GitHub issue URL.
253
- * @param timestamp - ISO timestamp of when the issue was dismissed.
254
- * @returns true if newly dismissed, false if already dismissed.
255
- */
256
81
  dismissIssue(url: string, timestamp: string): boolean;
257
- /**
258
- * Undismiss an issue by URL.
259
- * @param url - The full GitHub issue URL.
260
- * @returns true if found and removed, false if not dismissed.
261
- */
262
82
  undismissIssue(url: string): boolean;
263
- /**
264
- * Get the timestamp when an issue was dismissed.
265
- * @param url - The full GitHub issue URL.
266
- * @returns The ISO dismiss timestamp, or undefined if not dismissed.
267
- */
268
83
  getIssueDismissedAt(url: string): string | undefined;
269
- /**
270
- * Set a manual status override for a PR.
271
- * @param url - The full GitHub PR URL.
272
- * @param status - The target status to override to.
273
- * @param lastActivityAt - The PR's current updatedAt timestamp (for auto-clear detection).
274
- */
275
84
  setStatusOverride(url: string, status: FetchedPRStatus, lastActivityAt: string): void;
276
- /**
277
- * Clear a status override for a PR.
278
- * @param url - The full GitHub PR URL.
279
- * @returns true if found and removed, false if no override existed.
280
- */
281
85
  clearStatusOverride(url: string): boolean;
282
- /**
283
- * Get the status override for a PR, if one exists and hasn't been auto-cleared.
284
- * @param url - The full GitHub PR URL.
285
- * @param currentUpdatedAt - The PR's current updatedAt from GitHub. If newer than
286
- * the stored lastActivityAt, the override is stale and auto-cleared.
287
- * @returns The override metadata, or undefined if none exists or it was auto-cleared.
288
- */
289
86
  getStatusOverride(url: string, currentUpdatedAt?: string): StatusOverride | undefined;
290
- /**
291
- * Get the score record for a repository.
292
- * @param repo - Repository in "owner/repo" format.
293
- * @returns The RepoScore if the repo has been scored, or undefined if never evaluated.
294
- */
295
- getRepoScore(repo: string): RepoScore | undefined;
296
- /**
297
- * Create a default repo score for a new repository
298
- */
299
- private createDefaultRepoScore;
300
- /**
301
- * Calculate the score based on the repo's metrics.
302
- * Base 5, logarithmic merge bonus (max +5), -1 per closed without merge (max -3),
303
- * +1 if recently merged (within 90 days), +1 if responsive, -2 if hostile. Clamp 1-10.
304
- */
305
- private calculateScore;
306
- /**
307
- * Update a repository's score with partial updates. If the repo has no existing score,
308
- * a default score record is created first (base score 5). After applying updates, the
309
- * numeric score is recalculated using the formula: base 5, logarithmic merge bonus (max +5),
310
- * -1 per closed-without-merge (max -3), +1 if recently merged, +1 if responsive, -2 if hostile, clamped to [1, 10].
311
- * @param repo - Repository in "owner/repo" format.
312
- * @param updates - Updatable RepoScore fields to merge. The `score`, `repo`, and
313
- * `lastEvaluatedAt` fields are not accepted — score is always derived via
314
- * calculateScore(), and repo/lastEvaluatedAt are managed internally.
315
- */
87
+ getRepoScore(repo: string): Readonly<RepoScore> | undefined;
316
88
  updateRepoScore(repo: string, updates: RepoScoreUpdate): void;
317
- /**
318
- * Increment the merged PR count for a repository and recalculate its score.
319
- * Routes through {@link updateRepoScore} for a single mutation path.
320
- * @param repo - Repository in "owner/repo" format.
321
- */
322
89
  incrementMergedCount(repo: string): void;
323
- /**
324
- * Increment the closed-without-merge count for a repository and recalculate its score.
325
- * Routes through {@link updateRepoScore} for a single mutation path.
326
- * @param repo - Repository in "owner/repo" format.
327
- */
328
90
  incrementClosedCount(repo: string): void;
329
- /**
330
- * Mark a repository as having hostile maintainer comments and recalculate its score.
331
- * This applies a -2 penalty to the score. Creates a default score record if needed.
332
- * @param repo - Repository in "owner/repo" format.
333
- */
334
91
  markRepoHostile(repo: string): void;
335
- /**
336
- * Get repositories where the user has at least one merged PR, sorted by merged count descending.
337
- * These repos represent proven relationships with high merge probability.
338
- * @returns Array of "owner/repo" strings for repos with mergedPRCount > 0.
339
- */
340
92
  getReposWithMergedPRs(): string[];
341
- /**
342
- * Get repositories where the user has interacted (has a score record) but has NOT
343
- * yet had a PR merged, excluding repos where the only interaction was rejection.
344
- * These represent repos with open or in-progress PRs — relationships that benefit
345
- * from continued search attention.
346
- * @returns Array of "owner/repo" strings, sorted by score descending.
347
- */
348
93
  getReposWithOpenPRs(): string[];
349
- /**
350
- * Get repositories with a score at or above the given threshold, sorted highest first.
351
- * @param minScore - Minimum score (inclusive). Defaults to `config.minRepoScoreThreshold`.
352
- * @returns Array of "owner/repo" strings for repos meeting the threshold.
353
- */
354
94
  getHighScoringRepos(minScore?: number): string[];
355
- /**
356
- * Get repositories with a score at or below the given threshold, sorted lowest first.
357
- * @param maxScore - Maximum score (inclusive). Defaults to `config.minRepoScoreThreshold`.
358
- * @returns Array of "owner/repo" strings for repos at or below the threshold.
359
- */
360
95
  getLowScoringRepos(maxScore?: number): string[];
361
- /**
362
- * Compute aggregate statistics from the current state. `mergedPRs` and `closedPRs` counts
363
- * are summed from repo score records. `totalTracked` reflects the number of repositories with
364
- * score records above the minStars threshold.
365
- *
366
- * Note: `excludeRepos`/`excludeOrgs` only affect issue discovery, not stats (#591).
367
- * @returns A Stats snapshot computed from the current state.
368
- */
369
96
  getStats(): Stats;
370
97
  }
371
- /**
372
- * Aggregate statistics returned by {@link StateManager.getStats}.
373
- */
374
- export interface Stats {
375
- /** Total merged PRs across scored repositories (above minStars threshold). */
376
- mergedPRs: number;
377
- /** Total PRs closed without merge across scored repositories (above minStars threshold). */
378
- closedPRs: number;
379
- /** Number of active issues. Always 0 in v2 (sourced from fresh fetch instead). */
380
- activeIssues: number;
381
- /** Number of trusted projects. */
382
- trustedProjects: number;
383
- /** Merge success rate as a percentage string (e.g. "75.0%"). */
384
- mergeRate: string;
385
- /** Number of scored repositories (above minStars threshold). */
386
- totalTracked: number;
387
- /** Number of PRs needing a response. Always 0 in v2 (sourced from fresh fetch instead). */
388
- needsResponse: number;
389
- }
390
98
  /**
391
99
  * Get the singleton StateManager instance, creating it on first call.
392
- * Subsequent calls return the same instance. Use {@link resetStateManager} to
393
- * clear the singleton (primarily for testing).
394
- * @returns The shared StateManager instance.
395
100
  */
396
101
  export declare function getStateManager(): StateManager;
397
102
  /**
398
- * Reset the singleton StateManager instance to null. The next call to
399
- * {@link getStateManager} will create a fresh instance. Intended for test
400
- * isolation -- should not be called in production code.
103
+ * Reset the singleton StateManager instance to null. Intended for test isolation.
401
104
  */
402
105
  export declare function resetStateManager(): void;