@oss-autopilot/core 3.11.0 → 3.12.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 +29 -0
- package/dist/cli.bundle.cjs +144 -91
- package/dist/commands/daily-render.d.ts +2 -1
- package/dist/commands/daily-render.js +8 -2
- package/dist/commands/daily.d.ts +3 -1
- package/dist/commands/daily.js +7 -2
- package/dist/commands/dashboard-data.js +14 -5
- package/dist/commands/dashboard-server.js +7 -2
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/list-move-tier.d.ts +11 -3
- package/dist/commands/list-move-tier.js +18 -7
- package/dist/commands/search.js +17 -3
- package/dist/commands/verify-issue.d.ts +20 -0
- package/dist/commands/verify-issue.js +32 -0
- package/dist/core/daily-logic.js +65 -52
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/issue-verification.d.ts +91 -0
- package/dist/core/issue-verification.js +270 -0
- package/dist/core/pr-attention.d.ts +52 -0
- package/dist/core/pr-attention.js +76 -0
- package/dist/core/types.d.ts +7 -0
- package/dist/formatters/json.d.ts +74 -1
- package/dist/formatters/json.js +43 -1
- package/package.json +2 -2
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* (daily-logic.test.ts) — pure rendering, easy to assert on string output.
|
|
16
16
|
*/
|
|
17
17
|
import type { CapacityAssessment, CommentedIssue, CommentedIssueWithResponse, DailyDigest, MaintainerActionHint } from '../core/types.js';
|
|
18
|
+
import type { AttentionSummary } from '../core/pr-attention.js';
|
|
18
19
|
/**
|
|
19
20
|
* Format a maintainer action hint as a human-readable label.
|
|
20
21
|
*/
|
|
@@ -24,7 +25,7 @@ export declare function formatActionHint(hint: MaintainerActionHint): string;
|
|
|
24
25
|
*
|
|
25
26
|
* @returns One-line status string (e.g., "3 Active PRs | 1 needs attention | 2 issue replies")
|
|
26
27
|
*/
|
|
27
|
-
export declare function formatBriefSummary(digest: DailyDigest, issueCount: number, issueResponseCount?: number): string;
|
|
28
|
+
export declare function formatBriefSummary(digest: DailyDigest, issueCount: number, issueResponseCount?: number, attention?: Pick<AttentionSummary, 'stuckCI' | 'dormantFollowup'>): string;
|
|
28
29
|
/**
|
|
29
30
|
* Format the full dashboard summary as markdown.
|
|
30
31
|
* Used in JSON output for Claude to display verbatim — includes all PR sections,
|
|
@@ -42,10 +42,16 @@ export function formatActionHint(hint) {
|
|
|
42
42
|
*
|
|
43
43
|
* @returns One-line status string (e.g., "3 Active PRs | 1 needs attention | 2 issue replies")
|
|
44
44
|
*/
|
|
45
|
-
export function formatBriefSummary(digest, issueCount, issueResponseCount = 0) {
|
|
45
|
+
export function formatBriefSummary(digest, issueCount, issueResponseCount = 0, attention) {
|
|
46
46
|
const attentionText = issueCount > 0 ? `${issueCount} need${issueCount === 1 ? 's' : ''} attention` : 'all on track';
|
|
47
47
|
const issueReplyText = issueResponseCount > 0 ? ` | ${issueResponseCount} issue repl${issueResponseCount === 1 ? 'y' : 'ies'}` : '';
|
|
48
|
-
|
|
48
|
+
// #1352: the watch-style buckets are appended only when non-zero — they're
|
|
49
|
+
// ping/nudge workflows, not part of the headline "need attention" count.
|
|
50
|
+
const stuckText = attention && attention.stuckCI > 0 ? ` | ${attention.stuckCI} stuck CI` : '';
|
|
51
|
+
const dormantText = attention && attention.dormantFollowup > 0
|
|
52
|
+
? ` | ${attention.dormantFollowup} dormant follow-up${attention.dormantFollowup === 1 ? '' : 's'}`
|
|
53
|
+
: '';
|
|
54
|
+
return `\u{1F4CA} ${digest.summary.totalActivePRs} Active PRs | ${attentionText}${issueReplyText}${stuckText}${dormantText}`;
|
|
49
55
|
}
|
|
50
56
|
/**
|
|
51
57
|
* Format the full dashboard summary as markdown.
|
package/dist/commands/daily.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Domain logic lives in src/core/daily-logic.ts; this file is a thin
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
|
-
import { type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup } from '../core/index.js';
|
|
9
|
+
import { type AttentionSummary, type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup } from '../core/index.js';
|
|
10
10
|
import { type StrategyResult } from '../core/strategy.js';
|
|
11
11
|
import { type DailyOutput, type DailyWarning, type CapacityAssessment, type ActionableIssue, type ActionMenu } from '../formatters/json.js';
|
|
12
12
|
export { applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
@@ -23,6 +23,8 @@ export interface DailyCheckResult {
|
|
|
23
23
|
summary: string;
|
|
24
24
|
briefSummary: string;
|
|
25
25
|
actionableIssues: ActionableIssue[];
|
|
26
|
+
/** Unified attention-bucket counts over active PRs (#1352). */
|
|
27
|
+
attention: AttentionSummary;
|
|
26
28
|
actionMenu: ActionMenu;
|
|
27
29
|
commentedIssues: CommentedIssue[];
|
|
28
30
|
repoGroups: RepoGroup[];
|
package/dist/commands/daily.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Domain logic lives in src/core/daily-logic.ts; this file is a thin
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
|
-
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
|
|
9
|
+
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, summarizeAttentionBuckets, } from '../core/index.js';
|
|
10
10
|
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
11
11
|
import { wrapUntrustedContent } from '../core/untrusted-content.js';
|
|
12
12
|
import { computeStrategy, shouldComputeStrategy } from '../core/strategy.js';
|
|
@@ -420,7 +420,10 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
420
420
|
// Auto-undismiss mutations are auto-saved by undismissIssue()
|
|
421
421
|
const actionableIssues = collectActionableIssues(activePRs, previousLastDigestAt);
|
|
422
422
|
digest.summary.totalNeedingAttention = actionableIssues.length;
|
|
423
|
-
|
|
423
|
+
// #1352: one classifier for all attention surfaces — the dashboard stamps
|
|
424
|
+
// the same buckets per-PR, so the headline counts cannot diverge.
|
|
425
|
+
const attention = summarizeAttentionBuckets(activePRs);
|
|
426
|
+
const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length, attention);
|
|
424
427
|
const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues);
|
|
425
428
|
const repoGroups = groupPRsByRepo(activePRs);
|
|
426
429
|
// Periodic strategy snapshot (#1270 Step 2). Cadence-gated to fire every
|
|
@@ -447,6 +450,7 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
447
450
|
summary,
|
|
448
451
|
briefSummary,
|
|
449
452
|
actionableIssues,
|
|
453
|
+
attention,
|
|
450
454
|
actionMenu,
|
|
451
455
|
commentedIssues: filteredCommentedIssues,
|
|
452
456
|
repoGroups,
|
|
@@ -502,6 +506,7 @@ export function toDailyOutput(result) {
|
|
|
502
506
|
summary: result.summary,
|
|
503
507
|
briefSummary: result.briefSummary,
|
|
504
508
|
actionableIssues: compactActionableIssues(result.actionableIssues),
|
|
509
|
+
attention: result.attention,
|
|
505
510
|
actionMenu: result.actionMenu,
|
|
506
511
|
commentedIssues: result.commentedIssues.map(fenceCommentedIssue),
|
|
507
512
|
repoGroups: compactRepoGroups(result.repoGroups),
|
|
@@ -3,7 +3,7 @@
|
|
|
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 { getStateManager, PRMonitor, IssueConversationMonitor, getOctokit } from '../core/index.js';
|
|
6
|
+
import { getStateManager, PRMonitor, IssueConversationMonitor, getOctokit, CRITICAL_STATUSES } from '../core/index.js';
|
|
7
7
|
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
8
8
|
import { warn } from '../core/logger.js';
|
|
9
9
|
import { emptyPRCountsResult, fetchMergedPRsSince, fetchClosedPRsSince } from '../core/github-stats.js';
|
|
@@ -39,9 +39,16 @@ export function buildDashboardStats(digest, state, storedMergedCount, storedClos
|
|
|
39
39
|
* A PR is shelved for display when the user explicitly shelved it, or the
|
|
40
40
|
* dormant-auto-shelve rule applies (dormant + not needing attention). Mirrors
|
|
41
41
|
* the partition built in fetchDashboardData (see the `freshShelved` filter).
|
|
42
|
+
*
|
|
43
|
+
* #1352: an explicitly shelved PR that turns critical is NOT shelved for
|
|
44
|
+
* display — the daily check auto-unshelves it (`CRITICAL_STATUSES`), so the
|
|
45
|
+
* dashboard must agree immediately rather than diverging from the CLI's
|
|
46
|
+
* headline count until the next daily run.
|
|
42
47
|
*/
|
|
43
48
|
function isShelvedForDisplay(pr, explicitlyShelved) {
|
|
44
|
-
|
|
49
|
+
if (CRITICAL_STATUSES.has(pr.status))
|
|
50
|
+
return false;
|
|
51
|
+
return explicitlyShelved.has(pr.url) || pr.stalenessTier === 'dormant';
|
|
45
52
|
}
|
|
46
53
|
/**
|
|
47
54
|
* Re-derive `digest.shelvedPRs` and `summary.totalActivePRs` from the CURRENT
|
|
@@ -254,10 +261,12 @@ export async function fetchDashboardData(token) {
|
|
|
254
261
|
const { monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed } = closedResult;
|
|
255
262
|
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
256
263
|
const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
257
|
-
// Apply shelve partitioning for display (auto-unshelve only runs in daily check)
|
|
258
|
-
// Dormant PRs are treated as shelved unless they need addressing
|
|
264
|
+
// Apply shelve partitioning for display (auto-unshelve only runs in daily check).
|
|
265
|
+
// Dormant PRs are treated as shelved unless they need addressing, and a
|
|
266
|
+
// critical PR is never display-shelved (#1352) — mirrors the daily
|
|
267
|
+
// check's CRITICAL_STATUSES auto-unshelve so headline counts agree.
|
|
259
268
|
const shelvedUrls = new Set(stateManager.getState().config.shelvedPRUrls || []);
|
|
260
|
-
const freshShelved = prs.filter((pr) => shelvedUrls.has(pr.url) ||
|
|
269
|
+
const freshShelved = prs.filter((pr) => !CRITICAL_STATUSES.has(pr.status) && (shelvedUrls.has(pr.url) || pr.stalenessTier === 'dormant'));
|
|
261
270
|
digest.shelvedPRs = freshShelved.map(toShelvedPRRef);
|
|
262
271
|
digest.autoUnshelvedPRs = [];
|
|
263
272
|
digest.summary.totalActivePRs = prs.length - freshShelved.length;
|
|
@@ -9,7 +9,7 @@ import * as http from 'node:http';
|
|
|
9
9
|
import * as fs from 'node:fs';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import * as crypto from 'node:crypto';
|
|
12
|
-
import { getStateManager, getGitHubToken, getCLIVersion, applyStatusOverrides } from '../core/index.js';
|
|
12
|
+
import { getStateManager, getGitHubToken, getCLIVersion, applyStatusOverrides, classifyAttentionBucket, } from '../core/index.js';
|
|
13
13
|
import { errorMessage, ValidationError, ConcurrencyError, GistConcurrencyError } from '../core/errors.js';
|
|
14
14
|
import { warn } from '../core/logger.js';
|
|
15
15
|
import { validateUrl, validateGitHubUrl, PR_URL_PATTERN, ISSUE_URL_PATTERN } from './validation.js';
|
|
@@ -113,7 +113,12 @@ export function buildDashboardJson(digest, state, commentedIssues, allMergedPRs,
|
|
|
113
113
|
monthlyMerged,
|
|
114
114
|
monthlyOpened,
|
|
115
115
|
monthlyClosed,
|
|
116
|
-
|
|
116
|
+
// #1352: stamp the unified attention bucket so the SPA renders the same
|
|
117
|
+
// taxonomy the CLI brief counts (single classifier, no second opinion).
|
|
118
|
+
activePRs: applyStatusOverrides(digest.openPRs || [], state).map((pr) => ({
|
|
119
|
+
...pr,
|
|
120
|
+
attentionBucket: classifyAttentionBucket(pr),
|
|
121
|
+
})),
|
|
117
122
|
// Source of truth is digest.shelvedPRs (union of explicitly-shelved URLs
|
|
118
123
|
// and dormant-non-addressing PRs auto-shelved for display). Returning
|
|
119
124
|
// only state.config.shelvedPRUrls would under-count and desync from
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export { runSearch, MAX_SEARCH_RESULTS } from './search.js';
|
|
|
29
29
|
export { runFeatures, MAX_FEATURES_RESULTS } from './features.js';
|
|
30
30
|
/** Vet a single GitHub issue for claimability (open, unassigned, no linked PRs, repo health). */
|
|
31
31
|
export { runVet } from './vet.js';
|
|
32
|
+
/** Deterministic availability check: state/stateReason + linked-PR classification (#1353, #1354). */
|
|
33
|
+
export { runVerifyIssue, type VerifyIssueOptions } from './verify-issue.js';
|
|
32
34
|
/** Re-vet all available issues in a curated issue list for freshness. */
|
|
33
35
|
export { runVetList } from './vet-list.js';
|
|
34
36
|
/** Fetch PR metadata from GitHub (informational; nothing is persisted). */
|
package/dist/commands/index.js
CHANGED
|
@@ -30,6 +30,8 @@ export { runSearch, MAX_SEARCH_RESULTS } from './search.js';
|
|
|
30
30
|
export { runFeatures, MAX_FEATURES_RESULTS } from './features.js';
|
|
31
31
|
/** Vet a single GitHub issue for claimability (open, unassigned, no linked PRs, repo health). */
|
|
32
32
|
export { runVet } from './vet.js';
|
|
33
|
+
/** Deterministic availability check: state/stateReason + linked-PR classification (#1353, #1354). */
|
|
34
|
+
export { runVerifyIssue } from './verify-issue.js';
|
|
33
35
|
/** Re-vet all available issues in a curated issue list for freshness. */
|
|
34
36
|
export { runVetList } from './vet-list.js';
|
|
35
37
|
// ── PR Management ───────────────────────────────────────────────────────────
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* issue-list markdown file. Replaces the model-driven prose rewrite that
|
|
7
7
|
* lived in /oss-search with a deterministic file manipulation.
|
|
8
8
|
*
|
|
9
|
-
* Idempotent: re-running with the same target tier is a no-op.
|
|
9
|
+
* Idempotent: re-running with the same target tier is a no-op. A URL that is
|
|
10
|
+
* not in the list at all is an error (#1355) — the command does not create
|
|
11
|
+
* entries, and callers must add the entry before moving it.
|
|
10
12
|
*
|
|
11
13
|
* No GitHub calls — pure read/transform/write of a local file.
|
|
12
14
|
*/
|
|
@@ -17,7 +19,8 @@ export interface ListMoveTierOptions {
|
|
|
17
19
|
listPath: string;
|
|
18
20
|
}
|
|
19
21
|
export interface ListMoveTierOutput {
|
|
20
|
-
/** Whether anything moved (false when the
|
|
22
|
+
/** Whether anything moved (false only when the entry was already in the
|
|
23
|
+
* target tier — a URL missing from the list entirely throws instead, #1355). */
|
|
21
24
|
moved: boolean;
|
|
22
25
|
/** Fully-resolved file path that was inspected. */
|
|
23
26
|
filePath: string;
|
|
@@ -25,7 +28,8 @@ export interface ListMoveTierOutput {
|
|
|
25
28
|
url: string;
|
|
26
29
|
/** The target tier (always normalized to one of pursue/maybe/skip). */
|
|
27
30
|
toTier: Tier;
|
|
28
|
-
/** The tier the issue was moved out of, if it had one.
|
|
31
|
+
/** The tier the issue was moved out of, if it had one. Also populated on the
|
|
32
|
+
* already-in-target no-op; absent when the source block sat under no tier header. */
|
|
29
33
|
fromTier?: string;
|
|
30
34
|
/** Number of matching entries moved. Should normally be 1; >1 means the list contained duplicate entries (all moved). */
|
|
31
35
|
count: number;
|
|
@@ -36,11 +40,15 @@ export interface ListMoveTierOutput {
|
|
|
36
40
|
* Pure transform — accepts the file content and returns the rewritten content
|
|
37
41
|
* plus a summary of what changed. Exported for unit testing.
|
|
38
42
|
*/
|
|
43
|
+
/** Discriminates the two `moved: false` outcomes of {@link moveIssueToTier}. */
|
|
44
|
+
export type MoveNoOpReason = 'not-found' | 'already-in-target';
|
|
39
45
|
export declare function moveIssueToTier(content: string, issueUrl: string, targetTier: Tier): {
|
|
40
46
|
content: string;
|
|
41
47
|
moved: boolean;
|
|
42
48
|
fromTier?: string;
|
|
43
49
|
count: number;
|
|
44
50
|
reason?: string;
|
|
51
|
+
/** Set only when `moved` is false — why nothing changed. */
|
|
52
|
+
reasonCode?: MoveNoOpReason;
|
|
45
53
|
};
|
|
46
54
|
export declare function runListMoveTier(options: ListMoveTierOptions): Promise<ListMoveTierOutput>;
|
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
* issue-list markdown file. Replaces the model-driven prose rewrite that
|
|
7
7
|
* lived in /oss-search with a deterministic file manipulation.
|
|
8
8
|
*
|
|
9
|
-
* Idempotent: re-running with the same target tier is a no-op.
|
|
9
|
+
* Idempotent: re-running with the same target tier is a no-op. A URL that is
|
|
10
|
+
* not in the list at all is an error (#1355) — the command does not create
|
|
11
|
+
* entries, and callers must add the entry before moving it.
|
|
10
12
|
*
|
|
11
13
|
* No GitHub calls — pure read/transform/write of a local file.
|
|
12
14
|
*/
|
|
13
15
|
import * as fs from 'node:fs';
|
|
14
16
|
import * as path from 'node:path';
|
|
15
|
-
import { errorMessage } from '../core/errors.js';
|
|
17
|
+
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
16
18
|
const TIER_HEADERS = {
|
|
17
19
|
pursue: '## Pursue',
|
|
18
20
|
maybe: '## Maybe',
|
|
@@ -82,17 +84,19 @@ function findTierInsertionIndex(lines, headerIndex) {
|
|
|
82
84
|
}
|
|
83
85
|
return lines.length;
|
|
84
86
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Pure transform — accepts the file content and returns the rewritten content
|
|
87
|
-
* plus a summary of what changed. Exported for unit testing.
|
|
88
|
-
*/
|
|
89
87
|
export function moveIssueToTier(content, issueUrl, targetTier) {
|
|
90
88
|
// Preserve the trailing newline if present so we don't accidentally strip it.
|
|
91
89
|
const hadTrailingNewline = content.endsWith('\n');
|
|
92
90
|
const lines = (hadTrailingNewline ? content.slice(0, -1) : content).split('\n');
|
|
93
91
|
const blocks = findIssueBlocks(lines, issueUrl);
|
|
94
92
|
if (blocks.length === 0) {
|
|
95
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
content,
|
|
95
|
+
moved: false,
|
|
96
|
+
count: 0,
|
|
97
|
+
reason: 'issue URL not found in the list',
|
|
98
|
+
reasonCode: 'not-found',
|
|
99
|
+
};
|
|
96
100
|
}
|
|
97
101
|
const targetHeader = TIER_HEADERS[targetTier];
|
|
98
102
|
// If every match is already in the target tier, no-op (idempotent).
|
|
@@ -103,6 +107,7 @@ export function moveIssueToTier(content, issueUrl, targetTier) {
|
|
|
103
107
|
fromTier: blocks[0].tier,
|
|
104
108
|
count: blocks.length,
|
|
105
109
|
reason: 'already in target tier',
|
|
110
|
+
reasonCode: 'already-in-target',
|
|
106
111
|
};
|
|
107
112
|
}
|
|
108
113
|
// Extract the blocks (highest start index first so earlier indices stay
|
|
@@ -172,6 +177,12 @@ export async function runListMoveTier(options) {
|
|
|
172
177
|
throw new Error(`Failed to read file: ${errorMessage(error)}`, { cause: error });
|
|
173
178
|
}
|
|
174
179
|
const result = moveIssueToTier(content, options.issueUrl, options.tier);
|
|
180
|
+
// #1355: a missing entry is a caller error, not a quiet success. Idempotent
|
|
181
|
+
// re-runs (already-in-target) still resolve normally.
|
|
182
|
+
if (result.reasonCode === 'not-found') {
|
|
183
|
+
throw new ValidationError(`Issue URL not found in the list: ${options.issueUrl} (${filePath}). ` +
|
|
184
|
+
'Add the entry to the list first, then re-run list-move-tier.');
|
|
185
|
+
}
|
|
175
186
|
if (result.moved) {
|
|
176
187
|
try {
|
|
177
188
|
fs.writeFileSync(filePath, result.content, 'utf8');
|
package/dist/commands/search.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Search command
|
|
3
3
|
* Searches for new issues to work on via @oss-scout/core
|
|
4
4
|
*/
|
|
5
|
-
import { buildCandidateLinkedPR, createAutopilotScout } from './scout-bridge.js';
|
|
6
|
-
import { getStateManager } from '../core/index.js';
|
|
5
|
+
import { adaptScoutLinkedPR, buildCandidateLinkedPR, createAutopilotScout } from './scout-bridge.js';
|
|
6
|
+
import { classifyLinkedPR, getStateManager } from '../core/index.js';
|
|
7
7
|
import { gradeFromCandidate } from '../core/issue-grading.js';
|
|
8
8
|
import { computeStrategy } from '../core/strategy.js';
|
|
9
9
|
import { debug, warn } from '../core/logger.js';
|
|
@@ -64,8 +64,21 @@ export async function runSearch(options) {
|
|
|
64
64
|
preferLanguages,
|
|
65
65
|
preferRepos,
|
|
66
66
|
});
|
|
67
|
+
// #1354: never surface issues the user already has an open PR for. Uses
|
|
68
|
+
// scout's structured linked-PR metadata when present; candidates without it
|
|
69
|
+
// pass through (the issue-scout agent re-checks via verify-issue anyway).
|
|
70
|
+
// Empty login means "can't prove any PR is the user's own" — nothing hidden.
|
|
71
|
+
const userLogin = stateManager.getState().config?.githubUsername ?? '';
|
|
72
|
+
if (userLogin === '') {
|
|
73
|
+
warn(MODULE, 'githubUsername not configured — the own-PR filter (#1354) cannot run; hiddenOwnPRCount will be 0');
|
|
74
|
+
}
|
|
75
|
+
const visibleCandidates = result.candidates.filter((c) => classifyLinkedPR({ linkedPR: adaptScoutLinkedPR(c.vettingResult?.linkedPR), userLogin }) !== 'user_open');
|
|
76
|
+
const hiddenOwnPRCount = result.candidates.length - visibleCandidates.length;
|
|
77
|
+
if (hiddenOwnPRCount > 0) {
|
|
78
|
+
debug(MODULE, `Hid ${hiddenOwnPRCount} candidate(s) with the user's own open PR (#1354)`);
|
|
79
|
+
}
|
|
67
80
|
const searchOutput = {
|
|
68
|
-
candidates:
|
|
81
|
+
candidates: visibleCandidates.map((c) => {
|
|
69
82
|
const repoScoreRecord = stateManager.getRepoScore(c.issue.repo);
|
|
70
83
|
// Scout's `search` does not emit per-candidate projectHealth (only
|
|
71
84
|
// `vetIssue` does). Pass a sentinel `checkFailed: true` so the grader
|
|
@@ -119,6 +132,7 @@ export async function runSearch(options) {
|
|
|
119
132
|
}),
|
|
120
133
|
excludedRepos: result.excludedRepos,
|
|
121
134
|
aiPolicyBlocklist: result.aiPolicyBlocklist,
|
|
135
|
+
hiddenOwnPRCount,
|
|
122
136
|
};
|
|
123
137
|
if (result.rateLimitWarning) {
|
|
124
138
|
searchOutput.rateLimitWarning = result.rateLimitWarning;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-issue command (#1353, #1354).
|
|
3
|
+
*
|
|
4
|
+
* Deterministic availability check for one GitHub issue: state/stateReason,
|
|
5
|
+
* assignees, and linked PRs classified as `closing` vs `cross-referenced`,
|
|
6
|
+
* with the authenticated user's own PRs flagged. One GraphQL round-trip,
|
|
7
|
+
* no scoring, no scout heuristics — this is the ground truth the
|
|
8
|
+
* issue-scout agent consumes BEFORE any judgment-based vetting.
|
|
9
|
+
*/
|
|
10
|
+
import { type IssueVerification } from '../core/index.js';
|
|
11
|
+
export interface VerifyIssueOptions {
|
|
12
|
+
issueUrl: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Verify a GitHub issue's real availability.
|
|
16
|
+
*
|
|
17
|
+
* @throws {ValidationError} If the URL is not a valid GitHub issue URL or
|
|
18
|
+
* the issue does not exist.
|
|
19
|
+
*/
|
|
20
|
+
export declare function runVerifyIssue(options: VerifyIssueOptions): Promise<IssueVerification>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-issue command (#1353, #1354).
|
|
3
|
+
*
|
|
4
|
+
* Deterministic availability check for one GitHub issue: state/stateReason,
|
|
5
|
+
* assignees, and linked PRs classified as `closing` vs `cross-referenced`,
|
|
6
|
+
* with the authenticated user's own PRs flagged. One GraphQL round-trip,
|
|
7
|
+
* no scoring, no scout heuristics — this is the ground truth the
|
|
8
|
+
* issue-scout agent consumes BEFORE any judgment-based vetting.
|
|
9
|
+
*/
|
|
10
|
+
import { fetchIssueVerification, getOctokit, parseGitHubUrl, requireGitHubToken, } from '../core/index.js';
|
|
11
|
+
import { ValidationError } from '../core/errors.js';
|
|
12
|
+
import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
|
|
13
|
+
/**
|
|
14
|
+
* Verify a GitHub issue's real availability.
|
|
15
|
+
*
|
|
16
|
+
* @throws {ValidationError} If the URL is not a valid GitHub issue URL or
|
|
17
|
+
* the issue does not exist.
|
|
18
|
+
*/
|
|
19
|
+
export async function runVerifyIssue(options) {
|
|
20
|
+
validateUrl(options.issueUrl);
|
|
21
|
+
validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue');
|
|
22
|
+
const parsed = parseGitHubUrl(options.issueUrl);
|
|
23
|
+
if (!parsed || parsed.type !== 'issues') {
|
|
24
|
+
throw new ValidationError(`Not a parseable GitHub issue URL: ${options.issueUrl}`);
|
|
25
|
+
}
|
|
26
|
+
const octokit = getOctokit(requireGitHubToken());
|
|
27
|
+
return fetchIssueVerification(octokit, {
|
|
28
|
+
owner: parsed.owner,
|
|
29
|
+
repo: parsed.repo,
|
|
30
|
+
number: parsed.number,
|
|
31
|
+
});
|
|
32
|
+
}
|
package/dist/core/daily-logic.js
CHANGED
|
@@ -248,62 +248,75 @@ export function collectActionableIssues(prs, lastDigestAt) {
|
|
|
248
248
|
'merge_conflict',
|
|
249
249
|
'incomplete_checklist',
|
|
250
250
|
];
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
251
|
+
// #1352: every needs_addressing PR produces exactly one entry, including
|
|
252
|
+
// PRs whose actionReason is missing or outside reasonOrder (the defensive
|
|
253
|
+
// default below). This keeps the CLI brief's count equal to the dashboard's
|
|
254
|
+
// needs_attention bucket by construction — previously an unmapped reason
|
|
255
|
+
// silently dropped the PR from the brief while the dashboard still counted
|
|
256
|
+
// it. Unmapped reasons sort last.
|
|
257
|
+
const sortedPRs = [...actionPRs].sort((a, b) => {
|
|
258
|
+
const rank = (pr) => {
|
|
259
|
+
const i = reasonOrder.indexOf(pr.actionReason);
|
|
260
|
+
return i === -1 ? reasonOrder.length : i;
|
|
261
|
+
};
|
|
262
|
+
return rank(a) - rank(b);
|
|
263
|
+
});
|
|
264
|
+
for (const pr of sortedPRs) {
|
|
265
|
+
if (pr.actionReason === undefined) {
|
|
266
|
+
warn('daily-logic', `needs_addressing PR ${pr.url} has no actionReason — defaulting to needs_response`);
|
|
267
|
+
}
|
|
268
|
+
const reason = pr.actionReason ?? 'needs_response';
|
|
269
|
+
let label;
|
|
270
|
+
let type;
|
|
271
|
+
switch (reason) {
|
|
272
|
+
case 'needs_response': {
|
|
273
|
+
label = '[Needs Response]';
|
|
274
|
+
type = 'needs_response';
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case 'needs_changes': {
|
|
278
|
+
label = '[Needs Changes]';
|
|
279
|
+
type = 'needs_changes';
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
case 'failing_ci': {
|
|
283
|
+
const checkInfo = pr.failingCheckNames.length > 0 ? ` (${pr.failingCheckNames.join(', ')})` : '';
|
|
284
|
+
label = `[CI Failing${checkInfo}]`;
|
|
285
|
+
type = 'ci_failing';
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
case 'merge_conflict': {
|
|
289
|
+
label = '[Merge Conflict]';
|
|
290
|
+
type = 'merge_conflict';
|
|
291
|
+
break;
|
|
293
292
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
warn('daily-logic', `Invalid createdAt "${pr.createdAt}" for PR ${pr.url}, assuming new contribution`);
|
|
300
|
-
isNewContribution = true;
|
|
293
|
+
case 'incomplete_checklist': {
|
|
294
|
+
const stats = pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : '';
|
|
295
|
+
label = `[Incomplete Checklist${stats}]`;
|
|
296
|
+
type = 'incomplete_checklist';
|
|
297
|
+
break;
|
|
301
298
|
}
|
|
302
|
-
|
|
303
|
-
|
|
299
|
+
default: {
|
|
300
|
+
// Defensive fallback for ActionReason values not explicitly handled
|
|
301
|
+
// above (e.g. ci_not_running, needs_rebase, missing_required_files).
|
|
302
|
+
// These aren't in reasonOrder today but this guards future additions.
|
|
303
|
+
warn('daily-logic', `Unhandled ActionReason "${reason}" for PR ${pr.url} — falling back to needs_response`);
|
|
304
|
+
label = `[${reason}]`;
|
|
305
|
+
type = 'needs_response';
|
|
304
306
|
}
|
|
305
|
-
issues.push({ type, pr, label, isNewContribution });
|
|
306
307
|
}
|
|
308
|
+
// A PR is "new" if it was created after the last daily digest (first time seen).
|
|
309
|
+
// If there's no previous digest (first run) or createdAt is invalid, assume new.
|
|
310
|
+
const createdTime = new Date(pr.createdAt).getTime();
|
|
311
|
+
let isNewContribution;
|
|
312
|
+
if (isNaN(createdTime)) {
|
|
313
|
+
warn('daily-logic', `Invalid createdAt "${pr.createdAt}" for PR ${pr.url}, assuming new contribution`);
|
|
314
|
+
isNewContribution = true;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
isNewContribution = isNaN(lastDigestTime) || createdTime > lastDigestTime;
|
|
318
|
+
}
|
|
319
|
+
issues.push({ type, pr, label, isNewContribution });
|
|
307
320
|
}
|
|
308
321
|
return issues;
|
|
309
322
|
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsBy
|
|
|
22
22
|
export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
|
|
23
23
|
export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
|
|
24
24
|
export { classifyLinkedPR, isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, type LinkedPR, type LinkedPRClassification, type LinkedPRState, } from './linked-pr-classification.js';
|
|
25
|
+
export { classifyIssueAvailability, fetchIssueVerification, type IssueAvailabilityVerdict, type IssueVerification, type LinkedPRLinkType, type VerifiedLinkedPR, type VerifyIssueParams, } from './issue-verification.js';
|
|
26
|
+
export { classifyAttentionBucket, summarizeAttentionBuckets, STUCK_CI_THRESHOLD_DAYS, DORMANT_FOLLOWUP_THRESHOLD_DAYS, type AttentionBucket, type AttentionInput, type AttentionSummary, } from './pr-attention.js';
|
|
25
27
|
export { scanForAntiLLMPolicy, type AntiLLMCategory, type AntiLLMMatch, type AntiLLMScanResult, } from './anti-llm-policy.js';
|
|
26
28
|
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, type DetectedFormatter, type FormatterDetectionResult, type CIFormatterDiagnosis, type FormatterName, } from './formatter-detection.js';
|
|
27
29
|
export { CONFIG_KEY_REGISTRY, type ConfigKeyDef, type SettableVia, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
|
package/dist/core/index.js
CHANGED
|
@@ -23,6 +23,8 @@ export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsBy
|
|
|
23
23
|
export { computeContributionStats } from './stats.js';
|
|
24
24
|
export { fetchPRTemplate } from './pr-template.js';
|
|
25
25
|
export { classifyLinkedPR, isLinkedPRStalled, STALLED_PR_THRESHOLD_DAYS, } from './linked-pr-classification.js';
|
|
26
|
+
export { classifyIssueAvailability, fetchIssueVerification, } from './issue-verification.js';
|
|
27
|
+
export { classifyAttentionBucket, summarizeAttentionBuckets, STUCK_CI_THRESHOLD_DAYS, DORMANT_FOLLOWUP_THRESHOLD_DAYS, } from './pr-attention.js';
|
|
26
28
|
export { scanForAntiLLMPolicy, } from './anti-llm-policy.js';
|
|
27
29
|
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, } from './formatter-detection.js';
|
|
28
30
|
export { CONFIG_KEY_REGISTRY, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic issue verification (#1353, #1354).
|
|
3
|
+
*
|
|
4
|
+
* One GraphQL round-trip answers the two questions the issue-scout agent
|
|
5
|
+
* historically hallucinated: "is this issue actually open?" and "is it
|
|
6
|
+
* actually taken?". The query fetches the issue's `state`/`stateReason`,
|
|
7
|
+
* its assignees, every cross-referenced PR with `closingIssuesReferences`,
|
|
8
|
+
* and `viewer.login` — so "is this my PR?" is derived from the same token
|
|
9
|
+
* that fetched the data instead of a cached username.
|
|
10
|
+
*
|
|
11
|
+
* Classification is a pure function (`classifyIssueAvailability`) so the
|
|
12
|
+
* verdict rules are unit-testable without network access. The distinction
|
|
13
|
+
* that matters (#1353): a PR whose `closingIssuesReferences` includes this
|
|
14
|
+
* issue is a real claim (`closing`); a PR that merely mentions the issue in
|
|
15
|
+
* its timeline is just a mention (`cross-referenced`) and must not drive a
|
|
16
|
+
* "Taken" verdict.
|
|
17
|
+
*/
|
|
18
|
+
import type { Octokit } from '@octokit/rest';
|
|
19
|
+
/** How a PR found in the issue's timeline relates to the issue. */
|
|
20
|
+
export type LinkedPRLinkType = 'closing' | 'cross-referenced';
|
|
21
|
+
export interface VerifiedLinkedPR {
|
|
22
|
+
number: number;
|
|
23
|
+
url: string;
|
|
24
|
+
title: string;
|
|
25
|
+
/** Lowercase normalization of GraphQL's OPEN/CLOSED/MERGED union. */
|
|
26
|
+
state: 'open' | 'closed' | 'merged';
|
|
27
|
+
isDraft: boolean;
|
|
28
|
+
/** PR author login; null for ghost (deleted) accounts. */
|
|
29
|
+
author: string | null;
|
|
30
|
+
/** True when the PR author is the authenticated user (case-insensitive). */
|
|
31
|
+
isOwn: boolean;
|
|
32
|
+
/** `closing` = the PR's closingIssuesReferences names this issue (a real
|
|
33
|
+
* claim). `cross-referenced` = timeline mention only (NOT a claim). */
|
|
34
|
+
linkType: LinkedPRLinkType;
|
|
35
|
+
updatedAt: string | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The short-circuit signal consumers route on:
|
|
39
|
+
* - `closed` — issue is not open; stop all further analysis (#1353).
|
|
40
|
+
* - `own-open-pr` — the authenticated user already has an open closing PR;
|
|
41
|
+
* not a new opportunity (#1354).
|
|
42
|
+
* - `taken` — someone else has an open closing PR, or the issue is assigned
|
|
43
|
+
* to someone else.
|
|
44
|
+
* - `at-risk` — no hard claim, but signals competition or imminent closure
|
|
45
|
+
* (open cross-referencing PRs, or a merged closing PR awaiting issue close).
|
|
46
|
+
* - `available` — open, unassigned, no claiming PRs.
|
|
47
|
+
*/
|
|
48
|
+
export type IssueAvailabilityVerdict = 'closed' | 'own-open-pr' | 'taken' | 'at-risk' | 'available';
|
|
49
|
+
export interface IssueVerification {
|
|
50
|
+
url: string;
|
|
51
|
+
owner: string;
|
|
52
|
+
repo: string;
|
|
53
|
+
number: number;
|
|
54
|
+
title: string;
|
|
55
|
+
/** Lowercase normalization of GraphQL's OPEN/CLOSED union. */
|
|
56
|
+
state: 'open' | 'closed';
|
|
57
|
+
/** Lowercase GraphQL IssueStateReason. `completed` = fixed upstream (mark
|
|
58
|
+
* Done); `not_planned` = wontfix (drop permanently). Note: an OPEN issue
|
|
59
|
+
* can carry `reopened` — never infer closed-ness from a non-null reason;
|
|
60
|
+
* branch on `state` only. */
|
|
61
|
+
stateReason: 'completed' | 'not_planned' | 'reopened' | 'duplicate' | null;
|
|
62
|
+
closedAt: string | null;
|
|
63
|
+
assignees: string[];
|
|
64
|
+
linkedPRs: VerifiedLinkedPR[];
|
|
65
|
+
verdict: IssueAvailabilityVerdict;
|
|
66
|
+
verdictReason: string;
|
|
67
|
+
/** Login of the authenticated user the verdict was computed for. */
|
|
68
|
+
userLogin: string;
|
|
69
|
+
}
|
|
70
|
+
/** Inputs for the pure verdict classifier — the fetched facts, no I/O. */
|
|
71
|
+
export interface ClassifyIssueAvailabilityInput {
|
|
72
|
+
state: 'open' | 'closed';
|
|
73
|
+
stateReason: IssueVerification['stateReason'];
|
|
74
|
+
assignees: string[];
|
|
75
|
+
linkedPRs: VerifiedLinkedPR[];
|
|
76
|
+
userLogin: string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compute the availability verdict from fetched facts. Rules in priority
|
|
80
|
+
* order; the first match wins.
|
|
81
|
+
*/
|
|
82
|
+
export declare function classifyIssueAvailability(input: ClassifyIssueAvailabilityInput): {
|
|
83
|
+
verdict: IssueAvailabilityVerdict;
|
|
84
|
+
verdictReason: string;
|
|
85
|
+
};
|
|
86
|
+
export interface VerifyIssueParams {
|
|
87
|
+
owner: string;
|
|
88
|
+
repo: string;
|
|
89
|
+
number: number;
|
|
90
|
+
}
|
|
91
|
+
export declare function fetchIssueVerification(octokit: Octokit, params: VerifyIssueParams): Promise<IssueVerification>;
|