@oss-autopilot/core 1.6.3 → 1.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.
@@ -10,6 +10,7 @@ export interface DashboardStats {
10
10
  mergedPRs: number;
11
11
  closedPRs: number;
12
12
  mergeRate: string;
13
+ availableIssues?: number;
13
14
  }
14
15
  export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>, storedMergedCount?: number, storedClosedCount?: number): DashboardStats;
15
16
  /**
@@ -13,7 +13,8 @@ import { errorMessage, ValidationError } from '../core/errors.js';
13
13
  import { warn } from '../core/logger.js';
14
14
  import { validateUrl, validateGitHubUrl, PR_URL_PATTERN, ISSUE_URL_PATTERN } from './validation.js';
15
15
  import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, storedToMergedPRs, storedToClosedPRs, } from './dashboard-data.js';
16
- import { openInBrowser } from './startup.js';
16
+ import { openInBrowser, detectIssueList } from './startup.js';
17
+ import { parseIssueList } from './parse-list.js';
17
18
  import { writeDashboardServerInfo, removeDashboardServerInfo } from './dashboard-process.js';
18
19
  import { RateLimiter } from './rate-limiter.js';
19
20
  import { isBelowMinStars, } from '../core/types.js';
@@ -34,6 +35,22 @@ const MIME_TYPES = {
34
35
  '.ico': 'image/x-icon',
35
36
  };
36
37
  // ── Helpers ────────────────────────────────────────────────────────────────────
38
+ /**
39
+ * Read and parse the vetted issue list file (non-fatal on failure).
40
+ */
41
+ function readVettedIssues() {
42
+ try {
43
+ const info = detectIssueList();
44
+ if (!info)
45
+ return null;
46
+ const content = fs.readFileSync(info.path, 'utf-8');
47
+ return parseIssueList(content);
48
+ }
49
+ catch (error) {
50
+ warn(MODULE, `Failed to read vetted issue list: ${errorMessage(error)}`);
51
+ return null;
52
+ }
53
+ }
37
54
  /**
38
55
  * Build the JSON payload that the SPA expects from GET /api/data.
39
56
  */
@@ -60,6 +77,10 @@ function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClo
60
77
  repoMetadata[repo] = { stars: score.stargazersCount, language: score.language };
61
78
  }
62
79
  }
80
+ const vettedIssues = readVettedIssues();
81
+ if (vettedIssues) {
82
+ stats.availableIssues = vettedIssues.availableCount;
83
+ }
63
84
  return {
64
85
  stats,
65
86
  prsByRepo,
@@ -77,6 +98,7 @@ function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClo
77
98
  allMergedPRs: filteredMergedPRs,
78
99
  allClosedPRs: filteredClosedPRs,
79
100
  repoMetadata,
101
+ vettedIssues,
80
102
  };
81
103
  }
82
104
  /**
@@ -56,7 +56,7 @@ export { runSetup } from './setup.js';
56
56
  /** Check whether setup has been completed. */
57
57
  export { runCheckSetup } from './setup.js';
58
58
  /** Parse a curated markdown issue list file into structured issue items. */
59
- export { runParseList } from './parse-list.js';
59
+ export { runParseList, pruneIssueList } from './parse-list.js';
60
60
  /** Check if new files are properly referenced/integrated. */
61
61
  export { runCheckIntegration } from './check-integration.js';
62
62
  /** Detect formatters/linters configured in a local repository (#703). */
@@ -65,7 +65,7 @@ export { runDetectFormatters } from './detect-formatters.js';
65
65
  export { runLocalRepos } from './local-repos.js';
66
66
  export type { DailyOutput, SearchOutput, StartupOutput, StatusOutput, TrackOutput } from '../formatters/json.js';
67
67
  export type { VetOutput, CommentsOutput, PostOutput, ClaimOutput } from '../formatters/json.js';
68
- export type { ConfigOutput, DetectFormattersOutput, ParseIssueListOutput, CheckIntegrationOutput, LocalReposOutput, } from '../formatters/json.js';
68
+ export type { ConfigOutput, DetectFormattersOutput, ParseIssueListOutput, ParsedIssueItem, CheckIntegrationOutput, LocalReposOutput, } from '../formatters/json.js';
69
69
  export type { ReadOutput } from './read.js';
70
70
  export type { ShelveOutput, UnshelveOutput } from './shelve.js';
71
71
  export type { MoveOutput, MoveTarget } from './move.js';
@@ -61,7 +61,7 @@ export { runSetup } from './setup.js';
61
61
  export { runCheckSetup } from './setup.js';
62
62
  // ── Utilities ───────────────────────────────────────────────────────────────
63
63
  /** Parse a curated markdown issue list file into structured issue items. */
64
- export { runParseList } from './parse-list.js';
64
+ export { runParseList, pruneIssueList } from './parse-list.js';
65
65
  /** Check if new files are properly referenced/integrated. */
66
66
  export { runCheckIntegration } from './check-integration.js';
67
67
  /** Detect formatters/linters configured in a local repository (#703). */
@@ -9,4 +9,18 @@ interface ParseListOptions {
9
9
  export type { ParseIssueListOutput, ParsedIssueItem };
10
10
  /** Parse a markdown string into structured issue items */
11
11
  export declare function parseIssueList(content: string): ParseIssueListOutput;
12
+ /**
13
+ * Prune a markdown issue list: remove completed, skipped, and low-score items.
14
+ * Rewrites the file in place, removing:
15
+ * - Items with strikethrough, [x], Done, Skip, or Dropped status
16
+ * - Items with score < minScore
17
+ * - Empty section headings left after removal
18
+ * - Sub-bullets belonging to removed items
19
+ *
20
+ * @returns Count of items removed
21
+ */
22
+ export declare function pruneIssueList(content: string, minScore?: number): {
23
+ pruned: string;
24
+ removedCount: number;
25
+ };
12
26
  export declare function runParseList(options: ParseListOptions): Promise<ParseIssueListOutput>;
@@ -48,17 +48,34 @@ function isCompleted(line) {
48
48
  return true;
49
49
  return false;
50
50
  }
51
+ /** Extract a score from a sub-bullet line (e.g., "Score 8/10" or "Score 7.5/10") */
52
+ function extractScore(line) {
53
+ const match = line.match(/Score\s+(\d+(?:\.\d+)?)\/10/i);
54
+ return match ? parseFloat(match[1]) : undefined;
55
+ }
56
+ /** Check if a sub-bullet indicates the item is terminal (completed/abandoned — safe to prune) */
57
+ function isSubBulletTerminal(line) {
58
+ return /\*\*(Skip|Done|Dropped|Merged|Closed)\*\*/i.test(line);
59
+ }
60
+ /** Check if a sub-bullet indicates the item is in-progress (not available, but NOT safe to prune) */
61
+ function isSubBulletInProgress(line) {
62
+ return /\*\*(In Progress|Wait|Waiting)\*\*/i.test(line);
63
+ }
51
64
  /** Parse a markdown string into structured issue items */
52
65
  export function parseIssueList(content) {
53
66
  const lines = content.split('\n');
54
67
  const available = [];
55
68
  const completed = [];
56
69
  let currentTier = 'Uncategorized';
70
+ let lastItem = null;
71
+ // Track which array the last item was placed in so sub-bullets can move it
72
+ let lastItemInAvailable = false;
57
73
  for (const line of lines) {
58
74
  // Check for section headings (# or ##)
59
75
  const headingMatch = line.match(/^#{1,3}\s+(.+)/);
60
76
  if (headingMatch) {
61
77
  currentTier = headingMatch[1].trim();
78
+ lastItem = null;
62
79
  continue;
63
80
  }
64
81
  // Skip empty lines and non-list items
@@ -67,8 +84,26 @@ export function parseIssueList(content) {
67
84
  }
68
85
  // Extract GitHub URL -- skip lines without one
69
86
  const ghUrl = extractGitHubUrl(line);
70
- if (!ghUrl)
87
+ if (!ghUrl) {
88
+ // No URL — check if this is a sub-bullet for the previous item
89
+ if (lastItem && /^\s{2,}/.test(line)) {
90
+ const score = extractScore(line);
91
+ if (score !== undefined) {
92
+ lastItem.score = score;
93
+ }
94
+ // Check if sub-bullet marks item as terminal or in-progress
95
+ if (lastItemInAvailable && (isSubBulletTerminal(line) || isSubBulletInProgress(line))) {
96
+ // Move from available to completed
97
+ const idx = available.indexOf(lastItem);
98
+ if (idx !== -1) {
99
+ available.splice(idx, 1);
100
+ completed.push(lastItem);
101
+ lastItemInAvailable = false;
102
+ }
103
+ }
104
+ }
71
105
  continue;
106
+ }
72
107
  const title = extractTitle(line);
73
108
  const item = {
74
109
  repo: ghUrl.repo,
@@ -79,10 +114,13 @@ export function parseIssueList(content) {
79
114
  };
80
115
  if (isCompleted(line)) {
81
116
  completed.push(item);
117
+ lastItemInAvailable = false;
82
118
  }
83
119
  else {
84
120
  available.push(item);
121
+ lastItemInAvailable = true;
85
122
  }
123
+ lastItem = item;
86
124
  }
87
125
  return {
88
126
  available,
@@ -91,6 +129,112 @@ export function parseIssueList(content) {
91
129
  completedCount: completed.length,
92
130
  };
93
131
  }
132
+ /**
133
+ * Prune a markdown issue list: remove completed, skipped, and low-score items.
134
+ * Rewrites the file in place, removing:
135
+ * - Items with strikethrough, [x], Done, Skip, or Dropped status
136
+ * - Items with score < minScore
137
+ * - Empty section headings left after removal
138
+ * - Sub-bullets belonging to removed items
139
+ *
140
+ * @returns Count of items removed
141
+ */
142
+ export function pruneIssueList(content, minScore = 6) {
143
+ const lines = content.split('\n');
144
+ const output = [];
145
+ let removedCount = 0;
146
+ let skipSubBullets = false;
147
+ for (let i = 0; i < lines.length; i++) {
148
+ const line = lines[i];
149
+ // Always keep headings (we'll clean empty ones in a second pass)
150
+ if (/^#{1,3}\s+/.test(line)) {
151
+ skipSubBullets = false;
152
+ output.push(line);
153
+ continue;
154
+ }
155
+ // Sub-bullet of a removed item — skip it
156
+ if (skipSubBullets && /^\s{2,}/.test(line)) {
157
+ continue;
158
+ }
159
+ skipSubBullets = false;
160
+ // Check if this is a list item
161
+ const isList = /^\s*[-*+]\s/.test(line);
162
+ // Remove any completed list item (even without a GitHub URL)
163
+ if (isList && isCompleted(line)) {
164
+ removedCount++;
165
+ skipSubBullets = true;
166
+ continue;
167
+ }
168
+ const ghUrl = extractGitHubUrl(line);
169
+ if (ghUrl) {
170
+ // Look ahead at sub-bullets for terminal status or low score
171
+ let shouldRemove = false;
172
+ let j = i + 1;
173
+ while (j < lines.length && /^\s{2,}/.test(lines[j])) {
174
+ if (isSubBulletTerminal(lines[j])) {
175
+ shouldRemove = true;
176
+ }
177
+ const score = extractScore(lines[j]);
178
+ if (score !== undefined && score < minScore) {
179
+ shouldRemove = true;
180
+ }
181
+ j++;
182
+ }
183
+ if (shouldRemove) {
184
+ removedCount++;
185
+ skipSubBullets = true;
186
+ continue;
187
+ }
188
+ }
189
+ // Skip cruft lines: "Skip (N issues)", standalone "---", old "Removed" labels
190
+ if (/^\s*skip\s*\(\d+\s*issues?\)/i.test(line.replace(/[#*]/g, '').trim())) {
191
+ continue;
192
+ }
193
+ if (/^---\s*$/.test(line)) {
194
+ continue;
195
+ }
196
+ if (/^\s*(###?\s*)?(Removed|Previously dropped)/i.test(line)) {
197
+ continue;
198
+ }
199
+ // Skip blockquote metadata lines ("> Sources: ...", "> Prioritized ...")
200
+ if (/^>\s/.test(line)) {
201
+ continue;
202
+ }
203
+ // Non-list lines, metadata, and surviving items — keep
204
+ output.push(line);
205
+ }
206
+ // Second pass: remove empty section headings (heading followed by another heading or end of file)
207
+ const cleaned = [];
208
+ for (let i = 0; i < output.length; i++) {
209
+ const isHeading = /^#{1,3}\s+/.test(output[i]);
210
+ if (isHeading) {
211
+ // Check if next non-empty line is another heading or end of content
212
+ let nextContentIdx = i + 1;
213
+ while (nextContentIdx < output.length && output[nextContentIdx].trim() === '') {
214
+ nextContentIdx++;
215
+ }
216
+ const nextIsHeadingOrEnd = nextContentIdx >= output.length || /^#{1,3}\s+/.test(output[nextContentIdx]);
217
+ if (nextIsHeadingOrEnd) {
218
+ // Skip this empty heading and any blank lines after it
219
+ continue;
220
+ }
221
+ }
222
+ cleaned.push(output[i]);
223
+ }
224
+ // Collapse consecutive blank lines into at most one
225
+ const collapsed = [];
226
+ for (const line of cleaned) {
227
+ if (line.trim() === '' && collapsed.length > 0 && collapsed[collapsed.length - 1].trim() === '') {
228
+ continue;
229
+ }
230
+ collapsed.push(line);
231
+ }
232
+ // Remove trailing blank lines
233
+ while (collapsed.length > 0 && collapsed[collapsed.length - 1].trim() === '') {
234
+ collapsed.pop();
235
+ }
236
+ return { pruned: collapsed.join('\n') + '\n', removedCount };
237
+ }
94
238
  export async function runParseList(options) {
95
239
  const filePath = path.resolve(options.filePath);
96
240
  if (!fs.existsSync(filePath)) {
@@ -6,6 +6,7 @@ import { type VetListOutput, type VetOutput, type VetListItemStatus } from '../f
6
6
  interface VetListOptions {
7
7
  issueListPath?: string;
8
8
  concurrency?: number;
9
+ prune?: boolean;
9
10
  }
10
11
  /**
11
12
  * Determine the list status from vetting results.
@@ -2,7 +2,9 @@
2
2
  * Vet-list command (#764)
3
3
  * Re-vets all available issues in a curated issue list file.
4
4
  */
5
+ import * as fs from 'fs';
5
6
  import { IssueDiscovery, requireGitHubToken } from '../core/index.js';
7
+ import { runParseList, pruneIssueList } from './parse-list.js';
6
8
  import { detectIssueList } from './startup.js';
7
9
  /**
8
10
  * Determine the list status from vetting results.
@@ -43,7 +45,6 @@ export async function runVetList(options = {}) {
43
45
  }
44
46
  issueListPath = detected.path;
45
47
  }
46
- const { runParseList } = await import('./parse-list.js');
47
48
  const parsed = await runParseList({ filePath: issueListPath });
48
49
  if (parsed.available.length === 0) {
49
50
  return {
@@ -108,5 +109,21 @@ export async function runVetList(options = {}) {
108
109
  hasPR: results.filter((r) => r.listStatus === 'has_pr').length,
109
110
  errors: results.filter((r) => r.listStatus === 'error').length,
110
111
  };
111
- return { results, summary };
112
+ // 4. Prune the file if requested — remove completed/skipped/low-score items
113
+ let pruneResult;
114
+ if (options.prune && issueListPath) {
115
+ try {
116
+ const content = fs.readFileSync(issueListPath, 'utf-8');
117
+ const { pruned, removedCount } = pruneIssueList(content);
118
+ if (pruned !== content) {
119
+ fs.writeFileSync(issueListPath, pruned, 'utf-8');
120
+ }
121
+ pruneResult = { removedCount };
122
+ }
123
+ catch (error) {
124
+ const msg = error instanceof Error ? error.message : String(error);
125
+ console.error(`Warning: Failed to prune ${issueListPath}: ${msg}`);
126
+ }
127
+ }
128
+ return { results, summary, pruneResult };
112
129
  }
@@ -230,6 +230,18 @@ export class IssueVetter {
230
230
  viabilityScore,
231
231
  searchPriority,
232
232
  };
233
+ // Persist repo metadata (stars, language) so the dashboard can display it (#839)
234
+ if (!projectHealth.checkFailed) {
235
+ try {
236
+ this.stateManager.updateRepoScore(repoFullName, {
237
+ stargazersCount: projectHealth.stargazersCount,
238
+ language: projectHealth.language,
239
+ });
240
+ }
241
+ catch (error) {
242
+ warn(MODULE, `Failed to persist repo metadata for ${repoFullName}: ${errorMessage(error)}`);
243
+ }
244
+ }
233
245
  // Cache the vetting result to avoid redundant API calls on repeated searches
234
246
  cache.set(cacheKey, '', result);
235
247
  return result;
@@ -83,6 +83,7 @@ export async function checkProjectHealth(octokit, owner, repo) {
83
83
  isActive: daysSinceLastCommit < 30,
84
84
  stargazersCount: repoData.stargazers_count,
85
85
  forksCount: repoData.forks_count,
86
+ language: repoData.language,
86
87
  };
87
88
  });
88
89
  }
@@ -159,6 +159,8 @@ export interface ProjectHealth {
159
159
  stargazersCount?: number;
160
160
  /** GitHub fork count, used for repo quality scoring (#98). */
161
161
  forksCount?: number;
162
+ /** Primary programming language as reported by GitHub. */
163
+ language?: string | null;
162
164
  /** True if the health check itself failed (e.g., API error). */
163
165
  checkFailed?: boolean;
164
166
  failureReason?: string;
@@ -255,6 +255,7 @@ export interface ParsedIssueItem {
255
255
  title: string;
256
256
  tier: string;
257
257
  url: string;
258
+ score?: number;
258
259
  }
259
260
  /** Output of the parse-issue-list command (#82) */
260
261
  export interface ParseIssueListOutput {
@@ -291,6 +292,9 @@ export interface VetListOutput {
291
292
  hasPR: number;
292
293
  errors: number;
293
294
  };
295
+ pruneResult?: {
296
+ removedCount: number;
297
+ };
294
298
  }
295
299
  /** Output of the vet command */
296
300
  export interface VetOutput {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "1.6.3",
3
+ "version": "1.7.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {