@oss-autopilot/core 3.13.3 → 3.14.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/README.md +3 -3
- package/dist/cli-registry.js +50 -83
- package/dist/cli.bundle.cjs +110 -107
- package/dist/cli.js +17 -3
- package/dist/commands/comments.js +44 -10
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +50 -2
- package/dist/commands/curated-list.d.ts +17 -0
- package/dist/commands/curated-list.js +25 -0
- package/dist/commands/daily.d.ts +7 -1
- package/dist/commands/daily.js +136 -57
- package/dist/commands/dashboard-cache.d.ts +69 -0
- package/dist/commands/dashboard-cache.js +219 -0
- package/dist/commands/dashboard-data.d.ts +18 -10
- package/dist/commands/dashboard-data.js +35 -7
- package/dist/commands/dashboard-gist-sync.d.ts +93 -0
- package/dist/commands/dashboard-gist-sync.js +237 -0
- package/dist/commands/dashboard-server.d.ts +6 -10
- package/dist/commands/dashboard-server.js +155 -222
- package/dist/commands/features.js +6 -0
- package/dist/commands/guidelines.d.ts +6 -0
- package/dist/commands/guidelines.js +7 -0
- package/dist/commands/index.d.ts +2 -5
- package/dist/commands/index.js +2 -4
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +7 -1
- package/dist/commands/list-mark-done.js +6 -21
- package/dist/commands/list-move-tier.js +3 -5
- package/dist/commands/locate-issue-list.d.ts +25 -0
- package/dist/commands/locate-issue-list.js +67 -0
- package/dist/commands/merge-loop.d.ts +63 -0
- package/dist/commands/merge-loop.js +157 -0
- package/dist/commands/repo-vet.js +40 -1
- package/dist/commands/scout-bridge.d.ts +35 -2
- package/dist/commands/scout-bridge.js +65 -13
- package/dist/commands/search.d.ts +4 -6
- package/dist/commands/search.js +58 -11
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +56 -2
- package/dist/commands/skip-file-parser.d.ts +23 -0
- package/dist/commands/skip-file-parser.js +23 -10
- package/dist/commands/startup.d.ts +1 -6
- package/dist/commands/startup.js +25 -59
- package/dist/commands/track.d.ts +2 -2
- package/dist/commands/track.js +2 -2
- package/dist/commands/vet-list.js +4 -0
- package/dist/core/config-registry.js +36 -0
- package/dist/core/daily-logic.d.ts +25 -2
- package/dist/core/daily-logic.js +58 -3
- package/dist/core/gist-health.d.ts +81 -0
- package/dist/core/gist-health.js +39 -0
- package/dist/core/gist-state-store.d.ts +3 -1
- package/dist/core/gist-state-store.js +7 -2
- package/dist/core/github-stats.d.ts +2 -2
- package/dist/core/github-stats.js +20 -4
- package/dist/core/index.d.ts +4 -3
- package/dist/core/index.js +4 -3
- package/dist/core/issue-conversation.js +8 -2
- package/dist/core/issue-grading.d.ts +9 -0
- package/dist/core/issue-grading.js +9 -0
- package/dist/core/pagination.d.ts +27 -0
- package/dist/core/pagination.js +23 -5
- package/dist/core/pr-comments-fetcher.d.ts +7 -0
- package/dist/core/pr-comments-fetcher.js +19 -8
- package/dist/core/pr-monitor.d.ts +2 -0
- package/dist/core/pr-monitor.js +26 -9
- package/dist/core/repo-score-manager.d.ts +2 -2
- package/dist/core/repo-score-manager.js +3 -3
- package/dist/core/repo-vet.d.ts +2 -2
- package/dist/core/repo-vet.js +1 -1
- package/dist/core/review-analysis.d.ts +19 -0
- package/dist/core/review-analysis.js +28 -0
- package/dist/core/state-schema.d.ts +43 -6
- package/dist/core/state-schema.js +81 -4
- package/dist/core/state.d.ts +36 -5
- package/dist/core/state.js +177 -28
- package/dist/core/strategy.js +6 -5
- package/dist/core/types.d.ts +8 -0
- package/dist/core/untrusted-content.d.ts +45 -0
- package/dist/core/untrusted-content.js +54 -0
- package/dist/formatters/json.d.ts +89 -6
- package/dist/formatters/json.js +65 -1
- package/package.json +2 -2
- package/dist/commands/shelve.d.ts +0 -45
- package/dist/commands/shelve.js +0 -54
package/dist/commands/init.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Init command
|
|
3
3
|
* Initialize with GitHub username. In v2, PRs are fetched fresh on each daily run.
|
|
4
4
|
*/
|
|
5
|
-
import { getStateManager } from '../core/index.js';
|
|
5
|
+
import { getStateManager, maybeCheckpoint } from '../core/index.js';
|
|
6
6
|
import { validateGitHubUsername } from './validation.js';
|
|
7
|
+
const MODULE = 'init';
|
|
7
8
|
/**
|
|
8
9
|
* Initialize with a GitHub username.
|
|
9
10
|
*
|
|
@@ -17,8 +18,13 @@ export async function runInit(options) {
|
|
|
17
18
|
const stateManager = getStateManager();
|
|
18
19
|
// Set username in config
|
|
19
20
|
stateManager.updateConfig({ githubUsername: options.username });
|
|
21
|
+
// Push the config mutation to the Gist in gist mode (no-op locally).
|
|
22
|
+
// Without this the change only hits the local cache and the next
|
|
23
|
+
// bootstrap reverts it from the Gist (#1440).
|
|
24
|
+
const gistSyncWarning = await maybeCheckpoint(stateManager, MODULE);
|
|
20
25
|
return {
|
|
21
26
|
username: options.username,
|
|
22
27
|
message: 'Username saved. Run `daily` to fetch your open PRs from GitHub.',
|
|
28
|
+
...(gistSyncWarning ? { gistSyncWarning } : {}),
|
|
23
29
|
};
|
|
24
30
|
}
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
import * as fs from 'node:fs';
|
|
18
18
|
import * as path from 'node:path';
|
|
19
19
|
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
20
|
+
import { ENTRY_LINE_RE, ENTRY_MARKER_RE, lineMentionsUrl } from './curated-list.js';
|
|
20
21
|
const STRIKE = '~~';
|
|
21
22
|
const DONE_PREFIX = ' - **Done**';
|
|
22
|
-
const ISSUE_LINE_RE = /^[*+-]\s/;
|
|
23
23
|
const REPO_HEADING_RE = /^###\s/;
|
|
24
24
|
const SECTION_BREAK_RE = /^#{1,3}\s/;
|
|
25
25
|
const PR_NUMBER_RE = /\/(?:pull|issues)\/(\d+)(?:[/?#]|$)/;
|
|
@@ -31,7 +31,7 @@ function prNumberLabel(prUrl) {
|
|
|
31
31
|
/** Wrap a line in `~~...~~` if it isn't already. Returns the input unchanged when already wrapped. */
|
|
32
32
|
function strikeLine(line) {
|
|
33
33
|
// Strip the leading list marker so we wrap the content, not the bullet.
|
|
34
|
-
const markerMatch = line.match(/^(?:[*+-]\s+|#{1,6}\s+)/);
|
|
34
|
+
const markerMatch = line.match(/^(?:[*+-]\s+|\d+\.\s+|#{1,6}\s+)/);
|
|
35
35
|
const prefix = markerMatch ? markerMatch[0] : '';
|
|
36
36
|
const body = line.slice(prefix.length);
|
|
37
37
|
if (body.startsWith(STRIKE) && body.endsWith(STRIKE) && body.length >= 4) {
|
|
@@ -39,26 +39,11 @@ function strikeLine(line) {
|
|
|
39
39
|
}
|
|
40
40
|
return { line: `${prefix}${STRIKE}${body}${STRIKE}`, alreadyStruck: false };
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
43
|
-
* Match the URL only when followed by a non-digit, non-word character (or
|
|
44
|
-
* end of line). A bare `includes(issueUrl)` would match `issues/1` against
|
|
45
|
-
* a line containing `issues/10`, so finding/marking issue 1 would mark
|
|
46
|
-
* whichever number-prefix line appears first.
|
|
47
|
-
*/
|
|
48
|
-
function lineMentionsUrl(line, issueUrl) {
|
|
49
|
-
const idx = line.indexOf(issueUrl);
|
|
50
|
-
if (idx === -1)
|
|
51
|
-
return false;
|
|
52
|
-
const next = line.charCodeAt(idx + issueUrl.length);
|
|
53
|
-
// NaN (end of string) → digit boundary OK. Otherwise reject any digit
|
|
54
|
-
// immediately after the URL so 'issues/1' doesn't match 'issues/10'.
|
|
55
|
-
return Number.isNaN(next) || next < 48 /* '0' */ || next > 57; /* '9' */
|
|
56
|
-
}
|
|
57
42
|
/** Find the issue block (issue line plus any indented sub-bullets) that mentions the URL. */
|
|
58
43
|
function findIssueBlock(lines, issueUrl) {
|
|
59
44
|
for (let i = 0; i < lines.length; i++) {
|
|
60
45
|
const line = lines[i];
|
|
61
|
-
if (!
|
|
46
|
+
if (!ENTRY_LINE_RE.test(line))
|
|
62
47
|
continue;
|
|
63
48
|
if (!lineMentionsUrl(line, issueUrl))
|
|
64
49
|
continue;
|
|
@@ -96,9 +81,9 @@ function countOpenIssues(lines, section) {
|
|
|
96
81
|
let open = 0;
|
|
97
82
|
for (let i = section.headingIndex + 1; i < section.end; i++) {
|
|
98
83
|
const line = lines[i];
|
|
99
|
-
if (!
|
|
84
|
+
if (!ENTRY_LINE_RE.test(line))
|
|
100
85
|
continue;
|
|
101
|
-
const body = line.replace(
|
|
86
|
+
const body = line.replace(ENTRY_MARKER_RE, '');
|
|
102
87
|
if (!(body.startsWith(STRIKE) && body.endsWith(STRIKE)))
|
|
103
88
|
open++;
|
|
104
89
|
}
|
|
@@ -120,7 +105,7 @@ export function markIssueAsDone(content, opts) {
|
|
|
120
105
|
};
|
|
121
106
|
}
|
|
122
107
|
const issueLine = lines[block.start];
|
|
123
|
-
const issueBody = issueLine.replace(
|
|
108
|
+
const issueBody = issueLine.replace(ENTRY_MARKER_RE, '');
|
|
124
109
|
const alreadyMarked = issueBody.startsWith(STRIKE) && issueBody.endsWith(STRIKE);
|
|
125
110
|
if (alreadyMarked) {
|
|
126
111
|
const section = findRepoSection(lines, block.start);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import * as fs from 'node:fs';
|
|
16
16
|
import * as path from 'node:path';
|
|
17
17
|
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
18
|
+
import { ENTRY_LINE_RE, lineMentionsUrl } from './curated-list.js';
|
|
18
19
|
const TIER_HEADERS = {
|
|
19
20
|
pursue: '## Pursue',
|
|
20
21
|
maybe: '## Maybe',
|
|
@@ -44,13 +45,10 @@ function tierForLine(lines, lineIndex) {
|
|
|
44
45
|
/** Locate every issue block (issue line + any sub-bullets) that mentions the URL. */
|
|
45
46
|
function findIssueBlocks(lines, issueUrl) {
|
|
46
47
|
const blocks = [];
|
|
47
|
-
// Use exact substring match on the URL — no regex escaping pitfalls and
|
|
48
|
-
// the URL itself contains no markdown delimiters that would split a line.
|
|
49
48
|
for (let i = 0; i < lines.length; i++) {
|
|
50
49
|
const line = lines[i];
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
if (!isTopLevelListItem || !line.includes(issueUrl))
|
|
50
|
+
// lineMentionsUrl (not a bare includes) so issues/1 can't match issues/10 (#1442).
|
|
51
|
+
if (!ENTRY_LINE_RE.test(line) || !lineMentionsUrl(line, issueUrl))
|
|
54
52
|
continue;
|
|
55
53
|
// Capture indented sub-bullets that follow this line.
|
|
56
54
|
let end = i + 1;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated-list path detection (#1463).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `startup.ts` so the daily merge-loop reconciliation can
|
|
5
|
+
* locate the list without importing `startup.ts` — that would close an
|
|
6
|
+
* import cycle (startup → daily → merge-loop → startup). `startup.ts`
|
|
7
|
+
* re-exports `parseIssueListPathFromConfig` for back-compat and layers item
|
|
8
|
+
* counts / skipped-issues detection on top of `detectIssueListPath`.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Parse issueListPath from a config file's YAML frontmatter.
|
|
12
|
+
* @param configContent - Raw content of the config.md file
|
|
13
|
+
* @returns The path string or undefined if not found
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseIssueListPathFromConfig(configContent: string): string | undefined;
|
|
16
|
+
export interface IssueListLocation {
|
|
17
|
+
path: string;
|
|
18
|
+
source: 'configured' | 'auto-detected';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Locate the curated issue-list file: state config first, then the legacy
|
|
22
|
+
* config.md frontmatter, then known default paths. Returns undefined when
|
|
23
|
+
* no list exists — callers treat that as "user has no curated list".
|
|
24
|
+
*/
|
|
25
|
+
export declare function detectIssueListPath(): IssueListLocation | undefined;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated-list path detection (#1463).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `startup.ts` so the daily merge-loop reconciliation can
|
|
5
|
+
* locate the list without importing `startup.ts` — that would close an
|
|
6
|
+
* import cycle (startup → daily → merge-loop → startup). `startup.ts`
|
|
7
|
+
* re-exports `parseIssueListPathFromConfig` for back-compat and layers item
|
|
8
|
+
* counts / skipped-issues detection on top of `detectIssueListPath`.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import { getStateManager } from '../core/index.js';
|
|
12
|
+
import { errorMessage } from '../core/errors.js';
|
|
13
|
+
import { warn } from '../core/logger.js';
|
|
14
|
+
/**
|
|
15
|
+
* Parse issueListPath from a config file's YAML frontmatter.
|
|
16
|
+
* @param configContent - Raw content of the config.md file
|
|
17
|
+
* @returns The path string or undefined if not found
|
|
18
|
+
*/
|
|
19
|
+
export function parseIssueListPathFromConfig(configContent) {
|
|
20
|
+
const match = configContent.match(/^---\n([\s\S]*?)\n---/);
|
|
21
|
+
if (!match)
|
|
22
|
+
return undefined;
|
|
23
|
+
const frontmatter = match[1];
|
|
24
|
+
const pathMatch = frontmatter.match(/issueListPath:\s*["']?([^"'\n]+)["']?/);
|
|
25
|
+
return pathMatch ? pathMatch[1].trim() : undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Locate the curated issue-list file: state config first, then the legacy
|
|
29
|
+
* config.md frontmatter, then known default paths. Returns undefined when
|
|
30
|
+
* no list exists — callers treat that as "user has no curated list".
|
|
31
|
+
*/
|
|
32
|
+
export function detectIssueListPath() {
|
|
33
|
+
// 1. Check state.json config (primary)
|
|
34
|
+
try {
|
|
35
|
+
const stateManager = getStateManager();
|
|
36
|
+
const configuredPath = stateManager.getState().config.issueListPath;
|
|
37
|
+
if (configuredPath && fs.existsSync(configuredPath)) {
|
|
38
|
+
return { path: configuredPath, source: 'configured' };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// State manager may not be initialized yet — fall through to legacy config.md
|
|
43
|
+
warn('startup', `Could not read issueListPath from state: ${errorMessage(error)}`);
|
|
44
|
+
}
|
|
45
|
+
// 2. Fallback: config.md (legacy — will be removed in future)
|
|
46
|
+
const configPath = '.claude/oss-autopilot/config.md';
|
|
47
|
+
if (fs.existsSync(configPath)) {
|
|
48
|
+
try {
|
|
49
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
50
|
+
const configuredPath = parseIssueListPathFromConfig(configContent);
|
|
51
|
+
if (configuredPath && fs.existsSync(configuredPath)) {
|
|
52
|
+
return { path: configuredPath, source: 'configured' };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error('[STARTUP] Failed to read config:', errorMessage(error));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// 3. Probe known paths
|
|
60
|
+
const probes = ['open-source/potential-issue-list.md', 'oss/issue-list.md', 'issues.md'];
|
|
61
|
+
for (const probe of probes) {
|
|
62
|
+
if (fs.existsSync(probe)) {
|
|
63
|
+
return { path: probe, source: 'auto-detected' };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Close the merge loop (#1463).
|
|
3
|
+
*
|
|
4
|
+
* When the daily check detects recently merged PRs, nothing previously
|
|
5
|
+
* connected the merge back to the curated issue list — `list-mark-done` is
|
|
6
|
+
* only offered interactively at PR-creation time (draft-first-workflow
|
|
7
|
+
* Step 10), so a skipped prompt left the entry open forever.
|
|
8
|
+
*
|
|
9
|
+
* The join: neither the merged-PR search result (url/repo/number/title/
|
|
10
|
+
* mergedAt) nor `state.activeIssues` (TrackedIssue — issue URL only, no PR
|
|
11
|
+
* URL) carries a deterministic PR→issue link. The only deterministic
|
|
12
|
+
* linkage lives in the curated list itself: workflows record the PR URL in
|
|
13
|
+
* an entry's sub-bullets (`**Done**` at Step 10, or in-progress markers
|
|
14
|
+
* like `**In Progress** — PR [#N](url)`). So the reconciliation traverses
|
|
15
|
+
* the list file's entry blocks for the merged PR URL, recovers the entry's
|
|
16
|
+
* own URL from its entry line, and marks it done via the exact transform
|
|
17
|
+
* `list-mark-done` uses. Repo-level guessing (matching a merged PR to "some
|
|
18
|
+
* claimed issue in the same repo") is deliberately avoided — it could
|
|
19
|
+
* strike the wrong entry when a repo has several listed issues.
|
|
20
|
+
*
|
|
21
|
+
* Auto vs offer: marking a list entry done for a PR that GitHub reports as
|
|
22
|
+
* MERGED is safe and idempotent (already-marked entries are quiet no-ops),
|
|
23
|
+
* so it runs automatically. The extract-learnings nudge stays an offer —
|
|
24
|
+
* it costs API calls and an LLM pass — surfaced via the action menu's
|
|
25
|
+
* `extract_learnings` item (see computeActionMenu in core/daily-logic.ts).
|
|
26
|
+
*
|
|
27
|
+
* Failures never crash the daily run: they land in `warnings[]` under the
|
|
28
|
+
* `merge-loop` phase.
|
|
29
|
+
*/
|
|
30
|
+
import type { DailyWarning, MergedPRListUpdate } from '../formatters/json.js';
|
|
31
|
+
/**
|
|
32
|
+
* Find the curated-list entry whose block (entry line plus indented
|
|
33
|
+
* sub-bullets) mentions `prUrl` on a STATUS-marked line, and return the
|
|
34
|
+
* entry line's own GitHub URL — the URL `markIssueAsDone` needs to locate
|
|
35
|
+
* the block again.
|
|
36
|
+
*
|
|
37
|
+
* Returns undefined when no entry block mentions the PR URL on a marked
|
|
38
|
+
* line, or when the mentioning block's entry line carries no GitHub URL
|
|
39
|
+
* (nothing for the mark-done transform to anchor on).
|
|
40
|
+
*/
|
|
41
|
+
export declare function findListEntryUrlByPrUrl(content: string, prUrl: string): string | undefined;
|
|
42
|
+
export interface ReconcileMergedPRsInput {
|
|
43
|
+
/** Recently merged PRs from the daily fetch (Phase 1). */
|
|
44
|
+
mergedPRs: Array<{
|
|
45
|
+
url: string;
|
|
46
|
+
mergedAt?: string;
|
|
47
|
+
}>;
|
|
48
|
+
/** Shared daily warnings collector — failures land here, never throw. */
|
|
49
|
+
warnings: DailyWarning[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Auto-mark curated-list entries done for recently merged PRs.
|
|
53
|
+
*
|
|
54
|
+
* Silent no-op cases (by design — these are normal, not failures):
|
|
55
|
+
* - no merged PRs this run
|
|
56
|
+
* - no curated list configured/detected
|
|
57
|
+
* - no list entry mentions a merged PR's URL
|
|
58
|
+
* - the matching entry is already marked done (idempotent re-run)
|
|
59
|
+
*
|
|
60
|
+
* Returns the entries actually struck this run, or undefined when nothing
|
|
61
|
+
* changed — callers omit the field from output rather than emitting `[]`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function reconcileMergedPRsWithList(input: ReconcileMergedPRsInput): MergedPRListUpdate[] | undefined;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Close the merge loop (#1463).
|
|
3
|
+
*
|
|
4
|
+
* When the daily check detects recently merged PRs, nothing previously
|
|
5
|
+
* connected the merge back to the curated issue list — `list-mark-done` is
|
|
6
|
+
* only offered interactively at PR-creation time (draft-first-workflow
|
|
7
|
+
* Step 10), so a skipped prompt left the entry open forever.
|
|
8
|
+
*
|
|
9
|
+
* The join: neither the merged-PR search result (url/repo/number/title/
|
|
10
|
+
* mergedAt) nor `state.activeIssues` (TrackedIssue — issue URL only, no PR
|
|
11
|
+
* URL) carries a deterministic PR→issue link. The only deterministic
|
|
12
|
+
* linkage lives in the curated list itself: workflows record the PR URL in
|
|
13
|
+
* an entry's sub-bullets (`**Done**` at Step 10, or in-progress markers
|
|
14
|
+
* like `**In Progress** — PR [#N](url)`). So the reconciliation traverses
|
|
15
|
+
* the list file's entry blocks for the merged PR URL, recovers the entry's
|
|
16
|
+
* own URL from its entry line, and marks it done via the exact transform
|
|
17
|
+
* `list-mark-done` uses. Repo-level guessing (matching a merged PR to "some
|
|
18
|
+
* claimed issue in the same repo") is deliberately avoided — it could
|
|
19
|
+
* strike the wrong entry when a repo has several listed issues.
|
|
20
|
+
*
|
|
21
|
+
* Auto vs offer: marking a list entry done for a PR that GitHub reports as
|
|
22
|
+
* MERGED is safe and idempotent (already-marked entries are quiet no-ops),
|
|
23
|
+
* so it runs automatically. The extract-learnings nudge stays an offer —
|
|
24
|
+
* it costs API calls and an LLM pass — surfaced via the action menu's
|
|
25
|
+
* `extract_learnings` item (see computeActionMenu in core/daily-logic.ts).
|
|
26
|
+
*
|
|
27
|
+
* Failures never crash the daily run: they land in `warnings[]` under the
|
|
28
|
+
* `merge-loop` phase.
|
|
29
|
+
*/
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import { errorMessage } from '../core/errors.js';
|
|
33
|
+
import { warn } from '../core/logger.js';
|
|
34
|
+
import { ENTRY_LINE_RE, lineMentionsUrl } from './curated-list.js';
|
|
35
|
+
import { markIssueAsDone } from './list-mark-done.js';
|
|
36
|
+
import { detectIssueListPath } from './locate-issue-list.js';
|
|
37
|
+
const MODULE = 'merge-loop';
|
|
38
|
+
/** First GitHub issue-or-PR URL on a line, without trailing punctuation. */
|
|
39
|
+
const GITHUB_URL_RE = /https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/(?:issues|pull)\/\d+/;
|
|
40
|
+
/**
|
|
41
|
+
* A line counts as a STATUS sub-bullet only when it carries one of the
|
|
42
|
+
* markers the workflows write next to an entry's own PR (`**Done**`,
|
|
43
|
+
* `**In Progress**`, or a `PR [#N](...)` reference). A bare URL mention
|
|
44
|
+
* (e.g. a hand-written "blocked by <pr-url>" note under a DIFFERENT
|
|
45
|
+
* entry) must not auto-strike that entry (#1463 review).
|
|
46
|
+
*/
|
|
47
|
+
const STATUS_MARKER_RE = /\*\*(?:Done|In Progress)\*\*|PR \[#?\d/;
|
|
48
|
+
/**
|
|
49
|
+
* Find the curated-list entry whose block (entry line plus indented
|
|
50
|
+
* sub-bullets) mentions `prUrl` on a STATUS-marked line, and return the
|
|
51
|
+
* entry line's own GitHub URL — the URL `markIssueAsDone` needs to locate
|
|
52
|
+
* the block again.
|
|
53
|
+
*
|
|
54
|
+
* Returns undefined when no entry block mentions the PR URL on a marked
|
|
55
|
+
* line, or when the mentioning block's entry line carries no GitHub URL
|
|
56
|
+
* (nothing for the mark-done transform to anchor on).
|
|
57
|
+
*/
|
|
58
|
+
export function findListEntryUrlByPrUrl(content, prUrl) {
|
|
59
|
+
const lines = content.split('\n');
|
|
60
|
+
let i = 0;
|
|
61
|
+
while (i < lines.length) {
|
|
62
|
+
if (!ENTRY_LINE_RE.test(lines[i])) {
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Block = entry line + following indented sub-bullets (same shape as
|
|
67
|
+
// list-mark-done's findIssueBlock).
|
|
68
|
+
let end = i + 1;
|
|
69
|
+
while (end < lines.length && /^\s{2,}/.test(lines[end]))
|
|
70
|
+
end++;
|
|
71
|
+
// The entry line mentioning the PR itself is self-anchored (no
|
|
72
|
+
// cross-entry ambiguity); sub-bullets must carry a status marker.
|
|
73
|
+
const entryMentions = lineMentionsUrl(lines[i], prUrl);
|
|
74
|
+
const markedSubBulletMentions = lines
|
|
75
|
+
.slice(i + 1, end)
|
|
76
|
+
.some((line) => lineMentionsUrl(line, prUrl) && STATUS_MARKER_RE.test(line));
|
|
77
|
+
if (entryMentions || markedSubBulletMentions) {
|
|
78
|
+
const match = GITHUB_URL_RE.exec(lines[i]);
|
|
79
|
+
if (match)
|
|
80
|
+
return match[0];
|
|
81
|
+
// Entry line has no URL to anchor a mark-done on — keep scanning in
|
|
82
|
+
// case a later block also mentions the PR (e.g. duplicate sections).
|
|
83
|
+
}
|
|
84
|
+
i = end;
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Auto-mark curated-list entries done for recently merged PRs.
|
|
90
|
+
*
|
|
91
|
+
* Silent no-op cases (by design — these are normal, not failures):
|
|
92
|
+
* - no merged PRs this run
|
|
93
|
+
* - no curated list configured/detected
|
|
94
|
+
* - no list entry mentions a merged PR's URL
|
|
95
|
+
* - the matching entry is already marked done (idempotent re-run)
|
|
96
|
+
*
|
|
97
|
+
* Returns the entries actually struck this run, or undefined when nothing
|
|
98
|
+
* changed — callers omit the field from output rather than emitting `[]`.
|
|
99
|
+
*/
|
|
100
|
+
export function reconcileMergedPRsWithList(input) {
|
|
101
|
+
const { mergedPRs, warnings } = input;
|
|
102
|
+
if (mergedPRs.length === 0)
|
|
103
|
+
return undefined;
|
|
104
|
+
const located = detectIssueListPath();
|
|
105
|
+
if (!located)
|
|
106
|
+
return undefined;
|
|
107
|
+
const listPath = path.resolve(located.path);
|
|
108
|
+
let content;
|
|
109
|
+
try {
|
|
110
|
+
content = fs.readFileSync(listPath, 'utf8');
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const message = `read issue list at ${listPath}: ${errorMessage(error)}`;
|
|
114
|
+
warnings.push({ phase: 'merge-loop', operation: 'auto-mark merged list entries', message });
|
|
115
|
+
warn(MODULE, message);
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const updates = [];
|
|
119
|
+
for (const pr of mergedPRs) {
|
|
120
|
+
const entryUrl = findListEntryUrlByPrUrl(content, pr.url);
|
|
121
|
+
if (!entryUrl)
|
|
122
|
+
continue;
|
|
123
|
+
const prStatus = pr.mergedAt ? `merged ${pr.mergedAt.slice(0, 10)}` : 'merged';
|
|
124
|
+
const result = markIssueAsDone(content, { issueUrl: entryUrl, prUrl: pr.url, prStatus });
|
|
125
|
+
if (!result.marked)
|
|
126
|
+
continue; // already-marked (or not-found race) — quiet no-op
|
|
127
|
+
content = result.content;
|
|
128
|
+
updates.push({
|
|
129
|
+
prUrl: pr.url,
|
|
130
|
+
issueUrl: entryUrl,
|
|
131
|
+
repoHeadingStruck: result.repoHeadingStruck,
|
|
132
|
+
listPath,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (updates.length === 0)
|
|
136
|
+
return undefined;
|
|
137
|
+
// Atomic write (tmp + rename), mirroring runMarkIssueListItemDone.
|
|
138
|
+
const tmp = `${listPath}.tmp-${process.pid}-${Date.now()}`;
|
|
139
|
+
try {
|
|
140
|
+
fs.writeFileSync(tmp, content, 'utf8');
|
|
141
|
+
fs.renameSync(tmp, listPath);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
try {
|
|
145
|
+
fs.unlinkSync(tmp);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// best-effort cleanup
|
|
149
|
+
}
|
|
150
|
+
const message = `write issue list at ${listPath}: ${errorMessage(error)}`;
|
|
151
|
+
warnings.push({ phase: 'merge-loop', operation: 'auto-mark merged list entries', message });
|
|
152
|
+
warn(MODULE, message);
|
|
153
|
+
// Nothing persisted — do not report updates that are not on disk.
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
return updates;
|
|
157
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Same architectural shape as `compliance-score`: read-only API calls,
|
|
11
11
|
* no state mutation, runs against a public `owner/repo` slug.
|
|
12
12
|
*/
|
|
13
|
-
import { getOctokit, requireGitHubToken } from '../core/index.js';
|
|
13
|
+
import { getOctokit, getStateManager, requireGitHubToken } from '../core/index.js';
|
|
14
14
|
import { errorMessage, getHttpStatusCode, isRateLimitOrAuthError } from '../core/errors.js';
|
|
15
15
|
import { warn } from '../core/logger.js';
|
|
16
16
|
import { validateRepoIdentifier } from './validation.js';
|
|
@@ -102,6 +102,39 @@ async function checkCommunityHealth(octokit, owner, repo) {
|
|
|
102
102
|
incomplete,
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Look up the user's cached HISTORY score for a repo slug (#1465).
|
|
107
|
+
*
|
|
108
|
+
* This is the relationship score from `state.repoScores` (the user's own
|
|
109
|
+
* merge outcomes, computed by `repo-score-manager.ts`) — a different number
|
|
110
|
+
* from the fresh `rubricScore` this command computes. Surfacing both in one
|
|
111
|
+
* envelope, under distinct names, is the point: agents must never quote one
|
|
112
|
+
* as the other. See docs/repo-scores.md.
|
|
113
|
+
*
|
|
114
|
+
* State keys come from parsed GitHub URLs and preserve their original case,
|
|
115
|
+
* while the user types the slug by hand — GitHub slugs are case-insensitive,
|
|
116
|
+
* so fall back to a case-insensitive scan before reporting "no history".
|
|
117
|
+
* Returns `undefined` (field omitted from output) when the user has no
|
|
118
|
+
* cached score for the repo.
|
|
119
|
+
*/
|
|
120
|
+
function lookupHistoryScore(repoSlug) {
|
|
121
|
+
// Best-effort by design: repo-vet ran for years without touching local
|
|
122
|
+
// state, and an unreadable state file (EACCES rethrows from loadState)
|
|
123
|
+
// must degrade to "no history", not abort the vet (#1465 review).
|
|
124
|
+
try {
|
|
125
|
+
const stateManager = getStateManager();
|
|
126
|
+
const exact = stateManager.getRepoScore(repoSlug);
|
|
127
|
+
if (exact)
|
|
128
|
+
return exact.score;
|
|
129
|
+
const lower = repoSlug.toLowerCase();
|
|
130
|
+
const match = Object.values(stateManager.getState().repoScores).find((rs) => rs.repo.toLowerCase() === lower);
|
|
131
|
+
return match?.score;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
warn(MODULE, `History-score lookup skipped (state unavailable): ${errorMessage(error)}`);
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
105
138
|
function summarizePRMerges(prs, windowDays, now) {
|
|
106
139
|
const cutoff = now.getTime() - windowDays * DAY_MS;
|
|
107
140
|
const prMergeTimesDays = [];
|
|
@@ -226,6 +259,11 @@ export async function runRepoVet(options) {
|
|
|
226
259
|
// rubric score — the score reflects the fetched signals as-is and the
|
|
227
260
|
// flag tells consumers which signal was unverified.
|
|
228
261
|
const { repo: repoMeta, communityHealth, maintainerActivity, ...rest } = result;
|
|
262
|
+
// #1465: name both repo scores in one envelope. `rubricScore` is the fresh
|
|
263
|
+
// health score computed above; `historyScore` is the user's cached
|
|
264
|
+
// relationship score when one exists. Optional so the output is unchanged
|
|
265
|
+
// for repos the user has no history with (back-compat).
|
|
266
|
+
const historyScore = lookupHistoryScore(options.repo);
|
|
229
267
|
return {
|
|
230
268
|
repoSlug: options.repo,
|
|
231
269
|
fetchedAt: now.toISOString(),
|
|
@@ -233,5 +271,6 @@ export async function runRepoVet(options) {
|
|
|
233
271
|
communityHealth: { ...communityHealth, incomplete: communityHealthSummary.incomplete },
|
|
234
272
|
maintainerActivity: { ...maintainerActivity, releasesIncomplete },
|
|
235
273
|
...rest,
|
|
274
|
+
...(historyScore !== undefined ? { historyScore } : {}),
|
|
236
275
|
};
|
|
237
276
|
}
|
|
@@ -5,6 +5,32 @@
|
|
|
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
7
|
import type { CandidateLinkedPR } from '../formatters/json.js';
|
|
8
|
+
/**
|
|
9
|
+
* Fraction of search slots reserved for candidates that matched neither
|
|
10
|
+
* strategy-preferred languages nor repos (#1244). Counterweight against
|
|
11
|
+
* echo-chamber bias: without it, strategy-boosted searches return more of
|
|
12
|
+
* what already merged, which merges more of the same, and the profile
|
|
13
|
+
* narrows over time. Scout clamps to [0, 1]; 0.2 is the issue's proposal.
|
|
14
|
+
*
|
|
15
|
+
* Lives here (not `search.ts`, which re-exports it) since #1464: the same
|
|
16
|
+
* ratio is baked into the `ScoutPreferences` built by {@link buildScoutState}
|
|
17
|
+
* so every scout surface shares one policy.
|
|
18
|
+
*/
|
|
19
|
+
export declare const SEARCH_DIVERSITY_RATIO = 0.2;
|
|
20
|
+
/**
|
|
21
|
+
* Degradation signals collected while building a scout state (#1448).
|
|
22
|
+
* Callers pass an empty object and inspect it after the build — threading an
|
|
23
|
+
* out-param keeps `buildScoutState`/`createAutopilotScout` return types
|
|
24
|
+
* unchanged for the five command call sites.
|
|
25
|
+
*/
|
|
26
|
+
export interface ScoutBridgeDiagnostics {
|
|
27
|
+
/**
|
|
28
|
+
* Set when a configured skipped-issues file exists but could not be read.
|
|
29
|
+
* The scout state was built with an EMPTY skip list, so explicitly-skipped
|
|
30
|
+
* issues may resurface in results.
|
|
31
|
+
*/
|
|
32
|
+
skipListUnavailable?: boolean;
|
|
33
|
+
}
|
|
8
34
|
/**
|
|
9
35
|
* Convert scout 0.6.0's `LinkedPR` (separate `state` + `merged`) into the
|
|
10
36
|
* shape `classifyLinkedPR` expects (`state` already folded with `merged`).
|
|
@@ -29,10 +55,17 @@ export declare function buildCandidateLinkedPR(scoutLinkedPR: ScoutLinkedPR | nu
|
|
|
29
55
|
/**
|
|
30
56
|
* Build a ScoutState from the current AgentState.
|
|
31
57
|
* Maps oss-autopilot's config and state fields to oss-scout's state format.
|
|
58
|
+
*
|
|
59
|
+
* @param diagnostics - Optional collector for degradation signals (#1448);
|
|
60
|
+
* `skipListUnavailable` is set when the skipped-issues file exists but
|
|
61
|
+
* could not be read.
|
|
32
62
|
*/
|
|
33
|
-
export declare function buildScoutState(): ScoutState;
|
|
63
|
+
export declare function buildScoutState(diagnostics?: ScoutBridgeDiagnostics): ScoutState;
|
|
34
64
|
/**
|
|
35
65
|
* Create an OssScout instance backed by the current AgentState.
|
|
36
66
|
* Uses 'provided' persistence so scout reads from oss-autopilot's state.
|
|
67
|
+
*
|
|
68
|
+
* @param diagnostics - Optional collector for degradation signals (#1448),
|
|
69
|
+
* forwarded to {@link buildScoutState}.
|
|
37
70
|
*/
|
|
38
|
-
export declare function createAutopilotScout(): Promise<OssScout>;
|
|
71
|
+
export declare function createAutopilotScout(diagnostics?: ScoutBridgeDiagnostics): Promise<OssScout>;
|