@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
@@ -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;
@@ -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 { isBelowMinStars } from './types.js';
@@ -14,7 +14,7 @@ const MODULE = 'scoring';
14
14
  // ── Scoring constants (#1054) ─────────────────────────────────────────
15
15
  // Previously inlined as magic numbers in `calculateScore`. Extracted with
16
16
  // rationale comments so the formula is auditable without source spelunking.
17
- // Changing any of these is a behavior change — update docs/repo-scoring.md
17
+ // Changing any of these is a behavior change — update docs/repo-scores.md
18
18
  // and the tests below in lockstep.
19
19
  /** Starting point before any signals are applied. Deliberately optimistic so first-time repos aren't punished. */
20
20
  const BASE_SCORE = 5;
@@ -66,7 +66,7 @@ function createDefaultRepoScore(repo) {
66
66
  * − (hasHostileComments ? HOSTILITY_PENALTY : 0)
67
67
  * clamped to [SCORE_MIN, SCORE_MAX].
68
68
  *
69
- * See `docs/repo-scoring.md` for user-facing intent and what a given
69
+ * See `docs/repo-scores.md` for user-facing intent and what a given
70
70
  * score means in practice.
71
71
  */
72
72
  export function calculateScore(repoScore) {
@@ -11,7 +11,7 @@
11
11
  * supply pre-fetched repo signals so the score is reproducible
12
12
  * against fixture data and offline replays.
13
13
  *
14
- * Rubric reference: docs/repo-rubric.md.
14
+ * Rubric reference: docs/repo-scores.md.
15
15
  */
16
16
  export interface RepoVetInput {
17
17
  /** Star count from the GitHub repo metadata. */
@@ -78,7 +78,7 @@ export interface RepoVetResult {
78
78
  prTemplate: boolean;
79
79
  codeOfConduct: boolean;
80
80
  };
81
- /** Weighted 1-10 score per docs/repo-rubric.md. */
81
+ /** Weighted 1-10 score per docs/repo-scores.md. */
82
82
  rubricScore: number;
83
83
  /** Top-line verdict derived from the score and red-flag overrides. */
84
84
  rubricVerdict: RepoVetVerdict;
@@ -11,7 +11,7 @@
11
11
  * supply pre-fetched repo signals so the score is reproducible
12
12
  * against fixture data and offline replays.
13
13
  *
14
- * Rubric reference: docs/repo-rubric.md.
14
+ * Rubric reference: docs/repo-scores.md.
15
15
  */
16
16
  const WEIGHTS = {
17
17
  activity: 0.25,
@@ -53,6 +53,25 @@ export declare function getInlineCommentBody(reviewId: number, reviewComments: R
53
53
  * that also posted inline comments (#829).
54
54
  */
55
55
  export declare function reviewHasInlineComments(reviewId: number, reviewComments: ReviewComment[]): boolean;
56
+ /**
57
+ * Earliest maintainer response timestamp across issue comments and submitted
58
+ * reviews (#1461). A "response" is any comment or review by a non-bot
59
+ * account other than the contributor — acknowledgments and approvals count,
60
+ * since for latency purposes any human maintainer reply is a response.
61
+ * Computed from data fetchPRDetails already holds in memory (zero extra API
62
+ * calls). Returns undefined when no maintainer has responded yet.
63
+ */
64
+ export declare function getFirstMaintainerResponseAt(comments: Array<{
65
+ user?: {
66
+ login?: string;
67
+ } | null;
68
+ created_at: string;
69
+ }>, reviews: Array<{
70
+ user?: {
71
+ login?: string;
72
+ } | null;
73
+ submitted_at?: string | null;
74
+ }>, username: string): string | undefined;
56
75
  /**
57
76
  * Check if there are unresponded comments from maintainers.
58
77
  * Combines issue comments and review comments into a timeline,
@@ -97,6 +97,34 @@ export function getInlineCommentBody(reviewId, reviewComments) {
97
97
  export function reviewHasInlineComments(reviewId, reviewComments) {
98
98
  return reviewComments.some((c) => c.pull_request_review_id === reviewId);
99
99
  }
100
+ /**
101
+ * Earliest maintainer response timestamp across issue comments and submitted
102
+ * reviews (#1461). A "response" is any comment or review by a non-bot
103
+ * account other than the contributor — acknowledgments and approvals count,
104
+ * since for latency purposes any human maintainer reply is a response.
105
+ * Computed from data fetchPRDetails already holds in memory (zero extra API
106
+ * calls). Returns undefined when no maintainer has responded yet.
107
+ */
108
+ export function getFirstMaintainerResponseAt(comments, reviews, username) {
109
+ const usernameLower = username.toLowerCase();
110
+ let earliest;
111
+ const consider = (author, createdAt) => {
112
+ if (!author || !createdAt)
113
+ return;
114
+ if (author.toLowerCase() === usernameLower)
115
+ return;
116
+ if (isBotAuthor(author))
117
+ return;
118
+ if (!earliest || new Date(createdAt).getTime() < new Date(earliest).getTime()) {
119
+ earliest = createdAt;
120
+ }
121
+ };
122
+ for (const comment of comments)
123
+ consider(comment.user?.login, comment.created_at);
124
+ for (const review of reviews)
125
+ consider(review.user?.login, review.submitted_at);
126
+ return earliest;
127
+ }
100
128
  /**
101
129
  * Check if there are unresponded comments from maintainers.
102
130
  * Combines issue comments and review comments into a timeline,
@@ -9,6 +9,7 @@
9
9
  * Unknown keys are stripped by default (Zod 4 behavior).
10
10
  */
11
11
  import { z } from 'zod';
12
+ import type { FetchedPR } from './types.js';
12
13
  export declare const IssueStatusSchema: z.ZodEnum<{
13
14
  candidate: "candidate";
14
15
  claimed: "claimed";
@@ -63,6 +64,8 @@ export declare const StoredMergedPRSchema: z.ZodObject<{
63
64
  url: z.ZodString;
64
65
  title: z.ZodString;
65
66
  mergedAt: z.ZodString;
67
+ openedAt: z.ZodOptional<z.ZodString>;
68
+ firstMaintainerResponseAt: z.ZodOptional<z.ZodString>;
66
69
  commentsFetchedAt: z.ZodOptional<z.ZodString>;
67
70
  learningsExtractedAt: z.ZodOptional<z.ZodString>;
68
71
  }, z.core.$strip>;
@@ -70,6 +73,8 @@ export declare const StoredClosedPRSchema: z.ZodObject<{
70
73
  url: z.ZodString;
71
74
  title: z.ZodString;
72
75
  closedAt: z.ZodString;
76
+ openedAt: z.ZodOptional<z.ZodString>;
77
+ firstMaintainerResponseAt: z.ZodOptional<z.ZodString>;
73
78
  commentsFetchedAt: z.ZodOptional<z.ZodString>;
74
79
  learningsExtractedAt: z.ZodOptional<z.ZodString>;
75
80
  }, z.core.$strip>;
@@ -251,6 +256,8 @@ export declare const AgentConfigSchema: z.ZodObject<{
251
256
  }>>>;
252
257
  excludeRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
253
258
  excludeOrgs: z.ZodOptional<z.ZodArray<z.ZodString>>;
259
+ avoidRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
260
+ boostIssueTypes: z.ZodDefault<z.ZodArray<z.ZodString>>;
254
261
  trustedProjects: z.ZodDefault<z.ZodArray<z.ZodString>>;
255
262
  githubUsername: z.ZodDefault<z.ZodString>;
256
263
  minRepoScoreThreshold: z.ZodDefault<z.ZodNumber>;
@@ -311,6 +318,7 @@ export declare const ClosedPRSchema: z.ZodObject<{
311
318
  title: z.ZodString;
312
319
  closedAt: z.ZodString;
313
320
  closedBy: z.ZodOptional<z.ZodString>;
321
+ openedAt: z.ZodOptional<z.ZodString>;
314
322
  }, z.core.$strip>;
315
323
  export declare const MergedPRSchema: z.ZodObject<{
316
324
  url: z.ZodString;
@@ -318,6 +326,7 @@ export declare const MergedPRSchema: z.ZodObject<{
318
326
  number: z.ZodNumber;
319
327
  title: z.ZodString;
320
328
  mergedAt: z.ZodString;
329
+ openedAt: z.ZodOptional<z.ZodString>;
321
330
  }, z.core.$strip>;
322
331
  export declare const DailyDigestSummarySchema: z.ZodObject<{
323
332
  totalActivePRs: z.ZodNumber;
@@ -325,11 +334,29 @@ export declare const DailyDigestSummarySchema: z.ZodObject<{
325
334
  totalMergedAllTime: z.ZodNumber;
326
335
  mergeRate: z.ZodNumber;
327
336
  }, z.core.$strip>;
337
+ /**
338
+ * Minimal FetchedPR validation for persisted digest arrays (#1456). The
339
+ * persisted digest is rendered by the dashboard from cold start (before any
340
+ * refresh), so the fields it dereferences must be guaranteed present. Only
341
+ * the identity/status core is pinned — required on every real `FetchedPR`
342
+ * since the type's introduction — and `.passthrough()` keeps the dozens of
343
+ * volatile enrichment fields (CI, review, staleness) from rejecting digests
344
+ * persisted by older or newer producers.
345
+ */
346
+ export declare const FetchedPRSchema: z.ZodObject<{
347
+ url: z.ZodString;
348
+ repo: z.ZodString;
349
+ number: z.ZodNumber;
350
+ status: z.ZodEnum<{
351
+ needs_addressing: "needs_addressing";
352
+ waiting_on_maintainer: "waiting_on_maintainer";
353
+ }>;
354
+ }, z.core.$loose>;
328
355
  export declare const DailyDigestSchema: z.ZodObject<{
329
356
  generatedAt: z.ZodString;
330
- openPRs: z.ZodArray<z.ZodAny>;
331
- needsAddressingPRs: z.ZodArray<z.ZodAny>;
332
- waitingOnMaintainerPRs: z.ZodArray<z.ZodAny>;
357
+ openPRs: z.ZodType<FetchedPR[], unknown, z.core.$ZodTypeInternals<FetchedPR[], unknown>>;
358
+ needsAddressingPRs: z.ZodType<FetchedPR[], unknown, z.core.$ZodTypeInternals<FetchedPR[], unknown>>;
359
+ waitingOnMaintainerPRs: z.ZodType<FetchedPR[], unknown, z.core.$ZodTypeInternals<FetchedPR[], unknown>>;
333
360
  recentlyClosedPRs: z.ZodArray<z.ZodObject<{
334
361
  url: z.ZodString;
335
362
  repo: z.ZodString;
@@ -337,6 +364,7 @@ export declare const DailyDigestSchema: z.ZodObject<{
337
364
  title: z.ZodString;
338
365
  closedAt: z.ZodString;
339
366
  closedBy: z.ZodOptional<z.ZodString>;
367
+ openedAt: z.ZodOptional<z.ZodString>;
340
368
  }, z.core.$strip>>;
341
369
  recentlyMergedPRs: z.ZodArray<z.ZodObject<{
342
370
  url: z.ZodString;
@@ -344,6 +372,7 @@ export declare const DailyDigestSchema: z.ZodObject<{
344
372
  number: z.ZodNumber;
345
373
  title: z.ZodString;
346
374
  mergedAt: z.ZodString;
375
+ openedAt: z.ZodOptional<z.ZodString>;
347
376
  }, z.core.$strip>>;
348
377
  shelvedPRs: z.ZodArray<z.ZodObject<{
349
378
  number: z.ZodNumber;
@@ -413,6 +442,8 @@ export declare const AgentStateSchema: z.ZodObject<{
413
442
  }>>>;
414
443
  excludeRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
415
444
  excludeOrgs: z.ZodOptional<z.ZodArray<z.ZodString>>;
445
+ avoidRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
446
+ boostIssueTypes: z.ZodDefault<z.ZodArray<z.ZodString>>;
416
447
  trustedProjects: z.ZodDefault<z.ZodArray<z.ZodString>>;
417
448
  githubUsername: z.ZodDefault<z.ZodString>;
418
449
  minRepoScoreThreshold: z.ZodDefault<z.ZodNumber>;
@@ -462,9 +493,9 @@ export declare const AgentStateSchema: z.ZodObject<{
462
493
  lastStrategyAt: z.ZodOptional<z.ZodString>;
463
494
  lastDigest: z.ZodOptional<z.ZodObject<{
464
495
  generatedAt: z.ZodString;
465
- openPRs: z.ZodArray<z.ZodAny>;
466
- needsAddressingPRs: z.ZodArray<z.ZodAny>;
467
- waitingOnMaintainerPRs: z.ZodArray<z.ZodAny>;
496
+ openPRs: z.ZodType<FetchedPR[], unknown, z.core.$ZodTypeInternals<FetchedPR[], unknown>>;
497
+ needsAddressingPRs: z.ZodType<FetchedPR[], unknown, z.core.$ZodTypeInternals<FetchedPR[], unknown>>;
498
+ waitingOnMaintainerPRs: z.ZodType<FetchedPR[], unknown, z.core.$ZodTypeInternals<FetchedPR[], unknown>>;
468
499
  recentlyClosedPRs: z.ZodArray<z.ZodObject<{
469
500
  url: z.ZodString;
470
501
  repo: z.ZodString;
@@ -472,6 +503,7 @@ export declare const AgentStateSchema: z.ZodObject<{
472
503
  title: z.ZodString;
473
504
  closedAt: z.ZodString;
474
505
  closedBy: z.ZodOptional<z.ZodString>;
506
+ openedAt: z.ZodOptional<z.ZodString>;
475
507
  }, z.core.$strip>>;
476
508
  recentlyMergedPRs: z.ZodArray<z.ZodObject<{
477
509
  url: z.ZodString;
@@ -479,6 +511,7 @@ export declare const AgentStateSchema: z.ZodObject<{
479
511
  number: z.ZodNumber;
480
512
  title: z.ZodString;
481
513
  mergedAt: z.ZodString;
514
+ openedAt: z.ZodOptional<z.ZodString>;
482
515
  }, z.core.$strip>>;
483
516
  shelvedPRs: z.ZodArray<z.ZodObject<{
484
517
  number: z.ZodNumber;
@@ -525,6 +558,8 @@ export declare const AgentStateSchema: z.ZodObject<{
525
558
  url: z.ZodString;
526
559
  title: z.ZodString;
527
560
  mergedAt: z.ZodString;
561
+ openedAt: z.ZodOptional<z.ZodString>;
562
+ firstMaintainerResponseAt: z.ZodOptional<z.ZodString>;
528
563
  commentsFetchedAt: z.ZodOptional<z.ZodString>;
529
564
  learningsExtractedAt: z.ZodOptional<z.ZodString>;
530
565
  }, z.core.$strip>>>;
@@ -532,6 +567,8 @@ export declare const AgentStateSchema: z.ZodObject<{
532
567
  url: z.ZodString;
533
568
  title: z.ZodString;
534
569
  closedAt: z.ZodString;
570
+ openedAt: z.ZodOptional<z.ZodString>;
571
+ firstMaintainerResponseAt: z.ZodOptional<z.ZodString>;
535
572
  commentsFetchedAt: z.ZodOptional<z.ZodString>;
536
573
  learningsExtractedAt: z.ZodOptional<z.ZodString>;
537
574
  }, z.core.$strip>>>;
@@ -44,6 +44,20 @@ export const StoredMergedPRSchema = z.object({
44
44
  url: z.string(),
45
45
  title: z.string(),
46
46
  mergedAt: z.string(),
47
+ /**
48
+ * When the PR was opened (#1461). Optional: entries persisted before the
49
+ * outcome ledger existed don't carry it. Sourced from the Search API's
50
+ * `created_at` at merge-detection time — zero extra API calls.
51
+ */
52
+ openedAt: z.string().optional(),
53
+ /**
54
+ * Earliest maintainer (non-bot, non-author) comment or review on the
55
+ * PR (#1461). Best-effort: only derivable when the PR was enriched while
56
+ * open (looked up from the previous run's persisted digest at detection
57
+ * time), so it is absent for PRs that merged before ever appearing in an
58
+ * enriched run.
59
+ */
60
+ firstMaintainerResponseAt: z.string().optional(),
47
61
  /** When the raw review-comment bundle for this PR was last fetched (#867). */
48
62
  // ISO-8601 datetime guards against `markPRCommentsFetched(url, "garbage")`
49
63
  // poisoning state through the stamping API (#1209 L4).
@@ -55,6 +69,13 @@ export const StoredClosedPRSchema = z.object({
55
69
  url: z.string(),
56
70
  title: z.string(),
57
71
  closedAt: z.string(),
72
+ /** When the PR was opened (#1461). See StoredMergedPRSchema.openedAt. */
73
+ openedAt: z.string().optional(),
74
+ /**
75
+ * Earliest maintainer response (#1461). See
76
+ * StoredMergedPRSchema.firstMaintainerResponseAt.
77
+ */
78
+ firstMaintainerResponseAt: z.string().optional(),
58
79
  /** When the raw review-comment bundle for this PR was last fetched (#867). */
59
80
  // ISO-8601 datetime guards against `markPRCommentsFetched(url, "garbage")`
60
81
  // poisoning state through the stamping API (#1209 L4).
@@ -176,6 +197,21 @@ export const AgentConfigSchema = z.object({
176
197
  scope: z.array(IssueScopeSchema).optional(),
177
198
  excludeRepos: z.array(z.string()).default([]),
178
199
  excludeOrgs: z.array(z.string()).optional(),
200
+ /**
201
+ * Repos (owner/repo) to softly downrank in discovery results (#1464).
202
+ * Milder than `excludeRepos`' hard filter: scout pushes matches below
203
+ * equally-recommended candidates but does not remove them, and a strong
204
+ * affinity boost can still outweigh the penalty (scout #168).
205
+ * Threaded to scout via `scout-bridge.ts`.
206
+ */
207
+ avoidRepos: z.array(z.string()).default([]),
208
+ /**
209
+ * Issue label types (e.g. "bug", "good first issue") to softly boost in
210
+ * discovery ranking, matched case-insensitively against issue labels
211
+ * (scout #168 / #1464). Does not filter results or change viability
212
+ * scores. Threaded to scout via `scout-bridge.ts`.
213
+ */
214
+ boostIssueTypes: z.array(z.string()).default([]),
179
215
  trustedProjects: z.array(z.string()).default([]),
180
216
  githubUsername: z.string().default(''),
181
217
  minRepoScoreThreshold: z.number().default(4),
@@ -251,6 +287,12 @@ export const ClosedPRSchema = z.object({
251
287
  title: z.string(),
252
288
  closedAt: z.string(),
253
289
  closedBy: z.string().optional(),
290
+ /**
291
+ * When the PR was opened (#1461). Carried from the Search API's
292
+ * `created_at` so close detection can persist it into the outcome ledger.
293
+ * Optional: digests persisted by older producers don't have it.
294
+ */
295
+ openedAt: z.string().optional(),
254
296
  });
255
297
  export const MergedPRSchema = z.object({
256
298
  url: z.string(),
@@ -258,6 +300,12 @@ export const MergedPRSchema = z.object({
258
300
  number: z.number(),
259
301
  title: z.string(),
260
302
  mergedAt: z.string(),
303
+ /**
304
+ * When the PR was opened (#1461). Carried from the Search API's
305
+ * `created_at` so merge detection can persist it into the outcome ledger.
306
+ * Optional: digests persisted by older producers don't have it.
307
+ */
308
+ openedAt: z.string().optional(),
261
309
  });
262
310
  export const DailyDigestSummarySchema = z.object({
263
311
  totalActivePRs: z.number(),
@@ -265,12 +313,41 @@ export const DailyDigestSummarySchema = z.object({
265
313
  totalMergedAllTime: z.number(),
266
314
  mergeRate: z.number(),
267
315
  });
316
+ /**
317
+ * Minimal FetchedPR validation for persisted digest arrays (#1456). The
318
+ * persisted digest is rendered by the dashboard from cold start (before any
319
+ * refresh), so the fields it dereferences must be guaranteed present. Only
320
+ * the identity/status core is pinned — required on every real `FetchedPR`
321
+ * since the type's introduction — and `.passthrough()` keeps the dozens of
322
+ * volatile enrichment fields (CI, review, staleness) from rejecting digests
323
+ * persisted by older or newer producers.
324
+ */
325
+ export const FetchedPRSchema = z
326
+ .object({
327
+ url: z.string(),
328
+ repo: z.string(),
329
+ number: z.number(),
330
+ status: FetchedPRStatusSchema,
331
+ })
332
+ .passthrough();
333
+ /**
334
+ * The digest arrays hold full `FetchedPR` objects at runtime; validation is
335
+ * intentionally limited to the identity/status core above (everything else
336
+ * is ephemeral enrichment that older/newer producers legitimately differ
337
+ * on), so the inferred type is widened back to `FetchedPR[]` for consumers.
338
+ * The cast is sound in both directions: every real `FetchedPR` satisfies the
339
+ * core schema, and `.passthrough()` preserves the enrichment fields on parse
340
+ * output. This mirrors the pre-#1456 `z.array(z.any())` static contract
341
+ * while actually validating the fields the dashboard dereferences.
342
+ */
343
+ const FetchedPRArraySchema = z.array(FetchedPRSchema);
268
344
  export const DailyDigestSchema = z.object({
269
345
  generatedAt: z.string(),
270
- // FetchedPR arrays — ephemeral, regenerated each run. Validated loosely.
271
- openPRs: z.array(z.any()),
272
- needsAddressingPRs: z.array(z.any()),
273
- waitingOnMaintainerPRs: z.array(z.any()),
346
+ // FetchedPR arrays — ephemeral, regenerated each run. Validated for the
347
+ // identity/status core the dashboard dereferences; loose on the rest.
348
+ openPRs: FetchedPRArraySchema,
349
+ needsAddressingPRs: FetchedPRArraySchema,
350
+ waitingOnMaintainerPRs: FetchedPRArraySchema,
274
351
  recentlyClosedPRs: z.array(ClosedPRSchema),
275
352
  recentlyMergedPRs: z.array(MergedPRSchema),
276
353
  shelvedPRs: z.array(ShelvedPRRefSchema),