@oss-autopilot/core 0.50.0 → 0.51.1

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.
@@ -41,7 +41,7 @@ export interface DailyCheckResult {
41
41
  * 1. fetchPRData — fetch open PRs, merged/closed counts, issues
42
42
  * 2. updateRepoScores — update signals, star counts, trust in state
43
43
  * 3. updateAnalytics — store monthly chart data
44
- * 4. partitionPRs — expire snoozes, shelve/unshelve, generate digest
44
+ * 4. partitionPRs — shelve/unshelve, generate digest
45
45
  * 5. generateDigestOutput — capacity, dismiss filter, action menu assembly
46
46
  */
47
47
  export declare function executeDailyCheck(token: string): Promise<DailyOutput>;
@@ -230,25 +230,12 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
230
230
  }
231
231
  }
232
232
  /**
233
- * Phase 4: Expire snoozes and partition PRs into active vs shelved buckets.
233
+ * Phase 4: Partition PRs into active vs shelved buckets.
234
234
  * Auto-unshelves PRs where maintainers have engaged, generates the digest,
235
235
  * and persists state.
236
236
  */
237
237
  function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
238
238
  const stateManager = getStateManager();
239
- // Expire any snoozes that have passed their expiresAt timestamp.
240
- // Non-critical: corrupted snooze entries should not abort the daily check.
241
- try {
242
- const expiredSnoozes = stateManager.expireSnoozes();
243
- if (expiredSnoozes.length > 0) {
244
- const urls = expiredSnoozes.map((url) => ` - ${url}`).join('\n');
245
- warn(MODULE, `${expiredSnoozes.length} snoozed PR(s) expired and will resurface:\n${urls}`);
246
- stateManager.save();
247
- }
248
- }
249
- catch (error) {
250
- warn(MODULE, `Failed to expire/persist snoozes: ${errorMessage(error)}`);
251
- }
252
239
  // Apply dashboard/CLI status overrides before partitioning.
253
240
  // This ensures PRs reclassified in the dashboard (e.g., "Need Attention" → "Waiting")
254
241
  // are respected by the CLI pipeline.
@@ -328,31 +315,7 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
328
315
  });
329
316
  const issueResponses = filteredCommentedIssues.filter((i) => i.status === 'new_response');
330
317
  const summary = formatSummary(digest, capacity, issueResponses);
331
- const snoozedUrls = new Set(Object.keys(stateManager.getState().config.snoozedPRs ?? {}).filter((url) => stateManager.isSnoozed(url)));
332
- // Filter dismissed PRs: suppress if dismissed after last activity, auto-undismiss if new activity (#416, #468)
333
- const nonDismissedPRs = activePRs.filter((pr) => {
334
- const dismissedAt = stateManager.getIssueDismissedAt(pr.url);
335
- if (!dismissedAt)
336
- return true; // Not dismissed — include
337
- const activityTime = new Date(pr.updatedAt).getTime();
338
- const dismissTime = new Date(dismissedAt).getTime();
339
- if (isNaN(activityTime) || isNaN(dismissTime)) {
340
- // Invalid timestamp — fail open (include PR to be safe) without
341
- // permanently removing dismiss record (may be a transient data issue)
342
- warn(MODULE, `Invalid timestamp in PR dismiss check for ${pr.url}, including PR`);
343
- return true;
344
- }
345
- if (activityTime > dismissTime) {
346
- // New activity after dismiss — auto-undismiss and resurface
347
- warn(MODULE, `Auto-undismissing PR ${pr.url}: new activity at ${pr.updatedAt} after dismiss at ${dismissedAt}`);
348
- stateManager.undismissIssue(pr.url);
349
- hasAutoUndismissed = true;
350
- return true;
351
- }
352
- // Still dismissed (last activity is at or before dismiss timestamp)
353
- return false;
354
- });
355
- // Persist auto-undismiss state changes (issue + PR combined into one save)
318
+ // Persist auto-undismiss state changes for issues
356
319
  if (hasAutoUndismissed) {
357
320
  try {
358
321
  stateManager.save();
@@ -361,7 +324,7 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
361
324
  warn(MODULE, `Failed to persist auto-undismissed state: ${errorMessage(error)}`);
362
325
  }
363
326
  }
364
- const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls, previousLastDigestAt);
327
+ const actionableIssues = collectActionableIssues(activePRs, previousLastDigestAt);
365
328
  digest.summary.totalNeedingAttention = actionableIssues.length;
366
329
  const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
367
330
  const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues);
@@ -410,7 +373,7 @@ function toDailyOutput(result) {
410
373
  * 1. fetchPRData — fetch open PRs, merged/closed counts, issues
411
374
  * 2. updateRepoScores — update signals, star counts, trust in state
412
375
  * 3. updateAnalytics — store monthly chart data
413
- * 4. partitionPRs — expire snoozes, shelve/unshelve, generate digest
376
+ * 4. partitionPRs — shelve/unshelve, generate digest
414
377
  * 5. generateDigestOutput — capacity, dismiss filter, action menu assembly
415
378
  */
416
379
  export async function executeDailyCheck(token) {
@@ -432,7 +395,7 @@ async function executeDailyCheckInternal(token) {
432
395
  // Capture lastDigestAt BEFORE Phase 4 overwrites it with the current run's timestamp.
433
396
  // Used by collectActionableIssues to determine which PRs are "new" (created since last digest).
434
397
  const previousLastDigestAt = getStateManager().getState().lastDigestAt;
435
- // Phase 4: Expire snoozes, partition PRs, generate and save digest
398
+ // Phase 4: Partition PRs, generate and save digest
436
399
  const { activePRs, shelvedPRs, digest } = partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs);
437
400
  // Phase 5: Build structured output (capacity, dismiss filter, action menu)
438
401
  return generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures, previousLastDigestAt);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dashboard HTTP server.
3
3
  * Serves the Preact SPA from packages/dashboard/dist/ and provides API endpoints
4
- * for live data fetching and state mutations (shelve, unshelve, override, etc.).
4
+ * for live data fetching and state mutations (PR state transitions, issue dismiss).
5
5
  *
6
6
  * Uses Node's built-in http module — no Express/Fastify.
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dashboard HTTP server.
3
3
  * Serves the Preact SPA from packages/dashboard/dist/ and provides API endpoints
4
- * for live data fetching and state mutations (shelve, unshelve, override, etc.).
4
+ * for live data fetching and state mutations (PR state transitions, issue dismiss).
5
5
  *
6
6
  * Uses Node's built-in http module — no Express/Fastify.
7
7
  */
@@ -11,7 +11,7 @@ import * as path from 'path';
11
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
- import { validateUrl, validateGitHubUrl, PR_URL_PATTERN, ISSUE_OR_PR_URL_PATTERN } from './validation.js';
14
+ import { validateUrl, validateGitHubUrl, PR_URL_PATTERN, ISSUE_URL_PATTERN } from './validation.js';
15
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';
@@ -20,12 +20,7 @@ import { isBelowMinStars, } from '../core/types.js';
20
20
  // Re-export process management functions for backward compatibility
21
21
  export { getDashboardPidPath, writeDashboardServerInfo, readDashboardServerInfo, removeDashboardServerInfo, isDashboardServerRunning, findRunningDashboardServer, } from './dashboard-process.js';
22
22
  // ── Constants ────────────────────────────────────────────────────────────────
23
- const VALID_ACTIONS = new Set([
24
- 'shelve',
25
- 'unshelve',
26
- 'override_status',
27
- 'dismiss_issue_response',
28
- ]);
23
+ const VALID_ACTIONS = new Set(['move', 'dismiss_issue_response']);
29
24
  const MODULE = 'dashboard-server';
30
25
  const MAX_BODY_BYTES = 10_240;
31
26
  const REQUEST_TIMEOUT_MS = 30_000;
@@ -170,7 +165,7 @@ export async function startDashboardServer(options) {
170
165
  // ── Rate limiters ───────────────────────────────────────────────────────
171
166
  const dataLimiter = new RateLimiter({ maxRequests: 30, windowMs: 60_000 }); // 30/min
172
167
  const actionLimiter = new RateLimiter({ maxRequests: 10, windowMs: 60_000 }); // 10/min
173
- const refreshLimiter = new RateLimiter({ maxRequests: 2, windowMs: 60_000 }); // 2/min
168
+ const refreshLimiter = new RateLimiter({ maxRequests: 6, windowMs: 60_000 }); // 6/min
174
169
  // ── Request handler ──────────────────────────────────────────────────────
175
170
  const server = http.createServer(async (req, res) => {
176
171
  const method = req.method || 'GET';
@@ -184,6 +179,16 @@ export async function startDashboardServer(options) {
184
179
  sendError(res, 429, 'Too many requests');
185
180
  return;
186
181
  }
182
+ // Re-read state.json if CLI commands modified it externally
183
+ if (stateManager.reloadIfChanged()) {
184
+ try {
185
+ cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
186
+ }
187
+ catch (error) {
188
+ warn(MODULE, `Failed to rebuild dashboard data after state reload: ${errorMessage(error)}`);
189
+ // Intentional: serve previous cachedJsonData rather than returning 500
190
+ }
191
+ }
187
192
  sendJson(res, 200, cachedJsonData);
188
193
  return;
189
194
  }
@@ -232,6 +237,8 @@ export async function startDashboardServer(options) {
232
237
  server.requestTimeout = REQUEST_TIMEOUT_MS;
233
238
  // ── POST /api/action handler ─────────────────────────────────────────────
234
239
  async function handleAction(req, res) {
240
+ // Reload state before mutating to avoid overwriting external CLI changes
241
+ stateManager.reloadIfChanged();
235
242
  let body;
236
243
  try {
237
244
  const raw = await readBody(req);
@@ -250,11 +257,11 @@ export async function startDashboardServer(options) {
250
257
  sendError(res, 400, 'Missing or invalid "url" field');
251
258
  return;
252
259
  }
253
- // Validate URL format — dismiss_issue_response accepts issue or PR URLs, others are PR-only.
260
+ // Validate URL format — move is PR-only, dismiss_issue_response is issue-only.
254
261
  const isDismiss = body.action === 'dismiss_issue_response';
255
262
  try {
256
263
  validateUrl(body.url);
257
- validateGitHubUrl(body.url, isDismiss ? ISSUE_OR_PR_URL_PATTERN : PR_URL_PATTERN, isDismiss ? 'issue or PR' : 'PR');
264
+ validateGitHubUrl(body.url, isDismiss ? ISSUE_URL_PATTERN : PR_URL_PATTERN, isDismiss ? 'issue' : 'PR');
258
265
  }
259
266
  catch (err) {
260
267
  if (err instanceof ValidationError) {
@@ -266,35 +273,20 @@ export async function startDashboardServer(options) {
266
273
  }
267
274
  return;
268
275
  }
269
- // Validate override_status-specific fields
270
- if (body.action === 'override_status') {
271
- if (!body.status || (body.status !== 'needs_addressing' && body.status !== 'waiting_on_maintainer')) {
272
- sendError(res, 400, 'override_status requires a valid "status" field (needs_addressing or waiting_on_maintainer)');
273
- return;
274
- }
275
- }
276
276
  try {
277
- switch (body.action) {
278
- case 'shelve':
279
- stateManager.shelvePR(body.url);
280
- break;
281
- case 'unshelve':
282
- stateManager.unshelvePR(body.url);
283
- break;
284
- case 'override_status': {
285
- // body.status is validated above — the early return ensures it's defined here
286
- const overrideStatus = body.status;
287
- // Find the PR to get its current updatedAt for auto-clear tracking
288
- const targetPR = (cachedDigest?.openPRs || []).find((pr) => pr.url === body.url);
289
- const lastActivityAt = targetPR?.updatedAt || new Date().toISOString();
290
- stateManager.setStatusOverride(body.url, overrideStatus, lastActivityAt);
291
- break;
277
+ if (body.action === 'move') {
278
+ const { VALID_TARGETS, runMove } = await import('./move.js');
279
+ if (!body.target || !VALID_TARGETS.includes(body.target)) {
280
+ sendError(res, 400, `move requires a valid "target" field (${VALID_TARGETS.join(', ')})`);
281
+ return;
292
282
  }
293
- case 'dismiss_issue_response':
294
- stateManager.dismissIssue(body.url, new Date().toISOString());
295
- break;
283
+ await runMove({ prUrl: body.url, target: body.target });
284
+ }
285
+ else {
286
+ // dismiss_issue_response
287
+ stateManager.dismissIssue(body.url, new Date().toISOString());
288
+ stateManager.save();
296
289
  }
297
- stateManager.save();
298
290
  }
299
291
  catch (error) {
300
292
  warn(MODULE, `Action failed: ${body.action} ${body.url} ${errorMessage(error)}`);
@@ -313,6 +305,7 @@ export async function startDashboardServer(options) {
313
305
  return;
314
306
  }
315
307
  try {
308
+ stateManager.reloadIfChanged();
316
309
  warn(MODULE, 'Refreshing dashboard data from GitHub...');
317
310
  const result = await fetchDashboardData(currentToken);
318
311
  cachedDigest = result.digest;
@@ -428,6 +421,7 @@ export async function startDashboardServer(options) {
428
421
  if (token) {
429
422
  fetchDashboardData(token)
430
423
  .then((result) => {
424
+ stateManager.reloadIfChanged();
431
425
  cachedDigest = result.digest;
432
426
  cachedCommentedIssues = result.commentedIssues;
433
427
  cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues, result.allMergedPRs, result.allClosedPRs);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Dismiss/Undismiss commands
3
- * Manages dismissing issue and PR notifications without posting a comment.
3
+ * Manages dismissing issue notifications without posting a comment.
4
4
  * Dismissed URLs resurface automatically when new responses arrive after the dismiss timestamp.
5
5
  */
6
6
  export interface DismissOutput {
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * Dismiss/Undismiss commands
3
- * Manages dismissing issue and PR notifications without posting a comment.
3
+ * Manages dismissing issue notifications without posting a comment.
4
4
  * Dismissed URLs resurface automatically when new responses arrive after the dismiss timestamp.
5
5
  */
6
6
  import { getStateManager } from '../core/index.js';
7
- import { ISSUE_OR_PR_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
7
+ import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
8
8
  export async function runDismiss(options) {
9
9
  validateUrl(options.url);
10
- validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, 'issue or PR');
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
13
  if (added) {
@@ -17,7 +17,7 @@ export async function runDismiss(options) {
17
17
  }
18
18
  export async function runUndismiss(options) {
19
19
  validateUrl(options.url);
20
- validateGitHubUrl(options.url, ISSUE_OR_PR_URL_PATTERN, 'issue or PR');
20
+ validateGitHubUrl(options.url, ISSUE_URL_PATTERN, 'issue');
21
21
  const stateManager = getStateManager();
22
22
  const removed = stateManager.undismissIssue(options.url);
23
23
  if (removed) {
@@ -35,14 +35,12 @@ export { runRead } from './read.js';
35
35
  export { runShelve } from './shelve.js';
36
36
  /** Restore a shelved PR to the daily digest. */
37
37
  export { runUnshelve } from './shelve.js';
38
+ /** Move a PR between states: attention, waiting, shelved, auto. */
39
+ export { runMove } from './move.js';
38
40
  /** Dismiss issue reply notifications (auto-resurfaces on new activity). */
39
41
  export { runDismiss } from './dismiss.js';
40
42
  /** Restore a dismissed issue to notifications. */
41
43
  export { runUndismiss } from './dismiss.js';
42
- /** Temporarily suppress CI failure notifications for a PR. */
43
- export { runSnooze } from './snooze.js';
44
- /** Restore CI failure notifications for a snoozed PR. */
45
- export { runUnsnooze } from './snooze.js';
46
44
  /** Fetch comments for tracked issues/PRs. */
47
45
  export { runComments } from './comments.js';
48
46
  /** Post a comment to a GitHub issue or PR. */
@@ -68,8 +66,8 @@ export type { VetOutput, CommentsOutput, PostOutput, ClaimOutput } from '../form
68
66
  export type { ConfigOutput, ParseIssueListOutput, CheckIntegrationOutput, LocalReposOutput, } from '../formatters/json.js';
69
67
  export type { ReadOutput } from './read.js';
70
68
  export type { ShelveOutput, UnshelveOutput } from './shelve.js';
69
+ export type { MoveOutput, MoveTarget } from './move.js';
71
70
  export type { DismissOutput, UndismissOutput } from './dismiss.js';
72
- export type { SnoozeOutput, UnsnoozeOutput } from './snooze.js';
73
71
  export type { UntrackOutput } from './track.js';
74
72
  export type { InitOutput } from './init.js';
75
73
  export type { ConfigSetOutput, ConfigCommandOutput } from './config.js';
@@ -37,14 +37,12 @@ export { runRead } from './read.js';
37
37
  export { runShelve } from './shelve.js';
38
38
  /** Restore a shelved PR to the daily digest. */
39
39
  export { runUnshelve } from './shelve.js';
40
+ /** Move a PR between states: attention, waiting, shelved, auto. */
41
+ export { runMove } from './move.js';
40
42
  /** Dismiss issue reply notifications (auto-resurfaces on new activity). */
41
43
  export { runDismiss } from './dismiss.js';
42
44
  /** Restore a dismissed issue to notifications. */
43
45
  export { runUndismiss } from './dismiss.js';
44
- /** Temporarily suppress CI failure notifications for a PR. */
45
- export { runSnooze } from './snooze.js';
46
- /** Restore CI failure notifications for a snoozed PR. */
47
- export { runUnsnooze } from './snooze.js';
48
46
  // ── Issue & Comment Management ──────────────────────────────────────────────
49
47
  /** Fetch comments for tracked issues/PRs. */
50
48
  export { runComments } from './comments.js';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Move command — transition a PR between states:
3
+ * attention, waiting, shelved, or auto (reset to computed status).
4
+ */
5
+ export declare const VALID_TARGETS: readonly ["attention", "waiting", "shelved", "auto"];
6
+ export type MoveTarget = (typeof VALID_TARGETS)[number];
7
+ export interface MoveOutput {
8
+ url: string;
9
+ target: MoveTarget;
10
+ /** Human-readable description of what happened. */
11
+ description: string;
12
+ }
13
+ export declare function runMove(options: {
14
+ prUrl: string;
15
+ target: string;
16
+ }): Promise<MoveOutput>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Move command — transition a PR between states:
3
+ * attention, waiting, shelved, or auto (reset to computed status).
4
+ */
5
+ import { getStateManager } from '../core/index.js';
6
+ import { PR_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
7
+ export const VALID_TARGETS = ['attention', 'waiting', 'shelved', 'auto'];
8
+ export async function runMove(options) {
9
+ validateUrl(options.prUrl);
10
+ validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
11
+ const target = options.target;
12
+ if (!VALID_TARGETS.includes(target)) {
13
+ throw new Error(`Invalid target "${options.target}". Must be one of: ${VALID_TARGETS.join(', ')}`);
14
+ }
15
+ const stateManager = getStateManager();
16
+ switch (target) {
17
+ case 'attention':
18
+ case 'waiting': {
19
+ const status = target === 'attention' ? 'needs_addressing' : 'waiting_on_maintainer';
20
+ const label = target === 'attention' ? 'Need Attention' : 'Waiting on Maintainer';
21
+ // Use current time — the CLI doesn't have cached PR data. The override
22
+ // will auto-clear on the next daily run if the PR has new activity after this.
23
+ const lastActivityAt = new Date().toISOString();
24
+ stateManager.setStatusOverride(options.prUrl, status, lastActivityAt);
25
+ stateManager.unshelvePR(options.prUrl);
26
+ stateManager.save();
27
+ return { url: options.prUrl, target, description: `Moved to ${label}` };
28
+ }
29
+ case 'shelved': {
30
+ stateManager.shelvePR(options.prUrl);
31
+ stateManager.clearStatusOverride(options.prUrl);
32
+ stateManager.save();
33
+ return {
34
+ url: options.prUrl,
35
+ target,
36
+ description: 'Shelved — excluded from capacity and actionable items',
37
+ };
38
+ }
39
+ case 'auto': {
40
+ const clearedOverride = stateManager.clearStatusOverride(options.prUrl);
41
+ const unshelved = stateManager.unshelvePR(options.prUrl);
42
+ if (clearedOverride || unshelved) {
43
+ stateManager.save();
44
+ }
45
+ return {
46
+ url: options.prUrl,
47
+ target,
48
+ description: 'Reset to computed status',
49
+ };
50
+ }
51
+ default: {
52
+ const _exhaustive = target;
53
+ throw new Error(`Unhandled move target: ${_exhaustive}`);
54
+ }
55
+ }
56
+ }
@@ -2,6 +2,10 @@
2
2
  * Shelve/Unshelve commands
3
3
  * Manages shelving PRs to exclude them from capacity and actionable issues.
4
4
  * Shelved PRs are auto-unshelved when a maintainer engages.
5
+ *
6
+ * Note: The CLI and MCP shelve/unshelve commands delegate to runMove(),
7
+ * which also clears status overrides. These functions match that behavior
8
+ * to keep the library API consistent.
5
9
  */
6
10
  import { PR_URL_PATTERN } from './validation.js';
7
11
  export interface ShelveOutput {
@@ -2,6 +2,10 @@
2
2
  * Shelve/Unshelve commands
3
3
  * Manages shelving PRs to exclude them from capacity and actionable issues.
4
4
  * Shelved PRs are auto-unshelved when a maintainer engages.
5
+ *
6
+ * Note: The CLI and MCP shelve/unshelve commands delegate to runMove(),
7
+ * which also clears status overrides. These functions match that behavior
8
+ * to keep the library API consistent.
5
9
  */
6
10
  import { getStateManager } from '../core/index.js';
7
11
  import { PR_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
@@ -12,7 +16,8 @@ export async function runShelve(options) {
12
16
  validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
13
17
  const stateManager = getStateManager();
14
18
  const added = stateManager.shelvePR(options.prUrl);
15
- if (added) {
19
+ const clearedOverride = stateManager.clearStatusOverride(options.prUrl);
20
+ if (added || clearedOverride) {
16
21
  stateManager.save();
17
22
  }
18
23
  return { shelved: added, url: options.prUrl };
@@ -22,7 +27,8 @@ export async function runUnshelve(options) {
22
27
  validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
23
28
  const stateManager = getStateManager();
24
29
  const removed = stateManager.unshelvePR(options.prUrl);
25
- if (removed) {
30
+ const clearedOverride = stateManager.clearStatusOverride(options.prUrl);
31
+ if (removed || clearedOverride) {
26
32
  stateManager.save();
27
33
  }
28
34
  return { unshelved: removed, url: options.prUrl };
@@ -54,7 +54,7 @@ export declare function assessCapacity(activePRs: FetchedPR[], maxActivePRs: num
54
54
  * Note: Recently closed PRs are informational only and excluded from this list.
55
55
  * They are available separately in digest.recentlyClosedPRs (#156).
56
56
  */
57
- export declare function collectActionableIssues(prs: FetchedPR[], snoozedUrls?: Set<string>, lastDigestAt?: string): ActionableIssue[];
57
+ export declare function collectActionableIssues(prs: FetchedPR[], lastDigestAt?: string): ActionableIssue[];
58
58
  /**
59
59
  * Format a maintainer action hint as a human-readable label
60
60
  */
@@ -198,7 +198,7 @@ export function assessCapacity(activePRs, maxActivePRs, shelvedPRCount) {
198
198
  * Note: Recently closed PRs are informational only and excluded from this list.
199
199
  * They are available separately in digest.recentlyClosedPRs (#156).
200
200
  */
201
- export function collectActionableIssues(prs, snoozedUrls = new Set(), lastDigestAt) {
201
+ export function collectActionableIssues(prs, lastDigestAt) {
202
202
  const issues = [];
203
203
  const actionPRs = prs.filter((pr) => pr.status === 'needs_addressing');
204
204
  const lastDigestTime = lastDigestAt ? new Date(lastDigestAt).getTime() : NaN;
@@ -213,8 +213,6 @@ export function collectActionableIssues(prs, snoozedUrls = new Set(), lastDigest
213
213
  for (const pr of actionPRs) {
214
214
  if (pr.actionReason !== reason)
215
215
  continue;
216
- if (reason === 'failing_ci' && snoozedUrls.has(pr.url))
217
- continue;
218
216
  let label;
219
217
  let type;
220
218
  switch (reason) {
@@ -10,12 +10,14 @@
10
10
  * - maintainer-analysis.ts: Maintainer action hint extraction
11
11
  * - display-utils.ts: Display label computation
12
12
  * - github-stats.ts: Merged/closed PR counts and star fetching
13
+ * - status-determination.ts: PR status classification logic
13
14
  */
14
15
  import { FetchedPR, DailyDigest, ClosedPR, MergedPR, StarFilter } from './types.js';
15
16
  import { type PRCountsResult } from './github-stats.js';
16
17
  export { computeDisplayLabel } from './display-utils.js';
17
18
  export { classifyCICheck, classifyFailingChecks } from './ci-analysis.js';
18
19
  export { isConditionalChecklistItem } from './checklist-analysis.js';
20
+ export { determineStatus } from './status-determination.js';
19
21
  export interface PRCheckFailure {
20
22
  prUrl: string;
21
23
  error: string;
@@ -42,33 +44,6 @@ export declare class PRMonitor {
42
44
  * Centralizes PR construction and display label computation (#79).
43
45
  */
44
46
  private buildFetchedPR;
45
- /**
46
- * Determine the overall status of a PR
47
- */
48
- private determineStatus;
49
- /**
50
- * CI-fix bots that push commits as a direct result of the contributor's push (#568).
51
- * Their commits represent contributor work and should count as addressing feedback.
52
- * This is intentionally an allowlist — not all `[bot]` accounts are CI-fix bots
53
- * (e.g. dependabot[bot] and renovate[bot] open their own PRs).
54
- * Values must be lowercase — lookup uses .toLowerCase() for case-insensitive matching.
55
- */
56
- private static readonly CI_FIX_BOTS;
57
- /**
58
- * Check whether the HEAD commit was authored by the contributor (#547).
59
- * Returns true when the author matches, when the author is a known CI-fix
60
- * bot (#568), or when author info is unavailable (graceful degradation).
61
- */
62
- private isContributorCommit;
63
- /** Minimum gap (ms) between maintainer comment and contributor commit for
64
- * the commit to count as "addressing" the feedback (#547). Prevents false
65
- * positives from race conditions, clock skew, and in-flight pushes. */
66
- private static readonly MIN_RESPONSE_GAP_MS;
67
- /**
68
- * Check whether the contributor's commit is meaningfully after the maintainer's
69
- * comment — i.e. the commit timestamp is at least MIN_RESPONSE_GAP_MS later (#547).
70
- */
71
- private isCommitAfterComment;
72
47
  /**
73
48
  * Check if PR has merge conflict
74
49
  */