@oss-autopilot/core 0.44.3 → 0.44.16

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.
@@ -13,29 +13,15 @@ import { generateDashboardScripts } from './dashboard-scripts.js';
13
13
  export { escapeHtml } from './dashboard-formatters.js';
14
14
  export { buildDashboardStats } from './dashboard-data.js';
15
15
  export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses = []) {
16
- const approachingDormantDays = state.config?.approachingDormantDays ?? 25;
17
16
  const shelvedPRs = digest.shelvedPRs || [];
18
17
  const autoUnshelvedPRs = digest.autoUnshelvedPRs || [];
19
18
  const recentlyMerged = digest.recentlyMergedPRs || [];
20
19
  const shelvedUrls = new Set(shelvedPRs.map((pr) => pr.url));
21
20
  const activePRList = (digest.openPRs || []).filter((pr) => !shelvedUrls.has(pr.url));
22
21
  // Action Required: contributor must do something
23
- const actionRequired = [
24
- ...(digest.prsNeedingResponse || []),
25
- ...(digest.needsChangesPRs || []),
26
- ...(digest.ciFailingPRs || []),
27
- ...(digest.mergeConflictPRs || []),
28
- ...(digest.incompleteChecklistPRs || []),
29
- ...(digest.missingRequiredFilesPRs || []),
30
- ...(digest.needsRebasePRs || []),
31
- ];
22
+ const actionRequired = digest.needsAddressingPRs || [];
32
23
  // Waiting on Others: informational, no contributor action needed
33
- const waitingOnOthers = [
34
- ...(digest.changesAddressedPRs || []),
35
- ...(digest.waitingOnMaintainerPRs || []),
36
- ...(digest.ciBlockedPRs || []),
37
- ...(digest.ciNotRunningPRs || []),
38
- ];
24
+ const waitingOnOthers = digest.waitingOnMaintainerPRs || [];
39
25
  return `<!DOCTYPE html>
40
26
  <html lang="en">
41
27
  <head>
@@ -127,22 +113,12 @@ export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, month
127
113
  <input type="text" class="filter-search" id="searchInput" placeholder="Search by PR title..." />
128
114
  <select class="filter-select" id="statusFilter">
129
115
  <option value="all">All Statuses</option>
130
- <option value="needs-response">Needs Response</option>
131
- <option value="needs-changes">Needs Changes</option>
132
- <option value="ci-failing">CI Failing</option>
133
- <option value="conflict">Merge Conflict</option>
134
- <option value="changes-addressed">Changes Addressed</option>
135
- <option value="waiting-maintainer">Waiting on Maintainer</option>
136
- <option value="ci-blocked">CI Blocked</option>
137
- <option value="ci-not-running">CI Not Running</option>
138
- <option value="incomplete-checklist">Incomplete Checklist</option>
139
- <option value="missing-files">Missing Files</option>
140
- <option value="needs-rebase">Needs Rebase</option>
116
+ <option value="needs-addressing">Needs Addressing</option>
117
+ <option value="waiting-on-maintainer">Waiting on Maintainer</option>
141
118
  <option value="shelved">Shelved</option>
142
119
  <option value="merged">Recently Merged</option>
143
120
  <option value="closed">Recently Closed</option>
144
121
  <option value="auto-unshelved">Auto-Unshelved</option>
145
- <option value="active">Active (No Issues)</option>
146
122
  </select>
147
123
  <select class="filter-select" id="repoFilter">
148
124
  <option value="all">All Repositories</option>
@@ -184,15 +160,7 @@ export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, month
184
160
  <span class="health-badge">${actionRequired.length} issue${actionRequired.length !== 1 ? 's' : ''}</span>
185
161
  </div>
186
162
  <div class="health-items">
187
- ${renderHealthItems(digest.prsNeedingResponse || [], 'needs-response', SVG_ICONS.comment, 'Needs Response', (pr) => pr.lastMaintainerComment
188
- ? `@${escapeHtml(pr.lastMaintainerComment.author)}: ${truncateTitle(pr.lastMaintainerComment.body, 40)}`
189
- : truncateTitle(pr.title))}
190
- ${renderHealthItems(digest.needsChangesPRs || [], 'needs-changes', SVG_ICONS.edit, 'Needs Changes', titleMeta)}
191
- ${renderHealthItems(digest.ciFailingPRs || [], 'ci-failing', SVG_ICONS.xCircle, 'CI Failing', titleMeta)}
192
- ${renderHealthItems(digest.mergeConflictPRs || [], 'conflict', SVG_ICONS.conflict, 'Merge Conflict', titleMeta)}
193
- ${renderHealthItems(digest.incompleteChecklistPRs || [], 'incomplete-checklist', SVG_ICONS.checklist, (pr) => `Incomplete Checklist${pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : ''}`, titleMeta)}
194
- ${renderHealthItems(digest.missingRequiredFilesPRs || [], 'missing-files', SVG_ICONS.file, 'Missing Required Files', (pr) => (pr.missingRequiredFiles ? escapeHtml(pr.missingRequiredFiles.join(', ')) : truncateTitle(pr.title)))}
195
- ${renderHealthItems(digest.needsRebasePRs || [], 'needs-rebase', SVG_ICONS.refresh, (pr) => `Needs Rebase${pr.commitsBehindUpstream ? ` (${pr.commitsBehindUpstream} behind)` : ''}`, titleMeta)}
163
+ ${renderHealthItems(actionRequired, 'needs-addressing', SVG_ICONS.xCircle, (pr) => pr.displayLabel, (pr) => escapeHtml(pr.displayDescription))}
196
164
  </div>
197
165
  </section>
198
166
  `
@@ -210,10 +178,7 @@ export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, month
210
178
  <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${waitingOnOthers.length} PR${waitingOnOthers.length !== 1 ? 's' : ''}</span>
211
179
  </div>
212
180
  <div class="health-items">
213
- ${renderHealthItems(digest.changesAddressedPRs || [], 'changes-addressed', SVG_ICONS.checkCircle, 'Changes Addressed', (pr) => `Awaiting re-review${pr.lastMaintainerComment ? ` from @${escapeHtml(pr.lastMaintainerComment.author)}` : ''}`)}
214
- ${renderHealthItems(digest.waitingOnMaintainerPRs || [], 'waiting-maintainer', SVG_ICONS.clock, 'Waiting on Maintainer', titleMeta)}
215
- ${renderHealthItems(digest.ciBlockedPRs || [], 'ci-blocked', SVG_ICONS.lock, 'CI Blocked', titleMeta)}
216
- ${renderHealthItems(digest.ciNotRunningPRs || [], 'ci-not-running', SVG_ICONS.infoCircle, 'CI Not Running', titleMeta)}
181
+ ${renderHealthItems(waitingOnOthers, 'waiting-on-maintainer', SVG_ICONS.clock, (pr) => pr.displayLabel, (pr) => escapeHtml(pr.displayDescription))}
217
182
  </div>
218
183
  </section>
219
184
  `
@@ -230,7 +195,7 @@ export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, month
230
195
  <h2>Health Status</h2>
231
196
  </div>
232
197
  <div class="health-empty">
233
- All PRs are healthy - no CI failures, conflicts, or pending responses
198
+ All PRs are on track - no CI failures, conflicts, or pending responses
234
199
  </div>
235
200
  </section>
236
201
  `
@@ -313,7 +278,7 @@ export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, month
313
278
  <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${autoUnshelvedPRs.length} unshelved</span>
314
279
  </div>
315
280
  <div class="health-items">
316
- ${renderHealthItems(autoUnshelvedPRs, 'auto-unshelved', SVG_ICONS.bell, (pr) => 'Auto-Unshelved (' + pr.status.replace(/_/g, ' ') + ')', titleMeta)}
281
+ ${renderHealthItems(autoUnshelvedPRs, 'auto-unshelved', SVG_ICONS.bell, (pr) => `Auto-Unshelved (${pr.status.replace(/_/g, ' ')})`, titleMeta)}
317
282
  </div>
318
283
  </section>
319
284
  `
@@ -396,27 +361,14 @@ export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, month
396
361
  <div class="pr-list">
397
362
  ${activePRList
398
363
  .map((pr) => {
399
- const hasIssues = pr.ciStatus === 'failing' ||
400
- pr.hasMergeConflict ||
401
- (pr.hasUnrespondedComment && pr.status !== 'changes_addressed') ||
402
- pr.status === 'needs_changes';
403
- const isStale = pr.daysSinceActivity >= approachingDormantDays;
404
- const itemClass = hasIssues ? 'has-issues' : isStale ? 'stale' : '';
405
- const prStatus = pr.ciStatus === 'failing'
406
- ? 'ci-failing'
407
- : pr.hasMergeConflict
408
- ? 'conflict'
409
- : pr.hasUnrespondedComment && pr.status !== 'changes_addressed' && pr.status !== 'failing_ci'
410
- ? 'needs-response'
411
- : pr.status === 'needs_changes'
412
- ? 'needs-changes'
413
- : pr.status === 'changes_addressed'
414
- ? 'changes-addressed'
415
- : 'active';
364
+ const isNeedsAddressing = pr.status === 'needs_addressing';
365
+ const isStale = pr.stalenessTier !== 'active';
366
+ const itemClass = isNeedsAddressing ? 'has-issues' : isStale ? 'stale' : '';
367
+ const prStatus = pr.status === 'needs_addressing' ? 'needs-addressing' : 'waiting-on-maintainer';
416
368
  return `
417
369
  <div class="pr-item ${itemClass}" data-status="${prStatus}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
418
370
  <div class="pr-status-indicator">
419
- ${hasIssues
371
+ ${isNeedsAddressing
420
372
  ? `
421
373
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
422
374
  <circle cx="12" cy="12" r="10"/>
@@ -438,13 +390,7 @@ export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, month
438
390
  <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
439
391
  </div>
440
392
  <div class="pr-badges">
441
- ${pr.ciStatus === 'failing' ? '<span class="badge badge-ci-failing">CI Failing</span>' : ''}
442
- ${pr.ciStatus === 'passing' ? '<span class="badge badge-passing">CI Passing</span>' : ''}
443
- ${pr.ciStatus === 'pending' ? '<span class="badge badge-pending">CI Pending</span>' : ''}
444
- ${pr.hasMergeConflict ? '<span class="badge badge-conflict">Merge Conflict</span>' : ''}
445
- ${pr.hasUnrespondedComment && pr.status === 'changes_addressed' ? '<span class="badge badge-changes-addressed">Changes Addressed</span>' : ''}
446
- ${pr.hasUnrespondedComment && pr.status !== 'changes_addressed' && pr.status !== 'failing_ci' ? '<span class="badge badge-needs-response">Needs Response</span>' : ''}
447
- ${pr.reviewDecision === 'changes_requested' ? '<span class="badge badge-changes-requested">Changes Requested</span>' : ''}
393
+ <span class="badge ${isNeedsAddressing ? 'badge-ci-failing' : 'badge-passing'}">${escapeHtml(pr.displayLabel)}</span>
448
394
  ${isStale ? `<span class="badge badge-stale">${pr.daysSinceActivity}d inactive</span>` : ''}
449
395
  </div>
450
396
  </div>
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Override command
3
+ * Manually override a PR's status (needs_addressing ↔ waiting_on_maintainer).
4
+ * Overrides auto-clear when the PR has new activity.
5
+ */
6
+ import type { FetchedPRStatus } from '../core/types.js';
7
+ export interface OverrideOutput {
8
+ url: string;
9
+ status: FetchedPRStatus;
10
+ }
11
+ export interface ClearOverrideOutput {
12
+ url: string;
13
+ cleared: boolean;
14
+ }
15
+ export declare function runOverride(options: {
16
+ prUrl: string;
17
+ status: string;
18
+ }): Promise<OverrideOutput>;
19
+ export declare function runClearOverride(options: {
20
+ prUrl: string;
21
+ }): Promise<ClearOverrideOutput>;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Override command
3
+ * Manually override a PR's status (needs_addressing ↔ waiting_on_maintainer).
4
+ * Overrides auto-clear when the PR has new activity.
5
+ */
6
+ import { getStateManager } from '../core/index.js';
7
+ import { PR_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
8
+ const VALID_STATUSES = ['needs_addressing', 'waiting_on_maintainer'];
9
+ export async function runOverride(options) {
10
+ validateUrl(options.prUrl);
11
+ validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
12
+ if (!VALID_STATUSES.includes(options.status)) {
13
+ throw new Error(`Invalid status "${options.status}". Must be one of: ${VALID_STATUSES.join(', ')}`);
14
+ }
15
+ const status = options.status;
16
+ const stateManager = getStateManager();
17
+ // Use current time as lastActivityAt — the CLI doesn't have cached PR data.
18
+ // This means the override will auto-clear on the next daily run if the PR's
19
+ // updatedAt is after this timestamp (which is the desired behavior: the override
20
+ // will persist until new activity occurs on the PR).
21
+ const lastActivityAt = new Date().toISOString();
22
+ stateManager.setStatusOverride(options.prUrl, status, lastActivityAt);
23
+ stateManager.save();
24
+ return { url: options.prUrl, status };
25
+ }
26
+ export async function runClearOverride(options) {
27
+ validateUrl(options.prUrl);
28
+ validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
29
+ const stateManager = getStateManager();
30
+ const cleared = stateManager.clearStatusOverride(options.prUrl);
31
+ if (cleared) {
32
+ stateManager.save();
33
+ }
34
+ return { url: options.prUrl, cleared };
35
+ }
@@ -10,17 +10,21 @@
10
10
  * - formatActionHint — human-readable maintainer action hint label
11
11
  * - formatBriefSummary / formatSummary / printDigest — rendering
12
12
  */
13
- import type { FetchedPR, FetchedPRStatus, DailyDigest, ShelvedPRRef, MaintainerActionHint, ComputedRepoSignals, RepoGroup, CommentedIssue, CommentedIssueWithResponse } from './types.js';
13
+ import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, DailyDigest, ShelvedPRRef, MaintainerActionHint, ComputedRepoSignals, RepoGroup, CommentedIssue, CommentedIssueWithResponse } from './types.js';
14
14
  import type { CapacityAssessment, ActionableIssue, ActionMenu } from '../formatters/json.js';
15
15
  /**
16
- * Statuses indicating maintainer engagement or action needed from the contributor.
17
- * Used both for auto-unshelving shelved PRs and for counting critical issues in capacity assessment.
16
+ * Statuses indicating action needed from the contributor.
17
+ * Used for auto-unshelving shelved PRs.
18
18
  */
19
19
  export declare const CRITICAL_STATUSES: ReadonlySet<FetchedPRStatus>;
20
- /** Statuses indicating active maintainer engagement (reviews, feedback, merges). */
21
- export declare const ACTIVE_MAINTAINER_STATUSES: ReadonlySet<FetchedPRStatus>;
22
- /** Statuses indicating stalenessmaintainer comments during these statuses don't count as responsive. */
23
- export declare const STALE_STATUSES: ReadonlySet<FetchedPRStatus>;
20
+ /**
21
+ * ActionReason values that indicate high-priority issues blocking capacity.
22
+ * `incomplete_checklist` is excluded it's actionable but not blocking.
23
+ * Used in capacity assessment to determine if user can take on new work.
24
+ */
25
+ export declare const CRITICAL_ACTION_REASONS: ReadonlySet<ActionReason>;
26
+ /** Staleness tiers indicating staleness — maintainer comments during these tiers don't count as responsive. */
27
+ export declare const STALE_STATUSES: ReadonlySet<StalenessTier>;
24
28
  /**
25
29
  * Map a full FetchedPR to a lightweight ShelvedPRRef for digest output.
26
30
  * Only the fields needed for display are retained, reducing JSON payload size.
@@ -33,9 +37,8 @@ export declare function toShelvedPRRef(pr: FetchedPR): ShelvedPRRef;
33
37
  export declare function groupPRsByRepo(prs: FetchedPR[]): RepoGroup[];
34
38
  /**
35
39
  * Compute per-repo maintainer signals from observed open PR data.
36
- * - isResponsive: true if any PR in the repo has a maintainer comment and status
37
- * is not in STALE_STATUSES
38
- * - hasActiveMaintainers: true if any PR in the repo has a status in ACTIVE_MAINTAINER_STATUSES
40
+ * - isResponsive: true if any PR in the repo has a maintainer comment and stalenessTier is active
41
+ * - hasActiveMaintainers: true if any non-stale PR exists in the repo (stalenessTier is 'active')
39
42
  */
40
43
  export declare function computeRepoSignals(prs: FetchedPR[]): Map<string, ComputedRepoSignals>;
41
44
  /**
@@ -16,24 +16,22 @@ import { warn } from './logger.js';
16
16
  // Constants
17
17
  // ---------------------------------------------------------------------------
18
18
  /**
19
- * Statuses indicating maintainer engagement or action needed from the contributor.
20
- * Used both for auto-unshelving shelved PRs and for counting critical issues in capacity assessment.
19
+ * Statuses indicating action needed from the contributor.
20
+ * Used for auto-unshelving shelved PRs.
21
21
  */
22
- export const CRITICAL_STATUSES = new Set([
22
+ export const CRITICAL_STATUSES = new Set(['needs_addressing']);
23
+ /**
24
+ * ActionReason values that indicate high-priority issues blocking capacity.
25
+ * `incomplete_checklist` is excluded — it's actionable but not blocking.
26
+ * Used in capacity assessment to determine if user can take on new work.
27
+ */
28
+ export const CRITICAL_ACTION_REASONS = new Set([
23
29
  'needs_response',
24
30
  'needs_changes',
25
31
  'failing_ci',
26
32
  'merge_conflict',
27
33
  ]);
28
- /** Statuses indicating active maintainer engagement (reviews, feedback, merges). */
29
- export const ACTIVE_MAINTAINER_STATUSES = new Set([
30
- 'healthy',
31
- 'waiting_on_maintainer',
32
- 'changes_addressed',
33
- 'needs_response',
34
- 'needs_changes',
35
- ]);
36
- /** Statuses indicating staleness — maintainer comments during these statuses don't count as responsive. */
34
+ /** Staleness tiers indicating staleness maintainer comments during these tiers don't count as responsive. */
37
35
  export const STALE_STATUSES = new Set(['dormant', 'approaching_dormant']);
38
36
  // ---------------------------------------------------------------------------
39
37
  // Internal helpers
@@ -86,16 +84,15 @@ export function groupPRsByRepo(prs) {
86
84
  }
87
85
  /**
88
86
  * Compute per-repo maintainer signals from observed open PR data.
89
- * - isResponsive: true if any PR in the repo has a maintainer comment and status
90
- * is not in STALE_STATUSES
91
- * - hasActiveMaintainers: true if any PR in the repo has a status in ACTIVE_MAINTAINER_STATUSES
87
+ * - isResponsive: true if any PR in the repo has a maintainer comment and stalenessTier is active
88
+ * - hasActiveMaintainers: true if any non-stale PR exists in the repo (stalenessTier is 'active')
92
89
  */
93
90
  export function computeRepoSignals(prs) {
94
91
  const repoMap = buildRepoMap(prs, 'COMPUTE_SIGNALS');
95
92
  const result = new Map();
96
93
  for (const [repo, repoPRs] of repoMap) {
97
- const isResponsive = repoPRs.some((pr) => pr.lastMaintainerComment && !STALE_STATUSES.has(pr.status));
98
- const hasActiveMaintainers = repoPRs.some((pr) => ACTIVE_MAINTAINER_STATUSES.has(pr.status));
94
+ const isResponsive = repoPRs.some((pr) => pr.lastMaintainerComment && !STALE_STATUSES.has(pr.stalenessTier));
95
+ const hasActiveMaintainers = repoPRs.some((pr) => !STALE_STATUSES.has(pr.stalenessTier));
99
96
  result.set(repo, { isResponsive, hasActiveMaintainers });
100
97
  }
101
98
  return result;
@@ -106,7 +103,7 @@ export function computeRepoSignals(prs) {
106
103
  */
107
104
  export function assessCapacity(activePRs, maxActivePRs, shelvedPRCount) {
108
105
  const activePRCount = activePRs.length;
109
- const criticalIssueCount = activePRs.filter((pr) => CRITICAL_STATUSES.has(pr.status)).length;
106
+ const criticalIssueCount = activePRs.filter((pr) => pr.status === 'needs_addressing' && pr.actionReason && CRITICAL_ACTION_REASONS.has(pr.actionReason)).length;
110
107
  // Has capacity if: under PR limit AND no critical issues
111
108
  const underPRLimit = activePRCount < maxActivePRs;
112
109
  const noCriticalIssues = criticalIssueCount === 0;
@@ -145,37 +142,56 @@ export function assessCapacity(activePRs, maxActivePRs, shelvedPRCount) {
145
142
  */
146
143
  export function collectActionableIssues(prs, snoozedUrls = new Set()) {
147
144
  const issues = [];
148
- // 1. Needs Response (highest priority - someone is waiting for you)
149
- for (const pr of prs) {
150
- if (pr.status === 'needs_response') {
151
- issues.push({ type: 'needs_response', pr, label: '[Needs Response]' });
152
- }
153
- }
154
- // 2. Needs Changes (review requested changes, contributor hasn't pushed new code)
155
- for (const pr of prs) {
156
- if (pr.status === 'needs_changes') {
157
- issues.push({ type: 'needs_changes', pr, label: '[Needs Changes]' });
158
- }
159
- }
160
- // 3. CI Failing (include check names so user can distinguish real CI from validation bots)
161
- // Skip snoozed PRs — their CI failures are known and temporarily dismissed
162
- for (const pr of prs) {
163
- if (pr.status === 'failing_ci' && !snoozedUrls.has(pr.url)) {
164
- const checkInfo = pr.failingCheckNames.length > 0 ? ` (${pr.failingCheckNames.join(', ')})` : '';
165
- issues.push({ type: 'ci_failing', pr, label: `[CI Failing${checkInfo}]` });
166
- }
167
- }
168
- // 4. Merge Conflicts
169
- for (const pr of prs) {
170
- if (pr.status === 'merge_conflict') {
171
- issues.push({ type: 'merge_conflict', pr, label: '[Merge Conflict]' });
172
- }
173
- }
174
- // 5. Incomplete Checklist
175
- for (const pr of prs) {
176
- if (pr.status === 'incomplete_checklist') {
177
- const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : '';
178
- issues.push({ type: 'incomplete_checklist', pr, label: `[Incomplete Checklist${stats}]` });
145
+ const actionPRs = prs.filter((pr) => pr.status === 'needs_addressing');
146
+ const reasonOrder = [
147
+ 'needs_response',
148
+ 'needs_changes',
149
+ 'failing_ci',
150
+ 'merge_conflict',
151
+ 'incomplete_checklist',
152
+ ];
153
+ for (const reason of reasonOrder) {
154
+ for (const pr of actionPRs) {
155
+ if (pr.actionReason !== reason)
156
+ continue;
157
+ if (reason === 'failing_ci' && snoozedUrls.has(pr.url))
158
+ continue;
159
+ let label;
160
+ let type;
161
+ switch (reason) {
162
+ case 'needs_response':
163
+ label = '[Needs Response]';
164
+ type = 'needs_response';
165
+ break;
166
+ case 'needs_changes':
167
+ label = '[Needs Changes]';
168
+ type = 'needs_changes';
169
+ break;
170
+ case 'failing_ci': {
171
+ const checkInfo = pr.failingCheckNames.length > 0 ? ` (${pr.failingCheckNames.join(', ')})` : '';
172
+ label = `[CI Failing${checkInfo}]`;
173
+ type = 'ci_failing';
174
+ break;
175
+ }
176
+ case 'merge_conflict':
177
+ label = '[Merge Conflict]';
178
+ type = 'merge_conflict';
179
+ break;
180
+ case 'incomplete_checklist': {
181
+ const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : '';
182
+ label = `[Incomplete Checklist${stats}]`;
183
+ type = 'incomplete_checklist';
184
+ break;
185
+ }
186
+ default:
187
+ // Defensive fallback for ActionReason values not explicitly handled
188
+ // above (e.g. ci_not_running, needs_rebase, missing_required_files).
189
+ // These aren't in reasonOrder today but this guards future additions.
190
+ warn('daily-logic', `Unhandled ActionReason "${reason}" for PR ${pr.url} — falling back to needs_response`);
191
+ label = `[${reason}]`;
192
+ type = 'needs_response';
193
+ }
194
+ issues.push({ type, pr, label });
179
195
  }
180
196
  }
181
197
  return issues;
@@ -252,7 +268,7 @@ export function computeActionMenu(actionableIssues, capacity, commentedIssues =
252
268
  * Format a brief one-liner summary for the action-first flow
253
269
  */
254
270
  export function formatBriefSummary(digest, issueCount, issueResponseCount = 0) {
255
- const attentionText = issueCount > 0 ? `${issueCount} need${issueCount === 1 ? 's' : ''} attention` : 'all healthy';
271
+ const attentionText = issueCount > 0 ? `${issueCount} need${issueCount === 1 ? 's' : ''} attention` : 'all on track';
256
272
  const issueReplyText = issueResponseCount > 0 ? ` | ${issueResponseCount} issue repl${issueResponseCount === 1 ? 'y' : 'ies'}` : '';
257
273
  return `\u{1F4CA} ${digest.summary.totalActivePRs} Active PRs | ${attentionText}${issueReplyText}`;
258
274
  }
@@ -267,80 +283,21 @@ export function formatSummary(digest, capacity, issueResponses = []) {
267
283
  lines.push(`\u{1F4CA} **${digest.summary.totalActivePRs} Active PRs** | ${digest.summary.totalMergedAllTime} Merged | ${digest.summary.mergeRate}% Merge Rate`);
268
284
  lines.push('\u2713 Dashboard generated \u2014 say "open dashboard" to view in browser');
269
285
  lines.push('');
270
- // CI Failing
271
- if (digest.ciFailingPRs.length > 0) {
272
- lines.push('### \u274C CI Failing');
273
- for (const pr of digest.ciFailingPRs) {
274
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
275
- if (pr.failingCheckNames.length > 0) {
276
- lines.push(` \u2514\u2500 Failing: ${pr.failingCheckNames.join(', ')}`);
277
- }
278
- }
279
- lines.push('');
280
- }
281
- // Merge Conflicts
282
- if (digest.mergeConflictPRs.length > 0) {
283
- lines.push('### \u26A0\uFE0F Merge Conflicts');
284
- for (const pr of digest.mergeConflictPRs) {
285
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
286
- }
287
- lines.push('');
288
- }
289
- // Needs Response
290
- if (digest.prsNeedingResponse.length > 0) {
291
- lines.push('### \u{1F4AC} Needs Response');
292
- for (const pr of digest.prsNeedingResponse) {
293
- const maintainer = pr.lastMaintainerComment?.author || 'maintainer';
294
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
295
- lines.push(` \u2514\u2500 @${maintainer} commented`);
296
- if (pr.maintainerActionHints.length > 0) {
297
- const hintLabels = pr.maintainerActionHints.map(formatActionHint).join(', ');
298
- lines.push(` \u2514\u2500 Action: ${hintLabels}`);
299
- }
300
- }
301
- lines.push('');
302
- }
303
- // Needs Changes (review requested changes, no new commits yet)
304
- if (digest.needsChangesPRs.length > 0) {
305
- lines.push('### \u{1F527} Needs Changes');
306
- for (const pr of digest.needsChangesPRs) {
286
+ // Needs Addressing
287
+ if (digest.needsAddressingPRs.length > 0) {
288
+ lines.push('### \u274C Needs Addressing');
289
+ for (const pr of digest.needsAddressingPRs) {
307
290
  lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
308
- lines.push(` \u2514\u2500 Review requested changes \u2014 push commits to address`);
291
+ lines.push(` \u2514\u2500 ${pr.displayLabel} ${pr.displayDescription}`);
309
292
  }
310
293
  lines.push('');
311
294
  }
312
- // Incomplete Checklist
313
- if (digest.incompleteChecklistPRs.length > 0) {
314
- lines.push('### \u{1F4CB} Incomplete Checklist');
315
- for (const pr of digest.incompleteChecklistPRs) {
316
- const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total} checked)` : '';
317
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}${stats}`);
318
- }
319
- lines.push('');
320
- }
321
- // Changes Addressed (waiting for maintainer re-review)
322
- if (digest.changesAddressedPRs.length > 0) {
323
- lines.push('### \u{1F4E4} Changes Addressed');
324
- for (const pr of digest.changesAddressedPRs) {
325
- const maintainer = pr.lastMaintainerComment?.author || 'maintainer';
326
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
327
- lines.push(` \u2514\u2500 Waiting for @${maintainer} to re-review`);
328
- }
329
- lines.push('');
330
- }
331
- // Waiting on Maintainer (approved, no action needed from user)
295
+ // Waiting on Maintainer
332
296
  if (digest.waitingOnMaintainerPRs.length > 0) {
333
297
  lines.push('### \u23F3 Waiting on Maintainer');
334
298
  for (const pr of digest.waitingOnMaintainerPRs) {
335
- lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title} (approved)`);
336
- }
337
- lines.push('');
338
- }
339
- // Healthy PRs
340
- if (digest.healthyPRs.length > 0) {
341
- lines.push('### \u2705 Healthy');
342
- for (const pr of digest.healthyPRs) {
343
299
  lines.push(`- [${pr.repo}#${pr.number}](${pr.url}): ${pr.title}`);
300
+ lines.push(` \u2514\u2500 ${pr.displayDescription}`);
344
301
  }
345
302
  lines.push('');
346
303
  }
@@ -409,63 +366,19 @@ export function printDigest(digest, capacity, commentedIssues = []) {
409
366
  console.log(`Merge Rate: ${digest.summary.mergeRate}%`);
410
367
  console.log(`\nCapacity: ${capacity.hasCapacity ? '\u2705 Ready for new work' : '\u26A0\uFE0F Focus on existing work'}`);
411
368
  console.log(` ${capacity.reason}\n`);
412
- if (digest.ciFailingPRs.length > 0) {
413
- console.log('\u274C CI Failing:');
414
- for (const pr of digest.ciFailingPRs) {
415
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
416
- if (pr.failingCheckNames.length > 0) {
417
- console.log(` Failing: ${pr.failingCheckNames.join(', ')}`);
418
- }
419
- }
420
- console.log('');
421
- }
422
- if (digest.mergeConflictPRs.length > 0) {
423
- console.log('\u26A0\uFE0F Merge Conflicts:');
424
- for (const pr of digest.mergeConflictPRs) {
425
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
426
- }
427
- console.log('');
428
- }
429
- if (digest.prsNeedingResponse.length > 0) {
430
- console.log('\u{1F4AC} Needs Response:');
431
- for (const pr of digest.prsNeedingResponse) {
432
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
433
- if (pr.maintainerActionHints.length > 0) {
434
- const hintLabels = pr.maintainerActionHints.map(formatActionHint).join(', ');
435
- console.log(` Action: ${hintLabels}`);
436
- }
437
- }
438
- console.log('');
439
- }
440
- if (digest.needsChangesPRs.length > 0) {
441
- console.log('\u{1F527} Needs Changes:');
442
- for (const pr of digest.needsChangesPRs) {
369
+ if (digest.needsAddressingPRs.length > 0) {
370
+ console.log('\u274C Needs Addressing:');
371
+ for (const pr of digest.needsAddressingPRs) {
443
372
  console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
444
- console.log(` Review requested changes \u2014 push commits to address`);
445
- }
446
- console.log('');
447
- }
448
- if (digest.incompleteChecklistPRs.length > 0) {
449
- console.log('\u{1F4CB} Incomplete Checklist:');
450
- for (const pr of digest.incompleteChecklistPRs) {
451
- const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total} checked)` : '';
452
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}${stats}`);
453
- }
454
- console.log('');
455
- }
456
- if (digest.changesAddressedPRs.length > 0) {
457
- console.log('\u{1F4E4} Changes Addressed:');
458
- for (const pr of digest.changesAddressedPRs) {
459
- const maintainer = pr.lastMaintainerComment?.author || 'maintainer';
460
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
461
- console.log(` Waiting for @${maintainer} to re-review`);
373
+ console.log(` ${pr.displayLabel} ${pr.displayDescription}`);
462
374
  }
463
375
  console.log('');
464
376
  }
465
377
  if (digest.waitingOnMaintainerPRs.length > 0) {
466
378
  console.log('\u23F3 Waiting on Maintainer:');
467
379
  for (const pr of digest.waitingOnMaintainerPRs) {
468
- console.log(` - ${pr.repo}#${pr.number}: ${pr.title} (approved)`);
380
+ console.log(` - ${pr.repo}#${pr.number}: ${pr.title}`);
381
+ console.log(` ${pr.displayDescription}`);
469
382
  }
470
383
  console.log('');
471
384
  }
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Display Utils - Human-readable display label computation for PR statuses.
3
3
  * Extracted from PRMonitor to isolate presentation logic (#263).
4
+ *
5
+ * Uses two reason-keyed maps (ACTION_DISPLAY / WAIT_DISPLAY) instead of a
6
+ * single status-keyed map, reflecting the 2-status taxonomy where the
7
+ * granular reason lives in `actionReason` / `waitReason`.
4
8
  */
5
9
  import { FetchedPR } from './types.js';
6
10
  /** Compute display label and description for a FetchedPR (#79). */