@oss-autopilot/core 3.13.3 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +3 -3
  2. package/dist/cli-registry.js +50 -83
  3. package/dist/cli.bundle.cjs +110 -107
  4. package/dist/cli.js +17 -3
  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 +35 -7
  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 +155 -222
  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.js +4 -0
  47. package/dist/core/config-registry.js +36 -0
  48. package/dist/core/daily-logic.d.ts +25 -2
  49. package/dist/core/daily-logic.js +58 -3
  50. package/dist/core/gist-health.d.ts +81 -0
  51. package/dist/core/gist-health.js +39 -0
  52. package/dist/core/gist-state-store.d.ts +3 -1
  53. package/dist/core/gist-state-store.js +7 -2
  54. package/dist/core/github-stats.d.ts +2 -2
  55. package/dist/core/github-stats.js +20 -4
  56. package/dist/core/index.d.ts +4 -3
  57. package/dist/core/index.js +4 -3
  58. package/dist/core/issue-conversation.js +8 -2
  59. package/dist/core/issue-grading.d.ts +9 -0
  60. package/dist/core/issue-grading.js +9 -0
  61. package/dist/core/pagination.d.ts +27 -0
  62. package/dist/core/pagination.js +23 -5
  63. package/dist/core/pr-comments-fetcher.d.ts +7 -0
  64. package/dist/core/pr-comments-fetcher.js +19 -8
  65. package/dist/core/pr-monitor.d.ts +2 -0
  66. package/dist/core/pr-monitor.js +26 -9
  67. package/dist/core/repo-score-manager.d.ts +2 -2
  68. package/dist/core/repo-score-manager.js +3 -3
  69. package/dist/core/repo-vet.d.ts +2 -2
  70. package/dist/core/repo-vet.js +1 -1
  71. package/dist/core/review-analysis.d.ts +19 -0
  72. package/dist/core/review-analysis.js +28 -0
  73. package/dist/core/state-schema.d.ts +43 -6
  74. package/dist/core/state-schema.js +81 -4
  75. package/dist/core/state.d.ts +36 -5
  76. package/dist/core/state.js +177 -28
  77. package/dist/core/strategy.js +6 -5
  78. package/dist/core/types.d.ts +8 -0
  79. package/dist/core/untrusted-content.d.ts +45 -0
  80. package/dist/core/untrusted-content.js +54 -0
  81. package/dist/formatters/json.d.ts +89 -6
  82. package/dist/formatters/json.js +65 -1
  83. package/package.json +2 -2
  84. package/dist/commands/shelve.d.ts +0 -45
  85. package/dist/commands/shelve.js +0 -54
@@ -14,6 +14,7 @@
14
14
  * re-exports them at the bottom so existing imports keep working without
15
15
  * a sweep.
16
16
  */
17
+ import type { AttentionSummary } from './pr-attention.js';
17
18
  import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, AgentState, ShelvedPRRef, ComputedRepoSignals, RepoGroup, CommentedIssue, CapacityAssessment, ActionableIssue, ActionMenu, StarFilter } from './types.js';
18
19
  /**
19
20
  * Statuses indicating action needed from the contributor.
@@ -38,9 +39,13 @@ export declare const STALE_STATUSES: ReadonlySet<StalenessTier>;
38
39
  *
39
40
  * @param prs - The fetched PR list to apply overrides to
40
41
  * @param state - Current agent state containing status overrides
42
+ * @param failures - Optional collector (#1448): a message is pushed for each
43
+ * PR whose override lookup threw (that PR keeps its un-overridden status).
44
+ * Call sites surface these in warnings[]/partialFailures so the silently
45
+ * wrong status is visible in the envelope, not just stderr.
41
46
  * @returns New PR array with overrides applied (original array is not mutated)
42
47
  */
43
- export declare function applyStatusOverrides(prs: FetchedPR[], state: Readonly<AgentState>): FetchedPR[];
48
+ export declare function applyStatusOverrides(prs: FetchedPR[], state: Readonly<AgentState>, failures?: string[]): FetchedPR[];
44
49
  /**
45
50
  * Project a PR to a lightweight ShelvedPRRef for digest output.
46
51
  * Only the fields needed for display are retained, reducing JSON payload size.
@@ -61,6 +66,22 @@ export declare function toShelvedPRRef(pr: ShelvedPRRef): ShelvedPRRef;
61
66
  * @returns Star filter with minimum threshold and known counts, or undefined on first run
62
67
  */
63
68
  export declare function buildStarFilter(state: Readonly<AgentState>): StarFilter | undefined;
69
+ /**
70
+ * Look up a just-merged/closed PR's `firstMaintainerResponseAt` from the
71
+ * previous run's persisted digest (#1461). At detection time the PR is no
72
+ * longer open, so it cannot be enriched without new API calls — but if it
73
+ * was open during a prior enriched run, the persisted digest's openPRs carry
74
+ * the timestamp. Best-effort: returns undefined when the PR never appeared
75
+ * in a persisted digest (e.g. opened and merged between runs, or the digest
76
+ * predates the field).
77
+ *
78
+ * Lives in core/daily-logic.ts so both ledger writers (daily Phase 3 and
79
+ * dashboard-data) share one lookup. Callers must read `state.lastDigest`
80
+ * BEFORE the current run overwrites it via setLastDigest.
81
+ */
82
+ export declare function firstMaintainerResponseFromDigest(digest: {
83
+ openPRs: FetchedPR[];
84
+ } | undefined, url: string): string | undefined;
64
85
  /**
65
86
  * Group PRs by repository (#80).
66
87
  * Ensures one agent per repo during parallel dispatch, preventing branch checkout conflicts.
@@ -108,7 +129,9 @@ export declare function collectActionableIssues(prs: FetchedPR[], lastDigestAt?:
108
129
  * @param actionableIssues - Issues requiring attention
109
130
  * @param capacity - Current capacity assessment
110
131
  * @param commentedIssues - Issues with comment activity
132
+ * @param attention - Attention bucket counts; non-zero stuck-CI / dormant-followup buckets add a follow_up item
133
+ * @param unextractedMergeCount - Recently merged PRs whose learnings have not been extracted yet (#1463); non-zero adds an extract_learnings item
111
134
  * @returns Action menu with context flags for orchestration
112
135
  */
113
- export declare function computeActionMenu(actionableIssues: ActionableIssue[], capacity: CapacityAssessment, commentedIssues?: CommentedIssue[]): ActionMenu;
136
+ export declare function computeActionMenu(actionableIssues: ActionableIssue[], capacity: CapacityAssessment, commentedIssues?: CommentedIssue[], attention?: Pick<AttentionSummary, 'stuckCI' | 'dormantFollowup'>, unextractedMergeCount?: number): ActionMenu;
114
137
  export { formatActionHint, formatBriefSummary, formatSummary, printDigest } from '../commands/daily-render.js';
@@ -52,9 +52,13 @@ const VALID_OVERRIDE_STATUSES = new Set(['needs_addressing', 'waiting_on_maintai
52
52
  *
53
53
  * @param prs - The fetched PR list to apply overrides to
54
54
  * @param state - Current agent state containing status overrides
55
+ * @param failures - Optional collector (#1448): a message is pushed for each
56
+ * PR whose override lookup threw (that PR keeps its un-overridden status).
57
+ * Call sites surface these in warnings[]/partialFailures so the silently
58
+ * wrong status is visible in the envelope, not just stderr.
55
59
  * @returns New PR array with overrides applied (original array is not mutated)
56
60
  */
57
- export function applyStatusOverrides(prs, state) {
61
+ export function applyStatusOverrides(prs, state, failures) {
58
62
  const overrides = state.config.statusOverrides;
59
63
  if (!overrides || Object.keys(overrides).length === 0)
60
64
  return prs;
@@ -82,7 +86,9 @@ export function applyStatusOverrides(prs, state) {
82
86
  return { ...pr, status: override.status, waitReason: undefined, actionReason: 'needs_response' };
83
87
  }
84
88
  catch (err) {
85
- warn('daily-logic', `Failed to apply status override for ${pr.url}: ${errorMessage(err)}`);
89
+ const message = `Failed to apply status override for ${pr.url}: ${errorMessage(err)}`;
90
+ warn('daily-logic', message);
91
+ failures?.push(message);
86
92
  return pr;
87
93
  }
88
94
  });
@@ -152,6 +158,22 @@ export function buildStarFilter(state) {
152
158
  return undefined;
153
159
  return { minStars, knownStarCounts };
154
160
  }
161
+ /**
162
+ * Look up a just-merged/closed PR's `firstMaintainerResponseAt` from the
163
+ * previous run's persisted digest (#1461). At detection time the PR is no
164
+ * longer open, so it cannot be enriched without new API calls — but if it
165
+ * was open during a prior enriched run, the persisted digest's openPRs carry
166
+ * the timestamp. Best-effort: returns undefined when the PR never appeared
167
+ * in a persisted digest (e.g. opened and merged between runs, or the digest
168
+ * predates the field).
169
+ *
170
+ * Lives in core/daily-logic.ts so both ledger writers (daily Phase 3 and
171
+ * dashboard-data) share one lookup. Callers must read `state.lastDigest`
172
+ * BEFORE the current run overwrites it via setLastDigest.
173
+ */
174
+ export function firstMaintainerResponseFromDigest(digest, url) {
175
+ return digest?.openPRs.find((pr) => pr.url === url)?.firstMaintainerResponseAt;
176
+ }
155
177
  /**
156
178
  * Group PRs by repository (#80).
157
179
  * Ensures one agent per repo during parallel dispatch, preventing branch checkout conflicts.
@@ -328,9 +350,11 @@ export function collectActionableIssues(prs, lastDigestAt) {
328
350
  * @param actionableIssues - Issues requiring attention
329
351
  * @param capacity - Current capacity assessment
330
352
  * @param commentedIssues - Issues with comment activity
353
+ * @param attention - Attention bucket counts; non-zero stuck-CI / dormant-followup buckets add a follow_up item
354
+ * @param unextractedMergeCount - Recently merged PRs whose learnings have not been extracted yet (#1463); non-zero adds an extract_learnings item
331
355
  * @returns Action menu with context flags for orchestration
332
356
  */
333
- export function computeActionMenu(actionableIssues, capacity, commentedIssues = []) {
357
+ export function computeActionMenu(actionableIssues, capacity, commentedIssues = [], attention, unextractedMergeCount) {
334
358
  const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
335
359
  const items = [];
336
360
  const hasActionableIssues = actionableIssues.length > 0;
@@ -355,6 +379,37 @@ export function computeActionMenu(actionableIssues, capacity, commentedIssues =
355
379
  description: 'Maintainers responded to your comments on issues',
356
380
  });
357
381
  }
382
+ // Follow-up nudges (#1462) — the stuck_ci / dormant_followup buckets from
383
+ // summarizeAttentionBuckets get a menu path to workflows/dormant-pr-follow-up.md
384
+ // instead of only a headline count. Emitted only when a bucket is non-zero,
385
+ // so menus without stuck/dormant PRs are unchanged.
386
+ const stuckCI = attention?.stuckCI ?? 0;
387
+ const dormantFollowup = attention?.dormantFollowup ?? 0;
388
+ if (stuckCI > 0 || dormantFollowup > 0) {
389
+ const parts = [];
390
+ if (stuckCI > 0)
391
+ parts.push(`${stuckCI} stuck-CI`);
392
+ if (dormantFollowup > 0)
393
+ parts.push(`${dormantFollowup} dormant`);
394
+ const total = stuckCI + dormantFollowup;
395
+ items.push({
396
+ key: 'follow_up',
397
+ label: `Follow up on ${parts.join(' and ')} PR${total === 1 ? '' : 's'}`,
398
+ description: 'Waiting PRs that may need a nudge (pending CI or no review activity)',
399
+ });
400
+ }
401
+ // Extract-learnings nudge (#1463) — recently merged PRs whose ledger
402
+ // entries carry no learningsExtractedAt stamp get a menu path to
403
+ // workflows/extract-learnings.md. The nudge persists across runs for the
404
+ // recently-merged window until the extraction runs, then self-clears.
405
+ const unextracted = unextractedMergeCount ?? 0;
406
+ if (unextracted > 0) {
407
+ items.push({
408
+ key: 'extract_learnings',
409
+ label: `Extract learnings from ${unextracted} recently merged PR${unextracted === 1 ? '' : 's'}`,
410
+ description: 'Distill maintainer review feedback into per-repo contribution guidelines',
411
+ });
412
+ }
358
413
  // The orchestration layer (commands/oss.md Action Menu section) may insert issue-list
359
414
  // options before the search item when a curated list is available.
360
415
  const searchItem = {
@@ -0,0 +1,81 @@
1
+ /**
2
+ * One health source and one warning renderer for gist persistence (#1444).
3
+ *
4
+ * "Are we degraded?" used to be answered by three independent predicates
5
+ * (`config.persistence === 'gist' && !isGistMode()`, the
6
+ * `GistPersistenceStatus` union, `isGistMode() && isGistDegraded()`), each
7
+ * re-rendering its own warning prose. `StateManager.getGistHealth()` is now
8
+ * the single predicate for live-manager surfaces (daily warnings, dashboard
9
+ * probes), and {@link renderGistWarning} is the single prose renderer for
10
+ * every surface (CLI envelope, MCP injection, bootstrapGistBestEffort,
11
+ * daily warnings).
12
+ */
13
+ /**
14
+ * Degradation causes a live StateManager can attest to from its own fields
15
+ * (see `StateManager.getGistHealth()`):
16
+ *
17
+ * - `configured-but-local`: config says `persistence: 'gist'` but this
18
+ * process's manager has no gist store — init never ran here, or fell back
19
+ * to a local-only manager. Mutations save locally and will not sync.
20
+ * - `bootstrap-degraded`: the manager IS gist-backed, but the bootstrap fell
21
+ * back to the local cache and the store is disarmed (#1443) — reads may be
22
+ * stale and pushes fail.
23
+ */
24
+ export type GistHealthDegradedCause = 'configured-but-local' | 'bootstrap-degraded';
25
+ /**
26
+ * Causes {@link renderGistWarning} can name. Superset of
27
+ * {@link GistHealthDegradedCause}: the process-level bootstrap outcomes are
28
+ * observed by config-peek paths (`ensureGistPersistence`, the MCP init memo)
29
+ * that run before or around manager creation, where there is no
30
+ * StateManager to ask:
31
+ *
32
+ * - `no-token`: gist is configured but no GitHub token was available.
33
+ * - `state-unreadable`: the state file could not be read for this attempt.
34
+ * - `init-fallback`: init resolved degraded — `ensureGistPersistence`
35
+ * reported `'degraded'` (transient network fallback, or a #1443 degraded
36
+ * bootstrap, which it deliberately conflates at that layer).
37
+ */
38
+ export type GistWarningCause = GistHealthDegradedCause | 'no-token' | 'state-unreadable' | 'init-fallback';
39
+ /**
40
+ * Snapshot of gist persistence health from the one source of truth
41
+ * (`StateManager.getGistHealth()`), shaped per #1444.
42
+ */
43
+ export interface GistHealth {
44
+ /** `'gist'` when the manager is gist-backed (a GistStateStore is attached),
45
+ * `'local'` otherwise — regardless of what the config asks for. */
46
+ mode: 'local' | 'gist';
47
+ /** `null` when healthy: either genuinely local (config agrees) or
48
+ * gist-backed with an armed store. Non-null when this process is not
49
+ * reliably syncing to the Gist. */
50
+ degraded: null | {
51
+ cause: GistHealthDegradedCause;
52
+ /** ISO timestamp when the degradation was first observed, when known
53
+ * (a degraded bootstrap seeds the staleness marker; a
54
+ * configured-but-local manager has no marker to date it by). */
55
+ since?: string;
56
+ /** Whether a later init/recovery attempt can heal this without user
57
+ * action. Both manager-level causes are recoverable by re-running
58
+ * `ensureGistPersistence` with a token (#1415/#1443); a PERMANENT halt
59
+ * (e.g. token lacking the gist scope) surfaces as a thrown
60
+ * ConfigurationError at the surface that attempted recovery — the
61
+ * manager itself cannot observe it, so it never reports false here. */
62
+ recoverable: boolean;
63
+ };
64
+ }
65
+ /**
66
+ * Render the one gist-degradation warning shown by every surface (#1444).
67
+ *
68
+ * Accepts either a known {@link GistWarningCause} or `{ reason }` for the
69
+ * hard-error paths (ConfigurationError repair carve-outs) whose reason text
70
+ * is built from the underlying error.
71
+ *
72
+ * @param cause - Known cause, or a free-form reason for hard init errors.
73
+ * @param opts.retryHint - Optional trailing sentence describing the
74
+ * surface-specific retry behavior (e.g. the MCP's "Will retry on the next
75
+ * tool call.").
76
+ */
77
+ export declare function renderGistWarning(cause: GistWarningCause | {
78
+ reason: string;
79
+ }, opts?: {
80
+ retryHint?: string;
81
+ }): string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * One health source and one warning renderer for gist persistence (#1444).
3
+ *
4
+ * "Are we degraded?" used to be answered by three independent predicates
5
+ * (`config.persistence === 'gist' && !isGistMode()`, the
6
+ * `GistPersistenceStatus` union, `isGistMode() && isGistDegraded()`), each
7
+ * re-rendering its own warning prose. `StateManager.getGistHealth()` is now
8
+ * the single predicate for live-manager surfaces (daily warnings, dashboard
9
+ * probes), and {@link renderGistWarning} is the single prose renderer for
10
+ * every surface (CLI envelope, MCP injection, bootstrapGistBestEffort,
11
+ * daily warnings).
12
+ */
13
+ /** Cause → human reason, the MCP's former `gistWarningText` mapping promoted
14
+ * to core (#1444). Keep these one-line and append-only: surfaces interpolate
15
+ * them mid-sentence. */
16
+ const GIST_DEGRADED_REASON = {
17
+ 'no-token': 'no GitHub token was available',
18
+ 'state-unreadable': 'the state file could not be read',
19
+ 'init-fallback': 'initialization hit a transient network failure',
20
+ 'configured-but-local': 'this process is running local-only (Gist init has not succeeded)',
21
+ 'bootstrap-degraded': 'the Gist bootstrap fell back to the local cache (reads may be stale and pushes are failing)',
22
+ };
23
+ /**
24
+ * Render the one gist-degradation warning shown by every surface (#1444).
25
+ *
26
+ * Accepts either a known {@link GistWarningCause} or `{ reason }` for the
27
+ * hard-error paths (ConfigurationError repair carve-outs) whose reason text
28
+ * is built from the underlying error.
29
+ *
30
+ * @param cause - Known cause, or a free-form reason for hard init errors.
31
+ * @param opts.retryHint - Optional trailing sentence describing the
32
+ * surface-specific retry behavior (e.g. the MCP's "Will retry on the next
33
+ * tool call.").
34
+ */
35
+ export function renderGistWarning(cause, opts = {}) {
36
+ const reason = typeof cause === 'string' ? GIST_DEGRADED_REASON[cause] : cause.reason;
37
+ return (`Gist persistence is configured but ${reason}; writes are LOCAL-ONLY and may be ` +
38
+ `overwritten by the next successful Gist sync.${opts.retryHint ? ` ${opts.retryHint}` : ''}`);
39
+ }
@@ -232,7 +232,9 @@ export declare class GistStateStore {
232
232
  push(): Promise<boolean>;
233
233
  /**
234
234
  * Re-fetch the Gist and update the in-memory cache.
235
- * Throttled to at most once per 30 seconds.
235
+ * Throttled to at most once per 30 seconds — ATTEMPTS, not just successes:
236
+ * a failed fetch stamps the throttle too (#1443), so an outage does not
237
+ * turn every SPA poll into an immediate full re-fetch.
236
238
  *
237
239
  * Returns a discriminated union so callers can tell apart the four
238
240
  * outcomes that previously collapsed into a single boolean (#1209 L9):
@@ -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,22 +3,23 @@
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';
@@ -3,23 +3,24 @@
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';
@@ -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);
@@ -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