@oss-autopilot/core 0.50.0 → 0.51.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.
- package/dist/cli-registry.js +44 -98
- package/dist/cli.bundle.cjs +43 -45
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/commands/daily.d.ts +1 -1
- package/dist/commands/daily.js +5 -42
- package/dist/commands/dashboard-server.d.ts +1 -1
- package/dist/commands/dashboard-server.js +16 -36
- package/dist/commands/dismiss.d.ts +1 -1
- package/dist/commands/dismiss.js +4 -4
- package/dist/commands/index.d.ts +3 -5
- package/dist/commands/index.js +2 -4
- package/dist/commands/move.d.ts +16 -0
- package/dist/commands/move.js +56 -0
- package/dist/commands/shelve.d.ts +4 -0
- package/dist/commands/shelve.js +8 -2
- package/dist/core/daily-logic.d.ts +1 -1
- package/dist/core/daily-logic.js +1 -3
- package/dist/core/pr-monitor.d.ts +2 -27
- package/dist/core/pr-monitor.js +4 -110
- package/dist/core/state.d.ts +8 -40
- package/dist/core/state.js +36 -93
- package/dist/core/status-determination.d.ts +35 -0
- package/dist/core/status-determination.js +112 -0
- package/dist/core/types.d.ts +10 -11
- package/dist/core/types.js +0 -1
- package/package.json +1 -1
- package/dist/commands/override.d.ts +0 -21
- package/dist/commands/override.js +0 -35
- package/dist/commands/snooze.d.ts +0 -24
- package/dist/commands/snooze.js +0 -40
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,
|
|
5
|
+
import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, StateEvent, StateEventType, DailyDigest, LocalRepoCache, StatusOverride, FetchedPRStatus, StoredMergedPR, StoredClosedPR } 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),
|
|
@@ -240,57 +240,25 @@ export declare class StateManager {
|
|
|
240
240
|
*/
|
|
241
241
|
isPRShelved(url: string): boolean;
|
|
242
242
|
/**
|
|
243
|
-
* Dismiss an issue
|
|
243
|
+
* Dismiss an issue by URL. Dismissed issues are excluded from `new_response` notifications
|
|
244
244
|
* until new activity occurs after the dismiss timestamp.
|
|
245
|
-
* @param url - The full GitHub issue
|
|
246
|
-
* @param timestamp - ISO timestamp of when the issue
|
|
245
|
+
* @param url - The full GitHub issue URL.
|
|
246
|
+
* @param timestamp - ISO timestamp of when the issue was dismissed.
|
|
247
247
|
* @returns true if newly dismissed, false if already dismissed.
|
|
248
248
|
*/
|
|
249
249
|
dismissIssue(url: string, timestamp: string): boolean;
|
|
250
250
|
/**
|
|
251
|
-
* Undismiss an issue
|
|
252
|
-
* @param url - The full GitHub issue
|
|
251
|
+
* Undismiss an issue by URL.
|
|
252
|
+
* @param url - The full GitHub issue URL.
|
|
253
253
|
* @returns true if found and removed, false if not dismissed.
|
|
254
254
|
*/
|
|
255
255
|
undismissIssue(url: string): boolean;
|
|
256
256
|
/**
|
|
257
|
-
* Get the timestamp when an issue
|
|
258
|
-
* @param url - The full GitHub issue
|
|
257
|
+
* Get the timestamp when an issue was dismissed.
|
|
258
|
+
* @param url - The full GitHub issue URL.
|
|
259
259
|
* @returns The ISO dismiss timestamp, or undefined if not dismissed.
|
|
260
260
|
*/
|
|
261
261
|
getIssueDismissedAt(url: string): string | undefined;
|
|
262
|
-
/**
|
|
263
|
-
* Snooze a PR's CI failure for a given number of days.
|
|
264
|
-
* Snoozed PRs are excluded from actionable CI failure lists until the snooze expires.
|
|
265
|
-
* @param url - The full GitHub PR URL.
|
|
266
|
-
* @param reason - Why the CI failure is being snoozed (e.g., "upstream infrastructure issue").
|
|
267
|
-
* @param durationDays - Number of days to snooze. Default 7.
|
|
268
|
-
* @returns true if newly snoozed, false if already snoozed.
|
|
269
|
-
*/
|
|
270
|
-
snoozePR(url: string, reason: string, durationDays: number): boolean;
|
|
271
|
-
/**
|
|
272
|
-
* Unsnooze a PR by URL.
|
|
273
|
-
* @param url - The full GitHub PR URL.
|
|
274
|
-
* @returns true if found and removed, false if not snoozed.
|
|
275
|
-
*/
|
|
276
|
-
unsnoozePR(url: string): boolean;
|
|
277
|
-
/**
|
|
278
|
-
* Check if a PR is currently snoozed (not expired).
|
|
279
|
-
* @param url - The full GitHub PR URL.
|
|
280
|
-
* @returns true if the PR is snoozed and the snooze has not expired.
|
|
281
|
-
*/
|
|
282
|
-
isSnoozed(url: string): boolean;
|
|
283
|
-
/**
|
|
284
|
-
* Get snooze metadata for a PR.
|
|
285
|
-
* @param url - The full GitHub PR URL.
|
|
286
|
-
* @returns The snooze metadata, or undefined if not snoozed.
|
|
287
|
-
*/
|
|
288
|
-
getSnoozeInfo(url: string): SnoozeInfo | undefined;
|
|
289
|
-
/**
|
|
290
|
-
* Expire all snoozes that are past their `expiresAt` timestamp.
|
|
291
|
-
* @returns Array of PR URLs whose snoozes were expired.
|
|
292
|
-
*/
|
|
293
|
-
expireSnoozes(): string[];
|
|
294
262
|
/**
|
|
295
263
|
* Set a manual status override for a PR.
|
|
296
264
|
* @param url - The full GitHub PR URL.
|
package/dist/core/state.js
CHANGED
|
@@ -6,7 +6,7 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { INITIAL_STATE, isBelowMinStars, } from './types.js';
|
|
8
8
|
import { getStatePath, getBackupDir, getDataDir } from './utils.js';
|
|
9
|
-
import {
|
|
9
|
+
import { errorMessage } from './errors.js';
|
|
10
10
|
import { debug, warn } from './logger.js';
|
|
11
11
|
const MODULE = 'state';
|
|
12
12
|
// Current state version
|
|
@@ -196,7 +196,6 @@ export class StateManager {
|
|
|
196
196
|
trustedProjects: [],
|
|
197
197
|
shelvedPRUrls: [],
|
|
198
198
|
dismissedIssues: {},
|
|
199
|
-
snoozedPRs: {},
|
|
200
199
|
},
|
|
201
200
|
events: [],
|
|
202
201
|
lastRunAt: new Date().toISOString(),
|
|
@@ -342,6 +341,34 @@ export class StateManager {
|
|
|
342
341
|
atomicWriteFileSync(statePath, JSON.stringify(state, null, 2), 0o600);
|
|
343
342
|
debug(MODULE, 'Migrated state saved');
|
|
344
343
|
}
|
|
344
|
+
// Strip legacy fields from persisted state (snoozedPRs and PR dismiss
|
|
345
|
+
// entries were removed in the three-state PR model simplification)
|
|
346
|
+
try {
|
|
347
|
+
let needsCleanupSave = false;
|
|
348
|
+
const rawConfig = state.config;
|
|
349
|
+
if (rawConfig.snoozedPRs) {
|
|
350
|
+
delete rawConfig.snoozedPRs;
|
|
351
|
+
needsCleanupSave = true;
|
|
352
|
+
}
|
|
353
|
+
// Strip PR URLs from dismissedIssues (PR dismiss removed)
|
|
354
|
+
if (state.config.dismissedIssues) {
|
|
355
|
+
const PR_URL_RE = /\/pull\/\d+$/;
|
|
356
|
+
for (const url of Object.keys(state.config.dismissedIssues)) {
|
|
357
|
+
if (PR_URL_RE.test(url)) {
|
|
358
|
+
delete state.config.dismissedIssues[url];
|
|
359
|
+
needsCleanupSave = true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (needsCleanupSave) {
|
|
364
|
+
atomicWriteFileSync(statePath, JSON.stringify(state, null, 2), 0o600);
|
|
365
|
+
warn(MODULE, 'Cleaned up removed features (snoozedPRs, dismissed PR URLs) from persisted state');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (cleanupError) {
|
|
369
|
+
warn(MODULE, `Failed to clean up removed features from state: ${errorMessage(cleanupError)}`);
|
|
370
|
+
// Continue with loaded state — cleanup will be retried on next load
|
|
371
|
+
}
|
|
345
372
|
// Log appropriate message based on version
|
|
346
373
|
const repoCount = Object.keys(state.repoScores).length;
|
|
347
374
|
debug(MODULE, `Loaded state v${state.version}: ${repoCount} repo scores tracked`);
|
|
@@ -803,10 +830,10 @@ export class StateManager {
|
|
|
803
830
|
}
|
|
804
831
|
// === Dismiss / Undismiss Issues ===
|
|
805
832
|
/**
|
|
806
|
-
* Dismiss an issue
|
|
833
|
+
* Dismiss an issue by URL. Dismissed issues are excluded from `new_response` notifications
|
|
807
834
|
* until new activity occurs after the dismiss timestamp.
|
|
808
|
-
* @param url - The full GitHub issue
|
|
809
|
-
* @param timestamp - ISO timestamp of when the issue
|
|
835
|
+
* @param url - The full GitHub issue URL.
|
|
836
|
+
* @param timestamp - ISO timestamp of when the issue was dismissed.
|
|
810
837
|
* @returns true if newly dismissed, false if already dismissed.
|
|
811
838
|
*/
|
|
812
839
|
dismissIssue(url, timestamp) {
|
|
@@ -820,8 +847,8 @@ export class StateManager {
|
|
|
820
847
|
return true;
|
|
821
848
|
}
|
|
822
849
|
/**
|
|
823
|
-
* Undismiss an issue
|
|
824
|
-
* @param url - The full GitHub issue
|
|
850
|
+
* Undismiss an issue by URL.
|
|
851
|
+
* @param url - The full GitHub issue URL.
|
|
825
852
|
* @returns true if found and removed, false if not dismissed.
|
|
826
853
|
*/
|
|
827
854
|
undismissIssue(url) {
|
|
@@ -832,97 +859,13 @@ export class StateManager {
|
|
|
832
859
|
return true;
|
|
833
860
|
}
|
|
834
861
|
/**
|
|
835
|
-
* Get the timestamp when an issue
|
|
836
|
-
* @param url - The full GitHub issue
|
|
862
|
+
* Get the timestamp when an issue was dismissed.
|
|
863
|
+
* @param url - The full GitHub issue URL.
|
|
837
864
|
* @returns The ISO dismiss timestamp, or undefined if not dismissed.
|
|
838
865
|
*/
|
|
839
866
|
getIssueDismissedAt(url) {
|
|
840
867
|
return this.state.config.dismissedIssues?.[url];
|
|
841
868
|
}
|
|
842
|
-
// === Snooze / Unsnooze CI Failures ===
|
|
843
|
-
/**
|
|
844
|
-
* Snooze a PR's CI failure for a given number of days.
|
|
845
|
-
* Snoozed PRs are excluded from actionable CI failure lists until the snooze expires.
|
|
846
|
-
* @param url - The full GitHub PR URL.
|
|
847
|
-
* @param reason - Why the CI failure is being snoozed (e.g., "upstream infrastructure issue").
|
|
848
|
-
* @param durationDays - Number of days to snooze. Default 7.
|
|
849
|
-
* @returns true if newly snoozed, false if already snoozed.
|
|
850
|
-
*/
|
|
851
|
-
snoozePR(url, reason, durationDays) {
|
|
852
|
-
if (!Number.isFinite(durationDays) || durationDays <= 0) {
|
|
853
|
-
throw new ValidationError(`Invalid snooze duration: ${durationDays}. Must be a positive finite number.`);
|
|
854
|
-
}
|
|
855
|
-
if (!this.state.config.snoozedPRs) {
|
|
856
|
-
this.state.config.snoozedPRs = {};
|
|
857
|
-
}
|
|
858
|
-
if (url in this.state.config.snoozedPRs) {
|
|
859
|
-
return false;
|
|
860
|
-
}
|
|
861
|
-
const now = new Date();
|
|
862
|
-
const expiresAt = new Date(now.getTime() + durationDays * 24 * 60 * 60 * 1000);
|
|
863
|
-
this.state.config.snoozedPRs[url] = {
|
|
864
|
-
reason,
|
|
865
|
-
snoozedAt: now.toISOString(),
|
|
866
|
-
expiresAt: expiresAt.toISOString(),
|
|
867
|
-
};
|
|
868
|
-
return true;
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Unsnooze a PR by URL.
|
|
872
|
-
* @param url - The full GitHub PR URL.
|
|
873
|
-
* @returns true if found and removed, false if not snoozed.
|
|
874
|
-
*/
|
|
875
|
-
unsnoozePR(url) {
|
|
876
|
-
if (!this.state.config.snoozedPRs || !(url in this.state.config.snoozedPRs)) {
|
|
877
|
-
return false;
|
|
878
|
-
}
|
|
879
|
-
delete this.state.config.snoozedPRs[url];
|
|
880
|
-
return true;
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Check if a PR is currently snoozed (not expired).
|
|
884
|
-
* @param url - The full GitHub PR URL.
|
|
885
|
-
* @returns true if the PR is snoozed and the snooze has not expired.
|
|
886
|
-
*/
|
|
887
|
-
isSnoozed(url) {
|
|
888
|
-
const info = this.getSnoozeInfo(url);
|
|
889
|
-
if (!info)
|
|
890
|
-
return false;
|
|
891
|
-
const expiresAtMs = new Date(info.expiresAt).getTime();
|
|
892
|
-
if (isNaN(expiresAtMs)) {
|
|
893
|
-
warn(MODULE, `Invalid expiresAt for snoozed PR ${url}: "${info.expiresAt}". Treating as not snoozed.`);
|
|
894
|
-
return false;
|
|
895
|
-
}
|
|
896
|
-
return expiresAtMs > Date.now();
|
|
897
|
-
}
|
|
898
|
-
/**
|
|
899
|
-
* Get snooze metadata for a PR.
|
|
900
|
-
* @param url - The full GitHub PR URL.
|
|
901
|
-
* @returns The snooze metadata, or undefined if not snoozed.
|
|
902
|
-
*/
|
|
903
|
-
getSnoozeInfo(url) {
|
|
904
|
-
return this.state.config.snoozedPRs?.[url];
|
|
905
|
-
}
|
|
906
|
-
/**
|
|
907
|
-
* Expire all snoozes that are past their `expiresAt` timestamp.
|
|
908
|
-
* @returns Array of PR URLs whose snoozes were expired.
|
|
909
|
-
*/
|
|
910
|
-
expireSnoozes() {
|
|
911
|
-
if (!this.state.config.snoozedPRs)
|
|
912
|
-
return [];
|
|
913
|
-
const expired = [];
|
|
914
|
-
const now = Date.now();
|
|
915
|
-
for (const [url, info] of Object.entries(this.state.config.snoozedPRs)) {
|
|
916
|
-
const expiresAtMs = new Date(info.expiresAt).getTime();
|
|
917
|
-
if (isNaN(expiresAtMs) || expiresAtMs <= now) {
|
|
918
|
-
expired.push(url);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
for (const url of expired) {
|
|
922
|
-
delete this.state.config.snoozedPRs[url];
|
|
923
|
-
}
|
|
924
|
-
return expired;
|
|
925
|
-
}
|
|
926
869
|
// === Status Overrides ===
|
|
927
870
|
/**
|
|
928
871
|
* Set a manual status override for a PR.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status determination logic for PRs — extracted from PRMonitor (#263).
|
|
3
|
+
*
|
|
4
|
+
* Computes the top-level status (needs_addressing vs waiting_on_maintainer),
|
|
5
|
+
* granular action/wait reasons, and staleness tier for a single PR based on
|
|
6
|
+
* its CI, review, merge-conflict, and timeline signals.
|
|
7
|
+
*/
|
|
8
|
+
import type { DetermineStatusInput, DetermineStatusResult } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* CI-fix bots that push commits as a direct result of the contributor's push (#568).
|
|
11
|
+
* Their commits represent contributor work and should count as addressing feedback.
|
|
12
|
+
* This is intentionally an allowlist — not all `[bot]` accounts are CI-fix bots
|
|
13
|
+
* (e.g. dependabot[bot] and renovate[bot] open their own PRs).
|
|
14
|
+
* Values must be lowercase — lookup uses .toLowerCase() for case-insensitive matching.
|
|
15
|
+
*/
|
|
16
|
+
export declare const CI_FIX_BOTS: ReadonlySet<string>;
|
|
17
|
+
/** Minimum gap (ms) between maintainer comment and contributor commit for
|
|
18
|
+
* the commit to count as "addressing" the feedback (#547). Prevents false
|
|
19
|
+
* positives from race conditions, clock skew, and in-flight pushes. */
|
|
20
|
+
export declare const MIN_RESPONSE_GAP_MS: number;
|
|
21
|
+
/**
|
|
22
|
+
* Check whether the HEAD commit was authored by the contributor (#547).
|
|
23
|
+
* Returns true when the author matches, when the author is a known CI-fix
|
|
24
|
+
* bot (#568), or when author info is unavailable (graceful degradation).
|
|
25
|
+
*/
|
|
26
|
+
export declare function isContributorCommit(commitAuthor?: string, contributorUsername?: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Check whether the contributor's commit is meaningfully after the maintainer's
|
|
29
|
+
* comment — i.e. the commit timestamp is at least MIN_RESPONSE_GAP_MS later (#547).
|
|
30
|
+
*/
|
|
31
|
+
export declare function isCommitAfterComment(commitDate: string, commentDate: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Determine the overall status of a PR based on its signals.
|
|
34
|
+
*/
|
|
35
|
+
export declare function determineStatus(input: DetermineStatusInput): DetermineStatusResult;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status determination logic for PRs — extracted from PRMonitor (#263).
|
|
3
|
+
*
|
|
4
|
+
* Computes the top-level status (needs_addressing vs waiting_on_maintainer),
|
|
5
|
+
* granular action/wait reasons, and staleness tier for a single PR based on
|
|
6
|
+
* its CI, review, merge-conflict, and timeline signals.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* CI-fix bots that push commits as a direct result of the contributor's push (#568).
|
|
10
|
+
* Their commits represent contributor work and should count as addressing feedback.
|
|
11
|
+
* This is intentionally an allowlist — not all `[bot]` accounts are CI-fix bots
|
|
12
|
+
* (e.g. dependabot[bot] and renovate[bot] open their own PRs).
|
|
13
|
+
* Values must be lowercase — lookup uses .toLowerCase() for case-insensitive matching.
|
|
14
|
+
*/
|
|
15
|
+
export const CI_FIX_BOTS = new Set(['autofix-ci[bot]', 'prettier-ci[bot]', 'pre-commit-ci[bot]']);
|
|
16
|
+
/** Minimum gap (ms) between maintainer comment and contributor commit for
|
|
17
|
+
* the commit to count as "addressing" the feedback (#547). Prevents false
|
|
18
|
+
* positives from race conditions, clock skew, and in-flight pushes. */
|
|
19
|
+
export const MIN_RESPONSE_GAP_MS = 2 * 60 * 1000; // 2 minutes
|
|
20
|
+
/**
|
|
21
|
+
* Check whether the HEAD commit was authored by the contributor (#547).
|
|
22
|
+
* Returns true when the author matches, when the author is a known CI-fix
|
|
23
|
+
* bot (#568), or when author info is unavailable (graceful degradation).
|
|
24
|
+
*/
|
|
25
|
+
export function isContributorCommit(commitAuthor, contributorUsername) {
|
|
26
|
+
if (!commitAuthor || !contributorUsername)
|
|
27
|
+
return true; // degrade gracefully
|
|
28
|
+
const author = commitAuthor.toLowerCase();
|
|
29
|
+
if (CI_FIX_BOTS.has(author))
|
|
30
|
+
return true; // CI-fix bots act on behalf of the contributor (#568)
|
|
31
|
+
return author === contributorUsername.toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Check whether the contributor's commit is meaningfully after the maintainer's
|
|
35
|
+
* comment — i.e. the commit timestamp is at least MIN_RESPONSE_GAP_MS later (#547).
|
|
36
|
+
*/
|
|
37
|
+
export function isCommitAfterComment(commitDate, commentDate) {
|
|
38
|
+
const commitMs = new Date(commitDate).getTime();
|
|
39
|
+
const commentMs = new Date(commentDate).getTime();
|
|
40
|
+
if (Number.isNaN(commitMs) || Number.isNaN(commentMs)) {
|
|
41
|
+
// Fall back to simple string comparison (pre-#547 behavior)
|
|
42
|
+
return commitDate > commentDate;
|
|
43
|
+
}
|
|
44
|
+
return commitMs - commentMs >= MIN_RESPONSE_GAP_MS;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Determine the overall status of a PR based on its signals.
|
|
48
|
+
*/
|
|
49
|
+
export function determineStatus(input) {
|
|
50
|
+
const { ciStatus, hasMergeConflict, hasUnrespondedComment, hasIncompleteChecklist, reviewDecision, daysSinceActivity, dormantThreshold, approachingThreshold, latestCommitDate: rawCommitDate, latestCommitAuthor, contributorUsername, lastMaintainerCommentDate, latestChangesRequestedDate, hasActionableCIFailure = true, } = input;
|
|
51
|
+
// Compute staleness tier (independent of status)
|
|
52
|
+
let stalenessTier = 'active';
|
|
53
|
+
if (daysSinceActivity >= dormantThreshold)
|
|
54
|
+
stalenessTier = 'dormant';
|
|
55
|
+
else if (daysSinceActivity >= approachingThreshold)
|
|
56
|
+
stalenessTier = 'approaching_dormant';
|
|
57
|
+
// Only count the latest commit if it was authored by the contributor or a
|
|
58
|
+
// CI bot (#547, #568). Non-contributor commits (maintainer merge commits,
|
|
59
|
+
// GitHub suggestion commits) should not mask unaddressed feedback.
|
|
60
|
+
const latestCommitDate = rawCommitDate && isContributorCommit(latestCommitAuthor, contributorUsername) ? rawCommitDate : undefined;
|
|
61
|
+
// Priority order: needs_addressing (response/changes/ci/conflict/checklist) > waiting_on_maintainer (review/merge/addressed/ci_blocked)
|
|
62
|
+
if (hasUnrespondedComment) {
|
|
63
|
+
// If the contributor pushed a commit after the maintainer's comment,
|
|
64
|
+
// the changes have been addressed — waiting for maintainer re-review.
|
|
65
|
+
// Require a minimum 2-minute gap to avoid false positives from race
|
|
66
|
+
// conditions (pushing while review is being submitted) (#547).
|
|
67
|
+
if (latestCommitDate &&
|
|
68
|
+
lastMaintainerCommentDate &&
|
|
69
|
+
isCommitAfterComment(latestCommitDate, lastMaintainerCommentDate)) {
|
|
70
|
+
// Safety net (#431): if a CHANGES_REQUESTED review was submitted after
|
|
71
|
+
// the commit, the maintainer still expects changes — don't mask it
|
|
72
|
+
if (latestChangesRequestedDate && latestCommitDate < latestChangesRequestedDate) {
|
|
73
|
+
return { status: 'needs_addressing', actionReason: 'needs_response', stalenessTier };
|
|
74
|
+
}
|
|
75
|
+
if (ciStatus === 'failing' && hasActionableCIFailure)
|
|
76
|
+
return { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier };
|
|
77
|
+
// Non-actionable CI failures (infrastructure, fork, auth) don't block changes_addressed —
|
|
78
|
+
// the contributor can't fix them, so the relevant status is "waiting for re-review" (#502)
|
|
79
|
+
return { status: 'waiting_on_maintainer', waitReason: 'changes_addressed', stalenessTier };
|
|
80
|
+
}
|
|
81
|
+
return { status: 'needs_addressing', actionReason: 'needs_response', stalenessTier };
|
|
82
|
+
}
|
|
83
|
+
// Review requested changes but no unresponded comment.
|
|
84
|
+
// If the latest commit is before the review, the contributor hasn't addressed it yet.
|
|
85
|
+
if (reviewDecision === 'changes_requested' && latestChangesRequestedDate) {
|
|
86
|
+
if (!latestCommitDate || latestCommitDate < latestChangesRequestedDate) {
|
|
87
|
+
return { status: 'needs_addressing', actionReason: 'needs_changes', stalenessTier };
|
|
88
|
+
}
|
|
89
|
+
// Commit is after review — changes have been addressed
|
|
90
|
+
if (ciStatus === 'failing' && hasActionableCIFailure)
|
|
91
|
+
return { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier };
|
|
92
|
+
// Non-actionable CI failures don't block changes_addressed (#502)
|
|
93
|
+
return { status: 'waiting_on_maintainer', waitReason: 'changes_addressed', stalenessTier };
|
|
94
|
+
}
|
|
95
|
+
if (ciStatus === 'failing') {
|
|
96
|
+
return hasActionableCIFailure
|
|
97
|
+
? { status: 'needs_addressing', actionReason: 'failing_ci', stalenessTier }
|
|
98
|
+
: { status: 'waiting_on_maintainer', waitReason: 'ci_blocked', stalenessTier };
|
|
99
|
+
}
|
|
100
|
+
if (hasMergeConflict) {
|
|
101
|
+
return { status: 'needs_addressing', actionReason: 'merge_conflict', stalenessTier };
|
|
102
|
+
}
|
|
103
|
+
if (hasIncompleteChecklist) {
|
|
104
|
+
return { status: 'needs_addressing', actionReason: 'incomplete_checklist', stalenessTier };
|
|
105
|
+
}
|
|
106
|
+
// Approved and CI passing/unknown = waiting on maintainer to merge
|
|
107
|
+
if (reviewDecision === 'approved' && (ciStatus === 'passing' || ciStatus === 'unknown')) {
|
|
108
|
+
return { status: 'waiting_on_maintainer', waitReason: 'pending_merge', stalenessTier };
|
|
109
|
+
}
|
|
110
|
+
// Default: no actionable issues found. Covers pending CI, no reviews yet, etc.
|
|
111
|
+
return { status: 'waiting_on_maintainer', waitReason: 'pending_review', stalenessTier };
|
|
112
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface RepoGroup {
|
|
|
41
41
|
}
|
|
42
42
|
/** GitHub's pull request review decision (from the reviewDecision GraphQL field). */
|
|
43
43
|
export type ReviewDecision = 'approved' | 'changes_requested' | 'review_required' | 'unknown';
|
|
44
|
-
/** Input options for `
|
|
44
|
+
/** Input options for `determineStatus()` (see status-determination.ts). */
|
|
45
45
|
export interface DetermineStatusInput {
|
|
46
46
|
ciStatus: CIStatus;
|
|
47
47
|
hasMergeConflict: boolean;
|
|
@@ -61,6 +61,13 @@ export interface DetermineStatusInput {
|
|
|
61
61
|
/** True if at least one failing CI check is classified as 'actionable'. */
|
|
62
62
|
hasActionableCIFailure?: boolean;
|
|
63
63
|
}
|
|
64
|
+
/** Result of `determineStatus()` — the PR's computed status classification. */
|
|
65
|
+
export interface DetermineStatusResult {
|
|
66
|
+
status: FetchedPRStatus;
|
|
67
|
+
actionReason?: ActionReason;
|
|
68
|
+
waitReason?: WaitReason;
|
|
69
|
+
stalenessTier: StalenessTier;
|
|
70
|
+
}
|
|
64
71
|
/**
|
|
65
72
|
* Granular reason why a PR needs addressing (contributor's turn).
|
|
66
73
|
* Active values (produced by determineStatus): needs_response, needs_changes,
|
|
@@ -97,7 +104,7 @@ export interface FetchedPR {
|
|
|
97
104
|
repo: string;
|
|
98
105
|
number: number;
|
|
99
106
|
title: string;
|
|
100
|
-
/** Computed by `
|
|
107
|
+
/** Computed by `determineStatus()` based on the fields below. */
|
|
101
108
|
status: FetchedPRStatus;
|
|
102
109
|
/** Granular reason for needs_addressing status. Undefined when waiting_on_maintainer. */
|
|
103
110
|
actionReason?: ActionReason;
|
|
@@ -414,12 +421,6 @@ export interface LocalRepoCache {
|
|
|
414
421
|
/** ISO 8601 timestamp of when the scan was performed */
|
|
415
422
|
cachedAt: string;
|
|
416
423
|
}
|
|
417
|
-
/** Metadata for a snoozed PR's CI failure. */
|
|
418
|
-
export interface SnoozeInfo {
|
|
419
|
-
reason: string;
|
|
420
|
-
snoozedAt: string;
|
|
421
|
-
expiresAt: string;
|
|
422
|
-
}
|
|
423
424
|
/** Filter for excluding repos below a minimum star count from PR count queries. */
|
|
424
425
|
export interface StarFilter {
|
|
425
426
|
minStars: number;
|
|
@@ -479,10 +480,8 @@ export interface AgentConfig {
|
|
|
479
480
|
aiPolicyBlocklist?: string[];
|
|
480
481
|
/** PR URLs manually shelved by the user. Shelved PRs are excluded from capacity and actionable issues. Auto-unshelved when maintainers engage. */
|
|
481
482
|
shelvedPRUrls?: string[];
|
|
482
|
-
/** Issue
|
|
483
|
+
/** Issue URLs dismissed by the user, mapped to ISO timestamp of when dismissed. Issues with new responses after the dismiss timestamp resurface automatically. */
|
|
483
484
|
dismissedIssues?: Record<string, string>;
|
|
484
|
-
/** PR URLs with snoozed CI failures, mapped to snooze metadata. Snoozed PRs are excluded from actionable CI failure list until expiry. */
|
|
485
|
-
snoozedPRs?: Record<string, SnoozeInfo>;
|
|
486
485
|
/** Manual status overrides for PRs. Maps PR URL to override metadata. Auto-clears when the PR has new activity. */
|
|
487
486
|
statusOverrides?: Record<string, StatusOverride>;
|
|
488
487
|
/** Project categories the user is interested in (e.g., devtools, nonprofit). Used to prioritize search results. */
|
package/dist/core/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Override command
|
|
3
|
-
* Manually override a PR's status (needs_addressing ↔ waiting_on_maintainer).
|
|
4
|
-
* Overrides auto-clear when the PR has new activity.
|
|
5
|
-
*/
|
|
6
|
-
import type { FetchedPRStatus } from '../core/types.js';
|
|
7
|
-
export interface OverrideOutput {
|
|
8
|
-
url: string;
|
|
9
|
-
status: FetchedPRStatus;
|
|
10
|
-
}
|
|
11
|
-
export interface ClearOverrideOutput {
|
|
12
|
-
url: string;
|
|
13
|
-
cleared: boolean;
|
|
14
|
-
}
|
|
15
|
-
export declare function runOverride(options: {
|
|
16
|
-
prUrl: string;
|
|
17
|
-
status: string;
|
|
18
|
-
}): Promise<OverrideOutput>;
|
|
19
|
-
export declare function runClearOverride(options: {
|
|
20
|
-
prUrl: string;
|
|
21
|
-
}): Promise<ClearOverrideOutput>;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Override command
|
|
3
|
-
* Manually override a PR's status (needs_addressing ↔ waiting_on_maintainer).
|
|
4
|
-
* Overrides auto-clear when the PR has new activity.
|
|
5
|
-
*/
|
|
6
|
-
import { getStateManager } from '../core/index.js';
|
|
7
|
-
import { PR_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
|
|
8
|
-
const VALID_STATUSES = ['needs_addressing', 'waiting_on_maintainer'];
|
|
9
|
-
export async function runOverride(options) {
|
|
10
|
-
validateUrl(options.prUrl);
|
|
11
|
-
validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
|
|
12
|
-
if (!VALID_STATUSES.includes(options.status)) {
|
|
13
|
-
throw new Error(`Invalid status "${options.status}". Must be one of: ${VALID_STATUSES.join(', ')}`);
|
|
14
|
-
}
|
|
15
|
-
const status = options.status;
|
|
16
|
-
const stateManager = getStateManager();
|
|
17
|
-
// Use current time as lastActivityAt — the CLI doesn't have cached PR data.
|
|
18
|
-
// This means the override will auto-clear on the next daily run if the PR's
|
|
19
|
-
// updatedAt is after this timestamp (which is the desired behavior: the override
|
|
20
|
-
// will persist until new activity occurs on the PR).
|
|
21
|
-
const lastActivityAt = new Date().toISOString();
|
|
22
|
-
stateManager.setStatusOverride(options.prUrl, status, lastActivityAt);
|
|
23
|
-
stateManager.save();
|
|
24
|
-
return { url: options.prUrl, status };
|
|
25
|
-
}
|
|
26
|
-
export async function runClearOverride(options) {
|
|
27
|
-
validateUrl(options.prUrl);
|
|
28
|
-
validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
|
|
29
|
-
const stateManager = getStateManager();
|
|
30
|
-
const cleared = stateManager.clearStatusOverride(options.prUrl);
|
|
31
|
-
if (cleared) {
|
|
32
|
-
stateManager.save();
|
|
33
|
-
}
|
|
34
|
-
return { url: options.prUrl, cleared };
|
|
35
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Snooze/Unsnooze commands
|
|
3
|
-
* Manages snoozing CI failure notifications for PRs with known upstream/infrastructure issues.
|
|
4
|
-
* Snoozed PRs are excluded from the actionable CI failure list until the snooze expires.
|
|
5
|
-
*/
|
|
6
|
-
export interface SnoozeOutput {
|
|
7
|
-
snoozed: boolean;
|
|
8
|
-
url: string;
|
|
9
|
-
days: number;
|
|
10
|
-
reason: string;
|
|
11
|
-
expiresAt: string | undefined;
|
|
12
|
-
}
|
|
13
|
-
export interface UnsnoozeOutput {
|
|
14
|
-
unsnoozed: boolean;
|
|
15
|
-
url: string;
|
|
16
|
-
}
|
|
17
|
-
export declare function runSnooze(options: {
|
|
18
|
-
prUrl: string;
|
|
19
|
-
reason: string;
|
|
20
|
-
days?: number;
|
|
21
|
-
}): Promise<SnoozeOutput>;
|
|
22
|
-
export declare function runUnsnooze(options: {
|
|
23
|
-
prUrl: string;
|
|
24
|
-
}): Promise<UnsnoozeOutput>;
|
package/dist/commands/snooze.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Snooze/Unsnooze commands
|
|
3
|
-
* Manages snoozing CI failure notifications for PRs with known upstream/infrastructure issues.
|
|
4
|
-
* Snoozed PRs are excluded from the actionable CI failure list until the snooze expires.
|
|
5
|
-
*/
|
|
6
|
-
import { getStateManager } from '../core/index.js';
|
|
7
|
-
import { PR_URL_PATTERN, validateGitHubUrl, validateUrl, validateMessage } from './validation.js';
|
|
8
|
-
const DEFAULT_SNOOZE_DAYS = 7;
|
|
9
|
-
export async function runSnooze(options) {
|
|
10
|
-
validateUrl(options.prUrl);
|
|
11
|
-
validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
|
|
12
|
-
validateMessage(options.reason);
|
|
13
|
-
const days = options.days ?? DEFAULT_SNOOZE_DAYS;
|
|
14
|
-
if (!Number.isFinite(days) || days <= 0) {
|
|
15
|
-
throw new Error('Snooze duration must be a positive number of days.');
|
|
16
|
-
}
|
|
17
|
-
const stateManager = getStateManager();
|
|
18
|
-
const added = stateManager.snoozePR(options.prUrl, options.reason, days);
|
|
19
|
-
if (added) {
|
|
20
|
-
stateManager.save();
|
|
21
|
-
}
|
|
22
|
-
const snoozeInfo = stateManager.getSnoozeInfo(options.prUrl);
|
|
23
|
-
return {
|
|
24
|
-
snoozed: added,
|
|
25
|
-
url: options.prUrl,
|
|
26
|
-
days,
|
|
27
|
-
reason: options.reason,
|
|
28
|
-
expiresAt: snoozeInfo?.expiresAt,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
export async function runUnsnooze(options) {
|
|
32
|
-
validateUrl(options.prUrl);
|
|
33
|
-
validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
|
|
34
|
-
const stateManager = getStateManager();
|
|
35
|
-
const removed = stateManager.unsnoozePR(options.prUrl);
|
|
36
|
-
if (removed) {
|
|
37
|
-
stateManager.save();
|
|
38
|
-
}
|
|
39
|
-
return { unsnoozed: removed, url: options.prUrl };
|
|
40
|
-
}
|