@oss-scout/core 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.bundle.cjs +28 -27
- package/dist/core/issue-vetting.d.ts +9 -0
- package/dist/core/issue-vetting.js +18 -0
- package/dist/core/schemas.d.ts +4 -0
- package/dist/core/schemas.js +13 -0
- package/dist/core/slm-triage.d.ts +72 -0
- package/dist/core/slm-triage.js +135 -0
- package/dist/core/types.d.ts +14 -0
- package/dist/scout.d.ts +8 -0
- package/dist/scout.js +10 -0
- package/package.json +1 -1
|
@@ -23,6 +23,15 @@ export interface ScoutStateReader {
|
|
|
23
23
|
getProjectCategories(): ProjectCategory[];
|
|
24
24
|
/** Numeric quality score for a repo, or null if not evaluated. */
|
|
25
25
|
getRepoScore(repo: string): number | null;
|
|
26
|
+
/**
|
|
27
|
+
* SLM pre-triage config (oss-autopilot#1122). Returns the configured
|
|
28
|
+
* model id and Ollama host, or empty strings when not configured —
|
|
29
|
+
* vetIssue treats either of these as "skip the SLM call".
|
|
30
|
+
*/
|
|
31
|
+
getSLMTriageConfig?(): {
|
|
32
|
+
model: string;
|
|
33
|
+
host: string;
|
|
34
|
+
};
|
|
26
35
|
}
|
|
27
36
|
export declare class IssueVetter {
|
|
28
37
|
private octokit;
|
|
@@ -15,6 +15,7 @@ import { checkNoExistingPR, checkNotClaimed, checkUserMergedPRsInRepo, analyzeRe
|
|
|
15
15
|
import { checkProjectHealth, fetchContributionGuidelines, } from "./repo-health.js";
|
|
16
16
|
import { fetchAndScanAntiLLMPolicy } from "./anti-llm-policy.js";
|
|
17
17
|
import { getHttpCache } from "./http-cache.js";
|
|
18
|
+
import { triageWithSLM, buildTriageInput, } from "./slm-triage.js";
|
|
18
19
|
const MODULE = "issue-vetting";
|
|
19
20
|
/** Vetting concurrency: kept low to reduce burst pressure on GitHub's secondary rate limit. */
|
|
20
21
|
const MAX_CONCURRENT_VETTING = 3;
|
|
@@ -221,11 +222,28 @@ export class IssueVetter {
|
|
|
221
222
|
else if (starredRepos.includes(repoFullName)) {
|
|
222
223
|
searchPriority = "starred";
|
|
223
224
|
}
|
|
225
|
+
// Optional SLM pre-triage (oss-autopilot#1122). Fail-open: any error
|
|
226
|
+
// path returns null and the rest of the pipeline is unaffected.
|
|
227
|
+
const slmConfig = this.stateReader.getSLMTriageConfig?.() ?? {
|
|
228
|
+
model: "",
|
|
229
|
+
host: "",
|
|
230
|
+
};
|
|
231
|
+
let slmTriage = null;
|
|
232
|
+
if (slmConfig.model) {
|
|
233
|
+
const slmOpts = { model: slmConfig.model };
|
|
234
|
+
if (slmConfig.host)
|
|
235
|
+
slmOpts.host = slmConfig.host;
|
|
236
|
+
slmTriage = await triageWithSLM(buildTriageInput({
|
|
237
|
+
issue: { ...trackedIssue, body: ghIssue.body ?? "" },
|
|
238
|
+
linkedPR: existingPRCheck.linkedPR ?? null,
|
|
239
|
+
}), slmOpts);
|
|
240
|
+
}
|
|
224
241
|
const result = {
|
|
225
242
|
issue: trackedIssue,
|
|
226
243
|
vettingResult,
|
|
227
244
|
projectHealth,
|
|
228
245
|
antiLLMPolicy,
|
|
246
|
+
slmTriage,
|
|
229
247
|
recommendation,
|
|
230
248
|
reasonsToSkip,
|
|
231
249
|
reasonsToApprove,
|
package/dist/core/schemas.d.ts
CHANGED
|
@@ -252,6 +252,8 @@ export declare const ScoutPreferencesSchema: z.ZodObject<{
|
|
|
252
252
|
}>>>;
|
|
253
253
|
broadPhaseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
254
254
|
skipBroadWhenSufficientResults: z.ZodDefault<z.ZodNumber>;
|
|
255
|
+
slmTriageModel: z.ZodDefault<z.ZodString>;
|
|
256
|
+
slmTriageHost: z.ZodDefault<z.ZodString>;
|
|
255
257
|
}, z.core.$strip>;
|
|
256
258
|
export declare const ScoutStateSchema: z.ZodObject<{
|
|
257
259
|
version: z.ZodLiteral<1>;
|
|
@@ -293,6 +295,8 @@ export declare const ScoutStateSchema: z.ZodObject<{
|
|
|
293
295
|
}>>>;
|
|
294
296
|
broadPhaseDelayMs: z.ZodDefault<z.ZodNumber>;
|
|
295
297
|
skipBroadWhenSufficientResults: z.ZodDefault<z.ZodNumber>;
|
|
298
|
+
slmTriageModel: z.ZodDefault<z.ZodString>;
|
|
299
|
+
slmTriageHost: z.ZodDefault<z.ZodString>;
|
|
296
300
|
}, z.core.$strip>>;
|
|
297
301
|
repoScores: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
298
302
|
repo: z.ZodString;
|
package/dist/core/schemas.js
CHANGED
|
@@ -164,6 +164,19 @@ export const ScoutPreferencesSchema = z.object({
|
|
|
164
164
|
defaultStrategy: z.array(SearchStrategySchema).optional(),
|
|
165
165
|
broadPhaseDelayMs: z.number().min(0).max(300000).default(90000),
|
|
166
166
|
skipBroadWhenSufficientResults: z.number().int().min(0).max(100).default(15),
|
|
167
|
+
/**
|
|
168
|
+
* Optional Ollama model id used for SLM pre-triage during vetting
|
|
169
|
+
* (oss-autopilot#1122). Empty disables the feature. Recommended values:
|
|
170
|
+
* `gemma4:e4b` (default for capable hardware) or `gemma4:e2b` /
|
|
171
|
+
* `qwen3:1.7b` for low-RAM machines.
|
|
172
|
+
*/
|
|
173
|
+
slmTriageModel: z.string().default(""),
|
|
174
|
+
/**
|
|
175
|
+
* Override the Ollama HTTP host. Defaults to `http://127.0.0.1:11434`
|
|
176
|
+
* when empty. Useful when Ollama runs on a different machine on the
|
|
177
|
+
* local network.
|
|
178
|
+
*/
|
|
179
|
+
slmTriageHost: z.string().default(""),
|
|
167
180
|
});
|
|
168
181
|
// ── Root state schema ───────────────────────────────────────────────
|
|
169
182
|
export const ScoutStateSchema = z.object({
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional SLM (small language model) pre-triage pass for vetted issues
|
|
3
|
+
* (oss-autopilot#1122).
|
|
4
|
+
*
|
|
5
|
+
* When the user has an Ollama instance running locally and a model
|
|
6
|
+
* configured via `slmTriageModel`, vetting can call out to that model
|
|
7
|
+
* for a structured classification of each candidate issue. The result
|
|
8
|
+
* is surfaced on `IssueCandidate.slmTriage` so consumers (autopilot
|
|
9
|
+
* agents, dashboard, vet-list output) can show the call up-front and
|
|
10
|
+
* skip the cost of reading every issue body manually.
|
|
11
|
+
*
|
|
12
|
+
* Design highlights:
|
|
13
|
+
* - **Fail open.** Any failure (no model configured, Ollama down,
|
|
14
|
+
* timeout, malformed JSON, schema mismatch) returns `null`. Triage
|
|
15
|
+
* must never block the rest of the vetting pipeline.
|
|
16
|
+
* - **Schema-enforced JSON.** Uses Ollama's `format` parameter so the
|
|
17
|
+
* decoder produces JSON conformant to a fixed schema; eliminates the
|
|
18
|
+
* "model returned partial JSON" failure mode that plagues prompt-only
|
|
19
|
+
* structured-output schemes.
|
|
20
|
+
* - **15s timeout.** Local SLMs vary widely in latency; 15s covers
|
|
21
|
+
* small-to-mid models on consumer hardware (Gemma 4 e4b, Qwen 3 4b).
|
|
22
|
+
* Slower models simply produce `null` and don't block vetting.
|
|
23
|
+
*/
|
|
24
|
+
import type { TrackedIssue, LinkedPR } from "./schemas.js";
|
|
25
|
+
/**
|
|
26
|
+
* Result of an SLM triage call. The same shape Ollama is constrained to
|
|
27
|
+
* produce via `format` schema enforcement.
|
|
28
|
+
*/
|
|
29
|
+
export interface SLMTriageResult {
|
|
30
|
+
/** Three buckets that match human triage decisions. */
|
|
31
|
+
decision: "pursue" | "investigate" | "skip";
|
|
32
|
+
/** How sure the model is. Surface in UI; don't gate on it server-side. */
|
|
33
|
+
confidence: "high" | "medium" | "low";
|
|
34
|
+
/** Short phrases (not sentences) explaining the decision. 1–3 entries. */
|
|
35
|
+
reasons: string[];
|
|
36
|
+
/** Model id that produced this result. Useful when comparing runs. */
|
|
37
|
+
modelVersion: string;
|
|
38
|
+
}
|
|
39
|
+
/** Inputs to a triage call. */
|
|
40
|
+
export interface SLMTriageInput {
|
|
41
|
+
issue: Pick<TrackedIssue, "title" | "labels"> & {
|
|
42
|
+
body?: string;
|
|
43
|
+
};
|
|
44
|
+
linkedPRExists: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Runtime options for `triageWithSLM`. */
|
|
47
|
+
export interface SLMTriageOptions {
|
|
48
|
+
/** Model id (e.g. `gemma4:e4b`). Empty/unset disables triage. */
|
|
49
|
+
model: string;
|
|
50
|
+
/** Override Ollama base URL. Defaults to `http://127.0.0.1:11434`. */
|
|
51
|
+
host?: string;
|
|
52
|
+
/** Override request timeout. */
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
/** Override fetch implementation (for tests). */
|
|
55
|
+
fetchImpl?: typeof fetch;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Run an SLM triage classification. Returns `null` on any failure path
|
|
59
|
+
* — caller treats `null` as "no SLM signal available".
|
|
60
|
+
*/
|
|
61
|
+
export declare function triageWithSLM(input: SLMTriageInput, options: SLMTriageOptions): Promise<SLMTriageResult | null>;
|
|
62
|
+
/**
|
|
63
|
+
* Adapter: build an `SLMTriageInput` from the standard scout types we
|
|
64
|
+
* carry through `vetIssue`. Centralizes the mapping so callers don't
|
|
65
|
+
* have to know about the prompt internals.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildTriageInput(args: {
|
|
68
|
+
issue: TrackedIssue & {
|
|
69
|
+
body?: string;
|
|
70
|
+
};
|
|
71
|
+
linkedPR: LinkedPR | null | undefined;
|
|
72
|
+
}): SLMTriageInput;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Default Ollama HTTP endpoint when not overridden. */
|
|
2
|
+
const DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434";
|
|
3
|
+
/** Default per-call timeout. */
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
5
|
+
/** Hard cap on issue body length we send to the model. */
|
|
6
|
+
const MAX_BODY_CHARS = 2000;
|
|
7
|
+
/** JSON schema enforced server-side by Ollama's `format` parameter. */
|
|
8
|
+
const SLM_TRIAGE_SCHEMA = {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
decision: { type: "string", enum: ["pursue", "investigate", "skip"] },
|
|
12
|
+
confidence: { type: "string", enum: ["high", "medium", "low"] },
|
|
13
|
+
reasons: {
|
|
14
|
+
type: "array",
|
|
15
|
+
items: { type: "string" },
|
|
16
|
+
minItems: 1,
|
|
17
|
+
maxItems: 3,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
required: ["decision", "confidence", "reasons"],
|
|
21
|
+
};
|
|
22
|
+
/** Build the user-message prompt from an issue. */
|
|
23
|
+
function buildPrompt(input) {
|
|
24
|
+
const { issue, linkedPRExists } = input;
|
|
25
|
+
const body = (issue.body ?? "").slice(0, MAX_BODY_CHARS);
|
|
26
|
+
return [
|
|
27
|
+
"You triage open-source issues for an autonomous contribution agent.",
|
|
28
|
+
"Classify the issue into exactly one bucket:",
|
|
29
|
+
"- pursue: small, concrete bug or feature with clear acceptance; safe for an autonomous agent to attempt without further design input",
|
|
30
|
+
"- investigate: tractable but needs human reading first (ambiguous scope, design questions, recently-touched files)",
|
|
31
|
+
"- skip: not actionable autonomously (epic, creative, blocked, requires upstream change, requires infra)",
|
|
32
|
+
"",
|
|
33
|
+
"Return JSON only matching the provided schema. Reasons must be short phrases, not sentences. 1-3 reasons total.",
|
|
34
|
+
"",
|
|
35
|
+
"Issue:",
|
|
36
|
+
`Title: ${issue.title}`,
|
|
37
|
+
`Body: ${body}`,
|
|
38
|
+
`Labels: ${issue.labels.join(", ")}`,
|
|
39
|
+
`Linked PR exists: ${linkedPRExists}`,
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Run an SLM triage classification. Returns `null` on any failure path
|
|
44
|
+
* — caller treats `null` as "no SLM signal available".
|
|
45
|
+
*/
|
|
46
|
+
export async function triageWithSLM(input, options) {
|
|
47
|
+
if (!options.model)
|
|
48
|
+
return null;
|
|
49
|
+
const host = options.host ?? DEFAULT_OLLAMA_HOST;
|
|
50
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
51
|
+
const fetchFn = options.fetchImpl ?? fetch;
|
|
52
|
+
let response;
|
|
53
|
+
try {
|
|
54
|
+
response = await fetchFn(`${host}/api/chat`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
model: options.model,
|
|
59
|
+
messages: [{ role: "user", content: buildPrompt(input) }],
|
|
60
|
+
stream: false,
|
|
61
|
+
format: SLM_TRIAGE_SCHEMA,
|
|
62
|
+
options: { temperature: 0.1 },
|
|
63
|
+
}),
|
|
64
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Connection refused, timeout, DNS error, etc.
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (!response.ok)
|
|
72
|
+
return null;
|
|
73
|
+
let payload;
|
|
74
|
+
try {
|
|
75
|
+
payload = await response.json();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
// Ollama `chat` returns { message: { content: string }, ... }.
|
|
81
|
+
const content = payload?.message
|
|
82
|
+
?.content;
|
|
83
|
+
if (typeof content !== "string")
|
|
84
|
+
return null;
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(content);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (!isValidTriageShape(parsed))
|
|
93
|
+
return null;
|
|
94
|
+
return {
|
|
95
|
+
decision: parsed.decision,
|
|
96
|
+
confidence: parsed.confidence,
|
|
97
|
+
reasons: parsed.reasons,
|
|
98
|
+
modelVersion: options.model,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Adapter: build an `SLMTriageInput` from the standard scout types we
|
|
103
|
+
* carry through `vetIssue`. Centralizes the mapping so callers don't
|
|
104
|
+
* have to know about the prompt internals.
|
|
105
|
+
*/
|
|
106
|
+
export function buildTriageInput(args) {
|
|
107
|
+
return {
|
|
108
|
+
issue: {
|
|
109
|
+
title: args.issue.title,
|
|
110
|
+
labels: args.issue.labels,
|
|
111
|
+
body: args.issue.body,
|
|
112
|
+
},
|
|
113
|
+
linkedPRExists: !!args.linkedPR,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function isValidTriageShape(value) {
|
|
117
|
+
if (typeof value !== "object" || value === null)
|
|
118
|
+
return false;
|
|
119
|
+
const v = value;
|
|
120
|
+
if (v.decision !== "pursue" &&
|
|
121
|
+
v.decision !== "investigate" &&
|
|
122
|
+
v.decision !== "skip")
|
|
123
|
+
return false;
|
|
124
|
+
if (v.confidence !== "high" &&
|
|
125
|
+
v.confidence !== "medium" &&
|
|
126
|
+
v.confidence !== "low")
|
|
127
|
+
return false;
|
|
128
|
+
if (!Array.isArray(v.reasons) ||
|
|
129
|
+
v.reasons.length === 0 ||
|
|
130
|
+
v.reasons.length > 3)
|
|
131
|
+
return false;
|
|
132
|
+
if (!v.reasons.every((r) => typeof r === "string"))
|
|
133
|
+
return false;
|
|
134
|
+
return true;
|
|
135
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -28,12 +28,26 @@ export interface AntiLLMPolicyResult {
|
|
|
28
28
|
matchedKeywords: string[];
|
|
29
29
|
sourceFile: AntiLLMPolicySourceFile | null;
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Optional SLM (small language model) pre-triage classification for an
|
|
33
|
+
* issue (oss-autopilot#1122). Populated when the user has configured
|
|
34
|
+
* `slmTriageModel` and a local Ollama instance is reachable. Always
|
|
35
|
+
* fail-open: any error path leaves this `null`.
|
|
36
|
+
*/
|
|
37
|
+
export interface SLMTriageSummary {
|
|
38
|
+
decision: "pursue" | "investigate" | "skip";
|
|
39
|
+
confidence: "high" | "medium" | "low";
|
|
40
|
+
reasons: string[];
|
|
41
|
+
modelVersion: string;
|
|
42
|
+
}
|
|
31
43
|
/** A fully vetted issue candidate with scoring. */
|
|
32
44
|
export interface IssueCandidate {
|
|
33
45
|
issue: TrackedIssue;
|
|
34
46
|
vettingResult: IssueVettingResult;
|
|
35
47
|
projectHealth: ProjectHealth;
|
|
36
48
|
antiLLMPolicy: AntiLLMPolicyResult;
|
|
49
|
+
/** SLM pre-triage result, or `null` when not configured / unavailable. */
|
|
50
|
+
slmTriage: SLMTriageSummary | null;
|
|
37
51
|
recommendation: "approve" | "skip" | "needs_review";
|
|
38
52
|
reasonsToSkip: string[];
|
|
39
53
|
reasonsToApprove: string[];
|
package/dist/scout.d.ts
CHANGED
|
@@ -63,6 +63,14 @@ export declare class OssScout implements ScoutStateReader {
|
|
|
63
63
|
getStarredRepos(): string[];
|
|
64
64
|
getProjectCategories(): ProjectCategory[];
|
|
65
65
|
getRepoScore(repo: string): number | null;
|
|
66
|
+
/**
|
|
67
|
+
* Optional SLM pre-triage config read from preferences (oss-autopilot#1122).
|
|
68
|
+
* Empty `model` disables the call; the vetter treats it as a no-op.
|
|
69
|
+
*/
|
|
70
|
+
getSLMTriageConfig(): {
|
|
71
|
+
model: string;
|
|
72
|
+
host: string;
|
|
73
|
+
};
|
|
66
74
|
/** Get current preferences (read-only). */
|
|
67
75
|
getPreferences(): Readonly<ScoutPreferences>;
|
|
68
76
|
/** Get repo score record for a specific repository. */
|
package/dist/scout.js
CHANGED
|
@@ -258,6 +258,16 @@ export class OssScout {
|
|
|
258
258
|
const score = this.state.repoScores[repo];
|
|
259
259
|
return score ? score.score : null;
|
|
260
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Optional SLM pre-triage config read from preferences (oss-autopilot#1122).
|
|
263
|
+
* Empty `model` disables the call; the vetter treats it as a no-op.
|
|
264
|
+
*/
|
|
265
|
+
getSLMTriageConfig() {
|
|
266
|
+
return {
|
|
267
|
+
model: this.state.preferences.slmTriageModel ?? "",
|
|
268
|
+
host: this.state.preferences.slmTriageHost ?? "",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
261
271
|
/** Get current preferences (read-only). */
|
|
262
272
|
getPreferences() {
|
|
263
273
|
return this.state.preferences;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-scout/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Personalized GitHub issue finder with multi-strategy search, deep vetting, and viability scoring — CLI, library, MCP server, and Claude Code plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|