@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.
- package/dist/cli-registry.js +44 -98
- package/dist/cli.bundle.cjs +43 -45
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/commands/daily.d.ts +1 -1
- package/dist/commands/daily.js +5 -42
- package/dist/commands/dashboard-server.d.ts +1 -1
- package/dist/commands/dashboard-server.js +19 -29
- package/dist/commands/dismiss.d.ts +1 -1
- package/dist/commands/dismiss.js +4 -4
- package/dist/commands/index.d.ts +3 -5
- package/dist/commands/index.js +2 -4
- package/dist/commands/move.d.ts +16 -0
- package/dist/commands/move.js +56 -0
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.js +62 -0
- package/dist/commands/shelve.d.ts +4 -0
- package/dist/commands/shelve.js +8 -2
- package/dist/core/category-mapping.d.ts +19 -0
- package/dist/core/category-mapping.js +58 -0
- package/dist/core/daily-logic.d.ts +1 -1
- package/dist/core/daily-logic.js +8 -5
- package/dist/core/issue-discovery.js +55 -3
- package/dist/core/issue-scoring.d.ts +3 -0
- package/dist/core/issue-scoring.js +5 -0
- package/dist/core/issue-vetting.js +12 -0
- package/dist/core/pr-monitor.d.ts +2 -27
- package/dist/core/pr-monitor.js +4 -110
- package/dist/core/state.d.ts +8 -40
- package/dist/core/state.js +36 -93
- package/dist/core/status-determination.d.ts +35 -0
- package/dist/core/status-determination.js +112 -0
- package/dist/core/types.d.ts +18 -12
- package/dist/core/types.js +11 -1
- package/package.json +1 -1
- package/dist/commands/override.d.ts +0 -21
- package/dist/commands/override.js +0 -35
- package/dist/commands/snooze.d.ts +0 -24
- package/dist/commands/snooze.js +0 -40
package/dist/commands/daily.d.ts
CHANGED
|
@@ -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 —
|
|
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>;
|
package/dist/commands/daily.js
CHANGED
|
@@ -230,25 +230,12 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
/**
|
|
233
|
-
* Phase 4:
|
|
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
|
-
|
|
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(
|
|
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 —
|
|
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:
|
|
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 (
|
|
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 (
|
|
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(['
|
|
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
|
|
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 —
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
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 {
|
package/dist/commands/dismiss.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dismiss/Undismiss commands
|
|
3
|
-
* Manages dismissing issue
|
|
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 {
|
|
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,
|
|
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,
|
|
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) {
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/commands/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -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 {
|
package/dist/commands/setup.js
CHANGED
|
@@ -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 {
|
package/dist/commands/shelve.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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[],
|
|
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
|
*/
|
package/dist/core/daily-logic.js
CHANGED
|
@@ -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,
|
|
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:
|
|
300
|
-
|
|
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
|