@oss-autopilot/core 0.54.0 → 0.55.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 +63 -63
- package/dist/commands/comments.js +0 -1
- package/dist/commands/config.js +45 -5
- package/dist/commands/daily.js +190 -157
- package/dist/commands/dashboard-data.js +37 -30
- package/dist/commands/dashboard-server.js +0 -1
- package/dist/commands/dismiss.js +0 -6
- package/dist/commands/init.js +0 -1
- package/dist/commands/local-repos.js +1 -2
- package/dist/commands/move.js +12 -11
- package/dist/commands/setup.d.ts +2 -1
- package/dist/commands/setup.js +166 -130
- package/dist/commands/shelve.js +10 -10
- package/dist/commands/startup.js +30 -14
- package/dist/core/ci-analysis.d.ts +6 -0
- package/dist/core/ci-analysis.js +89 -12
- package/dist/core/daily-logic.js +24 -33
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -1
- package/dist/core/issue-discovery.d.ts +7 -44
- package/dist/core/issue-discovery.js +83 -188
- package/dist/core/issue-eligibility.d.ts +35 -0
- package/dist/core/issue-eligibility.js +126 -0
- package/dist/core/issue-vetting.d.ts +6 -21
- package/dist/core/issue-vetting.js +15 -279
- package/dist/core/pr-monitor.d.ts +7 -12
- package/dist/core/pr-monitor.js +14 -80
- package/dist/core/repo-health.d.ts +24 -0
- package/dist/core/repo-health.js +193 -0
- package/dist/core/search-phases.d.ts +55 -0
- package/dist/core/search-phases.js +155 -0
- package/dist/core/state.d.ts +11 -0
- package/dist/core/state.js +63 -4
- package/dist/core/types.d.ts +8 -1
- package/dist/core/types.js +7 -0
- package/dist/formatters/json.d.ts +1 -1
- package/package.json +1 -1
|
@@ -157,7 +157,6 @@ export async function runClaim(options) {
|
|
|
157
157
|
updatedAt: new Date().toISOString(),
|
|
158
158
|
vetted: false,
|
|
159
159
|
});
|
|
160
|
-
stateManager.save();
|
|
161
160
|
}
|
|
162
161
|
catch (error) {
|
|
163
162
|
console.error(`Warning: Comment posted on ${options.issueUrl} but failed to save to local state: ${error instanceof Error ? error.message : error}`);
|
package/dist/commands/config.js
CHANGED
|
@@ -3,7 +3,14 @@
|
|
|
3
3
|
* Shows or updates configuration
|
|
4
4
|
*/
|
|
5
5
|
import { getStateManager } from '../core/index.js';
|
|
6
|
+
import { ISSUE_SCOPES } from '../core/types.js';
|
|
6
7
|
import { validateGitHubUsername } from './validation.js';
|
|
8
|
+
function validateScope(value) {
|
|
9
|
+
if (!ISSUE_SCOPES.includes(value)) {
|
|
10
|
+
throw new Error(`Invalid scope "${value}". Valid scopes: ${ISSUE_SCOPES.join(', ')}`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
7
14
|
export async function runConfig(options) {
|
|
8
15
|
const stateManager = getStateManager();
|
|
9
16
|
const currentConfig = stateManager.getState().config;
|
|
@@ -30,6 +37,33 @@ export async function runConfig(options) {
|
|
|
30
37
|
stateManager.updateConfig({ labels: [...currentConfig.labels, value] });
|
|
31
38
|
}
|
|
32
39
|
break;
|
|
40
|
+
case 'remove-label':
|
|
41
|
+
if (!currentConfig.labels.includes(value)) {
|
|
42
|
+
throw new Error(`Label "${value}" is not currently configured. Current labels: ${currentConfig.labels.join(', ')}`);
|
|
43
|
+
}
|
|
44
|
+
stateManager.updateConfig({ labels: currentConfig.labels.filter((l) => l !== value) });
|
|
45
|
+
break;
|
|
46
|
+
case 'add-scope': {
|
|
47
|
+
const scope = validateScope(value);
|
|
48
|
+
const currentScopes = currentConfig.scope ?? [];
|
|
49
|
+
if (!currentScopes.includes(scope)) {
|
|
50
|
+
stateManager.updateConfig({ scope: [...currentScopes, scope] });
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
case 'remove-scope': {
|
|
55
|
+
const scope = validateScope(value);
|
|
56
|
+
const existingScopes = currentConfig.scope ?? [];
|
|
57
|
+
if (!existingScopes.includes(scope)) {
|
|
58
|
+
throw new Error(`Scope "${value}" is not currently set`);
|
|
59
|
+
}
|
|
60
|
+
const filtered = existingScopes.filter((s) => s !== scope);
|
|
61
|
+
if (filtered.length === 0) {
|
|
62
|
+
throw new Error('Cannot remove the last scope. Use setup to clear scopes entirely.');
|
|
63
|
+
}
|
|
64
|
+
stateManager.updateConfig({ scope: filtered });
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
33
67
|
case 'exclude-repo': {
|
|
34
68
|
const parts = value.split('/');
|
|
35
69
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
@@ -37,8 +71,10 @@ export async function runConfig(options) {
|
|
|
37
71
|
}
|
|
38
72
|
const valueLower = value.toLowerCase();
|
|
39
73
|
if (!currentConfig.excludeRepos.some((r) => r.toLowerCase() === valueLower)) {
|
|
40
|
-
stateManager.
|
|
41
|
-
|
|
74
|
+
stateManager.batch(() => {
|
|
75
|
+
stateManager.updateConfig({ excludeRepos: [...currentConfig.excludeRepos, value] });
|
|
76
|
+
stateManager.cleanupExcludedData([value], []);
|
|
77
|
+
});
|
|
42
78
|
}
|
|
43
79
|
break;
|
|
44
80
|
}
|
|
@@ -48,14 +84,18 @@ export async function runConfig(options) {
|
|
|
48
84
|
}
|
|
49
85
|
const currentOrgs = currentConfig.excludeOrgs ?? [];
|
|
50
86
|
if (!currentOrgs.some((o) => o.toLowerCase() === value.toLowerCase())) {
|
|
51
|
-
stateManager.
|
|
52
|
-
|
|
87
|
+
stateManager.batch(() => {
|
|
88
|
+
stateManager.updateConfig({ excludeOrgs: [...currentOrgs, value] });
|
|
89
|
+
stateManager.cleanupExcludedData([], [value]);
|
|
90
|
+
});
|
|
53
91
|
}
|
|
54
92
|
break;
|
|
55
93
|
}
|
|
94
|
+
case 'issueListPath':
|
|
95
|
+
stateManager.updateConfig({ issueListPath: value || undefined });
|
|
96
|
+
break;
|
|
56
97
|
default:
|
|
57
98
|
throw new Error(`Unknown config key: ${options.key}`);
|
|
58
99
|
}
|
|
59
|
-
stateManager.save();
|
|
60
100
|
return { success: true, key: options.key, value };
|
|
61
101
|
}
|
package/dist/commands/daily.js
CHANGED
|
@@ -123,74 +123,78 @@ async function fetchPRData(prMonitor, token) {
|
|
|
123
123
|
*/
|
|
124
124
|
async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
125
125
|
const stateManager = getStateManager();
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
126
|
+
// Batch all synchronous score mutations for a single disk write.
|
|
127
|
+
// Per-repo try-catch: a single corrupted repo should not prevent updates to others.
|
|
128
|
+
// Outer try-catch: save failure should not crash the daily check (in-memory mutations still apply).
|
|
129
|
+
try {
|
|
130
|
+
stateManager.batch(() => {
|
|
131
|
+
// Reset stale repos first (so excluded/removed repos get zeroed).
|
|
132
|
+
// Guard: if the API returned zero results but we have existing repos with merged PRs,
|
|
133
|
+
// skip the reset to avoid wiping scores due to transient API failures.
|
|
134
|
+
const existingReposWithMerges = Object.values(stateManager.getState().repoScores).filter((s) => s.mergedPRCount > 0);
|
|
135
|
+
if (mergedCounts.size === 0 && existingReposWithMerges.length > 0) {
|
|
136
|
+
warn(MODULE, `Skipping stale repo reset: API returned 0 merged PR results but state has ${existingReposWithMerges.length} repo(s) with merges. Possible API issue.`);
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
138
|
+
else {
|
|
139
|
+
for (const score of Object.values(stateManager.getState().repoScores)) {
|
|
140
|
+
if (!mergedCounts.has(score.repo)) {
|
|
141
|
+
stateManager.updateRepoScore(score.repo, { mergedPRCount: 0 });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Update merged/closed counts
|
|
146
|
+
let mergedCountFailures = 0;
|
|
147
|
+
for (const [repo, { count, lastMergedAt }] of mergedCounts) {
|
|
148
|
+
try {
|
|
149
|
+
stateManager.updateRepoScore(repo, { mergedPRCount: count, lastMergedAt: lastMergedAt || undefined });
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
mergedCountFailures++;
|
|
153
|
+
warn(MODULE, `Failed to update merged count for ${repo}: ${errorMessage(error)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (mergedCountFailures === mergedCounts.size && mergedCounts.size > 0) {
|
|
157
|
+
warn(MODULE, `[ALL_MERGED_COUNT_UPDATES_FAILED] All ${mergedCounts.size} merged count update(s) failed.`);
|
|
158
|
+
}
|
|
159
|
+
// Populate closedWithoutMergeCount in repo scores.
|
|
160
|
+
const existingReposWithClosed = Object.values(stateManager.getState().repoScores).filter((s) => (s.closedWithoutMergeCount || 0) > 0);
|
|
161
|
+
if (closedCounts.size === 0 && existingReposWithClosed.length > 0) {
|
|
162
|
+
warn(MODULE, `API returned 0 closed PR results but state has ${existingReposWithClosed.length} repo(s) with closed PRs. Possible transient API issue.`);
|
|
163
|
+
}
|
|
164
|
+
let closedCountFailures = 0;
|
|
165
|
+
for (const [repo, count] of closedCounts) {
|
|
166
|
+
try {
|
|
167
|
+
stateManager.updateRepoScore(repo, { closedWithoutMergeCount: count });
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
closedCountFailures++;
|
|
171
|
+
warn(MODULE, `Failed to update closed count for ${repo}: ${errorMessage(error)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (closedCountFailures === closedCounts.size && closedCounts.size > 0) {
|
|
175
|
+
warn(MODULE, `[ALL_CLOSED_COUNT_UPDATES_FAILED] All ${closedCounts.size} closed count update(s) failed.`);
|
|
176
|
+
}
|
|
177
|
+
// Update repo signals from observed open PR data
|
|
178
|
+
const repoSignals = computeRepoSignals(prs);
|
|
179
|
+
let signalUpdateFailures = 0;
|
|
180
|
+
for (const [repo, signals] of repoSignals) {
|
|
181
|
+
try {
|
|
182
|
+
stateManager.updateRepoScore(repo, { signals });
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
signalUpdateFailures++;
|
|
186
|
+
warn(MODULE, `Failed to update signals for ${repo}: ${errorMessage(error)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (signalUpdateFailures === repoSignals.size && repoSignals.size > 0) {
|
|
190
|
+
warn(MODULE, `[ALL_SIGNAL_UPDATES_FAILED] All ${repoSignals.size} signal update(s) failed. This may indicate corrupted state.`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
189
193
|
}
|
|
190
|
-
|
|
191
|
-
warn(MODULE, `
|
|
194
|
+
catch (error) {
|
|
195
|
+
warn(MODULE, `Failed to persist repo score updates: ${errorMessage(error)}`);
|
|
192
196
|
}
|
|
193
|
-
// Fetch metadata (stars + language) for all scored repos
|
|
197
|
+
// Fetch metadata (stars + language) for all scored repos — async, so outside the batch above
|
|
194
198
|
const allRepos = Object.keys(stateManager.getState().repoScores);
|
|
195
199
|
let repoMetadata;
|
|
196
200
|
try {
|
|
@@ -203,32 +207,40 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
203
207
|
warn(MODULE, 'Repos without cached metadata will be excluded from dashboard stats and metadata badges until fetched on the next successful run.');
|
|
204
208
|
repoMetadata = new Map();
|
|
205
209
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
210
|
+
// Batch metadata + trust sync mutations for a single disk write
|
|
211
|
+
try {
|
|
212
|
+
stateManager.batch(() => {
|
|
213
|
+
let metadataUpdateFailures = 0;
|
|
214
|
+
for (const [repo, { stars, language }] of repoMetadata) {
|
|
215
|
+
try {
|
|
216
|
+
stateManager.updateRepoScore(repo, { stargazersCount: stars, language });
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
metadataUpdateFailures++;
|
|
220
|
+
warn(MODULE, `Failed to update metadata for ${repo}: ${errorMessage(error)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (metadataUpdateFailures === repoMetadata.size && repoMetadata.size > 0) {
|
|
224
|
+
warn(MODULE, `[ALL_METADATA_UPDATES_FAILED] All ${repoMetadata.size} metadata update(s) failed.`);
|
|
225
|
+
}
|
|
226
|
+
// Auto-sync trustedProjects from repos with merged PRs
|
|
227
|
+
let trustSyncFailures = 0;
|
|
228
|
+
for (const [repo] of mergedCounts) {
|
|
229
|
+
try {
|
|
230
|
+
stateManager.addTrustedProject(repo);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
trustSyncFailures++;
|
|
234
|
+
warn(MODULE, `Failed to sync trusted project ${repo}: ${errorMessage(error)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (trustSyncFailures === mergedCounts.size && mergedCounts.size > 0) {
|
|
238
|
+
warn(MODULE, `[ALL_TRUST_SYNCS_FAILED] All ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
229
241
|
}
|
|
230
|
-
|
|
231
|
-
warn(MODULE, `
|
|
242
|
+
catch (error) {
|
|
243
|
+
warn(MODULE, `Failed to persist metadata/trust updates: ${errorMessage(error)}`);
|
|
232
244
|
}
|
|
233
245
|
}
|
|
234
246
|
/**
|
|
@@ -246,38 +258,47 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
|
|
|
246
258
|
const shelvedPRs = [];
|
|
247
259
|
const autoUnshelvedPRs = [];
|
|
248
260
|
const activePRs = [];
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
261
|
+
// Wrap mutations in batch: unshelvePR calls + setLastDigest produce a single save.
|
|
262
|
+
// Outer try-catch: save failure should not crash the daily check (in-memory mutations still apply).
|
|
263
|
+
try {
|
|
264
|
+
stateManager.batch(() => {
|
|
265
|
+
for (const pr of overriddenPRs) {
|
|
266
|
+
if (stateManager.isPRShelved(pr.url)) {
|
|
267
|
+
if (CRITICAL_STATUSES.has(pr.status)) {
|
|
268
|
+
stateManager.unshelvePR(pr.url);
|
|
269
|
+
autoUnshelvedPRs.push(toShelvedPRRef(pr));
|
|
270
|
+
activePRs.push(pr);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
shelvedPRs.push(toShelvedPRRef(pr));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (pr.stalenessTier === 'dormant' && !CRITICAL_STATUSES.has(pr.status)) {
|
|
277
|
+
// Dormant PRs are auto-shelved unless they need addressing
|
|
278
|
+
// (e.g. maintainer commented on a stale PR — it should resurface)
|
|
279
|
+
shelvedPRs.push(toShelvedPRRef(pr));
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
activePRs.push(pr);
|
|
283
|
+
}
|
|
258
284
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
activePRs.
|
|
267
|
-
|
|
285
|
+
// Generate digest from override-applied PRs so status categories are correct.
|
|
286
|
+
// Note: digest.openPRs contains ALL fetched PRs (including shelved).
|
|
287
|
+
// We override summary fields below to reflect active-only counts.
|
|
288
|
+
const digest = prMonitor.generateDigest(overriddenPRs, recentlyClosedPRs, recentlyMergedPRs);
|
|
289
|
+
// Attach shelve info to digest
|
|
290
|
+
digest.shelvedPRs = shelvedPRs;
|
|
291
|
+
digest.autoUnshelvedPRs = autoUnshelvedPRs;
|
|
292
|
+
digest.summary.totalActivePRs = activePRs.length;
|
|
293
|
+
// Store digest in state so dashboard can render it
|
|
294
|
+
stateManager.setLastDigest(digest);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
warn(MODULE, `Failed to persist partition state: ${errorMessage(error)}`);
|
|
268
299
|
}
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
// We override summary fields below to reflect active-only counts.
|
|
272
|
-
const digest = prMonitor.generateDigest(overriddenPRs, recentlyClosedPRs, recentlyMergedPRs);
|
|
273
|
-
// Attach shelve info to digest
|
|
274
|
-
digest.shelvedPRs = shelvedPRs;
|
|
275
|
-
digest.autoUnshelvedPRs = autoUnshelvedPRs;
|
|
276
|
-
digest.summary.totalActivePRs = activePRs.length;
|
|
277
|
-
// Store digest in state so dashboard can render it
|
|
278
|
-
stateManager.setLastDigest(digest);
|
|
279
|
-
// Save state (updates lastRunAt, lastDigest, and any auto-unshelve changes)
|
|
280
|
-
stateManager.save();
|
|
300
|
+
// Digest was created inside batch — reconstruct from state
|
|
301
|
+
const digest = stateManager.getState().lastDigest;
|
|
281
302
|
return { activePRs, shelvedPRs, autoUnshelvedPRs, digest };
|
|
282
303
|
}
|
|
283
304
|
/**
|
|
@@ -289,43 +310,47 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
289
310
|
const stateManager = getStateManager();
|
|
290
311
|
// Assess capacity from active PRs only (shelved PRs excluded)
|
|
291
312
|
const capacity = assessCapacity(activePRs, stateManager.getState().config.maxActivePRs, shelvedPRs.length);
|
|
292
|
-
// Filter dismissed issues: suppress if dismissed after last response, resurface + auto-undismiss if new activity
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
// Filter dismissed issues: suppress if dismissed after last response, resurface + auto-undismiss if new activity.
|
|
314
|
+
// Batch: undismissIssue calls trigger autoSave — batch produces a single disk write for all auto-undismisses.
|
|
315
|
+
let filteredCommentedIssues = [];
|
|
316
|
+
try {
|
|
317
|
+
stateManager.batch(() => {
|
|
318
|
+
filteredCommentedIssues = commentedIssues.filter((issue) => {
|
|
319
|
+
const dismissedAt = stateManager.getIssueDismissedAt(issue.url);
|
|
320
|
+
if (!dismissedAt)
|
|
321
|
+
return true; // Not dismissed — include
|
|
322
|
+
if (issue.status === 'new_response') {
|
|
323
|
+
const responseTime = new Date(issue.lastResponseAt).getTime();
|
|
324
|
+
const dismissTime = new Date(dismissedAt).getTime();
|
|
325
|
+
if (isNaN(responseTime) || isNaN(dismissTime)) {
|
|
326
|
+
// Invalid timestamp — fail open (include issue to be safe) without
|
|
327
|
+
// permanently removing dismiss record (may be a transient data issue)
|
|
328
|
+
warn(MODULE, `Invalid timestamp in dismiss check for ${issue.url}, including issue`);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
if (responseTime > dismissTime) {
|
|
332
|
+
// New activity after dismiss — auto-undismiss and resurface
|
|
333
|
+
warn(MODULE, `Auto-undismissing issue ${issue.url}: new response at ${issue.lastResponseAt} after dismiss at ${dismissedAt}`);
|
|
334
|
+
try {
|
|
335
|
+
stateManager.undismissIssue(issue.url);
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
warn(MODULE, `Failed to persist auto-undismiss for ${issue.url}: ${errorMessage(error)}`);
|
|
339
|
+
}
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Still dismissed (last response is at or before dismiss timestamp)
|
|
344
|
+
return false;
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
warn(MODULE, `Failed to persist auto-undismiss state: ${errorMessage(error)}`);
|
|
350
|
+
}
|
|
318
351
|
const issueResponses = filteredCommentedIssues.filter((i) => i.status === 'new_response');
|
|
319
352
|
const summary = formatSummary(digest, capacity, issueResponses);
|
|
320
|
-
//
|
|
321
|
-
if (hasAutoUndismissed) {
|
|
322
|
-
try {
|
|
323
|
-
stateManager.save();
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
warn(MODULE, `Failed to persist auto-undismissed state: ${errorMessage(error)}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
353
|
+
// Auto-undismiss mutations are auto-saved by undismissIssue()
|
|
329
354
|
const actionableIssues = collectActionableIssues(activePRs, previousLastDigestAt);
|
|
330
355
|
digest.summary.totalNeedingAttention = actionableIssues.length;
|
|
331
356
|
const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
|
|
@@ -392,8 +417,16 @@ async function executeDailyCheckInternal(token) {
|
|
|
392
417
|
const { prs, failures, mergedCounts, closedCounts, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed, recentlyClosedPRs, recentlyMergedPRs, commentedIssues, } = await fetchPRData(prMonitor, token);
|
|
393
418
|
// Phase 2: Update repo scores (signals, star counts, trust sync)
|
|
394
419
|
await updateRepoScores(prMonitor, prs, mergedCounts, closedCounts);
|
|
395
|
-
// Phase 3: Persist monthly analytics
|
|
396
|
-
|
|
420
|
+
// Phase 3: Persist monthly analytics (batch the 3 monthly setter calls).
|
|
421
|
+
// try-catch: analytics are supplementary — save failure should not crash the daily check.
|
|
422
|
+
try {
|
|
423
|
+
getStateManager().batch(() => {
|
|
424
|
+
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
warn(MODULE, `Failed to persist monthly analytics: ${errorMessage(error)}`);
|
|
429
|
+
}
|
|
397
430
|
// Capture lastDigestAt BEFORE Phase 4 overwrites it with the current run's timestamp.
|
|
398
431
|
// Used by collectActionableIssues to determine which PRs are "new" (created since last digest).
|
|
399
432
|
const previousLastDigestAt = getStateManager().getState().lastDigestAt;
|
|
@@ -169,43 +169,50 @@ export async function fetchDashboardData(token) {
|
|
|
169
169
|
if (failures.length > 0) {
|
|
170
170
|
warn(MODULE, `${failures.length} PR fetch(es) failed`);
|
|
171
171
|
}
|
|
172
|
-
//
|
|
172
|
+
// Wrap all state mutations in a batch for a single disk write.
|
|
173
|
+
// try-catch: save errors should not crash the dashboard data fetch.
|
|
173
174
|
try {
|
|
174
|
-
stateManager.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
175
|
+
stateManager.batch(() => {
|
|
176
|
+
// Store new merged PRs incrementally (dedupes by URL)
|
|
177
|
+
try {
|
|
178
|
+
stateManager.addMergedPRs(newMergedPRs);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
warn(MODULE, `Failed to store merged PRs: ${errorMessage(error)}`);
|
|
182
|
+
}
|
|
183
|
+
// Store new closed PRs incrementally (dedupes by URL)
|
|
184
|
+
try {
|
|
185
|
+
stateManager.addClosedPRs(newClosedPRs);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
warn(MODULE, `Failed to store closed PRs: ${errorMessage(error)}`);
|
|
189
|
+
}
|
|
190
|
+
// Store monthly chart data (opened/merged/closed) so charts have data
|
|
191
|
+
const { monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
|
|
192
|
+
const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
|
|
193
|
+
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
194
|
+
const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
195
|
+
// Apply shelve partitioning for display (auto-unshelve only runs in daily check)
|
|
196
|
+
// Dormant PRs are treated as shelved unless they need addressing
|
|
197
|
+
const shelvedUrls = new Set(stateManager.getState().config.shelvedPRUrls || []);
|
|
198
|
+
const freshShelved = prs.filter((pr) => shelvedUrls.has(pr.url) || (pr.stalenessTier === 'dormant' && pr.status !== 'needs_addressing'));
|
|
199
|
+
digest.shelvedPRs = freshShelved.map(toShelvedPRRef);
|
|
200
|
+
digest.autoUnshelvedPRs = [];
|
|
201
|
+
digest.summary.totalActivePRs = prs.length - freshShelved.length;
|
|
202
|
+
stateManager.setLastDigest(digest);
|
|
203
|
+
});
|
|
182
204
|
}
|
|
183
205
|
catch (error) {
|
|
184
|
-
warn(MODULE, `Failed to
|
|
206
|
+
warn(MODULE, `Failed to persist dashboard state: ${errorMessage(error)}`);
|
|
185
207
|
}
|
|
186
|
-
|
|
208
|
+
warn(MODULE, `Refreshed: ${prs.length} PRs fetched`);
|
|
209
|
+
// Convert stored PRs to full types (derive repo/number from URL) — read-only, outside batch
|
|
187
210
|
const allMergedPRs = storedToMergedPRs(stateManager.getMergedPRs());
|
|
188
211
|
const allClosedPRs = storedToClosedPRs(stateManager.getClosedPRs());
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
193
|
-
const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
194
|
-
// Apply shelve partitioning for display (auto-unshelve only runs in daily check)
|
|
195
|
-
// Dormant PRs are treated as shelved unless they need addressing
|
|
196
|
-
const shelvedUrls = new Set(stateManager.getState().config.shelvedPRUrls || []);
|
|
197
|
-
const freshShelved = prs.filter((pr) => shelvedUrls.has(pr.url) || (pr.stalenessTier === 'dormant' && pr.status !== 'needs_addressing'));
|
|
198
|
-
digest.shelvedPRs = freshShelved.map(toShelvedPRRef);
|
|
199
|
-
digest.autoUnshelvedPRs = [];
|
|
200
|
-
digest.summary.totalActivePRs = prs.length - freshShelved.length;
|
|
201
|
-
stateManager.setLastDigest(digest);
|
|
202
|
-
try {
|
|
203
|
-
stateManager.save();
|
|
212
|
+
const digest = stateManager.getState().lastDigest;
|
|
213
|
+
if (!digest) {
|
|
214
|
+
throw new Error('Dashboard data fetch failed: digest was not generated');
|
|
204
215
|
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
warn(MODULE, `Failed to save dashboard digest to state: ${errorMessage(error)}`);
|
|
207
|
-
}
|
|
208
|
-
warn(MODULE, `Refreshed: ${prs.length} PRs fetched`);
|
|
209
216
|
return { digest, commentedIssues, allMergedPRs, allClosedPRs };
|
|
210
217
|
}
|
|
211
218
|
/**
|
package/dist/commands/dismiss.js
CHANGED
|
@@ -10,9 +10,6 @@ export async function runDismiss(options) {
|
|
|
10
10
|
validateGitHubUrl(options.url, ISSUE_URL_PATTERN, 'issue');
|
|
11
11
|
const stateManager = getStateManager();
|
|
12
12
|
const added = stateManager.dismissIssue(options.url, new Date().toISOString());
|
|
13
|
-
if (added) {
|
|
14
|
-
stateManager.save();
|
|
15
|
-
}
|
|
16
13
|
return { dismissed: added, url: options.url };
|
|
17
14
|
}
|
|
18
15
|
export async function runUndismiss(options) {
|
|
@@ -20,8 +17,5 @@ export async function runUndismiss(options) {
|
|
|
20
17
|
validateGitHubUrl(options.url, ISSUE_URL_PATTERN, 'issue');
|
|
21
18
|
const stateManager = getStateManager();
|
|
22
19
|
const removed = stateManager.undismissIssue(options.url);
|
|
23
|
-
if (removed) {
|
|
24
|
-
stateManager.save();
|
|
25
|
-
}
|
|
26
20
|
return { undismissed: removed, url: options.url };
|
|
27
21
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -9,7 +9,6 @@ export async function runInit(options) {
|
|
|
9
9
|
const stateManager = getStateManager();
|
|
10
10
|
// Set username in config
|
|
11
11
|
stateManager.updateConfig({ githubUsername: options.username });
|
|
12
|
-
stateManager.save();
|
|
13
12
|
return {
|
|
14
13
|
username: options.username,
|
|
15
14
|
message: 'Username saved. Run `daily` to fetch your open PRs from GitHub.',
|
|
@@ -113,11 +113,10 @@ export async function runLocalRepos(options) {
|
|
|
113
113
|
const cachedAt = new Date().toISOString();
|
|
114
114
|
try {
|
|
115
115
|
stateManager.setLocalRepoCache({ repos, scanPaths, cachedAt });
|
|
116
|
-
stateManager.save();
|
|
117
116
|
}
|
|
118
117
|
catch (error) {
|
|
119
118
|
const msg = errorMessage(error);
|
|
120
|
-
console.error(`Warning: Failed to cache scan results: ${msg}`);
|
|
119
|
+
console.error(`Warning: Failed to cache scan results to disk: ${msg}`);
|
|
121
120
|
}
|
|
122
121
|
return {
|
|
123
122
|
repos,
|