@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
@@ -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),
@@ -7,6 +7,7 @@ import { AgentState, TrackedIssue, RepoScore, RepoScoreUpdate, DailyDigest, Loca
7
7
  import { type LoadRecoveryInfo } from './state-persistence.js';
8
8
  import type { Stats } from './repo-score-manager.js';
9
9
  import { GistStateStore } from './gist-state-store.js';
10
+ import { type GistHealth } from './gist-health.js';
10
11
  export { acquireLock, releaseLock, atomicWriteFileSync } from './state-persistence.js';
11
12
  export type { LoadRecoveryInfo } from './state-persistence.js';
12
13
  export type { Stats } from './repo-score-manager.js';
@@ -135,6 +136,20 @@ export declare class StateManager {
135
136
  isGistMode(): boolean;
136
137
  /** Whether the Gist is in degraded mode (using local cache fallback). */
137
138
  isGistDegraded(): boolean;
139
+ /**
140
+ * Single source of truth for gist persistence health (#1444). Surfaces
141
+ * that need "is this process reliably syncing to the Gist?" (daily
142
+ * warnings, dashboard banners/recovery gates) derive it from here instead
143
+ * of re-combining `config.persistence` / `isGistMode()` /
144
+ * `isGistDegraded()` at each call site.
145
+ *
146
+ * Composes the same primitive fields those accessors expose:
147
+ * - no gist store + config asks for gist → `configured-but-local`
148
+ * - gist store attached but disarmed (#1443) → `bootstrap-degraded`
149
+ * (`since` comes from the staleness marker the degraded bootstrap seeds)
150
+ * - otherwise healthy (`degraded: null`)
151
+ */
152
+ getGistHealth(): GistHealth;
138
153
  /**
139
154
  * Whether per-repo guidelines (#867) are available. True iff the Gist store
140
155
  * is initialized — in local-only mode, guidelines are unavailable and
@@ -225,7 +240,15 @@ export declare class StateManager {
225
240
  * Entries with URLs that fail {@link parseGitHubUrl} are dropped before
226
241
  * persistence (read-side filters in dashboard-data already skip them, but
227
242
  * this prevents the bad data from reaching disk in the first place).
228
- * @param prs - Merged PRs to add (duplicates by URL are ignored)
243
+ *
244
+ * Re-seen URLs are not re-added, but upgrade the stored entry in place
245
+ * (#1461): outcome-ledger fields (`openedAt`, `firstMaintainerResponseAt`)
246
+ * missing on the stored entry are filled from the richer incoming one.
247
+ * Existing values are never overwritten, so stamps that live only on the
248
+ * stored entry (commentsFetchedAt, learningsExtractedAt) and earlier
249
+ * enriched writes stay intact.
250
+ *
251
+ * @param prs - Merged PRs to add (duplicates by URL upgrade in place)
229
252
  * @returns count of entries added vs. dropped (invalid URL)
230
253
  */
231
254
  addMergedPRs(prs: StoredMergedPR[]): {
@@ -241,7 +264,11 @@ export declare class StateManager {
241
264
  * Entries with URLs that fail {@link parseGitHubUrl} are dropped before
242
265
  * persistence (read-side filters in dashboard-data already skip them, but
243
266
  * this prevents the bad data from reaching disk in the first place).
244
- * @param prs - Closed PRs to add (duplicates by URL are ignored)
267
+ *
268
+ * Re-seen URLs upgrade the stored entry's missing ledger fields in place
269
+ * (#1461) — see {@link addMergedPRs} for the full semantics.
270
+ *
271
+ * @param prs - Closed PRs to add (duplicates by URL upgrade in place)
245
272
  * @returns count of entries added vs. dropped (invalid URL)
246
273
  */
247
274
  addClosedPRs(prs: StoredClosedPR[]): {
@@ -450,10 +477,14 @@ export type GistPersistenceStatus =
450
477
  | 'state-unreadable'
451
478
  /** Gist mode is configured but no token was available for this attempt. */
452
479
  | 'no-token'
453
- /** Gist mode active: the singleton is gist-backed. */
480
+ /** Gist mode active: the singleton is gist-backed AND the store is armed
481
+ * (the bootstrap actually verified its Gist — not a degraded fallback). */
454
482
  | 'gist'
455
- /** Gist mode is configured and a token was available, but init fell back
456
- * to local-only (transient network failure). A later call may recover. */
483
+ /** Gist mode is configured and a token was available, but this process is
484
+ * not writing to the Gist: init either fell back to a local-only manager
485
+ * (transient network failure) or bootstrapped DEGRADED off the local
486
+ * cache (#1443 — gist-backed but disarmed, reads stale and pushes fail).
487
+ * A later call may recover. */
457
488
  | 'degraded';
458
489
  export declare function ensureGistPersistence(token: string | null): Promise<GistPersistenceStatus>;
459
490
  /**