@oss-autopilot/core 0.47.2 → 0.49.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.bundle.cjs +48 -48
- package/dist/cli.bundle.cjs.map +3 -3
- package/dist/commands/daily.d.ts +1 -1
- package/dist/commands/daily.js +15 -8
- package/dist/commands/dashboard-data.d.ts +8 -2
- package/dist/commands/dashboard-data.js +53 -9
- package/dist/commands/dashboard-lifecycle.js +1 -1
- package/dist/commands/dashboard-process.js +1 -1
- package/dist/commands/dashboard-server.js +16 -48
- package/dist/core/daily-logic.d.ts +3 -2
- package/dist/core/daily-logic.js +72 -2
- package/dist/core/github-stats.d.ts +10 -1
- package/dist/core/github-stats.js +58 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/state.d.ts +17 -1
- package/dist/core/state.js +37 -3
- package/dist/core/types.d.ts +10 -2
- package/dist/formatters/json.d.ts +4 -0
- package/dist/formatters/json.js +1 -0
- package/package.json +1 -1
package/dist/commands/daily.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup, type AgentState, type StarFilter } from '../core/index.js';
|
|
10
10
|
import { type DailyOutput, type CapacityAssessment, type ActionableIssue, type ActionMenu } from '../formatters/json.js';
|
|
11
|
-
export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
11
|
+
export { applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
12
12
|
/**
|
|
13
13
|
* Build a star filter from state for use in fetchUserPRCounts.
|
|
14
14
|
* Returns undefined if no star data is available (first run).
|
package/dist/commands/daily.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Domain logic lives in src/core/daily-logic.ts; this file is a thin
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
|
-
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
|
|
9
|
+
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
|
|
10
10
|
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
11
11
|
import { warn } from '../core/logger.js';
|
|
12
12
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
@@ -15,7 +15,7 @@ import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '
|
|
|
15
15
|
const MODULE = 'daily';
|
|
16
16
|
// Re-export domain functions so existing consumers (tests, dashboard, startup)
|
|
17
17
|
// can continue importing from './daily.js' without changes.
|
|
18
|
-
export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
18
|
+
export { applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
19
19
|
/**
|
|
20
20
|
* Build a star filter from state for use in fetchUserPRCounts.
|
|
21
21
|
* Returns undefined if no star data is available (first run).
|
|
@@ -249,11 +249,15 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
|
|
|
249
249
|
catch (error) {
|
|
250
250
|
warn(MODULE, `Failed to expire/persist snoozes: ${errorMessage(error)}`);
|
|
251
251
|
}
|
|
252
|
+
// Apply dashboard/CLI status overrides before partitioning.
|
|
253
|
+
// This ensures PRs reclassified in the dashboard (e.g., "Need Attention" → "Waiting")
|
|
254
|
+
// are respected by the CLI pipeline.
|
|
255
|
+
const overriddenPRs = applyStatusOverrides(prs, stateManager.getState());
|
|
252
256
|
// Partition PRs into active vs shelved, auto-unshelving when maintainers engage
|
|
253
257
|
const shelvedPRs = [];
|
|
254
258
|
const autoUnshelvedPRs = [];
|
|
255
259
|
const activePRs = [];
|
|
256
|
-
for (const pr of
|
|
260
|
+
for (const pr of overriddenPRs) {
|
|
257
261
|
if (stateManager.isPRShelved(pr.url)) {
|
|
258
262
|
if (CRITICAL_STATUSES.has(pr.status)) {
|
|
259
263
|
stateManager.unshelvePR(pr.url);
|
|
@@ -273,10 +277,10 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
|
|
|
273
277
|
activePRs.push(pr);
|
|
274
278
|
}
|
|
275
279
|
}
|
|
276
|
-
// Generate digest from
|
|
280
|
+
// Generate digest from override-applied PRs so status categories are correct.
|
|
277
281
|
// Note: digest.openPRs contains ALL fetched PRs (including shelved).
|
|
278
282
|
// We override summary fields below to reflect active-only counts.
|
|
279
|
-
const digest = prMonitor.generateDigest(
|
|
283
|
+
const digest = prMonitor.generateDigest(overriddenPRs, recentlyClosedPRs, recentlyMergedPRs);
|
|
280
284
|
// Attach shelve info to digest
|
|
281
285
|
digest.shelvedPRs = shelvedPRs;
|
|
282
286
|
digest.autoUnshelvedPRs = autoUnshelvedPRs;
|
|
@@ -292,7 +296,7 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
|
|
|
292
296
|
* Assesses capacity, filters dismissed issues, computes actionable items,
|
|
293
297
|
* and assembles the action menu.
|
|
294
298
|
*/
|
|
295
|
-
function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures) {
|
|
299
|
+
function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures, previousLastDigestAt) {
|
|
296
300
|
const stateManager = getStateManager();
|
|
297
301
|
// Assess capacity from active PRs only (shelved PRs excluded)
|
|
298
302
|
const capacity = assessCapacity(activePRs, stateManager.getState().config.maxActivePRs, shelvedPRs.length);
|
|
@@ -357,7 +361,7 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
357
361
|
warn(MODULE, `Failed to persist auto-undismissed state: ${errorMessage(error)}`);
|
|
358
362
|
}
|
|
359
363
|
}
|
|
360
|
-
const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls);
|
|
364
|
+
const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls, previousLastDigestAt);
|
|
361
365
|
digest.summary.totalNeedingAttention = actionableIssues.length;
|
|
362
366
|
const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
|
|
363
367
|
const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues);
|
|
@@ -425,10 +429,13 @@ async function executeDailyCheckInternal(token) {
|
|
|
425
429
|
await updateRepoScores(prMonitor, prs, mergedCounts, closedCounts);
|
|
426
430
|
// Phase 3: Persist monthly analytics
|
|
427
431
|
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
432
|
+
// Capture lastDigestAt BEFORE Phase 4 overwrites it with the current run's timestamp.
|
|
433
|
+
// Used by collectActionableIssues to determine which PRs are "new" (created since last digest).
|
|
434
|
+
const previousLastDigestAt = getStateManager().getState().lastDigestAt;
|
|
428
435
|
// Phase 4: Expire snoozes, partition PRs, generate and save digest
|
|
429
436
|
const { activePRs, shelvedPRs, digest } = partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
430
437
|
// Phase 5: Build structured output (capacity, dismiss filter, action menu)
|
|
431
|
-
return generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures);
|
|
438
|
+
return generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures, previousLastDigestAt);
|
|
432
439
|
}
|
|
433
440
|
/**
|
|
434
441
|
* Run the daily check and return deduplicated DailyOutput.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
|
|
4
4
|
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
|
-
import { type DailyDigest, type AgentState, type MergedPR, type StoredMergedPR, type CommentedIssue } from '../core/types.js';
|
|
6
|
+
import { type DailyDigest, type AgentState, type ClosedPR, type MergedPR, type StoredMergedPR, type StoredClosedPR, type CommentedIssue } from '../core/types.js';
|
|
7
7
|
export interface DashboardStats {
|
|
8
8
|
activePRs: number;
|
|
9
9
|
shelvedPRs: number;
|
|
@@ -11,7 +11,7 @@ export interface DashboardStats {
|
|
|
11
11
|
closedPRs: number;
|
|
12
12
|
mergeRate: string;
|
|
13
13
|
}
|
|
14
|
-
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>, storedMergedCount?: number): DashboardStats;
|
|
14
|
+
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>, storedMergedCount?: number, storedClosedCount?: number): DashboardStats;
|
|
15
15
|
/**
|
|
16
16
|
* Merge fresh API counts into existing stored counts.
|
|
17
17
|
* Months present in the fresh data are updated; months only in the existing data are preserved.
|
|
@@ -32,6 +32,7 @@ export interface DashboardFetchResult {
|
|
|
32
32
|
digest: DailyDigest;
|
|
33
33
|
commentedIssues: CommentedIssue[];
|
|
34
34
|
allMergedPRs: MergedPR[];
|
|
35
|
+
allClosedPRs: ClosedPR[];
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
38
|
* Fetch fresh dashboard data from GitHub.
|
|
@@ -44,6 +45,11 @@ export declare function fetchDashboardData(token: string): Promise<DashboardFetc
|
|
|
44
45
|
* Skips entries with unparseable URLs.
|
|
45
46
|
*/
|
|
46
47
|
export declare function storedToMergedPRs(stored: StoredMergedPR[]): MergedPR[];
|
|
48
|
+
/**
|
|
49
|
+
* Convert StoredClosedPR[] to ClosedPR[] by deriving repo and number from URL.
|
|
50
|
+
* Skips entries with unparseable URLs.
|
|
51
|
+
*/
|
|
52
|
+
export declare function storedToClosedPRs(stored: StoredClosedPR[]): ClosedPR[];
|
|
47
53
|
/**
|
|
48
54
|
* Compute PRs grouped by repository from a digest and state.
|
|
49
55
|
* Used for chart data in the dashboard API.
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
import { getStateManager, PRMonitor, IssueConversationMonitor, getOctokit } from '../core/index.js';
|
|
7
7
|
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
8
8
|
import { warn } from '../core/logger.js';
|
|
9
|
-
import { emptyPRCountsResult, fetchMergedPRsSince } from '../core/github-stats.js';
|
|
9
|
+
import { emptyPRCountsResult, fetchMergedPRsSince, fetchClosedPRsSince } from '../core/github-stats.js';
|
|
10
10
|
import { parseGitHubUrl } from '../core/utils.js';
|
|
11
11
|
import { isBelowMinStars, } from '../core/types.js';
|
|
12
12
|
import { toShelvedPRRef, buildStarFilter } from './daily.js';
|
|
13
13
|
const MODULE = 'dashboard-data';
|
|
14
|
-
export function buildDashboardStats(digest, state, storedMergedCount) {
|
|
14
|
+
export function buildDashboardStats(digest, state, storedMergedCount, storedClosedCount) {
|
|
15
15
|
const summary = digest.summary || {
|
|
16
16
|
totalActivePRs: 0,
|
|
17
17
|
totalMergedAllTime: 0,
|
|
@@ -19,16 +19,19 @@ export function buildDashboardStats(digest, state, storedMergedCount) {
|
|
|
19
19
|
totalNeedingAttention: 0,
|
|
20
20
|
};
|
|
21
21
|
const minStars = state.config.minStars ?? 50;
|
|
22
|
-
//
|
|
23
|
-
// when stored list hasn't caught up yet (
|
|
22
|
+
// Merged: use the higher of stored count vs repoScores aggregate to avoid regressions
|
|
23
|
+
// when the stored list hasn't caught up yet (initial fetch caps at 300 PRs)
|
|
24
24
|
const mergedPRs = storedMergedCount !== undefined
|
|
25
25
|
? Math.max(storedMergedCount, summary.totalMergedAllTime)
|
|
26
26
|
: summary.totalMergedAllTime;
|
|
27
|
+
// Closed: same anti-regression strategy — use the higher of stored count vs repoScores aggregate
|
|
28
|
+
const aggregateClosedCount = Object.values(state.repoScores || {}).reduce((sum, s) => sum + (isBelowMinStars(s.stargazersCount, minStars) ? 0 : s.closedWithoutMergeCount || 0), 0);
|
|
29
|
+
const closedPRs = storedClosedCount !== undefined ? Math.max(storedClosedCount, aggregateClosedCount) : aggregateClosedCount;
|
|
27
30
|
return {
|
|
28
31
|
activePRs: summary.totalActivePRs,
|
|
29
32
|
shelvedPRs: (digest.shelvedPRs || []).length,
|
|
30
33
|
mergedPRs,
|
|
31
|
-
closedPRs
|
|
34
|
+
closedPRs,
|
|
32
35
|
mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
|
|
33
36
|
};
|
|
34
37
|
}
|
|
@@ -102,9 +105,10 @@ export async function fetchDashboardData(token) {
|
|
|
102
105
|
const config = stateManager.getState().config;
|
|
103
106
|
// Build star filter from cached repoScores (#576)
|
|
104
107
|
const starFilter = buildStarFilter(stateManager.getState());
|
|
105
|
-
// Get
|
|
108
|
+
// Get watermarks for incremental PR fetch
|
|
106
109
|
const watermark = stateManager.getMergedPRWatermark();
|
|
107
|
-
const
|
|
110
|
+
const closedWatermark = stateManager.getClosedPRWatermark();
|
|
111
|
+
const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues, newMergedPRs, newClosedPRs,] = await Promise.all([
|
|
108
112
|
prMonitor.fetchUserOpenPRs(),
|
|
109
113
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
110
114
|
if (isRateLimitOrAuthError(err))
|
|
@@ -151,6 +155,12 @@ export async function fetchDashboardData(token) {
|
|
|
151
155
|
warn(MODULE, `Failed to fetch merged PRs for storage: ${errorMessage(err)}`);
|
|
152
156
|
return [];
|
|
153
157
|
}),
|
|
158
|
+
fetchClosedPRsSince(octokit, config, closedWatermark).catch((err) => {
|
|
159
|
+
if (isRateLimitOrAuthError(err))
|
|
160
|
+
throw err;
|
|
161
|
+
warn(MODULE, `Failed to fetch closed PRs for storage: ${errorMessage(err)}`);
|
|
162
|
+
return [];
|
|
163
|
+
}),
|
|
154
164
|
]);
|
|
155
165
|
const commentedIssues = fetchedIssues.issues;
|
|
156
166
|
if (fetchedIssues.failures.length > 0) {
|
|
@@ -166,8 +176,16 @@ export async function fetchDashboardData(token) {
|
|
|
166
176
|
catch (error) {
|
|
167
177
|
warn(MODULE, `Failed to store merged PRs: ${errorMessage(error)}`);
|
|
168
178
|
}
|
|
169
|
-
//
|
|
179
|
+
// Store new closed PRs incrementally (dedupes by URL)
|
|
180
|
+
try {
|
|
181
|
+
stateManager.addClosedPRs(newClosedPRs);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
warn(MODULE, `Failed to store closed PRs: ${errorMessage(error)}`);
|
|
185
|
+
}
|
|
186
|
+
// Convert stored PRs to full types (derive repo/number from URL)
|
|
170
187
|
const allMergedPRs = storedToMergedPRs(stateManager.getMergedPRs());
|
|
188
|
+
const allClosedPRs = storedToClosedPRs(stateManager.getClosedPRs());
|
|
171
189
|
// Store monthly chart data (opened/merged/closed) so charts have data
|
|
172
190
|
const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
|
|
173
191
|
const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
|
|
@@ -188,7 +206,7 @@ export async function fetchDashboardData(token) {
|
|
|
188
206
|
warn(MODULE, `Failed to save dashboard digest to state: ${errorMessage(error)}`);
|
|
189
207
|
}
|
|
190
208
|
warn(MODULE, `Refreshed: ${prs.length} PRs fetched`);
|
|
191
|
-
return { digest, commentedIssues, allMergedPRs };
|
|
209
|
+
return { digest, commentedIssues, allMergedPRs, allClosedPRs };
|
|
192
210
|
}
|
|
193
211
|
/**
|
|
194
212
|
* Convert StoredMergedPR[] to MergedPR[] by deriving repo and number from URL.
|
|
@@ -216,6 +234,32 @@ export function storedToMergedPRs(stored) {
|
|
|
216
234
|
}
|
|
217
235
|
return results;
|
|
218
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Convert StoredClosedPR[] to ClosedPR[] by deriving repo and number from URL.
|
|
239
|
+
* Skips entries with unparseable URLs.
|
|
240
|
+
*/
|
|
241
|
+
export function storedToClosedPRs(stored) {
|
|
242
|
+
const results = [];
|
|
243
|
+
let skipped = 0;
|
|
244
|
+
for (const pr of stored) {
|
|
245
|
+
const parsed = parseGitHubUrl(pr.url);
|
|
246
|
+
if (!parsed) {
|
|
247
|
+
skipped++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
results.push({
|
|
251
|
+
url: pr.url,
|
|
252
|
+
repo: `${parsed.owner}/${parsed.repo}`,
|
|
253
|
+
number: parsed.number,
|
|
254
|
+
title: pr.title,
|
|
255
|
+
closedAt: pr.closedAt,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (skipped > 0) {
|
|
259
|
+
warn(MODULE, `Skipped ${skipped} stored closed PR(s) with unparseable URLs`);
|
|
260
|
+
}
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
219
263
|
/**
|
|
220
264
|
* Compute PRs grouped by repository from a digest and state.
|
|
221
265
|
* Used for chart data in the dashboard API.
|
|
@@ -102,7 +102,7 @@ export async function launchDashboardServer(options) {
|
|
|
102
102
|
if (info) {
|
|
103
103
|
// PID file appeared — verify the server is responding
|
|
104
104
|
if (await isDashboardServerRunning(info.port)) {
|
|
105
|
-
return { url: `http://localhost:${info.port}`, port: info.port, alreadyRunning: false };
|
|
105
|
+
return { url: `http://oss.localhost:${info.port}`, port: info.port, alreadyRunning: false };
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
}
|
|
@@ -85,7 +85,7 @@ export async function findRunningDashboardServer() {
|
|
|
85
85
|
}
|
|
86
86
|
// Process exists — verify it's actually our server via HTTP probe
|
|
87
87
|
if (await isDashboardServerRunning(info.port)) {
|
|
88
|
-
return { port: info.port, url: `http://localhost:${info.port}` };
|
|
88
|
+
return { port: info.port, url: `http://oss.localhost:${info.port}` };
|
|
89
89
|
}
|
|
90
90
|
// Process exists but not responding on expected port — stale
|
|
91
91
|
removeDashboardServerInfo();
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
import * as http from 'http';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js';
|
|
11
|
+
import { getStateManager, getGitHubToken, getCLIVersion, applyStatusOverrides } from '../core/index.js';
|
|
12
12
|
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
13
13
|
import { warn } from '../core/logger.js';
|
|
14
14
|
import { validateUrl, validateGitHubUrl, PR_URL_PATTERN } from './validation.js';
|
|
15
|
-
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, storedToMergedPRs, } from './dashboard-data.js';
|
|
15
|
+
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, storedToMergedPRs, storedToClosedPRs, } from './dashboard-data.js';
|
|
16
16
|
import { openInBrowser } from './startup.js';
|
|
17
17
|
import { writeDashboardServerInfo, removeDashboardServerInfo } from './dashboard-process.js';
|
|
18
18
|
import { RateLimiter } from './rate-limiter.js';
|
|
@@ -33,57 +33,24 @@ const MIME_TYPES = {
|
|
|
33
33
|
'.png': 'image/png',
|
|
34
34
|
'.ico': 'image/x-icon',
|
|
35
35
|
};
|
|
36
|
-
/**
|
|
37
|
-
* Apply status overrides from state to the PR list.
|
|
38
|
-
* Overrides are auto-cleared if the PR has new activity since the override was set.
|
|
39
|
-
*/
|
|
40
|
-
function applyStatusOverrides(prs, state) {
|
|
41
|
-
const overrides = state.config.statusOverrides;
|
|
42
|
-
if (!overrides || Object.keys(overrides).length === 0)
|
|
43
|
-
return prs;
|
|
44
|
-
const stateManager = getStateManager();
|
|
45
|
-
// Snapshot keys before iteration — clearStatusOverride mutates the same object
|
|
46
|
-
const overrideUrls = new Set(Object.keys(overrides));
|
|
47
|
-
let didAutoClear = false;
|
|
48
|
-
const result = prs.map((pr) => {
|
|
49
|
-
const override = stateManager.getStatusOverride(pr.url, pr.updatedAt);
|
|
50
|
-
if (!override) {
|
|
51
|
-
if (overrideUrls.has(pr.url))
|
|
52
|
-
didAutoClear = true;
|
|
53
|
-
return pr;
|
|
54
|
-
}
|
|
55
|
-
if (override.status === pr.status)
|
|
56
|
-
return pr;
|
|
57
|
-
return { ...pr, status: override.status };
|
|
58
|
-
});
|
|
59
|
-
// Persist any auto-cleared overrides so they don't resurrect on restart
|
|
60
|
-
if (didAutoClear) {
|
|
61
|
-
try {
|
|
62
|
-
stateManager.save();
|
|
63
|
-
}
|
|
64
|
-
catch (err) {
|
|
65
|
-
warn(MODULE, `Failed to persist auto-cleared overrides — they may reappear on restart: ${errorMessage(err)}`);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
36
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
71
37
|
/**
|
|
72
38
|
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
73
39
|
*/
|
|
74
|
-
function buildDashboardJson(digest, state, commentedIssues, allMergedPRs) {
|
|
40
|
+
function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClosedPRs) {
|
|
75
41
|
const prsByRepo = computePRsByRepo(digest, state);
|
|
76
42
|
const topRepos = computeTopRepos(prsByRepo);
|
|
77
43
|
const { monthlyMerged, monthlyOpened, monthlyClosed } = getMonthlyData(state);
|
|
78
|
-
// Derive
|
|
44
|
+
// Derive from state if not provided (e.g. initial load from cached state)
|
|
79
45
|
const mergedPRs = allMergedPRs ?? storedToMergedPRs(getStateManager().getMergedPRs());
|
|
80
|
-
|
|
46
|
+
const closedPRs = allClosedPRs ?? storedToClosedPRs(getStateManager().getClosedPRs());
|
|
47
|
+
// Filter out PRs from repos below the minStars threshold
|
|
81
48
|
const minStars = state.config.minStars ?? 50;
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const stats = buildDashboardStats(digest, state, filteredMergedPRs.length);
|
|
49
|
+
const repoScores = state.repoScores || {};
|
|
50
|
+
const isAboveMinStars = (pr) => !isBelowMinStars(repoScores[pr.repo]?.stargazersCount, minStars);
|
|
51
|
+
const filteredMergedPRs = mergedPRs.filter(isAboveMinStars);
|
|
52
|
+
const filteredClosedPRs = closedPRs.filter(isAboveMinStars);
|
|
53
|
+
const stats = buildDashboardStats(digest, state, filteredMergedPRs.length, filteredClosedPRs.length);
|
|
87
54
|
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
|
|
88
55
|
return {
|
|
89
56
|
stats,
|
|
@@ -100,6 +67,7 @@ function buildDashboardJson(digest, state, commentedIssues, allMergedPRs) {
|
|
|
100
67
|
commentedIssues,
|
|
101
68
|
issueResponses,
|
|
102
69
|
allMergedPRs: filteredMergedPRs,
|
|
70
|
+
allClosedPRs: filteredClosedPRs,
|
|
103
71
|
};
|
|
104
72
|
}
|
|
105
73
|
/**
|
|
@@ -339,7 +307,7 @@ export async function startDashboardServer(options) {
|
|
|
339
307
|
const result = await fetchDashboardData(currentToken);
|
|
340
308
|
cachedDigest = result.digest;
|
|
341
309
|
cachedCommentedIssues = result.commentedIssues;
|
|
342
|
-
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs);
|
|
310
|
+
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs);
|
|
343
311
|
sendJson(res, 200, cachedJsonData);
|
|
344
312
|
}
|
|
345
313
|
catch (error) {
|
|
@@ -442,8 +410,8 @@ export async function startDashboardServer(options) {
|
|
|
442
410
|
startedAt: new Date().toISOString(),
|
|
443
411
|
version: getCLIVersion(),
|
|
444
412
|
});
|
|
445
|
-
const serverUrl = `http://localhost:${actualPort}`;
|
|
446
|
-
warn(MODULE, `Dashboard server running at ${serverUrl} (also: http://
|
|
413
|
+
const serverUrl = `http://oss.localhost:${actualPort}`;
|
|
414
|
+
warn(MODULE, `Dashboard server running at ${serverUrl} (also: http://localhost:${actualPort})`);
|
|
447
415
|
// ── Background refresh ─────────────────────────────────────────────────
|
|
448
416
|
// Port is bound and PID file written — now fetch fresh data from GitHub
|
|
449
417
|
// so subsequent /api/data requests get live data instead of cached state.
|
|
@@ -452,7 +420,7 @@ export async function startDashboardServer(options) {
|
|
|
452
420
|
.then((result) => {
|
|
453
421
|
cachedDigest = result.digest;
|
|
454
422
|
cachedCommentedIssues = result.commentedIssues;
|
|
455
|
-
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs);
|
|
423
|
+
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs);
|
|
456
424
|
warn(MODULE, 'Background data refresh complete');
|
|
457
425
|
})
|
|
458
426
|
.catch((error) => {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - formatActionHint — human-readable maintainer action hint label
|
|
11
11
|
* - formatBriefSummary / formatSummary / printDigest — rendering
|
|
12
12
|
*/
|
|
13
|
-
import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, DailyDigest, ShelvedPRRef, MaintainerActionHint, ComputedRepoSignals, RepoGroup, CommentedIssue, CommentedIssueWithResponse } from './types.js';
|
|
13
|
+
import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, DailyDigest, AgentState, ShelvedPRRef, MaintainerActionHint, ComputedRepoSignals, RepoGroup, CommentedIssue, CommentedIssueWithResponse } from './types.js';
|
|
14
14
|
import type { CapacityAssessment, ActionableIssue, ActionMenu } from '../formatters/json.js';
|
|
15
15
|
/**
|
|
16
16
|
* Statuses indicating action needed from the contributor.
|
|
@@ -25,6 +25,7 @@ export declare const CRITICAL_STATUSES: ReadonlySet<FetchedPRStatus>;
|
|
|
25
25
|
export declare const CRITICAL_ACTION_REASONS: ReadonlySet<ActionReason>;
|
|
26
26
|
/** Staleness tiers indicating staleness — maintainer comments during these tiers don't count as responsive. */
|
|
27
27
|
export declare const STALE_STATUSES: ReadonlySet<StalenessTier>;
|
|
28
|
+
export declare function applyStatusOverrides(prs: FetchedPR[], state: Readonly<AgentState>): FetchedPR[];
|
|
28
29
|
/**
|
|
29
30
|
* Map a full FetchedPR to a lightweight ShelvedPRRef for digest output.
|
|
30
31
|
* Only the fields needed for display are retained, reducing JSON payload size.
|
|
@@ -53,7 +54,7 @@ export declare function assessCapacity(activePRs: FetchedPR[], maxActivePRs: num
|
|
|
53
54
|
* Note: Recently closed PRs are informational only and excluded from this list.
|
|
54
55
|
* They are available separately in digest.recentlyClosedPRs (#156).
|
|
55
56
|
*/
|
|
56
|
-
export declare function collectActionableIssues(prs: FetchedPR[], snoozedUrls?: Set<string
|
|
57
|
+
export declare function collectActionableIssues(prs: FetchedPR[], snoozedUrls?: Set<string>, lastDigestAt?: string): ActionableIssue[];
|
|
57
58
|
/**
|
|
58
59
|
* Format a maintainer action hint as a human-readable label
|
|
59
60
|
*/
|
package/dist/core/daily-logic.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { formatRelativeTime } from './utils.js';
|
|
14
14
|
import { warn } from './logger.js';
|
|
15
|
+
import { errorMessage } from './errors.js';
|
|
16
|
+
import { getStateManager } from './state.js';
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
// Constants
|
|
17
19
|
// ---------------------------------------------------------------------------
|
|
@@ -34,6 +36,62 @@ export const CRITICAL_ACTION_REASONS = new Set([
|
|
|
34
36
|
/** Staleness tiers indicating staleness — maintainer comments during these tiers don't count as responsive. */
|
|
35
37
|
export const STALE_STATUSES = new Set(['dormant', 'approaching_dormant']);
|
|
36
38
|
// ---------------------------------------------------------------------------
|
|
39
|
+
// Status overrides
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/**
|
|
42
|
+
* Apply status overrides from state to the PR list.
|
|
43
|
+
* Overrides are auto-cleared if the PR has new activity since the override was set.
|
|
44
|
+
*
|
|
45
|
+
* When an override changes the status, the contradictory reason field is cleared
|
|
46
|
+
* and an appropriate default is set so downstream logic (assessCapacity, collectActionableIssues)
|
|
47
|
+
* works correctly.
|
|
48
|
+
*/
|
|
49
|
+
const VALID_OVERRIDE_STATUSES = new Set(['needs_addressing', 'waiting_on_maintainer']);
|
|
50
|
+
export function applyStatusOverrides(prs, state) {
|
|
51
|
+
const overrides = state.config.statusOverrides;
|
|
52
|
+
if (!overrides || Object.keys(overrides).length === 0)
|
|
53
|
+
return prs;
|
|
54
|
+
const stateManager = getStateManager();
|
|
55
|
+
// Snapshot keys before iteration — clearStatusOverride mutates the same object
|
|
56
|
+
const overrideUrls = new Set(Object.keys(overrides));
|
|
57
|
+
let didAutoClear = false;
|
|
58
|
+
const result = prs.map((pr) => {
|
|
59
|
+
try {
|
|
60
|
+
const override = stateManager.getStatusOverride(pr.url, pr.updatedAt);
|
|
61
|
+
if (!override) {
|
|
62
|
+
if (overrideUrls.has(pr.url))
|
|
63
|
+
didAutoClear = true;
|
|
64
|
+
return pr;
|
|
65
|
+
}
|
|
66
|
+
if (!VALID_OVERRIDE_STATUSES.has(override.status)) {
|
|
67
|
+
warn('daily-logic', `Invalid override status "${override.status}" for ${pr.url} — ignoring`);
|
|
68
|
+
return pr;
|
|
69
|
+
}
|
|
70
|
+
if (override.status === pr.status)
|
|
71
|
+
return pr;
|
|
72
|
+
// Clear the contradictory reason field and set an appropriate default
|
|
73
|
+
if (override.status === 'waiting_on_maintainer') {
|
|
74
|
+
return { ...pr, status: override.status, actionReason: undefined, waitReason: 'pending_review' };
|
|
75
|
+
}
|
|
76
|
+
return { ...pr, status: override.status, waitReason: undefined, actionReason: 'needs_response' };
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
warn('daily-logic', `Failed to apply status override for ${pr.url}: ${errorMessage(err)}`);
|
|
80
|
+
return pr;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// Persist any auto-cleared overrides so they don't resurrect on restart
|
|
84
|
+
if (didAutoClear) {
|
|
85
|
+
try {
|
|
86
|
+
stateManager.save();
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
warn('daily-logic', `Failed to persist auto-cleared overrides — they may reappear on restart: ${errorMessage(err)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
37
95
|
// Internal helpers
|
|
38
96
|
// ---------------------------------------------------------------------------
|
|
39
97
|
/**
|
|
@@ -140,9 +198,10 @@ export function assessCapacity(activePRs, maxActivePRs, shelvedPRCount) {
|
|
|
140
198
|
* Note: Recently closed PRs are informational only and excluded from this list.
|
|
141
199
|
* They are available separately in digest.recentlyClosedPRs (#156).
|
|
142
200
|
*/
|
|
143
|
-
export function collectActionableIssues(prs, snoozedUrls = new Set()) {
|
|
201
|
+
export function collectActionableIssues(prs, snoozedUrls = new Set(), lastDigestAt) {
|
|
144
202
|
const issues = [];
|
|
145
203
|
const actionPRs = prs.filter((pr) => pr.status === 'needs_addressing');
|
|
204
|
+
const lastDigestTime = lastDigestAt ? new Date(lastDigestAt).getTime() : NaN;
|
|
146
205
|
const reasonOrder = [
|
|
147
206
|
'needs_response',
|
|
148
207
|
'needs_changes',
|
|
@@ -191,7 +250,18 @@ export function collectActionableIssues(prs, snoozedUrls = new Set()) {
|
|
|
191
250
|
label = `[${reason}]`;
|
|
192
251
|
type = 'needs_response';
|
|
193
252
|
}
|
|
194
|
-
|
|
253
|
+
// A PR is "new" if it was created after the last daily digest (first time seen).
|
|
254
|
+
// If there's no previous digest (first run) or createdAt is invalid, assume new.
|
|
255
|
+
const createdTime = new Date(pr.createdAt).getTime();
|
|
256
|
+
let isNewContribution;
|
|
257
|
+
if (isNaN(createdTime)) {
|
|
258
|
+
warn('daily-logic', `Invalid createdAt "${pr.createdAt}" for PR ${pr.url}, assuming new contribution`);
|
|
259
|
+
isNewContribution = true;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
isNewContribution = isNaN(lastDigestTime) || createdTime > lastDigestTime;
|
|
263
|
+
}
|
|
264
|
+
issues.push({ type, pr, label, isNewContribution });
|
|
195
265
|
}
|
|
196
266
|
}
|
|
197
267
|
return issues;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Extracted from PRMonitor to isolate statistics-gathering API calls (#263).
|
|
4
4
|
*/
|
|
5
5
|
import { Octokit } from '@octokit/rest';
|
|
6
|
-
import { ClosedPR, MergedPR, StoredMergedPR, type StarFilter } from './types.js';
|
|
6
|
+
import { ClosedPR, MergedPR, StoredMergedPR, StoredClosedPR, type StarFilter } from './types.js';
|
|
7
7
|
/** TTL for cached PR count results (24 hours — these stats change slowly). */
|
|
8
8
|
export declare const PR_COUNTS_CACHE_TTL_MS: number;
|
|
9
9
|
/** Return type shared by both merged and closed PR count functions. */
|
|
@@ -50,3 +50,12 @@ export declare function fetchRecentlyMergedPRs(octokit: Octokit, config: {
|
|
|
50
50
|
export declare function fetchMergedPRsSince(octokit: Octokit, config: {
|
|
51
51
|
githubUsername: string;
|
|
52
52
|
}, since?: string): Promise<StoredMergedPR[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Fetch closed-without-merge PRs since a watermark date for incremental storage.
|
|
55
|
+
* If no watermark is provided (first-ever fetch), fetches all closed PRs (up to pagination cap).
|
|
56
|
+
* Returns StoredClosedPR[] (minimal: url, title, closedAt) for state persistence.
|
|
57
|
+
* Uses `is:unmerged` to exclude merged PRs (which are also "closed" in GitHub's model).
|
|
58
|
+
*/
|
|
59
|
+
export declare function fetchClosedPRsSince(octokit: Octokit, config: {
|
|
60
|
+
githubUsername: string;
|
|
61
|
+
}, since?: string): Promise<StoredClosedPR[]>;
|
|
@@ -287,3 +287,61 @@ export async function fetchMergedPRsSince(octokit, config, since) {
|
|
|
287
287
|
debug(MODULE, `Fetched ${results.length} merged PRs${since ? ' (incremental)' : ' (initial)'}`);
|
|
288
288
|
return results;
|
|
289
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Fetch closed-without-merge PRs since a watermark date for incremental storage.
|
|
292
|
+
* If no watermark is provided (first-ever fetch), fetches all closed PRs (up to pagination cap).
|
|
293
|
+
* Returns StoredClosedPR[] (minimal: url, title, closedAt) for state persistence.
|
|
294
|
+
* Uses `is:unmerged` to exclude merged PRs (which are also "closed" in GitHub's model).
|
|
295
|
+
*/
|
|
296
|
+
export async function fetchClosedPRsSince(octokit, config, since) {
|
|
297
|
+
if (!config.githubUsername) {
|
|
298
|
+
warn(MODULE, 'Skipping closed PRs fetch: no githubUsername configured.');
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
const dateFilter = since ? ` closed:>${since}` : '';
|
|
302
|
+
const q = `is:pr is:closed is:unmerged author:${config.githubUsername} -user:${config.githubUsername}${dateFilter}`;
|
|
303
|
+
debug(MODULE, `Fetching closed PRs${since ? ` since ${since}` : ' (all time)'}...`);
|
|
304
|
+
const results = [];
|
|
305
|
+
let page = 1;
|
|
306
|
+
let fetched = 0;
|
|
307
|
+
let totalCount;
|
|
308
|
+
while (true) {
|
|
309
|
+
const { data } = await octokit.search.issuesAndPullRequests({
|
|
310
|
+
q,
|
|
311
|
+
sort: 'updated',
|
|
312
|
+
order: 'desc',
|
|
313
|
+
per_page: 100,
|
|
314
|
+
page,
|
|
315
|
+
});
|
|
316
|
+
totalCount = data.total_count;
|
|
317
|
+
for (const item of data.items) {
|
|
318
|
+
const parsed = parseGitHubUrl(item.html_url);
|
|
319
|
+
if (!parsed) {
|
|
320
|
+
warn(MODULE, `Skipping closed PR with unparseable URL: ${item.html_url}`);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (isOwnRepo(parsed.owner, config.githubUsername))
|
|
324
|
+
continue;
|
|
325
|
+
const closedAt = item.closed_at || '';
|
|
326
|
+
if (!closedAt) {
|
|
327
|
+
warn(MODULE, `Skipping closed PR with no close date: ${item.html_url}`);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
results.push({
|
|
331
|
+
url: item.html_url,
|
|
332
|
+
title: item.title,
|
|
333
|
+
closedAt,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
fetched += data.items.length;
|
|
337
|
+
if (fetched >= totalCount || fetched >= 1000 || data.items.length === 0 || page >= MAX_PAGINATION_PAGES) {
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
page++;
|
|
341
|
+
}
|
|
342
|
+
if (fetched < totalCount && page >= MAX_PAGINATION_PAGES) {
|
|
343
|
+
warn(MODULE, `Pagination capped at ${MAX_PAGINATION_PAGES} pages: fetched ${fetched} of ${totalCount} closed PRs. Oldest PRs may be missing.`);
|
|
344
|
+
}
|
|
345
|
+
debug(MODULE, `Fetched ${results.length} closed PRs${since ? ' (incremental)' : ' (initial)'}`);
|
|
346
|
+
return results;
|
|
347
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDa
|
|
|
12
12
|
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
|
|
13
13
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
14
14
|
export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
|
|
15
|
-
export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
15
|
+
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
16
16
|
export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
|
|
17
17
|
export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
|
|
18
18
|
export * from './types.js';
|
package/dist/core/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDa
|
|
|
12
12
|
export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
|
|
13
13
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
14
14
|
export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
|
|
15
|
-
export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
15
|
+
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
16
16
|
export { computeContributionStats } from './stats.js';
|
|
17
17
|
export { fetchPRTemplate } from './pr-template.js';
|
|
18
18
|
export * from './types.js';
|