@oss-autopilot/core 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli-registry.js +44 -98
  2. package/dist/cli.bundle.cjs +43 -45
  3. package/dist/cli.bundle.cjs.map +4 -4
  4. package/dist/commands/daily.d.ts +1 -1
  5. package/dist/commands/daily.js +5 -42
  6. package/dist/commands/dashboard-server.d.ts +1 -1
  7. package/dist/commands/dashboard-server.js +19 -29
  8. package/dist/commands/dismiss.d.ts +1 -1
  9. package/dist/commands/dismiss.js +4 -4
  10. package/dist/commands/index.d.ts +3 -5
  11. package/dist/commands/index.js +2 -4
  12. package/dist/commands/move.d.ts +16 -0
  13. package/dist/commands/move.js +56 -0
  14. package/dist/commands/setup.d.ts +3 -0
  15. package/dist/commands/setup.js +62 -0
  16. package/dist/commands/shelve.d.ts +4 -0
  17. package/dist/commands/shelve.js +8 -2
  18. package/dist/core/category-mapping.d.ts +19 -0
  19. package/dist/core/category-mapping.js +58 -0
  20. package/dist/core/daily-logic.d.ts +1 -1
  21. package/dist/core/daily-logic.js +8 -5
  22. package/dist/core/issue-discovery.js +55 -3
  23. package/dist/core/issue-scoring.d.ts +3 -0
  24. package/dist/core/issue-scoring.js +5 -0
  25. package/dist/core/issue-vetting.js +12 -0
  26. package/dist/core/pr-monitor.d.ts +2 -27
  27. package/dist/core/pr-monitor.js +4 -110
  28. package/dist/core/state.d.ts +8 -40
  29. package/dist/core/state.js +36 -93
  30. package/dist/core/status-determination.d.ts +35 -0
  31. package/dist/core/status-determination.js +112 -0
  32. package/dist/core/types.d.ts +18 -12
  33. package/dist/core/types.js +11 -1
  34. package/package.json +1 -1
  35. package/dist/commands/override.d.ts +0 -21
  36. package/dist/commands/override.js +0 -35
  37. package/dist/commands/snooze.d.ts +0 -24
  38. package/dist/commands/snooze.js +0 -40
@@ -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 } 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,7 +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(['shelve', 'unshelve', 'override_status']);
23
+ const VALID_ACTIONS = new Set(['move', 'dismiss_issue_response']);
24
24
  const MODULE = 'dashboard-server';
25
25
  const MAX_BODY_BYTES = 10_240;
26
26
  const REQUEST_TIMEOUT_MS = 30_000;
@@ -51,7 +51,8 @@ function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClo
51
51
  const filteredMergedPRs = mergedPRs.filter(isAboveMinStars);
52
52
  const filteredClosedPRs = closedPRs.filter(isAboveMinStars);
53
53
  const stats = buildDashboardStats(digest, state, filteredMergedPRs.length, filteredClosedPRs.length);
54
- const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
54
+ const dismissedIssues = state.config.dismissedIssues || {};
55
+ const issueResponses = commentedIssues.filter((i) => i.status === 'new_response' && !(i.url in dismissedIssues));
55
56
  return {
56
57
  stats,
57
58
  prsByRepo,
@@ -244,10 +245,11 @@ export async function startDashboardServer(options) {
244
245
  sendError(res, 400, 'Missing or invalid "url" field');
245
246
  return;
246
247
  }
247
- // Validate URL format — all actions are PR-only now.
248
+ // Validate URL format — move is PR-only, dismiss_issue_response is issue-only.
249
+ const isDismiss = body.action === 'dismiss_issue_response';
248
250
  try {
249
251
  validateUrl(body.url);
250
- validateGitHubUrl(body.url, PR_URL_PATTERN, 'PR');
252
+ validateGitHubUrl(body.url, isDismiss ? ISSUE_URL_PATTERN : PR_URL_PATTERN, isDismiss ? 'issue' : 'PR');
251
253
  }
252
254
  catch (err) {
253
255
  if (err instanceof ValidationError) {
@@ -259,32 +261,20 @@ export async function startDashboardServer(options) {
259
261
  }
260
262
  return;
261
263
  }
262
- // Validate override_status-specific fields
263
- if (body.action === 'override_status') {
264
- if (!body.status || (body.status !== 'needs_addressing' && body.status !== 'waiting_on_maintainer')) {
265
- sendError(res, 400, 'override_status requires a valid "status" field (needs_addressing or waiting_on_maintainer)');
266
- return;
267
- }
268
- }
269
264
  try {
270
- switch (body.action) {
271
- case 'shelve':
272
- stateManager.shelvePR(body.url);
273
- break;
274
- case 'unshelve':
275
- stateManager.unshelvePR(body.url);
276
- break;
277
- case 'override_status': {
278
- // body.status is validated above — the early return ensures it's defined here
279
- const overrideStatus = body.status;
280
- // Find the PR to get its current updatedAt for auto-clear tracking
281
- const targetPR = (cachedDigest?.openPRs || []).find((pr) => pr.url === body.url);
282
- const lastActivityAt = targetPR?.updatedAt || new Date().toISOString();
283
- stateManager.setStatusOverride(body.url, overrideStatus, lastActivityAt);
284
- break;
265
+ if (body.action === 'move') {
266
+ const { VALID_TARGETS, runMove } = await import('./move.js');
267
+ if (!body.target || !VALID_TARGETS.includes(body.target)) {
268
+ sendError(res, 400, `move requires a valid "target" field (${VALID_TARGETS.join(', ')})`);
269
+ return;
285
270
  }
271
+ await runMove({ prUrl: body.url, target: body.target });
272
+ }
273
+ else {
274
+ // dismiss_issue_response
275
+ stateManager.dismissIssue(body.url, new Date().toISOString());
276
+ stateManager.save();
286
277
  }
287
- stateManager.save();
288
278
  }
289
279
  catch (error) {
290
280
  warn(MODULE, `Action failed: ${body.action} ${body.url} ${errorMessage(error)}`);
@@ -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,7 @@
2
2
  * Setup command
3
3
  * Interactive setup / configuration
4
4
  */
5
+ import { type ProjectCategory } from '../core/types.js';
5
6
  interface SetupOptions {
6
7
  reset?: boolean;
7
8
  set?: string[];
@@ -20,6 +21,8 @@ export interface SetupCompleteOutput {
20
21
  approachingDormantDays: number;
21
22
  languages: string[];
22
23
  labels: string[];
24
+ projectCategories: ProjectCategory[];
25
+ preferredOrgs: string[];
23
26
  };
24
27
  }
25
28
  export interface SetupPrompt {
@@ -5,6 +5,7 @@
5
5
  import { getStateManager, DEFAULT_CONFIG } from '../core/index.js';
6
6
  import { ValidationError } from '../core/errors.js';
7
7
  import { validateGitHubUsername } from './validation.js';
8
+ import { PROJECT_CATEGORIES } from '../core/types.js';
8
9
  /** Parse and validate a positive integer setting value. */
9
10
  function parsePositiveInt(value, settingName) {
10
11
  const parsed = Number(value);
@@ -111,6 +112,51 @@ export async function runSetup(options) {
111
112
  results[key] = valid.length > 0 ? valid.join(', ') : '(empty)';
112
113
  break;
113
114
  }
115
+ case 'projectCategories': {
116
+ const categories = value
117
+ .split(',')
118
+ .map((c) => c.trim())
119
+ .filter(Boolean);
120
+ const validCategories = [];
121
+ const invalidCategories = [];
122
+ for (const cat of categories) {
123
+ if (PROJECT_CATEGORIES.includes(cat)) {
124
+ validCategories.push(cat);
125
+ }
126
+ else {
127
+ invalidCategories.push(cat);
128
+ }
129
+ }
130
+ if (invalidCategories.length > 0) {
131
+ warnings.push(`Unknown project categories: ${invalidCategories.join(', ')}. Valid: ${PROJECT_CATEGORIES.join(', ')}`);
132
+ }
133
+ const dedupedCategories = [...new Set(validCategories)];
134
+ stateManager.updateConfig({ projectCategories: dedupedCategories });
135
+ results[key] = dedupedCategories.length > 0 ? dedupedCategories.join(', ') : '(empty)';
136
+ break;
137
+ }
138
+ case 'preferredOrgs': {
139
+ const orgs = value
140
+ .split(',')
141
+ .map((o) => o.trim())
142
+ .filter(Boolean);
143
+ const validOrgs = [];
144
+ for (const org of orgs) {
145
+ if (org.includes('/')) {
146
+ warnings.push(`"${org}" looks like a repo path. Use org name only (e.g., "vercel" not "vercel/next.js").`);
147
+ }
148
+ else if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(org)) {
149
+ warnings.push(`"${org}" is not a valid GitHub organization name. Skipping.`);
150
+ }
151
+ else {
152
+ validOrgs.push(org.toLowerCase());
153
+ }
154
+ }
155
+ const dedupedOrgs = [...new Set(validOrgs)];
156
+ stateManager.updateConfig({ preferredOrgs: dedupedOrgs });
157
+ results[key] = dedupedOrgs.length > 0 ? dedupedOrgs.join(', ') : '(empty)';
158
+ break;
159
+ }
114
160
  case 'complete':
115
161
  if (value === 'true') {
116
162
  stateManager.markSetupComplete();
@@ -135,6 +181,8 @@ export async function runSetup(options) {
135
181
  approachingDormantDays: config.approachingDormantDays,
136
182
  languages: config.languages,
137
183
  labels: config.labels,
184
+ projectCategories: config.projectCategories ?? [],
185
+ preferredOrgs: config.preferredOrgs ?? [],
138
186
  },
139
187
  };
140
188
  }
@@ -191,6 +239,20 @@ export async function runSetup(options) {
191
239
  default: ['matplotlib/matplotlib'],
192
240
  type: 'list',
193
241
  },
242
+ {
243
+ setting: 'projectCategories',
244
+ prompt: 'What types of projects interest you? (nonprofit, devtools, infrastructure, web-frameworks, data-ml, education)',
245
+ current: config.projectCategories ?? [],
246
+ default: [],
247
+ type: 'list',
248
+ },
249
+ {
250
+ setting: 'preferredOrgs',
251
+ prompt: 'Any GitHub organizations to prioritize? (org names, comma-separated)',
252
+ current: config.preferredOrgs ?? [],
253
+ default: [],
254
+ type: 'list',
255
+ },
194
256
  ],
195
257
  };
196
258
  }
@@ -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 };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Category Mapping — static mappings from project categories to GitHub topics and organizations.
3
+ *
4
+ * Used by issue discovery to prioritize repos matching user's category preferences.
5
+ */
6
+ import type { ProjectCategory } from './types.js';
7
+ /** GitHub topics associated with each project category, used for `topic:` search queries. */
8
+ export declare const CATEGORY_TOPICS: Record<ProjectCategory, string[]>;
9
+ /** Well-known GitHub organizations associated with each project category. */
10
+ export declare const CATEGORY_ORGS: Record<ProjectCategory, string[]>;
11
+ /**
12
+ * Check if a repo belongs to any of the given categories based on its owner matching a category org.
13
+ * Comparison is case-insensitive.
14
+ */
15
+ export declare function repoBelongsToCategory(repoFullName: string, categories: ProjectCategory[]): boolean;
16
+ /**
17
+ * Get deduplicated GitHub topics for the given categories, for use in `topic:` search queries.
18
+ */
19
+ export declare function getTopicsForCategories(categories: ProjectCategory[]): string[];
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Category Mapping — static mappings from project categories to GitHub topics and organizations.
3
+ *
4
+ * Used by issue discovery to prioritize repos matching user's category preferences.
5
+ */
6
+ /** GitHub topics associated with each project category, used for `topic:` search queries. */
7
+ export const CATEGORY_TOPICS = {
8
+ nonprofit: ['nonprofit', 'social-good', 'humanitarian', 'charity', 'social-impact', 'civic-tech'],
9
+ devtools: ['developer-tools', 'devtools', 'cli', 'sdk', 'linter', 'formatter', 'build-tool'],
10
+ infrastructure: ['infrastructure', 'cloud', 'kubernetes', 'docker', 'devops', 'monitoring', 'observability'],
11
+ 'web-frameworks': ['web-framework', 'frontend', 'backend', 'fullstack', 'nextjs', 'react', 'vue'],
12
+ 'data-ml': ['machine-learning', 'data-science', 'deep-learning', 'nlp', 'data-pipeline', 'analytics'],
13
+ education: ['education', 'learning', 'tutorial', 'courseware', 'edtech', 'teaching'],
14
+ };
15
+ /** Well-known GitHub organizations associated with each project category. */
16
+ export const CATEGORY_ORGS = {
17
+ nonprofit: ['code-for-america', 'opengovfoundation', 'ushahidi', 'hotosm', 'openfn', 'democracyearth'],
18
+ devtools: ['eslint', 'prettier', 'vitejs', 'biomejs', 'oxc-project', 'ast-grep', 'turbot'],
19
+ infrastructure: ['kubernetes', 'hashicorp', 'grafana', 'prometheus', 'open-telemetry', 'envoyproxy', 'cncf'],
20
+ 'web-frameworks': ['vercel', 'remix-run', 'sveltejs', 'nuxt', 'astro', 'redwoodjs', 'blitz-js'],
21
+ 'data-ml': ['huggingface', 'mlflow', 'apache', 'dbt-labs', 'dagster-io', 'prefecthq', 'langchain-ai'],
22
+ education: ['freeCodeCamp', 'TheOdinProject', 'exercism', 'codecademy', 'oppia', 'Khan'],
23
+ };
24
+ /**
25
+ * Check if a repo belongs to any of the given categories based on its owner matching a category org.
26
+ * Comparison is case-insensitive.
27
+ */
28
+ export function repoBelongsToCategory(repoFullName, categories) {
29
+ if (categories.length === 0)
30
+ return false;
31
+ const owner = repoFullName.split('/')[0]?.toLowerCase();
32
+ if (!owner)
33
+ return false;
34
+ for (const category of categories) {
35
+ const orgs = CATEGORY_ORGS[category];
36
+ if (!orgs)
37
+ continue; // Guard against invalid categories from hand-edited state.json
38
+ if (orgs.some((org) => org.toLowerCase() === owner)) {
39
+ return true;
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+ /**
45
+ * Get deduplicated GitHub topics for the given categories, for use in `topic:` search queries.
46
+ */
47
+ export function getTopicsForCategories(categories) {
48
+ const topics = new Set();
49
+ for (const category of categories) {
50
+ const categoryTopics = CATEGORY_TOPICS[category];
51
+ if (!categoryTopics)
52
+ continue; // Guard against invalid categories from hand-edited state.json
53
+ for (const topic of categoryTopics) {
54
+ topics.add(topic);
55
+ }
56
+ }
57
+ return [...topics];
58
+ }
@@ -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) {
@@ -294,10 +292,15 @@ export function computeActionMenu(actionableIssues, capacity, commentedIssues =
294
292
  const hasActionableIssues = actionableIssues.length > 0;
295
293
  const hasIssueResponses = issueResponses.length > 0;
296
294
  if (hasActionableIssues) {
295
+ const isSingle = actionableIssues.length === 1;
297
296
  items.push({
298
297
  key: 'address_all',
299
- label: `Work through all ${actionableIssues.length} issue${actionableIssues.length === 1 ? '' : 's'} (Recommended)`,
300
- description: 'Run maintenance in parallel, then address code changes one at a time',
298
+ label: isSingle
299
+ ? 'Address this issue (Recommended)'
300
+ : `Work through all ${actionableIssues.length} issues (Recommended)`,
301
+ description: isSingle
302
+ ? 'Fix the issue blocking your PR'
303
+ : 'Run maintenance in parallel, then address code changes one at a time',
301
304
  });
302
305
  }
303
306
  // Issue replies — positioned after address_all but before search