@oss-autopilot/core 0.48.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup, type AgentState, type StarFilter } from '../core/index.js';
10
10
  import { type DailyOutput, type CapacityAssessment, type ActionableIssue, type ActionMenu } from '../formatters/json.js';
11
- export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
11
+ export { applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
12
12
  /**
13
13
  * Build a star filter from state for use in fetchUserPRCounts.
14
14
  * Returns undefined if no star data is available (first run).
@@ -6,7 +6,7 @@
6
6
  * Domain logic lives in src/core/daily-logic.ts; this file is a thin
7
7
  * orchestration layer that wires up the phases and handles I/O.
8
8
  */
9
- import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
9
+ import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
10
10
  import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
11
11
  import { warn } from '../core/logger.js';
12
12
  import { emptyPRCountsResult } from '../core/github-stats.js';
@@ -15,7 +15,7 @@ import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '
15
15
  const MODULE = 'daily';
16
16
  // Re-export domain functions so existing consumers (tests, dashboard, startup)
17
17
  // can continue importing from './daily.js' without changes.
18
- export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
18
+ export { applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
19
19
  /**
20
20
  * Build a star filter from state for use in fetchUserPRCounts.
21
21
  * Returns undefined if no star data is available (first run).
@@ -249,11 +249,15 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
249
249
  catch (error) {
250
250
  warn(MODULE, `Failed to expire/persist snoozes: ${errorMessage(error)}`);
251
251
  }
252
+ // Apply dashboard/CLI status overrides before partitioning.
253
+ // This ensures PRs reclassified in the dashboard (e.g., "Need Attention" → "Waiting")
254
+ // are respected by the CLI pipeline.
255
+ const overriddenPRs = applyStatusOverrides(prs, stateManager.getState());
252
256
  // Partition PRs into active vs shelved, auto-unshelving when maintainers engage
253
257
  const shelvedPRs = [];
254
258
  const autoUnshelvedPRs = [];
255
259
  const activePRs = [];
256
- for (const pr of prs) {
260
+ for (const pr of overriddenPRs) {
257
261
  if (stateManager.isPRShelved(pr.url)) {
258
262
  if (CRITICAL_STATUSES.has(pr.status)) {
259
263
  stateManager.unshelvePR(pr.url);
@@ -273,10 +277,10 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
273
277
  activePRs.push(pr);
274
278
  }
275
279
  }
276
- // Generate digest from fresh data.
280
+ // Generate digest from override-applied PRs so status categories are correct.
277
281
  // Note: digest.openPRs contains ALL fetched PRs (including shelved).
278
282
  // We override summary fields below to reflect active-only counts.
279
- const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
283
+ const digest = prMonitor.generateDigest(overriddenPRs, recentlyClosedPRs, recentlyMergedPRs);
280
284
  // Attach shelve info to digest
281
285
  digest.shelvedPRs = shelvedPRs;
282
286
  digest.autoUnshelvedPRs = autoUnshelvedPRs;
@@ -292,7 +296,7 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
292
296
  * Assesses capacity, filters dismissed issues, computes actionable items,
293
297
  * and assembles the action menu.
294
298
  */
295
- function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures) {
299
+ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures, previousLastDigestAt) {
296
300
  const stateManager = getStateManager();
297
301
  // Assess capacity from active PRs only (shelved PRs excluded)
298
302
  const capacity = assessCapacity(activePRs, stateManager.getState().config.maxActivePRs, shelvedPRs.length);
@@ -357,7 +361,7 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
357
361
  warn(MODULE, `Failed to persist auto-undismissed state: ${errorMessage(error)}`);
358
362
  }
359
363
  }
360
- const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls);
364
+ const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls, previousLastDigestAt);
361
365
  digest.summary.totalNeedingAttention = actionableIssues.length;
362
366
  const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
363
367
  const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues);
@@ -425,10 +429,13 @@ async function executeDailyCheckInternal(token) {
425
429
  await updateRepoScores(prMonitor, prs, mergedCounts, closedCounts);
426
430
  // Phase 3: Persist monthly analytics
427
431
  updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
432
+ // Capture lastDigestAt BEFORE Phase 4 overwrites it with the current run's timestamp.
433
+ // Used by collectActionableIssues to determine which PRs are "new" (created since last digest).
434
+ const previousLastDigestAt = getStateManager().getState().lastDigestAt;
428
435
  // Phase 4: Expire snoozes, partition PRs, generate and save digest
429
436
  const { activePRs, shelvedPRs, digest } = partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs);
430
437
  // Phase 5: Build structured output (capacity, dismiss filter, action menu)
431
- return generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures);
438
+ return generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, failures, previousLastDigestAt);
432
439
  }
433
440
  /**
434
441
  * Run the daily check and return deduplicated DailyOutput.
@@ -8,7 +8,7 @@
8
8
  import * as http from 'http';
9
9
  import * as fs from 'fs';
10
10
  import * as path from 'path';
11
- import { getStateManager, getGitHubToken, getCLIVersion } from '../core/index.js';
11
+ import { getStateManager, getGitHubToken, getCLIVersion, applyStatusOverrides } from '../core/index.js';
12
12
  import { errorMessage, ValidationError } from '../core/errors.js';
13
13
  import { warn } from '../core/logger.js';
14
14
  import { validateUrl, validateGitHubUrl, PR_URL_PATTERN } from './validation.js';
@@ -33,40 +33,6 @@ const MIME_TYPES = {
33
33
  '.png': 'image/png',
34
34
  '.ico': 'image/x-icon',
35
35
  };
36
- /**
37
- * Apply status overrides from state to the PR list.
38
- * Overrides are auto-cleared if the PR has new activity since the override was set.
39
- */
40
- function applyStatusOverrides(prs, state) {
41
- const overrides = state.config.statusOverrides;
42
- if (!overrides || Object.keys(overrides).length === 0)
43
- return prs;
44
- const stateManager = getStateManager();
45
- // Snapshot keys before iteration — clearStatusOverride mutates the same object
46
- const overrideUrls = new Set(Object.keys(overrides));
47
- let didAutoClear = false;
48
- const result = prs.map((pr) => {
49
- const override = stateManager.getStatusOverride(pr.url, pr.updatedAt);
50
- if (!override) {
51
- if (overrideUrls.has(pr.url))
52
- didAutoClear = true;
53
- return pr;
54
- }
55
- if (override.status === pr.status)
56
- return pr;
57
- return { ...pr, status: override.status };
58
- });
59
- // Persist any auto-cleared overrides so they don't resurrect on restart
60
- if (didAutoClear) {
61
- try {
62
- stateManager.save();
63
- }
64
- catch (err) {
65
- warn(MODULE, `Failed to persist auto-cleared overrides — they may reappear on restart: ${errorMessage(err)}`);
66
- }
67
- }
68
- return result;
69
- }
70
36
  // ── Helpers ────────────────────────────────────────────────────────────────────
71
37
  /**
72
38
  * Build the JSON payload that the SPA expects from GET /api/data.
@@ -10,7 +10,7 @@
10
10
  * - formatActionHint — human-readable maintainer action hint label
11
11
  * - formatBriefSummary / formatSummary / printDigest — rendering
12
12
  */
13
- import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, DailyDigest, ShelvedPRRef, MaintainerActionHint, ComputedRepoSignals, RepoGroup, CommentedIssue, CommentedIssueWithResponse } from './types.js';
13
+ import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, DailyDigest, AgentState, ShelvedPRRef, MaintainerActionHint, ComputedRepoSignals, RepoGroup, CommentedIssue, CommentedIssueWithResponse } from './types.js';
14
14
  import type { CapacityAssessment, ActionableIssue, ActionMenu } from '../formatters/json.js';
15
15
  /**
16
16
  * Statuses indicating action needed from the contributor.
@@ -25,6 +25,7 @@ export declare const CRITICAL_STATUSES: ReadonlySet<FetchedPRStatus>;
25
25
  export declare const CRITICAL_ACTION_REASONS: ReadonlySet<ActionReason>;
26
26
  /** Staleness tiers indicating staleness — maintainer comments during these tiers don't count as responsive. */
27
27
  export declare const STALE_STATUSES: ReadonlySet<StalenessTier>;
28
+ export declare function applyStatusOverrides(prs: FetchedPR[], state: Readonly<AgentState>): FetchedPR[];
28
29
  /**
29
30
  * Map a full FetchedPR to a lightweight ShelvedPRRef for digest output.
30
31
  * Only the fields needed for display are retained, reducing JSON payload size.
@@ -53,7 +54,7 @@ export declare function assessCapacity(activePRs: FetchedPR[], maxActivePRs: num
53
54
  * Note: Recently closed PRs are informational only and excluded from this list.
54
55
  * They are available separately in digest.recentlyClosedPRs (#156).
55
56
  */
56
- export declare function collectActionableIssues(prs: FetchedPR[], snoozedUrls?: Set<string>): ActionableIssue[];
57
+ export declare function collectActionableIssues(prs: FetchedPR[], snoozedUrls?: Set<string>, lastDigestAt?: string): ActionableIssue[];
57
58
  /**
58
59
  * Format a maintainer action hint as a human-readable label
59
60
  */
@@ -12,6 +12,8 @@
12
12
  */
13
13
  import { formatRelativeTime } from './utils.js';
14
14
  import { warn } from './logger.js';
15
+ import { errorMessage } from './errors.js';
16
+ import { getStateManager } from './state.js';
15
17
  // ---------------------------------------------------------------------------
16
18
  // Constants
17
19
  // ---------------------------------------------------------------------------
@@ -34,6 +36,62 @@ export const CRITICAL_ACTION_REASONS = new Set([
34
36
  /** Staleness tiers indicating staleness — maintainer comments during these tiers don't count as responsive. */
35
37
  export const STALE_STATUSES = new Set(['dormant', 'approaching_dormant']);
36
38
  // ---------------------------------------------------------------------------
39
+ // Status overrides
40
+ // ---------------------------------------------------------------------------
41
+ /**
42
+ * Apply status overrides from state to the PR list.
43
+ * Overrides are auto-cleared if the PR has new activity since the override was set.
44
+ *
45
+ * When an override changes the status, the contradictory reason field is cleared
46
+ * and an appropriate default is set so downstream logic (assessCapacity, collectActionableIssues)
47
+ * works correctly.
48
+ */
49
+ const VALID_OVERRIDE_STATUSES = new Set(['needs_addressing', 'waiting_on_maintainer']);
50
+ export function applyStatusOverrides(prs, state) {
51
+ const overrides = state.config.statusOverrides;
52
+ if (!overrides || Object.keys(overrides).length === 0)
53
+ return prs;
54
+ const stateManager = getStateManager();
55
+ // Snapshot keys before iteration — clearStatusOverride mutates the same object
56
+ const overrideUrls = new Set(Object.keys(overrides));
57
+ let didAutoClear = false;
58
+ const result = prs.map((pr) => {
59
+ try {
60
+ const override = stateManager.getStatusOverride(pr.url, pr.updatedAt);
61
+ if (!override) {
62
+ if (overrideUrls.has(pr.url))
63
+ didAutoClear = true;
64
+ return pr;
65
+ }
66
+ if (!VALID_OVERRIDE_STATUSES.has(override.status)) {
67
+ warn('daily-logic', `Invalid override status "${override.status}" for ${pr.url} — ignoring`);
68
+ return pr;
69
+ }
70
+ if (override.status === pr.status)
71
+ return pr;
72
+ // Clear the contradictory reason field and set an appropriate default
73
+ if (override.status === 'waiting_on_maintainer') {
74
+ return { ...pr, status: override.status, actionReason: undefined, waitReason: 'pending_review' };
75
+ }
76
+ return { ...pr, status: override.status, waitReason: undefined, actionReason: 'needs_response' };
77
+ }
78
+ catch (err) {
79
+ warn('daily-logic', `Failed to apply status override for ${pr.url}: ${errorMessage(err)}`);
80
+ return pr;
81
+ }
82
+ });
83
+ // Persist any auto-cleared overrides so they don't resurrect on restart
84
+ if (didAutoClear) {
85
+ try {
86
+ stateManager.save();
87
+ }
88
+ catch (err) {
89
+ warn('daily-logic', `Failed to persist auto-cleared overrides — they may reappear on restart: ${errorMessage(err)}`);
90
+ }
91
+ }
92
+ return result;
93
+ }
94
+ // ---------------------------------------------------------------------------
37
95
  // Internal helpers
38
96
  // ---------------------------------------------------------------------------
39
97
  /**
@@ -140,9 +198,10 @@ export function assessCapacity(activePRs, maxActivePRs, shelvedPRCount) {
140
198
  * Note: Recently closed PRs are informational only and excluded from this list.
141
199
  * They are available separately in digest.recentlyClosedPRs (#156).
142
200
  */
143
- export function collectActionableIssues(prs, snoozedUrls = new Set()) {
201
+ export function collectActionableIssues(prs, snoozedUrls = new Set(), lastDigestAt) {
144
202
  const issues = [];
145
203
  const actionPRs = prs.filter((pr) => pr.status === 'needs_addressing');
204
+ const lastDigestTime = lastDigestAt ? new Date(lastDigestAt).getTime() : NaN;
146
205
  const reasonOrder = [
147
206
  'needs_response',
148
207
  'needs_changes',
@@ -191,7 +250,18 @@ export function collectActionableIssues(prs, snoozedUrls = new Set()) {
191
250
  label = `[${reason}]`;
192
251
  type = 'needs_response';
193
252
  }
194
- issues.push({ type, pr, label });
253
+ // A PR is "new" if it was created after the last daily digest (first time seen).
254
+ // If there's no previous digest (first run) or createdAt is invalid, assume new.
255
+ const createdTime = new Date(pr.createdAt).getTime();
256
+ let isNewContribution;
257
+ if (isNaN(createdTime)) {
258
+ warn('daily-logic', `Invalid createdAt "${pr.createdAt}" for PR ${pr.url}, assuming new contribution`);
259
+ isNewContribution = true;
260
+ }
261
+ else {
262
+ isNewContribution = isNaN(lastDigestTime) || createdTime > lastDigestTime;
263
+ }
264
+ issues.push({ type, pr, label, isNewContribution });
195
265
  }
196
266
  }
197
267
  return issues;
@@ -12,7 +12,7 @@ export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDa
12
12
  export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
13
13
  export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
14
14
  export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
15
- export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
15
+ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
16
16
  export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
17
17
  export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
18
18
  export * from './types.js';
@@ -12,7 +12,7 @@ export { parseGitHubUrl, daysBetween, splitRepo, isOwnRepo, getCLIVersion, getDa
12
12
  export { OssAutopilotError, ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, } from './errors.js';
13
13
  export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
14
14
  export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
15
- export { CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
15
+ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
16
16
  export { computeContributionStats } from './stats.js';
17
17
  export { fetchPRTemplate } from './pr-template.js';
18
18
  export * from './types.js';
@@ -25,6 +25,8 @@ export interface ActionableIssue {
25
25
  type: ActionableIssueType;
26
26
  pr: FetchedPR;
27
27
  label: string;
28
+ /** True if the PR was created after the last daily digest (first time seen). */
29
+ isNewContribution: boolean;
28
30
  }
29
31
  /**
30
32
  * Compact version of ActionableIssue for JSON output.
@@ -36,6 +38,8 @@ export interface CompactActionableIssue {
36
38
  type: ActionableIssueType;
37
39
  prUrl: string;
38
40
  label: string;
41
+ /** True if the PR was created after the last daily digest (first time seen). */
42
+ isNewContribution: boolean;
39
43
  }
40
44
  /**
41
45
  * A single action menu item pre-computed by the CLI.
@@ -30,6 +30,7 @@ export function compactActionableIssues(issues) {
30
30
  type: issue.type,
31
31
  prUrl: issue.pr.url,
32
32
  label: issue.label,
33
+ isNewContribution: issue.isNewContribution,
33
34
  }));
34
35
  }
35
36
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {