@oss-autopilot/core 3.6.0 → 3.7.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.
@@ -23,7 +23,35 @@ export interface LinkedPR {
23
23
  login: string;
24
24
  } | null;
25
25
  state: LinkedPRState;
26
+ /**
27
+ * ISO timestamp of the linked PR's last update, surfaced from scout's
28
+ * timeline-event metadata when available (#97). Optional so existing
29
+ * callers and fixtures that don't carry the field continue to type-check.
30
+ * Used by `isLinkedPRStalled` to flag open PRs that haven't been touched
31
+ * in the last `STALLED_PR_THRESHOLD_DAYS` days.
32
+ */
33
+ updatedAt?: string;
26
34
  }
35
+ /**
36
+ * Days of inactivity that classify an open linked PR as "stalled" — kept
37
+ * in sync with scout's `STALLED_PR_THRESHOLD_DAYS` so the autopilot helper
38
+ * and scout's own annotation use the same boundary.
39
+ */
40
+ export declare const STALLED_PR_THRESHOLD_DAYS = 30;
41
+ /**
42
+ * Determine whether an autopilot-shaped `LinkedPR` is stalled.
43
+ *
44
+ * True when:
45
+ * - the PR is open, AND
46
+ * - `updatedAt` is set, AND
47
+ * - the elapsed time since `updatedAt` is at least `thresholdDays`.
48
+ *
49
+ * Returns false for closed/merged PRs, missing `updatedAt`, or invalid
50
+ * timestamps. Reimplemented locally rather than delegating to scout's
51
+ * helper so this module owns autopilot's `LinkedPR` shape contract end
52
+ * to end (avoids a runtime import dependency on scout's internal type).
53
+ */
54
+ export declare function isLinkedPRStalled(linkedPR: LinkedPR | null | undefined, now?: Date, thresholdDays?: number): boolean;
27
55
  export declare function classifyLinkedPR(params: {
28
56
  linkedPR: LinkedPR | null;
29
57
  userLogin: string;
@@ -11,6 +11,38 @@
11
11
  * linked PR themselves (e.g. via Octokit) and hand the shape to this
12
12
  * function. See #978 for the upstream data-contract work.
13
13
  */
14
+ /**
15
+ * Days of inactivity that classify an open linked PR as "stalled" — kept
16
+ * in sync with scout's `STALLED_PR_THRESHOLD_DAYS` so the autopilot helper
17
+ * and scout's own annotation use the same boundary.
18
+ */
19
+ export const STALLED_PR_THRESHOLD_DAYS = 30;
20
+ /**
21
+ * Determine whether an autopilot-shaped `LinkedPR` is stalled.
22
+ *
23
+ * True when:
24
+ * - the PR is open, AND
25
+ * - `updatedAt` is set, AND
26
+ * - the elapsed time since `updatedAt` is at least `thresholdDays`.
27
+ *
28
+ * Returns false for closed/merged PRs, missing `updatedAt`, or invalid
29
+ * timestamps. Reimplemented locally rather than delegating to scout's
30
+ * helper so this module owns autopilot's `LinkedPR` shape contract end
31
+ * to end (avoids a runtime import dependency on scout's internal type).
32
+ */
33
+ export function isLinkedPRStalled(linkedPR, now = new Date(), thresholdDays = STALLED_PR_THRESHOLD_DAYS) {
34
+ if (!linkedPR)
35
+ return false;
36
+ if (linkedPR.state !== 'open')
37
+ return false;
38
+ if (!linkedPR.updatedAt)
39
+ return false;
40
+ const updatedMs = Date.parse(linkedPR.updatedAt);
41
+ if (!Number.isFinite(updatedMs))
42
+ return false;
43
+ const ageDays = (now.getTime() - updatedMs) / (1000 * 60 * 60 * 24);
44
+ return ageDays >= thresholdDays;
45
+ }
14
46
  /**
15
47
  * Normalize a state value from either REST (lowercase `open`/`closed`
16
48
  * plus a separate `merged` boolean) or GraphQL (uppercase `OPEN`/
@@ -509,11 +509,137 @@ export declare const SearchOutputSchema: z.ZodObject<{
509
509
  isResponsive: z.ZodBoolean;
510
510
  lastMergedAt: z.ZodOptional<z.ZodString>;
511
511
  }, z.core.$strip>>;
512
+ linkedPR: z.ZodOptional<z.ZodObject<{
513
+ number: z.ZodNumber;
514
+ state: z.ZodEnum<{
515
+ closed: "closed";
516
+ open: "open";
517
+ merged: "merged";
518
+ }>;
519
+ url: z.ZodString;
520
+ updatedAt: z.ZodOptional<z.ZodString>;
521
+ isStalled: z.ZodBoolean;
522
+ }, z.core.$strip>>;
512
523
  }, z.core.$strip>>;
513
524
  excludedRepos: z.ZodArray<z.ZodString>;
514
525
  aiPolicyBlocklist: z.ZodArray<z.ZodString>;
515
526
  rateLimitWarning: z.ZodOptional<z.ZodString>;
516
527
  }, z.core.$strip>;
528
+ export declare const FeaturesOutputSchema: z.ZodObject<{
529
+ quickWins: z.ZodArray<z.ZodObject<{
530
+ issue: z.ZodObject<{
531
+ repo: z.ZodString;
532
+ repoUrl: z.ZodString;
533
+ number: z.ZodNumber;
534
+ title: z.ZodString;
535
+ url: z.ZodString;
536
+ labels: z.ZodArray<z.ZodString>;
537
+ }, z.core.$strip>;
538
+ recommendation: z.ZodEnum<{
539
+ approve: "approve";
540
+ skip: "skip";
541
+ needs_review: "needs_review";
542
+ }>;
543
+ reasonsToApprove: z.ZodArray<z.ZodString>;
544
+ reasonsToSkip: z.ZodArray<z.ZodString>;
545
+ searchPriority: z.ZodEnum<{
546
+ normal: "normal";
547
+ merged_pr: "merged_pr";
548
+ preferred_org: "preferred_org";
549
+ starred: "starred";
550
+ }>;
551
+ viabilityScore: z.ZodNumber;
552
+ grade: z.ZodObject<{
553
+ letter: z.ZodEnum<{
554
+ A: "A";
555
+ B: "B";
556
+ C: "C";
557
+ F: "F";
558
+ }>;
559
+ reason: z.ZodString;
560
+ }, z.core.$strip>;
561
+ repoScore: z.ZodOptional<z.ZodObject<{
562
+ score: z.ZodNumber;
563
+ mergedPRCount: z.ZodNumber;
564
+ closedWithoutMergeCount: z.ZodNumber;
565
+ isResponsive: z.ZodBoolean;
566
+ lastMergedAt: z.ZodOptional<z.ZodString>;
567
+ }, z.core.$strip>>;
568
+ linkedPR: z.ZodOptional<z.ZodObject<{
569
+ number: z.ZodNumber;
570
+ state: z.ZodEnum<{
571
+ closed: "closed";
572
+ open: "open";
573
+ merged: "merged";
574
+ }>;
575
+ url: z.ZodString;
576
+ updatedAt: z.ZodOptional<z.ZodString>;
577
+ isStalled: z.ZodBoolean;
578
+ }, z.core.$strip>>;
579
+ horizon: z.ZodEnum<{
580
+ "quick-win": "quick-win";
581
+ "bigger-bet": "bigger-bet";
582
+ }>;
583
+ }, z.core.$strip>>;
584
+ biggerBets: z.ZodArray<z.ZodObject<{
585
+ issue: z.ZodObject<{
586
+ repo: z.ZodString;
587
+ repoUrl: z.ZodString;
588
+ number: z.ZodNumber;
589
+ title: z.ZodString;
590
+ url: z.ZodString;
591
+ labels: z.ZodArray<z.ZodString>;
592
+ }, z.core.$strip>;
593
+ recommendation: z.ZodEnum<{
594
+ approve: "approve";
595
+ skip: "skip";
596
+ needs_review: "needs_review";
597
+ }>;
598
+ reasonsToApprove: z.ZodArray<z.ZodString>;
599
+ reasonsToSkip: z.ZodArray<z.ZodString>;
600
+ searchPriority: z.ZodEnum<{
601
+ normal: "normal";
602
+ merged_pr: "merged_pr";
603
+ preferred_org: "preferred_org";
604
+ starred: "starred";
605
+ }>;
606
+ viabilityScore: z.ZodNumber;
607
+ grade: z.ZodObject<{
608
+ letter: z.ZodEnum<{
609
+ A: "A";
610
+ B: "B";
611
+ C: "C";
612
+ F: "F";
613
+ }>;
614
+ reason: z.ZodString;
615
+ }, z.core.$strip>;
616
+ repoScore: z.ZodOptional<z.ZodObject<{
617
+ score: z.ZodNumber;
618
+ mergedPRCount: z.ZodNumber;
619
+ closedWithoutMergeCount: z.ZodNumber;
620
+ isResponsive: z.ZodBoolean;
621
+ lastMergedAt: z.ZodOptional<z.ZodString>;
622
+ }, z.core.$strip>>;
623
+ linkedPR: z.ZodOptional<z.ZodObject<{
624
+ number: z.ZodNumber;
625
+ state: z.ZodEnum<{
626
+ closed: "closed";
627
+ open: "open";
628
+ merged: "merged";
629
+ }>;
630
+ url: z.ZodString;
631
+ updatedAt: z.ZodOptional<z.ZodString>;
632
+ isStalled: z.ZodBoolean;
633
+ }, z.core.$strip>>;
634
+ horizon: z.ZodEnum<{
635
+ "quick-win": "quick-win";
636
+ "bigger-bet": "bigger-bet";
637
+ }>;
638
+ }, z.core.$strip>>;
639
+ anchorRepos: z.ZodArray<z.ZodString>;
640
+ message: z.ZodNullable<z.ZodString>;
641
+ rateLimitWarning: z.ZodOptional<z.ZodString>;
642
+ }, z.core.$strip>;
517
643
  export declare const DoctorOutputSchema: z.ZodObject<{
518
644
  checks: z.ZodArray<z.ZodObject<{
519
645
  name: z.ZodString;
@@ -846,46 +972,95 @@ export declare const LocalReposOutputSchema: z.ZodObject<{
846
972
  cachedAt: z.ZodString;
847
973
  fromCache: z.ZodBoolean;
848
974
  }, z.core.$strip>;
975
+ /**
976
+ * Compact summary of an issue's first linked PR, surfaced on candidate
977
+ * outputs (#97 / scout 0.9.0). `isStalled` is `true` when the PR is open
978
+ * and has not been updated for `STALLED_PR_THRESHOLD_DAYS` (default 30) —
979
+ * a revive-opportunity signal callers can render or filter on.
980
+ *
981
+ * `state` mirrors autopilot's existing tri-state classifier (`'merged'` is
982
+ * folded in from scout's `merged: true` boolean), not scout's raw enum.
983
+ */
984
+ export interface CandidateLinkedPR {
985
+ number: number;
986
+ state: 'open' | 'closed' | 'merged';
987
+ url: string;
988
+ /** ISO timestamp of the PR's last update (when scout surfaces it). */
989
+ updatedAt?: string;
990
+ /** True when the PR is open AND `updatedAt` is more than 30 days old. */
991
+ isStalled: boolean;
992
+ }
993
+ /**
994
+ * One candidate row in `SearchOutput`/`FeaturesOutput`. Extracted so the
995
+ * features command can reuse the exact contract `runSearch` already
996
+ * publishes — keeping the two outputs structurally identical for everything
997
+ * except the bucket-specific `horizon` annotation.
998
+ */
999
+ export interface SearchCandidate {
1000
+ issue: {
1001
+ repo: string;
1002
+ repoUrl: string;
1003
+ number: number;
1004
+ title: string;
1005
+ url: string;
1006
+ labels: string[];
1007
+ };
1008
+ recommendation: 'approve' | 'skip' | 'needs_review';
1009
+ reasonsToApprove: string[];
1010
+ reasonsToSkip: string[];
1011
+ searchPriority: SearchPriority;
1012
+ /** 0-100 scale composite viability score. Sanitized on the boundary (#1043): out-of-contract values are coerced to 0 and logged. */
1013
+ viabilityScore: number;
1014
+ /**
1015
+ * Letter grade (A/B/C/F) computed from the autopilot-tracked repoScore.
1016
+ * Scout's `search` does not emit per-candidate projectHealth, so scout-side
1017
+ * signals are treated as unknown; unscored repos grade 'F'. See #1043.
1018
+ */
1019
+ grade: {
1020
+ letter: 'A' | 'B' | 'C' | 'F';
1021
+ reason: string;
1022
+ };
1023
+ repoScore?: {
1024
+ /** 1-10 scale repository quality score */
1025
+ score: number;
1026
+ mergedPRCount: number;
1027
+ closedWithoutMergeCount: number;
1028
+ isResponsive: boolean;
1029
+ lastMergedAt?: string;
1030
+ };
1031
+ /**
1032
+ * First linked PR on the issue, when scout surfaced one. Optional —
1033
+ * absent when no linked PR exists. `isStalled` flags revive
1034
+ * opportunities (open PR + no updates for 30+ days, scout 0.9.0 #97).
1035
+ */
1036
+ linkedPR?: CandidateLinkedPR;
1037
+ }
849
1038
  export interface SearchOutput {
850
- candidates: Array<{
851
- issue: {
852
- repo: string;
853
- repoUrl: string;
854
- number: number;
855
- title: string;
856
- url: string;
857
- labels: string[];
858
- };
859
- recommendation: 'approve' | 'skip' | 'needs_review';
860
- reasonsToApprove: string[];
861
- reasonsToSkip: string[];
862
- searchPriority: SearchPriority;
863
- /** 0-100 scale composite viability score. Sanitized on the boundary (#1043): out-of-contract values are coerced to 0 and logged. */
864
- viabilityScore: number;
865
- /**
866
- * Letter grade (A/B/C/F) computed from the autopilot-tracked repoScore.
867
- * Scout's `search` does not emit per-candidate projectHealth, so scout-side
868
- * signals are treated as unknown; unscored repos grade 'F'. See #1043.
869
- */
870
- grade: {
871
- letter: 'A' | 'B' | 'C' | 'F';
872
- reason: string;
873
- };
874
- repoScore?: {
875
- /** 1-10 scale repository quality score */
876
- score: number;
877
- mergedPRCount: number;
878
- closedWithoutMergeCount: number;
879
- isResponsive: boolean;
880
- lastMergedAt?: string;
881
- };
882
- }>;
1039
+ candidates: SearchCandidate[];
883
1040
  excludedRepos: string[];
884
1041
  /** Repos with known anti-AI contribution policies, filtered from search results (#108). */
885
1042
  aiPolicyBlocklist: string[];
886
1043
  /** Present when rate limits affected the search — either low pre-flight quota or mid-search rate limit hits (#100). */
887
1044
  rateLimitWarning?: string;
888
1045
  }
1046
+ /** Horizon classification stamped on each features-mode candidate. */
1047
+ export type FeaturesHorizon = 'quick-win' | 'bigger-bet';
1048
+ /** A `SearchCandidate` augmented with its features-mode horizon. */
1049
+ export type FeaturesCandidate = SearchCandidate & {
1050
+ horizon: FeaturesHorizon;
1051
+ };
1052
+ export interface FeaturesOutput {
1053
+ /** "Quick-win" bucket: feature-scoped issues without strong commitment markers (no milestone, no roadmap, no bigger-bet labels). */
1054
+ quickWins: FeaturesCandidate[];
1055
+ /** "Bigger-bet" bucket: feature-scoped issues that carry maintainer-commitment signals (milestone, roadmap, bigger-bet label). */
1056
+ biggerBets: FeaturesCandidate[];
1057
+ /** Repos that qualified as anchors for this run (3+ merged PRs, configurable). Empty when the user has no anchor repos yet. */
1058
+ anchorRepos: string[];
1059
+ /** Human-friendly explainer shown when neither bucket has results (no anchors, or anchors but no open feature opportunities). `null` on success. */
1060
+ message: string | null;
1061
+ /** Present when rate limits affected the search. Mirrors `SearchOutput.rateLimitWarning`. */
1062
+ rateLimitWarning?: string;
1063
+ }
889
1064
  export interface TrackOutput {
890
1065
  pr: {
891
1066
  repo: string;
@@ -991,8 +1166,14 @@ export interface CheckIntegrationOutput {
991
1166
  newFiles: NewFileInfo[];
992
1167
  unreferencedCount: number;
993
1168
  }
994
- /** Status of a re-vetted issue from the curated list (#764). */
995
- export type VetListItemStatus = 'still_available' | 'claimed' | 'closed' | 'has_pr' | 'error';
1169
+ /**
1170
+ * Status of a re-vetted issue from the curated list (#764).
1171
+ *
1172
+ * `has_stalled_pr` (scout 0.9.0 #97) distinguishes open-but-stalled linked
1173
+ * PRs from cleanly-claimed `has_pr` issues — the issue is still actionable
1174
+ * as a revive opportunity rather than something to drop.
1175
+ */
1176
+ export type VetListItemStatus = 'still_available' | 'claimed' | 'closed' | 'has_pr' | 'has_stalled_pr' | 'error';
996
1177
  /** Output of the vet-list command (#764). */
997
1178
  export interface VetListOutput {
998
1179
  results: Array<VetOutput & {
@@ -1005,6 +1186,8 @@ export interface VetListOutput {
1005
1186
  claimed: number;
1006
1187
  closed: number;
1007
1188
  hasPR: number;
1189
+ /** Open linked PRs that haven't been touched in 30+ days (scout 0.9.0 #97). Surfaced as revive opportunities, not auto-dropped. */
1190
+ hasStalledPR: number;
1008
1191
  errors: number;
1009
1192
  };
1010
1193
  pruneResult?: {
@@ -1042,6 +1225,12 @@ export interface VetOutput {
1042
1225
  * already has work in flight vs. a competing contributor.
1043
1226
  */
1044
1227
  linkedPRClassification?: 'none' | 'user_open' | 'user_closed' | 'user_merged' | 'other_open' | 'other_closed' | 'other_merged';
1228
+ /**
1229
+ * Compact linked-PR summary (#97 / scout 0.9.0). Present when the issue
1230
+ * has a linked PR; absent otherwise. `isStalled` flags open PRs that
1231
+ * haven't been touched in 30+ days as revive opportunities.
1232
+ */
1233
+ linkedPR?: CandidateLinkedPR;
1045
1234
  /**
1046
1235
  * Optional SLM pre-triage classification (#1122). Populated when the
1047
1236
  * user has set `slmTriageModel` and a local Ollama instance answered
@@ -284,6 +284,17 @@ export const CompactDailyOutputSchema = z.object({
284
284
  });
285
285
  // ── Search output schema (#1147) ─────────────────────────────────────
286
286
  const SearchPrioritySchema = z.enum(['merged_pr', 'preferred_org', 'starred', 'normal']);
287
+ /**
288
+ * Schema for the compact linked-PR annotation surfaced on candidate
289
+ * outputs (#97 / scout 0.9.0). Mirrors {@link CandidateLinkedPR}.
290
+ */
291
+ const CandidateLinkedPRSchema = z.object({
292
+ number: z.number().int().positive(),
293
+ state: z.enum(['open', 'closed', 'merged']),
294
+ url: z.string(),
295
+ updatedAt: z.string().optional(),
296
+ isStalled: z.boolean(),
297
+ });
287
298
  const SearchCandidateSchema = z.object({
288
299
  issue: z.object({
289
300
  repo: z.string(),
@@ -311,6 +322,7 @@ const SearchCandidateSchema = z.object({
311
322
  lastMergedAt: z.string().optional(),
312
323
  })
313
324
  .optional(),
325
+ linkedPR: CandidateLinkedPRSchema.optional(),
314
326
  });
315
327
  export const SearchOutputSchema = z.object({
316
328
  candidates: z.array(SearchCandidateSchema),
@@ -318,6 +330,22 @@ export const SearchOutputSchema = z.object({
318
330
  aiPolicyBlocklist: z.array(z.string()),
319
331
  rateLimitWarning: z.string().optional(),
320
332
  });
333
+ // ── Features output schema (scout 0.9.0 #97/#98/#99) ─────────────────
334
+ //
335
+ // `SearchCandidateSchema` augmented with the horizon literal that scout
336
+ // stamps in features mode. Reusing the search candidate keeps the two
337
+ // envelopes structurally identical apart from the bucket annotation.
338
+ const FeaturesHorizonSchema = z.enum(['quick-win', 'bigger-bet']);
339
+ const FeaturesCandidateSchema = SearchCandidateSchema.extend({
340
+ horizon: FeaturesHorizonSchema,
341
+ });
342
+ export const FeaturesOutputSchema = z.object({
343
+ quickWins: z.array(FeaturesCandidateSchema),
344
+ biggerBets: z.array(FeaturesCandidateSchema),
345
+ anchorRepos: z.array(z.string()),
346
+ message: z.string().nullable(),
347
+ rateLimitWarning: z.string().optional(),
348
+ });
321
349
  // ── Doctor / skip-add / list-move-tier output schemas (#1148) ────────
322
350
  const DoctorCheckSchema = z.object({
323
351
  name: z.string(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "3.6.0",
3
+ "version": "3.7.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,7 +54,7 @@
54
54
  "dependencies": {
55
55
  "@octokit/plugin-throttling": "^11.0.3",
56
56
  "@octokit/rest": "^22.0.1",
57
- "@oss-scout/core": "^0.8.0",
57
+ "@oss-scout/core": "^0.9.0",
58
58
  "commander": "^14.0.3",
59
59
  "zod": "^4.4.3"
60
60
  },