@oss-autopilot/core 1.6.2 → 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.
- package/dist/cli-registry.js +5 -1
- package/dist/cli.bundle.cjs +63 -59
- package/dist/commands/dashboard-data.d.ts +1 -0
- package/dist/commands/dashboard-server.js +23 -1
- package/dist/commands/index.d.ts +2 -2
- package/dist/commands/index.js +1 -1
- package/dist/commands/parse-list.d.ts +14 -0
- package/dist/commands/parse-list.js +145 -1
- package/dist/commands/vet-list.d.ts +1 -0
- package/dist/commands/vet-list.js +19 -2
- package/dist/core/issue-vetting.js +12 -0
- package/dist/core/repo-health.js +1 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/formatters/json.d.ts +4 -0
- package/package.json +1 -1
|
@@ -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
|
/**
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/commands/index.js
CHANGED
|
@@ -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)) {
|
|
@@ -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
|
-
|
|
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;
|
package/dist/core/repo-health.js
CHANGED
package/dist/core/types.d.ts
CHANGED
|
@@ -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 {
|