@oss-autopilot/core 1.14.0 → 1.15.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-registry.js +2 -1
- package/dist/cli.bundle.cjs +58 -58
- package/dist/commands/daily.js +16 -25
- package/dist/commands/dashboard-data.d.ts +38 -1
- package/dist/commands/dashboard-data.js +33 -62
- package/dist/commands/dashboard-server.d.ts +10 -0
- package/dist/commands/dashboard-server.js +10 -2
- package/dist/commands/scout-bridge.js +7 -0
- package/dist/commands/vet-list.js +10 -0
- package/dist/commands/vet.js +8 -0
- package/dist/core/anti-llm-policy.d.ts +32 -0
- package/dist/core/anti-llm-policy.js +101 -0
- package/dist/core/errors.d.ts +24 -0
- package/dist/core/errors.js +28 -0
- package/dist/core/github-stats.js +38 -71
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +3 -1
- package/dist/core/issue-grading.d.ts +75 -0
- package/dist/core/issue-grading.js +146 -0
- package/dist/core/linked-pr-classification.d.ts +30 -0
- package/dist/core/linked-pr-classification.js +53 -0
- package/dist/formatters/json.d.ts +5 -0
- package/package.json +2 -2
package/dist/commands/daily.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
9
|
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
|
|
10
|
-
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
10
|
+
import { errorMessage, isRateLimitOrAuthError, nonFatalCatch } from '../core/errors.js';
|
|
11
11
|
import { warn } from '../core/logger.js';
|
|
12
12
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
13
13
|
import { createAutopilotScout } from './scout-bridge.js';
|
|
@@ -60,30 +60,21 @@ async function fetchPRData(prMonitor, token) {
|
|
|
60
60
|
// All stats fetches are non-critical (cosmetic/scoring), so isolate their failure
|
|
61
61
|
const issueMonitor = new IssueConversationMonitor(token);
|
|
62
62
|
const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all([
|
|
63
|
-
prMonitor.fetchUserMergedPRCounts(starFilter).catch((
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
warn(MODULE, `Failed to fetch recently closed PRs: ${errorMessage(err)}`);
|
|
79
|
-
return [];
|
|
80
|
-
}),
|
|
81
|
-
prMonitor.fetchRecentlyMergedPRs().catch((err) => {
|
|
82
|
-
if (isRateLimitOrAuthError(err))
|
|
83
|
-
throw err;
|
|
84
|
-
warn(MODULE, `Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
85
|
-
return [];
|
|
86
|
-
}),
|
|
63
|
+
prMonitor.fetchUserMergedPRCounts(starFilter).catch(nonFatalCatch({
|
|
64
|
+
module: MODULE,
|
|
65
|
+
label: 'fetch merged PR counts',
|
|
66
|
+
fallback: emptyPRCountsResult(),
|
|
67
|
+
})),
|
|
68
|
+
prMonitor
|
|
69
|
+
.fetchUserClosedPRCounts(starFilter)
|
|
70
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch closed PR counts', fallback: emptyPRCountsResult() })),
|
|
71
|
+
prMonitor
|
|
72
|
+
.fetchRecentlyClosedPRs()
|
|
73
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch recently closed PRs', fallback: [] })),
|
|
74
|
+
prMonitor
|
|
75
|
+
.fetchRecentlyMergedPRs()
|
|
76
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch recently merged PRs', fallback: [] })),
|
|
77
|
+
// Issue conversation fetch has custom messaging based on the error content, so it keeps its bespoke catch.
|
|
87
78
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
88
79
|
if (isRateLimitOrAuthError(error))
|
|
89
80
|
throw error;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
|
|
4
4
|
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
|
-
import { type DailyDigest, type AgentState, type ClosedPR, type MergedPR, type StoredMergedPR, type StoredClosedPR, type CommentedIssue } from '../core/types.js';
|
|
6
|
+
import { type DailyDigest, type AgentState, type ClosedPR, type MergedPR, type StoredMergedPR, type StoredClosedPR, type CommentedIssue, type CommentedIssueWithResponse, type FetchedPR, type ShelvedPRRef, type RepoMetadataEntry } from '../core/types.js';
|
|
7
|
+
import type { ParseIssueListOutput } from '../formatters/json.js';
|
|
7
8
|
export interface DashboardStats {
|
|
8
9
|
activePRs: number;
|
|
9
10
|
shelvedPRs: number;
|
|
@@ -12,6 +13,42 @@ export interface DashboardStats {
|
|
|
12
13
|
mergeRate: string;
|
|
13
14
|
availableIssues?: number;
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Shape of the JSON payload served by `GET /api/data` from the dashboard
|
|
18
|
+
* HTTP server. Single source of truth (#965) — imported by
|
|
19
|
+
* dashboard-server.ts, which previously redeclared this interface inline
|
|
20
|
+
* and so silently drifted whenever this module changed shape.
|
|
21
|
+
*/
|
|
22
|
+
export interface DashboardJsonData {
|
|
23
|
+
stats: DashboardStats;
|
|
24
|
+
prsByRepo: Record<string, {
|
|
25
|
+
active: number;
|
|
26
|
+
merged: number;
|
|
27
|
+
closed: number;
|
|
28
|
+
}>;
|
|
29
|
+
topRepos: Array<{
|
|
30
|
+
repo: string;
|
|
31
|
+
active: number;
|
|
32
|
+
merged: number;
|
|
33
|
+
closed: number;
|
|
34
|
+
}>;
|
|
35
|
+
monthlyMerged: Record<string, number>;
|
|
36
|
+
monthlyOpened: Record<string, number>;
|
|
37
|
+
monthlyClosed: Record<string, number>;
|
|
38
|
+
activePRs: FetchedPR[];
|
|
39
|
+
shelvedPRUrls: string[];
|
|
40
|
+
recentlyMergedPRs: MergedPR[];
|
|
41
|
+
recentlyClosedPRs: ClosedPR[];
|
|
42
|
+
autoUnshelvedPRs: ShelvedPRRef[];
|
|
43
|
+
commentedIssues: CommentedIssue[];
|
|
44
|
+
issueResponses: CommentedIssueWithResponse[];
|
|
45
|
+
allMergedPRs: MergedPR[];
|
|
46
|
+
allClosedPRs: ClosedPR[];
|
|
47
|
+
repoMetadata: Record<string, RepoMetadataEntry>;
|
|
48
|
+
vettedIssues?: ParseIssueListOutput | null;
|
|
49
|
+
offline?: boolean;
|
|
50
|
+
lastUpdated?: string;
|
|
51
|
+
}
|
|
15
52
|
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>, storedMergedCount?: number, storedClosedCount?: number): DashboardStats;
|
|
16
53
|
/**
|
|
17
54
|
* Merge fresh API counts into existing stored counts.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
6
|
import { getStateManager, PRMonitor, IssueConversationMonitor, getOctokit } from '../core/index.js';
|
|
7
|
-
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
7
|
+
import { errorMessage, isRateLimitOrAuthError, nonFatalCatch } from '../core/errors.js';
|
|
8
8
|
import { warn } from '../core/logger.js';
|
|
9
9
|
import { emptyPRCountsResult, fetchMergedPRsSince, fetchClosedPRsSince } from '../core/github-stats.js';
|
|
10
10
|
import { parseGitHubUrl } from '../core/utils.js';
|
|
@@ -110,30 +110,21 @@ export async function fetchDashboardData(token) {
|
|
|
110
110
|
const closedWatermark = stateManager.getClosedPRWatermark();
|
|
111
111
|
const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues, newMergedPRs, newClosedPRs,] = await Promise.all([
|
|
112
112
|
prMonitor.fetchUserOpenPRs(),
|
|
113
|
-
prMonitor
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
prMonitor.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
warn(MODULE, `Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
129
|
-
return emptyPRCountsResult();
|
|
130
|
-
}),
|
|
131
|
-
prMonitor.fetchUserClosedPRCounts(starFilter).catch((err) => {
|
|
132
|
-
if (isRateLimitOrAuthError(err))
|
|
133
|
-
throw err;
|
|
134
|
-
warn(MODULE, `Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
135
|
-
return emptyPRCountsResult();
|
|
136
|
-
}),
|
|
113
|
+
prMonitor
|
|
114
|
+
.fetchRecentlyClosedPRs()
|
|
115
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch recently closed PRs', fallback: [] })),
|
|
116
|
+
prMonitor
|
|
117
|
+
.fetchRecentlyMergedPRs()
|
|
118
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch recently merged PRs', fallback: [] })),
|
|
119
|
+
prMonitor.fetchUserMergedPRCounts(starFilter).catch(nonFatalCatch({
|
|
120
|
+
module: MODULE,
|
|
121
|
+
label: 'fetch merged PR counts',
|
|
122
|
+
fallback: emptyPRCountsResult(),
|
|
123
|
+
})),
|
|
124
|
+
prMonitor
|
|
125
|
+
.fetchUserClosedPRCounts(starFilter)
|
|
126
|
+
.catch(nonFatalCatch({ module: MODULE, label: 'fetch closed PR counts', fallback: emptyPRCountsResult() })),
|
|
127
|
+
// Issue conversation fetch has custom messaging based on the error content, so it keeps its bespoke catch.
|
|
137
128
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
138
129
|
if (isRateLimitOrAuthError(error))
|
|
139
130
|
throw error;
|
|
@@ -149,18 +140,8 @@ export async function fetchDashboardData(token) {
|
|
|
149
140
|
failures: [{ issueUrl: 'N/A', error: `Issue conversation fetch failed: ${msg}` }],
|
|
150
141
|
};
|
|
151
142
|
}),
|
|
152
|
-
fetchMergedPRsSince(octokit, config, watermark).catch((
|
|
153
|
-
|
|
154
|
-
throw err;
|
|
155
|
-
warn(MODULE, `Failed to fetch merged PRs for storage: ${errorMessage(err)}`);
|
|
156
|
-
return [];
|
|
157
|
-
}),
|
|
158
|
-
fetchClosedPRsSince(octokit, config, closedWatermark).catch((err) => {
|
|
159
|
-
if (isRateLimitOrAuthError(err))
|
|
160
|
-
throw err;
|
|
161
|
-
warn(MODULE, `Failed to fetch closed PRs for storage: ${errorMessage(err)}`);
|
|
162
|
-
return [];
|
|
163
|
-
}),
|
|
143
|
+
fetchMergedPRsSince(octokit, config, watermark).catch(nonFatalCatch({ module: MODULE, label: 'fetch merged PRs for storage', fallback: [] })),
|
|
144
|
+
fetchClosedPRsSince(octokit, config, closedWatermark).catch(nonFatalCatch({ module: MODULE, label: 'fetch closed PRs for storage', fallback: [] })),
|
|
164
145
|
]);
|
|
165
146
|
const commentedIssues = fetchedIssues.issues;
|
|
166
147
|
if (fetchedIssues.failures.length > 0) {
|
|
@@ -216,10 +197,12 @@ export async function fetchDashboardData(token) {
|
|
|
216
197
|
return { digest, commentedIssues, allMergedPRs, allClosedPRs };
|
|
217
198
|
}
|
|
218
199
|
/**
|
|
219
|
-
* Convert StoredMergedPR[]
|
|
220
|
-
*
|
|
200
|
+
* Convert a stored-shape PR array (StoredMergedPR[] or StoredClosedPR[]) to
|
|
201
|
+
* its display-shape equivalent (MergedPR[] / ClosedPR[]) by deriving `repo`
|
|
202
|
+
* and `number` from the URL. Skips entries whose URL cannot be parsed.
|
|
203
|
+
* Shared by storedToMergedPRs and storedToClosedPRs (#959).
|
|
221
204
|
*/
|
|
222
|
-
|
|
205
|
+
function storedToPRs(stored, dateField, label) {
|
|
223
206
|
const results = [];
|
|
224
207
|
let skipped = 0;
|
|
225
208
|
for (const pr of stored) {
|
|
@@ -233,39 +216,27 @@ export function storedToMergedPRs(stored) {
|
|
|
233
216
|
repo: `${parsed.owner}/${parsed.repo}`,
|
|
234
217
|
number: parsed.number,
|
|
235
218
|
title: pr.title,
|
|
236
|
-
|
|
219
|
+
[dateField]: pr[dateField],
|
|
237
220
|
});
|
|
238
221
|
}
|
|
239
222
|
if (skipped > 0) {
|
|
240
|
-
warn(MODULE, `Skipped ${skipped} stored
|
|
223
|
+
warn(MODULE, `Skipped ${skipped} stored ${label} PR(s) with unparseable URLs`);
|
|
241
224
|
}
|
|
242
225
|
return results;
|
|
243
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Convert StoredMergedPR[] to MergedPR[] by deriving repo and number from URL.
|
|
229
|
+
* Skips entries with unparseable URLs.
|
|
230
|
+
*/
|
|
231
|
+
export function storedToMergedPRs(stored) {
|
|
232
|
+
return storedToPRs(stored, 'mergedAt', 'merged');
|
|
233
|
+
}
|
|
244
234
|
/**
|
|
245
235
|
* Convert StoredClosedPR[] to ClosedPR[] by deriving repo and number from URL.
|
|
246
236
|
* Skips entries with unparseable URLs.
|
|
247
237
|
*/
|
|
248
238
|
export function storedToClosedPRs(stored) {
|
|
249
|
-
|
|
250
|
-
let skipped = 0;
|
|
251
|
-
for (const pr of stored) {
|
|
252
|
-
const parsed = parseGitHubUrl(pr.url);
|
|
253
|
-
if (!parsed) {
|
|
254
|
-
skipped++;
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
results.push({
|
|
258
|
-
url: pr.url,
|
|
259
|
-
repo: `${parsed.owner}/${parsed.repo}`,
|
|
260
|
-
number: parsed.number,
|
|
261
|
-
title: pr.title,
|
|
262
|
-
closedAt: pr.closedAt,
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
if (skipped > 0) {
|
|
266
|
-
warn(MODULE, `Skipped ${skipped} stored closed PR(s) with unparseable URLs`);
|
|
267
|
-
}
|
|
268
|
-
return results;
|
|
239
|
+
return storedToPRs(stored, 'closedAt', 'closed');
|
|
269
240
|
}
|
|
270
241
|
/**
|
|
271
242
|
* Compute PRs grouped by repository from a digest and state.
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Uses Node's built-in http module — no Express/Fastify.
|
|
7
7
|
*/
|
|
8
|
+
import { type DashboardJsonData } from './dashboard-data.js';
|
|
9
|
+
import { type DailyDigest, type AgentState, type CommentedIssue, type MergedPR, type ClosedPR } from '../core/types.js';
|
|
8
10
|
export { getDashboardPidPath, writeDashboardServerInfo, readDashboardServerInfo, removeDashboardServerInfo, isDashboardServerRunning, findRunningDashboardServer, type DashboardServerInfo, } from './dashboard-process.js';
|
|
9
11
|
export interface DashboardServerOptions {
|
|
10
12
|
port: number;
|
|
@@ -12,4 +14,12 @@ export interface DashboardServerOptions {
|
|
|
12
14
|
token: string | null;
|
|
13
15
|
open: boolean;
|
|
14
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
19
|
+
*
|
|
20
|
+
* Exported for unit testing of response-shape concerns that the full
|
|
21
|
+
* handler harness can't reach (it bakes a stale cachedDigest at server
|
|
22
|
+
* start-up, so tests that need a specific digest should call this directly).
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildDashboardJson(digest: DailyDigest, state: Readonly<AgentState>, commentedIssues: CommentedIssue[], allMergedPRs?: MergedPR[], allClosedPRs?: ClosedPR[]): DashboardJsonData;
|
|
15
25
|
export declare function startDashboardServer(options: DashboardServerOptions): Promise<void>;
|
|
@@ -68,8 +68,12 @@ function getIssueListMtimeMs() {
|
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
70
|
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
71
|
+
*
|
|
72
|
+
* Exported for unit testing of response-shape concerns that the full
|
|
73
|
+
* handler harness can't reach (it bakes a stale cachedDigest at server
|
|
74
|
+
* start-up, so tests that need a specific digest should call this directly).
|
|
71
75
|
*/
|
|
72
|
-
function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClosedPRs) {
|
|
76
|
+
export function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClosedPRs) {
|
|
73
77
|
const prsByRepo = computePRsByRepo(digest, state);
|
|
74
78
|
const topRepos = computeTopRepos(prsByRepo);
|
|
75
79
|
const { monthlyMerged, monthlyOpened, monthlyClosed } = getMonthlyData(state);
|
|
@@ -104,7 +108,11 @@ function buildDashboardJson(digest, state, commentedIssues, allMergedPRs, allClo
|
|
|
104
108
|
monthlyOpened,
|
|
105
109
|
monthlyClosed,
|
|
106
110
|
activePRs: applyStatusOverrides(digest.openPRs || [], state),
|
|
107
|
-
|
|
111
|
+
// Source of truth is digest.shelvedPRs (union of explicitly-shelved URLs
|
|
112
|
+
// and dormant-non-addressing PRs auto-shelved for display). Returning
|
|
113
|
+
// only state.config.shelvedPRUrls would under-count and desync from
|
|
114
|
+
// stats.shelvedPRs, which is already derived from digest.shelvedPRs. (#981)
|
|
115
|
+
shelvedPRUrls: (digest.shelvedPRs || []).map((ref) => ref.url),
|
|
108
116
|
recentlyMergedPRs: digest.recentlyMergedPRs || [],
|
|
109
117
|
recentlyClosedPRs: digest.recentlyClosedPRs || [],
|
|
110
118
|
autoUnshelvedPRs: digest.autoUnshelvedPRs || [],
|
|
@@ -44,6 +44,13 @@ export function buildScoutState() {
|
|
|
44
44
|
title: pr.title,
|
|
45
45
|
closedAt: pr.closedAt,
|
|
46
46
|
})),
|
|
47
|
+
// Map ephemeral openPRs (regenerated each daily run) from the last digest
|
|
48
|
+
// so oss-scout's Phase 0 also searches repos with active-but-unmerged PRs.
|
|
49
|
+
openPRs: (state.lastDigest?.openPRs ?? []).map((pr) => ({
|
|
50
|
+
url: pr.url,
|
|
51
|
+
title: pr.title,
|
|
52
|
+
openedAt: pr.createdAt,
|
|
53
|
+
})),
|
|
47
54
|
savedResults: [],
|
|
48
55
|
skippedIssues: [],
|
|
49
56
|
lastRunAt: state.lastRunAt,
|
|
@@ -6,6 +6,9 @@ import * as fs from 'fs';
|
|
|
6
6
|
import { createAutopilotScout } from './scout-bridge.js';
|
|
7
7
|
import { runParseList, pruneIssueList } from './parse-list.js';
|
|
8
8
|
import { detectIssueList } from './startup.js';
|
|
9
|
+
import { computeSuccessGrade, gradeFromCandidate } from '../core/issue-grading.js';
|
|
10
|
+
import { getStateManager } from '../core/index.js';
|
|
11
|
+
const UNKNOWN_GRADE = computeSuccessGrade({ avgResponseDays: null, mergeRate: null, daysSinceLastCommit: null });
|
|
9
12
|
/**
|
|
10
13
|
* Determine the list status from vetting results.
|
|
11
14
|
* Maps vetting recommendation + reasons to a list-level status.
|
|
@@ -62,6 +65,11 @@ export async function runVetList(options = {}) {
|
|
|
62
65
|
const item = items[index++];
|
|
63
66
|
try {
|
|
64
67
|
const candidate = await scout.vetIssue(item.url);
|
|
68
|
+
const grade = gradeFromCandidate({
|
|
69
|
+
repo: candidate.issue.repo,
|
|
70
|
+
projectHealth: candidate.projectHealth,
|
|
71
|
+
getRepoScore: (repo) => getStateManager().getRepoScore(repo),
|
|
72
|
+
});
|
|
65
73
|
const vetResult = {
|
|
66
74
|
issue: {
|
|
67
75
|
repo: candidate.issue.repo,
|
|
@@ -75,6 +83,7 @@ export async function runVetList(options = {}) {
|
|
|
75
83
|
reasonsToSkip: candidate.reasonsToSkip,
|
|
76
84
|
projectHealth: candidate.projectHealth,
|
|
77
85
|
vettingResult: candidate.vettingResult,
|
|
86
|
+
grade,
|
|
78
87
|
};
|
|
79
88
|
results.push({
|
|
80
89
|
...vetResult,
|
|
@@ -90,6 +99,7 @@ export async function runVetList(options = {}) {
|
|
|
90
99
|
reasonsToSkip: [`Error: ${error instanceof Error ? error.message : String(error)}`],
|
|
91
100
|
projectHealth: {},
|
|
92
101
|
vettingResult: {},
|
|
102
|
+
grade: UNKNOWN_GRADE,
|
|
93
103
|
listStatus: 'error',
|
|
94
104
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
95
105
|
});
|
package/dist/commands/vet.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createAutopilotScout } from './scout-bridge.js';
|
|
6
6
|
import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
|
|
7
|
+
import { gradeFromCandidate } from '../core/issue-grading.js';
|
|
8
|
+
import { getStateManager } from '../core/index.js';
|
|
7
9
|
/**
|
|
8
10
|
* Vet a specific GitHub issue for claimability and project health.
|
|
9
11
|
*
|
|
@@ -17,6 +19,11 @@ export async function runVet(options) {
|
|
|
17
19
|
validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue');
|
|
18
20
|
const scout = await createAutopilotScout();
|
|
19
21
|
const candidate = await scout.vetIssue(options.issueUrl);
|
|
22
|
+
const grade = gradeFromCandidate({
|
|
23
|
+
repo: candidate.issue.repo,
|
|
24
|
+
projectHealth: candidate.projectHealth,
|
|
25
|
+
getRepoScore: (repo) => getStateManager().getRepoScore(repo),
|
|
26
|
+
});
|
|
20
27
|
return {
|
|
21
28
|
issue: {
|
|
22
29
|
repo: candidate.issue.repo,
|
|
@@ -30,5 +37,6 @@ export async function runVet(options) {
|
|
|
30
37
|
reasonsToSkip: candidate.reasonsToSkip,
|
|
31
38
|
projectHealth: candidate.projectHealth,
|
|
32
39
|
vettingResult: candidate.vettingResult,
|
|
40
|
+
grade,
|
|
33
41
|
};
|
|
34
42
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-LLM policy scan (#108, #911, #979).
|
|
3
|
+
*
|
|
4
|
+
* Scan concatenated repo docs (CONTRIBUTING.md, CODE_OF_CONDUCT.md,
|
|
5
|
+
* README) for language that indicates the project does not accept
|
|
6
|
+
* AI/LLM-generated contributions. Previously described as a keyword
|
|
7
|
+
* table in prose in agents/issue-scout.md.
|
|
8
|
+
*
|
|
9
|
+
* The long-term home for this logic is `@oss-scout/core`, where the
|
|
10
|
+
* relevant files are already fetched during vetting. Keeping it here
|
|
11
|
+
* for now lets the agent invoke it directly and gives scout a
|
|
12
|
+
* reference implementation + test fixtures to adopt. See #979.
|
|
13
|
+
*
|
|
14
|
+
* Precision matters more than recall. False positives (flagging a
|
|
15
|
+
* project that actually welcomes AI help) silently shrink the user's
|
|
16
|
+
* contribution surface without recourse. We only match on phrases
|
|
17
|
+
* that combine a rejection keyword (no / reject / will be closed /
|
|
18
|
+
* don't accept) with an AI/LLM noun.
|
|
19
|
+
*/
|
|
20
|
+
export type AntiLLMCategory = 'explicit_ban' | 'tool_ban' | 'reject_framing';
|
|
21
|
+
export interface AntiLLMMatch {
|
|
22
|
+
category: AntiLLMCategory;
|
|
23
|
+
/** The exact substring from the source text that triggered the match. */
|
|
24
|
+
phrase: string;
|
|
25
|
+
/** ~80 character window around the match, for surfacing to the user. */
|
|
26
|
+
excerpt: string;
|
|
27
|
+
}
|
|
28
|
+
export interface AntiLLMScanResult {
|
|
29
|
+
matched: boolean;
|
|
30
|
+
matches: AntiLLMMatch[];
|
|
31
|
+
}
|
|
32
|
+
export declare function scanForAntiLLMPolicy(text: string): AntiLLMScanResult;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-LLM policy scan (#108, #911, #979).
|
|
3
|
+
*
|
|
4
|
+
* Scan concatenated repo docs (CONTRIBUTING.md, CODE_OF_CONDUCT.md,
|
|
5
|
+
* README) for language that indicates the project does not accept
|
|
6
|
+
* AI/LLM-generated contributions. Previously described as a keyword
|
|
7
|
+
* table in prose in agents/issue-scout.md.
|
|
8
|
+
*
|
|
9
|
+
* The long-term home for this logic is `@oss-scout/core`, where the
|
|
10
|
+
* relevant files are already fetched during vetting. Keeping it here
|
|
11
|
+
* for now lets the agent invoke it directly and gives scout a
|
|
12
|
+
* reference implementation + test fixtures to adopt. See #979.
|
|
13
|
+
*
|
|
14
|
+
* Precision matters more than recall. False positives (flagging a
|
|
15
|
+
* project that actually welcomes AI help) silently shrink the user's
|
|
16
|
+
* contribution surface without recourse. We only match on phrases
|
|
17
|
+
* that combine a rejection keyword (no / reject / will be closed /
|
|
18
|
+
* don't accept) with an AI/LLM noun.
|
|
19
|
+
*/
|
|
20
|
+
const PATTERNS = [
|
|
21
|
+
// Explicit "no X" bans against AI/LLM nouns.
|
|
22
|
+
{ category: 'explicit_ban', regex: /\bno\s+(ai|llm)[-\s](generated|authored|written|assisted|contributions?)/i },
|
|
23
|
+
{ category: 'explicit_ban', regex: /\b(ban|banned|banning)\s+(ai|llm)\b/i },
|
|
24
|
+
// Named-tool bans. Optionally match a "-generated/-authored/-…"
|
|
25
|
+
// continuation (clear ban wording), and use a negative lookahead to
|
|
26
|
+
// reject unrelated hyphen-words like "no copilot-style autocomplete"
|
|
27
|
+
// (which describes a feature, not a contribution policy).
|
|
28
|
+
{
|
|
29
|
+
category: 'tool_ban',
|
|
30
|
+
regex: /\bno\s+(copilot|chatgpt|claude|cursor)(-(generated|authored|assisted|written))?(?![a-z-])/i,
|
|
31
|
+
},
|
|
32
|
+
{ category: 'tool_ban', regex: /\bno\s+ai\s+coding\s+tools?\b/i },
|
|
33
|
+
// Rejection framing. To avoid false positives like "AI PRs are closed
|
|
34
|
+
// to new comments" (closed means something else) or "AI suggestions
|
|
35
|
+
// from your IDE" (not a contribution), we require both an AI/LLM
|
|
36
|
+
// qualifier AND a contribution noun AND a rejection verb phrase. The
|
|
37
|
+
// two patterns cover "AI-generated code will be closed" (with
|
|
38
|
+
// participle) and "AI contributions will be closed" (without).
|
|
39
|
+
{
|
|
40
|
+
category: 'reject_framing',
|
|
41
|
+
regex: /\b(ai|llm)[-\s](generated|assisted|authored|written)\s+(code|prs?|contributions?)\s+(will\s+be\s+(closed|rejected)|are\s+rejected)/i,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
category: 'reject_framing',
|
|
45
|
+
regex: /\b(ai|llm)\s+(code|prs?|contributions?)\s+(will\s+be\s+(closed|rejected)|are\s+rejected)/i,
|
|
46
|
+
},
|
|
47
|
+
// "do/does not accept AI-{noun}" / "reject AI contributions" — both
|
|
48
|
+
// require a contribution noun to avoid matching "accept AI suggestions
|
|
49
|
+
// from your IDE" or similar incidental mentions.
|
|
50
|
+
{
|
|
51
|
+
category: 'reject_framing',
|
|
52
|
+
regex: /\b(do|does)(\s+not|n't)\s+accept\s+(ai|llm)[-\s](generated|assisted|authored|written|contributions?|code|prs?)\b/i,
|
|
53
|
+
},
|
|
54
|
+
{ category: 'reject_framing', regex: /\breject\s+(ai|llm)\s+contributions?\b/i },
|
|
55
|
+
];
|
|
56
|
+
const EXCERPT_RADIUS = 40;
|
|
57
|
+
function makeExcerpt(text, matchIndex, matchLength) {
|
|
58
|
+
const start = Math.max(0, matchIndex - EXCERPT_RADIUS);
|
|
59
|
+
const end = Math.min(text.length, matchIndex + matchLength + EXCERPT_RADIUS);
|
|
60
|
+
const prefix = start > 0 ? '…' : '';
|
|
61
|
+
const suffix = end < text.length ? '…' : '';
|
|
62
|
+
return `${prefix}${text.slice(start, end).replace(/\s+/g, ' ').trim()}${suffix}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Normalize exotic whitespace and hyphens so patterns written with plain
|
|
66
|
+
* ASCII still match real-world markdown. Covers non-breaking space,
|
|
67
|
+
* non-breaking hyphen, en dash, em dash, and figure dash — all of which
|
|
68
|
+
* show up in CONTRIBUTING files authored in rich-text editors.
|
|
69
|
+
*/
|
|
70
|
+
function normalizeText(text) {
|
|
71
|
+
return text
|
|
72
|
+
.normalize('NFKC')
|
|
73
|
+
.replace(/[\u00A0]/g, ' ')
|
|
74
|
+
.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015]/g, '-');
|
|
75
|
+
}
|
|
76
|
+
export function scanForAntiLLMPolicy(text) {
|
|
77
|
+
if (typeof text !== 'string') {
|
|
78
|
+
throw new TypeError(`scanForAntiLLMPolicy: expected string, received ${typeof text}`);
|
|
79
|
+
}
|
|
80
|
+
if (text === '')
|
|
81
|
+
return { matched: false, matches: [] };
|
|
82
|
+
const normalized = normalizeText(text);
|
|
83
|
+
const seenLabels = new Set();
|
|
84
|
+
const matches = [];
|
|
85
|
+
for (const pattern of PATTERNS) {
|
|
86
|
+
const hit = normalized.match(pattern.regex);
|
|
87
|
+
if (!hit || hit.index === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
const phrase = hit[0];
|
|
90
|
+
const key = `${pattern.category}:${phrase.toLowerCase()}`;
|
|
91
|
+
if (seenLabels.has(key))
|
|
92
|
+
continue;
|
|
93
|
+
seenLabels.add(key);
|
|
94
|
+
matches.push({
|
|
95
|
+
category: pattern.category,
|
|
96
|
+
phrase,
|
|
97
|
+
excerpt: makeExcerpt(normalized, hit.index, phrase.length),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return { matched: matches.length > 0, matches };
|
|
101
|
+
}
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -45,6 +45,30 @@ export declare function getHttpStatusCode(error: unknown): number | undefined;
|
|
|
45
45
|
export declare function isRateLimitError(error: unknown): boolean;
|
|
46
46
|
/** Return true for errors that should propagate (not degrade gracefully): rate limits, auth failures, abuse detection. */
|
|
47
47
|
export declare function isRateLimitOrAuthError(err: unknown): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Build a `.catch()` handler for the "non-fatal parallel fetch" pattern used
|
|
50
|
+
* by daily.ts and dashboard-data.ts (#960). When a sibling fetch fails during
|
|
51
|
+
* a bulk-parallel orchestration, we want:
|
|
52
|
+
*
|
|
53
|
+
* - rate-limit / auth errors to propagate (those abort the whole run — the
|
|
54
|
+
* user needs to see them), and
|
|
55
|
+
* - every other error to log a warning and fall back to a safe default so
|
|
56
|
+
* the other siblings can still succeed.
|
|
57
|
+
*
|
|
58
|
+
* Inline at each call site, this is ~4 lines of boilerplate repeated 10+
|
|
59
|
+
* times. Consolidated here so the rate-limit-rethrow rule lives in exactly
|
|
60
|
+
* one place.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* prMonitor.fetchRecentlyClosedPRs().catch(
|
|
64
|
+
* nonFatalCatch({ module: MODULE, label: 'fetch recently closed PRs', fallback: [] as ClosedPR[] })
|
|
65
|
+
* );
|
|
66
|
+
*/
|
|
67
|
+
export declare function nonFatalCatch<T>(params: {
|
|
68
|
+
module: string;
|
|
69
|
+
label: string;
|
|
70
|
+
fallback: T;
|
|
71
|
+
}): (err: unknown) => T;
|
|
48
72
|
/**
|
|
49
73
|
* Map an unknown error to a structured ErrorCode for JSON output.
|
|
50
74
|
* Checks custom error classes, HTTP status codes (Octokit errors),
|
package/dist/core/errors.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* propagate to the caller via isRateLimitError/isRateLimitOrAuthError.
|
|
8
8
|
* Other errors degrade gracefully — modules return partial results and log warnings.
|
|
9
9
|
*/
|
|
10
|
+
import { warn } from './logger.js';
|
|
10
11
|
/**
|
|
11
12
|
* Base error for all oss-autopilot errors.
|
|
12
13
|
*/
|
|
@@ -87,6 +88,33 @@ export function isRateLimitOrAuthError(err) {
|
|
|
87
88
|
}
|
|
88
89
|
return false;
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Build a `.catch()` handler for the "non-fatal parallel fetch" pattern used
|
|
93
|
+
* by daily.ts and dashboard-data.ts (#960). When a sibling fetch fails during
|
|
94
|
+
* a bulk-parallel orchestration, we want:
|
|
95
|
+
*
|
|
96
|
+
* - rate-limit / auth errors to propagate (those abort the whole run — the
|
|
97
|
+
* user needs to see them), and
|
|
98
|
+
* - every other error to log a warning and fall back to a safe default so
|
|
99
|
+
* the other siblings can still succeed.
|
|
100
|
+
*
|
|
101
|
+
* Inline at each call site, this is ~4 lines of boilerplate repeated 10+
|
|
102
|
+
* times. Consolidated here so the rate-limit-rethrow rule lives in exactly
|
|
103
|
+
* one place.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* prMonitor.fetchRecentlyClosedPRs().catch(
|
|
107
|
+
* nonFatalCatch({ module: MODULE, label: 'fetch recently closed PRs', fallback: [] as ClosedPR[] })
|
|
108
|
+
* );
|
|
109
|
+
*/
|
|
110
|
+
export function nonFatalCatch(params) {
|
|
111
|
+
return (err) => {
|
|
112
|
+
if (isRateLimitOrAuthError(err))
|
|
113
|
+
throw err;
|
|
114
|
+
warn(params.module, `Failed to ${params.label}: ${errorMessage(err)}`);
|
|
115
|
+
return params.fallback;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
90
118
|
/**
|
|
91
119
|
* Map an unknown error to a structured ErrorCode for JSON output.
|
|
92
120
|
* Checks custom error classes, HTTP status codes (Octokit errors),
|