@oss-autopilot/core 3.6.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 +94 -1
- package/dist/cli.bundle.cjs +99 -91
- 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 +3 -1
- package/dist/commands/index.js +2 -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 +52 -6
- package/dist/commands/vet-list.js +21 -5
- package/dist/commands/vet.js +3 -1
- 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/formatters/json.d.ts +224 -35
- package/dist/formatters/json.js +28 -0
- package/package.json +2 -2
|
@@ -7,6 +7,13 @@ export interface LaunchResult {
|
|
|
7
7
|
url: string;
|
|
8
8
|
port: number;
|
|
9
9
|
alreadyRunning: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* When `alreadyRunning` is true, the timestamp the running server most
|
|
12
|
+
* recently recorded a browser-open at (or undefined if never recorded).
|
|
13
|
+
* Used by startup to throttle duplicate browser tabs across `/oss` runs.
|
|
14
|
+
* Always undefined for fresh launches.
|
|
15
|
+
*/
|
|
16
|
+
lastBrowserOpenedAt?: string;
|
|
10
17
|
}
|
|
11
18
|
/**
|
|
12
19
|
* Launch the interactive dashboard SPA server as a detached background process.
|
|
@@ -61,12 +61,22 @@ export async function launchDashboardServer(options) {
|
|
|
61
61
|
else {
|
|
62
62
|
// Could not kill old server (e.g. EPERM); return it rather than
|
|
63
63
|
// attempting a doomed spawn on the same port.
|
|
64
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
url: existing.url,
|
|
66
|
+
port: existing.port,
|
|
67
|
+
alreadyRunning: true,
|
|
68
|
+
lastBrowserOpenedAt: info?.lastBrowserOpenedAt,
|
|
69
|
+
};
|
|
65
70
|
}
|
|
66
71
|
// Fall through to launch a new server
|
|
67
72
|
}
|
|
68
73
|
else {
|
|
69
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
url: existing.url,
|
|
76
|
+
port: existing.port,
|
|
77
|
+
alreadyRunning: true,
|
|
78
|
+
lastBrowserOpenedAt: info.lastBrowserOpenedAt,
|
|
79
|
+
};
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
// 3. Launch as detached child process
|
|
@@ -7,10 +7,18 @@ export interface DashboardServerInfo {
|
|
|
7
7
|
port: number;
|
|
8
8
|
startedAt: string;
|
|
9
9
|
version?: string;
|
|
10
|
+
lastBrowserOpenedAt?: string;
|
|
10
11
|
}
|
|
11
12
|
export declare function getDashboardPidPath(): string;
|
|
12
13
|
export declare function writeDashboardServerInfo(info: DashboardServerInfo): void;
|
|
13
14
|
export declare function readDashboardServerInfo(): DashboardServerInfo | null;
|
|
15
|
+
/**
|
|
16
|
+
* Stamp the PID file with the current time as `lastBrowserOpenedAt`. Used by
|
|
17
|
+
* startup to throttle re-opening the dashboard tab — see the `shouldOpenBrowser`
|
|
18
|
+
* helper in `startup.ts`. No-ops if the PID file is missing or the recorded
|
|
19
|
+
* port doesn't match (server may have restarted between read and write).
|
|
20
|
+
*/
|
|
21
|
+
export declare function recordBrowserOpened(port: number): void;
|
|
14
22
|
export declare function removeDashboardServerInfo(): void;
|
|
15
23
|
export declare function isDashboardServerRunning(port: number): Promise<boolean>;
|
|
16
24
|
export declare function findRunningDashboardServer(): Promise<{
|
|
@@ -29,6 +29,9 @@ export function readDashboardServerInfo() {
|
|
|
29
29
|
warn(MODULE, 'PID file has invalid structure, ignoring');
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
+
if (parsed.lastBrowserOpenedAt !== undefined && typeof parsed.lastBrowserOpenedAt !== 'string') {
|
|
33
|
+
delete parsed.lastBrowserOpenedAt;
|
|
34
|
+
}
|
|
32
35
|
return parsed;
|
|
33
36
|
}
|
|
34
37
|
catch (err) {
|
|
@@ -39,6 +42,23 @@ export function readDashboardServerInfo() {
|
|
|
39
42
|
return null;
|
|
40
43
|
}
|
|
41
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Stamp the PID file with the current time as `lastBrowserOpenedAt`. Used by
|
|
47
|
+
* startup to throttle re-opening the dashboard tab — see the `shouldOpenBrowser`
|
|
48
|
+
* helper in `startup.ts`. No-ops if the PID file is missing or the recorded
|
|
49
|
+
* port doesn't match (server may have restarted between read and write).
|
|
50
|
+
*/
|
|
51
|
+
export function recordBrowserOpened(port) {
|
|
52
|
+
const info = readDashboardServerInfo();
|
|
53
|
+
if (!info || info.port !== port)
|
|
54
|
+
return;
|
|
55
|
+
try {
|
|
56
|
+
writeDashboardServerInfo({ ...info, lastBrowserOpenedAt: new Date().toISOString() });
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
warn(MODULE, `Failed to record browser-opened timestamp: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
42
62
|
export function removeDashboardServerInfo() {
|
|
43
63
|
try {
|
|
44
64
|
fs.unlinkSync(getDashboardPidPath());
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Features command (scout 0.9.0 #97/#98/#99)
|
|
3
|
+
*
|
|
4
|
+
* Wraps `scout.features()` to surface feature-scoped contribution
|
|
5
|
+
* opportunities in repos where the user already has 3+ merged PRs
|
|
6
|
+
* (configurable via `featuresAnchorThreshold`). Returns two ranked
|
|
7
|
+
* buckets — "quick wins" and "bigger bets" — split by maintainer-
|
|
8
|
+
* commitment signals (milestones, roadmap membership, label set).
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the shape of `runSearch`: each bucket entry is a
|
|
11
|
+
* {@link SearchCandidate} augmented with a `horizon` literal so callers
|
|
12
|
+
* can render bucket-specific UX while reusing the search candidate
|
|
13
|
+
* formatter.
|
|
14
|
+
*/
|
|
15
|
+
import { type FeaturesOutput } from '../formatters/json.js';
|
|
16
|
+
export { type FeaturesOutput, type FeaturesCandidate } from '../formatters/json.js';
|
|
17
|
+
/**
|
|
18
|
+
* Hard cap on feature-search result count, matching `MAX_SEARCH_RESULTS`
|
|
19
|
+
* for `runSearch`. Shared between CLI (`cli-registry.ts`) and the MCP
|
|
20
|
+
* tool registration so a future adjustment lands in one place.
|
|
21
|
+
*/
|
|
22
|
+
export declare const MAX_FEATURES_RESULTS = 100;
|
|
23
|
+
interface FeaturesOptions {
|
|
24
|
+
maxResults: number;
|
|
25
|
+
/** Override `featuresAnchorThreshold` for this call (1-50). */
|
|
26
|
+
anchorThreshold?: number;
|
|
27
|
+
/** Override `featuresSplitRatio` for this call (0-1). */
|
|
28
|
+
splitRatio?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Run feature-scoped contribution discovery via `scout.features()`.
|
|
32
|
+
*
|
|
33
|
+
* @param options - Feature search configuration
|
|
34
|
+
* @param options.maxResults - Total candidates returned across both buckets
|
|
35
|
+
* @param options.anchorThreshold - Optional per-call override for the merged-PR threshold (default: scout's `featuresAnchorThreshold`)
|
|
36
|
+
* @param options.splitRatio - Optional per-call override for the quick-win split ratio (default: scout's `featuresSplitRatio`)
|
|
37
|
+
* @returns Two ranked buckets (quick-wins / bigger-bets) plus anchor list and an optional human message
|
|
38
|
+
* @throws {ConfigurationError} If no GitHub token is available
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { runFeatures } from '@oss-autopilot/core/commands';
|
|
43
|
+
*
|
|
44
|
+
* const result = await runFeatures({ maxResults: 10 });
|
|
45
|
+
* for (const c of [...result.quickWins, ...result.biggerBets]) {
|
|
46
|
+
* console.log(`${c.issue.repo}#${c.issue.number} — ${c.horizon}`);
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function runFeatures(options: FeaturesOptions): Promise<FeaturesOutput>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Features command (scout 0.9.0 #97/#98/#99)
|
|
3
|
+
*
|
|
4
|
+
* Wraps `scout.features()` to surface feature-scoped contribution
|
|
5
|
+
* opportunities in repos where the user already has 3+ merged PRs
|
|
6
|
+
* (configurable via `featuresAnchorThreshold`). Returns two ranked
|
|
7
|
+
* buckets — "quick wins" and "bigger bets" — split by maintainer-
|
|
8
|
+
* commitment signals (milestones, roadmap membership, label set).
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the shape of `runSearch`: each bucket entry is a
|
|
11
|
+
* {@link SearchCandidate} augmented with a `horizon` literal so callers
|
|
12
|
+
* can render bucket-specific UX while reusing the search candidate
|
|
13
|
+
* formatter.
|
|
14
|
+
*/
|
|
15
|
+
import { buildCandidateLinkedPR, createAutopilotScout } from './scout-bridge.js';
|
|
16
|
+
import { getStateManager } from '../core/index.js';
|
|
17
|
+
import { gradeFromCandidate } from '../core/issue-grading.js';
|
|
18
|
+
import { warn } from '../core/logger.js';
|
|
19
|
+
const MODULE = 'features';
|
|
20
|
+
/**
|
|
21
|
+
* Hard cap on feature-search result count, matching `MAX_SEARCH_RESULTS`
|
|
22
|
+
* for `runSearch`. Shared between CLI (`cli-registry.ts`) and the MCP
|
|
23
|
+
* tool registration so a future adjustment lands in one place.
|
|
24
|
+
*/
|
|
25
|
+
export const MAX_FEATURES_RESULTS = 100;
|
|
26
|
+
/**
|
|
27
|
+
* Coerce scout's raw `viabilityScore` into a trustworthy 0-100 number.
|
|
28
|
+
* Same defensive boundary check as `runSearch` — kept inline (not
|
|
29
|
+
* imported) so this command remains self-contained per the existing
|
|
30
|
+
* autopilot convention. See #1043.
|
|
31
|
+
*/
|
|
32
|
+
function sanitizeViabilityScore(raw) {
|
|
33
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0 || raw > 100) {
|
|
34
|
+
warn(MODULE, `Ignoring out-of-contract viabilityScore from scout: ${JSON.stringify(raw)}`);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
return raw;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Map one scout `FeatureCandidate` into the autopilot output shape.
|
|
41
|
+
* Mirrors the candidate-mapping branch in `runSearch` and stamps the
|
|
42
|
+
* `horizon` field.
|
|
43
|
+
*/
|
|
44
|
+
function toFeaturesCandidate(scoutCandidate, getState) {
|
|
45
|
+
const repoScoreRecord = getState.getRepoScore(scoutCandidate.issue.repo);
|
|
46
|
+
// Same `checkFailed: true` sentinel `runSearch` uses — scout's `features`
|
|
47
|
+
// pipeline reuses the same vetting code that does emit projectHealth on
|
|
48
|
+
// `vetIssue`, but on this surface scout returns the multi-issue list view
|
|
49
|
+
// which today does not propagate per-candidate health into IssueCandidate.
|
|
50
|
+
// Treating health as unknown grades from the autopilot-tracked repoScore
|
|
51
|
+
// alone, falling to 'F' for unfamiliar repos — an honest "we haven't seen
|
|
52
|
+
// this repo" rather than a fabricated score.
|
|
53
|
+
const grade = gradeFromCandidate({
|
|
54
|
+
repo: scoutCandidate.issue.repo,
|
|
55
|
+
projectHealth: { avgIssueResponseDays: null, daysSinceLastCommit: null, checkFailed: true },
|
|
56
|
+
getRepoScore: (repo) => {
|
|
57
|
+
const score = getState.getRepoScore(repo);
|
|
58
|
+
return score
|
|
59
|
+
? {
|
|
60
|
+
mergedPRCount: score.mergedPRCount,
|
|
61
|
+
closedWithoutMergeCount: score.closedWithoutMergeCount,
|
|
62
|
+
avgResponseDays: score.avgResponseDays ?? null,
|
|
63
|
+
}
|
|
64
|
+
: undefined;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const linkedPR = buildCandidateLinkedPR(scoutCandidate.vettingResult?.linkedPR);
|
|
68
|
+
return {
|
|
69
|
+
issue: {
|
|
70
|
+
repo: scoutCandidate.issue.repo,
|
|
71
|
+
repoUrl: `https://github.com/${scoutCandidate.issue.repo}`,
|
|
72
|
+
number: scoutCandidate.issue.number,
|
|
73
|
+
title: scoutCandidate.issue.title,
|
|
74
|
+
url: scoutCandidate.issue.url,
|
|
75
|
+
labels: scoutCandidate.issue.labels,
|
|
76
|
+
},
|
|
77
|
+
recommendation: scoutCandidate.recommendation,
|
|
78
|
+
reasonsToApprove: scoutCandidate.reasonsToApprove,
|
|
79
|
+
reasonsToSkip: scoutCandidate.reasonsToSkip,
|
|
80
|
+
searchPriority: scoutCandidate.searchPriority,
|
|
81
|
+
viabilityScore: sanitizeViabilityScore(scoutCandidate.viabilityScore),
|
|
82
|
+
grade,
|
|
83
|
+
repoScore: repoScoreRecord
|
|
84
|
+
? {
|
|
85
|
+
score: repoScoreRecord.score,
|
|
86
|
+
mergedPRCount: repoScoreRecord.mergedPRCount,
|
|
87
|
+
closedWithoutMergeCount: repoScoreRecord.closedWithoutMergeCount,
|
|
88
|
+
isResponsive: repoScoreRecord.signals?.isResponsive ?? false,
|
|
89
|
+
lastMergedAt: repoScoreRecord.lastMergedAt,
|
|
90
|
+
}
|
|
91
|
+
: undefined,
|
|
92
|
+
...(linkedPR ? { linkedPR } : {}),
|
|
93
|
+
horizon: scoutCandidate.horizon,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Run feature-scoped contribution discovery via `scout.features()`.
|
|
98
|
+
*
|
|
99
|
+
* @param options - Feature search configuration
|
|
100
|
+
* @param options.maxResults - Total candidates returned across both buckets
|
|
101
|
+
* @param options.anchorThreshold - Optional per-call override for the merged-PR threshold (default: scout's `featuresAnchorThreshold`)
|
|
102
|
+
* @param options.splitRatio - Optional per-call override for the quick-win split ratio (default: scout's `featuresSplitRatio`)
|
|
103
|
+
* @returns Two ranked buckets (quick-wins / bigger-bets) plus anchor list and an optional human message
|
|
104
|
+
* @throws {ConfigurationError} If no GitHub token is available
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* import { runFeatures } from '@oss-autopilot/core/commands';
|
|
109
|
+
*
|
|
110
|
+
* const result = await runFeatures({ maxResults: 10 });
|
|
111
|
+
* for (const c of [...result.quickWins, ...result.biggerBets]) {
|
|
112
|
+
* console.log(`${c.issue.repo}#${c.issue.number} — ${c.horizon}`);
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export async function runFeatures(options) {
|
|
117
|
+
const scout = await createAutopilotScout();
|
|
118
|
+
const result = await scout.features({
|
|
119
|
+
count: options.maxResults,
|
|
120
|
+
anchorThreshold: options.anchorThreshold,
|
|
121
|
+
splitRatio: options.splitRatio,
|
|
122
|
+
});
|
|
123
|
+
const stateManager = getStateManager();
|
|
124
|
+
const featuresOutput = {
|
|
125
|
+
quickWins: result.quickWins.map((c) => toFeaturesCandidate(c, stateManager)),
|
|
126
|
+
biggerBets: result.biggerBets.map((c) => toFeaturesCandidate(c, stateManager)),
|
|
127
|
+
anchorRepos: result.anchorRepos,
|
|
128
|
+
message: result.message,
|
|
129
|
+
};
|
|
130
|
+
return featuresOutput;
|
|
131
|
+
}
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export { runStartup } from './startup.js';
|
|
|
23
23
|
export { runStatus } from './status.js';
|
|
24
24
|
/** Search GitHub for contributable issues using multi-strategy discovery. */
|
|
25
25
|
export { runSearch, MAX_SEARCH_RESULTS } from './search.js';
|
|
26
|
+
/** Surface feature-scoped opportunities in repos with 3+ merged PRs (scout 0.9.0). */
|
|
27
|
+
export { runFeatures, MAX_FEATURES_RESULTS } from './features.js';
|
|
26
28
|
/** Vet a single GitHub issue for claimability (open, unassigned, no linked PRs, repo health). */
|
|
27
29
|
export { runVet } from './vet.js';
|
|
28
30
|
/** Re-vet all available issues in a curated issue list for freshness. */
|
|
@@ -87,7 +89,7 @@ export { runDetectFormatters } from './detect-formatters.js';
|
|
|
87
89
|
export { runLocalRepos } from './local-repos.js';
|
|
88
90
|
export type { DashboardJsonData, DashboardStats, DashboardActionType, ActionRequest } from './dashboard-data.js';
|
|
89
91
|
export type { ErrorCode } from '../formatters/json.js';
|
|
90
|
-
export type { DailyOutput, SearchOutput, StartupOutput, StatusOutput, TrackOutput } from '../formatters/json.js';
|
|
92
|
+
export type { DailyOutput, SearchOutput, SearchCandidate, CandidateLinkedPR, FeaturesOutput, FeaturesCandidate, FeaturesHorizon, StartupOutput, StatusOutput, TrackOutput, } from '../formatters/json.js';
|
|
91
93
|
export type { VetOutput, CommentsOutput, PostOutput, ClaimOutput, VetListOutput, VetListItemStatus, } from '../formatters/json.js';
|
|
92
94
|
export type { ConfigOutput, DetectFormattersOutput, ParseIssueListOutput, ParsedIssueItem, CheckIntegrationOutput, LocalReposOutput, } from '../formatters/json.js';
|
|
93
95
|
export type { ShelveOutput, UnshelveOutput } from './shelve.js';
|
package/dist/commands/index.js
CHANGED
|
@@ -24,6 +24,8 @@ export { runStartup } from './startup.js';
|
|
|
24
24
|
export { runStatus } from './status.js';
|
|
25
25
|
/** Search GitHub for contributable issues using multi-strategy discovery. */
|
|
26
26
|
export { runSearch, MAX_SEARCH_RESULTS } from './search.js';
|
|
27
|
+
/** Surface feature-scoped opportunities in repos with 3+ merged PRs (scout 0.9.0). */
|
|
28
|
+
export { runFeatures, MAX_FEATURES_RESULTS } from './features.js';
|
|
27
29
|
/** Vet a single GitHub issue for claimability (open, unassigned, no linked PRs, repo health). */
|
|
28
30
|
export { runVet } from './vet.js';
|
|
29
31
|
/** Re-vet all available issues in a curated issue list for freshness. */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type LinkedPR as ScoutLinkedPR, type OssScout, type ScoutState } from '@oss-scout/core';
|
|
6
6
|
import type { LinkedPR } from '../core/linked-pr-classification.js';
|
|
7
|
+
import type { CandidateLinkedPR } from '../formatters/json.js';
|
|
7
8
|
/**
|
|
8
9
|
* Convert scout 0.6.0's `LinkedPR` (separate `state` + `merged`) into the
|
|
9
10
|
* shape `classifyLinkedPR` expects (`state` already folded with `merged`).
|
|
@@ -12,8 +13,19 @@ import type { LinkedPR } from '../core/linked-pr-classification.js';
|
|
|
12
13
|
* written before scout surfaced this data and uses a tri-state
|
|
13
14
|
* `'open' | 'closed' | 'merged'` enum. Folding `merged` into the state
|
|
14
15
|
* preserves the function's existing contract + tests.
|
|
16
|
+
*
|
|
17
|
+
* `updatedAt` (added in scout 0.9.0) flows through unchanged so consumers
|
|
18
|
+
* can hand the result to `isLinkedPRStalled` without re-fetching.
|
|
15
19
|
*/
|
|
16
20
|
export declare function adaptScoutLinkedPR(scoutLinkedPR: ScoutLinkedPR | null | undefined): LinkedPR | null;
|
|
21
|
+
/**
|
|
22
|
+
* Build the autopilot-shaped `linkedPR` slice consumed by `SearchOutput`,
|
|
23
|
+
* `VetOutput`, and `FeaturesOutput` from scout's raw `LinkedPR`
|
|
24
|
+
* (#97 / scout 0.9.0). Returns `undefined` when scout reported no linked
|
|
25
|
+
* PR. Computes `isStalled` from the adapted (autopilot-shape) PR so the
|
|
26
|
+
* rule stays consistent with `classifyLinkedPR` and downstream consumers.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildCandidateLinkedPR(scoutLinkedPR: ScoutLinkedPR | null | undefined): CandidateLinkedPR | undefined;
|
|
17
29
|
/**
|
|
18
30
|
* Build a ScoutState from the current AgentState.
|
|
19
31
|
* Maps oss-autopilot's config and state fields to oss-scout's state format.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Maps state fields and creates scout instances for search/vet commands.
|
|
4
4
|
*/
|
|
5
5
|
import { createScout } from '@oss-scout/core';
|
|
6
|
-
import { getStateManager, requireGitHubToken } from '../core/index.js';
|
|
6
|
+
import { getStateManager, isLinkedPRStalled, requireGitHubToken } from '../core/index.js';
|
|
7
7
|
import { loadSkippedIssues } from './skip-file-parser.js';
|
|
8
8
|
/**
|
|
9
9
|
* Convert scout 0.6.0's `LinkedPR` (separate `state` + `merged`) into the
|
|
@@ -13,14 +13,45 @@ import { loadSkippedIssues } from './skip-file-parser.js';
|
|
|
13
13
|
* written before scout surfaced this data and uses a tri-state
|
|
14
14
|
* `'open' | 'closed' | 'merged'` enum. Folding `merged` into the state
|
|
15
15
|
* preserves the function's existing contract + tests.
|
|
16
|
+
*
|
|
17
|
+
* `updatedAt` (added in scout 0.9.0) flows through unchanged so consumers
|
|
18
|
+
* can hand the result to `isLinkedPRStalled` without re-fetching.
|
|
16
19
|
*/
|
|
17
20
|
export function adaptScoutLinkedPR(scoutLinkedPR) {
|
|
18
21
|
if (!scoutLinkedPR)
|
|
19
22
|
return null;
|
|
20
|
-
|
|
23
|
+
const adapted = {
|
|
21
24
|
author: { login: scoutLinkedPR.author },
|
|
22
25
|
state: scoutLinkedPR.merged ? 'merged' : scoutLinkedPR.state,
|
|
23
26
|
};
|
|
27
|
+
if (scoutLinkedPR.updatedAt !== undefined) {
|
|
28
|
+
adapted.updatedAt = scoutLinkedPR.updatedAt;
|
|
29
|
+
}
|
|
30
|
+
return adapted;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the autopilot-shaped `linkedPR` slice consumed by `SearchOutput`,
|
|
34
|
+
* `VetOutput`, and `FeaturesOutput` from scout's raw `LinkedPR`
|
|
35
|
+
* (#97 / scout 0.9.0). Returns `undefined` when scout reported no linked
|
|
36
|
+
* PR. Computes `isStalled` from the adapted (autopilot-shape) PR so the
|
|
37
|
+
* rule stays consistent with `classifyLinkedPR` and downstream consumers.
|
|
38
|
+
*/
|
|
39
|
+
export function buildCandidateLinkedPR(scoutLinkedPR) {
|
|
40
|
+
if (!scoutLinkedPR)
|
|
41
|
+
return undefined;
|
|
42
|
+
const adapted = adaptScoutLinkedPR(scoutLinkedPR);
|
|
43
|
+
if (!adapted)
|
|
44
|
+
return undefined;
|
|
45
|
+
const linkedPR = {
|
|
46
|
+
number: scoutLinkedPR.number,
|
|
47
|
+
state: adapted.state,
|
|
48
|
+
url: scoutLinkedPR.url,
|
|
49
|
+
isStalled: isLinkedPRStalled(adapted),
|
|
50
|
+
};
|
|
51
|
+
if (scoutLinkedPR.updatedAt !== undefined) {
|
|
52
|
+
linkedPR.updatedAt = scoutLinkedPR.updatedAt;
|
|
53
|
+
}
|
|
54
|
+
return linkedPR;
|
|
24
55
|
}
|
|
25
56
|
/**
|
|
26
57
|
* Build a ScoutState from the current AgentState.
|
|
@@ -50,6 +81,15 @@ export function buildScoutState() {
|
|
|
50
81
|
persistence: config.persistence,
|
|
51
82
|
slmTriageModel: config.slmTriageModel,
|
|
52
83
|
slmTriageHost: config.slmTriageHost,
|
|
84
|
+
// Scout 0.9.0 made these required on `ScoutPreferences` (their schema
|
|
85
|
+
// ZodDefaults still apply at parse time, but the inferred TS type now
|
|
86
|
+
// demands the fields). We pass scout's documented defaults here so
|
|
87
|
+
// autopilot doesn't have to introduce a new pair of user-visible
|
|
88
|
+
// settings just to satisfy the type. The features CLI command
|
|
89
|
+
// (#runFeatures) accepts per-call `--anchor-threshold` and
|
|
90
|
+
// `--split-ratio` flags for overrides.
|
|
91
|
+
featuresAnchorThreshold: 3,
|
|
92
|
+
featuresSplitRatio: 0.6,
|
|
53
93
|
},
|
|
54
94
|
repoScores: state.repoScores,
|
|
55
95
|
starredRepos: config.starredRepos,
|
package/dist/commands/search.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Search command
|
|
3
3
|
* Searches for new issues to work on via @oss-scout/core
|
|
4
4
|
*/
|
|
5
|
-
import { createAutopilotScout } from './scout-bridge.js';
|
|
5
|
+
import { buildCandidateLinkedPR, createAutopilotScout } from './scout-bridge.js';
|
|
6
6
|
import { getStateManager } from '../core/index.js';
|
|
7
7
|
import { gradeFromCandidate } from '../core/issue-grading.js';
|
|
8
8
|
import { warn } from '../core/logger.js';
|
|
@@ -71,6 +71,7 @@ export async function runSearch(options) {
|
|
|
71
71
|
: undefined;
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
|
+
const linkedPR = buildCandidateLinkedPR(c.vettingResult?.linkedPR);
|
|
74
75
|
return {
|
|
75
76
|
issue: {
|
|
76
77
|
repo: c.issue.repo,
|
|
@@ -95,6 +96,7 @@ export async function runSearch(options) {
|
|
|
95
96
|
lastMergedAt: repoScoreRecord.lastMergedAt,
|
|
96
97
|
}
|
|
97
98
|
: undefined,
|
|
99
|
+
...(linkedPR ? { linkedPR } : {}),
|
|
98
100
|
};
|
|
99
101
|
}),
|
|
100
102
|
excludedRepos: result.excludedRepos,
|
package/dist/commands/startup.js
CHANGED
|
@@ -14,6 +14,7 @@ import { errorMessage } from '../core/errors.js';
|
|
|
14
14
|
import { warn } from '../core/logger.js';
|
|
15
15
|
import { executeDailyCheck } from './daily.js';
|
|
16
16
|
import { launchDashboardServer } from './dashboard-lifecycle.js';
|
|
17
|
+
import { recordBrowserOpened } from './dashboard-process.js';
|
|
17
18
|
import { parseIssueList } from './parse-list.js';
|
|
18
19
|
/**
|
|
19
20
|
* Parse issueListPath from a config file's YAML frontmatter.
|
|
@@ -181,6 +182,48 @@ export function openInBrowser(url) {
|
|
|
181
182
|
* Hits POST /api/refresh so the SPA picks up fresh data on its next poll.
|
|
182
183
|
* Non-fatal: errors are logged but don't propagate (#830).
|
|
183
184
|
*/
|
|
185
|
+
const DEFAULT_REOPEN_THROTTLE_MS = 30 * 60 * 1000; // 30 minutes
|
|
186
|
+
/**
|
|
187
|
+
* Resolve the browser-reopen throttle window from `OSS_DASHBOARD_REOPEN_THROTTLE_MS`,
|
|
188
|
+
* falling back to {@link DEFAULT_REOPEN_THROTTLE_MS}. A value of `0` disables the
|
|
189
|
+
* throttle entirely, restoring the pre-#1339 behavior of always re-opening.
|
|
190
|
+
*/
|
|
191
|
+
function getReopenThrottleMs() {
|
|
192
|
+
const raw = process.env.OSS_DASHBOARD_REOPEN_THROTTLE_MS;
|
|
193
|
+
if (!raw)
|
|
194
|
+
return DEFAULT_REOPEN_THROTTLE_MS;
|
|
195
|
+
const n = Number(raw);
|
|
196
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_REOPEN_THROTTLE_MS;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Decide whether `openInBrowser` should run. Throttles re-opens against an
|
|
200
|
+
* already-running server's `lastBrowserOpenedAt` timestamp so back-to-back
|
|
201
|
+
* `/oss` runs don't pile up duplicate tabs (#1339), while still re-surfacing
|
|
202
|
+
* the dashboard for users who closed the tab and came back later (#1100).
|
|
203
|
+
*
|
|
204
|
+
* Fresh launches (`alreadyRunning === false`) always open; the new server has
|
|
205
|
+
* no recorded open yet by definition.
|
|
206
|
+
*
|
|
207
|
+
* Set `OSS_NO_BROWSER=1` to skip opening unconditionally (e.g., headless / CI).
|
|
208
|
+
*/
|
|
209
|
+
function shouldOpenBrowser(spaResult, throttleMs) {
|
|
210
|
+
if (process.env.OSS_NO_BROWSER === '1')
|
|
211
|
+
return false;
|
|
212
|
+
if (!spaResult.alreadyRunning)
|
|
213
|
+
return true;
|
|
214
|
+
if (throttleMs === 0)
|
|
215
|
+
return true;
|
|
216
|
+
const last = spaResult.lastBrowserOpenedAt;
|
|
217
|
+
if (!last)
|
|
218
|
+
return true;
|
|
219
|
+
const lastMs = Date.parse(last);
|
|
220
|
+
if (!Number.isFinite(lastMs))
|
|
221
|
+
return true;
|
|
222
|
+
const elapsed = Date.now() - lastMs;
|
|
223
|
+
if (elapsed < 0)
|
|
224
|
+
return true; // clock skew or future timestamp; treat as stale
|
|
225
|
+
return elapsed >= throttleMs;
|
|
226
|
+
}
|
|
184
227
|
async function triggerDashboardRefresh(port) {
|
|
185
228
|
try {
|
|
186
229
|
const res = await fetch(`http://127.0.0.1:${port}/api/refresh`, {
|
|
@@ -271,12 +314,15 @@ export async function runStartup() {
|
|
|
271
314
|
else {
|
|
272
315
|
dashboardStatus = 'opened';
|
|
273
316
|
}
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
317
|
+
// Throttle re-opens against the running server's last-opened timestamp
|
|
318
|
+
// so back-to-back /oss runs don't pile up duplicate tabs (#1339), while
|
|
319
|
+
// still re-surfacing the dashboard for users who closed the tab and
|
|
320
|
+
// came back later (#1100). `OSS_NO_BROWSER=1` skips entirely;
|
|
321
|
+
// `OSS_DASHBOARD_REOPEN_THROTTLE_MS=0` restores always-open behavior.
|
|
322
|
+
if (shouldOpenBrowser(spaResult, getReopenThrottleMs())) {
|
|
323
|
+
openInBrowser(spaResult.url);
|
|
324
|
+
recordBrowserOpened(spaResult.port);
|
|
325
|
+
}
|
|
280
326
|
}
|
|
281
327
|
else {
|
|
282
328
|
dashboardError = 'Dashboard SPA assets not found. Build with: cd packages/dashboard && pnpm run build';
|
|
@@ -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
|
};
|
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';
|