@oss-autopilot/core 3.13.4 → 3.14.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/README.md +3 -3
- package/dist/cli-registry.js +59 -84
- package/dist/cli.bundle.cjs +112 -109
- package/dist/cli.js +5 -4
- package/dist/commands/comments.js +44 -10
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +50 -2
- package/dist/commands/curated-list.d.ts +17 -0
- package/dist/commands/curated-list.js +25 -0
- package/dist/commands/daily.d.ts +7 -1
- package/dist/commands/daily.js +136 -57
- package/dist/commands/dashboard-cache.d.ts +69 -0
- package/dist/commands/dashboard-cache.js +219 -0
- package/dist/commands/dashboard-data.d.ts +18 -10
- package/dist/commands/dashboard-data.js +58 -8
- package/dist/commands/dashboard-gist-sync.d.ts +93 -0
- package/dist/commands/dashboard-gist-sync.js +237 -0
- package/dist/commands/dashboard-server.d.ts +6 -10
- package/dist/commands/dashboard-server.js +181 -347
- package/dist/commands/features.js +6 -0
- package/dist/commands/guidelines.d.ts +6 -0
- package/dist/commands/guidelines.js +7 -0
- package/dist/commands/index.d.ts +2 -5
- package/dist/commands/index.js +2 -4
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +7 -1
- package/dist/commands/list-mark-done.js +6 -21
- package/dist/commands/list-move-tier.js +3 -5
- package/dist/commands/locate-issue-list.d.ts +25 -0
- package/dist/commands/locate-issue-list.js +67 -0
- package/dist/commands/merge-loop.d.ts +63 -0
- package/dist/commands/merge-loop.js +157 -0
- package/dist/commands/repo-vet.js +40 -1
- package/dist/commands/scout-bridge.d.ts +35 -2
- package/dist/commands/scout-bridge.js +65 -13
- package/dist/commands/search.d.ts +4 -6
- package/dist/commands/search.js +58 -11
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +56 -2
- package/dist/commands/skip-file-parser.d.ts +23 -0
- package/dist/commands/skip-file-parser.js +23 -10
- package/dist/commands/startup.d.ts +1 -6
- package/dist/commands/startup.js +25 -59
- package/dist/commands/track.d.ts +2 -2
- package/dist/commands/track.js +2 -2
- package/dist/commands/vet-list.d.ts +6 -6
- package/dist/commands/vet-list.js +194 -65
- package/dist/core/config-registry.js +36 -0
- package/dist/core/daily-logic.d.ts +25 -2
- package/dist/core/daily-logic.js +58 -3
- package/dist/core/gist-health.d.ts +81 -0
- package/dist/core/gist-health.js +39 -0
- package/dist/core/gist-state-store.d.ts +3 -1
- package/dist/core/gist-state-store.js +7 -2
- package/dist/core/github-stats.d.ts +2 -2
- package/dist/core/github-stats.js +20 -4
- package/dist/core/index.d.ts +5 -4
- package/dist/core/index.js +5 -4
- package/dist/core/issue-conversation.js +8 -2
- package/dist/core/issue-grading.d.ts +9 -0
- package/dist/core/issue-grading.js +9 -0
- package/dist/core/issue-verification.d.ts +39 -0
- package/dist/core/issue-verification.js +48 -0
- package/dist/core/pagination.d.ts +27 -0
- package/dist/core/pagination.js +23 -5
- package/dist/core/pr-comments-fetcher.d.ts +7 -0
- package/dist/core/pr-comments-fetcher.js +19 -8
- package/dist/core/pr-monitor.d.ts +2 -0
- package/dist/core/pr-monitor.js +26 -9
- package/dist/core/repo-score-manager.d.ts +2 -2
- package/dist/core/repo-score-manager.js +3 -3
- package/dist/core/repo-vet.d.ts +2 -2
- package/dist/core/repo-vet.js +1 -1
- package/dist/core/review-analysis.d.ts +19 -0
- package/dist/core/review-analysis.js +28 -0
- package/dist/core/state-schema.d.ts +43 -6
- package/dist/core/state-schema.js +81 -4
- package/dist/core/state.d.ts +36 -5
- package/dist/core/state.js +177 -28
- package/dist/core/strategy.js +6 -5
- package/dist/core/types.d.ts +8 -0
- package/dist/core/untrusted-content.d.ts +45 -0
- package/dist/core/untrusted-content.js +54 -0
- package/dist/formatters/json.d.ts +120 -12
- package/dist/formatters/json.js +55 -2
- package/package.json +2 -2
- package/dist/commands/shelve.d.ts +0 -45
- package/dist/commands/shelve.js +0 -54
|
@@ -7,7 +7,8 @@ import { adaptScoutLinkedPR, buildCandidateLinkedPR, createAutopilotScout } from
|
|
|
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';
|
|
10
|
-
import {
|
|
10
|
+
import { ValidationError } from '../core/errors.js';
|
|
11
|
+
import { getStateManager, classifyLinkedPR, getOctokit, requireGitHubToken, parseGitHubUrl, verifyIssuesBatch, } from '../core/index.js';
|
|
11
12
|
const UNKNOWN_GRADE = computeSuccessGrade({ avgResponseDays: null, mergeRate: null, daysSinceLastCommit: null });
|
|
12
13
|
const KNOWN_SKIP_REASONS = new Set([
|
|
13
14
|
'issue_closed',
|
|
@@ -93,13 +94,64 @@ export function classifyListStatus(vetResult, skipReason) {
|
|
|
93
94
|
return 'still_available';
|
|
94
95
|
}
|
|
95
96
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
* Map a deterministic verify-issue verdict onto the vet-list status taxonomy
|
|
98
|
+
* (#1494). This is the authoritative path: the GraphQL verdict knows the
|
|
99
|
+
* closing-vs-mention distinction (#1353) that scout's substring heuristic
|
|
100
|
+
* cannot, so `at_risk` (mention only) no longer collapses into `claimed`.
|
|
101
|
+
*/
|
|
102
|
+
export function listStatusFromVerdict(verdict) {
|
|
103
|
+
switch (verdict) {
|
|
104
|
+
case 'closed': {
|
|
105
|
+
return 'closed';
|
|
106
|
+
}
|
|
107
|
+
case 'own-open-pr': {
|
|
108
|
+
return 'own_open_pr';
|
|
109
|
+
}
|
|
110
|
+
case 'taken': {
|
|
111
|
+
return 'claimed';
|
|
112
|
+
}
|
|
113
|
+
case 'at-risk': {
|
|
114
|
+
return 'at_risk';
|
|
115
|
+
}
|
|
116
|
+
case 'available': {
|
|
117
|
+
return 'still_available';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Verdicts that conclusively rule an issue out as a new opportunity. For these
|
|
123
|
+
* we skip scout's grade/health vet entirely — there is nothing to grade — and
|
|
124
|
+
* build the result row straight from the verification.
|
|
102
125
|
*/
|
|
126
|
+
const RULED_OUT_VERDICTS = new Set(['closed', 'taken', 'own-open-pr']);
|
|
127
|
+
/** The per-row `verification` sub-object: the authoritative facts behind the
|
|
128
|
+
* status, projected from the full {@link IssueVerification}. */
|
|
129
|
+
function toVerificationSummary(v) {
|
|
130
|
+
return {
|
|
131
|
+
verdict: v.verdict,
|
|
132
|
+
verdictReason: v.verdictReason,
|
|
133
|
+
state: v.state,
|
|
134
|
+
stateReason: v.stateReason,
|
|
135
|
+
assignees: v.assignees,
|
|
136
|
+
linkedPRs: v.linkedPRs,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Minimal VetOutput for a ruled-out issue (no scout vet was run). The
|
|
140
|
+
* verification carries the real reason; scout-derived fields are left empty. */
|
|
141
|
+
function ruledOutVetOutput(v) {
|
|
142
|
+
return {
|
|
143
|
+
issue: { repo: `${v.owner}/${v.repo}`, number: v.number, title: v.title, url: v.url, labels: [] },
|
|
144
|
+
recommendation: 'skip',
|
|
145
|
+
reasonsToApprove: [],
|
|
146
|
+
reasonsToSkip: [v.verdictReason],
|
|
147
|
+
projectHealth: {},
|
|
148
|
+
vettingResult: {},
|
|
149
|
+
grade: UNKNOWN_GRADE,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function toMessage(error) {
|
|
153
|
+
return error instanceof Error ? error.message : String(error);
|
|
154
|
+
}
|
|
103
155
|
export async function runVetList(options = {}) {
|
|
104
156
|
const concurrency = options.concurrency ?? 5;
|
|
105
157
|
// 1. Find and parse the issue list
|
|
@@ -115,75 +167,146 @@ export async function runVetList(options = {}) {
|
|
|
115
167
|
if (parsed.available.length === 0) {
|
|
116
168
|
return {
|
|
117
169
|
results: [],
|
|
118
|
-
summary: {
|
|
170
|
+
summary: {
|
|
171
|
+
total: 0,
|
|
172
|
+
stillAvailable: 0,
|
|
173
|
+
claimed: 0,
|
|
174
|
+
closed: 0,
|
|
175
|
+
hasPR: 0,
|
|
176
|
+
hasStalledPR: 0,
|
|
177
|
+
atRisk: 0,
|
|
178
|
+
ownOpenPr: 0,
|
|
179
|
+
errors: 0,
|
|
180
|
+
},
|
|
119
181
|
};
|
|
120
182
|
}
|
|
121
|
-
// 2. Vet each available issue in parallel with concurrency limit
|
|
122
|
-
const scout = await createAutopilotScout();
|
|
123
|
-
const results = [];
|
|
124
|
-
// Simple concurrency limiter
|
|
125
183
|
const items = parsed.available;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
184
|
+
// 2. Verify FIRST (#1494): one deterministic GraphQL check per entry, in a
|
|
185
|
+
// bounded batch. The verdict is closing-vs-mention aware, so it carries
|
|
186
|
+
// the authoritative status — and ruling issues out here means we skip
|
|
187
|
+
// scout's search-driven vet (a net REDUCTION in /search/issues calls).
|
|
188
|
+
const octokit = getOctokit(requireGitHubToken());
|
|
189
|
+
const verifyTargets = [];
|
|
190
|
+
items.forEach((item, i) => {
|
|
191
|
+
const ghUrl = parseGitHubUrl(item.url);
|
|
192
|
+
if (ghUrl && ghUrl.type === 'issues') {
|
|
193
|
+
verifyTargets.push({ index: i, params: { owner: ghUrl.owner, repo: ghUrl.repo, number: ghUrl.number } });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
const batch = await verifyIssuesBatch(octokit, verifyTargets.map((t) => t.params), { concurrency });
|
|
197
|
+
const verificationByIndex = new Map();
|
|
198
|
+
verifyTargets.forEach((t, k) => verificationByIndex.set(t.index, batch[k]));
|
|
199
|
+
// 3. Vet only the issues still in play; build each row.
|
|
200
|
+
const scout = await createAutopilotScout();
|
|
201
|
+
/** Run scout's grade/health vet for an issue (the search-driven path). */
|
|
202
|
+
async function runScoutVet(item) {
|
|
203
|
+
const candidate = await scout.vetIssue(item.url);
|
|
204
|
+
const grade = gradeFromCandidate({
|
|
205
|
+
repo: candidate.issue.repo,
|
|
206
|
+
projectHealth: candidate.projectHealth,
|
|
207
|
+
getRepoScore: (repo) => getStateManager().getRepoScore(repo),
|
|
208
|
+
});
|
|
209
|
+
const userLogin = getStateManager().getState().config.githubUsername;
|
|
210
|
+
const linkedPRClassification = classifyLinkedPR({
|
|
211
|
+
linkedPR: adaptScoutLinkedPR(candidate.vettingResult.linkedPR),
|
|
212
|
+
userLogin,
|
|
213
|
+
});
|
|
214
|
+
const linkedPR = buildCandidateLinkedPR(candidate.vettingResult.linkedPR);
|
|
215
|
+
const vetResult = {
|
|
216
|
+
issue: {
|
|
217
|
+
repo: candidate.issue.repo,
|
|
218
|
+
number: candidate.issue.number,
|
|
219
|
+
title: candidate.issue.title,
|
|
220
|
+
url: candidate.issue.url,
|
|
221
|
+
labels: candidate.issue.labels,
|
|
222
|
+
},
|
|
223
|
+
recommendation: candidate.recommendation,
|
|
224
|
+
reasonsToApprove: candidate.reasonsToApprove,
|
|
225
|
+
reasonsToSkip: candidate.reasonsToSkip,
|
|
226
|
+
projectHealth: candidate.projectHealth,
|
|
227
|
+
vettingResult: candidate.vettingResult,
|
|
228
|
+
antiLLMPolicy: candidate.antiLLMPolicy,
|
|
229
|
+
linkedPRClassification,
|
|
230
|
+
...(linkedPR ? { linkedPR } : {}),
|
|
231
|
+
slmTriage: candidate.slmTriage ?? null,
|
|
232
|
+
grade,
|
|
233
|
+
};
|
|
234
|
+
return { vetResult, skipReason: extractSkipReason(candidate) };
|
|
235
|
+
}
|
|
236
|
+
function errorRow(item, error) {
|
|
237
|
+
const message = toMessage(error);
|
|
238
|
+
return {
|
|
239
|
+
issue: { repo: item.repo, number: item.number, title: item.title, url: item.url, labels: [] },
|
|
240
|
+
recommendation: 'skip',
|
|
241
|
+
reasonsToApprove: [],
|
|
242
|
+
reasonsToSkip: [`Error: ${message}`],
|
|
243
|
+
projectHealth: {},
|
|
244
|
+
vettingResult: {},
|
|
245
|
+
grade: UNKNOWN_GRADE,
|
|
246
|
+
listStatus: 'error',
|
|
247
|
+
errorMessage: message,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function processItem(item, verification) {
|
|
251
|
+
// No authoritative verdict for this row: either verify errored, or the URL
|
|
252
|
+
// was never enrolled (parse-list also accepts PR URLs, which `verify-issue`
|
|
253
|
+
// can't classify). Either way `listStatus` falls back to scout's heuristic,
|
|
254
|
+
// so we ALWAYS tag the row with `verifyError` — the explicit signal that the
|
|
255
|
+
// status is not authoritative and must be re-verified before acting.
|
|
256
|
+
if (!verification || verification.verification === undefined) {
|
|
257
|
+
const verifyError = verification?.error;
|
|
258
|
+
// A ValidationError means the issue definitively doesn't exist (deleted,
|
|
259
|
+
// private, or the URL points at a PR). Don't re-grade it via scout — that
|
|
260
|
+
// could mislabel a dead issue as available. Report it as an error row.
|
|
261
|
+
if (verifyError instanceof ValidationError) {
|
|
262
|
+
return { ...errorRow(item, verifyError), verifyError: toMessage(verifyError) };
|
|
263
|
+
}
|
|
264
|
+
// Transient verify failure (rate limit, network, truncated timeline), or a
|
|
265
|
+
// non-issue URL that was never sent to verify. Fall back to scout's
|
|
266
|
+
// heuristic, tagged so the row is re-verified later.
|
|
267
|
+
const verifyErrorTag = {
|
|
268
|
+
verifyError: verifyError === undefined
|
|
269
|
+
? `not verified: ${item.url} is not a verifiable issue URL`
|
|
270
|
+
: toMessage(verifyError),
|
|
271
|
+
};
|
|
130
272
|
try {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
repo: candidate.issue.repo,
|
|
134
|
-
projectHealth: candidate.projectHealth,
|
|
135
|
-
getRepoScore: (repo) => getStateManager().getRepoScore(repo),
|
|
136
|
-
});
|
|
137
|
-
const userLogin = getStateManager().getState().config.githubUsername;
|
|
138
|
-
const linkedPRClassification = classifyLinkedPR({
|
|
139
|
-
linkedPR: adaptScoutLinkedPR(candidate.vettingResult.linkedPR),
|
|
140
|
-
userLogin,
|
|
141
|
-
});
|
|
142
|
-
const linkedPR = buildCandidateLinkedPR(candidate.vettingResult.linkedPR);
|
|
143
|
-
const vetResult = {
|
|
144
|
-
issue: {
|
|
145
|
-
repo: candidate.issue.repo,
|
|
146
|
-
number: candidate.issue.number,
|
|
147
|
-
title: candidate.issue.title,
|
|
148
|
-
url: candidate.issue.url,
|
|
149
|
-
labels: candidate.issue.labels,
|
|
150
|
-
},
|
|
151
|
-
recommendation: candidate.recommendation,
|
|
152
|
-
reasonsToApprove: candidate.reasonsToApprove,
|
|
153
|
-
reasonsToSkip: candidate.reasonsToSkip,
|
|
154
|
-
projectHealth: candidate.projectHealth,
|
|
155
|
-
vettingResult: candidate.vettingResult,
|
|
156
|
-
antiLLMPolicy: candidate.antiLLMPolicy,
|
|
157
|
-
linkedPRClassification,
|
|
158
|
-
...(linkedPR ? { linkedPR } : {}),
|
|
159
|
-
slmTriage: candidate.slmTriage ?? null,
|
|
160
|
-
grade,
|
|
161
|
-
};
|
|
162
|
-
results.push({
|
|
163
|
-
...vetResult,
|
|
164
|
-
listStatus: classifyListStatus(vetResult, extractSkipReason(candidate)),
|
|
165
|
-
});
|
|
273
|
+
const { vetResult, skipReason } = await runScoutVet(item);
|
|
274
|
+
return { ...vetResult, listStatus: classifyListStatus(vetResult, skipReason), ...verifyErrorTag };
|
|
166
275
|
}
|
|
167
276
|
catch (error) {
|
|
168
|
-
|
|
169
|
-
results.push({
|
|
170
|
-
issue: { repo: item.repo, number: item.number, title: item.title, url: item.url, labels: [] },
|
|
171
|
-
recommendation: 'skip',
|
|
172
|
-
reasonsToApprove: [],
|
|
173
|
-
reasonsToSkip: [`Error: ${error instanceof Error ? error.message : String(error)}`],
|
|
174
|
-
projectHealth: {},
|
|
175
|
-
vettingResult: {},
|
|
176
|
-
grade: UNKNOWN_GRADE,
|
|
177
|
-
listStatus: 'error',
|
|
178
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
179
|
-
});
|
|
277
|
+
return { ...errorRow(item, error), ...verifyErrorTag };
|
|
180
278
|
}
|
|
181
279
|
}
|
|
280
|
+
const v = verification.verification;
|
|
281
|
+
const summary = toVerificationSummary(v);
|
|
282
|
+
const listStatus = listStatusFromVerdict(v.verdict);
|
|
283
|
+
// Conclusively ruled out — skip the scout vet entirely.
|
|
284
|
+
if (RULED_OUT_VERDICTS.has(v.verdict)) {
|
|
285
|
+
return { ...ruledOutVetOutput(v), listStatus, verification: summary };
|
|
286
|
+
}
|
|
287
|
+
// available / at-risk: still worth grading. The verdict stays authoritative
|
|
288
|
+
// for listStatus; scout only supplies grade/health/recommendation.
|
|
289
|
+
try {
|
|
290
|
+
const { vetResult } = await runScoutVet(item);
|
|
291
|
+
return { ...vetResult, listStatus, verification: summary };
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
// Verify succeeded, so keep its verdict facts even though grading failed.
|
|
295
|
+
return { ...errorRow(item, error), verification: summary };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const results = new Array(items.length);
|
|
299
|
+
let index = 0;
|
|
300
|
+
async function processNext() {
|
|
301
|
+
while (index < items.length) {
|
|
302
|
+
const i = index++;
|
|
303
|
+
results[i] = await processItem(items[i], verificationByIndex.get(i));
|
|
304
|
+
}
|
|
182
305
|
}
|
|
183
|
-
// Start `concurrency` workers
|
|
306
|
+
// Start `concurrency` workers (bounds the scout-vet fan-out).
|
|
184
307
|
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => processNext());
|
|
185
308
|
await Promise.all(workers);
|
|
186
|
-
//
|
|
309
|
+
// 4. Compute summary
|
|
187
310
|
const summary = {
|
|
188
311
|
total: results.length,
|
|
189
312
|
stillAvailable: results.filter((r) => r.listStatus === 'still_available').length,
|
|
@@ -191,6 +314,8 @@ export async function runVetList(options = {}) {
|
|
|
191
314
|
closed: results.filter((r) => r.listStatus === 'closed').length,
|
|
192
315
|
hasPR: results.filter((r) => r.listStatus === 'has_pr').length,
|
|
193
316
|
hasStalledPR: results.filter((r) => r.listStatus === 'has_stalled_pr').length,
|
|
317
|
+
atRisk: results.filter((r) => r.listStatus === 'at_risk').length,
|
|
318
|
+
ownOpenPr: results.filter((r) => r.listStatus === 'own_open_pr').length,
|
|
194
319
|
errors: results.filter((r) => r.listStatus === 'error').length,
|
|
195
320
|
};
|
|
196
321
|
// 4. Prune the file if requested — remove completed/skipped/low-score items
|
|
@@ -207,6 +332,10 @@ export async function runVetList(options = {}) {
|
|
|
207
332
|
catch (error) {
|
|
208
333
|
const msg = error instanceof Error ? error.message : String(error);
|
|
209
334
|
console.error(`Warning: Failed to prune ${issueListPath}: ${msg}`);
|
|
335
|
+
// Surface the failure in the envelope (#1448): without this, a failed
|
|
336
|
+
// prune returned a plain success with pruneResult simply absent — the
|
|
337
|
+
// same shape as "prune not requested".
|
|
338
|
+
pruneResult = { removedCount: 0, error: msg };
|
|
210
339
|
}
|
|
211
340
|
}
|
|
212
341
|
return { results, summary, pruneResult };
|
|
@@ -104,6 +104,18 @@ export const CONFIG_KEY_REGISTRY = [
|
|
|
104
104
|
settableVia: 'setup',
|
|
105
105
|
valueHint: 'comma-separated list of owner/repo',
|
|
106
106
|
},
|
|
107
|
+
{
|
|
108
|
+
key: 'avoidRepos',
|
|
109
|
+
description: 'Repos (owner/repo) to softly downrank in discovery results — milder than excludeRepos (whole-list replace).',
|
|
110
|
+
settableVia: 'setup',
|
|
111
|
+
valueHint: 'comma-separated list of owner/repo',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
key: 'boostIssueTypes',
|
|
115
|
+
description: 'Issue label types to softly boost in discovery ranking (whole-list replace).',
|
|
116
|
+
settableVia: 'setup',
|
|
117
|
+
valueHint: 'comma-separated list (e.g. bug,good first issue)',
|
|
118
|
+
},
|
|
107
119
|
// ── Exclusion list mutators (config-only) ────────────────────────────
|
|
108
120
|
{
|
|
109
121
|
key: 'add-language',
|
|
@@ -147,6 +159,30 @@ export const CONFIG_KEY_REGISTRY = [
|
|
|
147
159
|
settableVia: 'config',
|
|
148
160
|
valueHint: 'org name (no slash)',
|
|
149
161
|
},
|
|
162
|
+
{
|
|
163
|
+
key: 'add-avoid-repo',
|
|
164
|
+
description: 'Append a repo (owner/repo) to the soft-downrank avoid list.',
|
|
165
|
+
settableVia: 'config',
|
|
166
|
+
valueHint: 'owner/repo',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: 'remove-avoid-repo',
|
|
170
|
+
description: 'Remove a repo from the soft-downrank avoid list.',
|
|
171
|
+
settableVia: 'config',
|
|
172
|
+
valueHint: 'owner/repo (must already be present)',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
key: 'add-boost-issue-type',
|
|
176
|
+
description: 'Append an issue label type to the discovery ranking boost list.',
|
|
177
|
+
settableVia: 'config',
|
|
178
|
+
valueHint: 'issue label (e.g. bug)',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
key: 'remove-boost-issue-type',
|
|
182
|
+
description: 'Remove an issue label type from the discovery ranking boost list.',
|
|
183
|
+
settableVia: 'config',
|
|
184
|
+
valueHint: 'issue label (must already be present)',
|
|
185
|
+
},
|
|
150
186
|
// ── Tooling ──────────────────────────────────────────────────────────
|
|
151
187
|
{
|
|
152
188
|
key: 'issueListPath',
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* re-exports them at the bottom so existing imports keep working without
|
|
15
15
|
* a sweep.
|
|
16
16
|
*/
|
|
17
|
+
import type { AttentionSummary } from './pr-attention.js';
|
|
17
18
|
import type { FetchedPR, FetchedPRStatus, StalenessTier, ActionReason, AgentState, ShelvedPRRef, ComputedRepoSignals, RepoGroup, CommentedIssue, CapacityAssessment, ActionableIssue, ActionMenu, StarFilter } from './types.js';
|
|
18
19
|
/**
|
|
19
20
|
* Statuses indicating action needed from the contributor.
|
|
@@ -38,9 +39,13 @@ export declare const STALE_STATUSES: ReadonlySet<StalenessTier>;
|
|
|
38
39
|
*
|
|
39
40
|
* @param prs - The fetched PR list to apply overrides to
|
|
40
41
|
* @param state - Current agent state containing status overrides
|
|
42
|
+
* @param failures - Optional collector (#1448): a message is pushed for each
|
|
43
|
+
* PR whose override lookup threw (that PR keeps its un-overridden status).
|
|
44
|
+
* Call sites surface these in warnings[]/partialFailures so the silently
|
|
45
|
+
* wrong status is visible in the envelope, not just stderr.
|
|
41
46
|
* @returns New PR array with overrides applied (original array is not mutated)
|
|
42
47
|
*/
|
|
43
|
-
export declare function applyStatusOverrides(prs: FetchedPR[], state: Readonly<AgentState
|
|
48
|
+
export declare function applyStatusOverrides(prs: FetchedPR[], state: Readonly<AgentState>, failures?: string[]): FetchedPR[];
|
|
44
49
|
/**
|
|
45
50
|
* Project a PR to a lightweight ShelvedPRRef for digest output.
|
|
46
51
|
* Only the fields needed for display are retained, reducing JSON payload size.
|
|
@@ -61,6 +66,22 @@ export declare function toShelvedPRRef(pr: ShelvedPRRef): ShelvedPRRef;
|
|
|
61
66
|
* @returns Star filter with minimum threshold and known counts, or undefined on first run
|
|
62
67
|
*/
|
|
63
68
|
export declare function buildStarFilter(state: Readonly<AgentState>): StarFilter | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Look up a just-merged/closed PR's `firstMaintainerResponseAt` from the
|
|
71
|
+
* previous run's persisted digest (#1461). At detection time the PR is no
|
|
72
|
+
* longer open, so it cannot be enriched without new API calls — but if it
|
|
73
|
+
* was open during a prior enriched run, the persisted digest's openPRs carry
|
|
74
|
+
* the timestamp. Best-effort: returns undefined when the PR never appeared
|
|
75
|
+
* in a persisted digest (e.g. opened and merged between runs, or the digest
|
|
76
|
+
* predates the field).
|
|
77
|
+
*
|
|
78
|
+
* Lives in core/daily-logic.ts so both ledger writers (daily Phase 3 and
|
|
79
|
+
* dashboard-data) share one lookup. Callers must read `state.lastDigest`
|
|
80
|
+
* BEFORE the current run overwrites it via setLastDigest.
|
|
81
|
+
*/
|
|
82
|
+
export declare function firstMaintainerResponseFromDigest(digest: {
|
|
83
|
+
openPRs: FetchedPR[];
|
|
84
|
+
} | undefined, url: string): string | undefined;
|
|
64
85
|
/**
|
|
65
86
|
* Group PRs by repository (#80).
|
|
66
87
|
* Ensures one agent per repo during parallel dispatch, preventing branch checkout conflicts.
|
|
@@ -108,7 +129,9 @@ export declare function collectActionableIssues(prs: FetchedPR[], lastDigestAt?:
|
|
|
108
129
|
* @param actionableIssues - Issues requiring attention
|
|
109
130
|
* @param capacity - Current capacity assessment
|
|
110
131
|
* @param commentedIssues - Issues with comment activity
|
|
132
|
+
* @param attention - Attention bucket counts; non-zero stuck-CI / dormant-followup buckets add a follow_up item
|
|
133
|
+
* @param unextractedMergeCount - Recently merged PRs whose learnings have not been extracted yet (#1463); non-zero adds an extract_learnings item
|
|
111
134
|
* @returns Action menu with context flags for orchestration
|
|
112
135
|
*/
|
|
113
|
-
export declare function computeActionMenu(actionableIssues: ActionableIssue[], capacity: CapacityAssessment, commentedIssues?: CommentedIssue[]): ActionMenu;
|
|
136
|
+
export declare function computeActionMenu(actionableIssues: ActionableIssue[], capacity: CapacityAssessment, commentedIssues?: CommentedIssue[], attention?: Pick<AttentionSummary, 'stuckCI' | 'dormantFollowup'>, unextractedMergeCount?: number): ActionMenu;
|
|
114
137
|
export { formatActionHint, formatBriefSummary, formatSummary, printDigest } from '../commands/daily-render.js';
|
package/dist/core/daily-logic.js
CHANGED
|
@@ -52,9 +52,13 @@ const VALID_OVERRIDE_STATUSES = new Set(['needs_addressing', 'waiting_on_maintai
|
|
|
52
52
|
*
|
|
53
53
|
* @param prs - The fetched PR list to apply overrides to
|
|
54
54
|
* @param state - Current agent state containing status overrides
|
|
55
|
+
* @param failures - Optional collector (#1448): a message is pushed for each
|
|
56
|
+
* PR whose override lookup threw (that PR keeps its un-overridden status).
|
|
57
|
+
* Call sites surface these in warnings[]/partialFailures so the silently
|
|
58
|
+
* wrong status is visible in the envelope, not just stderr.
|
|
55
59
|
* @returns New PR array with overrides applied (original array is not mutated)
|
|
56
60
|
*/
|
|
57
|
-
export function applyStatusOverrides(prs, state) {
|
|
61
|
+
export function applyStatusOverrides(prs, state, failures) {
|
|
58
62
|
const overrides = state.config.statusOverrides;
|
|
59
63
|
if (!overrides || Object.keys(overrides).length === 0)
|
|
60
64
|
return prs;
|
|
@@ -82,7 +86,9 @@ export function applyStatusOverrides(prs, state) {
|
|
|
82
86
|
return { ...pr, status: override.status, waitReason: undefined, actionReason: 'needs_response' };
|
|
83
87
|
}
|
|
84
88
|
catch (err) {
|
|
85
|
-
|
|
89
|
+
const message = `Failed to apply status override for ${pr.url}: ${errorMessage(err)}`;
|
|
90
|
+
warn('daily-logic', message);
|
|
91
|
+
failures?.push(message);
|
|
86
92
|
return pr;
|
|
87
93
|
}
|
|
88
94
|
});
|
|
@@ -152,6 +158,22 @@ export function buildStarFilter(state) {
|
|
|
152
158
|
return undefined;
|
|
153
159
|
return { minStars, knownStarCounts };
|
|
154
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Look up a just-merged/closed PR's `firstMaintainerResponseAt` from the
|
|
163
|
+
* previous run's persisted digest (#1461). At detection time the PR is no
|
|
164
|
+
* longer open, so it cannot be enriched without new API calls — but if it
|
|
165
|
+
* was open during a prior enriched run, the persisted digest's openPRs carry
|
|
166
|
+
* the timestamp. Best-effort: returns undefined when the PR never appeared
|
|
167
|
+
* in a persisted digest (e.g. opened and merged between runs, or the digest
|
|
168
|
+
* predates the field).
|
|
169
|
+
*
|
|
170
|
+
* Lives in core/daily-logic.ts so both ledger writers (daily Phase 3 and
|
|
171
|
+
* dashboard-data) share one lookup. Callers must read `state.lastDigest`
|
|
172
|
+
* BEFORE the current run overwrites it via setLastDigest.
|
|
173
|
+
*/
|
|
174
|
+
export function firstMaintainerResponseFromDigest(digest, url) {
|
|
175
|
+
return digest?.openPRs.find((pr) => pr.url === url)?.firstMaintainerResponseAt;
|
|
176
|
+
}
|
|
155
177
|
/**
|
|
156
178
|
* Group PRs by repository (#80).
|
|
157
179
|
* Ensures one agent per repo during parallel dispatch, preventing branch checkout conflicts.
|
|
@@ -328,9 +350,11 @@ export function collectActionableIssues(prs, lastDigestAt) {
|
|
|
328
350
|
* @param actionableIssues - Issues requiring attention
|
|
329
351
|
* @param capacity - Current capacity assessment
|
|
330
352
|
* @param commentedIssues - Issues with comment activity
|
|
353
|
+
* @param attention - Attention bucket counts; non-zero stuck-CI / dormant-followup buckets add a follow_up item
|
|
354
|
+
* @param unextractedMergeCount - Recently merged PRs whose learnings have not been extracted yet (#1463); non-zero adds an extract_learnings item
|
|
331
355
|
* @returns Action menu with context flags for orchestration
|
|
332
356
|
*/
|
|
333
|
-
export function computeActionMenu(actionableIssues, capacity, commentedIssues = []) {
|
|
357
|
+
export function computeActionMenu(actionableIssues, capacity, commentedIssues = [], attention, unextractedMergeCount) {
|
|
334
358
|
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
|
|
335
359
|
const items = [];
|
|
336
360
|
const hasActionableIssues = actionableIssues.length > 0;
|
|
@@ -355,6 +379,37 @@ export function computeActionMenu(actionableIssues, capacity, commentedIssues =
|
|
|
355
379
|
description: 'Maintainers responded to your comments on issues',
|
|
356
380
|
});
|
|
357
381
|
}
|
|
382
|
+
// Follow-up nudges (#1462) — the stuck_ci / dormant_followup buckets from
|
|
383
|
+
// summarizeAttentionBuckets get a menu path to workflows/dormant-pr-follow-up.md
|
|
384
|
+
// instead of only a headline count. Emitted only when a bucket is non-zero,
|
|
385
|
+
// so menus without stuck/dormant PRs are unchanged.
|
|
386
|
+
const stuckCI = attention?.stuckCI ?? 0;
|
|
387
|
+
const dormantFollowup = attention?.dormantFollowup ?? 0;
|
|
388
|
+
if (stuckCI > 0 || dormantFollowup > 0) {
|
|
389
|
+
const parts = [];
|
|
390
|
+
if (stuckCI > 0)
|
|
391
|
+
parts.push(`${stuckCI} stuck-CI`);
|
|
392
|
+
if (dormantFollowup > 0)
|
|
393
|
+
parts.push(`${dormantFollowup} dormant`);
|
|
394
|
+
const total = stuckCI + dormantFollowup;
|
|
395
|
+
items.push({
|
|
396
|
+
key: 'follow_up',
|
|
397
|
+
label: `Follow up on ${parts.join(' and ')} PR${total === 1 ? '' : 's'}`,
|
|
398
|
+
description: 'Waiting PRs that may need a nudge (pending CI or no review activity)',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
// Extract-learnings nudge (#1463) — recently merged PRs whose ledger
|
|
402
|
+
// entries carry no learningsExtractedAt stamp get a menu path to
|
|
403
|
+
// workflows/extract-learnings.md. The nudge persists across runs for the
|
|
404
|
+
// recently-merged window until the extraction runs, then self-clears.
|
|
405
|
+
const unextracted = unextractedMergeCount ?? 0;
|
|
406
|
+
if (unextracted > 0) {
|
|
407
|
+
items.push({
|
|
408
|
+
key: 'extract_learnings',
|
|
409
|
+
label: `Extract learnings from ${unextracted} recently merged PR${unextracted === 1 ? '' : 's'}`,
|
|
410
|
+
description: 'Distill maintainer review feedback into per-repo contribution guidelines',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
358
413
|
// The orchestration layer (commands/oss.md Action Menu section) may insert issue-list
|
|
359
414
|
// options before the search item when a curated list is available.
|
|
360
415
|
const searchItem = {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One health source and one warning renderer for gist persistence (#1444).
|
|
3
|
+
*
|
|
4
|
+
* "Are we degraded?" used to be answered by three independent predicates
|
|
5
|
+
* (`config.persistence === 'gist' && !isGistMode()`, the
|
|
6
|
+
* `GistPersistenceStatus` union, `isGistMode() && isGistDegraded()`), each
|
|
7
|
+
* re-rendering its own warning prose. `StateManager.getGistHealth()` is now
|
|
8
|
+
* the single predicate for live-manager surfaces (daily warnings, dashboard
|
|
9
|
+
* probes), and {@link renderGistWarning} is the single prose renderer for
|
|
10
|
+
* every surface (CLI envelope, MCP injection, bootstrapGistBestEffort,
|
|
11
|
+
* daily warnings).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Degradation causes a live StateManager can attest to from its own fields
|
|
15
|
+
* (see `StateManager.getGistHealth()`):
|
|
16
|
+
*
|
|
17
|
+
* - `configured-but-local`: config says `persistence: 'gist'` but this
|
|
18
|
+
* process's manager has no gist store — init never ran here, or fell back
|
|
19
|
+
* to a local-only manager. Mutations save locally and will not sync.
|
|
20
|
+
* - `bootstrap-degraded`: the manager IS gist-backed, but the bootstrap fell
|
|
21
|
+
* back to the local cache and the store is disarmed (#1443) — reads may be
|
|
22
|
+
* stale and pushes fail.
|
|
23
|
+
*/
|
|
24
|
+
export type GistHealthDegradedCause = 'configured-but-local' | 'bootstrap-degraded';
|
|
25
|
+
/**
|
|
26
|
+
* Causes {@link renderGistWarning} can name. Superset of
|
|
27
|
+
* {@link GistHealthDegradedCause}: the process-level bootstrap outcomes are
|
|
28
|
+
* observed by config-peek paths (`ensureGistPersistence`, the MCP init memo)
|
|
29
|
+
* that run before or around manager creation, where there is no
|
|
30
|
+
* StateManager to ask:
|
|
31
|
+
*
|
|
32
|
+
* - `no-token`: gist is configured but no GitHub token was available.
|
|
33
|
+
* - `state-unreadable`: the state file could not be read for this attempt.
|
|
34
|
+
* - `init-fallback`: init resolved degraded — `ensureGistPersistence`
|
|
35
|
+
* reported `'degraded'` (transient network fallback, or a #1443 degraded
|
|
36
|
+
* bootstrap, which it deliberately conflates at that layer).
|
|
37
|
+
*/
|
|
38
|
+
export type GistWarningCause = GistHealthDegradedCause | 'no-token' | 'state-unreadable' | 'init-fallback';
|
|
39
|
+
/**
|
|
40
|
+
* Snapshot of gist persistence health from the one source of truth
|
|
41
|
+
* (`StateManager.getGistHealth()`), shaped per #1444.
|
|
42
|
+
*/
|
|
43
|
+
export interface GistHealth {
|
|
44
|
+
/** `'gist'` when the manager is gist-backed (a GistStateStore is attached),
|
|
45
|
+
* `'local'` otherwise — regardless of what the config asks for. */
|
|
46
|
+
mode: 'local' | 'gist';
|
|
47
|
+
/** `null` when healthy: either genuinely local (config agrees) or
|
|
48
|
+
* gist-backed with an armed store. Non-null when this process is not
|
|
49
|
+
* reliably syncing to the Gist. */
|
|
50
|
+
degraded: null | {
|
|
51
|
+
cause: GistHealthDegradedCause;
|
|
52
|
+
/** ISO timestamp when the degradation was first observed, when known
|
|
53
|
+
* (a degraded bootstrap seeds the staleness marker; a
|
|
54
|
+
* configured-but-local manager has no marker to date it by). */
|
|
55
|
+
since?: string;
|
|
56
|
+
/** Whether a later init/recovery attempt can heal this without user
|
|
57
|
+
* action. Both manager-level causes are recoverable by re-running
|
|
58
|
+
* `ensureGistPersistence` with a token (#1415/#1443); a PERMANENT halt
|
|
59
|
+
* (e.g. token lacking the gist scope) surfaces as a thrown
|
|
60
|
+
* ConfigurationError at the surface that attempted recovery — the
|
|
61
|
+
* manager itself cannot observe it, so it never reports false here. */
|
|
62
|
+
recoverable: boolean;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render the one gist-degradation warning shown by every surface (#1444).
|
|
67
|
+
*
|
|
68
|
+
* Accepts either a known {@link GistWarningCause} or `{ reason }` for the
|
|
69
|
+
* hard-error paths (ConfigurationError repair carve-outs) whose reason text
|
|
70
|
+
* is built from the underlying error.
|
|
71
|
+
*
|
|
72
|
+
* @param cause - Known cause, or a free-form reason for hard init errors.
|
|
73
|
+
* @param opts.retryHint - Optional trailing sentence describing the
|
|
74
|
+
* surface-specific retry behavior (e.g. the MCP's "Will retry on the next
|
|
75
|
+
* tool call.").
|
|
76
|
+
*/
|
|
77
|
+
export declare function renderGistWarning(cause: GistWarningCause | {
|
|
78
|
+
reason: string;
|
|
79
|
+
}, opts?: {
|
|
80
|
+
retryHint?: string;
|
|
81
|
+
}): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One health source and one warning renderer for gist persistence (#1444).
|
|
3
|
+
*
|
|
4
|
+
* "Are we degraded?" used to be answered by three independent predicates
|
|
5
|
+
* (`config.persistence === 'gist' && !isGistMode()`, the
|
|
6
|
+
* `GistPersistenceStatus` union, `isGistMode() && isGistDegraded()`), each
|
|
7
|
+
* re-rendering its own warning prose. `StateManager.getGistHealth()` is now
|
|
8
|
+
* the single predicate for live-manager surfaces (daily warnings, dashboard
|
|
9
|
+
* probes), and {@link renderGistWarning} is the single prose renderer for
|
|
10
|
+
* every surface (CLI envelope, MCP injection, bootstrapGistBestEffort,
|
|
11
|
+
* daily warnings).
|
|
12
|
+
*/
|
|
13
|
+
/** Cause → human reason, the MCP's former `gistWarningText` mapping promoted
|
|
14
|
+
* to core (#1444). Keep these one-line and append-only: surfaces interpolate
|
|
15
|
+
* them mid-sentence. */
|
|
16
|
+
const GIST_DEGRADED_REASON = {
|
|
17
|
+
'no-token': 'no GitHub token was available',
|
|
18
|
+
'state-unreadable': 'the state file could not be read',
|
|
19
|
+
'init-fallback': 'initialization hit a transient network failure',
|
|
20
|
+
'configured-but-local': 'this process is running local-only (Gist init has not succeeded)',
|
|
21
|
+
'bootstrap-degraded': 'the Gist bootstrap fell back to the local cache (reads may be stale and pushes are failing)',
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Render the one gist-degradation warning shown by every surface (#1444).
|
|
25
|
+
*
|
|
26
|
+
* Accepts either a known {@link GistWarningCause} or `{ reason }` for the
|
|
27
|
+
* hard-error paths (ConfigurationError repair carve-outs) whose reason text
|
|
28
|
+
* is built from the underlying error.
|
|
29
|
+
*
|
|
30
|
+
* @param cause - Known cause, or a free-form reason for hard init errors.
|
|
31
|
+
* @param opts.retryHint - Optional trailing sentence describing the
|
|
32
|
+
* surface-specific retry behavior (e.g. the MCP's "Will retry on the next
|
|
33
|
+
* tool call.").
|
|
34
|
+
*/
|
|
35
|
+
export function renderGistWarning(cause, opts = {}) {
|
|
36
|
+
const reason = typeof cause === 'string' ? GIST_DEGRADED_REASON[cause] : cause.reason;
|
|
37
|
+
return (`Gist persistence is configured but ${reason}; writes are LOCAL-ONLY and may be ` +
|
|
38
|
+
`overwritten by the next successful Gist sync.${opts.retryHint ? ` ${opts.retryHint}` : ''}`);
|
|
39
|
+
}
|
|
@@ -232,7 +232,9 @@ export declare class GistStateStore {
|
|
|
232
232
|
push(): Promise<boolean>;
|
|
233
233
|
/**
|
|
234
234
|
* Re-fetch the Gist and update the in-memory cache.
|
|
235
|
-
* Throttled to at most once per 30 seconds
|
|
235
|
+
* Throttled to at most once per 30 seconds — ATTEMPTS, not just successes:
|
|
236
|
+
* a failed fetch stamps the throttle too (#1443), so an outage does not
|
|
237
|
+
* turn every SPA poll into an immediate full re-fetch.
|
|
236
238
|
*
|
|
237
239
|
* Returns a discriminated union so callers can tell apart the four
|
|
238
240
|
* outcomes that previously collapsed into a single boolean (#1209 L9):
|