@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared scout construction for CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* Picks the persistence mode from `state.preferences.persistence` so the
|
|
5
|
+
* `gist` preference is actually honored (#115). Previously every command
|
|
6
|
+
* hardcoded `provided` mode, leaving gist sync unreachable no matter what
|
|
7
|
+
* `oss-scout config set persistence gist` wrote.
|
|
8
|
+
*/
|
|
9
|
+
import type { ScoutState } from "../core/schemas.js";
|
|
10
|
+
import { type OssScout } from "../scout.js";
|
|
11
|
+
/**
|
|
12
|
+
* Build a scout for a CLI command from already-loaded local state and a token.
|
|
13
|
+
*
|
|
14
|
+
* - `persistence: "gist"` preference → gist-backed scout. createScout loads
|
|
15
|
+
* local state itself and merges it with the gist, and `checkpoint()` pushes
|
|
16
|
+
* to the gist. The caller still calls `saveLocalState` to keep the local
|
|
17
|
+
* file fresh as an offline cache.
|
|
18
|
+
* - otherwise → provided-state scout backed by the local file. The command's
|
|
19
|
+
* `saveLocalState` + `checkpoint()` persist locally.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildCommandScout(state: ScoutState, token: string): Promise<OssScout>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createScout } from "../scout.js";
|
|
2
|
+
/**
|
|
3
|
+
* Build a scout for a CLI command from already-loaded local state and a token.
|
|
4
|
+
*
|
|
5
|
+
* - `persistence: "gist"` preference → gist-backed scout. createScout loads
|
|
6
|
+
* local state itself and merges it with the gist, and `checkpoint()` pushes
|
|
7
|
+
* to the gist. The caller still calls `saveLocalState` to keep the local
|
|
8
|
+
* file fresh as an offline cache.
|
|
9
|
+
* - otherwise → provided-state scout backed by the local file. The command's
|
|
10
|
+
* `saveLocalState` + `checkpoint()` persist locally.
|
|
11
|
+
*/
|
|
12
|
+
export async function buildCommandScout(state, token) {
|
|
13
|
+
if (state.preferences.persistence === "gist") {
|
|
14
|
+
return createScout({ githubToken: token, persistence: "gist" });
|
|
15
|
+
}
|
|
16
|
+
return createScout({
|
|
17
|
+
githubToken: token,
|
|
18
|
+
persistence: "provided",
|
|
19
|
+
initialState: state,
|
|
20
|
+
});
|
|
21
|
+
}
|
package/dist/commands/config.js
CHANGED
|
@@ -2,82 +2,8 @@
|
|
|
2
2
|
* Config command — view and update oss-scout preferences.
|
|
3
3
|
*/
|
|
4
4
|
import { loadLocalState, saveLocalState } from "../core/local-state.js";
|
|
5
|
-
import { ScoutPreferencesSchema
|
|
6
|
-
import {
|
|
7
|
-
const FIELD_CONFIGS = {
|
|
8
|
-
languages: { type: "array" },
|
|
9
|
-
labels: { type: "array" },
|
|
10
|
-
excludeRepos: { type: "array" },
|
|
11
|
-
excludeOrgs: { type: "array" },
|
|
12
|
-
aiPolicyBlocklist: { type: "array" },
|
|
13
|
-
minStars: { type: "number" },
|
|
14
|
-
maxIssueAgeDays: { type: "number" },
|
|
15
|
-
minRepoScoreThreshold: { type: "number" },
|
|
16
|
-
interPhaseDelayMs: { type: "number" },
|
|
17
|
-
includeDocIssues: { type: "boolean" },
|
|
18
|
-
scope: { type: "enum-array", validValues: IssueScopeSchema.options },
|
|
19
|
-
projectCategories: {
|
|
20
|
-
type: "enum-array",
|
|
21
|
-
validValues: ProjectCategorySchema.options,
|
|
22
|
-
},
|
|
23
|
-
persistence: { type: "enum", validValues: PersistenceModeSchema.options },
|
|
24
|
-
defaultStrategy: {
|
|
25
|
-
type: "enum-array",
|
|
26
|
-
validValues: SearchStrategySchema.options,
|
|
27
|
-
},
|
|
28
|
-
githubUsername: { type: "string" },
|
|
29
|
-
broadPhaseDelayMs: { type: "number" },
|
|
30
|
-
skipBroadWhenSufficientResults: { type: "number" },
|
|
31
|
-
featuresAnchorThreshold: { type: "number" },
|
|
32
|
-
featuresSplitRatio: { type: "float" },
|
|
33
|
-
};
|
|
34
|
-
function parseBoolean(value) {
|
|
35
|
-
const lower = value.toLowerCase();
|
|
36
|
-
if (lower === "true" || lower === "yes")
|
|
37
|
-
return true;
|
|
38
|
-
if (lower === "false" || lower === "no")
|
|
39
|
-
return false;
|
|
40
|
-
throw new ValidationError(`Invalid boolean value: "${value}". Use true/false or yes/no.`);
|
|
41
|
-
}
|
|
42
|
-
function parseNumber(value, key) {
|
|
43
|
-
const num = parseInt(value, 10);
|
|
44
|
-
if (isNaN(num)) {
|
|
45
|
-
throw new ValidationError(`Invalid number for "${key}": "${value}"`);
|
|
46
|
-
}
|
|
47
|
-
return num;
|
|
48
|
-
}
|
|
49
|
-
function parseFloat(value, key) {
|
|
50
|
-
const num = Number.parseFloat(value);
|
|
51
|
-
if (isNaN(num)) {
|
|
52
|
-
throw new ValidationError(`Invalid number for "${key}": "${value}"`);
|
|
53
|
-
}
|
|
54
|
-
return num;
|
|
55
|
-
}
|
|
56
|
-
function parseArrayValue(value) {
|
|
57
|
-
return value
|
|
58
|
-
.split(",")
|
|
59
|
-
.map((s) => s.trim())
|
|
60
|
-
.filter((s) => s.length > 0);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Apply an array update: plain set, +append, or -remove.
|
|
64
|
-
*/
|
|
65
|
-
function updateArray(current, value) {
|
|
66
|
-
if (value.startsWith("+")) {
|
|
67
|
-
const toAdd = parseArrayValue(value.slice(1));
|
|
68
|
-
const merged = [...current];
|
|
69
|
-
for (const item of toAdd) {
|
|
70
|
-
if (!merged.includes(item))
|
|
71
|
-
merged.push(item);
|
|
72
|
-
}
|
|
73
|
-
return merged;
|
|
74
|
-
}
|
|
75
|
-
if (value.startsWith("-")) {
|
|
76
|
-
const toRemove = new Set(parseArrayValue(value.slice(1)));
|
|
77
|
-
return current.filter((item) => !toRemove.has(item));
|
|
78
|
-
}
|
|
79
|
-
return parseArrayValue(value);
|
|
80
|
-
}
|
|
5
|
+
import { ScoutPreferencesSchema } from "../core/schemas.js";
|
|
6
|
+
import { applyPreferenceField } from "../core/preference-fields.js";
|
|
81
7
|
function formatArray(arr) {
|
|
82
8
|
return arr.length > 0 ? arr.join(", ") : "(none)";
|
|
83
9
|
}
|
|
@@ -103,8 +29,13 @@ export function runConfigShow() {
|
|
|
103
29
|
console.log(` aiPolicyBlocklist: ${formatArray(prefs.aiPolicyBlocklist)}`);
|
|
104
30
|
console.log(` defaultStrategy: ${prefs.defaultStrategy ? formatArray(prefs.defaultStrategy) : "(all)"}`);
|
|
105
31
|
console.log(` persistence: ${prefs.persistence}`);
|
|
32
|
+
console.log(` preferLanguages: ${formatArray(prefs.preferLanguages)}`);
|
|
33
|
+
console.log(` preferRepos: ${formatArray(prefs.preferRepos)}`);
|
|
34
|
+
console.log(` diversityRatio: ${prefs.diversityRatio}`);
|
|
106
35
|
console.log(` broadPhaseDelayMs: ${prefs.broadPhaseDelayMs}ms (${(prefs.broadPhaseDelayMs / 1000).toFixed(0)}s)`);
|
|
107
36
|
console.log(` skipBroadWhenSufficientResults: ${prefs.skipBroadWhenSufficientResults}`);
|
|
37
|
+
console.log(` slmTriageModel: ${prefs.slmTriageModel || "(disabled)"}`);
|
|
38
|
+
console.log(` slmTriageHost: ${prefs.slmTriageHost || "(default 127.0.0.1:11434)"}`);
|
|
108
39
|
console.log(` featuresAnchorThreshold: ${prefs.featuresAnchorThreshold}`);
|
|
109
40
|
console.log(` featuresSplitRatio: ${prefs.featuresSplitRatio}`);
|
|
110
41
|
console.log();
|
|
@@ -120,60 +51,10 @@ export function getConfigData() {
|
|
|
120
51
|
* Update a single preference by key.
|
|
121
52
|
*/
|
|
122
53
|
export function runConfigSet(key, value) {
|
|
123
|
-
const field = FIELD_CONFIGS[key];
|
|
124
|
-
if (!field) {
|
|
125
|
-
throw new ValidationError(`Unknown config key: "${key}". Valid keys: ${Object.keys(FIELD_CONFIGS).sort().join(", ")}`);
|
|
126
|
-
}
|
|
127
54
|
const state = loadLocalState();
|
|
128
|
-
const
|
|
129
|
-
switch (field.type) {
|
|
130
|
-
case "string":
|
|
131
|
-
prefs[key] = value;
|
|
132
|
-
break;
|
|
133
|
-
case "boolean":
|
|
134
|
-
prefs[key] = parseBoolean(value);
|
|
135
|
-
break;
|
|
136
|
-
case "number":
|
|
137
|
-
prefs[key] = parseNumber(value, key);
|
|
138
|
-
break;
|
|
139
|
-
case "float":
|
|
140
|
-
prefs[key] = parseFloat(value, key);
|
|
141
|
-
break;
|
|
142
|
-
case "array": {
|
|
143
|
-
const current = prefs[key] ?? [];
|
|
144
|
-
prefs[key] = updateArray(current, value);
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
case "enum": {
|
|
148
|
-
const validValues = field.validValues;
|
|
149
|
-
if (!validValues.includes(value)) {
|
|
150
|
-
throw new ValidationError(`Invalid value for "${key}": "${value}". Valid: ${validValues.join(", ")}`);
|
|
151
|
-
}
|
|
152
|
-
prefs[key] = value;
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
case "enum-array": {
|
|
156
|
-
const current = prefs[key] ?? [];
|
|
157
|
-
const updated = updateArray(current, value);
|
|
158
|
-
const validValues = field.validValues;
|
|
159
|
-
const invalid = updated.filter((s) => !validValues.includes(s));
|
|
160
|
-
if (invalid.length > 0) {
|
|
161
|
-
throw new ValidationError(`Invalid value(s) for "${key}": ${invalid.join(", ")}. Valid: ${validValues.join(", ")}`);
|
|
162
|
-
}
|
|
163
|
-
// For 'scope', empty array means undefined (all scopes)
|
|
164
|
-
if (key === "scope") {
|
|
165
|
-
prefs[key] =
|
|
166
|
-
updated.length > 0 ? updated : undefined;
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
prefs[key] = updated;
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
// Validate the full preferences object
|
|
175
|
-
const validated = ScoutPreferencesSchema.parse(prefs);
|
|
55
|
+
const validated = applyPreferenceField(state.preferences, key, value);
|
|
176
56
|
state.preferences = validated;
|
|
57
|
+
state.preferencesUpdatedAt = new Date().toISOString(); // #117 merge recency
|
|
177
58
|
saveLocalState(state);
|
|
178
59
|
return validated;
|
|
179
60
|
}
|
|
@@ -184,6 +65,7 @@ export function runConfigReset() {
|
|
|
184
65
|
const state = loadLocalState();
|
|
185
66
|
const defaults = ScoutPreferencesSchema.parse({});
|
|
186
67
|
state.preferences = defaults;
|
|
68
|
+
state.preferencesUpdatedAt = new Date().toISOString(); // #117 merge recency
|
|
187
69
|
saveLocalState(state);
|
|
188
70
|
return defaults;
|
|
189
71
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Features command — surfaces feature opportunities in anchor repos.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { requireGitHubToken } from "../core/utils.js";
|
|
6
|
-
import { saveLocalState } from "../core/local-state.js";
|
|
4
|
+
import { withScout } from "./with-scout.js";
|
|
7
5
|
import { isLinkedPRStalled } from "../core/linked-pr.js";
|
|
8
6
|
function mapLinkedPR(c) {
|
|
9
7
|
const linkedPR = c.vettingResult.linkedPR;
|
|
@@ -48,29 +46,18 @@ function mapBiggerBet(c) {
|
|
|
48
46
|
};
|
|
49
47
|
}
|
|
50
48
|
export async function runFeatures(options) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
65
|
-
saveLocalState(scout.getState());
|
|
66
|
-
const persisted = await scout.checkpoint();
|
|
67
|
-
if (!persisted) {
|
|
68
|
-
console.error("Warning: changes saved locally but gist sync failed.");
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
quickWins: result.quickWins.map(mapQuickWin),
|
|
72
|
-
biggerBets: result.biggerBets.map(mapBiggerBet),
|
|
73
|
-
anchorRepos: result.anchorRepos,
|
|
74
|
-
message: result.message,
|
|
75
|
-
};
|
|
49
|
+
return withScout(options.state, async (scout) => {
|
|
50
|
+
const result = await scout.features({
|
|
51
|
+
count: options.maxResults,
|
|
52
|
+
anchorThreshold: options.anchorThreshold,
|
|
53
|
+
splitRatio: options.splitRatio,
|
|
54
|
+
broad: options.broad,
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
quickWins: result.quickWins.map(mapQuickWin),
|
|
58
|
+
biggerBets: result.biggerBets.map(mapBiggerBet),
|
|
59
|
+
anchorRepos: result.anchorRepos,
|
|
60
|
+
message: result.message,
|
|
61
|
+
};
|
|
62
|
+
}, { persist: true });
|
|
76
63
|
}
|
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
* Results command — display and manage saved search results.
|
|
3
3
|
*/
|
|
4
4
|
import type { SavedCandidate } from "../core/schemas.js";
|
|
5
|
-
export
|
|
5
|
+
export interface ResultsOptions {
|
|
6
6
|
json?: boolean;
|
|
7
|
-
|
|
7
|
+
/** Only results first seen at/after this ISO date (or any Date-parseable string). */
|
|
8
|
+
since?: string;
|
|
9
|
+
/** Only results first seen during/after the most recent search run. */
|
|
10
|
+
newOnly?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Return saved results, optionally narrowed to "new" ones. `--since` takes an
|
|
14
|
+
* explicit cutoff; `--new-only` uses the last search timestamp so a scheduler
|
|
15
|
+
* can run `search` then `results --new-only` to see just that run's fresh
|
|
16
|
+
* finds (#170). Both compare against each result's `firstSeenAt`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runResults(options?: ResultsOptions): Promise<SavedCandidate[]>;
|
|
8
19
|
export declare function runResultsClear(): Promise<void>;
|
package/dist/commands/results.js
CHANGED
|
@@ -2,9 +2,36 @@
|
|
|
2
2
|
* Results command — display and manage saved search results.
|
|
3
3
|
*/
|
|
4
4
|
import { loadLocalState, saveLocalState } from "../core/local-state.js";
|
|
5
|
-
|
|
5
|
+
import { ValidationError } from "../core/errors.js";
|
|
6
|
+
/**
|
|
7
|
+
* Return saved results, optionally narrowed to "new" ones. `--since` takes an
|
|
8
|
+
* explicit cutoff; `--new-only` uses the last search timestamp so a scheduler
|
|
9
|
+
* can run `search` then `results --new-only` to see just that run's fresh
|
|
10
|
+
* finds (#170). Both compare against each result's `firstSeenAt`.
|
|
11
|
+
*/
|
|
12
|
+
export async function runResults(options = {}) {
|
|
6
13
|
const state = loadLocalState();
|
|
7
|
-
|
|
14
|
+
const results = state.savedResults ?? [];
|
|
15
|
+
let cutoff;
|
|
16
|
+
if (options.since !== undefined) {
|
|
17
|
+
const parsed = Date.parse(options.since);
|
|
18
|
+
if (Number.isNaN(parsed)) {
|
|
19
|
+
throw new ValidationError(`Invalid --since date: "${options.since}". Use an ISO date like 2026-06-01.`);
|
|
20
|
+
}
|
|
21
|
+
cutoff = parsed;
|
|
22
|
+
}
|
|
23
|
+
else if (options.newOnly) {
|
|
24
|
+
// No prior search recorded → everything counts as new.
|
|
25
|
+
const parsed = state.lastSearchAt ? Date.parse(state.lastSearchAt) : NaN;
|
|
26
|
+
cutoff = Number.isNaN(parsed) ? undefined : parsed;
|
|
27
|
+
}
|
|
28
|
+
if (cutoff === undefined)
|
|
29
|
+
return results;
|
|
30
|
+
const threshold = cutoff;
|
|
31
|
+
return results.filter((r) => {
|
|
32
|
+
const seen = Date.parse(r.firstSeenAt);
|
|
33
|
+
return !Number.isNaN(seen) && seen >= threshold;
|
|
34
|
+
});
|
|
8
35
|
}
|
|
9
36
|
export async function runResultsClear() {
|
|
10
37
|
const state = loadLocalState();
|
|
@@ -64,6 +64,10 @@ interface SearchCommandOptions {
|
|
|
64
64
|
preferLanguages?: string[];
|
|
65
65
|
/** Soft sort boost for candidates in these `owner/repo` slugs (#1244). */
|
|
66
66
|
preferRepos?: string[];
|
|
67
|
+
/** Soft sort penalty for candidates in these `owner/repo` slugs (#168). */
|
|
68
|
+
avoidRepos?: string[];
|
|
69
|
+
/** Soft sort boost for candidates whose labels match these types (#168). */
|
|
70
|
+
boostIssueTypes?: string[];
|
|
67
71
|
/** Diversity counterweight: fraction of slots reserved for unboosted candidates (#1244). */
|
|
68
72
|
diversityRatio?: number;
|
|
69
73
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -1,76 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Search command — finds contributable issues using multi-strategy search.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import { requireGitHubToken } from "../core/utils.js";
|
|
6
|
-
import { saveLocalState } from "../core/local-state.js";
|
|
4
|
+
import { withScout } from "./with-scout.js";
|
|
7
5
|
import { isLinkedPRStalled } from "../core/linked-pr.js";
|
|
8
6
|
export async function runSearch(options) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
rateLimitWarning: result.rateLimitWarning,
|
|
74
|
-
strategiesUsed: result.strategiesUsed,
|
|
75
|
-
};
|
|
7
|
+
return withScout(options.state, async (scout) => {
|
|
8
|
+
const result = await scout.search({
|
|
9
|
+
maxResults: options.maxResults,
|
|
10
|
+
strategies: options.strategies,
|
|
11
|
+
preferLanguages: options.preferLanguages,
|
|
12
|
+
preferRepos: options.preferRepos,
|
|
13
|
+
avoidRepos: options.avoidRepos,
|
|
14
|
+
boostIssueTypes: options.boostIssueTypes,
|
|
15
|
+
diversityRatio: options.diversityRatio,
|
|
16
|
+
});
|
|
17
|
+
scout.saveResults(result.candidates);
|
|
18
|
+
return {
|
|
19
|
+
candidates: result.candidates.map((c) => {
|
|
20
|
+
const repoScoreRecord = scout.getRepoScoreRecord(c.issue.repo);
|
|
21
|
+
return {
|
|
22
|
+
issue: {
|
|
23
|
+
repo: c.issue.repo,
|
|
24
|
+
repoUrl: `https://github.com/${c.issue.repo}`,
|
|
25
|
+
number: c.issue.number,
|
|
26
|
+
title: c.issue.title,
|
|
27
|
+
url: c.issue.url,
|
|
28
|
+
labels: c.issue.labels,
|
|
29
|
+
},
|
|
30
|
+
recommendation: c.recommendation,
|
|
31
|
+
reasonsToApprove: c.reasonsToApprove,
|
|
32
|
+
reasonsToSkip: c.reasonsToSkip,
|
|
33
|
+
searchPriority: c.searchPriority,
|
|
34
|
+
viabilityScore: c.viabilityScore,
|
|
35
|
+
repoScore: repoScoreRecord
|
|
36
|
+
? {
|
|
37
|
+
score: repoScoreRecord.score,
|
|
38
|
+
mergedPRCount: repoScoreRecord.mergedPRCount,
|
|
39
|
+
closedWithoutMergeCount: repoScoreRecord.closedWithoutMergeCount,
|
|
40
|
+
isResponsive: repoScoreRecord.signals?.isResponsive ?? false,
|
|
41
|
+
lastMergedAt: repoScoreRecord.lastMergedAt,
|
|
42
|
+
}
|
|
43
|
+
: undefined,
|
|
44
|
+
linkedPR: c.vettingResult.linkedPR
|
|
45
|
+
? {
|
|
46
|
+
number: c.vettingResult.linkedPR.number,
|
|
47
|
+
state: c.vettingResult.linkedPR.state,
|
|
48
|
+
url: c.vettingResult.linkedPR.url,
|
|
49
|
+
updatedAt: c.vettingResult.linkedPR.updatedAt,
|
|
50
|
+
isStalled: isLinkedPRStalled(c.vettingResult.linkedPR),
|
|
51
|
+
}
|
|
52
|
+
: undefined,
|
|
53
|
+
// Keep the JSON output shape stable across the #158 internal
|
|
54
|
+
// refactor: derive the flat boost fields from the structural
|
|
55
|
+
// `personalization` marker.
|
|
56
|
+
boostScore: c.personalization?.kind === "boosted"
|
|
57
|
+
? c.personalization.score
|
|
58
|
+
: undefined,
|
|
59
|
+
boostReasons: c.personalization?.kind === "boosted"
|
|
60
|
+
? c.personalization.reasons
|
|
61
|
+
: undefined,
|
|
62
|
+
diversitySlot: c.personalization?.kind === "diversity" ? true : undefined,
|
|
63
|
+
};
|
|
64
|
+
}),
|
|
65
|
+
excludedRepos: result.excludedRepos,
|
|
66
|
+
aiPolicyBlocklist: result.aiPolicyBlocklist,
|
|
67
|
+
rateLimitWarning: result.rateLimitWarning,
|
|
68
|
+
strategiesUsed: result.strategiesUsed,
|
|
69
|
+
};
|
|
70
|
+
}, { persist: true });
|
|
76
71
|
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { ScoutPreferences } from "../core/schemas.js";
|
|
|
5
5
|
interface ReadlineInterface {
|
|
6
6
|
question(query: string, callback: (answer: string) => void): void;
|
|
7
7
|
close(): void;
|
|
8
|
+
on?(event: "close", listener: () => void): unknown;
|
|
9
|
+
off?(event: "close", listener: () => void): unknown;
|
|
8
10
|
}
|
|
9
11
|
export interface SetupOptions {
|
|
10
12
|
rl?: ReadlineInterface;
|
package/dist/commands/setup.js
CHANGED
|
@@ -4,17 +4,36 @@
|
|
|
4
4
|
import * as readline from "readline";
|
|
5
5
|
import { execFile } from "child_process";
|
|
6
6
|
import { ScoutPreferencesSchema, ProjectCategorySchema, IssueScopeSchema, } from "../core/schemas.js";
|
|
7
|
+
import { ConfigurationError } from "../core/errors.js";
|
|
7
8
|
const ALL_CATEGORIES = ProjectCategorySchema.options;
|
|
8
9
|
const ALL_SCOPES = IssueScopeSchema.options;
|
|
9
10
|
function createReadlineInterface() {
|
|
11
|
+
// Prompts echo on stderr so stdout stays pure for --json output (#131)
|
|
10
12
|
return readline.createInterface({
|
|
11
13
|
input: process.stdin,
|
|
12
|
-
output: process.
|
|
14
|
+
output: process.stderr,
|
|
13
15
|
});
|
|
14
16
|
}
|
|
15
17
|
function ask(rl, query) {
|
|
16
|
-
return new Promise((resolve) => {
|
|
17
|
-
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
// Piped stdin that ends early used to leave the pending question
|
|
20
|
+
// unresolved: the event loop drained and the process exited 0 without
|
|
21
|
+
// saving anything (#131). Reject on close instead.
|
|
22
|
+
let settled = false;
|
|
23
|
+
const onClose = () => {
|
|
24
|
+
if (settled)
|
|
25
|
+
return;
|
|
26
|
+
settled = true;
|
|
27
|
+
reject(new ConfigurationError("Input ended before setup finished; preferences were not saved"));
|
|
28
|
+
};
|
|
29
|
+
rl.on?.("close", onClose);
|
|
30
|
+
rl.question(query, (answer) => {
|
|
31
|
+
if (settled)
|
|
32
|
+
return;
|
|
33
|
+
settled = true;
|
|
34
|
+
rl.off?.("close", onClose);
|
|
35
|
+
resolve(answer.trim());
|
|
36
|
+
});
|
|
18
37
|
});
|
|
19
38
|
}
|
|
20
39
|
function detectGitHubUsername() {
|
|
@@ -45,12 +64,19 @@ function parseMultiSelect(input, options) {
|
|
|
45
64
|
* Run the interactive setup flow and return the configured preferences.
|
|
46
65
|
*/
|
|
47
66
|
export async function runSetup(options) {
|
|
67
|
+
// Fail fast in non-interactive contexts (CI, piped stdin): prompting
|
|
68
|
+
// would hang or silently save defaults (#131). Injected rl (tests, hosts)
|
|
69
|
+
// opts out of the guard.
|
|
70
|
+
if (!options?.rl && !process.stdin.isTTY) {
|
|
71
|
+
throw new ConfigurationError("setup is interactive and requires a terminal. Use `oss-scout config set <key> <value>` for non-interactive configuration.");
|
|
72
|
+
}
|
|
48
73
|
const rl = options?.rl ?? createReadlineInterface();
|
|
49
74
|
const detect = options?.detectUsername ?? detectGitHubUsername;
|
|
50
75
|
try {
|
|
51
|
-
|
|
76
|
+
// Interactive chrome goes to stderr; stdout stays pure for --json (#131)
|
|
77
|
+
console.error("\n🔧 oss-scout setup\n");
|
|
52
78
|
// Detect GitHub username
|
|
53
|
-
console.
|
|
79
|
+
console.error("Detecting GitHub username...");
|
|
54
80
|
const detectedUsername = await detect();
|
|
55
81
|
const usernameDefault = detectedUsername || "";
|
|
56
82
|
const usernamePrompt = detectedUsername
|
|
@@ -84,6 +110,8 @@ export async function runSetup(options) {
|
|
|
84
110
|
// Repos to exclude
|
|
85
111
|
const excludeInput = await ask(rl, "Repos to exclude (owner/repo, comma-separated, optional): ");
|
|
86
112
|
const excludeRepos = parseCSV(excludeInput);
|
|
113
|
+
// Optional local SLM pre-triage (Ollama). Empty leaves it disabled.
|
|
114
|
+
const slmTriageModel = await ask(rl, "Local SLM triage model for faster pre-filtering (Ollama model id, e.g. gemma4:e4b, optional): ");
|
|
87
115
|
const prefs = ScoutPreferencesSchema.parse({
|
|
88
116
|
githubUsername,
|
|
89
117
|
languages,
|
|
@@ -92,8 +120,9 @@ export async function runSetup(options) {
|
|
|
92
120
|
excludeRepos,
|
|
93
121
|
projectCategories,
|
|
94
122
|
minStars: isNaN(minStars) ? 50 : minStars,
|
|
123
|
+
slmTriageModel,
|
|
95
124
|
});
|
|
96
|
-
console.
|
|
125
|
+
console.error("\n✅ Setup complete! Preferences saved.\n");
|
|
97
126
|
return prefs;
|
|
98
127
|
}
|
|
99
128
|
finally {
|
package/dist/commands/skip.d.ts
CHANGED
|
@@ -25,6 +25,10 @@ export declare function runSkipList(options?: {
|
|
|
25
25
|
export declare function runSkipClear(): Promise<void>;
|
|
26
26
|
/**
|
|
27
27
|
* Remove a specific issue from the skip list (unskip).
|
|
28
|
+
*
|
|
29
|
+
* Deliberately does NOT validate the URL: entries stored before skip-add
|
|
30
|
+
* validation existed may be junk, and exact-match removal is the only way
|
|
31
|
+
* to clean them up short of `skip clear`.
|
|
28
32
|
*/
|
|
29
33
|
export declare function runSkipRemove(options: {
|
|
30
34
|
issueUrl: string;
|