@oss-autopilot/core 0.44.2 → 0.44.15
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.
- package/dist/cli-registry.js +61 -0
- package/dist/cli.bundle.cjs +101 -127
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/commands/daily.d.ts +6 -1
- package/dist/commands/daily.js +29 -64
- package/dist/commands/dashboard-data.d.ts +22 -1
- package/dist/commands/dashboard-data.js +85 -62
- package/dist/commands/dashboard-lifecycle.js +39 -2
- package/dist/commands/dashboard-scripts.d.ts +1 -1
- package/dist/commands/dashboard-scripts.js +2 -1
- package/dist/commands/dashboard-server.d.ts +2 -1
- package/dist/commands/dashboard-server.js +120 -81
- package/dist/commands/dashboard-templates.js +15 -69
- package/dist/commands/override.d.ts +21 -0
- package/dist/commands/override.js +35 -0
- package/dist/core/checklist-analysis.js +3 -1
- package/dist/core/daily-logic.d.ts +13 -10
- package/dist/core/daily-logic.js +79 -166
- package/dist/core/display-utils.d.ts +4 -0
- package/dist/core/display-utils.js +53 -54
- package/dist/core/errors.d.ts +8 -0
- package/dist/core/errors.js +26 -0
- package/dist/core/github-stats.d.ts +3 -3
- package/dist/core/github-stats.js +15 -7
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/core/issue-conversation.js +2 -2
- package/dist/core/issue-discovery.d.ts +0 -5
- package/dist/core/issue-discovery.js +4 -11
- package/dist/core/issue-vetting.d.ts +0 -2
- package/dist/core/issue-vetting.js +31 -45
- package/dist/core/pr-monitor.d.ts +26 -3
- package/dist/core/pr-monitor.js +106 -93
- package/dist/core/state.d.ts +22 -1
- package/dist/core/state.js +50 -1
- package/dist/core/test-utils.js +6 -16
- package/dist/core/types.d.ts +51 -38
- package/dist/core/types.js +8 -0
- package/dist/core/utils.d.ts +2 -0
- package/dist/core/utils.js +5 -1
- package/dist/formatters/json.d.ts +1 -13
- package/dist/formatters/json.js +1 -13
- package/package.json +2 -2
package/dist/core/pr-monitor.js
CHANGED
|
@@ -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
|
-
|
|
35
|
-
const MAX_CONCURRENT_REQUESTS = 5;
|
|
34
|
+
const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
|
|
36
35
|
export class PRMonitor {
|
|
37
36
|
octokit;
|
|
38
37
|
stateManager;
|
|
@@ -120,27 +119,11 @@ export class PRMonitor {
|
|
|
120
119
|
}
|
|
121
120
|
}, MAX_CONCURRENT_REQUESTS);
|
|
122
121
|
});
|
|
123
|
-
// Sort by
|
|
122
|
+
// Sort by status (needs_addressing first, then waiting_on_maintainer)
|
|
124
123
|
prs.sort((a, b) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
needs_changes: 1,
|
|
129
|
-
failing_ci: 2,
|
|
130
|
-
ci_blocked: 3,
|
|
131
|
-
ci_not_running: 4,
|
|
132
|
-
merge_conflict: 5,
|
|
133
|
-
needs_rebase: 6,
|
|
134
|
-
missing_required_files: 7,
|
|
135
|
-
incomplete_checklist: 8,
|
|
136
|
-
changes_addressed: 9,
|
|
137
|
-
approaching_dormant: 10,
|
|
138
|
-
dormant: 11,
|
|
139
|
-
waiting: 12,
|
|
140
|
-
waiting_on_maintainer: 13,
|
|
141
|
-
healthy: 14,
|
|
142
|
-
};
|
|
143
|
-
return statusPriority[a.status] - statusPriority[b.status];
|
|
124
|
+
if (a.status === b.status)
|
|
125
|
+
return 0;
|
|
126
|
+
return a.status === 'needs_addressing' ? -1 : 1;
|
|
144
127
|
});
|
|
145
128
|
return { prs, failures };
|
|
146
129
|
}
|
|
@@ -196,14 +179,18 @@ export class PRMonitor {
|
|
|
196
179
|
const { hasUnrespondedComment, lastMaintainerComment } = checkUnrespondedComments(comments, reviews, reviewComments, config.githubUsername);
|
|
197
180
|
// Fetch CI status and (conditionally) latest commit date in parallel
|
|
198
181
|
// We need the commit date when hasUnrespondedComment is true (to distinguish
|
|
199
|
-
// "needs_response" from "
|
|
182
|
+
// "needs_response" from "waiting_on_maintainer") OR when reviewDecision is "changes_requested"
|
|
200
183
|
// (to detect needs_changes: review requested changes but no new commits pushed)
|
|
201
184
|
const ciPromise = this.getCIStatus(owner, repo, ghPR.head.sha);
|
|
202
185
|
const needCommitDate = hasUnrespondedComment || reviewDecision === 'changes_requested';
|
|
203
|
-
const
|
|
186
|
+
const commitInfoPromise = needCommitDate
|
|
204
187
|
? this.octokit.repos
|
|
205
188
|
.getCommit({ owner, repo, ref: ghPR.head.sha })
|
|
206
|
-
.then((res) =>
|
|
189
|
+
.then((res) => ({
|
|
190
|
+
date: res.data.commit.author?.date,
|
|
191
|
+
// GitHub user login of the commit author (may differ from git author)
|
|
192
|
+
author: res.data.author?.login,
|
|
193
|
+
}))
|
|
207
194
|
.catch((err) => {
|
|
208
195
|
// Rate limit errors must propagate — silently swallowing them produces
|
|
209
196
|
// misleading status (e.g. needs_changes when changes were addressed) (#469).
|
|
@@ -222,10 +209,12 @@ export class PRMonitor {
|
|
|
222
209
|
return undefined;
|
|
223
210
|
})
|
|
224
211
|
: Promise.resolve(undefined);
|
|
225
|
-
const [{ status: ciStatus, failingCheckNames, failingCheckConclusions },
|
|
212
|
+
const [{ status: ciStatus, failingCheckNames, failingCheckConclusions }, commitInfo] = await Promise.all([
|
|
226
213
|
ciPromise,
|
|
227
|
-
|
|
214
|
+
commitInfoPromise,
|
|
228
215
|
]);
|
|
216
|
+
const latestCommitDate = commitInfo?.date;
|
|
217
|
+
const latestCommitAuthor = commitInfo?.author;
|
|
229
218
|
// Analyze PR body for incomplete checklists (delegated to checklist-analysis module)
|
|
230
219
|
const { hasIncompleteChecklist, checklistStats } = analyzeChecklist(ghPR.body || '');
|
|
231
220
|
// Extract maintainer action hints from comments (delegated to maintainer-analysis module)
|
|
@@ -238,7 +227,7 @@ export class PRMonitor {
|
|
|
238
227
|
const classifiedChecks = classifyFailingChecks(failingCheckNames, failingCheckConclusions);
|
|
239
228
|
// Determine status
|
|
240
229
|
const hasActionableCIFailure = ciStatus === 'failing' && classifiedChecks.some((c) => c.category === 'actionable');
|
|
241
|
-
const status = this.determineStatus({
|
|
230
|
+
const { status, actionReason, waitReason, stalenessTier } = this.determineStatus({
|
|
242
231
|
ciStatus,
|
|
243
232
|
hasMergeConflict,
|
|
244
233
|
hasUnrespondedComment,
|
|
@@ -248,6 +237,8 @@ export class PRMonitor {
|
|
|
248
237
|
dormantThreshold: config.dormantThresholdDays,
|
|
249
238
|
approachingThreshold: config.approachingDormantDays,
|
|
250
239
|
latestCommitDate,
|
|
240
|
+
latestCommitAuthor,
|
|
241
|
+
contributorUsername: config.githubUsername,
|
|
251
242
|
lastMaintainerCommentDate: lastMaintainerComment?.createdAt,
|
|
252
243
|
latestChangesRequestedDate,
|
|
253
244
|
hasActionableCIFailure,
|
|
@@ -259,6 +250,9 @@ export class PRMonitor {
|
|
|
259
250
|
number,
|
|
260
251
|
title: ghPR.title,
|
|
261
252
|
status,
|
|
253
|
+
actionReason,
|
|
254
|
+
waitReason,
|
|
255
|
+
stalenessTier,
|
|
262
256
|
createdAt: ghPR.created_at,
|
|
263
257
|
updatedAt: ghPR.updated_at,
|
|
264
258
|
daysSinceActivity,
|
|
@@ -295,61 +289,110 @@ export class PRMonitor {
|
|
|
295
289
|
* Determine the overall status of a PR
|
|
296
290
|
*/
|
|
297
291
|
determineStatus(input) {
|
|
298
|
-
const { ciStatus, hasMergeConflict, hasUnrespondedComment, hasIncompleteChecklist, reviewDecision, daysSinceActivity, dormantThreshold, approachingThreshold, latestCommitDate, lastMaintainerCommentDate, latestChangesRequestedDate, hasActionableCIFailure = true, } = input;
|
|
299
|
-
//
|
|
292
|
+
const { ciStatus, hasMergeConflict, hasUnrespondedComment, hasIncompleteChecklist, reviewDecision, daysSinceActivity, dormantThreshold, approachingThreshold, latestCommitDate: rawCommitDate, latestCommitAuthor, contributorUsername, lastMaintainerCommentDate, latestChangesRequestedDate, hasActionableCIFailure = true, } = input;
|
|
293
|
+
// Compute staleness tier (independent of status)
|
|
294
|
+
let stalenessTier = 'active';
|
|
295
|
+
if (daysSinceActivity >= dormantThreshold)
|
|
296
|
+
stalenessTier = 'dormant';
|
|
297
|
+
else if (daysSinceActivity >= approachingThreshold)
|
|
298
|
+
stalenessTier = 'approaching_dormant';
|
|
299
|
+
// Only count the latest commit if it was authored by the contributor or a
|
|
300
|
+
// CI bot (#547, #568). Non-contributor commits (maintainer merge commits,
|
|
301
|
+
// GitHub suggestion commits) should not mask unaddressed feedback.
|
|
302
|
+
const latestCommitDate = rawCommitDate && this.isContributorCommit(latestCommitAuthor, contributorUsername) ? rawCommitDate : undefined;
|
|
303
|
+
// Priority order: needs_addressing (response/changes/ci/conflict/checklist) > waiting_on_maintainer (review/merge/addressed/ci_blocked)
|
|
300
304
|
if (hasUnrespondedComment) {
|
|
301
305
|
// If the contributor pushed a commit after the maintainer's comment,
|
|
302
|
-
// the changes have been addressed — waiting for maintainer re-review
|
|
303
|
-
|
|
306
|
+
// the changes have been addressed — waiting for maintainer re-review.
|
|
307
|
+
// Require a minimum 2-minute gap to avoid false positives from race
|
|
308
|
+
// conditions (pushing while review is being submitted) (#547).
|
|
309
|
+
if (latestCommitDate &&
|
|
310
|
+
lastMaintainerCommentDate &&
|
|
311
|
+
this.isCommitAfterComment(latestCommitDate, lastMaintainerCommentDate)) {
|
|
304
312
|
// Safety net (#431): if a CHANGES_REQUESTED review was submitted after
|
|
305
313
|
// the commit, the maintainer still expects changes — don't mask it
|
|
306
314
|
if (latestChangesRequestedDate && latestCommitDate < latestChangesRequestedDate) {
|
|
307
|
-
return 'needs_response';
|
|
315
|
+
return { status: 'needs_addressing', actionReason: 'needs_response', stalenessTier };
|
|
308
316
|
}
|
|
309
317
|
if (ciStatus === 'failing' && hasActionableCIFailure)
|
|
310
|
-
return 'failing_ci';
|
|
318
|
+
return { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier };
|
|
311
319
|
// Non-actionable CI failures (infrastructure, fork, auth) don't block changes_addressed —
|
|
312
320
|
// the contributor can't fix them, so the relevant status is "waiting for re-review" (#502)
|
|
313
|
-
return 'changes_addressed';
|
|
321
|
+
return { status: 'waiting_on_maintainer', waitReason: 'changes_addressed', stalenessTier };
|
|
314
322
|
}
|
|
315
|
-
return 'needs_response';
|
|
323
|
+
return { status: 'needs_addressing', actionReason: 'needs_response', stalenessTier };
|
|
316
324
|
}
|
|
317
325
|
// Review requested changes but no unresponded comment.
|
|
318
326
|
// If the latest commit is before the review, the contributor hasn't addressed it yet.
|
|
319
327
|
if (reviewDecision === 'changes_requested' && latestChangesRequestedDate) {
|
|
320
328
|
if (!latestCommitDate || latestCommitDate < latestChangesRequestedDate) {
|
|
321
|
-
return 'needs_changes';
|
|
329
|
+
return { status: 'needs_addressing', actionReason: 'needs_changes', stalenessTier };
|
|
322
330
|
}
|
|
323
331
|
// Commit is after review — changes have been addressed
|
|
324
332
|
if (ciStatus === 'failing' && hasActionableCIFailure)
|
|
325
|
-
return 'failing_ci';
|
|
333
|
+
return { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier };
|
|
326
334
|
// Non-actionable CI failures don't block changes_addressed (#502)
|
|
327
|
-
return 'changes_addressed';
|
|
335
|
+
return { status: 'waiting_on_maintainer', waitReason: 'changes_addressed', stalenessTier };
|
|
328
336
|
}
|
|
329
337
|
if (ciStatus === 'failing') {
|
|
330
|
-
return hasActionableCIFailure
|
|
338
|
+
return hasActionableCIFailure
|
|
339
|
+
? { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier }
|
|
340
|
+
: { status: 'waiting_on_maintainer', waitReason: 'ci_blocked', stalenessTier };
|
|
331
341
|
}
|
|
332
342
|
if (hasMergeConflict) {
|
|
333
|
-
return 'merge_conflict';
|
|
343
|
+
return { status: 'needs_addressing', actionReason: 'merge_conflict', stalenessTier };
|
|
334
344
|
}
|
|
335
345
|
if (hasIncompleteChecklist) {
|
|
336
|
-
return 'incomplete_checklist';
|
|
337
|
-
}
|
|
338
|
-
if (daysSinceActivity >= dormantThreshold) {
|
|
339
|
-
return 'dormant';
|
|
340
|
-
}
|
|
341
|
-
if (daysSinceActivity >= approachingThreshold) {
|
|
342
|
-
return 'approaching_dormant';
|
|
346
|
+
return { status: 'needs_addressing', actionReason: 'incomplete_checklist', stalenessTier };
|
|
343
347
|
}
|
|
344
348
|
// Approved and CI passing/unknown = waiting on maintainer to merge
|
|
345
349
|
if (reviewDecision === 'approved' && (ciStatus === 'passing' || ciStatus === 'unknown')) {
|
|
346
|
-
return 'waiting_on_maintainer';
|
|
350
|
+
return { status: 'waiting_on_maintainer', waitReason: 'pending_merge', stalenessTier };
|
|
347
351
|
}
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
352
|
+
// Default: no actionable issues found. Covers pending CI, no reviews yet, etc.
|
|
353
|
+
return { status: 'waiting_on_maintainer', waitReason: 'pending_review', stalenessTier };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* CI-fix bots that push commits as a direct result of the contributor's push (#568).
|
|
357
|
+
* Their commits represent contributor work and should count as addressing feedback.
|
|
358
|
+
* This is intentionally an allowlist — not all `[bot]` accounts are CI-fix bots
|
|
359
|
+
* (e.g. dependabot[bot] and renovate[bot] open their own PRs).
|
|
360
|
+
* Values must be lowercase — lookup uses .toLowerCase() for case-insensitive matching.
|
|
361
|
+
*/
|
|
362
|
+
static CI_FIX_BOTS = new Set([
|
|
363
|
+
'autofix-ci[bot]',
|
|
364
|
+
'prettier-ci[bot]',
|
|
365
|
+
'pre-commit-ci[bot]',
|
|
366
|
+
]);
|
|
367
|
+
/**
|
|
368
|
+
* Check whether the HEAD commit was authored by the contributor (#547).
|
|
369
|
+
* Returns true when the author matches, when the author is a known CI-fix
|
|
370
|
+
* bot (#568), or when author info is unavailable (graceful degradation).
|
|
371
|
+
*/
|
|
372
|
+
isContributorCommit(commitAuthor, contributorUsername) {
|
|
373
|
+
if (!commitAuthor || !contributorUsername)
|
|
374
|
+
return true; // degrade gracefully
|
|
375
|
+
const author = commitAuthor.toLowerCase();
|
|
376
|
+
if (PRMonitor.CI_FIX_BOTS.has(author))
|
|
377
|
+
return true; // CI-fix bots act on behalf of the contributor (#568)
|
|
378
|
+
return author === contributorUsername.toLowerCase();
|
|
379
|
+
}
|
|
380
|
+
/** Minimum gap (ms) between maintainer comment and contributor commit for
|
|
381
|
+
* the commit to count as "addressing" the feedback (#547). Prevents false
|
|
382
|
+
* positives from race conditions, clock skew, and in-flight pushes. */
|
|
383
|
+
static MIN_RESPONSE_GAP_MS = 2 * 60 * 1000; // 2 minutes
|
|
384
|
+
/**
|
|
385
|
+
* Check whether the contributor's commit is meaningfully after the maintainer's
|
|
386
|
+
* comment — i.e. the commit timestamp is at least MIN_RESPONSE_GAP_MS later (#547).
|
|
387
|
+
*/
|
|
388
|
+
isCommitAfterComment(commitDate, commentDate) {
|
|
389
|
+
const commitMs = new Date(commitDate).getTime();
|
|
390
|
+
const commentMs = new Date(commentDate).getTime();
|
|
391
|
+
if (Number.isNaN(commitMs) || Number.isNaN(commentMs)) {
|
|
392
|
+
// Fall back to simple string comparison (pre-#547 behavior)
|
|
393
|
+
return commitDate > commentDate;
|
|
351
394
|
}
|
|
352
|
-
return
|
|
395
|
+
return commitMs - commentMs >= PRMonitor.MIN_RESPONSE_GAP_MS;
|
|
353
396
|
}
|
|
354
397
|
/**
|
|
355
398
|
* Check if PR has merge conflict
|
|
@@ -427,17 +470,17 @@ export class PRMonitor {
|
|
|
427
470
|
* Fetch merged PR counts and latest merge dates per repository for the configured user.
|
|
428
471
|
* Delegates to github-stats module.
|
|
429
472
|
*/
|
|
430
|
-
async fetchUserMergedPRCounts() {
|
|
473
|
+
async fetchUserMergedPRCounts(starFilter) {
|
|
431
474
|
const config = this.stateManager.getState().config;
|
|
432
|
-
return fetchUserMergedPRCountsImpl(this.octokit, config.githubUsername);
|
|
475
|
+
return fetchUserMergedPRCountsImpl(this.octokit, config.githubUsername, starFilter);
|
|
433
476
|
}
|
|
434
477
|
/**
|
|
435
478
|
* Fetch closed-without-merge PR counts per repository for the configured user.
|
|
436
479
|
* Delegates to github-stats module.
|
|
437
480
|
*/
|
|
438
|
-
async fetchUserClosedPRCounts() {
|
|
481
|
+
async fetchUserClosedPRCounts(starFilter) {
|
|
439
482
|
const config = this.stateManager.getState().config;
|
|
440
|
-
return fetchUserClosedPRCountsImpl(this.octokit, config.githubUsername);
|
|
483
|
+
return fetchUserClosedPRCountsImpl(this.octokit, config.githubUsername, starFilter);
|
|
441
484
|
}
|
|
442
485
|
/**
|
|
443
486
|
* Fetch GitHub star counts for a list of repositories.
|
|
@@ -482,7 +525,7 @@ export class PRMonitor {
|
|
|
482
525
|
}
|
|
483
526
|
// If entire chunk failed, likely a systemic issue (rate limit, auth, outage) — abort remaining
|
|
484
527
|
if (chunkFailures === chunk.length && chunk.length > 0) {
|
|
485
|
-
const remaining =
|
|
528
|
+
const remaining = uniqueRepos.length - i - chunkSize;
|
|
486
529
|
if (remaining > 0) {
|
|
487
530
|
warn(MODULE, `Entire chunk failed, aborting remaining ${remaining} repos`);
|
|
488
531
|
}
|
|
@@ -514,52 +557,22 @@ export class PRMonitor {
|
|
|
514
557
|
generateDigest(prs, recentlyClosedPRs = [], recentlyMergedPRs = []) {
|
|
515
558
|
const now = new Date().toISOString();
|
|
516
559
|
// Categorize PRs
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
const mergeConflictPRs = prs.filter((pr) => pr.status === 'merge_conflict');
|
|
520
|
-
const approachingDormant = prs.filter((pr) => pr.status === 'approaching_dormant');
|
|
521
|
-
const dormantPRs = prs.filter((pr) => pr.status === 'dormant');
|
|
522
|
-
const healthyPRs = prs.filter((pr) => pr.status === 'healthy' || pr.status === 'waiting');
|
|
560
|
+
const needsAddressingPRs = prs.filter((pr) => pr.status === 'needs_addressing');
|
|
561
|
+
const waitingOnMaintainerPRs = prs.filter((pr) => pr.status === 'waiting_on_maintainer');
|
|
523
562
|
// Get stats from state manager (historical data from repo scores)
|
|
524
563
|
const stats = this.stateManager.getStats();
|
|
525
|
-
const ciBlockedPRs = prs.filter((pr) => pr.status === 'ci_blocked');
|
|
526
|
-
const ciNotRunningPRs = prs.filter((pr) => pr.status === 'ci_not_running');
|
|
527
|
-
const needsRebasePRs = prs.filter((pr) => pr.status === 'needs_rebase');
|
|
528
|
-
const missingRequiredFilesPRs = prs.filter((pr) => pr.status === 'missing_required_files');
|
|
529
|
-
const incompleteChecklistPRs = prs.filter((pr) => pr.status === 'incomplete_checklist');
|
|
530
|
-
const needsChangesPRs = prs.filter((pr) => pr.status === 'needs_changes');
|
|
531
|
-
const changesAddressedPRs = prs.filter((pr) => pr.status === 'changes_addressed');
|
|
532
|
-
const waitingOnMaintainerPRs = prs.filter((pr) => pr.status === 'waiting_on_maintainer');
|
|
533
564
|
return {
|
|
534
565
|
generatedAt: now,
|
|
535
566
|
openPRs: prs,
|
|
536
|
-
|
|
537
|
-
ciFailingPRs,
|
|
538
|
-
ciBlockedPRs,
|
|
539
|
-
ciNotRunningPRs,
|
|
540
|
-
mergeConflictPRs,
|
|
541
|
-
needsRebasePRs,
|
|
542
|
-
missingRequiredFilesPRs,
|
|
543
|
-
incompleteChecklistPRs,
|
|
544
|
-
needsChangesPRs,
|
|
545
|
-
changesAddressedPRs,
|
|
567
|
+
needsAddressingPRs,
|
|
546
568
|
waitingOnMaintainerPRs,
|
|
547
|
-
approachingDormant,
|
|
548
|
-
dormantPRs,
|
|
549
|
-
healthyPRs,
|
|
550
569
|
recentlyClosedPRs,
|
|
551
570
|
recentlyMergedPRs,
|
|
552
571
|
shelvedPRs: [],
|
|
553
572
|
autoUnshelvedPRs: [],
|
|
554
573
|
summary: {
|
|
555
574
|
totalActivePRs: prs.length,
|
|
556
|
-
totalNeedingAttention:
|
|
557
|
-
needsChangesPRs.length +
|
|
558
|
-
ciFailingPRs.length +
|
|
559
|
-
mergeConflictPRs.length +
|
|
560
|
-
needsRebasePRs.length +
|
|
561
|
-
missingRequiredFilesPRs.length +
|
|
562
|
-
incompleteChecklistPRs.length,
|
|
575
|
+
totalNeedingAttention: needsAddressingPRs.length,
|
|
563
576
|
totalMergedAllTime: stats.mergedPRs,
|
|
564
577
|
mergeRate: parseFloat(stats.mergeRate),
|
|
565
578
|
},
|
package/dist/core/state.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* State management for the OSS Contribution Agent
|
|
3
3
|
* Persists state to a JSON file in ~/.oss-autopilot/
|
|
4
4
|
*/
|
|
5
|
-
import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, SnoozeInfo } from './types.js';
|
|
5
|
+
import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, SnoozeInfo, StatusOverride, FetchedPRStatus } from './types.js';
|
|
6
6
|
/**
|
|
7
7
|
* Acquire an advisory file lock using exclusive-create (`wx` flag).
|
|
8
8
|
* If the lock file already exists but is stale (older than LOCK_TIMEOUT_MS or corrupt),
|
|
@@ -257,6 +257,27 @@ export declare class StateManager {
|
|
|
257
257
|
* @returns Array of PR URLs whose snoozes were expired.
|
|
258
258
|
*/
|
|
259
259
|
expireSnoozes(): string[];
|
|
260
|
+
/**
|
|
261
|
+
* Set a manual status override for a PR.
|
|
262
|
+
* @param url - The full GitHub PR URL.
|
|
263
|
+
* @param status - The target status to override to.
|
|
264
|
+
* @param lastActivityAt - The PR's current updatedAt timestamp (for auto-clear detection).
|
|
265
|
+
*/
|
|
266
|
+
setStatusOverride(url: string, status: FetchedPRStatus, lastActivityAt: string): void;
|
|
267
|
+
/**
|
|
268
|
+
* Clear a status override for a PR.
|
|
269
|
+
* @param url - The full GitHub PR URL.
|
|
270
|
+
* @returns true if found and removed, false if no override existed.
|
|
271
|
+
*/
|
|
272
|
+
clearStatusOverride(url: string): boolean;
|
|
273
|
+
/**
|
|
274
|
+
* Get the status override for a PR, if one exists and hasn't been auto-cleared.
|
|
275
|
+
* @param url - The full GitHub PR URL.
|
|
276
|
+
* @param currentUpdatedAt - The PR's current updatedAt from GitHub. If newer than
|
|
277
|
+
* the stored lastActivityAt, the override is stale and auto-cleared.
|
|
278
|
+
* @returns The override metadata, or undefined if none exists or it was auto-cleared.
|
|
279
|
+
*/
|
|
280
|
+
getStatusOverride(url: string, currentUpdatedAt?: string): StatusOverride | undefined;
|
|
260
281
|
/**
|
|
261
282
|
* Get the score record for a repository.
|
|
262
283
|
* @param repo - Repository in "owner/repo" format.
|
package/dist/core/state.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import { INITIAL_STATE, } from './types.js';
|
|
7
|
+
import { INITIAL_STATE, isBelowMinStars, } from './types.js';
|
|
8
8
|
import { getStatePath, getBackupDir, getDataDir } from './utils.js';
|
|
9
9
|
import { ValidationError, errorMessage } from './errors.js';
|
|
10
10
|
import { debug, warn } from './logger.js';
|
|
@@ -843,6 +843,53 @@ export class StateManager {
|
|
|
843
843
|
}
|
|
844
844
|
return expired;
|
|
845
845
|
}
|
|
846
|
+
// === Status Overrides ===
|
|
847
|
+
/**
|
|
848
|
+
* Set a manual status override for a PR.
|
|
849
|
+
* @param url - The full GitHub PR URL.
|
|
850
|
+
* @param status - The target status to override to.
|
|
851
|
+
* @param lastActivityAt - The PR's current updatedAt timestamp (for auto-clear detection).
|
|
852
|
+
*/
|
|
853
|
+
setStatusOverride(url, status, lastActivityAt) {
|
|
854
|
+
if (!this.state.config.statusOverrides) {
|
|
855
|
+
this.state.config.statusOverrides = {};
|
|
856
|
+
}
|
|
857
|
+
this.state.config.statusOverrides[url] = {
|
|
858
|
+
status,
|
|
859
|
+
setAt: new Date().toISOString(),
|
|
860
|
+
lastActivityAt,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Clear a status override for a PR.
|
|
865
|
+
* @param url - The full GitHub PR URL.
|
|
866
|
+
* @returns true if found and removed, false if no override existed.
|
|
867
|
+
*/
|
|
868
|
+
clearStatusOverride(url) {
|
|
869
|
+
if (!this.state.config.statusOverrides || !(url in this.state.config.statusOverrides)) {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
delete this.state.config.statusOverrides[url];
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Get the status override for a PR, if one exists and hasn't been auto-cleared.
|
|
877
|
+
* @param url - The full GitHub PR URL.
|
|
878
|
+
* @param currentUpdatedAt - The PR's current updatedAt from GitHub. If newer than
|
|
879
|
+
* the stored lastActivityAt, the override is stale and auto-cleared.
|
|
880
|
+
* @returns The override metadata, or undefined if none exists or it was auto-cleared.
|
|
881
|
+
*/
|
|
882
|
+
getStatusOverride(url, currentUpdatedAt) {
|
|
883
|
+
const override = this.state.config.statusOverrides?.[url];
|
|
884
|
+
if (!override)
|
|
885
|
+
return undefined;
|
|
886
|
+
// Auto-clear if the PR has new activity since the override was set
|
|
887
|
+
if (currentUpdatedAt && currentUpdatedAt > override.lastActivityAt) {
|
|
888
|
+
this.clearStatusOverride(url);
|
|
889
|
+
return undefined;
|
|
890
|
+
}
|
|
891
|
+
return override;
|
|
892
|
+
}
|
|
846
893
|
// === Repository Scoring ===
|
|
847
894
|
/**
|
|
848
895
|
* Get the score record for a repository.
|
|
@@ -1060,6 +1107,8 @@ export class StateManager {
|
|
|
1060
1107
|
for (const [repoKey, score] of Object.entries(this.state.repoScores)) {
|
|
1061
1108
|
if (this.isExcluded(repoKey))
|
|
1062
1109
|
continue;
|
|
1110
|
+
if (isBelowMinStars(score.stargazersCount, this.state.config.minStars ?? 50))
|
|
1111
|
+
continue;
|
|
1063
1112
|
totalTracked++;
|
|
1064
1113
|
totalMerged += score.mergedPRCount;
|
|
1065
1114
|
totalClosed += score.closedWithoutMergeCount;
|
package/dist/core/test-utils.js
CHANGED
|
@@ -17,9 +17,11 @@ export function makeFetchedPR(overrides = {}) {
|
|
|
17
17
|
repo,
|
|
18
18
|
number,
|
|
19
19
|
title: 'Test PR',
|
|
20
|
-
status: '
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
status: 'waiting_on_maintainer',
|
|
21
|
+
waitReason: 'pending_review',
|
|
22
|
+
stalenessTier: 'active',
|
|
23
|
+
displayLabel: '[Waiting on Maintainer]',
|
|
24
|
+
displayDescription: 'Awaiting review',
|
|
23
25
|
createdAt: '2025-06-01T00:00:00Z',
|
|
24
26
|
updatedAt: '2025-06-15T00:00:00Z',
|
|
25
27
|
daysSinceActivity: 2,
|
|
@@ -41,20 +43,8 @@ export function makeDailyDigest(overrides = {}) {
|
|
|
41
43
|
return {
|
|
42
44
|
generatedAt: '2025-06-20T00:00:00Z',
|
|
43
45
|
openPRs: [],
|
|
44
|
-
|
|
45
|
-
ciFailingPRs: [],
|
|
46
|
-
ciBlockedPRs: [],
|
|
47
|
-
ciNotRunningPRs: [],
|
|
48
|
-
mergeConflictPRs: [],
|
|
49
|
-
needsRebasePRs: [],
|
|
50
|
-
missingRequiredFilesPRs: [],
|
|
51
|
-
incompleteChecklistPRs: [],
|
|
52
|
-
needsChangesPRs: [],
|
|
53
|
-
changesAddressedPRs: [],
|
|
46
|
+
needsAddressingPRs: [],
|
|
54
47
|
waitingOnMaintainerPRs: [],
|
|
55
|
-
approachingDormant: [],
|
|
56
|
-
dormantPRs: [],
|
|
57
|
-
healthyPRs: [],
|
|
58
48
|
recentlyClosedPRs: [],
|
|
59
49
|
recentlyMergedPRs: [],
|
|
60
50
|
shelvedPRs: [],
|
package/dist/core/types.d.ts
CHANGED
|
@@ -52,37 +52,35 @@ export interface DetermineStatusInput {
|
|
|
52
52
|
dormantThreshold: number;
|
|
53
53
|
approachingThreshold: number;
|
|
54
54
|
latestCommitDate?: string;
|
|
55
|
+
/** GitHub login of the HEAD commit's author (from `repos.getCommit`). */
|
|
56
|
+
latestCommitAuthor?: string;
|
|
57
|
+
/** GitHub login of the PR contributor (configured username). */
|
|
58
|
+
contributorUsername?: string;
|
|
55
59
|
lastMaintainerCommentDate?: string;
|
|
56
60
|
latestChangesRequestedDate?: string;
|
|
57
61
|
/** True if at least one failing CI check is classified as 'actionable'. */
|
|
58
62
|
hasActionableCIFailure?: boolean;
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* - `
|
|
76
|
-
* - `changes_addressed` — Contributor pushed commits after reviewer feedback; awaiting re-review
|
|
77
|
-
* - `waiting` — CI is pending or no specific action needed
|
|
78
|
-
* - `waiting_on_maintainer` — PR is approved and CI passes; waiting for maintainer to merge
|
|
79
|
-
* - `healthy` — Everything looks good; normal review cycle
|
|
65
|
+
* Granular reason why a PR needs addressing (contributor's turn).
|
|
66
|
+
* Active values (produced by determineStatus): needs_response, needs_changes,
|
|
67
|
+
* failing_ci, merge_conflict, incomplete_checklist.
|
|
68
|
+
* Reserved (display mappings exist but detection not yet wired): ci_not_running,
|
|
69
|
+
* needs_rebase, missing_required_files.
|
|
70
|
+
*/
|
|
71
|
+
export type ActionReason = 'needs_response' | 'needs_changes' | 'failing_ci' | 'merge_conflict' | 'incomplete_checklist' | 'ci_not_running' | 'needs_rebase' | 'missing_required_files';
|
|
72
|
+
/** Granular reason why a PR is waiting on the maintainer. */
|
|
73
|
+
export type WaitReason = 'pending_review' | 'pending_merge' | 'changes_addressed' | 'ci_blocked';
|
|
74
|
+
/** How stale is the PR based on days since activity. Orthogonal to status. */
|
|
75
|
+
export type StalenessTier = 'active' | 'approaching_dormant' | 'dormant';
|
|
76
|
+
/**
|
|
77
|
+
* Top-level classification of a PR's state. Only two values:
|
|
78
|
+
* - `needs_addressing` — Contributor's turn. See `actionReason` for what to do.
|
|
79
|
+
* - `waiting_on_maintainer` — Maintainer's turn. See `waitReason` for why.
|
|
80
80
|
*
|
|
81
|
-
*
|
|
82
|
-
* - `approaching_dormant` — No activity for `approachingDormantDays` (default 25)
|
|
83
|
-
* - `dormant` — No activity for `dormantThresholdDays` (default 30)
|
|
81
|
+
* Staleness (active/approaching_dormant/dormant) is tracked separately in `stalenessTier`.
|
|
84
82
|
*/
|
|
85
|
-
export type FetchedPRStatus = '
|
|
83
|
+
export type FetchedPRStatus = 'needs_addressing' | 'waiting_on_maintainer';
|
|
86
84
|
/**
|
|
87
85
|
* Hints about what a maintainer is asking for in their review comments.
|
|
88
86
|
* Extracted from comment text by keyword matching.
|
|
@@ -101,6 +99,12 @@ export interface FetchedPR {
|
|
|
101
99
|
title: string;
|
|
102
100
|
/** Computed by `PRMonitor.determineStatus()` based on the fields below. */
|
|
103
101
|
status: FetchedPRStatus;
|
|
102
|
+
/** Granular reason for needs_addressing status. Undefined when waiting_on_maintainer. */
|
|
103
|
+
actionReason?: ActionReason;
|
|
104
|
+
/** Granular reason for waiting_on_maintainer status. Undefined when needs_addressing. */
|
|
105
|
+
waitReason?: WaitReason;
|
|
106
|
+
/** How stale the PR is based on activity age. Independent of status — a PR can be both needs_addressing and dormant. */
|
|
107
|
+
stalenessTier: StalenessTier;
|
|
104
108
|
/** Human-readable status label for consistent display (#79). E.g., "[CI Failing]", "[Needs Response]". */
|
|
105
109
|
displayLabel: string;
|
|
106
110
|
/** Brief description of what's happening (#79). E.g., "3 checks failed", "@maintainer commented". */
|
|
@@ -321,21 +325,10 @@ export interface DailyDigest {
|
|
|
321
325
|
generatedAt: string;
|
|
322
326
|
/** All open PRs authored by the user, fetched from GitHub Search API. */
|
|
323
327
|
openPRs: FetchedPR[];
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
ciNotRunningPRs: FetchedPR[];
|
|
328
|
-
mergeConflictPRs: FetchedPR[];
|
|
329
|
-
needsRebasePRs: FetchedPR[];
|
|
330
|
-
missingRequiredFilesPRs: FetchedPR[];
|
|
331
|
-
incompleteChecklistPRs: FetchedPR[];
|
|
332
|
-
needsChangesPRs: FetchedPR[];
|
|
333
|
-
changesAddressedPRs: FetchedPR[];
|
|
328
|
+
/** PRs where the contributor needs to take action. Subset of openPRs where status === 'needs_addressing'. */
|
|
329
|
+
needsAddressingPRs: FetchedPR[];
|
|
330
|
+
/** PRs waiting on the maintainer. Subset of openPRs where status === 'waiting_on_maintainer'. */
|
|
334
331
|
waitingOnMaintainerPRs: FetchedPR[];
|
|
335
|
-
/** PRs with no activity for 25+ days (configurable via `approachingDormantDays`). */
|
|
336
|
-
approachingDormant: FetchedPR[];
|
|
337
|
-
dormantPRs: FetchedPR[];
|
|
338
|
-
healthyPRs: FetchedPR[];
|
|
339
332
|
/** PRs closed without merge in the last 7 days. Surfaced to alert the contributor. */
|
|
340
333
|
recentlyClosedPRs: ClosedPR[];
|
|
341
334
|
/** PRs merged in the last 7 days. Surfaced as wins in the dashboard. */
|
|
@@ -354,7 +347,7 @@ export interface DailyDigest {
|
|
|
354
347
|
totalActivePRs: number;
|
|
355
348
|
/** Count of PRs requiring contributor action (response, CI fix, conflict resolution, etc.). */
|
|
356
349
|
totalNeedingAttention: number;
|
|
357
|
-
/** Lifetime merged PR count across all repos, derived from
|
|
350
|
+
/** Lifetime merged PR count across all repos, derived from RepoScore data. */
|
|
358
351
|
totalMergedAllTime: number;
|
|
359
352
|
/** Percentage of all-time PRs that were merged (merged / (merged + closed)). */
|
|
360
353
|
mergeRate: number;
|
|
@@ -411,6 +404,24 @@ export interface SnoozeInfo {
|
|
|
411
404
|
snoozedAt: string;
|
|
412
405
|
expiresAt: string;
|
|
413
406
|
}
|
|
407
|
+
/** Filter for excluding repos below a minimum star count from PR count queries. */
|
|
408
|
+
export interface StarFilter {
|
|
409
|
+
minStars: number;
|
|
410
|
+
knownStarCounts: ReadonlyMap<string, number>;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Check if a repo should be excluded based on its star count.
|
|
414
|
+
* Returns true if the repo is known to be below the threshold.
|
|
415
|
+
* Repos with unknown star counts pass through (fail-open).
|
|
416
|
+
*/
|
|
417
|
+
export declare function isBelowMinStars(stargazersCount: number | undefined, minStars: number): boolean;
|
|
418
|
+
/** Manual status override for a PR, set via dashboard or CLI. Auto-clears when new activity is detected. */
|
|
419
|
+
export interface StatusOverride {
|
|
420
|
+
status: FetchedPRStatus;
|
|
421
|
+
setAt: string;
|
|
422
|
+
/** PR's updatedAt at the time the override was set. Used to detect new activity for auto-clear. */
|
|
423
|
+
lastActivityAt: string;
|
|
424
|
+
}
|
|
414
425
|
/** User-configurable settings, populated via `/setup-oss` and stored in {@link AgentState}. */
|
|
415
426
|
export interface AgentConfig {
|
|
416
427
|
/** False until the user completes initial setup via `/setup-oss`. */
|
|
@@ -457,6 +468,8 @@ export interface AgentConfig {
|
|
|
457
468
|
dismissedIssues?: Record<string, string>;
|
|
458
469
|
/** PR URLs with snoozed CI failures, mapped to snooze metadata. Snoozed PRs are excluded from actionable CI failure list until expiry. */
|
|
459
470
|
snoozedPRs?: Record<string, SnoozeInfo>;
|
|
471
|
+
/** Manual status overrides for PRs. Maps PR URL to override metadata. Auto-clears when the PR has new activity. */
|
|
472
|
+
statusOverrides?: Record<string, StatusOverride>;
|
|
460
473
|
}
|
|
461
474
|
/** Status of a user's comment thread on a GitHub issue. */
|
|
462
475
|
export type IssueConversationStatus = 'new_response' | 'waiting' | 'acknowledged';
|
package/dist/core/types.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core types for the Open Source Contribution Agent
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Check if a repo should be excluded based on its star count.
|
|
6
|
+
* Returns true if the repo is known to be below the threshold.
|
|
7
|
+
* Repos with unknown star counts pass through (fail-open).
|
|
8
|
+
*/
|
|
9
|
+
export function isBelowMinStars(stargazersCount, minStars) {
|
|
10
|
+
return stargazersCount !== undefined && stargazersCount < minStars;
|
|
11
|
+
}
|
|
4
12
|
/** Default configuration applied to new state files. All fields can be overridden via `/setup-oss`. */
|
|
5
13
|
export const DEFAULT_CONFIG = {
|
|
6
14
|
setupComplete: false,
|
package/dist/core/utils.d.ts
CHANGED