@oss-scout/core 0.11.0 → 1.1.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.
- package/dist/cli.bundle.cjs +89 -66
- package/dist/cli.js +302 -436
- package/dist/commands/command-scout.d.ts +21 -0
- package/dist/commands/command-scout.js +21 -0
- package/dist/commands/config.js +10 -128
- package/dist/commands/features.js +15 -28
- package/dist/commands/results.d.ts +13 -2
- package/dist/commands/results.js +29 -2
- package/dist/commands/search.d.ts +4 -0
- package/dist/commands/search.js +65 -70
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +35 -6
- package/dist/commands/skip.d.ts +4 -0
- package/dist/commands/skip.js +45 -55
- package/dist/commands/sync.d.ts +10 -0
- package/dist/commands/sync.js +10 -0
- package/dist/commands/vet-list.js +3 -19
- package/dist/commands/vet.js +18 -25
- package/dist/commands/with-scout.d.ts +32 -0
- package/dist/commands/with-scout.js +41 -0
- package/dist/core/anti-llm-policy.js +5 -33
- package/dist/core/bootstrap.d.ts +2 -2
- package/dist/core/bootstrap.js +5 -9
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.js +20 -5
- package/dist/core/feature-discovery.d.ts +13 -1
- package/dist/core/feature-discovery.js +104 -81
- package/dist/core/gist-state-store.d.ts +13 -12
- package/dist/core/gist-state-store.js +128 -53
- package/dist/core/http-cache.d.ts +32 -2
- package/dist/core/http-cache.js +74 -19
- package/dist/core/issue-discovery.d.ts +12 -1
- package/dist/core/issue-discovery.js +94 -67
- package/dist/core/issue-eligibility.d.ts +11 -4
- package/dist/core/issue-eligibility.js +124 -69
- package/dist/core/issue-graphql.d.ts +58 -0
- package/dist/core/issue-graphql.js +108 -0
- package/dist/core/issue-vetting.d.ts +115 -9
- package/dist/core/issue-vetting.js +246 -109
- package/dist/core/local-state.d.ts +6 -2
- package/dist/core/local-state.js +23 -5
- package/dist/core/logger.d.ts +12 -4
- package/dist/core/logger.js +33 -7
- package/dist/core/personalization.d.ts +30 -10
- package/dist/core/personalization.js +64 -24
- package/dist/core/preference-fields.d.ts +47 -0
- package/dist/core/preference-fields.js +180 -0
- package/dist/core/probe-repo-file.d.ts +47 -0
- package/dist/core/probe-repo-file.js +57 -0
- package/dist/core/repo-health.js +40 -32
- package/dist/core/roadmap.js +26 -22
- package/dist/core/schemas.d.ts +148 -26
- package/dist/core/schemas.js +83 -17
- package/dist/core/search-budget.d.ts +9 -0
- package/dist/core/search-budget.js +36 -3
- package/dist/core/search-phases.d.ts +4 -21
- package/dist/core/search-phases.js +37 -89
- package/dist/core/types.d.ts +151 -38
- package/dist/core/utils.js +60 -26
- package/dist/formatters/human.d.ts +60 -0
- package/dist/formatters/human.js +199 -0
- package/dist/formatters/markdown.d.ts +10 -0
- package/dist/formatters/markdown.js +31 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +8 -0
- package/dist/scout.d.ts +75 -12
- package/dist/scout.js +265 -26
- package/package.json +1 -1
package/dist/core/roadmap.js
CHANGED
|
@@ -10,9 +10,7 @@
|
|
|
10
10
|
* Auth (401) and rate-limit errors propagate, matching the rest of the
|
|
11
11
|
* codebase's error strategy. Other errors degrade gracefully (warn + empty).
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import { warn } from "./logger.js";
|
|
15
|
-
const MODULE = "roadmap";
|
|
13
|
+
import { probeRepoFile } from "./probe-repo-file.js";
|
|
16
14
|
/** TTL for roadmap fetch results (1 hour). */
|
|
17
15
|
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
18
16
|
/** Paths probed in priority order. First success wins. */
|
|
@@ -97,26 +95,32 @@ export async function fetchRoadmapIssueRefs(octokit, owner, repo) {
|
|
|
97
95
|
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
98
96
|
return cached.refs;
|
|
99
97
|
}
|
|
98
|
+
// Concurrent feature vets of issues from one repo share a probe (#124)
|
|
99
|
+
const inflight = roadmapInflight.get(cacheKey);
|
|
100
|
+
if (inflight)
|
|
101
|
+
return inflight;
|
|
102
|
+
const promise = fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey);
|
|
103
|
+
roadmapInflight.set(cacheKey, promise);
|
|
104
|
+
try {
|
|
105
|
+
return await promise;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
roadmapInflight.delete(cacheKey);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const roadmapInflight = new Map();
|
|
112
|
+
async function fetchRoadmapIssueRefsUncached(octokit, owner, repo, cacheKey) {
|
|
100
113
|
for (const path of ROADMAP_PATHS) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
catch (err) {
|
|
112
|
-
if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
|
|
113
|
-
throw err;
|
|
114
|
-
const status = getHttpStatusCode(err);
|
|
115
|
-
if (status === 404)
|
|
116
|
-
continue; // path missing — try next
|
|
117
|
-
warn(MODULE, `Unexpected error fetching ${path} from ${owner}/${repo}: ${errorMessage(err)}`);
|
|
118
|
-
// Fall through and try next path.
|
|
119
|
-
}
|
|
114
|
+
// probeRepoFile rethrows 401/rate-limit, treats 404 and non-content
|
|
115
|
+
// payloads as a null text, and warns on 5xx — all of which we degrade past
|
|
116
|
+
// by trying the next path.
|
|
117
|
+
const { text } = await probeRepoFile(octokit, owner, repo, path);
|
|
118
|
+
if (!text)
|
|
119
|
+
continue;
|
|
120
|
+
const refs = parseRoadmapIssueRefs(text, owner, repo);
|
|
121
|
+
roadmapCache.set(cacheKey, { refs, fetchedAt: Date.now() });
|
|
122
|
+
pruneCache();
|
|
123
|
+
return refs;
|
|
120
124
|
}
|
|
121
125
|
// No roadmap found (or all probes errored softly). Cache the empty result
|
|
122
126
|
// so we don't re-probe every run.
|
package/dist/core/schemas.d.ts
CHANGED
|
@@ -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.$
|
|
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.$
|
|
50
|
+
}, z.core.$loose>;
|
|
54
51
|
stargazersCount: z.ZodOptional<z.ZodNumber>;
|
|
55
52
|
language: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
56
|
-
}, z.core.$
|
|
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.$
|
|
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.$
|
|
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.$
|
|
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.$
|
|
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.
|
|
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
|
-
|
|
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,50 @@ 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>;
|
|
282
|
+
avoidRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
283
|
+
boostIssueTypes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
264
284
|
broadPhaseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
285
|
+
/**
|
|
286
|
+
* Skip the expensive broad phase once this many candidates were found by
|
|
287
|
+
* the cheaper phases. Clamped at runtime to maxResults - 1 so it stays
|
|
288
|
+
* satisfiable; 0 disables skipping entirely.
|
|
289
|
+
*/
|
|
265
290
|
skipBroadWhenSufficientResults: z.ZodDefault<z.ZodNumber>;
|
|
291
|
+
/**
|
|
292
|
+
* Optional Ollama model id used for SLM pre-triage during vetting
|
|
293
|
+
* (oss-autopilot#1122). Empty disables the feature. Recommended values:
|
|
294
|
+
* `gemma4:e4b` (default for capable hardware) or `gemma4:e2b` /
|
|
295
|
+
* `qwen3:1.7b` for low-RAM machines.
|
|
296
|
+
*/
|
|
266
297
|
slmTriageModel: z.ZodDefault<z.ZodString>;
|
|
298
|
+
/**
|
|
299
|
+
* Override the Ollama HTTP host. Defaults to `http://127.0.0.1:11434`
|
|
300
|
+
* when empty. Useful when Ollama runs on a different machine on the
|
|
301
|
+
* local network.
|
|
302
|
+
*/
|
|
267
303
|
slmTriageHost: z.ZodDefault<z.ZodString>;
|
|
304
|
+
/**
|
|
305
|
+
* Minimum merged-PR count for a repo to qualify as an anchor in
|
|
306
|
+
* `scout features` (#98). Lowering surfaces more anchors at the cost
|
|
307
|
+
* of weaker prior engagement signal.
|
|
308
|
+
*/
|
|
268
309
|
featuresAnchorThreshold: z.ZodDefault<z.ZodNumber>;
|
|
310
|
+
/**
|
|
311
|
+
* Quick-wins / bigger-bets split ratio for `scout features` (#99).
|
|
312
|
+
* 0.6 means 60% quick wins, 40% bigger bets when both pools are
|
|
313
|
+
* abundant. Deficits redirect to the other bucket.
|
|
314
|
+
*/
|
|
269
315
|
featuresSplitRatio: z.ZodDefault<z.ZodNumber>;
|
|
270
|
-
}, z.core.$
|
|
316
|
+
}, z.core.$loose>;
|
|
271
317
|
export declare const ScoutStateSchema: z.ZodObject<{
|
|
272
318
|
version: z.ZodLiteral<1>;
|
|
273
319
|
preferences: z.ZodDefault<z.ZodObject<{
|
|
@@ -306,13 +352,50 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
306
352
|
broad: "broad";
|
|
307
353
|
maintained: "maintained";
|
|
308
354
|
}>>>;
|
|
355
|
+
/**
|
|
356
|
+
* Persisted personalization defaults (#168). The `--prefer-languages`,
|
|
357
|
+
* `--prefer-repos`, and `--diversity-ratio` search flags override these when
|
|
358
|
+
* passed, so a stored preference removes the need to retype the boost every
|
|
359
|
+
* search. Empty / 0 disables the corresponding signal (same as the flags).
|
|
360
|
+
*/
|
|
361
|
+
preferLanguages: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
362
|
+
preferRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
363
|
+
diversityRatio: z.ZodDefault<z.ZodNumber>;
|
|
364
|
+
avoidRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
365
|
+
boostIssueTypes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
309
366
|
broadPhaseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
367
|
+
/**
|
|
368
|
+
* Skip the expensive broad phase once this many candidates were found by
|
|
369
|
+
* the cheaper phases. Clamped at runtime to maxResults - 1 so it stays
|
|
370
|
+
* satisfiable; 0 disables skipping entirely.
|
|
371
|
+
*/
|
|
310
372
|
skipBroadWhenSufficientResults: z.ZodDefault<z.ZodNumber>;
|
|
373
|
+
/**
|
|
374
|
+
* Optional Ollama model id used for SLM pre-triage during vetting
|
|
375
|
+
* (oss-autopilot#1122). Empty disables the feature. Recommended values:
|
|
376
|
+
* `gemma4:e4b` (default for capable hardware) or `gemma4:e2b` /
|
|
377
|
+
* `qwen3:1.7b` for low-RAM machines.
|
|
378
|
+
*/
|
|
311
379
|
slmTriageModel: z.ZodDefault<z.ZodString>;
|
|
380
|
+
/**
|
|
381
|
+
* Override the Ollama HTTP host. Defaults to `http://127.0.0.1:11434`
|
|
382
|
+
* when empty. Useful when Ollama runs on a different machine on the
|
|
383
|
+
* local network.
|
|
384
|
+
*/
|
|
312
385
|
slmTriageHost: z.ZodDefault<z.ZodString>;
|
|
386
|
+
/**
|
|
387
|
+
* Minimum merged-PR count for a repo to qualify as an anchor in
|
|
388
|
+
* `scout features` (#98). Lowering surfaces more anchors at the cost
|
|
389
|
+
* of weaker prior engagement signal.
|
|
390
|
+
*/
|
|
313
391
|
featuresAnchorThreshold: z.ZodDefault<z.ZodNumber>;
|
|
392
|
+
/**
|
|
393
|
+
* Quick-wins / bigger-bets split ratio for `scout features` (#99).
|
|
394
|
+
* 0.6 means 60% quick wins, 40% bigger bets when both pools are
|
|
395
|
+
* abundant. Deficits redirect to the other bucket.
|
|
396
|
+
*/
|
|
314
397
|
featuresSplitRatio: z.ZodDefault<z.ZodNumber>;
|
|
315
|
-
}, z.core.$
|
|
398
|
+
}, z.core.$loose>>;
|
|
316
399
|
repoScores: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
317
400
|
repo: z.ZodString;
|
|
318
401
|
score: z.ZodNumber;
|
|
@@ -325,27 +408,27 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
325
408
|
hasActiveMaintainers: z.ZodBoolean;
|
|
326
409
|
isResponsive: z.ZodBoolean;
|
|
327
410
|
hasHostileComments: z.ZodBoolean;
|
|
328
|
-
}, z.core.$
|
|
411
|
+
}, z.core.$loose>;
|
|
329
412
|
stargazersCount: z.ZodOptional<z.ZodNumber>;
|
|
330
413
|
language: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
331
|
-
}, z.core.$
|
|
414
|
+
}, z.core.$loose>>>;
|
|
332
415
|
starredRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
333
416
|
starredReposLastFetched: z.ZodOptional<z.ZodString>;
|
|
334
417
|
mergedPRs: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
335
418
|
url: z.ZodString;
|
|
336
419
|
title: z.ZodString;
|
|
337
420
|
mergedAt: z.ZodString;
|
|
338
|
-
}, z.core.$
|
|
421
|
+
}, z.core.$loose>>>;
|
|
339
422
|
closedPRs: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
340
423
|
url: z.ZodString;
|
|
341
424
|
title: z.ZodString;
|
|
342
425
|
closedAt: z.ZodString;
|
|
343
|
-
}, z.core.$
|
|
426
|
+
}, z.core.$loose>>>;
|
|
344
427
|
openPRs: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
345
428
|
url: z.ZodString;
|
|
346
429
|
title: z.ZodString;
|
|
347
430
|
openedAt: z.ZodString;
|
|
348
|
-
}, z.core.$
|
|
431
|
+
}, z.core.$loose>>>;
|
|
349
432
|
savedResults: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
350
433
|
issueUrl: z.ZodString;
|
|
351
434
|
repo: z.ZodString;
|
|
@@ -358,7 +441,11 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
358
441
|
needs_review: "needs_review";
|
|
359
442
|
}>;
|
|
360
443
|
viabilityScore: z.ZodNumber;
|
|
361
|
-
searchPriority: z.
|
|
444
|
+
searchPriority: z.ZodCatch<z.ZodEnum<{
|
|
445
|
+
normal: "normal";
|
|
446
|
+
starred: "starred";
|
|
447
|
+
merged_pr: "merged_pr";
|
|
448
|
+
}>>;
|
|
362
449
|
firstSeenAt: z.ZodString;
|
|
363
450
|
lastSeenAt: z.ZodString;
|
|
364
451
|
lastScore: z.ZodNumber;
|
|
@@ -366,18 +453,53 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
366
453
|
"quick-win": "quick-win";
|
|
367
454
|
"bigger-bet": "bigger-bet";
|
|
368
455
|
}>>;
|
|
369
|
-
|
|
456
|
+
/**
|
|
457
|
+
* Availability status recorded by the previous `vet-list` run, so the next
|
|
458
|
+
* run can report transitions ("was available, now claimed") instead of
|
|
459
|
+
* re-diffing full snapshots (#165). "error" is never stored.
|
|
460
|
+
*/
|
|
461
|
+
lastStatus: z.ZodOptional<z.ZodEnum<{
|
|
462
|
+
closed: "closed";
|
|
463
|
+
still_available: "still_available";
|
|
464
|
+
claimed: "claimed";
|
|
465
|
+
has_pr: "has_pr";
|
|
466
|
+
}>>;
|
|
467
|
+
}, z.core.$loose>>>;
|
|
370
468
|
skippedIssues: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
371
469
|
url: z.ZodString;
|
|
372
470
|
repo: z.ZodString;
|
|
373
471
|
number: z.ZodNumber;
|
|
374
472
|
title: z.ZodString;
|
|
375
473
|
skippedAt: z.ZodString;
|
|
376
|
-
}, z.core.$
|
|
474
|
+
}, z.core.$loose>>>;
|
|
475
|
+
/**
|
|
476
|
+
* Deletion tombstones (#117). Without these, the gist union-merge would
|
|
477
|
+
* resurrect any saved result or skipped issue removed on one machine the
|
|
478
|
+
* next time it merged against another machine's copy that still had it.
|
|
479
|
+
* A tombstone suppresses an item across a merge until the item is
|
|
480
|
+
* re-added with a newer timestamp. Pruned after 90 days.
|
|
481
|
+
*/
|
|
482
|
+
tombstones: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
483
|
+
url: z.ZodString;
|
|
484
|
+
removedAt: z.ZodString;
|
|
485
|
+
}, z.core.$loose>>>;
|
|
486
|
+
/**
|
|
487
|
+
* When preferences were last changed (#117). The gist merge previously
|
|
488
|
+
* always took the remote preferences, silently reverting a local edit;
|
|
489
|
+
* the side with the fresher timestamp now wins.
|
|
490
|
+
*/
|
|
491
|
+
preferencesUpdatedAt: z.ZodOptional<z.ZodString>;
|
|
377
492
|
lastSearchAt: z.ZodOptional<z.ZodString>;
|
|
378
493
|
lastRunAt: z.ZodDefault<z.ZodString>;
|
|
379
494
|
gistId: z.ZodOptional<z.ZodString>;
|
|
380
|
-
}, z.core.$
|
|
495
|
+
}, z.core.$loose>;
|
|
496
|
+
/**
|
|
497
|
+
* Single entry point for parsing persisted state (local file, gist, gist
|
|
498
|
+
* cache). Version migrations belong here: when a version 2 ships, transform
|
|
499
|
+
* older raw shapes before validation so no load site ever sees unmigrated
|
|
500
|
+
* data. Unknown keys round-trip via the loose schemas above.
|
|
501
|
+
*/
|
|
502
|
+
export declare function parseScoutState(raw: unknown): ScoutState;
|
|
381
503
|
export type ProjectCategory = z.infer<typeof ProjectCategorySchema>;
|
|
382
504
|
export type IssueScope = z.infer<typeof IssueScopeSchema>;
|
|
383
505
|
export type SearchStrategy = z.infer<typeof SearchStrategySchema>;
|
package/dist/core/schemas.js
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
// ── Enum schemas ────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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,15 @@ export const CONCRETE_STRATEGIES = [
|
|
|
40
40
|
"maintained",
|
|
41
41
|
];
|
|
42
42
|
// ── Leaf schemas ────────────────────────────────────────────────────
|
|
43
|
-
export const RepoSignalsSchema = z.
|
|
43
|
+
export const RepoSignalsSchema = z.looseObject({
|
|
44
44
|
hasActiveMaintainers: z.boolean(),
|
|
45
|
+
// Retained for backward compatibility but no longer affects the repo score
|
|
46
|
+
// (#167): nothing computes it, and hasActiveMaintainers is the live activity
|
|
47
|
+
// proxy. Kept so old persisted state and the search JSON output still parse.
|
|
45
48
|
isResponsive: z.boolean(),
|
|
46
49
|
hasHostileComments: z.boolean(),
|
|
47
50
|
});
|
|
48
|
-
export const RepoScoreSchema = z.
|
|
51
|
+
export const RepoScoreSchema = z.looseObject({
|
|
49
52
|
repo: z.string(),
|
|
50
53
|
score: z.number(),
|
|
51
54
|
mergedPRCount: z.number(),
|
|
@@ -57,17 +60,17 @@ export const RepoScoreSchema = z.object({
|
|
|
57
60
|
stargazersCount: z.number().optional(),
|
|
58
61
|
language: z.string().nullable().optional(),
|
|
59
62
|
});
|
|
60
|
-
export const StoredMergedPRSchema = z.
|
|
63
|
+
export const StoredMergedPRSchema = z.looseObject({
|
|
61
64
|
url: z.string(),
|
|
62
65
|
title: z.string(),
|
|
63
66
|
mergedAt: z.string(),
|
|
64
67
|
});
|
|
65
|
-
export const StoredClosedPRSchema = z.
|
|
68
|
+
export const StoredClosedPRSchema = z.looseObject({
|
|
66
69
|
url: z.string(),
|
|
67
70
|
title: z.string(),
|
|
68
71
|
closedAt: z.string(),
|
|
69
72
|
});
|
|
70
|
-
export const StoredOpenPRSchema = z.
|
|
73
|
+
export const StoredOpenPRSchema = z.looseObject({
|
|
71
74
|
url: z.string(),
|
|
72
75
|
title: z.string(),
|
|
73
76
|
openedAt: z.string(),
|
|
@@ -129,7 +132,7 @@ export const TrackedIssueSchema = z.object({
|
|
|
129
132
|
vettingResult: IssueVettingResultSchema.optional(),
|
|
130
133
|
});
|
|
131
134
|
// ── Skipped issue schema ──────────────────────────────────────────
|
|
132
|
-
export const SkippedIssueSchema = z.
|
|
135
|
+
export const SkippedIssueSchema = z.looseObject({
|
|
133
136
|
url: z.string(),
|
|
134
137
|
repo: z.string(),
|
|
135
138
|
number: z.number(),
|
|
@@ -138,7 +141,7 @@ export const SkippedIssueSchema = z.object({
|
|
|
138
141
|
});
|
|
139
142
|
// ── Saved candidate schema ─────────────────────────────────────────
|
|
140
143
|
export const HorizonSchema = z.enum(["quick-win", "bigger-bet"]);
|
|
141
|
-
export const SavedCandidateSchema = z.
|
|
144
|
+
export const SavedCandidateSchema = z.looseObject({
|
|
142
145
|
issueUrl: z.string(),
|
|
143
146
|
repo: z.string(),
|
|
144
147
|
number: z.number(),
|
|
@@ -146,15 +149,26 @@ export const SavedCandidateSchema = z.object({
|
|
|
146
149
|
labels: z.array(z.string()),
|
|
147
150
|
recommendation: z.enum(["approve", "skip", "needs_review"]),
|
|
148
151
|
viabilityScore: z.number(),
|
|
149
|
-
|
|
152
|
+
// Tightened from z.string() to the actual 3-value union (#158). `.catch`
|
|
153
|
+
// keeps old persisted state loadable: any unrecognized stored value (or a
|
|
154
|
+
// future-version value) decodes to "normal" instead of failing the parse.
|
|
155
|
+
searchPriority: z.enum(["merged_pr", "starred", "normal"]).catch("normal"),
|
|
150
156
|
firstSeenAt: z.string(),
|
|
151
157
|
lastSeenAt: z.string(),
|
|
152
158
|
lastScore: z.number(),
|
|
153
159
|
horizon: HorizonSchema.optional(),
|
|
160
|
+
/**
|
|
161
|
+
* Availability status recorded by the previous `vet-list` run, so the next
|
|
162
|
+
* run can report transitions ("was available, now claimed") instead of
|
|
163
|
+
* re-diffing full snapshots (#165). "error" is never stored.
|
|
164
|
+
*/
|
|
165
|
+
lastStatus: z
|
|
166
|
+
.enum(["still_available", "claimed", "closed", "has_pr"])
|
|
167
|
+
.optional(),
|
|
154
168
|
});
|
|
155
169
|
// ── Scout preferences schema ────────────────────────────────────────
|
|
156
170
|
export const PersistenceModeSchema = z.enum(["local", "gist"]);
|
|
157
|
-
export const ScoutPreferencesSchema = z.
|
|
171
|
+
export const ScoutPreferencesSchema = z.looseObject({
|
|
158
172
|
githubUsername: z.string().default(""),
|
|
159
173
|
languages: z.array(z.string()).default(["any"]),
|
|
160
174
|
labels: z.array(z.string()).default(["good first issue", "help wanted"]),
|
|
@@ -170,8 +184,28 @@ export const ScoutPreferencesSchema = z.object({
|
|
|
170
184
|
interPhaseDelayMs: z.number().min(0).max(120000).default(30000),
|
|
171
185
|
persistence: PersistenceModeSchema.default("local"),
|
|
172
186
|
defaultStrategy: z.array(SearchStrategySchema).optional(),
|
|
187
|
+
/**
|
|
188
|
+
* Persisted personalization defaults (#168). The `--prefer-languages`,
|
|
189
|
+
* `--prefer-repos`, and `--diversity-ratio` search flags override these when
|
|
190
|
+
* passed, so a stored preference removes the need to retype the boost every
|
|
191
|
+
* search. Empty / 0 disables the corresponding signal (same as the flags).
|
|
192
|
+
*/
|
|
193
|
+
preferLanguages: z.array(z.string()).default([]),
|
|
194
|
+
preferRepos: z.array(z.string()).default([]),
|
|
195
|
+
diversityRatio: z.number().min(0).max(1).default(0),
|
|
196
|
+
// Soft penalty (milder than the hard excludeRepos filter): candidates in
|
|
197
|
+
// these `owner/repo` slugs are pushed down the ranking but not removed (#168).
|
|
198
|
+
avoidRepos: z.array(z.string()).default([]),
|
|
199
|
+
// Soft boost for candidates whose issue labels match one of these types,
|
|
200
|
+
// case-insensitive (e.g. "bug", "good first issue") (#168).
|
|
201
|
+
boostIssueTypes: z.array(z.string()).default([]),
|
|
173
202
|
broadPhaseDelayMs: z.number().min(0).max(300000).default(90000),
|
|
174
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Skip the expensive broad phase once this many candidates were found by
|
|
205
|
+
* the cheaper phases. Clamped at runtime to maxResults - 1 so it stays
|
|
206
|
+
* satisfiable; 0 disables skipping entirely.
|
|
207
|
+
*/
|
|
208
|
+
skipBroadWhenSufficientResults: z.number().int().min(0).max(100).default(8),
|
|
175
209
|
/**
|
|
176
210
|
* Optional Ollama model id used for SLM pre-triage during vetting
|
|
177
211
|
* (oss-autopilot#1122). Empty disables the feature. Recommended values:
|
|
@@ -199,7 +233,10 @@ export const ScoutPreferencesSchema = z.object({
|
|
|
199
233
|
featuresSplitRatio: z.number().min(0).max(1).default(0.6),
|
|
200
234
|
});
|
|
201
235
|
// ── Root state schema ───────────────────────────────────────────────
|
|
202
|
-
|
|
236
|
+
// Persisted schemas are loose (unknown keys round-trip) so an older binary
|
|
237
|
+
// loading state written by a newer one cannot silently strip and then
|
|
238
|
+
// persist away the newer fields (#137).
|
|
239
|
+
export const ScoutStateSchema = z.looseObject({
|
|
203
240
|
version: z.literal(1),
|
|
204
241
|
preferences: ScoutPreferencesSchema.default(() => ScoutPreferencesSchema.parse({})),
|
|
205
242
|
repoScores: z.record(z.string(), RepoScoreSchema).default({}),
|
|
@@ -210,7 +247,36 @@ export const ScoutStateSchema = z.object({
|
|
|
210
247
|
openPRs: z.array(StoredOpenPRSchema).default([]),
|
|
211
248
|
savedResults: z.array(SavedCandidateSchema).default([]),
|
|
212
249
|
skippedIssues: z.array(SkippedIssueSchema).default([]),
|
|
250
|
+
/**
|
|
251
|
+
* Deletion tombstones (#117). Without these, the gist union-merge would
|
|
252
|
+
* resurrect any saved result or skipped issue removed on one machine the
|
|
253
|
+
* next time it merged against another machine's copy that still had it.
|
|
254
|
+
* A tombstone suppresses an item across a merge until the item is
|
|
255
|
+
* re-added with a newer timestamp. Pruned after 90 days.
|
|
256
|
+
*/
|
|
257
|
+
tombstones: z
|
|
258
|
+
.array(z.looseObject({
|
|
259
|
+
url: z.string(),
|
|
260
|
+
removedAt: z.string(),
|
|
261
|
+
}))
|
|
262
|
+
.default([]),
|
|
263
|
+
/**
|
|
264
|
+
* When preferences were last changed (#117). The gist merge previously
|
|
265
|
+
* always took the remote preferences, silently reverting a local edit;
|
|
266
|
+
* the side with the fresher timestamp now wins.
|
|
267
|
+
*/
|
|
268
|
+
preferencesUpdatedAt: z.string().optional(),
|
|
213
269
|
lastSearchAt: z.string().optional(),
|
|
214
270
|
lastRunAt: z.string().default(() => new Date().toISOString()),
|
|
215
271
|
gistId: z.string().optional(),
|
|
216
272
|
});
|
|
273
|
+
/**
|
|
274
|
+
* Single entry point for parsing persisted state (local file, gist, gist
|
|
275
|
+
* cache). Version migrations belong here: when a version 2 ships, transform
|
|
276
|
+
* older raw shapes before validation so no load site ever sees unmigrated
|
|
277
|
+
* data. Unknown keys round-trip via the loose schemas above.
|
|
278
|
+
*/
|
|
279
|
+
export function parseScoutState(raw) {
|
|
280
|
+
// No migrations yet; version 1 is current.
|
|
281
|
+
return ScoutStateSchema.parse(raw);
|
|
282
|
+
}
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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();
|