@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.
Files changed (68) hide show
  1. package/dist/cli.bundle.cjs +89 -66
  2. package/dist/cli.js +302 -436
  3. package/dist/commands/command-scout.d.ts +21 -0
  4. package/dist/commands/command-scout.js +21 -0
  5. package/dist/commands/config.js +10 -128
  6. package/dist/commands/features.js +15 -28
  7. package/dist/commands/results.d.ts +13 -2
  8. package/dist/commands/results.js +29 -2
  9. package/dist/commands/search.d.ts +4 -0
  10. package/dist/commands/search.js +65 -70
  11. package/dist/commands/setup.d.ts +2 -0
  12. package/dist/commands/setup.js +35 -6
  13. package/dist/commands/skip.d.ts +4 -0
  14. package/dist/commands/skip.js +45 -55
  15. package/dist/commands/sync.d.ts +10 -0
  16. package/dist/commands/sync.js +10 -0
  17. package/dist/commands/vet-list.js +3 -19
  18. package/dist/commands/vet.js +18 -25
  19. package/dist/commands/with-scout.d.ts +32 -0
  20. package/dist/commands/with-scout.js +41 -0
  21. package/dist/core/anti-llm-policy.js +5 -33
  22. package/dist/core/bootstrap.d.ts +2 -2
  23. package/dist/core/bootstrap.js +5 -9
  24. package/dist/core/errors.d.ts +10 -0
  25. package/dist/core/errors.js +20 -5
  26. package/dist/core/feature-discovery.d.ts +13 -1
  27. package/dist/core/feature-discovery.js +104 -81
  28. package/dist/core/gist-state-store.d.ts +13 -12
  29. package/dist/core/gist-state-store.js +128 -53
  30. package/dist/core/http-cache.d.ts +32 -2
  31. package/dist/core/http-cache.js +74 -19
  32. package/dist/core/issue-discovery.d.ts +12 -1
  33. package/dist/core/issue-discovery.js +94 -67
  34. package/dist/core/issue-eligibility.d.ts +11 -4
  35. package/dist/core/issue-eligibility.js +124 -69
  36. package/dist/core/issue-graphql.d.ts +58 -0
  37. package/dist/core/issue-graphql.js +108 -0
  38. package/dist/core/issue-vetting.d.ts +115 -9
  39. package/dist/core/issue-vetting.js +246 -109
  40. package/dist/core/local-state.d.ts +6 -2
  41. package/dist/core/local-state.js +23 -5
  42. package/dist/core/logger.d.ts +12 -4
  43. package/dist/core/logger.js +33 -7
  44. package/dist/core/personalization.d.ts +30 -10
  45. package/dist/core/personalization.js +64 -24
  46. package/dist/core/preference-fields.d.ts +47 -0
  47. package/dist/core/preference-fields.js +180 -0
  48. package/dist/core/probe-repo-file.d.ts +47 -0
  49. package/dist/core/probe-repo-file.js +57 -0
  50. package/dist/core/repo-health.js +40 -32
  51. package/dist/core/roadmap.js +26 -22
  52. package/dist/core/schemas.d.ts +148 -26
  53. package/dist/core/schemas.js +83 -17
  54. package/dist/core/search-budget.d.ts +9 -0
  55. package/dist/core/search-budget.js +36 -3
  56. package/dist/core/search-phases.d.ts +4 -21
  57. package/dist/core/search-phases.js +37 -89
  58. package/dist/core/types.d.ts +151 -38
  59. package/dist/core/utils.js +60 -26
  60. package/dist/formatters/human.d.ts +60 -0
  61. package/dist/formatters/human.js +199 -0
  62. package/dist/formatters/markdown.d.ts +10 -0
  63. package/dist/formatters/markdown.js +31 -0
  64. package/dist/index.d.ts +6 -2
  65. package/dist/index.js +8 -0
  66. package/dist/scout.d.ts +75 -12
  67. package/dist/scout.js +265 -26
  68. package/package.json +1 -1
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Human-readable (non-JSON) output formatters for the oss-scout CLI.
3
+ *
4
+ * Each renderer is a pure function that returns the exact multi-line string the
5
+ * CLI used to emit via a sequence of `console.log` calls. The caller does a
6
+ * single `console.log(renderX(...))`, which appends the one trailing newline
7
+ * that the final `console.log` in the old inline block produced.
8
+ *
9
+ * To stay byte-identical: every old `console.log(line)` becomes one entry in a
10
+ * lines array, a bare `console.log()` (blank line) becomes an empty entry, and
11
+ * the array is joined with "\n". The caller's own `console.log` supplies the
12
+ * last newline. STDERR output (the search rate-limit warning) is deliberately
13
+ * NOT folded in here — it stays a `console.error` in the caller.
14
+ */
15
+ import type { SearchOutput } from "../commands/search.js";
16
+ import type { FeaturesOutput } from "../commands/features.js";
17
+ import type { SavedCandidate } from "../core/schemas.js";
18
+ import type { VetListResult } from "../core/types.js";
19
+ import type { VetOutput } from "../commands/vet.js";
20
+ /** Emoji for a vetting recommendation, shared by the search and vet renderers. */
21
+ export declare function recommendationIcon(recommendation: "approve" | "skip" | "needs_review"): string;
22
+ /**
23
+ * Render the human-readable `search` output: the "Found N issue candidates"
24
+ * block with per-candidate icon, personalization and stalled tags, and the
25
+ * optional repoScore line. The trailing rate-limit warning is NOT included
26
+ * here; it goes to stderr in the caller.
27
+ */
28
+ export declare function renderSearch(results: SearchOutput): string;
29
+ /**
30
+ * Render the human-readable `features` output: the optional message, the
31
+ * "Feature opportunities" header, the anchor repos line, and the Quick wins /
32
+ * Bigger bets sections. Returns "" when there is nothing to print beyond an
33
+ * absent message (caller guards against logging a blank line).
34
+ */
35
+ export declare function renderFeatures(result: FeaturesOutput, options: {
36
+ broad?: boolean;
37
+ }): string;
38
+ /** The empty-state message printed by `results` when nothing is saved. */
39
+ export declare const RESULTS_EMPTY_MESSAGE = "\nNo saved results. Run `oss-scout search` to find issues.\n";
40
+ /**
41
+ * Render the human-readable `results` table: the "Saved results" header and a
42
+ * Score / Repo / Issue / Recommendation / Title row per saved candidate.
43
+ * Callers handle the empty state (RESULTS_EMPTY_MESSAGE) separately.
44
+ */
45
+ export declare function renderResults(results: SavedCandidate[]): string;
46
+ /** The empty-state message printed by `vet-list` when there is nothing to vet. */
47
+ export declare const VET_LIST_EMPTY_MESSAGE = "\nNo saved results to vet. Run `oss-scout search` first.\n";
48
+ /**
49
+ * Render the human-readable `vet-list` output: the "Vet-list results (N)"
50
+ * block with a per-row status icon, the "Changes since last check"
51
+ * transitions block, the summary line, and the optional pruned-count line.
52
+ * Callers handle the empty state (VET_LIST_EMPTY_MESSAGE) separately.
53
+ */
54
+ export declare function renderVetList(result: VetListResult): string;
55
+ /**
56
+ * Render the human-readable single-issue `vet` output: the recommendation
57
+ * header, the reasons to approve / skip, and the project-health block. The
58
+ * checkFailed branch (#158) is preserved exactly.
59
+ */
60
+ export declare function renderVet(result: VetOutput): string;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Human-readable (non-JSON) output formatters for the oss-scout CLI.
3
+ *
4
+ * Each renderer is a pure function that returns the exact multi-line string the
5
+ * CLI used to emit via a sequence of `console.log` calls. The caller does a
6
+ * single `console.log(renderX(...))`, which appends the one trailing newline
7
+ * that the final `console.log` in the old inline block produced.
8
+ *
9
+ * To stay byte-identical: every old `console.log(line)` becomes one entry in a
10
+ * lines array, a bare `console.log()` (blank line) becomes an empty entry, and
11
+ * the array is joined with "\n". The caller's own `console.log` supplies the
12
+ * last newline. STDERR output (the search rate-limit warning) is deliberately
13
+ * NOT folded in here — it stays a `console.error` in the caller.
14
+ */
15
+ /** Emoji for a vetting recommendation, shared by the search and vet renderers. */
16
+ export function recommendationIcon(recommendation) {
17
+ if (recommendation === "approve")
18
+ return "✅";
19
+ if (recommendation === "skip")
20
+ return "❌";
21
+ return "⚠️";
22
+ }
23
+ /**
24
+ * Render the human-readable `search` output: the "Found N issue candidates"
25
+ * block with per-candidate icon, personalization and stalled tags, and the
26
+ * optional repoScore line. The trailing rate-limit warning is NOT included
27
+ * here; it goes to stderr in the caller.
28
+ */
29
+ export function renderSearch(results) {
30
+ const lines = [];
31
+ lines.push(`\nFound ${results.candidates.length} issue candidates:\n`);
32
+ for (const c of results.candidates) {
33
+ const icon = recommendationIcon(c.recommendation);
34
+ const stalledTag = c.linkedPR?.isStalled
35
+ ? " (stalled PR, revive opportunity)"
36
+ : "";
37
+ // Personalization tag (#1244). A candidate is either boosted (matched a
38
+ // preference) or a diversity slot (matched none and filled a reserved
39
+ // slot); never both.
40
+ let personalizationTag = "";
41
+ if (c.boostReasons && c.boostReasons.length > 0) {
42
+ // Net score can be negative when avoidRepos applied (#168).
43
+ const verb = (c.boostScore ?? 0) >= 0 ? "boosted" : "deprioritized";
44
+ personalizationTag = ` [${verb}: ${c.boostReasons.join("; ")}]`;
45
+ }
46
+ else if (c.diversitySlot) {
47
+ personalizationTag = " [diversity slot]";
48
+ }
49
+ lines.push(` ${icon} ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100]${personalizationTag}${stalledTag}`);
50
+ lines.push(` ${c.issue.title}`);
51
+ lines.push(` ${c.issue.url}`);
52
+ if (c.repoScore) {
53
+ lines.push(` Repo: ${c.repoScore.score}/10, ${c.repoScore.mergedPRCount} merged PRs`);
54
+ }
55
+ lines.push("");
56
+ }
57
+ return lines.join("\n");
58
+ }
59
+ /**
60
+ * Render the human-readable `features` output: the optional message, the
61
+ * "Feature opportunities" header, the anchor repos line, and the Quick wins /
62
+ * Bigger bets sections. Returns "" when there is nothing to print beyond an
63
+ * absent message (caller guards against logging a blank line).
64
+ */
65
+ export function renderFeatures(result, options) {
66
+ const lines = [];
67
+ const total = result.quickWins.length + result.biggerBets.length;
68
+ if (result.message) {
69
+ lines.push(`\n${result.message}\n`);
70
+ }
71
+ if (total === 0)
72
+ return lines.join("\n");
73
+ const headerScope = options.broad
74
+ ? "across the ecosystem"
75
+ : "in your anchor repos";
76
+ lines.push(`\n🎯 Feature opportunities ${headerScope} (${result.quickWins.length} quick wins + ${result.biggerBets.length} bigger bets)\n`);
77
+ if (!options.broad) {
78
+ lines.push(`Anchor repos: ${result.anchorRepos.join(", ")}\n`);
79
+ }
80
+ if (result.quickWins.length) {
81
+ lines.push("── Quick wins ─────────────────────────────────────────");
82
+ for (const c of result.quickWins) {
83
+ const stalledTag = c.linkedPR?.isStalled
84
+ ? " (stalled PR, revive opportunity)"
85
+ : "";
86
+ lines.push(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
87
+ lines.push(` ${c.issue.url}`);
88
+ }
89
+ lines.push("");
90
+ }
91
+ if (result.biggerBets.length) {
92
+ lines.push("── Bigger bets ────────────────────────────────────────");
93
+ for (const c of result.biggerBets) {
94
+ const stalledTag = c.linkedPR?.isStalled
95
+ ? " (stalled PR, revive opportunity)"
96
+ : "";
97
+ lines.push(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
98
+ lines.push(` ${c.issue.url}`);
99
+ }
100
+ lines.push("");
101
+ }
102
+ return lines.join("\n");
103
+ }
104
+ /** The empty-state message printed by `results` when nothing is saved. */
105
+ export const RESULTS_EMPTY_MESSAGE = "\nNo saved results. Run `oss-scout search` to find issues.\n";
106
+ /**
107
+ * Render the human-readable `results` table: the "Saved results" header and a
108
+ * Score / Repo / Issue / Recommendation / Title row per saved candidate.
109
+ * Callers handle the empty state (RESULTS_EMPTY_MESSAGE) separately.
110
+ */
111
+ export function renderResults(results) {
112
+ const lines = [];
113
+ lines.push(`\nSaved results (${results.length}):\n`);
114
+ lines.push(" Score Repo Issue Recommendation Title");
115
+ lines.push(" ───── ──────────────────────────────── ────── ────────────── ─────");
116
+ for (const r of results) {
117
+ const score = String(r.viabilityScore).padStart(3);
118
+ const repo = r.repo.padEnd(32).slice(0, 32);
119
+ const issue = `#${r.number}`.padEnd(6);
120
+ const rec = r.recommendation.padEnd(14);
121
+ const title = r.title.length > 50 ? r.title.slice(0, 47) + "..." : r.title;
122
+ lines.push(` ${score} ${repo} ${issue} ${rec} ${title}`);
123
+ }
124
+ lines.push("");
125
+ return lines.join("\n");
126
+ }
127
+ /** The empty-state message printed by `vet-list` when there is nothing to vet. */
128
+ export const VET_LIST_EMPTY_MESSAGE = "\nNo saved results to vet. Run `oss-scout search` first.\n";
129
+ /** Icon for a vet-list entry's availability status. */
130
+ function vetListStatusIcon(status) {
131
+ return status === "still_available"
132
+ ? "✅"
133
+ : status === "claimed"
134
+ ? "🔒"
135
+ : status === "has_pr"
136
+ ? "🔀"
137
+ : status === "closed"
138
+ ? "🚫"
139
+ : "❌";
140
+ }
141
+ /**
142
+ * Render the human-readable `vet-list` output: the "Vet-list results (N)"
143
+ * block with a per-row status icon, the "Changes since last check"
144
+ * transitions block, the summary line, and the optional pruned-count line.
145
+ * Callers handle the empty state (VET_LIST_EMPTY_MESSAGE) separately.
146
+ */
147
+ export function renderVetList(result) {
148
+ const lines = [];
149
+ lines.push(`\nVet-list results (${result.summary.total}):\n`);
150
+ for (const r of result.results) {
151
+ const icon = vetListStatusIcon(r.status);
152
+ const score = r.ok ? ` [${r.viabilityScore}/100]` : "";
153
+ lines.push(` ${icon} ${r.repo}#${r.number} — ${r.status}${score}`);
154
+ lines.push(` ${r.title}`);
155
+ }
156
+ if (result.transitions.length > 0) {
157
+ lines.push(`\n🔔 Changes since last check (${result.transitions.length}):`);
158
+ for (const t of result.transitions) {
159
+ lines.push(` ${t.repo}#${t.number}: ${t.from} → ${t.to}`);
160
+ }
161
+ }
162
+ lines.push(`\nSummary: ${result.summary.stillAvailable} available, ${result.summary.claimed} claimed, ${result.summary.hasPR} has PR, ${result.summary.closed} closed, ${result.summary.errors} errors`);
163
+ if (result.prunedCount != null) {
164
+ lines.push(`Pruned ${result.prunedCount} unavailable issues from saved results.`);
165
+ }
166
+ lines.push("");
167
+ return lines.join("\n");
168
+ }
169
+ /**
170
+ * Render the human-readable single-issue `vet` output: the recommendation
171
+ * header, the reasons to approve / skip, and the project-health block. The
172
+ * checkFailed branch (#158) is preserved exactly.
173
+ */
174
+ export function renderVet(result) {
175
+ const lines = [];
176
+ const icon = recommendationIcon(result.recommendation);
177
+ lines.push(`\n${icon} ${result.issue.repo}#${result.issue.number}: ${result.recommendation.toUpperCase()}`);
178
+ lines.push(` ${result.issue.title}`);
179
+ lines.push(` ${result.issue.url}\n`);
180
+ if (result.reasonsToApprove.length > 0) {
181
+ lines.push("Reasons to approve:");
182
+ for (const r of result.reasonsToApprove)
183
+ lines.push(` + ${r}`);
184
+ }
185
+ if (result.reasonsToSkip.length > 0) {
186
+ lines.push("Reasons to skip:");
187
+ for (const r of result.reasonsToSkip)
188
+ lines.push(` - ${r}`);
189
+ }
190
+ if (result.projectHealth.checkFailed) {
191
+ lines.push(`\nProject health: unknown (check failed: ${result.projectHealth.failureReason})`);
192
+ }
193
+ else {
194
+ lines.push(`\nProject health: ${result.projectHealth.isActive ? "Active" : "Inactive"}`);
195
+ lines.push(` Last commit: ${result.projectHealth.daysSinceLastCommit} days ago`);
196
+ lines.push(` CI status: ${result.projectHealth.ciStatus}`);
197
+ }
198
+ return lines.join("\n");
199
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Markdown output formatter (#170) — renders saved results as a table for
3
+ * digests, notes export, and scheduled GitHub-issue summaries.
4
+ */
5
+ import type { SavedCandidate } from "../core/schemas.js";
6
+ /**
7
+ * Render saved results as a GitHub-flavored markdown table, sorted by
8
+ * viability score descending. Returns a friendly message when empty.
9
+ */
10
+ export declare function formatResultsMarkdown(results: SavedCandidate[]): string;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Markdown output formatter (#170) — renders saved results as a table for
3
+ * digests, notes export, and scheduled GitHub-issue summaries.
4
+ */
5
+ /** Escape pipe and newline so a title can't break the markdown table. */
6
+ function cell(value) {
7
+ return value.replace(/\r?\n/g, " ").replace(/\|/g, "\\|").trim();
8
+ }
9
+ /**
10
+ * Render saved results as a GitHub-flavored markdown table, sorted by
11
+ * viability score descending. Returns a friendly message when empty.
12
+ */
13
+ export function formatResultsMarkdown(results) {
14
+ if (results.length === 0) {
15
+ return "_No saved results._";
16
+ }
17
+ const sorted = [...results].sort((a, b) => b.viabilityScore - a.viabilityScore);
18
+ const header = "| Score | Repo | Issue | Recommendation | Title |";
19
+ const divider = "| ----- | ---- | ----- | -------------- | ----- |";
20
+ const rows = sorted.map((r) => {
21
+ const issueLink = `[#${r.number}](${r.issueUrl})`;
22
+ return `| ${r.viabilityScore} | ${cell(r.repo)} | ${issueLink} | ${cell(r.recommendation)} | ${cell(r.title)} |`;
23
+ });
24
+ return [
25
+ `## oss-scout results (${results.length})`,
26
+ "",
27
+ header,
28
+ divider,
29
+ ...rows,
30
+ ].join("\n");
31
+ }
package/dist/index.d.ts CHANGED
@@ -15,13 +15,17 @@
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, AntiLLMPolicyResult, AntiLLMPolicySourceFile, VetListOptions, VetListResult, VetListEntry, VetListSummary, } from "./core/types.js";
18
+ export type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, OpenPRRecord, RepoScoreUpdate, ProjectHealth, ProjectHealthData, ProjectHealthFailure, SearchPriority, CheckResult, AntiLLMPolicyResult, AntiLLMPolicySourceFile, VetListOptions, VetListResult, VetListEntry, VetListEntryBase, VetListSummary, SyncResult, } from "./core/types.js";
19
19
  export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, LinkedPR, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, StoredOpenPR, SearchStrategy, SkippedIssue, Horizon, } from "./core/schemas.js";
20
20
  export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, SkippedIssueSchema, HorizonSchema, } from "./core/schemas.js";
21
+ export { applyPreferenceField, FIELD_CONFIGS, PREFERENCE_KEYS, SORTED_PREFERENCE_KEYS, assertFieldConfigsCover, updateArray, type FieldConfig, } from "./core/preference-fields.js";
21
22
  export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
22
23
  export { IssueDiscovery } from "./core/issue-discovery.js";
23
- export { IssueVetter, type ScoutStateReader, type FeatureSignals, } from "./core/issue-vetting.js";
24
+ export { IssueVetter, type ScoutStateReader, type ScoutStateWriter, type SLMConfig, type FeatureSignals, } from "./core/issue-vetting.js";
24
25
  export { scanForAntiLLMPolicy, ANTI_LLM_KEYWORDS, } from "./core/anti-llm-policy.js";
26
+ export { bootstrapScout, type BootstrapResult } from "./core/bootstrap.js";
27
+ export { setLogLevel, getLogLevel, enableDebug, type LogLevel, } from "./core/logger.js";
25
28
  export { discoverFeatures, resolveAnchorRepos, classifyHorizon, splitByHorizon, ANCHOR_THRESHOLD, FEATURE_LABELS, NO_ANCHORS_MESSAGE, NO_RESULTS_MESSAGE, type FeatureCandidate, type FeatureSearchResult, type DiscoverFeaturesOptions, } from "./core/feature-discovery.js";
26
29
  export { isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, } from "./core/linked-pr.js";
27
30
  export { fetchRoadmapIssueRefs, parseRoadmapIssueRefs, } from "./core/roadmap.js";
31
+ export { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl, } from "./commands/validation.js";
package/dist/index.js CHANGED
@@ -18,15 +18,23 @@
18
18
  export { createScout, OssScout } from "./scout.js";
19
19
  // Schemas (for consumers who need runtime validation)
20
20
  export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, SkippedIssueSchema, HorizonSchema, } from "./core/schemas.js";
21
+ // Preference-field metadata + parsing (shared by the CLI and the MCP server)
22
+ export { applyPreferenceField, FIELD_CONFIGS, PREFERENCE_KEYS, SORTED_PREFERENCE_KEYS, assertFieldConfigsCover, updateArray, } from "./core/preference-fields.js";
21
23
  // Utilities
22
24
  export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
23
25
  // Internal classes (for advanced use)
24
26
  export { IssueDiscovery } from "./core/issue-discovery.js";
25
27
  export { IssueVetter, } from "./core/issue-vetting.js";
26
28
  export { scanForAntiLLMPolicy, ANTI_LLM_KEYWORDS, } from "./core/anti-llm-policy.js";
29
+ // Bootstrap (seed state from GitHub) — usable by library/MCP hosts (#156)
30
+ export { bootstrapScout } from "./core/bootstrap.js";
31
+ // Log-level control for library hosts (#156)
32
+ export { setLogLevel, getLogLevel, enableDebug, } from "./core/logger.js";
27
33
  // Feature discovery API
28
34
  export { discoverFeatures, resolveAnchorRepos, classifyHorizon, splitByHorizon, ANCHOR_THRESHOLD, FEATURE_LABELS, NO_ANCHORS_MESSAGE, NO_RESULTS_MESSAGE, } from "./core/feature-discovery.js";
29
35
  // Linked-PR helpers (#97)
30
36
  export { isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, } from "./core/linked-pr.js";
31
37
  // Roadmap scraping (#95)
32
38
  export { fetchRoadmapIssueRefs, parseRoadmapIssueRefs, } from "./core/roadmap.js";
39
+ // Issue-URL validation (shared by the CLI and the MCP server)
40
+ export { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl, } from "./commands/validation.js";
package/dist/scout.d.ts CHANGED
@@ -4,11 +4,19 @@
4
4
  * Provides personalized issue discovery, vetting, and scoring.
5
5
  * Implements ScoutStateReader to bridge state with the search engine.
6
6
  */
7
- import type { ScoutStateReader } from "./core/issue-vetting.js";
7
+ import type { ScoutStateReader, ScoutStateWriter, SLMConfig } from "./core/issue-vetting.js";
8
8
  import { type FeatureSearchResult } from "./core/feature-discovery.js";
9
9
  import type { ScoutState, ScoutPreferences, RepoScore, SavedCandidate, SkippedIssue, Horizon } from "./core/schemas.js";
10
- import type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, OpenPRRecord, RepoScoreUpdate, ProjectCategory, VetListOptions, VetListResult } from "./core/types.js";
10
+ import type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, OpenPRRecord, RepoScoreUpdate, ProjectCategory, SyncResult, VetListOptions, VetListResult } from "./core/types.js";
11
11
  import { GistStateStore } from "./core/gist-state-store.js";
12
+ import type { GistOctokitLike } from "./core/gist-state-store.js";
13
+ import type { Octokit } from "@octokit/rest";
14
+ /**
15
+ * Wrap a real Octokit instance as GistOctokitLike without unsafe double casts.
16
+ * Exported (not via the package index) so the response-narrowing logic — the
17
+ * "no id" guards and the files/list mapping — is unit-testable (#162).
18
+ */
19
+ export declare function toGistOctokit(octokit: Octokit): GistOctokitLike;
12
20
  /**
13
21
  * Create an OssScout instance.
14
22
  *
@@ -37,17 +45,42 @@ export declare function createScout(config: ScoutConfig): Promise<OssScout>;
37
45
  * Implements ScoutStateReader so the search engine can read state
38
46
  * without knowing about the persistence layer.
39
47
  */
40
- export declare class OssScout implements ScoutStateReader {
48
+ export declare class OssScout implements ScoutStateReader, ScoutStateWriter {
41
49
  private githubToken;
42
50
  private gistStore;
43
51
  private state;
44
52
  private dirty;
45
- constructor(githubToken: string, initialState: ScoutState, gistStore?: GistStateStore | null);
53
+ /** When true, checkpoint() also writes ~/.oss-scout/state.json. */
54
+ private persistLocal;
55
+ constructor(githubToken: string, initialState: ScoutState, gistStore?: GistStateStore | null, opts?: {
56
+ persistLocal?: boolean;
57
+ });
58
+ /**
59
+ * Drop stale disk-cache entries. Called at the top of every cache-burning
60
+ * entry point (search, features, vetList); without it ~/.oss-scout/cache
61
+ * grows without bound. evictStale never throws (fs errors degrade to warn).
62
+ */
63
+ private evictStaleCacheEntries;
46
64
  /**
47
65
  * Multi-strategy issue search. Returns scored, sorted candidates.
48
66
  * Automatically culls expired skip entries and filters skipped issues.
49
67
  */
50
68
  search(options?: SearchOptions): Promise<SearchResult>;
69
+ /**
70
+ * Populate the `hasActiveMaintainers` repo-score signal from a freshly
71
+ * computed projectHealth (#167). It was initialized false and never set, so
72
+ * calculateScore's +1 active-maintainers weight was inert; `isActive`
73
+ * (recent commit activity) is a real, already-computed proxy.
74
+ *
75
+ * `isResponsive` and `avgResponseDays` are deliberately NOT set here, and
76
+ * `isResponsive` no longer carries a score weight at all (#167): real
77
+ * responsiveness needs an actual response-time measurement (extra API calls)
78
+ * that is out of scope, and `hasActiveMaintainers` already covers the
79
+ * activity proxy. `hasHostileComments` stays a host-settable capability (it
80
+ * needs comment sentiment, out of scope). A failed health check is skipped so
81
+ * its neutral-default fields don't pollute the score.
82
+ */
83
+ private updateRepoSignalsFromHealth;
51
84
  /**
52
85
  * Vet a single issue URL for claimability.
53
86
  */
@@ -83,15 +116,21 @@ export declare class OssScout implements ScoutStateReader {
83
116
  getReposWithOpenPRs(): string[];
84
117
  getStarredRepos(): string[];
85
118
  getProjectCategories(): ProjectCategory[];
119
+ /** Configured GitHub username (used to classify own vs competing PRs, #166). */
120
+ getGitHubUsername(): string;
86
121
  getRepoScore(repo: string): number | null;
87
122
  /**
88
- * Optional SLM pre-triage config read from preferences (oss-autopilot#1122).
89
- * Empty `model` disables the call; the vetter treats it as a no-op.
123
+ * Number of the user's PRs closed without merge in this repo (#125).
124
+ * Prefers the tracked repo score; falls back to counting closedPRs so the
125
+ * scoring penalty works even before a score record exists.
90
126
  */
91
- getSLMTriageConfig(): {
92
- model: string;
93
- host: string;
94
- };
127
+ getClosedWithoutMergeCount(repo: string): number;
128
+ /**
129
+ * SLM pre-triage config read from preferences (oss-autopilot#1122). Returns
130
+ * `null` when no `slmTriageModel` is configured — the vetter skips the SLM
131
+ * call entirely (#158).
132
+ */
133
+ getSLMTriageConfig(): SLMConfig | null;
95
134
  /** Get current preferences (read-only). */
96
135
  getPreferences(): Readonly<ScoutPreferences>;
97
136
  /** Get repo score record for a specific repository. */
@@ -110,6 +149,17 @@ export declare class OssScout implements ScoutStateReader {
110
149
  * Open PRs signal active engagement even when nothing is merged yet.
111
150
  */
112
151
  recordOpenPR(pr: OpenPRRecord): void;
152
+ /**
153
+ * Reconcile tracked open PRs against their current GitHub state (#164).
154
+ *
155
+ * `state.openPRs` was append-only — nothing transitioned an open PR to
156
+ * merged/closed, so getReposWithOpenPRs() over-reported forever once a PR
157
+ * merged. This checks each open PR, records merges/closures (which updates
158
+ * the repo score), prunes resolved entries, and checkpoints. Cheaper than a
159
+ * full bootstrap, so a host can call it on daily startup. Transient errors
160
+ * leave the entry in place; auth/rate-limit failures propagate.
161
+ */
162
+ syncOpenPRs(): Promise<SyncResult>;
113
163
  /**
114
164
  * Update repo score with observed signals.
115
165
  */
@@ -138,6 +188,12 @@ export declare class OssScout implements ScoutStateReader {
138
188
  * Clear all saved results.
139
189
  */
140
190
  clearResults(): void;
191
+ /**
192
+ * Record deletion tombstones (#117) so a later gist merge does not
193
+ * resurrect these URLs from another machine's copy. A re-add with a newer
194
+ * timestamp overrides the tombstone in mergeStates.
195
+ */
196
+ private addTombstones;
141
197
  /**
142
198
  * Skip an issue — excludes it from future searches. Auto-culled after 90 days.
143
199
  */
@@ -178,9 +234,16 @@ export declare class OssScout implements ScoutStateReader {
178
234
  getState(): Readonly<ScoutState>;
179
235
  private updateRepoScoreFromPRs;
180
236
  /**
181
- * Calculate repo score (1-10) from observed data.
237
+ * Calculate repo score from observed data.
182
238
  * base 5, +1 per merged PR (max +3), -1 per closed-without-merge (max -3),
183
- * +1 responsive, +1 active maintainers, -2 hostile comments, clamped 1-10
239
+ * +1 active maintainers, -2 hostile comments, clamped to [1, 10].
240
+ *
241
+ * `isResponsive` is intentionally NOT scored (#167): nothing in oss-scout
242
+ * ever computes it, so awarding +1 for it was dead weight that the
243
+ * now-computed `hasActiveMaintainers` signal already covers as the activity
244
+ * proxy. The field is retained on RepoSignals for backward compatibility but
245
+ * no longer affects the score. With the current signals the reachable range
246
+ * is [1, 9]; the upper clamp stays as defensive hygiene.
184
247
  */
185
248
  private calculateScore;
186
249
  }