@oss-scout/core 0.10.0 → 1.0.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 (64) hide show
  1. package/dist/cli.bundle.cjs +77 -60
  2. package/dist/cli.js +403 -416
  3. package/dist/commands/command-scout.d.ts +21 -0
  4. package/dist/commands/command-scout.js +21 -0
  5. package/dist/commands/config.js +10 -128
  6. package/dist/commands/features.js +15 -28
  7. package/dist/commands/results.d.ts +13 -2
  8. package/dist/commands/results.js +29 -2
  9. package/dist/commands/search.d.ts +7 -0
  10. package/dist/commands/search.js +63 -68
  11. package/dist/commands/setup.d.ts +2 -0
  12. package/dist/commands/setup.js +35 -6
  13. package/dist/commands/skip.d.ts +4 -0
  14. package/dist/commands/skip.js +45 -55
  15. package/dist/commands/sync.d.ts +10 -0
  16. package/dist/commands/sync.js +10 -0
  17. package/dist/commands/vet-list.js +3 -19
  18. package/dist/commands/vet.js +18 -25
  19. package/dist/commands/with-scout.d.ts +32 -0
  20. package/dist/commands/with-scout.js +41 -0
  21. package/dist/core/anti-llm-policy.js +4 -5
  22. package/dist/core/bootstrap.d.ts +2 -2
  23. package/dist/core/bootstrap.js +5 -9
  24. package/dist/core/errors.d.ts +10 -0
  25. package/dist/core/errors.js +20 -5
  26. package/dist/core/feature-discovery.d.ts +13 -1
  27. package/dist/core/feature-discovery.js +104 -81
  28. package/dist/core/gist-state-store.d.ts +13 -12
  29. package/dist/core/gist-state-store.js +128 -53
  30. package/dist/core/http-cache.d.ts +32 -2
  31. package/dist/core/http-cache.js +74 -19
  32. package/dist/core/issue-discovery.d.ts +3 -0
  33. package/dist/core/issue-discovery.js +51 -31
  34. package/dist/core/issue-eligibility.d.ts +10 -4
  35. package/dist/core/issue-eligibility.js +119 -67
  36. package/dist/core/issue-graphql.d.ts +58 -0
  37. package/dist/core/issue-graphql.js +108 -0
  38. package/dist/core/issue-vetting.d.ts +105 -8
  39. package/dist/core/issue-vetting.js +234 -107
  40. package/dist/core/local-state.d.ts +6 -2
  41. package/dist/core/local-state.js +23 -5
  42. package/dist/core/logger.d.ts +12 -4
  43. package/dist/core/logger.js +33 -7
  44. package/dist/core/personalization.d.ts +51 -18
  45. package/dist/core/personalization.js +101 -27
  46. package/dist/core/preference-fields.d.ts +47 -0
  47. package/dist/core/preference-fields.js +178 -0
  48. package/dist/core/repo-health.js +31 -15
  49. package/dist/core/roadmap.js +17 -3
  50. package/dist/core/schemas.d.ts +144 -26
  51. package/dist/core/schemas.js +74 -17
  52. package/dist/core/search-budget.d.ts +9 -0
  53. package/dist/core/search-budget.js +36 -3
  54. package/dist/core/search-phases.d.ts +0 -18
  55. package/dist/core/search-phases.js +27 -82
  56. package/dist/core/types.d.ts +146 -30
  57. package/dist/core/utils.js +60 -26
  58. package/dist/formatters/markdown.d.ts +10 -0
  59. package/dist/formatters/markdown.js +31 -0
  60. package/dist/index.d.ts +6 -2
  61. package/dist/index.js +8 -0
  62. package/dist/scout.d.ts +59 -10
  63. package/dist/scout.js +244 -19
  64. package/package.json +1 -1
@@ -7,9 +7,6 @@
7
7
  import { z } from "zod";
8
8
  export declare const IssueStatusSchema: z.ZodEnum<{
9
9
  candidate: "candidate";
10
- claimed: "claimed";
11
- in_progress: "in_progress";
12
- pr_submitted: "pr_submitted";
13
10
  }>;
14
11
  export declare const ProjectCategorySchema: z.ZodEnum<{
15
12
  nonprofit: "nonprofit";
@@ -37,7 +34,7 @@ export declare const RepoSignalsSchema: z.ZodObject<{
37
34
  hasActiveMaintainers: z.ZodBoolean;
38
35
  isResponsive: z.ZodBoolean;
39
36
  hasHostileComments: z.ZodBoolean;
40
- }, z.core.$strip>;
37
+ }, z.core.$loose>;
41
38
  export declare const RepoScoreSchema: z.ZodObject<{
42
39
  repo: z.ZodString;
43
40
  score: z.ZodNumber;
@@ -50,25 +47,25 @@ export declare const RepoScoreSchema: z.ZodObject<{
50
47
  hasActiveMaintainers: z.ZodBoolean;
51
48
  isResponsive: z.ZodBoolean;
52
49
  hasHostileComments: z.ZodBoolean;
53
- }, z.core.$strip>;
50
+ }, z.core.$loose>;
54
51
  stargazersCount: z.ZodOptional<z.ZodNumber>;
55
52
  language: z.ZodOptional<z.ZodNullable<z.ZodString>>;
56
- }, z.core.$strip>;
53
+ }, z.core.$loose>;
57
54
  export declare const StoredMergedPRSchema: z.ZodObject<{
58
55
  url: z.ZodString;
59
56
  title: z.ZodString;
60
57
  mergedAt: z.ZodString;
61
- }, z.core.$strip>;
58
+ }, z.core.$loose>;
62
59
  export declare const StoredClosedPRSchema: z.ZodObject<{
63
60
  url: z.ZodString;
64
61
  title: z.ZodString;
65
62
  closedAt: z.ZodString;
66
- }, z.core.$strip>;
63
+ }, z.core.$loose>;
67
64
  export declare const StoredOpenPRSchema: z.ZodObject<{
68
65
  url: z.ZodString;
69
66
  title: z.ZodString;
70
67
  openedAt: z.ZodString;
71
- }, z.core.$strip>;
68
+ }, z.core.$loose>;
72
69
  export declare const ContributionGuidelinesSchema: z.ZodObject<{
73
70
  branchNamingConvention: z.ZodOptional<z.ZodString>;
74
71
  commitMessageFormat: z.ZodOptional<z.ZodString>;
@@ -142,9 +139,6 @@ export declare const TrackedIssueSchema: z.ZodObject<{
142
139
  title: z.ZodString;
143
140
  status: z.ZodEnum<{
144
141
  candidate: "candidate";
145
- claimed: "claimed";
146
- in_progress: "in_progress";
147
- pr_submitted: "pr_submitted";
148
142
  }>;
149
143
  labels: z.ZodArray<z.ZodString>;
150
144
  createdAt: z.ZodString;
@@ -195,7 +189,7 @@ export declare const SkippedIssueSchema: z.ZodObject<{
195
189
  number: z.ZodNumber;
196
190
  title: z.ZodString;
197
191
  skippedAt: z.ZodString;
198
- }, z.core.$strip>;
192
+ }, z.core.$loose>;
199
193
  export declare const HorizonSchema: z.ZodEnum<{
200
194
  "quick-win": "quick-win";
201
195
  "bigger-bet": "bigger-bet";
@@ -212,7 +206,11 @@ export declare const SavedCandidateSchema: z.ZodObject<{
212
206
  needs_review: "needs_review";
213
207
  }>;
214
208
  viabilityScore: z.ZodNumber;
215
- searchPriority: z.ZodString;
209
+ searchPriority: z.ZodCatch<z.ZodEnum<{
210
+ normal: "normal";
211
+ starred: "starred";
212
+ merged_pr: "merged_pr";
213
+ }>>;
216
214
  firstSeenAt: z.ZodString;
217
215
  lastSeenAt: z.ZodString;
218
216
  lastScore: z.ZodNumber;
@@ -220,7 +218,18 @@ export declare const SavedCandidateSchema: z.ZodObject<{
220
218
  "quick-win": "quick-win";
221
219
  "bigger-bet": "bigger-bet";
222
220
  }>>;
223
- }, z.core.$strip>;
221
+ /**
222
+ * Availability status recorded by the previous `vet-list` run, so the next
223
+ * run can report transitions ("was available, now claimed") instead of
224
+ * re-diffing full snapshots (#165). "error" is never stored.
225
+ */
226
+ lastStatus: z.ZodOptional<z.ZodEnum<{
227
+ closed: "closed";
228
+ still_available: "still_available";
229
+ claimed: "claimed";
230
+ has_pr: "has_pr";
231
+ }>>;
232
+ }, z.core.$loose>;
224
233
  export declare const PersistenceModeSchema: z.ZodEnum<{
225
234
  local: "local";
226
235
  gist: "gist";
@@ -261,13 +270,48 @@ export declare const ScoutPreferencesSchema: z.ZodObject<{
261
270
  broad: "broad";
262
271
  maintained: "maintained";
263
272
  }>>>;
273
+ /**
274
+ * Persisted personalization defaults (#168). The `--prefer-languages`,
275
+ * `--prefer-repos`, and `--diversity-ratio` search flags override these when
276
+ * passed, so a stored preference removes the need to retype the boost every
277
+ * search. Empty / 0 disables the corresponding signal (same as the flags).
278
+ */
279
+ preferLanguages: z.ZodDefault<z.ZodArray<z.ZodString>>;
280
+ preferRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
281
+ diversityRatio: z.ZodDefault<z.ZodNumber>;
264
282
  broadPhaseDelayMs: z.ZodDefault<z.ZodNumber>;
283
+ /**
284
+ * Skip the expensive broad phase once this many candidates were found by
285
+ * the cheaper phases. Clamped at runtime to maxResults - 1 so it stays
286
+ * satisfiable; 0 disables skipping entirely.
287
+ */
265
288
  skipBroadWhenSufficientResults: z.ZodDefault<z.ZodNumber>;
289
+ /**
290
+ * Optional Ollama model id used for SLM pre-triage during vetting
291
+ * (oss-autopilot#1122). Empty disables the feature. Recommended values:
292
+ * `gemma4:e4b` (default for capable hardware) or `gemma4:e2b` /
293
+ * `qwen3:1.7b` for low-RAM machines.
294
+ */
266
295
  slmTriageModel: z.ZodDefault<z.ZodString>;
296
+ /**
297
+ * Override the Ollama HTTP host. Defaults to `http://127.0.0.1:11434`
298
+ * when empty. Useful when Ollama runs on a different machine on the
299
+ * local network.
300
+ */
267
301
  slmTriageHost: z.ZodDefault<z.ZodString>;
302
+ /**
303
+ * Minimum merged-PR count for a repo to qualify as an anchor in
304
+ * `scout features` (#98). Lowering surfaces more anchors at the cost
305
+ * of weaker prior engagement signal.
306
+ */
268
307
  featuresAnchorThreshold: z.ZodDefault<z.ZodNumber>;
308
+ /**
309
+ * Quick-wins / bigger-bets split ratio for `scout features` (#99).
310
+ * 0.6 means 60% quick wins, 40% bigger bets when both pools are
311
+ * abundant. Deficits redirect to the other bucket.
312
+ */
269
313
  featuresSplitRatio: z.ZodDefault<z.ZodNumber>;
270
- }, z.core.$strip>;
314
+ }, z.core.$loose>;
271
315
  export declare const ScoutStateSchema: z.ZodObject<{
272
316
  version: z.ZodLiteral<1>;
273
317
  preferences: z.ZodDefault<z.ZodObject<{
@@ -306,13 +350,48 @@ export declare const ScoutStateSchema: z.ZodObject<{
306
350
  broad: "broad";
307
351
  maintained: "maintained";
308
352
  }>>>;
353
+ /**
354
+ * Persisted personalization defaults (#168). The `--prefer-languages`,
355
+ * `--prefer-repos`, and `--diversity-ratio` search flags override these when
356
+ * passed, so a stored preference removes the need to retype the boost every
357
+ * search. Empty / 0 disables the corresponding signal (same as the flags).
358
+ */
359
+ preferLanguages: z.ZodDefault<z.ZodArray<z.ZodString>>;
360
+ preferRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
361
+ diversityRatio: z.ZodDefault<z.ZodNumber>;
309
362
  broadPhaseDelayMs: z.ZodDefault<z.ZodNumber>;
363
+ /**
364
+ * Skip the expensive broad phase once this many candidates were found by
365
+ * the cheaper phases. Clamped at runtime to maxResults - 1 so it stays
366
+ * satisfiable; 0 disables skipping entirely.
367
+ */
310
368
  skipBroadWhenSufficientResults: z.ZodDefault<z.ZodNumber>;
369
+ /**
370
+ * Optional Ollama model id used for SLM pre-triage during vetting
371
+ * (oss-autopilot#1122). Empty disables the feature. Recommended values:
372
+ * `gemma4:e4b` (default for capable hardware) or `gemma4:e2b` /
373
+ * `qwen3:1.7b` for low-RAM machines.
374
+ */
311
375
  slmTriageModel: z.ZodDefault<z.ZodString>;
376
+ /**
377
+ * Override the Ollama HTTP host. Defaults to `http://127.0.0.1:11434`
378
+ * when empty. Useful when Ollama runs on a different machine on the
379
+ * local network.
380
+ */
312
381
  slmTriageHost: z.ZodDefault<z.ZodString>;
382
+ /**
383
+ * Minimum merged-PR count for a repo to qualify as an anchor in
384
+ * `scout features` (#98). Lowering surfaces more anchors at the cost
385
+ * of weaker prior engagement signal.
386
+ */
313
387
  featuresAnchorThreshold: z.ZodDefault<z.ZodNumber>;
388
+ /**
389
+ * Quick-wins / bigger-bets split ratio for `scout features` (#99).
390
+ * 0.6 means 60% quick wins, 40% bigger bets when both pools are
391
+ * abundant. Deficits redirect to the other bucket.
392
+ */
314
393
  featuresSplitRatio: z.ZodDefault<z.ZodNumber>;
315
- }, z.core.$strip>>;
394
+ }, z.core.$loose>>;
316
395
  repoScores: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
317
396
  repo: z.ZodString;
318
397
  score: z.ZodNumber;
@@ -325,27 +404,27 @@ export declare const ScoutStateSchema: z.ZodObject<{
325
404
  hasActiveMaintainers: z.ZodBoolean;
326
405
  isResponsive: z.ZodBoolean;
327
406
  hasHostileComments: z.ZodBoolean;
328
- }, z.core.$strip>;
407
+ }, z.core.$loose>;
329
408
  stargazersCount: z.ZodOptional<z.ZodNumber>;
330
409
  language: z.ZodOptional<z.ZodNullable<z.ZodString>>;
331
- }, z.core.$strip>>>;
410
+ }, z.core.$loose>>>;
332
411
  starredRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
333
412
  starredReposLastFetched: z.ZodOptional<z.ZodString>;
334
413
  mergedPRs: z.ZodDefault<z.ZodArray<z.ZodObject<{
335
414
  url: z.ZodString;
336
415
  title: z.ZodString;
337
416
  mergedAt: z.ZodString;
338
- }, z.core.$strip>>>;
417
+ }, z.core.$loose>>>;
339
418
  closedPRs: z.ZodDefault<z.ZodArray<z.ZodObject<{
340
419
  url: z.ZodString;
341
420
  title: z.ZodString;
342
421
  closedAt: z.ZodString;
343
- }, z.core.$strip>>>;
422
+ }, z.core.$loose>>>;
344
423
  openPRs: z.ZodDefault<z.ZodArray<z.ZodObject<{
345
424
  url: z.ZodString;
346
425
  title: z.ZodString;
347
426
  openedAt: z.ZodString;
348
- }, z.core.$strip>>>;
427
+ }, z.core.$loose>>>;
349
428
  savedResults: z.ZodDefault<z.ZodArray<z.ZodObject<{
350
429
  issueUrl: z.ZodString;
351
430
  repo: z.ZodString;
@@ -358,7 +437,11 @@ export declare const ScoutStateSchema: z.ZodObject<{
358
437
  needs_review: "needs_review";
359
438
  }>;
360
439
  viabilityScore: z.ZodNumber;
361
- searchPriority: z.ZodString;
440
+ searchPriority: z.ZodCatch<z.ZodEnum<{
441
+ normal: "normal";
442
+ starred: "starred";
443
+ merged_pr: "merged_pr";
444
+ }>>;
362
445
  firstSeenAt: z.ZodString;
363
446
  lastSeenAt: z.ZodString;
364
447
  lastScore: z.ZodNumber;
@@ -366,18 +449,53 @@ export declare const ScoutStateSchema: z.ZodObject<{
366
449
  "quick-win": "quick-win";
367
450
  "bigger-bet": "bigger-bet";
368
451
  }>>;
369
- }, z.core.$strip>>>;
452
+ /**
453
+ * Availability status recorded by the previous `vet-list` run, so the next
454
+ * run can report transitions ("was available, now claimed") instead of
455
+ * re-diffing full snapshots (#165). "error" is never stored.
456
+ */
457
+ lastStatus: z.ZodOptional<z.ZodEnum<{
458
+ closed: "closed";
459
+ still_available: "still_available";
460
+ claimed: "claimed";
461
+ has_pr: "has_pr";
462
+ }>>;
463
+ }, z.core.$loose>>>;
370
464
  skippedIssues: z.ZodDefault<z.ZodArray<z.ZodObject<{
371
465
  url: z.ZodString;
372
466
  repo: z.ZodString;
373
467
  number: z.ZodNumber;
374
468
  title: z.ZodString;
375
469
  skippedAt: z.ZodString;
376
- }, z.core.$strip>>>;
470
+ }, z.core.$loose>>>;
471
+ /**
472
+ * Deletion tombstones (#117). Without these, the gist union-merge would
473
+ * resurrect any saved result or skipped issue removed on one machine the
474
+ * next time it merged against another machine's copy that still had it.
475
+ * A tombstone suppresses an item across a merge until the item is
476
+ * re-added with a newer timestamp. Pruned after 90 days.
477
+ */
478
+ tombstones: z.ZodDefault<z.ZodArray<z.ZodObject<{
479
+ url: z.ZodString;
480
+ removedAt: z.ZodString;
481
+ }, z.core.$loose>>>;
482
+ /**
483
+ * When preferences were last changed (#117). The gist merge previously
484
+ * always took the remote preferences, silently reverting a local edit;
485
+ * the side with the fresher timestamp now wins.
486
+ */
487
+ preferencesUpdatedAt: z.ZodOptional<z.ZodString>;
377
488
  lastSearchAt: z.ZodOptional<z.ZodString>;
378
489
  lastRunAt: z.ZodDefault<z.ZodString>;
379
490
  gistId: z.ZodOptional<z.ZodString>;
380
- }, z.core.$strip>;
491
+ }, z.core.$loose>;
492
+ /**
493
+ * Single entry point for parsing persisted state (local file, gist, gist
494
+ * cache). Version migrations belong here: when a version 2 ships, transform
495
+ * older raw shapes before validation so no load site ever sees unmigrated
496
+ * data. Unknown keys round-trip via the loose schemas above.
497
+ */
498
+ export declare function parseScoutState(raw: unknown): ScoutState;
381
499
  export type ProjectCategory = z.infer<typeof ProjectCategorySchema>;
382
500
  export type IssueScope = z.infer<typeof IssueScopeSchema>;
383
501
  export type SearchStrategy = z.infer<typeof SearchStrategySchema>;
@@ -6,12 +6,12 @@
6
6
  */
7
7
  import { z } from "zod";
8
8
  // ── Enum schemas ────────────────────────────────────────────────────
9
- export const IssueStatusSchema = z.enum([
10
- "candidate",
11
- "claimed",
12
- "in_progress",
13
- "pr_submitted",
14
- ]);
9
+ // Only "candidate" is ever assigned (TrackedIssue.status is hardcoded in
10
+ // issue-vetting). The "claimed" / "in_progress" / "pr_submitted" values were
11
+ // vestigial from the autopilot extraction and never set; TrackedIssue is an
12
+ // in-memory type, not part of persisted ScoutState, so trimming them carries
13
+ // no migration risk (#155).
14
+ export const IssueStatusSchema = z.enum(["candidate"]);
15
15
  export const ProjectCategorySchema = z.enum([
16
16
  "nonprofit",
17
17
  "devtools",
@@ -40,12 +40,12 @@ export const CONCRETE_STRATEGIES = [
40
40
  "maintained",
41
41
  ];
42
42
  // ── Leaf schemas ────────────────────────────────────────────────────
43
- export const RepoSignalsSchema = z.object({
43
+ export const RepoSignalsSchema = z.looseObject({
44
44
  hasActiveMaintainers: z.boolean(),
45
45
  isResponsive: z.boolean(),
46
46
  hasHostileComments: z.boolean(),
47
47
  });
48
- export const RepoScoreSchema = z.object({
48
+ export const RepoScoreSchema = z.looseObject({
49
49
  repo: z.string(),
50
50
  score: z.number(),
51
51
  mergedPRCount: z.number(),
@@ -57,17 +57,17 @@ export const RepoScoreSchema = z.object({
57
57
  stargazersCount: z.number().optional(),
58
58
  language: z.string().nullable().optional(),
59
59
  });
60
- export const StoredMergedPRSchema = z.object({
60
+ export const StoredMergedPRSchema = z.looseObject({
61
61
  url: z.string(),
62
62
  title: z.string(),
63
63
  mergedAt: z.string(),
64
64
  });
65
- export const StoredClosedPRSchema = z.object({
65
+ export const StoredClosedPRSchema = z.looseObject({
66
66
  url: z.string(),
67
67
  title: z.string(),
68
68
  closedAt: z.string(),
69
69
  });
70
- export const StoredOpenPRSchema = z.object({
70
+ export const StoredOpenPRSchema = z.looseObject({
71
71
  url: z.string(),
72
72
  title: z.string(),
73
73
  openedAt: z.string(),
@@ -129,7 +129,7 @@ export const TrackedIssueSchema = z.object({
129
129
  vettingResult: IssueVettingResultSchema.optional(),
130
130
  });
131
131
  // ── Skipped issue schema ──────────────────────────────────────────
132
- export const SkippedIssueSchema = z.object({
132
+ export const SkippedIssueSchema = z.looseObject({
133
133
  url: z.string(),
134
134
  repo: z.string(),
135
135
  number: z.number(),
@@ -138,7 +138,7 @@ export const SkippedIssueSchema = z.object({
138
138
  });
139
139
  // ── Saved candidate schema ─────────────────────────────────────────
140
140
  export const HorizonSchema = z.enum(["quick-win", "bigger-bet"]);
141
- export const SavedCandidateSchema = z.object({
141
+ export const SavedCandidateSchema = z.looseObject({
142
142
  issueUrl: z.string(),
143
143
  repo: z.string(),
144
144
  number: z.number(),
@@ -146,15 +146,26 @@ export const SavedCandidateSchema = z.object({
146
146
  labels: z.array(z.string()),
147
147
  recommendation: z.enum(["approve", "skip", "needs_review"]),
148
148
  viabilityScore: z.number(),
149
- searchPriority: z.string(),
149
+ // Tightened from z.string() to the actual 3-value union (#158). `.catch`
150
+ // keeps old persisted state loadable: any unrecognized stored value (or a
151
+ // future-version value) decodes to "normal" instead of failing the parse.
152
+ searchPriority: z.enum(["merged_pr", "starred", "normal"]).catch("normal"),
150
153
  firstSeenAt: z.string(),
151
154
  lastSeenAt: z.string(),
152
155
  lastScore: z.number(),
153
156
  horizon: HorizonSchema.optional(),
157
+ /**
158
+ * Availability status recorded by the previous `vet-list` run, so the next
159
+ * run can report transitions ("was available, now claimed") instead of
160
+ * re-diffing full snapshots (#165). "error" is never stored.
161
+ */
162
+ lastStatus: z
163
+ .enum(["still_available", "claimed", "closed", "has_pr"])
164
+ .optional(),
154
165
  });
155
166
  // ── Scout preferences schema ────────────────────────────────────────
156
167
  export const PersistenceModeSchema = z.enum(["local", "gist"]);
157
- export const ScoutPreferencesSchema = z.object({
168
+ export const ScoutPreferencesSchema = z.looseObject({
158
169
  githubUsername: z.string().default(""),
159
170
  languages: z.array(z.string()).default(["any"]),
160
171
  labels: z.array(z.string()).default(["good first issue", "help wanted"]),
@@ -170,8 +181,22 @@ export const ScoutPreferencesSchema = z.object({
170
181
  interPhaseDelayMs: z.number().min(0).max(120000).default(30000),
171
182
  persistence: PersistenceModeSchema.default("local"),
172
183
  defaultStrategy: z.array(SearchStrategySchema).optional(),
184
+ /**
185
+ * Persisted personalization defaults (#168). The `--prefer-languages`,
186
+ * `--prefer-repos`, and `--diversity-ratio` search flags override these when
187
+ * passed, so a stored preference removes the need to retype the boost every
188
+ * search. Empty / 0 disables the corresponding signal (same as the flags).
189
+ */
190
+ preferLanguages: z.array(z.string()).default([]),
191
+ preferRepos: z.array(z.string()).default([]),
192
+ diversityRatio: z.number().min(0).max(1).default(0),
173
193
  broadPhaseDelayMs: z.number().min(0).max(300000).default(90000),
174
- skipBroadWhenSufficientResults: z.number().int().min(0).max(100).default(15),
194
+ /**
195
+ * Skip the expensive broad phase once this many candidates were found by
196
+ * the cheaper phases. Clamped at runtime to maxResults - 1 so it stays
197
+ * satisfiable; 0 disables skipping entirely.
198
+ */
199
+ skipBroadWhenSufficientResults: z.number().int().min(0).max(100).default(8),
175
200
  /**
176
201
  * Optional Ollama model id used for SLM pre-triage during vetting
177
202
  * (oss-autopilot#1122). Empty disables the feature. Recommended values:
@@ -199,7 +224,10 @@ export const ScoutPreferencesSchema = z.object({
199
224
  featuresSplitRatio: z.number().min(0).max(1).default(0.6),
200
225
  });
201
226
  // ── Root state schema ───────────────────────────────────────────────
202
- export const ScoutStateSchema = z.object({
227
+ // Persisted schemas are loose (unknown keys round-trip) so an older binary
228
+ // loading state written by a newer one cannot silently strip and then
229
+ // persist away the newer fields (#137).
230
+ export const ScoutStateSchema = z.looseObject({
203
231
  version: z.literal(1),
204
232
  preferences: ScoutPreferencesSchema.default(() => ScoutPreferencesSchema.parse({})),
205
233
  repoScores: z.record(z.string(), RepoScoreSchema).default({}),
@@ -210,7 +238,36 @@ export const ScoutStateSchema = z.object({
210
238
  openPRs: z.array(StoredOpenPRSchema).default([]),
211
239
  savedResults: z.array(SavedCandidateSchema).default([]),
212
240
  skippedIssues: z.array(SkippedIssueSchema).default([]),
241
+ /**
242
+ * Deletion tombstones (#117). Without these, the gist union-merge would
243
+ * resurrect any saved result or skipped issue removed on one machine the
244
+ * next time it merged against another machine's copy that still had it.
245
+ * A tombstone suppresses an item across a merge until the item is
246
+ * re-added with a newer timestamp. Pruned after 90 days.
247
+ */
248
+ tombstones: z
249
+ .array(z.looseObject({
250
+ url: z.string(),
251
+ removedAt: z.string(),
252
+ }))
253
+ .default([]),
254
+ /**
255
+ * When preferences were last changed (#117). The gist merge previously
256
+ * always took the remote preferences, silently reverting a local edit;
257
+ * the side with the fresher timestamp now wins.
258
+ */
259
+ preferencesUpdatedAt: z.string().optional(),
213
260
  lastSearchAt: z.string().optional(),
214
261
  lastRunAt: z.string().default(() => new Date().toISOString()),
215
262
  gistId: z.string().optional(),
216
263
  });
264
+ /**
265
+ * Single entry point for parsing persisted state (local file, gist, gist
266
+ * cache). Version migrations belong here: when a version 2 ships, transform
267
+ * older raw shapes before validation so no load site ever sees unmigrated
268
+ * data. Unknown keys round-trip via the loose schemas above.
269
+ */
270
+ export function parseScoutState(raw) {
271
+ // No migrations yet; version 1 is current.
272
+ return ScoutStateSchema.parse(raw);
273
+ }
@@ -20,6 +20,8 @@ export declare class SearchBudgetTracker {
20
20
  private resetAt;
21
21
  /** Total calls recorded since init (for diagnostics). */
22
22
  private totalCalls;
23
+ /** Calls made since the last known quota reset (external accounting). */
24
+ private callsSinceReset;
23
25
  /**
24
26
  * Initialize with pre-flight rate limit data from GitHub.
25
27
  */
@@ -28,6 +30,13 @@ export declare class SearchBudgetTracker {
28
30
  * Record that a Search API call was just made.
29
31
  */
30
32
  recordCall(): void;
33
+ /**
34
+ * Replenish the external budget once GitHub's quota window has reset.
35
+ * Without this, the pre-flight remaining acted as a never-replenishing
36
+ * run-lifetime total, throttling long multi-phase runs to ~1 call/min
37
+ * even though GitHub fully resets every 60 seconds (#119).
38
+ */
39
+ private refreshExternalBudget;
31
40
  /**
32
41
  * Remove timestamps older than the sliding window.
33
42
  */
@@ -30,6 +30,8 @@ export class SearchBudgetTracker {
30
30
  resetAt = 0;
31
31
  /** Total calls recorded since init (for diagnostics). */
32
32
  totalCalls = 0;
33
+ /** Calls made since the last known quota reset (external accounting). */
34
+ callsSinceReset = 0;
33
35
  /**
34
36
  * Initialize with pre-flight rate limit data from GitHub.
35
37
  */
@@ -38,6 +40,7 @@ export class SearchBudgetTracker {
38
40
  this.resetAt = new Date(resetAt).getTime();
39
41
  this.callTimestamps = [];
40
42
  this.totalCalls = 0;
43
+ this.callsSinceReset = 0;
41
44
  debug(MODULE, `Initialized: ${remaining} remaining, resets at ${new Date(this.resetAt).toLocaleTimeString()}`);
42
45
  }
43
46
  /**
@@ -46,8 +49,28 @@ export class SearchBudgetTracker {
46
49
  recordCall() {
47
50
  this.callTimestamps.push(Date.now());
48
51
  this.totalCalls++;
52
+ this.callsSinceReset++;
49
53
  this.pruneOldTimestamps();
50
54
  }
55
+ /**
56
+ * Replenish the external budget once GitHub's quota window has reset.
57
+ * Without this, the pre-flight remaining acted as a never-replenishing
58
+ * run-lifetime total, throttling long multi-phase runs to ~1 call/min
59
+ * even though GitHub fully resets every 60 seconds (#119).
60
+ */
61
+ refreshExternalBudget() {
62
+ if (this.resetAt <= 0)
63
+ return;
64
+ const now = Date.now();
65
+ if (now < this.resetAt)
66
+ return;
67
+ this.knownRemaining = SEARCH_RATE_LIMIT;
68
+ this.callsSinceReset = 0;
69
+ while (this.resetAt <= now) {
70
+ this.resetAt += SEARCH_WINDOW_MS;
71
+ }
72
+ debug(MODULE, "Quota window reset; external search budget replenished");
73
+ }
51
74
  /**
52
75
  * Remove timestamps older than the sliding window.
53
76
  */
@@ -69,9 +92,11 @@ export class SearchBudgetTracker {
69
92
  * and the pre-flight remaining quota from GitHub.
70
93
  */
71
94
  getEffectiveBudget() {
72
- // Use the stricter of: local window limit vs. pre-flight remaining minus calls made
95
+ this.refreshExternalBudget();
96
+ // Use the stricter of: local window limit vs. known remaining quota
97
+ // minus calls made since the last reset
73
98
  const localBudget = EFFECTIVE_BUDGET - this.callTimestamps.length;
74
- const externalBudget = this.knownRemaining - this.totalCalls;
99
+ const externalBudget = this.knownRemaining - this.callsSinceReset;
75
100
  return Math.max(0, Math.min(localBudget, externalBudget));
76
101
  }
77
102
  /**
@@ -97,7 +122,15 @@ export class SearchBudgetTracker {
97
122
  // Wait until the oldest call in the window ages out
98
123
  const oldestInWindow = this.callTimestamps[0];
99
124
  if (!oldestInWindow) {
100
- return; // No calls in window — budget exhausted by external consumption, can't wait it out
125
+ // No calls in window — the external quota is exhausted. Wait for
126
+ // GitHub's reset when we know it, otherwise proceed (#119).
127
+ const untilReset = this.resetAt - Date.now();
128
+ if (this.resetAt > 0 && untilReset > 0) {
129
+ debug(MODULE, `External quota exhausted, waiting ${untilReset}ms for reset`);
130
+ await sleep(untilReset + 100);
131
+ continue;
132
+ }
133
+ return;
101
134
  }
102
135
  const waitUntil = oldestInWindow + SEARCH_WINDOW_MS;
103
136
  const waitMs = waitUntil - Date.now();
@@ -94,21 +94,3 @@ export declare function filterVetAndScore(vetter: IssueVetter, items: GitHubSear
94
94
  allVetFailed: boolean;
95
95
  rateLimitHit: boolean;
96
96
  }>;
97
- /**
98
- * Search for issues within specific repos using batched queries.
99
- *
100
- * To avoid GitHub's secondary rate limit (30 requests/minute), we batch
101
- * multiple repos into a single search query using OR syntax:
102
- * repo:owner1/repo1 OR repo:owner2/repo2 OR repo:owner3/repo3
103
- *
104
- * Labels are chunked separately to stay within GitHub's 5 boolean operator limit.
105
- * Each batch of repos consumes (batch.length - 1) OR operators, and the remaining
106
- * budget is used for label OR operators.
107
- *
108
- * This reduces API calls from N (one per repo) to ceil(N/BATCH_SIZE) * label_chunks.
109
- */
110
- export declare function searchInRepos(octokit: Octokit, vetter: IssueVetter, repos: string[], baseQualifiers: string, labels: string[], maxResults: number, priority: SearchPriority, filterFn: (items: GitHubSearchItem[]) => GitHubSearchItem[]): Promise<{
111
- candidates: IssueCandidate[];
112
- allBatchesFailed: boolean;
113
- rateLimitHit: boolean;
114
- }>;