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