@oss-autopilot/core 3.13.4 → 3.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +3 -3
  2. package/dist/cli-registry.js +59 -84
  3. package/dist/cli.bundle.cjs +112 -109
  4. package/dist/cli.js +5 -4
  5. package/dist/commands/comments.js +44 -10
  6. package/dist/commands/config.d.ts +2 -0
  7. package/dist/commands/config.js +50 -2
  8. package/dist/commands/curated-list.d.ts +17 -0
  9. package/dist/commands/curated-list.js +25 -0
  10. package/dist/commands/daily.d.ts +7 -1
  11. package/dist/commands/daily.js +136 -57
  12. package/dist/commands/dashboard-cache.d.ts +69 -0
  13. package/dist/commands/dashboard-cache.js +219 -0
  14. package/dist/commands/dashboard-data.d.ts +18 -10
  15. package/dist/commands/dashboard-data.js +58 -8
  16. package/dist/commands/dashboard-gist-sync.d.ts +93 -0
  17. package/dist/commands/dashboard-gist-sync.js +237 -0
  18. package/dist/commands/dashboard-server.d.ts +6 -10
  19. package/dist/commands/dashboard-server.js +181 -347
  20. package/dist/commands/features.js +6 -0
  21. package/dist/commands/guidelines.d.ts +6 -0
  22. package/dist/commands/guidelines.js +7 -0
  23. package/dist/commands/index.d.ts +2 -5
  24. package/dist/commands/index.js +2 -4
  25. package/dist/commands/init.d.ts +2 -0
  26. package/dist/commands/init.js +7 -1
  27. package/dist/commands/list-mark-done.js +6 -21
  28. package/dist/commands/list-move-tier.js +3 -5
  29. package/dist/commands/locate-issue-list.d.ts +25 -0
  30. package/dist/commands/locate-issue-list.js +67 -0
  31. package/dist/commands/merge-loop.d.ts +63 -0
  32. package/dist/commands/merge-loop.js +157 -0
  33. package/dist/commands/repo-vet.js +40 -1
  34. package/dist/commands/scout-bridge.d.ts +35 -2
  35. package/dist/commands/scout-bridge.js +65 -13
  36. package/dist/commands/search.d.ts +4 -6
  37. package/dist/commands/search.js +58 -11
  38. package/dist/commands/setup.d.ts +2 -0
  39. package/dist/commands/setup.js +56 -2
  40. package/dist/commands/skip-file-parser.d.ts +23 -0
  41. package/dist/commands/skip-file-parser.js +23 -10
  42. package/dist/commands/startup.d.ts +1 -6
  43. package/dist/commands/startup.js +25 -59
  44. package/dist/commands/track.d.ts +2 -2
  45. package/dist/commands/track.js +2 -2
  46. package/dist/commands/vet-list.d.ts +6 -6
  47. package/dist/commands/vet-list.js +194 -65
  48. package/dist/core/config-registry.js +36 -0
  49. package/dist/core/daily-logic.d.ts +25 -2
  50. package/dist/core/daily-logic.js +58 -3
  51. package/dist/core/gist-health.d.ts +81 -0
  52. package/dist/core/gist-health.js +39 -0
  53. package/dist/core/gist-state-store.d.ts +3 -1
  54. package/dist/core/gist-state-store.js +7 -2
  55. package/dist/core/github-stats.d.ts +2 -2
  56. package/dist/core/github-stats.js +20 -4
  57. package/dist/core/index.d.ts +5 -4
  58. package/dist/core/index.js +5 -4
  59. package/dist/core/issue-conversation.js +8 -2
  60. package/dist/core/issue-grading.d.ts +9 -0
  61. package/dist/core/issue-grading.js +9 -0
  62. package/dist/core/issue-verification.d.ts +39 -0
  63. package/dist/core/issue-verification.js +48 -0
  64. package/dist/core/pagination.d.ts +27 -0
  65. package/dist/core/pagination.js +23 -5
  66. package/dist/core/pr-comments-fetcher.d.ts +7 -0
  67. package/dist/core/pr-comments-fetcher.js +19 -8
  68. package/dist/core/pr-monitor.d.ts +2 -0
  69. package/dist/core/pr-monitor.js +26 -9
  70. package/dist/core/repo-score-manager.d.ts +2 -2
  71. package/dist/core/repo-score-manager.js +3 -3
  72. package/dist/core/repo-vet.d.ts +2 -2
  73. package/dist/core/repo-vet.js +1 -1
  74. package/dist/core/review-analysis.d.ts +19 -0
  75. package/dist/core/review-analysis.js +28 -0
  76. package/dist/core/state-schema.d.ts +43 -6
  77. package/dist/core/state-schema.js +81 -4
  78. package/dist/core/state.d.ts +36 -5
  79. package/dist/core/state.js +177 -28
  80. package/dist/core/strategy.js +6 -5
  81. package/dist/core/types.d.ts +8 -0
  82. package/dist/core/untrusted-content.d.ts +45 -0
  83. package/dist/core/untrusted-content.js +54 -0
  84. package/dist/formatters/json.d.ts +120 -12
  85. package/dist/formatters/json.js +55 -2
  86. package/package.json +2 -2
  87. package/dist/commands/shelve.d.ts +0 -45
  88. package/dist/commands/shelve.js +0 -54
@@ -522,7 +522,9 @@ export class GistStateStore {
522
522
  }
523
523
  /**
524
524
  * Re-fetch the Gist and update the in-memory cache.
525
- * Throttled to at most once per 30 seconds.
525
+ * Throttled to at most once per 30 seconds — ATTEMPTS, not just successes:
526
+ * a failed fetch stamps the throttle too (#1443), so an outage does not
527
+ * turn every SPA poll into an immediate full re-fetch.
526
528
  *
527
529
  * Returns a discriminated union so callers can tell apart the four
528
530
  * outcomes that previously collapsed into a single boolean (#1209 L9):
@@ -541,9 +543,12 @@ export class GistStateStore {
541
543
  if (sinceLastMs < GistStateStore.REFRESH_THROTTLE_MS) {
542
544
  return { status: 'throttled', sinceLastMs };
543
545
  }
546
+ // Stamp the ATTEMPT, success or failure (#1443): the success-only stamp
547
+ // let failed refreshes bypass the throttle entirely, contradicting the
548
+ // doc comment and hammering the API during an outage.
549
+ this.lastRefreshAt = now;
544
550
  try {
545
551
  await this.fetchAndCache(this.gistId);
546
- this.lastRefreshAt = now;
547
552
  this.lastRefreshError = null;
548
553
  return { status: 'refreshed' };
549
554
  }
@@ -45,7 +45,7 @@ export declare function fetchRecentlyMergedPRs(octokit: Octokit, config: {
45
45
  /**
46
46
  * Fetch merged PRs since a watermark date for incremental storage.
47
47
  * If no watermark is provided (first-ever fetch), fetches all merged PRs (up to pagination cap).
48
- * Returns StoredMergedPR[] (minimal: url, title, mergedAt) for state persistence.
48
+ * Returns StoredMergedPR[] (url, title, mergedAt, openedAt) for state persistence.
49
49
  */
50
50
  export declare function fetchMergedPRsSince(octokit: Octokit, config: {
51
51
  githubUsername: string;
@@ -53,7 +53,7 @@ export declare function fetchMergedPRsSince(octokit: Octokit, config: {
53
53
  /**
54
54
  * Fetch closed-without-merge PRs since a watermark date for incremental storage.
55
55
  * If no watermark is provided (first-ever fetch), fetches all closed PRs (up to pagination cap).
56
- * Returns StoredClosedPR[] (minimal: url, title, closedAt) for state persistence.
56
+ * Returns StoredClosedPR[] (url, title, closedAt, openedAt) for state persistence.
57
57
  * Uses `is:unmerged` to exclude merged PRs (which are also "closed" in GitHub's model).
58
58
  */
59
59
  export declare function fetchClosedPRsSince(octokit: Octokit, config: {
@@ -214,6 +214,8 @@ export async function fetchRecentlyClosedPRs(octokit, config, days = 7) {
214
214
  number,
215
215
  title: item.title,
216
216
  closedAt: item.closed_at || '',
217
+ // Free on the search result — feeds the outcome ledger (#1461).
218
+ openedAt: item.created_at || undefined,
217
219
  }));
218
220
  }
219
221
  /**
@@ -232,6 +234,8 @@ export async function fetchRecentlyMergedPRs(octokit, config, days = 7) {
232
234
  number,
233
235
  title: item.title,
234
236
  mergedAt: mergedAt || item.closed_at || '',
237
+ // Free on the search result — feeds the outcome ledger (#1461).
238
+ openedAt: item.created_at || undefined,
235
239
  };
236
240
  });
237
241
  }
@@ -286,7 +290,7 @@ async function fetchPRsSince(octokit, config, adapter, since) {
286
290
  /**
287
291
  * Fetch merged PRs since a watermark date for incremental storage.
288
292
  * If no watermark is provided (first-ever fetch), fetches all merged PRs (up to pagination cap).
289
- * Returns StoredMergedPR[] (minimal: url, title, mergedAt) for state persistence.
293
+ * Returns StoredMergedPR[] (url, title, mergedAt, openedAt) for state persistence.
290
294
  */
291
295
  export async function fetchMergedPRsSince(octokit, config, since) {
292
296
  return fetchPRsSince(octokit, config, {
@@ -294,13 +298,19 @@ export async function fetchMergedPRsSince(octokit, config, since) {
294
298
  dateNoun: 'merge',
295
299
  buildQuery: (u, s) => `is:pr is:merged author:${u} -user:${u}${s ? ` merged:>${s}` : ''}`,
296
300
  extractDate: (item) => item.pull_request?.merged_at || item.closed_at || '',
297
- buildRecord: (item, date) => ({ url: item.html_url, title: item.title, mergedAt: date }),
301
+ buildRecord: (item, date) => ({
302
+ url: item.html_url,
303
+ title: item.title,
304
+ mergedAt: date,
305
+ // Free on the search result — feeds the outcome ledger (#1461).
306
+ openedAt: item.created_at || undefined,
307
+ }),
298
308
  }, since);
299
309
  }
300
310
  /**
301
311
  * Fetch closed-without-merge PRs since a watermark date for incremental storage.
302
312
  * If no watermark is provided (first-ever fetch), fetches all closed PRs (up to pagination cap).
303
- * Returns StoredClosedPR[] (minimal: url, title, closedAt) for state persistence.
313
+ * Returns StoredClosedPR[] (url, title, closedAt, openedAt) for state persistence.
304
314
  * Uses `is:unmerged` to exclude merged PRs (which are also "closed" in GitHub's model).
305
315
  */
306
316
  export async function fetchClosedPRsSince(octokit, config, since) {
@@ -309,6 +319,12 @@ export async function fetchClosedPRsSince(octokit, config, since) {
309
319
  dateNoun: 'close',
310
320
  buildQuery: (u, s) => `is:pr is:closed is:unmerged author:${u} -user:${u}${s ? ` closed:>${s}` : ''}`,
311
321
  extractDate: (item) => item.closed_at || '',
312
- buildRecord: (item, date) => ({ url: item.html_url, title: item.title, closedAt: date }),
322
+ buildRecord: (item, date) => ({
323
+ url: item.html_url,
324
+ title: item.title,
325
+ closedAt: date,
326
+ // Free on the search result — feeds the outcome ledger (#1461).
327
+ openedAt: item.created_at || undefined,
328
+ }),
313
329
  }, since);
314
330
  }
@@ -3,26 +3,27 @@
3
3
  * Re-exports all core functionality for convenient imports
4
4
  */
5
5
  export { StateManager, getStateManager, getStateManagerAsync, ensureGistPersistence, bootstrapGistBestEffort, type GistPersistenceStatus, maybeCheckpoint, resetStateManager, type Stats, } from './state.js';
6
+ export { renderGistWarning, type GistHealth, type GistHealthDegradedCause, type GistWarningCause, } from './gist-health.js';
6
7
  export { GistStateStore } from './gist-state-store.js';
7
8
  export { guidelinesFilename, repoFromGuidelinesFilename, GUIDELINES_FILE_PREFIX, GUIDELINES_MAX_BYTES, GuidelinesNotAvailableError, GuidelinesTooLargeError, } from './guidelines-store.js';
8
9
  export { PRMonitor, type PRCheckFailure, type FetchPRsResult, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
9
10
  export { IssueConversationMonitor } from './issue-conversation.js';
10
11
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
11
- export { wrapUntrustedContent, fenceFetchedPR, extractFromFence, safeExtractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, type UntrustedContentMeta, } from './untrusted-content.js';
12
+ export { wrapUntrustedContent, fenceFetchedPR, fenceFetchedPRTitles, labelGuidelinesContent, GUIDELINES_PROVENANCE_NOTE, extractFromFence, safeExtractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, type UntrustedContentMeta, } from './untrusted-content.js';
12
13
  export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
13
14
  export { parseGitHubUrl, splitRepo, isOwnRepo } from './urls.js';
14
15
  export { daysBetween, formatRelativeTime, byDateDescending } from './dates.js';
15
16
  export { getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, stateFileExists } from './paths.js';
16
17
  export { getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, } from './auth.js';
17
18
  export { DEFAULT_CONCURRENCY } from './concurrency.js';
18
- export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
19
+ export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, ConcurrencyError, GistConcurrencyError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
19
20
  export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
20
21
  export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
21
- export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, buildStarFilter, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
22
+ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, buildStarFilter, firstMaintainerResponseFromDigest, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
22
23
  export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
23
24
  export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
24
25
  export { classifyLinkedPR, isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, type LinkedPR, type LinkedPRClassification, type LinkedPRState, } from './linked-pr-classification.js';
25
- export { classifyIssueAvailability, fetchIssueVerification, type IssueAvailabilityVerdict, type IssueVerification, type LinkedPRLinkType, type VerifiedLinkedPR, type VerifyIssueParams, } from './issue-verification.js';
26
+ export { classifyIssueAvailability, fetchIssueVerification, verifyIssuesBatch, MAX_VERIFY_CONCURRENCY, type BatchVerificationResult, type IssueAvailabilityVerdict, type IssueVerification, type LinkedPRLinkType, type VerifiedLinkedPR, type VerifyIssueParams, } from './issue-verification.js';
26
27
  export { classifyAttentionBucket, summarizeAttentionBuckets, STUCK_CI_THRESHOLD_DAYS, DORMANT_FOLLOWUP_THRESHOLD_DAYS, type AttentionBucket, type AttentionInput, type AttentionSummary, } from './pr-attention.js';
27
28
  export { scanForAntiLLMPolicy, type AntiLLMCategory, type AntiLLMMatch, type AntiLLMScanResult, } from './anti-llm-policy.js';
28
29
  export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, type DetectedFormatter, type FormatterDetectionResult, type CIFormatterDiagnosis, type FormatterName, } from './formatter-detection.js';
@@ -3,27 +3,28 @@
3
3
  * Re-exports all core functionality for convenient imports
4
4
  */
5
5
  export { StateManager, getStateManager, getStateManagerAsync, ensureGistPersistence, bootstrapGistBestEffort, maybeCheckpoint, resetStateManager, } from './state.js';
6
+ export { renderGistWarning, } from './gist-health.js';
6
7
  export { GistStateStore } from './gist-state-store.js';
7
8
  export { guidelinesFilename, repoFromGuidelinesFilename, GUIDELINES_FILE_PREFIX, GUIDELINES_MAX_BYTES, GuidelinesNotAvailableError, GuidelinesTooLargeError, } from './guidelines-store.js';
8
9
  export { PRMonitor, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
9
10
  // Search/vetting now delegated to @oss-scout/core via commands/scout-bridge.ts
10
11
  export { IssueConversationMonitor } from './issue-conversation.js';
11
12
  export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
12
- export { wrapUntrustedContent, fenceFetchedPR, extractFromFence, safeExtractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, } from './untrusted-content.js';
13
+ export { wrapUntrustedContent, fenceFetchedPR, fenceFetchedPRTitles, labelGuidelinesContent, GUIDELINES_PROVENANCE_NOTE, extractFromFence, safeExtractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, } from './untrusted-content.js';
13
14
  export { getOctokit, checkRateLimit } from './github.js';
14
15
  export { parseGitHubUrl, splitRepo, isOwnRepo } from './urls.js';
15
16
  export { daysBetween, formatRelativeTime, byDateDescending } from './dates.js';
16
17
  export { getCLIVersion, getDataDir, getStatePath, getBackupDir, getCacheDir, stateFileExists } from './paths.js';
17
18
  export { getGitHubToken, getGitHubTokenAsync, requireGitHubToken, resetGitHubTokenCache, detectGitHubUsername, } from './auth.js';
18
19
  export { DEFAULT_CONCURRENCY } from './concurrency.js';
19
- export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
20
+ export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, ConcurrencyError, GistConcurrencyError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
20
21
  export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
21
22
  export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
22
- export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, buildStarFilter, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
23
+ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, buildStarFilter, firstMaintainerResponseFromDigest, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
23
24
  export { computeContributionStats } from './stats.js';
24
25
  export { fetchPRTemplate } from './pr-template.js';
25
26
  export { classifyLinkedPR, isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, } from './linked-pr-classification.js';
26
- export { classifyIssueAvailability, fetchIssueVerification, } from './issue-verification.js';
27
+ export { classifyIssueAvailability, fetchIssueVerification, verifyIssuesBatch, MAX_VERIFY_CONCURRENCY, } from './issue-verification.js';
27
28
  export { classifyAttentionBucket, summarizeAttentionBuckets, STUCK_CI_THRESHOLD_DAYS, DORMANT_FOLLOWUP_THRESHOLD_DAYS, } from './pr-attention.js';
28
29
  export { scanForAntiLLMPolicy, } from './anti-llm-policy.js';
29
30
  export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, } from './formatter-detection.js';
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { getOctokit } from './github.js';
9
9
  import { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
10
- import { paginateAll } from './pagination.js';
10
+ import { paginateAllDetailed } from './pagination.js';
11
11
  import { getStateManager } from './state.js';
12
12
  import { daysBetween } from './dates.js';
13
13
  import { splitRepo, extractOwnerRepo, isOwnRepo } from './urls.js';
@@ -142,13 +142,19 @@ export class IssueConversationMonitor {
142
142
  */
143
143
  async analyzeIssueConversation(item, repoFullName, username) {
144
144
  const { owner, repo } = splitRepo(repoFullName);
145
- const allComments = await paginateAll((page) => this.octokit.issues.listComments({
145
+ const { items: allComments, truncated } = await paginateAllDetailed((page) => this.octokit.issues.listComments({
146
146
  owner,
147
147
  repo,
148
148
  issue_number: item.number,
149
149
  per_page: 100,
150
150
  page,
151
151
  }));
152
+ if (truncated) {
153
+ // Comments arrive oldest-first, so hitting the pagination cap drops
154
+ // the NEWEST responses — conversation status for this issue may be
155
+ // stale (#1456). Same log-only channel as the search truncation above.
156
+ warn(MODULE, `Comment pagination cap reached for ${repoFullName}#${item.number}; the newest comments were not fetched and conversation status may be stale.`);
157
+ }
152
158
  const timeline = [];
153
159
  for (const comment of allComments) {
154
160
  if (!comment.user?.login)
@@ -59,6 +59,15 @@ export declare function deriveGradeSignals(params: {
59
59
  * End-to-end helper for vet callers: reads the repo score, derives
60
60
  * signals from a scout candidate, and returns the grade. Callers pass
61
61
  * the `projectHealth` straight through from `scout.vetIssue()`.
62
+ *
63
+ * Which "repo score" this grades from (#1465): the `getRepoScore` input is
64
+ * the cached HISTORY record (the user's own merge outcomes, see
65
+ * docs/repo-scores.md §History score) — NOT `repo-vet`'s fresh health
66
+ * rubric. The fresh side only enters through `projectHealth`, and only when
67
+ * scout actually fetched it: the `search` surface passes a `checkFailed`
68
+ * sentinel (health not fetched per candidate), so search grades purely from
69
+ * history-side signals, while `vet` re-grades with fresh health. Same letter
70
+ * scale, different inputs — the two surfaces can legitimately disagree.
62
71
  */
63
72
  export declare function gradeFromCandidate(params: {
64
73
  repo: string;
@@ -100,6 +100,15 @@ export function deriveGradeSignals(params) {
100
100
  * End-to-end helper for vet callers: reads the repo score, derives
101
101
  * signals from a scout candidate, and returns the grade. Callers pass
102
102
  * the `projectHealth` straight through from `scout.vetIssue()`.
103
+ *
104
+ * Which "repo score" this grades from (#1465): the `getRepoScore` input is
105
+ * the cached HISTORY record (the user's own merge outcomes, see
106
+ * docs/repo-scores.md §History score) — NOT `repo-vet`'s fresh health
107
+ * rubric. The fresh side only enters through `projectHealth`, and only when
108
+ * scout actually fetched it: the `search` surface passes a `checkFailed`
109
+ * sentinel (health not fetched per candidate), so search grades purely from
110
+ * history-side signals, while `vet` re-grades with fresh health. Same letter
111
+ * scale, different inputs — the two surfaces can legitimately disagree.
103
112
  */
104
113
  export function gradeFromCandidate(params) {
105
114
  const repoScore = params.getRepoScore(params.repo);
@@ -89,3 +89,42 @@ export interface VerifyIssueParams {
89
89
  number: number;
90
90
  }
91
91
  export declare function fetchIssueVerification(octokit: Octokit, params: VerifyIssueParams): Promise<IssueVerification>;
92
+ /**
93
+ * Hard ceiling on concurrent in-flight verifications. Each issue runs its own
94
+ * GraphQL round-trip (point-heavy, with timeline pagination), so we never run
95
+ * more than this many at once regardless of what the caller requests. The
96
+ * single-issue path already leans on ThrottledOctokit's secondary-rate-limit
97
+ * backoff — this cap keeps us from spraying enough parallel queries to trip it.
98
+ */
99
+ export declare const MAX_VERIFY_CONCURRENCY = 5;
100
+ /**
101
+ * Per-item outcome of a batched verification run. Aligned by index with the
102
+ * input `items`. Error isolation: one item's failure lands as `{ error }` and
103
+ * never aborts the batch, so a single NOT_FOUND (or transient network error)
104
+ * does not poison every other entry — unlike an aliased mega-query would.
105
+ */
106
+ export interface BatchVerificationResult {
107
+ params: VerifyIssueParams;
108
+ /** Present when verification succeeded for this item. */
109
+ verification?: IssueVerification;
110
+ /** Present when `fetchIssueVerification` threw for this item. */
111
+ error?: unknown;
112
+ }
113
+ /**
114
+ * Verify many issues with bounded concurrent fan-out of the single-issue
115
+ * {@link fetchIssueVerification}.
116
+ *
117
+ * This is a deliberately thin worker pool, NOT one aliased GraphQL query:
118
+ * per-issue timeline cursors make aliasing unmanageable, and all-or-nothing
119
+ * failure (one bad issue poisoning the batch) is exactly what we want to
120
+ * avoid. No retry layer is added here — `octokit` is expected to be a
121
+ * ThrottledOctokit whose backoff already covers secondary rate limits.
122
+ *
123
+ * @param octokit - Throttled Octokit instance (see `getOctokit`).
124
+ * @param items - Issues to verify; results align with this order.
125
+ * @param options.concurrency - Desired parallelism; capped at
126
+ * {@link MAX_VERIFY_CONCURRENCY} and floored at 1.
127
+ */
128
+ export declare function verifyIssuesBatch(octokit: Octokit, items: readonly VerifyIssueParams[], options?: {
129
+ concurrency?: number;
130
+ }): Promise<BatchVerificationResult[]>;
@@ -268,3 +268,51 @@ export async function fetchIssueVerification(octokit, params) {
268
268
  userLogin,
269
269
  };
270
270
  }
271
+ // ── Batched verification ───────────────────────────────────────────────
272
+ /**
273
+ * Hard ceiling on concurrent in-flight verifications. Each issue runs its own
274
+ * GraphQL round-trip (point-heavy, with timeline pagination), so we never run
275
+ * more than this many at once regardless of what the caller requests. The
276
+ * single-issue path already leans on ThrottledOctokit's secondary-rate-limit
277
+ * backoff — this cap keeps us from spraying enough parallel queries to trip it.
278
+ */
279
+ export const MAX_VERIFY_CONCURRENCY = 5;
280
+ /**
281
+ * Verify many issues with bounded concurrent fan-out of the single-issue
282
+ * {@link fetchIssueVerification}.
283
+ *
284
+ * This is a deliberately thin worker pool, NOT one aliased GraphQL query:
285
+ * per-issue timeline cursors make aliasing unmanageable, and all-or-nothing
286
+ * failure (one bad issue poisoning the batch) is exactly what we want to
287
+ * avoid. No retry layer is added here — `octokit` is expected to be a
288
+ * ThrottledOctokit whose backoff already covers secondary rate limits.
289
+ *
290
+ * @param octokit - Throttled Octokit instance (see `getOctokit`).
291
+ * @param items - Issues to verify; results align with this order.
292
+ * @param options.concurrency - Desired parallelism; capped at
293
+ * {@link MAX_VERIFY_CONCURRENCY} and floored at 1.
294
+ */
295
+ export async function verifyIssuesBatch(octokit, items, options = {}) {
296
+ const results = new Array(items.length);
297
+ if (items.length === 0)
298
+ return results;
299
+ const requested = options.concurrency ?? MAX_VERIFY_CONCURRENCY;
300
+ // Floor at 1 so a 0/negative/NaN request can never produce zero workers
301
+ // (which would leave the results array full of holes).
302
+ const concurrency = Math.max(1, Math.min(Number.isFinite(requested) ? requested : MAX_VERIFY_CONCURRENCY, MAX_VERIFY_CONCURRENCY, items.length));
303
+ let index = 0;
304
+ async function worker() {
305
+ while (index < items.length) {
306
+ const i = index++;
307
+ const params = items[i];
308
+ try {
309
+ results[i] = { params, verification: await fetchIssueVerification(octokit, params) };
310
+ }
311
+ catch (error) {
312
+ results[i] = { params, error };
313
+ }
314
+ }
315
+ }
316
+ await Promise.all(Array.from({ length: concurrency }, () => worker()));
317
+ return results;
318
+ }
@@ -1,7 +1,34 @@
1
+ /** Result of {@link paginateAllDetailed}: the items plus a truncation signal. */
2
+ export interface PaginateAllResult<T> {
3
+ items: T[];
4
+ /**
5
+ * True when the page cap was reached with a full final page — more data
6
+ * may exist on the server beyond what was fetched. GitHub list endpoints
7
+ * return oldest-first, so on truncation it is the NEWEST entries that are
8
+ * missing. Callers should surface this through their warning channel
9
+ * rather than silently presenting a partial view (#1456).
10
+ */
11
+ truncated: boolean;
12
+ }
13
+ /**
14
+ * Auto-paginate an Octokit list endpoint, reporting whether the page cap
15
+ * truncated the results. Fetches additional pages when the result count
16
+ * equals per_page (indicating more data may exist).
17
+ *
18
+ * @param fetchPage Function that fetches a single page given a page number
19
+ * @param perPage Items per page (default 100)
20
+ * @param maxPages Maximum pages to fetch (default 10 = 1000 items)
21
+ */
22
+ export declare function paginateAllDetailed<T>(fetchPage: (page: number) => Promise<{
23
+ data: T[];
24
+ }>, perPage?: number, maxPages?: number): Promise<PaginateAllResult<T>>;
1
25
  /**
2
26
  * Auto-paginate an Octokit list endpoint. Fetches additional pages when
3
27
  * the result count equals per_page (indicating more data may exist).
4
28
  *
29
+ * Silently drops anything past `maxPages` — use {@link paginateAllDetailed}
30
+ * when the caller needs to know the results were truncated.
31
+ *
5
32
  * @param fetchPage Function that fetches a single page given a page number
6
33
  * @param perPage Items per page (default 100)
7
34
  * @param maxPages Maximum pages to fetch (default 10 = 1000 items)
@@ -1,20 +1,38 @@
1
1
  /** Maximum pages to fetch to prevent runaway pagination */
2
2
  const MAX_PAGES = 10;
3
3
  /**
4
- * Auto-paginate an Octokit list endpoint. Fetches additional pages when
5
- * the result count equals per_page (indicating more data may exist).
4
+ * Auto-paginate an Octokit list endpoint, reporting whether the page cap
5
+ * truncated the results. Fetches additional pages when the result count
6
+ * equals per_page (indicating more data may exist).
6
7
  *
7
8
  * @param fetchPage Function that fetches a single page given a page number
8
9
  * @param perPage Items per page (default 100)
9
10
  * @param maxPages Maximum pages to fetch (default 10 = 1000 items)
10
11
  */
11
- export async function paginateAll(fetchPage, perPage = 100, maxPages = MAX_PAGES) {
12
+ export async function paginateAllDetailed(fetchPage, perPage = 100, maxPages = MAX_PAGES) {
12
13
  const allItems = [];
13
14
  for (let page = 1; page <= maxPages; page++) {
14
15
  const { data } = await fetchPage(page);
15
16
  allItems.push(...data);
16
17
  if (data.length < perPage)
17
- break; // No more pages
18
+ return { items: allItems, truncated: false }; // No more pages
18
19
  }
19
- return allItems;
20
+ // Every fetched page (including the last allowed one) was full — more
21
+ // data may exist beyond the cap.
22
+ return { items: allItems, truncated: true };
23
+ }
24
+ /**
25
+ * Auto-paginate an Octokit list endpoint. Fetches additional pages when
26
+ * the result count equals per_page (indicating more data may exist).
27
+ *
28
+ * Silently drops anything past `maxPages` — use {@link paginateAllDetailed}
29
+ * when the caller needs to know the results were truncated.
30
+ *
31
+ * @param fetchPage Function that fetches a single page given a page number
32
+ * @param perPage Items per page (default 100)
33
+ * @param maxPages Maximum pages to fetch (default 10 = 1000 items)
34
+ */
35
+ export async function paginateAll(fetchPage, perPage = 100, maxPages = MAX_PAGES) {
36
+ const { items } = await paginateAllDetailed(fetchPage, perPage, maxPages);
37
+ return items;
20
38
  }
@@ -52,6 +52,13 @@ export interface PRCommentBundle {
52
52
  reviews: PRReviewEntry[];
53
53
  reviewComments: PRReviewCommentEntry[];
54
54
  issueComments: PRIssueCommentEntry[];
55
+ /**
56
+ * Set to true when any of the three comment streams hit the pagination
57
+ * cap (#1456). These endpoints return oldest-first, so a truncated bundle
58
+ * is missing the NEWEST reviewer voices — corpus consumers should treat
59
+ * the bundle as partial. Omitted when every stream fetched completely.
60
+ */
61
+ truncated?: boolean;
55
62
  }
56
63
  /**
57
64
  * Fetch a single PR's comment bundle. Filters out the authenticated user's
@@ -1,4 +1,4 @@
1
- import { paginateAll } from './pagination.js';
1
+ import { paginateAllDetailed } from './pagination.js';
2
2
  import { wrapUntrustedContent } from './untrusted-content.js';
3
3
  import { isBotAuthor } from './comment-utils.js';
4
4
  import { parseGitHubUrl } from './urls.js';
@@ -18,26 +18,29 @@ export async function fetchPRCommentBundle(octokit, prUrl, githubUsername) {
18
18
  }
19
19
  const { owner, repo, number: pull_number } = parsed;
20
20
  const repoFull = `${owner}/${repo}`;
21
- // Fetch the PR + all three comment streams in parallel. We always fetch
22
- // every page — corpus quality depends on having every reviewer voice, not
23
- // just the first 100 comments.
24
- const [{ data: pr }, reviews, reviewComments, issueComments] = await Promise.all([
21
+ // Fetch the PR + all three comment streams in parallel. We fetch every
22
+ // page up to the pagination cap — corpus quality depends on having every
23
+ // reviewer voice, not just the first 100 comments. When a stream still
24
+ // truncates (1000+ entries), the bundle is flagged `truncated` so callers
25
+ // can surface the partial-corpus signal instead of silently dropping the
26
+ // newest comments (#1456).
27
+ const [{ data: pr }, reviewsResult, reviewCommentsResult, issueCommentsResult] = await Promise.all([
25
28
  octokit.pulls.get({ owner, repo, pull_number }),
26
- paginateAll((page) => octokit.pulls.listReviews({
29
+ paginateAllDetailed((page) => octokit.pulls.listReviews({
27
30
  owner,
28
31
  repo,
29
32
  pull_number,
30
33
  per_page: 100,
31
34
  page,
32
35
  })),
33
- paginateAll((page) => octokit.pulls.listReviewComments({
36
+ paginateAllDetailed((page) => octokit.pulls.listReviewComments({
34
37
  owner,
35
38
  repo,
36
39
  pull_number,
37
40
  per_page: 100,
38
41
  page,
39
42
  })),
40
- paginateAll((page) => octokit.issues.listComments({
43
+ paginateAllDetailed((page) => octokit.issues.listComments({
41
44
  owner,
42
45
  repo,
43
46
  issue_number: pull_number,
@@ -45,6 +48,13 @@ export async function fetchPRCommentBundle(octokit, prUrl, githubUsername) {
45
48
  page,
46
49
  })),
47
50
  ]);
51
+ const { items: reviews } = reviewsResult;
52
+ const { items: reviewComments } = reviewCommentsResult;
53
+ const { items: issueComments } = issueCommentsResult;
54
+ const truncated = reviewsResult.truncated || reviewCommentsResult.truncated || issueCommentsResult.truncated;
55
+ if (truncated) {
56
+ warn(MODULE, `Comment streams truncated at pagination cap for ${repoFull}#${pull_number}; bundle is partial`);
57
+ }
48
58
  const ownLogin = githubUsername.toLowerCase();
49
59
  /**
50
60
  * Drop entries that aren't useful corpus: the user's own comments, bots,
@@ -97,6 +107,7 @@ export async function fetchPRCommentBundle(octokit, prUrl, githubUsername) {
97
107
  body: fence(c.body ?? '', 'pr-issue-comment', c.user?.login ?? '', c.author_association ?? 'NONE'),
98
108
  createdAt: c.created_at ?? '',
99
109
  })),
110
+ ...(truncated ? { truncated: true } : {}),
100
111
  };
101
112
  }
102
113
  export async function fetchPRCommentBundlesBatch(octokit, prUrls, githubUsername, concurrency = DEFAULT_BATCH_CONCURRENCY) {
@@ -40,6 +40,8 @@ export interface FetchPRsResult {
40
40
  * - Post-fetch viewer-mismatch guardrail (configured username differs
41
41
  * from the authenticated viewer when the search returned zero PRs).
42
42
  * - Search API 1000-result truncation (#1057 M25).
43
+ * - Per-PR comment pagination truncation (#1456) — newest comments
44
+ * dropped, so unresponded-comment detection may be incomplete.
43
45
  * Callers (daily, dashboard) surface these so users see the signal.
44
46
  */
45
47
  warnings?: string[];
@@ -19,11 +19,11 @@ import { parseGitHubUrl, extractOwnerRepo, isOwnRepo } from './urls.js';
19
19
  import { DEFAULT_CONCURRENCY, runWorkerPool } from './concurrency.js';
20
20
  import { determineStatus } from './status-determination.js';
21
21
  import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isInvalidUserSearchError, isRateLimitOrAuthError, } from './errors.js';
22
- import { paginateAll } from './pagination.js';
22
+ import { paginateAllDetailed } from './pagination.js';
23
23
  import { debug, warn, timed } from './logger.js';
24
24
  import { getHttpCache, cachedRequest } from './http-cache.js';
25
25
  import { categorizeCIStatus, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
26
- import { determineReviewDecision, getLatestChangesRequestedDate, checkUnrespondedComments, } from './review-analysis.js';
26
+ import { determineReviewDecision, getLatestChangesRequestedDate, checkUnrespondedComments, getFirstMaintainerResponseAt, } from './review-analysis.js';
27
27
  import { analyzeChecklist } from './checklist-analysis.js';
28
28
  import { extractMaintainerActionHints } from './maintainer-analysis.js';
29
29
  import { computeDisplayLabel } from './display-utils.js';
@@ -239,7 +239,7 @@ export class PRMonitor {
239
239
  await runWorkerPool(filteredItems, async (item) => {
240
240
  try {
241
241
  debug('pr-monitor', `Fetching details for ${item.html_url}`);
242
- const pr = await this.fetchPRDetails(item.html_url);
242
+ const pr = await this.fetchPRDetails(item.html_url, warnings);
243
243
  if (pr)
244
244
  prs.push(pr);
245
245
  }
@@ -261,7 +261,7 @@ export class PRMonitor {
261
261
  /**
262
262
  * Fetch detailed information for a single PR
263
263
  */
264
- async fetchPRDetails(prUrl) {
264
+ async fetchPRDetails(prUrl, warnings) {
265
265
  const parsed = parseGitHubUrl(prUrl);
266
266
  if (!parsed || parsed.type !== 'pull') {
267
267
  throw new ValidationError(`Invalid PR URL format: ${prUrl}`);
@@ -271,11 +271,11 @@ export class PRMonitor {
271
271
  // Fetch PR data, comments, reviews, and inline review comments in parallel.
272
272
  // listReviewComments is non-critical (used for self-reply detection), so degrade
273
273
  // gracefully on failure rather than dropping the entire PR (#199).
274
- const [prResponse, comments, reviewsResponse, reviewComments] = await Promise.all([
274
+ const [prResponse, commentsResult, reviewsResponse, reviewCommentsResult] = await Promise.all([
275
275
  this.octokit.pulls.get({ owner, repo, pull_number: number }),
276
- paginateAll((page) => this.octokit.issues.listComments({ owner, repo, issue_number: number, per_page: 100, page })),
276
+ paginateAllDetailed((page) => this.octokit.issues.listComments({ owner, repo, issue_number: number, per_page: 100, page })),
277
277
  this.octokit.pulls.listReviews({ owner, repo, pull_number: number }),
278
- paginateAll((page) => this.octokit.pulls.listReviewComments({ owner, repo, pull_number: number, per_page: 100, page })).catch((err) => {
278
+ paginateAllDetailed((page) => this.octokit.pulls.listReviewComments({ owner, repo, pull_number: number, per_page: 100, page })).catch((err) => {
279
279
  const status = getHttpStatusCode(err);
280
280
  // Rate limit errors must propagate — silently swallowing them hides
281
281
  // a systemic problem and produces misleading results (#229).
@@ -289,7 +289,7 @@ export class PRMonitor {
289
289
  }
290
290
  // Non-rate-limit 403 (DMCA, private repo, SSO) — degrade gracefully
291
291
  warn('pr-monitor', `403 fetching review comments for ${owner}/${repo}#${number}: ${msg}`);
292
- return [];
292
+ return { items: [], truncated: false };
293
293
  }
294
294
  if (status === 404) {
295
295
  debug('pr-monitor', `Review comments 404 for ${owner}/${repo}#${number} (likely no inline comments)`);
@@ -297,9 +297,21 @@ export class PRMonitor {
297
297
  else {
298
298
  warn('pr-monitor', `Failed to fetch review comments for ${owner}/${repo}#${number} (status ${status ?? 'unknown'}): self-reply detection will be skipped`);
299
299
  }
300
- return [];
300
+ return { items: [], truncated: false };
301
301
  }),
302
302
  ]);
303
+ const { items: comments } = commentsResult;
304
+ const { items: reviewComments } = reviewCommentsResult;
305
+ // Surface pagination truncation through the caller's warnings channel
306
+ // (#1456): comments arrive oldest-first, so hitting the cap drops the
307
+ // NEWEST maintainer feedback — status determination (unresponded-comment
308
+ // detection) may be wrong for this PR.
309
+ if (commentsResult.truncated || reviewCommentsResult.truncated) {
310
+ const message = `Comment pagination cap reached for ${owner}/${repo}#${number}; the newest comments were not ` +
311
+ `fetched and unresponded-comment detection may be incomplete.`;
312
+ warnings?.push(message);
313
+ warn(MODULE, message);
314
+ }
303
315
  const ghPR = prResponse.data;
304
316
  const reviews = reviewsResponse.data;
305
317
  // Determine review decision (delegated to review-analysis module)
@@ -308,6 +320,10 @@ export class PRMonitor {
308
320
  const mergeConflict = hasMergeConflict(ghPR.mergeable, ghPR.mergeable_state);
309
321
  // Check if there's an unresponded maintainer comment (delegated to review-analysis module)
310
322
  const { hasUnrespondedComment, lastMaintainerComment } = checkUnrespondedComments(comments, reviews, reviewComments, config.githubUsername);
323
+ // Earliest maintainer response, computed from the comment/review timeline
324
+ // already in memory (#1461). Rides into the persisted digest so the
325
+ // outcome ledger can recover it after the PR merges or closes.
326
+ const firstMaintainerResponseAt = getFirstMaintainerResponseAt(comments, reviews, config.githubUsername);
311
327
  // Fetch CI status and (conditionally) latest commit date in parallel
312
328
  // We need the commit date when hasUnrespondedComment is true (to distinguish
313
329
  // "needs_response" from "waiting_on_maintainer") OR when reviewDecision is "changes_requested"
@@ -391,6 +407,7 @@ export class PRMonitor {
391
407
  actionReasons,
392
408
  createdAt: ghPR.created_at,
393
409
  updatedAt: ghPR.updated_at,
410
+ firstMaintainerResponseAt,
394
411
  daysSinceActivity,
395
412
  ciStatus,
396
413
  failingCheckNames,
@@ -4,7 +4,7 @@
4
4
  * and computing aggregate statistics. Mutation functions modify
5
5
  * the passed state object in place; query functions are pure.
6
6
  *
7
- * **User-facing reference:** `docs/repo-scoring.md` — plain-language
7
+ * **User-facing reference:** `docs/repo-scores.md` — plain-language
8
8
  * explanation of the formula and what a given score means.
9
9
  */
10
10
  import { AgentState, RepoScore, RepoScoreUpdate, StoredMergedPR, StoredClosedPR } from './types.js';
@@ -21,7 +21,7 @@ import { AgentState, RepoScore, RepoScoreUpdate, StoredMergedPR, StoredClosedPR
21
21
  * − (hasHostileComments ? HOSTILITY_PENALTY : 0)
22
22
  * clamped to [SCORE_MIN, SCORE_MAX].
23
23
  *
24
- * See `docs/repo-scoring.md` for user-facing intent and what a given
24
+ * See `docs/repo-scores.md` for user-facing intent and what a given
25
25
  * score means in practice.
26
26
  */
27
27
  export declare function calculateScore(repoScore: RepoScore): number;