@oss-autopilot/core 3.5.0 → 3.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 +143 -1
- package/dist/cli.bundle.cjs +120 -108
- package/dist/commands/daily.d.ts +8 -0
- package/dist/commands/daily.js +21 -0
- package/dist/commands/dashboard-lifecycle.d.ts +7 -0
- package/dist/commands/dashboard-lifecycle.js +12 -2
- package/dist/commands/dashboard-process.d.ts +8 -0
- package/dist/commands/dashboard-process.js +20 -0
- package/dist/commands/features.d.ts +50 -0
- package/dist/commands/features.js +131 -0
- package/dist/commands/index.d.ts +5 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/scout-bridge.d.ts +12 -0
- package/dist/commands/scout-bridge.js +42 -2
- package/dist/commands/search.js +3 -1
- package/dist/commands/startup.js +75 -7
- package/dist/commands/vet-list.js +21 -5
- package/dist/commands/vet.js +3 -1
- package/dist/core/anti-llm-policy.d.ts +42 -13
- package/dist/core/anti-llm-policy.js +102 -13
- package/dist/core/ci-analysis.d.ts +32 -1
- package/dist/core/ci-analysis.js +92 -0
- package/dist/core/errors.d.ts +19 -0
- package/dist/core/errors.js +54 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/linked-pr-classification.d.ts +28 -0
- package/dist/core/linked-pr-classification.js +32 -0
- package/dist/core/pr-monitor.d.ts +1 -1
- package/dist/core/pr-monitor.js +31 -11
- package/dist/core/state-schema.d.ts +1 -0
- package/dist/core/state-schema.js +9 -0
- package/dist/core/state.d.ts +7 -0
- package/dist/core/state.js +10 -0
- package/dist/core/strategy.d.ts +21 -1
- package/dist/core/strategy.js +44 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/formatters/json.d.ts +329 -35
- package/dist/formatters/json.js +102 -0
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Re-vets all available issues in a curated issue list file via @oss-scout/core.
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'node:fs';
|
|
6
|
-
import { adaptScoutLinkedPR, createAutopilotScout } from './scout-bridge.js';
|
|
6
|
+
import { adaptScoutLinkedPR, buildCandidateLinkedPR, createAutopilotScout } from './scout-bridge.js';
|
|
7
7
|
import { runParseList, pruneIssueList } from './parse-list.js';
|
|
8
8
|
import { detectIssueList } from './startup.js';
|
|
9
9
|
import { computeSuccessGrade, gradeFromCandidate } from '../core/issue-grading.js';
|
|
@@ -17,7 +17,7 @@ const KNOWN_SKIP_REASONS = new Set([
|
|
|
17
17
|
'anti_llm_policy',
|
|
18
18
|
'other',
|
|
19
19
|
]);
|
|
20
|
-
function mapSkipReasonToStatus(reason) {
|
|
20
|
+
function mapSkipReasonToStatus(reason, vetResult) {
|
|
21
21
|
switch (reason) {
|
|
22
22
|
case 'issue_closed': {
|
|
23
23
|
return 'closed';
|
|
@@ -26,6 +26,12 @@ function mapSkipReasonToStatus(reason) {
|
|
|
26
26
|
return 'claimed';
|
|
27
27
|
}
|
|
28
28
|
case 'has_linked_pr': {
|
|
29
|
+
// Open linked PRs that have been idle for 30+ days are revive
|
|
30
|
+
// opportunities (#97 / scout 0.9.0) — surface them with a distinct
|
|
31
|
+
// status rather than auto-dropping as `has_pr`.
|
|
32
|
+
if (vetResult.linkedPR?.state === 'open' && vetResult.linkedPR.isStalled) {
|
|
33
|
+
return 'has_stalled_pr';
|
|
34
|
+
}
|
|
29
35
|
return 'has_pr';
|
|
30
36
|
}
|
|
31
37
|
case 'score_too_low':
|
|
@@ -57,7 +63,7 @@ export function extractSkipReason(candidate) {
|
|
|
57
63
|
*/
|
|
58
64
|
export function classifyListStatus(vetResult, skipReason) {
|
|
59
65
|
if (skipReason) {
|
|
60
|
-
const fromEnum = mapSkipReasonToStatus(skipReason);
|
|
66
|
+
const fromEnum = mapSkipReasonToStatus(skipReason, vetResult);
|
|
61
67
|
if (fromEnum)
|
|
62
68
|
return fromEnum;
|
|
63
69
|
// skipReason was set but maps to 'other' / low-score / policy — let the
|
|
@@ -69,8 +75,15 @@ export function classifyListStatus(vetResult, skipReason) {
|
|
|
69
75
|
return 'closed';
|
|
70
76
|
if (skipReasons.some((r) => r.includes('claimed') || r.includes('assigned')))
|
|
71
77
|
return 'claimed';
|
|
72
|
-
if (skipReasons.some((r) => r.includes('existing pr') || r.includes('linked pr') || r.includes('pull request')))
|
|
78
|
+
if (skipReasons.some((r) => r.includes('existing pr') || r.includes('linked pr') || r.includes('pull request'))) {
|
|
79
|
+
// Same revive-opportunity branch as the enum path above — when scout
|
|
80
|
+
// hasn't yet emitted skipReason but we can see a stalled open PR on
|
|
81
|
+
// the candidate, prefer the dedicated status (#97 / scout 0.9.0).
|
|
82
|
+
if (vetResult.linkedPR?.state === 'open' && vetResult.linkedPR.isStalled) {
|
|
83
|
+
return 'has_stalled_pr';
|
|
84
|
+
}
|
|
73
85
|
return 'has_pr';
|
|
86
|
+
}
|
|
74
87
|
}
|
|
75
88
|
if (vetResult.recommendation === 'approve' || vetResult.recommendation === 'needs_review') {
|
|
76
89
|
return 'still_available';
|
|
@@ -102,7 +115,7 @@ export async function runVetList(options = {}) {
|
|
|
102
115
|
if (parsed.available.length === 0) {
|
|
103
116
|
return {
|
|
104
117
|
results: [],
|
|
105
|
-
summary: { total: 0, stillAvailable: 0, claimed: 0, closed: 0, hasPR: 0, errors: 0 },
|
|
118
|
+
summary: { total: 0, stillAvailable: 0, claimed: 0, closed: 0, hasPR: 0, hasStalledPR: 0, errors: 0 },
|
|
106
119
|
};
|
|
107
120
|
}
|
|
108
121
|
// 2. Vet each available issue in parallel with concurrency limit
|
|
@@ -126,6 +139,7 @@ export async function runVetList(options = {}) {
|
|
|
126
139
|
linkedPR: adaptScoutLinkedPR(candidate.vettingResult.linkedPR),
|
|
127
140
|
userLogin,
|
|
128
141
|
});
|
|
142
|
+
const linkedPR = buildCandidateLinkedPR(candidate.vettingResult.linkedPR);
|
|
129
143
|
const vetResult = {
|
|
130
144
|
issue: {
|
|
131
145
|
repo: candidate.issue.repo,
|
|
@@ -141,6 +155,7 @@ export async function runVetList(options = {}) {
|
|
|
141
155
|
vettingResult: candidate.vettingResult,
|
|
142
156
|
antiLLMPolicy: candidate.antiLLMPolicy,
|
|
143
157
|
linkedPRClassification,
|
|
158
|
+
...(linkedPR ? { linkedPR } : {}),
|
|
144
159
|
slmTriage: candidate.slmTriage ?? null,
|
|
145
160
|
grade,
|
|
146
161
|
};
|
|
@@ -175,6 +190,7 @@ export async function runVetList(options = {}) {
|
|
|
175
190
|
claimed: results.filter((r) => r.listStatus === 'claimed').length,
|
|
176
191
|
closed: results.filter((r) => r.listStatus === 'closed').length,
|
|
177
192
|
hasPR: results.filter((r) => r.listStatus === 'has_pr').length,
|
|
193
|
+
hasStalledPR: results.filter((r) => r.listStatus === 'has_stalled_pr').length,
|
|
178
194
|
errors: results.filter((r) => r.listStatus === 'error').length,
|
|
179
195
|
};
|
|
180
196
|
// 4. Prune the file if requested — remove completed/skipped/low-score items
|
package/dist/commands/vet.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Vet command
|
|
3
3
|
* Vets a specific issue before working on it via @oss-scout/core
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { adaptScoutLinkedPR, buildCandidateLinkedPR, createAutopilotScout } from './scout-bridge.js';
|
|
6
6
|
import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
|
|
7
7
|
import { gradeFromCandidate } from '../core/issue-grading.js';
|
|
8
8
|
import { getStateManager, classifyLinkedPR } from '../core/index.js';
|
|
@@ -29,6 +29,7 @@ export async function runVet(options) {
|
|
|
29
29
|
linkedPR: adaptScoutLinkedPR(candidate.vettingResult.linkedPR),
|
|
30
30
|
userLogin,
|
|
31
31
|
});
|
|
32
|
+
const linkedPR = buildCandidateLinkedPR(candidate.vettingResult.linkedPR);
|
|
32
33
|
return {
|
|
33
34
|
issue: {
|
|
34
35
|
repo: candidate.issue.repo,
|
|
@@ -44,6 +45,7 @@ export async function runVet(options) {
|
|
|
44
45
|
vettingResult: candidate.vettingResult,
|
|
45
46
|
antiLLMPolicy: candidate.antiLLMPolicy,
|
|
46
47
|
linkedPRClassification,
|
|
48
|
+
...(linkedPR ? { linkedPR } : {}),
|
|
47
49
|
slmTriage: candidate.slmTriage ?? null,
|
|
48
50
|
grade,
|
|
49
51
|
};
|
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* AI/LLM policy scans (#108, #911, #979, #1269).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Two complementary scanners over the same input (concatenated repo docs
|
|
5
|
+
* — CONTRIBUTING.md, CODE_OF_CONDUCT.md, README, etc.):
|
|
6
|
+
*
|
|
7
|
+
* 1. {@link scanForAntiLLMPolicy} — detects language indicating the
|
|
8
|
+
* project does NOT accept AI/LLM-generated contributions. Used by
|
|
9
|
+
* issue-scout / repo-evaluator to filter out repos where landing a
|
|
10
|
+
* PR is impossible.
|
|
11
|
+
*
|
|
12
|
+
* 2. {@link scanAIDisclosureRequirement} — detects the opposite:
|
|
13
|
+
* language requiring or inviting AI disclosure ("must disclose AI
|
|
14
|
+
* use", "credit AI tools"). Used by pr-compliance-checker to decide
|
|
15
|
+
* whether AI attribution should be flagged as a violation, encouraged,
|
|
16
|
+
* or required (#1269 Improvement C).
|
|
8
17
|
*
|
|
9
18
|
* The long-term home for this logic is `@oss-scout/core`, where the
|
|
10
19
|
* relevant files are already fetched during vetting. Keeping it here
|
|
11
20
|
* for now lets the agent invoke it directly and gives scout a
|
|
12
21
|
* reference implementation + test fixtures to adopt. See #979.
|
|
13
22
|
*
|
|
14
|
-
* Precision matters more than recall
|
|
15
|
-
*
|
|
16
|
-
* contribution surface
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* Precision matters more than recall in both directions. A false
|
|
24
|
+
* positive on the anti-LLM side silently shrinks the user's
|
|
25
|
+
* contribution surface; a false positive on the disclosure side tells
|
|
26
|
+
* the user to add attribution that the maintainer didn't actually ask
|
|
27
|
+
* for. Patterns require an explicit verb-phrase + AI/LLM-noun pairing.
|
|
19
28
|
*
|
|
20
29
|
* **User-facing reference:** `docs/anti-llm-policy.md` — explains the
|
|
21
|
-
* three categories, example phrases per category, and the
|
|
22
|
-
* resistance design (why "AI division will be closed at
|
|
23
|
-
* does NOT match).
|
|
30
|
+
* three anti-LLM categories, example phrases per category, and the
|
|
31
|
+
* false-positive-resistance design (why "AI division will be closed at
|
|
32
|
+
* end of Q4" does NOT match).
|
|
24
33
|
*/
|
|
25
34
|
export type AntiLLMCategory = 'explicit_ban' | 'tool_ban' | 'reject_framing';
|
|
26
35
|
export interface AntiLLMMatch {
|
|
@@ -35,3 +44,23 @@ export interface AntiLLMScanResult {
|
|
|
35
44
|
matches: AntiLLMMatch[];
|
|
36
45
|
}
|
|
37
46
|
export declare function scanForAntiLLMPolicy(text: string): AntiLLMScanResult;
|
|
47
|
+
export type AIDisclosureCategory = 'mandatory' | 'recommended' | 'invited';
|
|
48
|
+
export interface AIDisclosureMatch {
|
|
49
|
+
category: AIDisclosureCategory;
|
|
50
|
+
phrase: string;
|
|
51
|
+
excerpt: string;
|
|
52
|
+
}
|
|
53
|
+
export interface AIDisclosureScanResult {
|
|
54
|
+
matched: boolean;
|
|
55
|
+
matches: AIDisclosureMatch[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Scan repo docs for language requiring or inviting AI disclosure (#1269).
|
|
59
|
+
*
|
|
60
|
+
* Mirrors {@link scanForAntiLLMPolicy}'s shape: same input contract,
|
|
61
|
+
* same false-positive-resistance discipline, same per-match excerpt for
|
|
62
|
+
* surfacing to the user. Categories are ordered by binding strength —
|
|
63
|
+
* callers that want to avoid false-positive flagging on weak invitations
|
|
64
|
+
* can filter to 'mandatory' / 'recommended' only.
|
|
65
|
+
*/
|
|
66
|
+
export declare function scanAIDisclosureRequirement(text: string): AIDisclosureScanResult;
|
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* AI/LLM policy scans (#108, #911, #979, #1269).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Two complementary scanners over the same input (concatenated repo docs
|
|
5
|
+
* — CONTRIBUTING.md, CODE_OF_CONDUCT.md, README, etc.):
|
|
6
|
+
*
|
|
7
|
+
* 1. {@link scanForAntiLLMPolicy} — detects language indicating the
|
|
8
|
+
* project does NOT accept AI/LLM-generated contributions. Used by
|
|
9
|
+
* issue-scout / repo-evaluator to filter out repos where landing a
|
|
10
|
+
* PR is impossible.
|
|
11
|
+
*
|
|
12
|
+
* 2. {@link scanAIDisclosureRequirement} — detects the opposite:
|
|
13
|
+
* language requiring or inviting AI disclosure ("must disclose AI
|
|
14
|
+
* use", "credit AI tools"). Used by pr-compliance-checker to decide
|
|
15
|
+
* whether AI attribution should be flagged as a violation, encouraged,
|
|
16
|
+
* or required (#1269 Improvement C).
|
|
8
17
|
*
|
|
9
18
|
* The long-term home for this logic is `@oss-scout/core`, where the
|
|
10
19
|
* relevant files are already fetched during vetting. Keeping it here
|
|
11
20
|
* for now lets the agent invoke it directly and gives scout a
|
|
12
21
|
* reference implementation + test fixtures to adopt. See #979.
|
|
13
22
|
*
|
|
14
|
-
* Precision matters more than recall
|
|
15
|
-
*
|
|
16
|
-
* contribution surface
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* Precision matters more than recall in both directions. A false
|
|
24
|
+
* positive on the anti-LLM side silently shrinks the user's
|
|
25
|
+
* contribution surface; a false positive on the disclosure side tells
|
|
26
|
+
* the user to add attribution that the maintainer didn't actually ask
|
|
27
|
+
* for. Patterns require an explicit verb-phrase + AI/LLM-noun pairing.
|
|
19
28
|
*
|
|
20
29
|
* **User-facing reference:** `docs/anti-llm-policy.md` — explains the
|
|
21
|
-
* three categories, example phrases per category, and the
|
|
22
|
-
* resistance design (why "AI division will be closed at
|
|
23
|
-
* does NOT match).
|
|
30
|
+
* three anti-LLM categories, example phrases per category, and the
|
|
31
|
+
* false-positive-resistance design (why "AI division will be closed at
|
|
32
|
+
* end of Q4" does NOT match).
|
|
24
33
|
*/
|
|
25
34
|
const PATTERNS = [
|
|
26
35
|
// Explicit "no X" bans against AI/LLM nouns.
|
|
@@ -104,3 +113,83 @@ export function scanForAntiLLMPolicy(text) {
|
|
|
104
113
|
}
|
|
105
114
|
return { matched: matches.length > 0, matches };
|
|
106
115
|
}
|
|
116
|
+
const DISCLOSURE_PATTERNS = [
|
|
117
|
+
// Mandatory: imperative verbs binding to an AI/LLM disclosure noun.
|
|
118
|
+
// Match shape: <verb-phrase> <AI noun> <disclosure noun>?
|
|
119
|
+
// Verb phrases: "must disclose", "required to disclose", "are required
|
|
120
|
+
// to indicate", "you must indicate". The optional disclosure-action
|
|
121
|
+
// noun catches "must disclose use of AI" without requiring a separate
|
|
122
|
+
// pattern. The AI noun can be plain "ai/llm" or a tool name.
|
|
123
|
+
{
|
|
124
|
+
category: 'mandatory',
|
|
125
|
+
regex: /\b(must|required\s+to|are\s+required\s+to)\s+(disclose|indicate|declare|note|state|mention|label|tag|credit|acknowledge)\s+(?:[a-z\s'-]{0,40}?\b)?(ai|llm|generative\s+ai|copilot|chatgpt|claude|cursor)\b/i,
|
|
126
|
+
},
|
|
127
|
+
// "PRs using AI must be labeled / tagged / disclosed"
|
|
128
|
+
{
|
|
129
|
+
category: 'mandatory',
|
|
130
|
+
regex: /\b(prs?|contributions?|commits?|code)\s+(using|generated\s+by|written\s+by|made\s+with)\s+(ai|llm|copilot|chatgpt|claude|cursor)\s+(?:tools?\s+)?(must\s+be|need\s+to\s+be|are\s+to\s+be)\s+(labeled|tagged|disclosed|marked|flagged|noted)\b/i,
|
|
131
|
+
},
|
|
132
|
+
// "Disclosure of AI assistance is required" — passive form
|
|
133
|
+
{
|
|
134
|
+
category: 'mandatory',
|
|
135
|
+
regex: /\b(disclosure|disclosing|labeling|labelling|tagging|crediting)\s+(of\s+)?(ai|llm|copilot|chatgpt|claude|cursor)(\s+(use|usage|assistance|tools?|contributions?|generated\s+code))?\s+(is\s+required|is\s+mandatory|must\s+be\s+included)\b/i,
|
|
136
|
+
},
|
|
137
|
+
// Recommended: softer "should" or "we ask" framing. Same noun anchors.
|
|
138
|
+
// Verb forms allow optional gerund (-ing) endings since "we strongly
|
|
139
|
+
// encourage acknowledging AI" reads naturally even though "acknowledge"
|
|
140
|
+
// is the bare verb in the imperative list.
|
|
141
|
+
{
|
|
142
|
+
category: 'recommended',
|
|
143
|
+
regex: /\b(should|we\s+ask\s+you\s+to|we\s+ask\s+that\s+you|we\s+(?:strongly\s+)?(?:encourage|recommend))\s+(disclose|disclosing|indicate|indicating|declare|declaring|note|noting|mention|mentioning|label|labeling|labelling|tag|tagging|credit|crediting|acknowledge|acknowledging)\s+(?:[a-z\s'-]{0,40}?\b)?(ai|llm|generative\s+ai|copilot|chatgpt|claude|cursor)\b/i,
|
|
144
|
+
},
|
|
145
|
+
// "credit AI tools you used" — direct imperative without "must/should"
|
|
146
|
+
{
|
|
147
|
+
category: 'recommended',
|
|
148
|
+
regex: /\b(credit|acknowledge|attribute)\s+(ai|llm|generative\s+ai)\s+(tools?|assistants?|use|usage|assistance)\b/i,
|
|
149
|
+
},
|
|
150
|
+
// Invited: permissive framing. Lower confidence.
|
|
151
|
+
// The "please" branch is dropped intentionally — bare "please mention X"
|
|
152
|
+
// doesn't carry enough policy weight on its own, and including it as
|
|
153
|
+
// `please\s+(?:feel\s+free\s+to)?\s+` introduces super-linear backtracking
|
|
154
|
+
// (regexp/no-super-linear-backtracking) because of the ambiguous \s+
|
|
155
|
+
// boundary on either side of the optional group.
|
|
156
|
+
{
|
|
157
|
+
category: 'invited',
|
|
158
|
+
regex: /\b(feel\s+free\s+to|please\s+feel\s+free\s+to|you('re|\s+are)\s+welcome\s+to|welcome\s+to)\s+(disclose|mention|note|indicate|label|tag|credit)\s+(?:[a-z\s'-]{0,40}?\b)?(ai|llm|generative\s+ai|copilot|chatgpt|claude|cursor)\b/i,
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
/**
|
|
162
|
+
* Scan repo docs for language requiring or inviting AI disclosure (#1269).
|
|
163
|
+
*
|
|
164
|
+
* Mirrors {@link scanForAntiLLMPolicy}'s shape: same input contract,
|
|
165
|
+
* same false-positive-resistance discipline, same per-match excerpt for
|
|
166
|
+
* surfacing to the user. Categories are ordered by binding strength —
|
|
167
|
+
* callers that want to avoid false-positive flagging on weak invitations
|
|
168
|
+
* can filter to 'mandatory' / 'recommended' only.
|
|
169
|
+
*/
|
|
170
|
+
export function scanAIDisclosureRequirement(text) {
|
|
171
|
+
if (typeof text !== 'string') {
|
|
172
|
+
throw new TypeError(`scanAIDisclosureRequirement: expected string, received ${typeof text}`);
|
|
173
|
+
}
|
|
174
|
+
if (text === '')
|
|
175
|
+
return { matched: false, matches: [] };
|
|
176
|
+
const normalized = normalizeText(text);
|
|
177
|
+
const seenLabels = new Set();
|
|
178
|
+
const matches = [];
|
|
179
|
+
for (const pattern of DISCLOSURE_PATTERNS) {
|
|
180
|
+
const hit = normalized.match(pattern.regex);
|
|
181
|
+
if (!hit || hit.index === undefined)
|
|
182
|
+
continue;
|
|
183
|
+
const phrase = hit[0];
|
|
184
|
+
const key = `${pattern.category}:${phrase.toLowerCase()}`;
|
|
185
|
+
if (seenLabels.has(key))
|
|
186
|
+
continue;
|
|
187
|
+
seenLabels.add(key);
|
|
188
|
+
matches.push({
|
|
189
|
+
category: pattern.category,
|
|
190
|
+
phrase,
|
|
191
|
+
excerpt: makeExcerpt(normalized, hit.index, phrase.length),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return { matched: matches.length > 0, matches };
|
|
195
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Extracted from PRMonitor to isolate CI-related logic (#263).
|
|
4
4
|
*/
|
|
5
5
|
import type { Octokit } from '@octokit/rest';
|
|
6
|
-
import { CIFailureCategory, ClassifiedCheck, CIStatusResult } from './types.js';
|
|
6
|
+
import { CIFailureCategory, ClassifiedCheck, CIStatusResult, CIStatus, CIStatusCategorization } from './types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Classify a failing CI check as actionable, fork_limitation, auth_gate, or infrastructure (#81, #145, #743).
|
|
9
9
|
* Default is 'actionable' — only known patterns get reclassified.
|
|
@@ -16,6 +16,37 @@ export declare function classifyCICheck(name: string, description?: string, conc
|
|
|
16
16
|
* Accepts optional conclusion data to detect infrastructure failures and auth gates.
|
|
17
17
|
*/
|
|
18
18
|
export declare function classifyFailingChecks(failingCheckNames: string[], conclusions?: Map<string, string>): ClassifiedCheck[];
|
|
19
|
+
/**
|
|
20
|
+
* Map an aggregate `ciStatus + failingCheckNames + classifiedChecks` triple
|
|
21
|
+
* into one of five mutually exclusive overall states (#1272). The 5-row
|
|
22
|
+
* truth table previously lived as prose in `agents/pr-health-checker.md`;
|
|
23
|
+
* extracting it lets that agent (and any future consumer — dashboard,
|
|
24
|
+
* MCP, sibling agents that adopt the field) read a single typed value
|
|
25
|
+
* instead of re-deriving from `ciStatus + failingCheckNames + classifiedChecks`.
|
|
26
|
+
*
|
|
27
|
+
* Decision order (each branch is exclusive):
|
|
28
|
+
* 1. `passing` → `all_passing`
|
|
29
|
+
* 2. `pending` → `blocked` (awaiting trigger / completion)
|
|
30
|
+
* 3. `failing` + actionable → `failing` (real test/lint/build issue)
|
|
31
|
+
* 4. `failing` + only infrastructure → `blocked` (cancelled/timed-out runner — needs rerun)
|
|
32
|
+
* 5. `failing` + only fork/auth → `fork_limitation` (informational)
|
|
33
|
+
* 6. `failing` + zero classified → `failing` w/ "details unavailable" summary
|
|
34
|
+
* 7. `unknown` → `not_running`
|
|
35
|
+
*
|
|
36
|
+
* Why infrastructure routes to `blocked` and not `fork_limitation`:
|
|
37
|
+
* a cancelled or timed-out runner is genuinely worth re-running; calling
|
|
38
|
+
* it "informational" would tell the agent to ignore something the user
|
|
39
|
+
* can fix with a rerun-request.
|
|
40
|
+
*
|
|
41
|
+
* The `summary` is short (≤180 char even for 10+ failing checks) and
|
|
42
|
+
* suitable for inline display. `action` is a hint, not enforcement —
|
|
43
|
+
* agents may still escalate based on other PR context.
|
|
44
|
+
*/
|
|
45
|
+
export declare function categorizeCIStatus(input: {
|
|
46
|
+
ciStatus: CIStatus;
|
|
47
|
+
failingCheckNames: string[];
|
|
48
|
+
classifiedChecks: ClassifiedCheck[];
|
|
49
|
+
}): CIStatusCategorization;
|
|
19
50
|
/**
|
|
20
51
|
* Analyze check runs (GitHub Actions, etc.) and categorize them.
|
|
21
52
|
* Returns flags for failing/pending/success and lists of failing check names + conclusions.
|
package/dist/core/ci-analysis.js
CHANGED
|
@@ -92,6 +92,98 @@ export function classifyFailingChecks(failingCheckNames, conclusions) {
|
|
|
92
92
|
};
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Map an aggregate `ciStatus + failingCheckNames + classifiedChecks` triple
|
|
97
|
+
* into one of five mutually exclusive overall states (#1272). The 5-row
|
|
98
|
+
* truth table previously lived as prose in `agents/pr-health-checker.md`;
|
|
99
|
+
* extracting it lets that agent (and any future consumer — dashboard,
|
|
100
|
+
* MCP, sibling agents that adopt the field) read a single typed value
|
|
101
|
+
* instead of re-deriving from `ciStatus + failingCheckNames + classifiedChecks`.
|
|
102
|
+
*
|
|
103
|
+
* Decision order (each branch is exclusive):
|
|
104
|
+
* 1. `passing` → `all_passing`
|
|
105
|
+
* 2. `pending` → `blocked` (awaiting trigger / completion)
|
|
106
|
+
* 3. `failing` + actionable → `failing` (real test/lint/build issue)
|
|
107
|
+
* 4. `failing` + only infrastructure → `blocked` (cancelled/timed-out runner — needs rerun)
|
|
108
|
+
* 5. `failing` + only fork/auth → `fork_limitation` (informational)
|
|
109
|
+
* 6. `failing` + zero classified → `failing` w/ "details unavailable" summary
|
|
110
|
+
* 7. `unknown` → `not_running`
|
|
111
|
+
*
|
|
112
|
+
* Why infrastructure routes to `blocked` and not `fork_limitation`:
|
|
113
|
+
* a cancelled or timed-out runner is genuinely worth re-running; calling
|
|
114
|
+
* it "informational" would tell the agent to ignore something the user
|
|
115
|
+
* can fix with a rerun-request.
|
|
116
|
+
*
|
|
117
|
+
* The `summary` is short (≤180 char even for 10+ failing checks) and
|
|
118
|
+
* suitable for inline display. `action` is a hint, not enforcement —
|
|
119
|
+
* agents may still escalate based on other PR context.
|
|
120
|
+
*/
|
|
121
|
+
export function categorizeCIStatus(input) {
|
|
122
|
+
const { ciStatus, failingCheckNames, classifiedChecks } = input;
|
|
123
|
+
if (ciStatus === 'passing') {
|
|
124
|
+
return { category: 'all_passing', summary: 'All checks passing', action: 'none' };
|
|
125
|
+
}
|
|
126
|
+
if (ciStatus === 'pending') {
|
|
127
|
+
// `mergeStatuses` currently sets `failingCheckNames: []` on pending,
|
|
128
|
+
// so the count branch is defensive — kept so a future caller that
|
|
129
|
+
// forwards pending names doesn't silently drop them.
|
|
130
|
+
const count = failingCheckNames.length;
|
|
131
|
+
const summary = count > 0 ? `${count} pending check(s); CI run incomplete` : 'CI checks pending';
|
|
132
|
+
return { category: 'blocked', summary, action: 'request_rerun' };
|
|
133
|
+
}
|
|
134
|
+
if (ciStatus === 'failing') {
|
|
135
|
+
const actionable = classifiedChecks.filter((c) => c.category === 'actionable');
|
|
136
|
+
if (actionable.length > 0) {
|
|
137
|
+
const preview = actionable
|
|
138
|
+
.slice(0, 3)
|
|
139
|
+
.map((c) => c.name)
|
|
140
|
+
.join(', ');
|
|
141
|
+
const more = actionable.length > 3 ? ` (+${actionable.length - 3} more)` : '';
|
|
142
|
+
return {
|
|
143
|
+
category: 'failing',
|
|
144
|
+
summary: `${actionable.length} actionable failure(s): ${preview}${more}`,
|
|
145
|
+
action: 'investigate',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// No actionable failures. Distinguish three sub-cases:
|
|
149
|
+
// (a) failing with zero classified checks: status came from the
|
|
150
|
+
// legacy combined-status endpoint without per-check detail. Be
|
|
151
|
+
// honest about the missing detail rather than asserting "fork
|
|
152
|
+
// limitations" the caller can't verify.
|
|
153
|
+
if (classifiedChecks.length === 0) {
|
|
154
|
+
return {
|
|
155
|
+
category: 'failing',
|
|
156
|
+
summary: 'CI reported failure but no check details available',
|
|
157
|
+
action: 'investigate',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
// (b) at least one infrastructure failure (cancelled / timed-out /
|
|
161
|
+
// dependency-install). Re-running often fixes the issue, so
|
|
162
|
+
// surface as `blocked` with `request_rerun` rather than
|
|
163
|
+
// mislabeling as "fork limits / auth gates."
|
|
164
|
+
const hasInfrastructure = classifiedChecks.some((c) => c.category === 'infrastructure');
|
|
165
|
+
if (hasInfrastructure) {
|
|
166
|
+
const total = classifiedChecks.length;
|
|
167
|
+
return {
|
|
168
|
+
category: 'blocked',
|
|
169
|
+
summary: `${total} non-actionable failure(s) including infrastructure issues; rerun may resolve`,
|
|
170
|
+
action: 'request_rerun',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// (c) only fork-limitation / auth-gate failures — purely informational.
|
|
174
|
+
const total = classifiedChecks.length;
|
|
175
|
+
return {
|
|
176
|
+
category: 'fork_limitation',
|
|
177
|
+
summary: `${total} non-actionable failure(s) (fork limits / auth gates)`,
|
|
178
|
+
action: 'informational',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// ciStatus === 'unknown' — no checks reported, or status couldn't be
|
|
182
|
+
// determined. Treat both as not_running so callers don't have to
|
|
183
|
+
// distinguish the rare indeterminate case from the common "no CI
|
|
184
|
+
// configured" case.
|
|
185
|
+
return { category: 'not_running', summary: 'No CI checks reported', action: 'check_workflows' };
|
|
186
|
+
}
|
|
95
187
|
/**
|
|
96
188
|
* Analyze check runs (GitHub Actions, etc.) and categorize them.
|
|
97
189
|
* Returns flags for failing/pending/success and lists of failing check names + conclusions.
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -85,6 +85,25 @@ export declare function errorMessage(e: unknown): string;
|
|
|
85
85
|
export declare function getHttpStatusCode(error: unknown): number | undefined;
|
|
86
86
|
/** Check if an error is a GitHub rate limit error (429 or rate-limit 403). */
|
|
87
87
|
export declare function isRateLimitError(error: unknown): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Check if an error is GitHub's "users do not exist" Search-API validation
|
|
90
|
+
* failure (HTTP 422 with `resource: 'Search', code: 'invalid'` and a message
|
|
91
|
+
* indicating the user couldn't be resolved). Returned when the Search API
|
|
92
|
+
* can't resolve the user named in an `author:`/`user:` qualifier — the
|
|
93
|
+
* typical cause is a stale or mis-typed `githubUsername` in
|
|
94
|
+
* `~/.oss-autopilot/state.json`.
|
|
95
|
+
*
|
|
96
|
+
* Surfaced as a generic "Validation Failed" string by Octokit, which gives
|
|
97
|
+
* the user no actionable signal. Callers wrap the search and rethrow this
|
|
98
|
+
* as a {@link ConfigurationError} so the CLI prints the configured username
|
|
99
|
+
* and points at `/setup-oss`.
|
|
100
|
+
*
|
|
101
|
+
* The message-text gate is load-bearing: GitHub returns the same
|
|
102
|
+
* `resource`/`code` pair for other Search 422s (query too long, too many
|
|
103
|
+
* ORs). Without the gate, those would silently rewrite to "your configured
|
|
104
|
+
* username is wrong," which is actively misleading.
|
|
105
|
+
*/
|
|
106
|
+
export declare function isInvalidUserSearchError(err: unknown): boolean;
|
|
88
107
|
/** Return true for errors that should propagate (not degrade gracefully): rate limits, auth failures, abuse detection. */
|
|
89
108
|
export declare function isRateLimitOrAuthError(err: unknown): boolean;
|
|
90
109
|
/**
|
package/dist/core/errors.js
CHANGED
|
@@ -142,6 +142,60 @@ export function isRateLimitError(error) {
|
|
|
142
142
|
}
|
|
143
143
|
return false;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Match-text used to discriminate the user-resolution failure from sibling
|
|
147
|
+
* `resource: 'Search', code: 'invalid'` 422s (query-too-long,
|
|
148
|
+
* too-many-OR-operators, malformed qualifier). Both the structured and the
|
|
149
|
+
* fallback paths gate on this pattern so the matcher's name remains accurate
|
|
150
|
+
* if a future caller uses a different Search query.
|
|
151
|
+
*/
|
|
152
|
+
const USER_NOT_FOUND_SEARCH_MESSAGE = /users.*do not exist|cannot be searched/i;
|
|
153
|
+
/**
|
|
154
|
+
* Check if an error is GitHub's "users do not exist" Search-API validation
|
|
155
|
+
* failure (HTTP 422 with `resource: 'Search', code: 'invalid'` and a message
|
|
156
|
+
* indicating the user couldn't be resolved). Returned when the Search API
|
|
157
|
+
* can't resolve the user named in an `author:`/`user:` qualifier — the
|
|
158
|
+
* typical cause is a stale or mis-typed `githubUsername` in
|
|
159
|
+
* `~/.oss-autopilot/state.json`.
|
|
160
|
+
*
|
|
161
|
+
* Surfaced as a generic "Validation Failed" string by Octokit, which gives
|
|
162
|
+
* the user no actionable signal. Callers wrap the search and rethrow this
|
|
163
|
+
* as a {@link ConfigurationError} so the CLI prints the configured username
|
|
164
|
+
* and points at `/setup-oss`.
|
|
165
|
+
*
|
|
166
|
+
* The message-text gate is load-bearing: GitHub returns the same
|
|
167
|
+
* `resource`/`code` pair for other Search 422s (query too long, too many
|
|
168
|
+
* ORs). Without the gate, those would silently rewrite to "your configured
|
|
169
|
+
* username is wrong," which is actively misleading.
|
|
170
|
+
*/
|
|
171
|
+
export function isInvalidUserSearchError(err) {
|
|
172
|
+
if (getHttpStatusCode(err) !== 422)
|
|
173
|
+
return false;
|
|
174
|
+
const data = err?.response?.data;
|
|
175
|
+
const errors = data && typeof data === 'object' ? data.errors : undefined;
|
|
176
|
+
if (Array.isArray(errors)) {
|
|
177
|
+
return errors.some((e) => {
|
|
178
|
+
if (!e || typeof e !== 'object')
|
|
179
|
+
return false;
|
|
180
|
+
const entry = e;
|
|
181
|
+
if (entry.resource !== 'Search' || entry.code !== 'invalid')
|
|
182
|
+
return false;
|
|
183
|
+
// The Search API includes a per-error `message` for this case. When
|
|
184
|
+
// present, gate on it to avoid matching sibling validation failures
|
|
185
|
+
// that share the resource/code pair. When absent, fall back to the
|
|
186
|
+
// top-level message check below — some serializations drop the
|
|
187
|
+
// per-entry message but keep it on the response.
|
|
188
|
+
if (typeof entry.message === 'string') {
|
|
189
|
+
return USER_NOT_FOUND_SEARCH_MESSAGE.test(entry.message);
|
|
190
|
+
}
|
|
191
|
+
return USER_NOT_FOUND_SEARCH_MESSAGE.test(errorMessage(err));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// Fallback for serialized errors that lost the structured `response.data`
|
|
195
|
+
// (e.g. messages re-thrown across boundaries). The Search API's own copy
|
|
196
|
+
// is stable enough to match against.
|
|
197
|
+
return USER_NOT_FOUND_SEARCH_MESSAGE.test(errorMessage(err));
|
|
198
|
+
}
|
|
145
199
|
/** Return true for errors that should propagate (not degrade gracefully): rate limits, auth failures, abuse detection. */
|
|
146
200
|
export function isRateLimitOrAuthError(err) {
|
|
147
201
|
const status = getHttpStatusCode(err);
|
package/dist/core/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-
|
|
|
21
21
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, buildStarFilter, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
22
22
|
export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
|
|
23
23
|
export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
|
|
24
|
-
export { classifyLinkedPR, type LinkedPR, type LinkedPRClassification, type LinkedPRState, } from './linked-pr-classification.js';
|
|
24
|
+
export { classifyLinkedPR, isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, type LinkedPR, type LinkedPRClassification, type LinkedPRState, } from './linked-pr-classification.js';
|
|
25
25
|
export { scanForAntiLLMPolicy, type AntiLLMCategory, type AntiLLMMatch, type AntiLLMScanResult, } from './anti-llm-policy.js';
|
|
26
26
|
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, type DetectedFormatter, type FormatterDetectionResult, type CIFormatterDiagnosis, type FormatterName, } from './formatter-detection.js';
|
|
27
27
|
export { CONFIG_KEY_REGISTRY, type ConfigKeyDef, type SettableVia, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
|
package/dist/core/index.js
CHANGED
|
@@ -22,7 +22,7 @@ export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
|
|
|
22
22
|
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, buildStarFilter, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
23
23
|
export { computeContributionStats } from './stats.js';
|
|
24
24
|
export { fetchPRTemplate } from './pr-template.js';
|
|
25
|
-
export { classifyLinkedPR, } from './linked-pr-classification.js';
|
|
25
|
+
export { classifyLinkedPR, isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, } from './linked-pr-classification.js';
|
|
26
26
|
export { scanForAntiLLMPolicy, } from './anti-llm-policy.js';
|
|
27
27
|
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, } from './formatter-detection.js';
|
|
28
28
|
export { CONFIG_KEY_REGISTRY, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
|
|
@@ -23,7 +23,35 @@ export interface LinkedPR {
|
|
|
23
23
|
login: string;
|
|
24
24
|
} | null;
|
|
25
25
|
state: LinkedPRState;
|
|
26
|
+
/**
|
|
27
|
+
* ISO timestamp of the linked PR's last update, surfaced from scout's
|
|
28
|
+
* timeline-event metadata when available (#97). Optional so existing
|
|
29
|
+
* callers and fixtures that don't carry the field continue to type-check.
|
|
30
|
+
* Used by `isLinkedPRStalled` to flag open PRs that haven't been touched
|
|
31
|
+
* in the last `STALLED_PR_THRESHOLD_DAYS` days.
|
|
32
|
+
*/
|
|
33
|
+
updatedAt?: string;
|
|
26
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Days of inactivity that classify an open linked PR as "stalled" — kept
|
|
37
|
+
* in sync with scout's `STALLED_PR_THRESHOLD_DAYS` so the autopilot helper
|
|
38
|
+
* and scout's own annotation use the same boundary.
|
|
39
|
+
*/
|
|
40
|
+
export declare const STALLED_PR_THRESHOLD_DAYS = 30;
|
|
41
|
+
/**
|
|
42
|
+
* Determine whether an autopilot-shaped `LinkedPR` is stalled.
|
|
43
|
+
*
|
|
44
|
+
* True when:
|
|
45
|
+
* - the PR is open, AND
|
|
46
|
+
* - `updatedAt` is set, AND
|
|
47
|
+
* - the elapsed time since `updatedAt` is at least `thresholdDays`.
|
|
48
|
+
*
|
|
49
|
+
* Returns false for closed/merged PRs, missing `updatedAt`, or invalid
|
|
50
|
+
* timestamps. Reimplemented locally rather than delegating to scout's
|
|
51
|
+
* helper so this module owns autopilot's `LinkedPR` shape contract end
|
|
52
|
+
* to end (avoids a runtime import dependency on scout's internal type).
|
|
53
|
+
*/
|
|
54
|
+
export declare function isLinkedPRStalled(linkedPR: LinkedPR | null | undefined, now?: Date, thresholdDays?: number): boolean;
|
|
27
55
|
export declare function classifyLinkedPR(params: {
|
|
28
56
|
linkedPR: LinkedPR | null;
|
|
29
57
|
userLogin: string;
|
|
@@ -11,6 +11,38 @@
|
|
|
11
11
|
* linked PR themselves (e.g. via Octokit) and hand the shape to this
|
|
12
12
|
* function. See #978 for the upstream data-contract work.
|
|
13
13
|
*/
|
|
14
|
+
/**
|
|
15
|
+
* Days of inactivity that classify an open linked PR as "stalled" — kept
|
|
16
|
+
* in sync with scout's `STALLED_PR_THRESHOLD_DAYS` so the autopilot helper
|
|
17
|
+
* and scout's own annotation use the same boundary.
|
|
18
|
+
*/
|
|
19
|
+
export const STALLED_PR_THRESHOLD_DAYS = 30;
|
|
20
|
+
/**
|
|
21
|
+
* Determine whether an autopilot-shaped `LinkedPR` is stalled.
|
|
22
|
+
*
|
|
23
|
+
* True when:
|
|
24
|
+
* - the PR is open, AND
|
|
25
|
+
* - `updatedAt` is set, AND
|
|
26
|
+
* - the elapsed time since `updatedAt` is at least `thresholdDays`.
|
|
27
|
+
*
|
|
28
|
+
* Returns false for closed/merged PRs, missing `updatedAt`, or invalid
|
|
29
|
+
* timestamps. Reimplemented locally rather than delegating to scout's
|
|
30
|
+
* helper so this module owns autopilot's `LinkedPR` shape contract end
|
|
31
|
+
* to end (avoids a runtime import dependency on scout's internal type).
|
|
32
|
+
*/
|
|
33
|
+
export function isLinkedPRStalled(linkedPR, now = new Date(), thresholdDays = STALLED_PR_THRESHOLD_DAYS) {
|
|
34
|
+
if (!linkedPR)
|
|
35
|
+
return false;
|
|
36
|
+
if (linkedPR.state !== 'open')
|
|
37
|
+
return false;
|
|
38
|
+
if (!linkedPR.updatedAt)
|
|
39
|
+
return false;
|
|
40
|
+
const updatedMs = Date.parse(linkedPR.updatedAt);
|
|
41
|
+
if (!Number.isFinite(updatedMs))
|
|
42
|
+
return false;
|
|
43
|
+
const ageDays = (now.getTime() - updatedMs) / (1000 * 60 * 60 * 24);
|
|
44
|
+
return ageDays >= thresholdDays;
|
|
45
|
+
}
|
|
14
46
|
/**
|
|
15
47
|
* Normalize a state value from either REST (lowercase `open`/`closed`
|
|
16
48
|
* plus a separate `merged` boolean) or GraphQL (uppercase `OPEN`/
|