@oss-autopilot/core 3.0.1 → 3.2.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 +80 -0
- package/dist/cli.bundle.cjs +67 -65
- package/dist/commands/guidelines.d.ts +67 -0
- package/dist/commands/guidelines.js +147 -0
- package/dist/commands/index.d.ts +9 -0
- package/dist/commands/index.js +9 -0
- package/dist/commands/scout-bridge.d.ts +12 -1
- package/dist/commands/scout-bridge.js +19 -0
- package/dist/commands/vet-list.js +10 -2
- package/dist/commands/vet.js +10 -1
- package/dist/core/gist-state-store.d.ts +1 -1
- package/dist/core/gist-state-store.js +8 -2
- package/dist/core/guidelines-store.d.ts +74 -0
- package/dist/core/guidelines-store.js +130 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/pr-comments-fetcher.d.ts +67 -0
- package/dist/core/pr-comments-fetcher.js +125 -0
- package/dist/core/state-persistence.d.ts +6 -0
- package/dist/core/state-persistence.js +20 -2
- package/dist/core/state-schema.d.ts +9 -1
- package/dist/core/state-schema.js +20 -1
- package/dist/core/state.d.ts +33 -0
- package/dist/core/state.js +70 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/core/types.js +2 -2
- package/dist/formatters/json.d.ts +28 -0
- package/package.json +7 -7
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type PRCommentBundle } from '../core/pr-comments-fetcher.js';
|
|
2
|
+
export interface GuidelinesViewOutput {
|
|
3
|
+
repo: string;
|
|
4
|
+
/** Markdown content, or null when the repo has no guidelines stored. */
|
|
5
|
+
content: string | null;
|
|
6
|
+
/** UTF-8 byte size of `content`, or 0 when content is null. */
|
|
7
|
+
byteSize: number;
|
|
8
|
+
/** Whether a guidelines file exists for this repo. */
|
|
9
|
+
exists: boolean;
|
|
10
|
+
/** Where the guidelines would be persisted if a write happened now. */
|
|
11
|
+
storageMode: 'gist' | 'local-unavailable';
|
|
12
|
+
}
|
|
13
|
+
export interface GuidelinesStoreOutput {
|
|
14
|
+
repo: string;
|
|
15
|
+
byteSize: number;
|
|
16
|
+
stored: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface GuidelinesResetOutput {
|
|
19
|
+
repo: string;
|
|
20
|
+
/** True when an existing file was tombstoned, false when no file existed. */
|
|
21
|
+
deleted: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface FetchCorpusOutput {
|
|
24
|
+
repo: string;
|
|
25
|
+
bundles: PRCommentBundle[];
|
|
26
|
+
/** How many PRs were considered after recency + already-fetched filtering. */
|
|
27
|
+
prCount: number;
|
|
28
|
+
/** PRs skipped because `commentsFetchedAt` is already set (without --force). */
|
|
29
|
+
skipped: number;
|
|
30
|
+
}
|
|
31
|
+
interface RepoOption {
|
|
32
|
+
repo: string;
|
|
33
|
+
}
|
|
34
|
+
interface FetchCorpusOptions extends RepoOption {
|
|
35
|
+
/** Cap on PRs to process. Defaults to 5; capped at 10 to bound prompt size. */
|
|
36
|
+
limit?: number;
|
|
37
|
+
/** Re-fetch even when commentsFetchedAt is already set. */
|
|
38
|
+
forceRefetch?: boolean;
|
|
39
|
+
}
|
|
40
|
+
interface StoreOptions extends RepoOption {
|
|
41
|
+
content: string;
|
|
42
|
+
}
|
|
43
|
+
/** Read the per-repo guidelines for `repo`. Returns a `local-unavailable` envelope in non-Gist mode. */
|
|
44
|
+
export declare function runGuidelinesView(options: RepoOption): Promise<GuidelinesViewOutput>;
|
|
45
|
+
/**
|
|
46
|
+
* Persist per-repo guidelines for `repo`. Throws when not in Gist mode or
|
|
47
|
+
* when content exceeds the byte budget — the CLI surface relies on the
|
|
48
|
+
* outputJson error envelope to surface that to the host cleanly.
|
|
49
|
+
*/
|
|
50
|
+
export declare function runGuidelinesStore(options: StoreOptions): Promise<GuidelinesStoreOutput>;
|
|
51
|
+
/** Tombstone the guidelines file for `repo`. */
|
|
52
|
+
export declare function runGuidelinesReset(options: RepoOption): Promise<GuidelinesResetOutput>;
|
|
53
|
+
/**
|
|
54
|
+
* Fetch raw PR comment bundles for the most recent merged/closed PRs in `repo`
|
|
55
|
+
* that haven't already been processed. Returns a serializable corpus for the
|
|
56
|
+
* host's extract-learnings prompt.
|
|
57
|
+
*
|
|
58
|
+
* Filters applied at the CLI layer (not the fetcher):
|
|
59
|
+
* - Only PRs in the requested `repo`
|
|
60
|
+
* - PRs older than 12 months are dropped (recency cliff)
|
|
61
|
+
* - PRs with `commentsFetchedAt` already set are skipped unless `forceRefetch`
|
|
62
|
+
* - Capped at `limit` (default 5, max 10)
|
|
63
|
+
*
|
|
64
|
+
* Stamps `commentsFetchedAt` on every PR that was successfully fetched.
|
|
65
|
+
*/
|
|
66
|
+
export declare function runFetchCorpus(options: FetchCorpusOptions): Promise<FetchCorpusOutput>;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guidelines CLI commands (#867 PR 4).
|
|
3
|
+
*
|
|
4
|
+
* `guidelines view` — read the per-repo guidelines file from the Gist.
|
|
5
|
+
* `guidelines store` — overwrite the per-repo guidelines file.
|
|
6
|
+
* `guidelines reset` — tombstone the file so subsequent reads return null.
|
|
7
|
+
* `guidelines fetch-corpus` — pull raw PR comment bundles for the host's
|
|
8
|
+
* extract-learnings prompt to consume.
|
|
9
|
+
*
|
|
10
|
+
* The CLI layer is pure data plumbing — extraction is the host's job.
|
|
11
|
+
* Standalone-mode users see "not available" on store/reset/fetch-corpus
|
|
12
|
+
* because per-repo guidelines require Gist persistence to be useful.
|
|
13
|
+
*/
|
|
14
|
+
import { getStateManager, requireGitHubToken, getOctokit, GuidelinesNotAvailableError } from '../core/index.js';
|
|
15
|
+
import { fetchPRCommentBundlesBatch } from '../core/pr-comments-fetcher.js';
|
|
16
|
+
import { warn } from '../core/logger.js';
|
|
17
|
+
const MODULE = 'guidelines';
|
|
18
|
+
const REPO_ID_PATTERN = /^[^/]+\/[^/]+$/;
|
|
19
|
+
const DEFAULT_FETCH_LIMIT = 5;
|
|
20
|
+
const MAX_FETCH_LIMIT = 10;
|
|
21
|
+
/**
|
|
22
|
+
* Cliff at 12 months: PRs older than this are excluded from corpus fetches
|
|
23
|
+
* (#867 design decision §5). Computed at call time, not module load.
|
|
24
|
+
*/
|
|
25
|
+
const RECENCY_CLIFF_MS = 365 * 24 * 60 * 60 * 1000;
|
|
26
|
+
function validateRepo(repo) {
|
|
27
|
+
if (!REPO_ID_PATTERN.test(repo)) {
|
|
28
|
+
throw new Error(`Invalid repo identifier "${repo}". Expected "owner/repo" format.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Read the per-repo guidelines for `repo`. Returns a `local-unavailable` envelope in non-Gist mode. */
|
|
32
|
+
export async function runGuidelinesView(options) {
|
|
33
|
+
validateRepo(options.repo);
|
|
34
|
+
const sm = getStateManager();
|
|
35
|
+
const content = sm.getGuidelines(options.repo);
|
|
36
|
+
return {
|
|
37
|
+
repo: options.repo,
|
|
38
|
+
content,
|
|
39
|
+
byteSize: content === null ? 0 : Buffer.byteLength(content, 'utf-8'),
|
|
40
|
+
exists: content !== null,
|
|
41
|
+
storageMode: sm.isGuidelinesAvailable() ? 'gist' : 'local-unavailable',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Persist per-repo guidelines for `repo`. Throws when not in Gist mode or
|
|
46
|
+
* when content exceeds the byte budget — the CLI surface relies on the
|
|
47
|
+
* outputJson error envelope to surface that to the host cleanly.
|
|
48
|
+
*/
|
|
49
|
+
export async function runGuidelinesStore(options) {
|
|
50
|
+
validateRepo(options.repo);
|
|
51
|
+
if (!options.content || options.content.length === 0) {
|
|
52
|
+
throw new Error('Cannot store empty content. Use `guidelines reset` to delete a guidelines file.');
|
|
53
|
+
}
|
|
54
|
+
const sm = getStateManager();
|
|
55
|
+
// Throws GuidelinesNotAvailableError or GuidelinesTooLargeError on failure.
|
|
56
|
+
sm.setGuidelines(options.repo, options.content);
|
|
57
|
+
return {
|
|
58
|
+
repo: options.repo,
|
|
59
|
+
byteSize: Buffer.byteLength(options.content, 'utf-8'),
|
|
60
|
+
stored: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Tombstone the guidelines file for `repo`. */
|
|
64
|
+
export async function runGuidelinesReset(options) {
|
|
65
|
+
validateRepo(options.repo);
|
|
66
|
+
const sm = getStateManager();
|
|
67
|
+
if (!sm.isGuidelinesAvailable()) {
|
|
68
|
+
throw new GuidelinesNotAvailableError();
|
|
69
|
+
}
|
|
70
|
+
const existed = sm.getGuidelines(options.repo) !== null;
|
|
71
|
+
if (existed) {
|
|
72
|
+
sm.deleteGuidelines(options.repo);
|
|
73
|
+
}
|
|
74
|
+
return { repo: options.repo, deleted: existed };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fetch raw PR comment bundles for the most recent merged/closed PRs in `repo`
|
|
78
|
+
* that haven't already been processed. Returns a serializable corpus for the
|
|
79
|
+
* host's extract-learnings prompt.
|
|
80
|
+
*
|
|
81
|
+
* Filters applied at the CLI layer (not the fetcher):
|
|
82
|
+
* - Only PRs in the requested `repo`
|
|
83
|
+
* - PRs older than 12 months are dropped (recency cliff)
|
|
84
|
+
* - PRs with `commentsFetchedAt` already set are skipped unless `forceRefetch`
|
|
85
|
+
* - Capped at `limit` (default 5, max 10)
|
|
86
|
+
*
|
|
87
|
+
* Stamps `commentsFetchedAt` on every PR that was successfully fetched.
|
|
88
|
+
*/
|
|
89
|
+
export async function runFetchCorpus(options) {
|
|
90
|
+
validateRepo(options.repo);
|
|
91
|
+
const limit = clampLimit(options.limit);
|
|
92
|
+
const sm = getStateManager();
|
|
93
|
+
const merged = sm.getMergedPRs() ?? [];
|
|
94
|
+
const closed = sm.getClosedPRs() ?? [];
|
|
95
|
+
const cutoffMs = Date.now() - RECENCY_CLIFF_MS;
|
|
96
|
+
/** Treat both merged and closed-without-merge PRs as candidates. */
|
|
97
|
+
const candidates = [
|
|
98
|
+
...merged.map((pr) => ({ url: pr.url, timestamp: pr.mergedAt, alreadyFetched: !!pr.commentsFetchedAt })),
|
|
99
|
+
...closed.map((pr) => ({ url: pr.url, timestamp: pr.closedAt, alreadyFetched: !!pr.commentsFetchedAt })),
|
|
100
|
+
];
|
|
101
|
+
const repoUrlPrefix = `https://github.com/${options.repo}/`;
|
|
102
|
+
const eligible = candidates.filter((c) => {
|
|
103
|
+
if (!c.url.startsWith(repoUrlPrefix))
|
|
104
|
+
return false;
|
|
105
|
+
if (Date.parse(c.timestamp || '') < cutoffMs)
|
|
106
|
+
return false;
|
|
107
|
+
return true;
|
|
108
|
+
});
|
|
109
|
+
// Skipped count tracks ONLY the eligibility-passing PRs that were excluded
|
|
110
|
+
// for already-fetched. PRs filtered by repo/recency aren't "skipped" — they
|
|
111
|
+
// just don't apply to this repo or cutoff.
|
|
112
|
+
const skipped = options.forceRefetch ? 0 : eligible.filter((c) => c.alreadyFetched).length;
|
|
113
|
+
const toFetch = (options.forceRefetch ? eligible : eligible.filter((c) => !c.alreadyFetched))
|
|
114
|
+
// Most-recent first so the host always sees the freshest signal in its corpus window.
|
|
115
|
+
.sort((a, b) => Date.parse(b.timestamp || '') - Date.parse(a.timestamp || ''))
|
|
116
|
+
.slice(0, limit);
|
|
117
|
+
if (toFetch.length === 0) {
|
|
118
|
+
return { repo: options.repo, bundles: [], prCount: 0, skipped };
|
|
119
|
+
}
|
|
120
|
+
const token = requireGitHubToken();
|
|
121
|
+
const octokit = getOctokit(token);
|
|
122
|
+
const username = sm.getState().config.githubUsername;
|
|
123
|
+
if (!username) {
|
|
124
|
+
warn(MODULE, 'githubUsername is not set; bot/own-comment filtering will be incomplete');
|
|
125
|
+
}
|
|
126
|
+
const bundles = await fetchPRCommentBundlesBatch(octokit, toFetch.map((c) => c.url), username);
|
|
127
|
+
// Stamp commentsFetchedAt on each PR we successfully fetched — the bundle
|
|
128
|
+
// list mirrors the input order until paginateAll fan-out, so we mark on
|
|
129
|
+
// a per-bundle basis to be safe.
|
|
130
|
+
const now = new Date().toISOString();
|
|
131
|
+
for (const bundle of bundles) {
|
|
132
|
+
sm.markPRCommentsFetched(bundle.prUrl, now);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
repo: options.repo,
|
|
136
|
+
bundles,
|
|
137
|
+
prCount: bundles.length,
|
|
138
|
+
skipped,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function clampLimit(limit) {
|
|
142
|
+
if (limit === undefined)
|
|
143
|
+
return DEFAULT_FETCH_LIMIT;
|
|
144
|
+
if (!Number.isFinite(limit) || limit < 1)
|
|
145
|
+
return DEFAULT_FETCH_LIMIT;
|
|
146
|
+
return Math.min(limit, MAX_FETCH_LIMIT);
|
|
147
|
+
}
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -53,6 +53,14 @@ export { runInit } from './init.js';
|
|
|
53
53
|
export { runSetup } from './setup.js';
|
|
54
54
|
/** Check whether setup has been completed. */
|
|
55
55
|
export { runCheckSetup } from './setup.js';
|
|
56
|
+
/** Read the guidelines file for a repo. */
|
|
57
|
+
export { runGuidelinesView } from './guidelines.js';
|
|
58
|
+
/** Persist a guidelines file for a repo (overwrites on subsequent calls). */
|
|
59
|
+
export { runGuidelinesStore } from './guidelines.js';
|
|
60
|
+
/** Tombstone a guidelines file so subsequent reads return null. */
|
|
61
|
+
export { runGuidelinesReset } from './guidelines.js';
|
|
62
|
+
/** Fetch raw PR comment bundles for the host's extract-learnings prompt. */
|
|
63
|
+
export { runFetchCorpus } from './guidelines.js';
|
|
56
64
|
/** Show current persistence mode, Gist ID, and sync status. */
|
|
57
65
|
export { runStateShow } from './state-cmd.js';
|
|
58
66
|
/** Force push state to the backing Gist (no-op in local mode). */
|
|
@@ -78,6 +86,7 @@ export type { VetOutput, CommentsOutput, PostOutput, ClaimOutput, VetListOutput,
|
|
|
78
86
|
export type { ConfigOutput, DetectFormattersOutput, ParseIssueListOutput, ParsedIssueItem, CheckIntegrationOutput, LocalReposOutput, } from '../formatters/json.js';
|
|
79
87
|
export type { ShelveOutput, UnshelveOutput } from './shelve.js';
|
|
80
88
|
export type { MoveOutput, MoveTarget } from './move.js';
|
|
89
|
+
export type { GuidelinesViewOutput, GuidelinesStoreOutput, GuidelinesResetOutput, FetchCorpusOutput, } from './guidelines.js';
|
|
81
90
|
export type { DismissOutput, UndismissOutput } from './dismiss.js';
|
|
82
91
|
export type { InitOutput } from './init.js';
|
|
83
92
|
export type { ConfigSetOutput, ConfigCommandOutput } from './config.js';
|
package/dist/commands/index.js
CHANGED
|
@@ -57,6 +57,15 @@ export { runInit } from './init.js';
|
|
|
57
57
|
export { runSetup } from './setup.js';
|
|
58
58
|
/** Check whether setup has been completed. */
|
|
59
59
|
export { runCheckSetup } from './setup.js';
|
|
60
|
+
// ── Per-Repo Guidelines (#867) ──────────────────────────────────────────────
|
|
61
|
+
/** Read the guidelines file for a repo. */
|
|
62
|
+
export { runGuidelinesView } from './guidelines.js';
|
|
63
|
+
/** Persist a guidelines file for a repo (overwrites on subsequent calls). */
|
|
64
|
+
export { runGuidelinesStore } from './guidelines.js';
|
|
65
|
+
/** Tombstone a guidelines file so subsequent reads return null. */
|
|
66
|
+
export { runGuidelinesReset } from './guidelines.js';
|
|
67
|
+
/** Fetch raw PR comment bundles for the host's extract-learnings prompt. */
|
|
68
|
+
export { runFetchCorpus } from './guidelines.js';
|
|
60
69
|
// ── State Persistence ────────────────────────────────────────────────────────
|
|
61
70
|
/** Show current persistence mode, Gist ID, and sync status. */
|
|
62
71
|
export { runStateShow } from './state-cmd.js';
|
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
* Bridge between oss-autopilot's AgentState and oss-scout's OssScout API.
|
|
3
3
|
* Maps state fields and creates scout instances for search/vet commands.
|
|
4
4
|
*/
|
|
5
|
-
import { type OssScout, type ScoutState } from '@oss-scout/core';
|
|
5
|
+
import { type LinkedPR as ScoutLinkedPR, type OssScout, type ScoutState } from '@oss-scout/core';
|
|
6
|
+
import type { LinkedPR } from '../core/linked-pr-classification.js';
|
|
7
|
+
/**
|
|
8
|
+
* Convert scout 0.6.0's `LinkedPR` (separate `state` + `merged`) into the
|
|
9
|
+
* shape `classifyLinkedPR` expects (`state` already folded with `merged`).
|
|
10
|
+
*
|
|
11
|
+
* Scout exposes the raw GitHub fields verbatim, but the classifier was
|
|
12
|
+
* written before scout surfaced this data and uses a tri-state
|
|
13
|
+
* `'open' | 'closed' | 'merged'` enum. Folding `merged` into the state
|
|
14
|
+
* preserves the function's existing contract + tests.
|
|
15
|
+
*/
|
|
16
|
+
export declare function adaptScoutLinkedPR(scoutLinkedPR: ScoutLinkedPR | null | undefined): LinkedPR | null;
|
|
6
17
|
/**
|
|
7
18
|
* Build a ScoutState from the current AgentState.
|
|
8
19
|
* Maps oss-autopilot's config and state fields to oss-scout's state format.
|
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
import { createScout } from '@oss-scout/core';
|
|
6
6
|
import { getStateManager, requireGitHubToken } from '../core/index.js';
|
|
7
7
|
import { loadSkippedIssues } from './skip-file-parser.js';
|
|
8
|
+
/**
|
|
9
|
+
* Convert scout 0.6.0's `LinkedPR` (separate `state` + `merged`) into the
|
|
10
|
+
* shape `classifyLinkedPR` expects (`state` already folded with `merged`).
|
|
11
|
+
*
|
|
12
|
+
* Scout exposes the raw GitHub fields verbatim, but the classifier was
|
|
13
|
+
* written before scout surfaced this data and uses a tri-state
|
|
14
|
+
* `'open' | 'closed' | 'merged'` enum. Folding `merged` into the state
|
|
15
|
+
* preserves the function's existing contract + tests.
|
|
16
|
+
*/
|
|
17
|
+
export function adaptScoutLinkedPR(scoutLinkedPR) {
|
|
18
|
+
if (!scoutLinkedPR)
|
|
19
|
+
return null;
|
|
20
|
+
return {
|
|
21
|
+
author: { login: scoutLinkedPR.author },
|
|
22
|
+
state: scoutLinkedPR.merged ? 'merged' : scoutLinkedPR.state,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
8
25
|
/**
|
|
9
26
|
* Build a ScoutState from the current AgentState.
|
|
10
27
|
* Maps oss-autopilot's config and state fields to oss-scout's state format.
|
|
@@ -31,6 +48,8 @@ export function buildScoutState() {
|
|
|
31
48
|
broadPhaseDelayMs: 90000,
|
|
32
49
|
skipBroadWhenSufficientResults: 15,
|
|
33
50
|
persistence: config.persistence,
|
|
51
|
+
slmTriageModel: config.slmTriageModel,
|
|
52
|
+
slmTriageHost: config.slmTriageHost,
|
|
34
53
|
},
|
|
35
54
|
repoScores: state.repoScores,
|
|
36
55
|
starredRepos: config.starredRepos,
|
|
@@ -3,11 +3,11 @@
|
|
|
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 'fs';
|
|
6
|
-
import { createAutopilotScout } from './scout-bridge.js';
|
|
6
|
+
import { adaptScoutLinkedPR, 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';
|
|
10
|
-
import { getStateManager } from '../core/index.js';
|
|
10
|
+
import { getStateManager, classifyLinkedPR } from '../core/index.js';
|
|
11
11
|
const UNKNOWN_GRADE = computeSuccessGrade({ avgResponseDays: null, mergeRate: null, daysSinceLastCommit: null });
|
|
12
12
|
const KNOWN_SKIP_REASONS = new Set([
|
|
13
13
|
'issue_closed',
|
|
@@ -117,6 +117,11 @@ export async function runVetList(options = {}) {
|
|
|
117
117
|
projectHealth: candidate.projectHealth,
|
|
118
118
|
getRepoScore: (repo) => getStateManager().getRepoScore(repo),
|
|
119
119
|
});
|
|
120
|
+
const userLogin = getStateManager().getState().config.githubUsername;
|
|
121
|
+
const linkedPRClassification = classifyLinkedPR({
|
|
122
|
+
linkedPR: adaptScoutLinkedPR(candidate.vettingResult.linkedPR),
|
|
123
|
+
userLogin,
|
|
124
|
+
});
|
|
120
125
|
const vetResult = {
|
|
121
126
|
issue: {
|
|
122
127
|
repo: candidate.issue.repo,
|
|
@@ -130,6 +135,9 @@ export async function runVetList(options = {}) {
|
|
|
130
135
|
reasonsToSkip: candidate.reasonsToSkip,
|
|
131
136
|
projectHealth: candidate.projectHealth,
|
|
132
137
|
vettingResult: candidate.vettingResult,
|
|
138
|
+
antiLLMPolicy: candidate.antiLLMPolicy,
|
|
139
|
+
linkedPRClassification,
|
|
140
|
+
slmTriage: candidate.slmTriage ?? null,
|
|
133
141
|
grade,
|
|
134
142
|
};
|
|
135
143
|
results.push({
|
package/dist/commands/vet.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import { 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
|
-
import { getStateManager } from '../core/index.js';
|
|
8
|
+
import { getStateManager, classifyLinkedPR } from '../core/index.js';
|
|
9
|
+
import { adaptScoutLinkedPR } from './scout-bridge.js';
|
|
9
10
|
/**
|
|
10
11
|
* Vet a specific GitHub issue for claimability and project health.
|
|
11
12
|
*
|
|
@@ -24,6 +25,11 @@ export async function runVet(options) {
|
|
|
24
25
|
projectHealth: candidate.projectHealth,
|
|
25
26
|
getRepoScore: (repo) => getStateManager().getRepoScore(repo),
|
|
26
27
|
});
|
|
28
|
+
const userLogin = getStateManager().getState().config.githubUsername;
|
|
29
|
+
const linkedPRClassification = classifyLinkedPR({
|
|
30
|
+
linkedPR: adaptScoutLinkedPR(candidate.vettingResult.linkedPR),
|
|
31
|
+
userLogin,
|
|
32
|
+
});
|
|
27
33
|
return {
|
|
28
34
|
issue: {
|
|
29
35
|
repo: candidate.issue.repo,
|
|
@@ -37,6 +43,9 @@ export async function runVet(options) {
|
|
|
37
43
|
reasonsToSkip: candidate.reasonsToSkip,
|
|
38
44
|
projectHealth: candidate.projectHealth,
|
|
39
45
|
vettingResult: candidate.vettingResult,
|
|
46
|
+
antiLLMPolicy: candidate.antiLLMPolicy,
|
|
47
|
+
linkedPRClassification,
|
|
48
|
+
slmTriage: candidate.slmTriage ?? null,
|
|
40
49
|
grade,
|
|
41
50
|
};
|
|
42
51
|
}
|
|
@@ -200,7 +200,7 @@ export declare class GistStateStore {
|
|
|
200
200
|
private fetchAndCache;
|
|
201
201
|
/**
|
|
202
202
|
* Parse `state.json` from the in-memory cache. Handles v2 migration
|
|
203
|
-
* by running through the Zod schema (which requires version:
|
|
203
|
+
* by running through the Zod schema (which requires version: 4).
|
|
204
204
|
* Falls back to fresh state if the file is missing or unparseable.
|
|
205
205
|
*/
|
|
206
206
|
private parseStateFromCache;
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import * as fs from 'fs';
|
|
35
35
|
import { AgentStateSchema } from './state-schema.js';
|
|
36
|
-
import { atomicWriteFileSync, createFreshState, migrateV1ToV2, migrateV2ToV3 } from './state-persistence.js';
|
|
36
|
+
import { atomicWriteFileSync, createFreshState, migrateV1ToV2, migrateV2ToV3, migrateV3ToV4, } from './state-persistence.js';
|
|
37
37
|
import { getGistIdPath, getStateCachePath } from './paths.js';
|
|
38
38
|
import { debug, warn } from './logger.js';
|
|
39
39
|
import { GistPermissionError, GistConcurrencyError, isRateLimitError } from './errors.js';
|
|
@@ -127,6 +127,8 @@ export class GistStateStore {
|
|
|
127
127
|
obj = migrateV1ToV2(record);
|
|
128
128
|
if (obj.version === 2)
|
|
129
129
|
obj = migrateV2ToV3(obj);
|
|
130
|
+
if (obj.version === 3)
|
|
131
|
+
obj = migrateV3ToV4(obj);
|
|
130
132
|
}
|
|
131
133
|
const cachedState = AgentStateSchema.parse(obj);
|
|
132
134
|
debug(MODULE, 'Loaded state from local cache in degraded mode');
|
|
@@ -197,6 +199,8 @@ export class GistStateStore {
|
|
|
197
199
|
obj = migrateV1ToV2(record);
|
|
198
200
|
if (obj.version === 2)
|
|
199
201
|
obj = migrateV2ToV3(obj);
|
|
202
|
+
if (obj.version === 3)
|
|
203
|
+
obj = migrateV3ToV4(obj);
|
|
200
204
|
}
|
|
201
205
|
const cachedState = AgentStateSchema.parse(obj);
|
|
202
206
|
debug(MODULE, 'bootstrapWithMigration: loaded state from local cache in degraded mode');
|
|
@@ -433,7 +437,7 @@ export class GistStateStore {
|
|
|
433
437
|
}
|
|
434
438
|
/**
|
|
435
439
|
* Parse `state.json` from the in-memory cache. Handles v2 migration
|
|
436
|
-
* by running through the Zod schema (which requires version:
|
|
440
|
+
* by running through the Zod schema (which requires version: 4).
|
|
437
441
|
* Falls back to fresh state if the file is missing or unparseable.
|
|
438
442
|
*/
|
|
439
443
|
parseStateFromCache() {
|
|
@@ -451,6 +455,8 @@ export class GistStateStore {
|
|
|
451
455
|
obj = migrateV1ToV2(record);
|
|
452
456
|
if (obj.version === 2)
|
|
453
457
|
obj = migrateV2ToV3(obj);
|
|
458
|
+
if (obj.version === 3)
|
|
459
|
+
obj = migrateV3ToV4(obj);
|
|
454
460
|
}
|
|
455
461
|
return AgentStateSchema.parse(obj);
|
|
456
462
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-repo guidelines persistence on top of the Gist freeform-document API
|
|
3
|
+
* (#867 PR 2).
|
|
4
|
+
*
|
|
5
|
+
* Each repo gets one markdown file named `guidelines--{owner}--{repo}.md`
|
|
6
|
+
* stored in the user's oss-autopilot Gist alongside `state.json`. The file
|
|
7
|
+
* holds extracted guidance from past PR review feedback. Reads and writes go
|
|
8
|
+
* through the in-memory cache that `GistStateStore` already maintains; the
|
|
9
|
+
* file persists on the next `push()`.
|
|
10
|
+
*
|
|
11
|
+
* The byte cap (8 KB) is enforced at write time to keep claim-time context
|
|
12
|
+
* injection small. Larger guidance should be split across categories or
|
|
13
|
+
* deferred to a follow-up consolidation pass.
|
|
14
|
+
*/
|
|
15
|
+
import type { GistStateStore } from './gist-state-store.js';
|
|
16
|
+
import { OssAutopilotError } from './errors.js';
|
|
17
|
+
/** Filename prefix shared by every guidelines file in the Gist. */
|
|
18
|
+
export declare const GUIDELINES_FILE_PREFIX = "guidelines--";
|
|
19
|
+
/** Hard byte budget for a single guidelines file (#867 design log §1). */
|
|
20
|
+
export declare const GUIDELINES_MAX_BYTES = 8192;
|
|
21
|
+
/**
|
|
22
|
+
* Convert an `owner/repo` pair into the filename used inside the Gist.
|
|
23
|
+
* Slashes are escaped as `--` so the filename is filesystem-safe and
|
|
24
|
+
* unambiguous when parsing back to a repo string.
|
|
25
|
+
*/
|
|
26
|
+
export declare function guidelinesFilename(repo: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Inverse of {@link guidelinesFilename}. Returns null when the filename
|
|
29
|
+
* doesn't match the guidelines convention.
|
|
30
|
+
*/
|
|
31
|
+
export declare function repoFromGuidelinesFilename(filename: string): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Thrown by {@link setGuidelines} / {@link deleteGuidelines} when the
|
|
34
|
+
* StateManager is not in Gist mode. Catch + degrade gracefully when surfacing
|
|
35
|
+
* to user-facing flows: per-repo guidelines simply aren't available without a
|
|
36
|
+
* Gist to store them in.
|
|
37
|
+
*/
|
|
38
|
+
export declare class GuidelinesNotAvailableError extends OssAutopilotError {
|
|
39
|
+
constructor(message?: string);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Thrown by {@link setGuidelines} when content exceeds {@link GUIDELINES_MAX_BYTES}.
|
|
43
|
+
* Surfaced separately from generic validation errors so consumers can prompt the
|
|
44
|
+
* user with a "trim or split" UX rather than a generic shape rejection.
|
|
45
|
+
*/
|
|
46
|
+
export declare class GuidelinesTooLargeError extends OssAutopilotError {
|
|
47
|
+
constructor(byteSize: number, max?: number);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read the guidelines file for a repo from the Gist cache. Returns null when
|
|
51
|
+
* the store is not in Gist mode, the file does not exist, or the file is
|
|
52
|
+
* present but empty (treated as a tombstone left by {@link deleteGuidelines}).
|
|
53
|
+
*/
|
|
54
|
+
export declare function getGuidelines(store: GistStateStore | null, repo: string): string | null;
|
|
55
|
+
/**
|
|
56
|
+
* Write or replace the guidelines file for a repo. Throws if the store is not
|
|
57
|
+
* in Gist mode or the content exceeds the byte budget.
|
|
58
|
+
*/
|
|
59
|
+
export declare function setGuidelines(store: GistStateStore | null, repo: string, content: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Delete the guidelines file for a repo. No-op if the file doesn't exist.
|
|
62
|
+
* Implementation: write an empty string. The Gist API treats files with
|
|
63
|
+
* empty content as deletions on the next push, matching the existing
|
|
64
|
+
* single-source-of-truth model.
|
|
65
|
+
*/
|
|
66
|
+
export declare function deleteGuidelines(store: GistStateStore | null, repo: string): void;
|
|
67
|
+
/**
|
|
68
|
+
* List every repo (as `owner/repo`) that has a non-empty guidelines file in
|
|
69
|
+
* the cache. Tombstoned (empty-content) files are excluded so the result
|
|
70
|
+
* matches what {@link getGuidelines} would actually return.
|
|
71
|
+
*
|
|
72
|
+
* Returns an empty array when the store is null or no files exist.
|
|
73
|
+
*/
|
|
74
|
+
export declare function listGuidelinesRepos(store: GistStateStore | null): string[];
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { OssAutopilotError } from './errors.js';
|
|
2
|
+
/** Filename prefix shared by every guidelines file in the Gist. */
|
|
3
|
+
export const GUIDELINES_FILE_PREFIX = 'guidelines--';
|
|
4
|
+
/** Hard byte budget for a single guidelines file (#867 design log §1). */
|
|
5
|
+
export const GUIDELINES_MAX_BYTES = 8_192;
|
|
6
|
+
/** Suffix appended to the filename so it renders as markdown in Gist. */
|
|
7
|
+
const GUIDELINES_FILE_SUFFIX = '.md';
|
|
8
|
+
/**
|
|
9
|
+
* Convert an `owner/repo` pair into the filename used inside the Gist.
|
|
10
|
+
* Slashes are escaped as `--` so the filename is filesystem-safe and
|
|
11
|
+
* unambiguous when parsing back to a repo string.
|
|
12
|
+
*/
|
|
13
|
+
export function guidelinesFilename(repo) {
|
|
14
|
+
if (!repo.includes('/')) {
|
|
15
|
+
throw new OssAutopilotError(`Invalid repo identifier "${repo}". Expected "owner/repo" format.`, 'INVALID_REPO_ID');
|
|
16
|
+
}
|
|
17
|
+
// GitHub forbids `/` in owner and repo, so the only `/` is the separator.
|
|
18
|
+
const [owner, name] = repo.split('/');
|
|
19
|
+
return `${GUIDELINES_FILE_PREFIX}${owner}--${name}${GUIDELINES_FILE_SUFFIX}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Inverse of {@link guidelinesFilename}. Returns null when the filename
|
|
23
|
+
* doesn't match the guidelines convention.
|
|
24
|
+
*/
|
|
25
|
+
export function repoFromGuidelinesFilename(filename) {
|
|
26
|
+
if (!filename.startsWith(GUIDELINES_FILE_PREFIX))
|
|
27
|
+
return null;
|
|
28
|
+
if (!filename.endsWith(GUIDELINES_FILE_SUFFIX))
|
|
29
|
+
return null;
|
|
30
|
+
const middle = filename.slice(GUIDELINES_FILE_PREFIX.length, filename.length - GUIDELINES_FILE_SUFFIX.length);
|
|
31
|
+
// Only split on the FIRST `--` separator. Repo names with `--` are rare
|
|
32
|
+
// but legal; owner names cannot contain `--` per GitHub username rules.
|
|
33
|
+
const sep = middle.indexOf('--');
|
|
34
|
+
if (sep === -1)
|
|
35
|
+
return null;
|
|
36
|
+
const owner = middle.slice(0, sep);
|
|
37
|
+
const name = middle.slice(sep + 2);
|
|
38
|
+
if (!owner || !name)
|
|
39
|
+
return null;
|
|
40
|
+
return `${owner}/${name}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Thrown by {@link setGuidelines} / {@link deleteGuidelines} when the
|
|
44
|
+
* StateManager is not in Gist mode. Catch + degrade gracefully when surfacing
|
|
45
|
+
* to user-facing flows: per-repo guidelines simply aren't available without a
|
|
46
|
+
* Gist to store them in.
|
|
47
|
+
*/
|
|
48
|
+
export class GuidelinesNotAvailableError extends OssAutopilotError {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message ??
|
|
51
|
+
'Per-repo guidelines require Gist persistence. Run `oss-autopilot setup` to enable Gist sync, then retry.', 'GUIDELINES_NOT_AVAILABLE');
|
|
52
|
+
this.name = 'GuidelinesNotAvailableError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Thrown by {@link setGuidelines} when content exceeds {@link GUIDELINES_MAX_BYTES}.
|
|
57
|
+
* Surfaced separately from generic validation errors so consumers can prompt the
|
|
58
|
+
* user with a "trim or split" UX rather than a generic shape rejection.
|
|
59
|
+
*/
|
|
60
|
+
export class GuidelinesTooLargeError extends OssAutopilotError {
|
|
61
|
+
constructor(byteSize, max = GUIDELINES_MAX_BYTES) {
|
|
62
|
+
super(`Guidelines content is ${byteSize} bytes, exceeding the ${max}-byte cap. ` + `Trim or split across categories.`, 'GUIDELINES_TOO_LARGE');
|
|
63
|
+
this.name = 'GuidelinesTooLargeError';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read the guidelines file for a repo from the Gist cache. Returns null when
|
|
68
|
+
* the store is not in Gist mode, the file does not exist, or the file is
|
|
69
|
+
* present but empty (treated as a tombstone left by {@link deleteGuidelines}).
|
|
70
|
+
*/
|
|
71
|
+
export function getGuidelines(store, repo) {
|
|
72
|
+
if (!store)
|
|
73
|
+
return null;
|
|
74
|
+
const content = store.getDocument(guidelinesFilename(repo));
|
|
75
|
+
if (content === null || content === '')
|
|
76
|
+
return null;
|
|
77
|
+
return content;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Write or replace the guidelines file for a repo. Throws if the store is not
|
|
81
|
+
* in Gist mode or the content exceeds the byte budget.
|
|
82
|
+
*/
|
|
83
|
+
export function setGuidelines(store, repo, content) {
|
|
84
|
+
if (!store)
|
|
85
|
+
throw new GuidelinesNotAvailableError();
|
|
86
|
+
const byteSize = Buffer.byteLength(content, 'utf-8');
|
|
87
|
+
if (byteSize > GUIDELINES_MAX_BYTES) {
|
|
88
|
+
throw new GuidelinesTooLargeError(byteSize);
|
|
89
|
+
}
|
|
90
|
+
store.setDocument(guidelinesFilename(repo), content);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Delete the guidelines file for a repo. No-op if the file doesn't exist.
|
|
94
|
+
* Implementation: write an empty string. The Gist API treats files with
|
|
95
|
+
* empty content as deletions on the next push, matching the existing
|
|
96
|
+
* single-source-of-truth model.
|
|
97
|
+
*/
|
|
98
|
+
export function deleteGuidelines(store, repo) {
|
|
99
|
+
if (!store)
|
|
100
|
+
throw new GuidelinesNotAvailableError();
|
|
101
|
+
// setDocument('') is interpreted as deletion; the GistStateStore push path
|
|
102
|
+
// already strips empty-content files before sending to the Gist API.
|
|
103
|
+
store.setDocument(guidelinesFilename(repo), '');
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* List every repo (as `owner/repo`) that has a non-empty guidelines file in
|
|
107
|
+
* the cache. Tombstoned (empty-content) files are excluded so the result
|
|
108
|
+
* matches what {@link getGuidelines} would actually return.
|
|
109
|
+
*
|
|
110
|
+
* Returns an empty array when the store is null or no files exist.
|
|
111
|
+
*/
|
|
112
|
+
export function listGuidelinesRepos(store) {
|
|
113
|
+
if (!store)
|
|
114
|
+
return [];
|
|
115
|
+
const filenames = store.listDocuments(GUIDELINES_FILE_PREFIX);
|
|
116
|
+
const repos = [];
|
|
117
|
+
for (const filename of filenames) {
|
|
118
|
+
const repo = repoFromGuidelinesFilename(filename);
|
|
119
|
+
// Skip files we can't decode (e.g. older formats, hand-edited) — better
|
|
120
|
+
// than throwing and breaking listGuidelinesRepos for everyone.
|
|
121
|
+
if (!repo)
|
|
122
|
+
continue;
|
|
123
|
+
// Skip tombstones — empty content means the user deleted these guidelines.
|
|
124
|
+
const content = store.getDocument(filename);
|
|
125
|
+
if (content === null || content === '')
|
|
126
|
+
continue;
|
|
127
|
+
repos.push(repo);
|
|
128
|
+
}
|
|
129
|
+
return repos.sort();
|
|
130
|
+
}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { StateManager, getStateManager, getStateManagerAsync, ensureGistPersistence, maybeCheckpoint, resetStateManager, type Stats, } from './state.js';
|
|
6
6
|
export { GistStateStore } from './gist-state-store.js';
|
|
7
|
+
export { guidelinesFilename, repoFromGuidelinesFilename, GUIDELINES_FILE_PREFIX, GUIDELINES_MAX_BYTES, GuidelinesNotAvailableError, GuidelinesTooLargeError, } from './guidelines-store.js';
|
|
7
8
|
export { PRMonitor, type PRCheckFailure, type FetchPRsResult, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
|
|
8
9
|
export { IssueConversationMonitor } from './issue-conversation.js';
|
|
9
10
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
package/dist/core/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export { StateManager, getStateManager, getStateManagerAsync, ensureGistPersistence, maybeCheckpoint, resetStateManager, } from './state.js';
|
|
6
6
|
export { GistStateStore } from './gist-state-store.js';
|
|
7
|
+
export { guidelinesFilename, repoFromGuidelinesFilename, GUIDELINES_FILE_PREFIX, GUIDELINES_MAX_BYTES, GuidelinesNotAvailableError, GuidelinesTooLargeError, } from './guidelines-store.js';
|
|
7
8
|
export { PRMonitor, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
|
|
8
9
|
// Search/vetting now delegated to @oss-scout/core via commands/scout-bridge.ts
|
|
9
10
|
export { IssueConversationMonitor } from './issue-conversation.js';
|