@oss-scout/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }
@@ -2,7 +2,7 @@
2
2
  * Core types for oss-scout — ephemeral types that are never persisted.
3
3
  */
4
4
  import type { RepoSignals, TrackedIssue, IssueVettingResult, IssueScope, ScoutState, SearchStrategy } from "./schemas.js";
5
- export type { ProjectCategory, IssueScope, RepoSignals, RepoScore, StoredMergedPR, StoredClosedPR, ContributionGuidelines, IssueVettingResult, TrackedIssue, ScoutPreferences, SavedCandidate, ScoutState, SearchStrategy, } from "./schemas.js";
5
+ export type { ProjectCategory, IssueScope, RepoSignals, RepoScore, StoredMergedPR, StoredClosedPR, ContributionGuidelines, IssueVettingResult, LinkedPR, TrackedIssue, ScoutPreferences, SavedCandidate, ScoutState, SearchStrategy, } from "./schemas.js";
6
6
  /** Health snapshot of a GitHub repository. */
7
7
  export interface ProjectHealth {
8
8
  repo: string;
@@ -20,11 +20,34 @@ export interface ProjectHealth {
20
20
  }
21
21
  /** Priority tier for issue search results. */
22
22
  export type SearchPriority = "merged_pr" | "starred" | "normal";
23
+ /** Source file the anti-LLM policy match came from, or null when no file matched. */
24
+ export type AntiLLMPolicySourceFile = "CONTRIBUTING.md" | "CODE_OF_CONDUCT.md" | "README.md";
25
+ /** Result of scanning a repo's policy docs for anti-LLM/AI keywords. */
26
+ export interface AntiLLMPolicyResult {
27
+ matched: boolean;
28
+ matchedKeywords: string[];
29
+ sourceFile: AntiLLMPolicySourceFile | null;
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
+ }
23
43
  /** A fully vetted issue candidate with scoring. */
24
44
  export interface IssueCandidate {
25
45
  issue: TrackedIssue;
26
46
  vettingResult: IssueVettingResult;
27
47
  projectHealth: ProjectHealth;
48
+ antiLLMPolicy: AntiLLMPolicyResult;
49
+ /** SLM pre-triage result, or `null` when not configured / unavailable. */
50
+ slmTriage: SLMTriageSummary | null;
28
51
  recommendation: "approve" | "skip" | "needs_review";
29
52
  reasonsToSkip: string[];
30
53
  reasonsToApprove: string[];
package/dist/index.d.ts CHANGED
@@ -15,9 +15,10 @@
15
15
  * @packageDocumentation
16
16
  */
17
17
  export { createScout, OssScout } from "./scout.js";
18
- export type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, OpenPRRecord, RepoScoreUpdate, ProjectHealth, SearchPriority, CheckResult, VetListOptions, VetListResult, VetListEntry, VetListSummary, } from "./core/types.js";
19
- export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, StoredOpenPR, SearchStrategy, SkippedIssue, } from "./core/schemas.js";
18
+ export type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, OpenPRRecord, RepoScoreUpdate, ProjectHealth, SearchPriority, CheckResult, AntiLLMPolicyResult, AntiLLMPolicySourceFile, VetListOptions, VetListResult, VetListEntry, VetListSummary, } from "./core/types.js";
19
+ export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, LinkedPR, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, StoredOpenPR, SearchStrategy, SkippedIssue, } from "./core/schemas.js";
20
20
  export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, SkippedIssueSchema, } from "./core/schemas.js";
21
21
  export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
22
22
  export { IssueDiscovery } from "./core/issue-discovery.js";
23
23
  export { IssueVetter, type ScoutStateReader } from "./core/issue-vetting.js";
24
+ export { scanForAntiLLMPolicy, ANTI_LLM_KEYWORDS, } from "./core/anti-llm-policy.js";
package/dist/index.js CHANGED
@@ -23,3 +23,4 @@ export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
23
23
  // Internal classes (for advanced use)
24
24
  export { IssueDiscovery } from "./core/issue-discovery.js";
25
25
  export { IssueVetter } from "./core/issue-vetting.js";
26
+ export { scanForAntiLLMPolicy, ANTI_LLM_KEYWORDS, } from "./core/anti-llm-policy.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-scout/core",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
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": {