@oss-autopilot/core 3.5.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-registry.js +143 -1
- package/dist/cli.bundle.cjs +120 -108
- package/dist/commands/daily.d.ts +8 -0
- package/dist/commands/daily.js +21 -0
- package/dist/commands/dashboard-lifecycle.d.ts +7 -0
- package/dist/commands/dashboard-lifecycle.js +12 -2
- package/dist/commands/dashboard-process.d.ts +8 -0
- package/dist/commands/dashboard-process.js +20 -0
- package/dist/commands/features.d.ts +50 -0
- package/dist/commands/features.js +131 -0
- package/dist/commands/index.d.ts +5 -1
- package/dist/commands/index.js +4 -0
- package/dist/commands/scout-bridge.d.ts +12 -0
- package/dist/commands/scout-bridge.js +42 -2
- package/dist/commands/search.js +3 -1
- package/dist/commands/startup.js +75 -7
- package/dist/commands/vet-list.js +21 -5
- package/dist/commands/vet.js +3 -1
- package/dist/core/anti-llm-policy.d.ts +42 -13
- package/dist/core/anti-llm-policy.js +102 -13
- package/dist/core/ci-analysis.d.ts +32 -1
- package/dist/core/ci-analysis.js +92 -0
- package/dist/core/errors.d.ts +19 -0
- package/dist/core/errors.js +54 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/linked-pr-classification.d.ts +28 -0
- package/dist/core/linked-pr-classification.js +32 -0
- package/dist/core/pr-monitor.d.ts +1 -1
- package/dist/core/pr-monitor.js +31 -11
- package/dist/core/state-schema.d.ts +1 -0
- package/dist/core/state-schema.js +9 -0
- package/dist/core/state.d.ts +7 -0
- package/dist/core/state.js +10 -0
- package/dist/core/strategy.d.ts +21 -1
- package/dist/core/strategy.js +44 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/formatters/json.d.ts +329 -35
- package/dist/formatters/json.js +102 -0
- package/package.json +2 -2
package/dist/commands/daily.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
9
|
import { type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup } from '../core/index.js';
|
|
10
|
+
import { type StrategyResult } from '../core/strategy.js';
|
|
10
11
|
import { type DailyOutput, type DailyWarning, type CapacityAssessment, type ActionableIssue, type ActionMenu } from '../formatters/json.js';
|
|
11
12
|
export { applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
12
13
|
import { buildStarFilter } from '../core/daily-logic.js';
|
|
@@ -28,6 +29,13 @@ export interface DailyCheckResult {
|
|
|
28
29
|
failures: PRCheckFailure[];
|
|
29
30
|
/** Non-fatal warnings from ancillary pipeline phases — see #1042. */
|
|
30
31
|
warnings: DailyWarning[];
|
|
32
|
+
/**
|
|
33
|
+
* Periodic strategy snapshot (#1270). Set when the cadence trigger
|
|
34
|
+
* fires AND the user has crossed `STRATEGY_MIN_PRS`. The action-menu
|
|
35
|
+
* renderer in `commands/oss.md` reads this; absent or null on runs
|
|
36
|
+
* where the gate stays silent.
|
|
37
|
+
*/
|
|
38
|
+
strategySummary?: StrategyResult | null;
|
|
31
39
|
}
|
|
32
40
|
/**
|
|
33
41
|
* Convert a full DailyCheckResult to the compact DailyOutput for JSON serialization (#287).
|
package/dist/commands/daily.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
|
|
10
10
|
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
11
|
+
import { computeStrategy, shouldComputeStrategy } from '../core/strategy.js';
|
|
11
12
|
import { warn } from '../core/logger.js';
|
|
12
13
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
13
14
|
import { createAutopilotScout } from './scout-bridge.js';
|
|
@@ -421,6 +422,24 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
421
422
|
const briefSummary = formatBriefSummary(digest, actionableIssues.length, issueResponses.length);
|
|
422
423
|
const actionMenu = computeActionMenu(actionableIssues, capacity, filteredCommentedIssues);
|
|
423
424
|
const repoGroups = groupPRsByRepo(activePRs);
|
|
425
|
+
// Periodic strategy snapshot (#1270 Step 2). Cadence-gated to fire every
|
|
426
|
+
// 30 days OR after 5+ PRs merge since the last snapshot, whichever comes
|
|
427
|
+
// first. Below STRATEGY_MIN_PRS the gate stays silent. Compute failures
|
|
428
|
+
// are non-fatal — the daily run continues and the snapshot is omitted.
|
|
429
|
+
let strategySummary;
|
|
430
|
+
try {
|
|
431
|
+
const state = stateManager.getState();
|
|
432
|
+
const nowIso = new Date().toISOString();
|
|
433
|
+
if (shouldComputeStrategy(state, nowIso)) {
|
|
434
|
+
strategySummary = computeStrategy(state);
|
|
435
|
+
if (strategySummary) {
|
|
436
|
+
stateManager.setLastStrategyAt(nowIso);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
recordWarning(warnings, 'analytics', 'compute strategy snapshot', error);
|
|
442
|
+
}
|
|
424
443
|
return {
|
|
425
444
|
digest,
|
|
426
445
|
capacity,
|
|
@@ -432,6 +451,7 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
432
451
|
repoGroups,
|
|
433
452
|
failures,
|
|
434
453
|
warnings,
|
|
454
|
+
strategySummary,
|
|
435
455
|
};
|
|
436
456
|
}
|
|
437
457
|
// ---------------------------------------------------------------------------
|
|
@@ -457,6 +477,7 @@ export function toDailyOutput(result) {
|
|
|
457
477
|
repoGroups: compactRepoGroups(result.repoGroups),
|
|
458
478
|
failures: result.failures,
|
|
459
479
|
warnings: result.warnings,
|
|
480
|
+
strategySummary: result.strategySummary,
|
|
460
481
|
};
|
|
461
482
|
}
|
|
462
483
|
/**
|
|
@@ -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. */
|
|
@@ -75,6 +77,8 @@ export { runStateUnlink } from './state-cmd.js';
|
|
|
75
77
|
export { runParseList, pruneIssueList } from './parse-list.js';
|
|
76
78
|
/** Move an issue between Pursue / Maybe / Skip sections of a curated list (#1107). */
|
|
77
79
|
export { runListMoveTier, moveIssueToTier, type Tier, type ListMoveTierOptions, type ListMoveTierOutput, } from './list-move-tier.js';
|
|
80
|
+
/** Mark an issue line in a curated list as done with strikethrough + Done sub-bullet (#1299). */
|
|
81
|
+
export { runMarkIssueListItemDone, markIssueAsDone, type MarkDoneOptions, type MarkDoneOutput, } from './list-mark-done.js';
|
|
78
82
|
/** Check if new files are properly referenced/integrated. */
|
|
79
83
|
export { runCheckIntegration } from './check-integration.js';
|
|
80
84
|
/** System-health diagnostic — verifies tokens, bundle, state, scout, rate limit. */
|
|
@@ -85,7 +89,7 @@ export { runDetectFormatters } from './detect-formatters.js';
|
|
|
85
89
|
export { runLocalRepos } from './local-repos.js';
|
|
86
90
|
export type { DashboardJsonData, DashboardStats, DashboardActionType, ActionRequest } from './dashboard-data.js';
|
|
87
91
|
export type { ErrorCode } from '../formatters/json.js';
|
|
88
|
-
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';
|
|
89
93
|
export type { VetOutput, CommentsOutput, PostOutput, ClaimOutput, VetListOutput, VetListItemStatus, } from '../formatters/json.js';
|
|
90
94
|
export type { ConfigOutput, DetectFormattersOutput, ParseIssueListOutput, ParsedIssueItem, CheckIntegrationOutput, LocalReposOutput, } from '../formatters/json.js';
|
|
91
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. */
|
|
@@ -82,6 +84,8 @@ export { runStateUnlink } from './state-cmd.js';
|
|
|
82
84
|
export { runParseList, pruneIssueList } from './parse-list.js';
|
|
83
85
|
/** Move an issue between Pursue / Maybe / Skip sections of a curated list (#1107). */
|
|
84
86
|
export { runListMoveTier, moveIssueToTier, } from './list-move-tier.js';
|
|
87
|
+
/** Mark an issue line in a curated list as done with strikethrough + Done sub-bullet (#1299). */
|
|
88
|
+
export { runMarkIssueListItemDone, markIssueAsDone, } from './list-mark-done.js';
|
|
85
89
|
/** Check if new files are properly referenced/integrated. */
|
|
86
90
|
export { runCheckIntegration } from './check-integration.js';
|
|
87
91
|
/** System-health diagnostic — verifies tokens, bundle, state, scout, rate limit. */
|
|
@@ -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.
|
|
@@ -115,11 +116,33 @@ export function detectIssueList() {
|
|
|
115
116
|
// Matches the sibling warn at line 64 for `issueListPath` read failures.
|
|
116
117
|
warn('startup', `Could not read skippedIssuesPath from state: ${errorMessage(err)}`);
|
|
117
118
|
}
|
|
118
|
-
// Probe default path: same directory as issue list, named skipped-issues.md
|
|
119
|
+
// Probe default path: same directory as issue list, named skipped-issues.md.
|
|
120
|
+
// When found, also persist to state.config so downstream commands
|
|
121
|
+
// (`skip-add`, `scout search`'s skip-list filter) read the same path
|
|
122
|
+
// instead of silently no-opping with "No skipped-issues path configured"
|
|
123
|
+
// (#1330). Without persistence, the auto-detect printed the path on
|
|
124
|
+
// every startup but nothing else honored it — search would re-surface
|
|
125
|
+
// already-skipped candidates round after round.
|
|
119
126
|
if (!skippedIssuesPath && issueListPath) {
|
|
120
127
|
const defaultSkipPath = path.join(path.dirname(issueListPath), 'skipped-issues.md');
|
|
121
128
|
if (fs.existsSync(defaultSkipPath)) {
|
|
122
129
|
skippedIssuesPath = defaultSkipPath;
|
|
130
|
+
try {
|
|
131
|
+
const stateManager = getStateManager();
|
|
132
|
+
// Only write when config actually doesn't have one — re-running
|
|
133
|
+
// startup shouldn't trigger an autoSave on every run if the
|
|
134
|
+
// value is already there.
|
|
135
|
+
const current = stateManager.getState().config.skippedIssuesPath;
|
|
136
|
+
if (!current) {
|
|
137
|
+
stateManager.updateConfig({ skippedIssuesPath: defaultSkipPath });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
// Persistence is best-effort — startup still surfaces the path
|
|
142
|
+
// in its return value so the current run benefits, but the next
|
|
143
|
+
// run won't. Log so the failure is debuggable.
|
|
144
|
+
warn('startup', `Could not persist auto-detected skippedIssuesPath: ${errorMessage(err)}`);
|
|
145
|
+
}
|
|
123
146
|
}
|
|
124
147
|
}
|
|
125
148
|
return { path: issueListPath, source, availableCount, completedCount, skippedIssuesPath };
|
|
@@ -159,6 +182,48 @@ export function openInBrowser(url) {
|
|
|
159
182
|
* Hits POST /api/refresh so the SPA picks up fresh data on its next poll.
|
|
160
183
|
* Non-fatal: errors are logged but don't propagate (#830).
|
|
161
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
|
+
}
|
|
162
227
|
async function triggerDashboardRefresh(port) {
|
|
163
228
|
try {
|
|
164
229
|
const res = await fetch(`http://127.0.0.1:${port}/api/refresh`, {
|
|
@@ -249,12 +314,15 @@ export async function runStartup() {
|
|
|
249
314
|
else {
|
|
250
315
|
dashboardStatus = 'opened';
|
|
251
316
|
}
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
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
|
+
}
|
|
258
326
|
}
|
|
259
327
|
else {
|
|
260
328
|
dashboardError = 'Dashboard SPA assets not found. Build with: cd packages/dashboard && pnpm run build';
|