@oss-autopilot/core 3.10.0 → 3.11.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.d.ts +7 -0
- package/dist/cli-registry.js +29 -5
- package/dist/cli.bundle.cjs +114 -114
- package/dist/cli.js +11 -3
- package/dist/commands/comments.js +31 -15
- package/dist/commands/compliance-score.js +12 -4
- package/dist/commands/daily.js +47 -2
- package/dist/commands/dashboard-data.d.ts +17 -0
- package/dist/commands/dashboard-data.js +49 -0
- package/dist/commands/dashboard-server.js +93 -24
- package/dist/commands/dismiss.d.ts +4 -0
- package/dist/commands/dismiss.js +4 -4
- package/dist/commands/guidelines.d.ts +19 -0
- package/dist/commands/guidelines.js +23 -4
- package/dist/commands/index.d.ts +3 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/move.d.ts +2 -0
- package/dist/commands/move.js +12 -8
- package/dist/commands/repo-vet.js +30 -8
- package/dist/commands/shelve.d.ts +4 -0
- package/dist/commands/shelve.js +4 -4
- package/dist/core/gist-state-store.js +42 -7
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/issue-conversation.js +15 -2
- package/dist/core/paths.d.ts +12 -0
- package/dist/core/paths.js +16 -0
- package/dist/core/pr-comments-fetcher.d.ts +10 -2
- package/dist/core/pr-comments-fetcher.js +22 -4
- package/dist/core/state-persistence.d.ts +31 -9
- package/dist/core/state-persistence.js +51 -16
- package/dist/core/state.d.ts +18 -1
- package/dist/core/state.js +35 -3
- package/dist/core/untrusted-content.d.ts +24 -3
- package/dist/core/untrusted-content.js +31 -3
- package/dist/formatters/json.d.ts +9 -1
- package/dist/formatters/json.js +12 -0
- package/package.json +7 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guidelines CLI commands (#867 PR 4).
|
|
3
3
|
*
|
|
4
|
+
* `guidelines list` — list repos with stored guidelines.
|
|
4
5
|
* `guidelines view` — read the per-repo guidelines file from the Gist.
|
|
5
6
|
* `guidelines store` — overwrite the per-repo guidelines file.
|
|
6
7
|
* `guidelines reset` — tombstone the file so subsequent reads return null.
|
|
@@ -41,6 +42,20 @@ export async function runGuidelinesView(options) {
|
|
|
41
42
|
storageMode: sm.isGuidelinesAvailable() ? 'gist' : 'local-unavailable',
|
|
42
43
|
};
|
|
43
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* List every repo with non-empty stored guidelines. Never throws in local
|
|
47
|
+
* mode — returns an empty list with `storageMode: 'local-unavailable'` so
|
|
48
|
+
* hosts can distinguish "nothing stored" from "storage not configured".
|
|
49
|
+
*/
|
|
50
|
+
export async function runGuidelinesList() {
|
|
51
|
+
const sm = getStateManager();
|
|
52
|
+
const repos = sm.listGuidelinesRepos();
|
|
53
|
+
return {
|
|
54
|
+
repos,
|
|
55
|
+
count: repos.length,
|
|
56
|
+
storageMode: sm.isGuidelinesAvailable() ? 'gist' : 'local-unavailable',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
44
59
|
/**
|
|
45
60
|
* Persist per-repo guidelines for `repo`. Throws when not in Gist mode or
|
|
46
61
|
* when content exceeds the byte budget — the CLI surface relies on the
|
|
@@ -57,11 +72,12 @@ export async function runGuidelinesStore(options) {
|
|
|
57
72
|
// Push to Gist — autoSave only writes the local state-cache mirror in Gist
|
|
58
73
|
// mode, so without this checkpoint the change never propagates across
|
|
59
74
|
// machines (#1200).
|
|
60
|
-
await maybeCheckpoint(sm, MODULE);
|
|
75
|
+
const gistSyncWarning = await maybeCheckpoint(sm, MODULE);
|
|
61
76
|
return {
|
|
62
77
|
repo: options.repo,
|
|
63
78
|
byteSize: Buffer.byteLength(options.content, 'utf8'),
|
|
64
79
|
stored: true,
|
|
80
|
+
...(gistSyncWarning ? { gistSyncWarning } : {}),
|
|
65
81
|
};
|
|
66
82
|
}
|
|
67
83
|
/** Tombstone the guidelines file for `repo`. */
|
|
@@ -72,12 +88,13 @@ export async function runGuidelinesReset(options) {
|
|
|
72
88
|
throw new GuidelinesNotAvailableError();
|
|
73
89
|
}
|
|
74
90
|
const existed = sm.getGuidelines(options.repo) !== null;
|
|
91
|
+
let gistSyncWarning = null;
|
|
75
92
|
if (existed) {
|
|
76
93
|
sm.deleteGuidelines(options.repo);
|
|
77
94
|
// Push to Gist — see runGuidelinesStore note (#1200).
|
|
78
|
-
await maybeCheckpoint(sm, MODULE);
|
|
95
|
+
gistSyncWarning = await maybeCheckpoint(sm, MODULE);
|
|
79
96
|
}
|
|
80
|
-
return { repo: options.repo, deleted: existed };
|
|
97
|
+
return { repo: options.repo, deleted: existed, ...(gistSyncWarning ? { gistSyncWarning } : {}) };
|
|
81
98
|
}
|
|
82
99
|
/**
|
|
83
100
|
* Fetch raw PR comment bundles for the most recent merged/closed PRs in `repo`
|
|
@@ -146,8 +163,9 @@ export async function runFetchCorpus(options) {
|
|
|
146
163
|
// Push commentsFetchedAt stamps to Gist so other machines don't re-fetch
|
|
147
164
|
// the same PRs forever. autoSave only writes the local mirror in Gist
|
|
148
165
|
// mode (#1200).
|
|
166
|
+
let gistSyncWarning = null;
|
|
149
167
|
if (bundles.length > 0) {
|
|
150
|
-
await maybeCheckpoint(sm, MODULE);
|
|
168
|
+
gistSyncWarning = await maybeCheckpoint(sm, MODULE);
|
|
151
169
|
}
|
|
152
170
|
return {
|
|
153
171
|
repo: options.repo,
|
|
@@ -155,6 +173,7 @@ export async function runFetchCorpus(options) {
|
|
|
155
173
|
prCount: bundles.length,
|
|
156
174
|
skipped,
|
|
157
175
|
failures,
|
|
176
|
+
...(gistSyncWarning ? { gistSyncWarning } : {}),
|
|
158
177
|
};
|
|
159
178
|
}
|
|
160
179
|
function clampLimit(limit) {
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -61,6 +61,8 @@ export { runInit } from './init.js';
|
|
|
61
61
|
export { runSetup } from './setup.js';
|
|
62
62
|
/** Check whether setup has been completed. */
|
|
63
63
|
export { runCheckSetup } from './setup.js';
|
|
64
|
+
/** List repos with stored guidelines (empty in local mode). */
|
|
65
|
+
export { runGuidelinesList } from './guidelines.js';
|
|
64
66
|
/** Read the guidelines file for a repo. */
|
|
65
67
|
export { runGuidelinesView } from './guidelines.js';
|
|
66
68
|
/** Persist a guidelines file for a repo (overwrites on subsequent calls). */
|
|
@@ -96,7 +98,7 @@ export type { VetOutput, CommentsOutput, PostOutput, ClaimOutput, VetListOutput,
|
|
|
96
98
|
export type { ConfigOutput, DetectFormattersOutput, ParseIssueListOutput, ParsedIssueItem, CheckIntegrationOutput, LocalReposOutput, } from '../formatters/json.js';
|
|
97
99
|
export type { ShelveOutput, UnshelveOutput } from './shelve.js';
|
|
98
100
|
export type { MoveOutput, MoveTarget } from './move.js';
|
|
99
|
-
export type { GuidelinesViewOutput, GuidelinesStoreOutput, GuidelinesResetOutput, FetchCorpusOutput, } from './guidelines.js';
|
|
101
|
+
export type { GuidelinesListOutput, GuidelinesViewOutput, GuidelinesStoreOutput, GuidelinesResetOutput, FetchCorpusOutput, } from './guidelines.js';
|
|
100
102
|
export type { DismissOutput, UndismissOutput } from './dismiss.js';
|
|
101
103
|
export type { InitOutput } from './init.js';
|
|
102
104
|
export type { ConfigSetOutput, ConfigCommandOutput } from './config.js';
|
package/dist/commands/index.js
CHANGED
|
@@ -66,6 +66,8 @@ export { runSetup } from './setup.js';
|
|
|
66
66
|
/** Check whether setup has been completed. */
|
|
67
67
|
export { runCheckSetup } from './setup.js';
|
|
68
68
|
// ── Per-Repo Guidelines (#867) ──────────────────────────────────────────────
|
|
69
|
+
/** List repos with stored guidelines (empty in local mode). */
|
|
70
|
+
export { runGuidelinesList } from './guidelines.js';
|
|
69
71
|
/** Read the guidelines file for a repo. */
|
|
70
72
|
export { runGuidelinesView } from './guidelines.js';
|
|
71
73
|
/** Persist a guidelines file for a repo (overwrites on subsequent calls). */
|
package/dist/commands/move.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface MoveOutput {
|
|
|
9
9
|
target: MoveTarget;
|
|
10
10
|
/** Human-readable description of what happened. */
|
|
11
11
|
description: string;
|
|
12
|
+
/** Set when the post-mutation Gist checkpoint failed; the local mutation succeeded (#1370). */
|
|
13
|
+
gistSyncWarning?: string;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Move a PR between states: attention, waiting, shelved, or auto (computed).
|
package/dist/commands/move.js
CHANGED
|
@@ -7,6 +7,10 @@ import { ValidationError } from '../core/errors.js';
|
|
|
7
7
|
import { PR_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
|
|
8
8
|
const MODULE = 'move';
|
|
9
9
|
export const VALID_TARGETS = ['attention', 'waiting', 'shelved', 'auto'];
|
|
10
|
+
/** Attach the checkpoint warning only when present, keeping the key off the wire on clean runs (#1370). */
|
|
11
|
+
function withGistSyncWarning(output, gistSyncWarning) {
|
|
12
|
+
return gistSyncWarning ? { ...output, gistSyncWarning } : output;
|
|
13
|
+
}
|
|
10
14
|
/**
|
|
11
15
|
* Move a PR between states: attention, waiting, shelved, or auto (computed).
|
|
12
16
|
*
|
|
@@ -36,32 +40,32 @@ export async function runMove(options) {
|
|
|
36
40
|
stateManager.setStatusOverride(options.prUrl, status, lastActivityAt);
|
|
37
41
|
stateManager.unshelvePR(options.prUrl);
|
|
38
42
|
});
|
|
39
|
-
await maybeCheckpoint(stateManager, MODULE);
|
|
40
|
-
return { url: options.prUrl, target, description: `Moved to ${label}` };
|
|
43
|
+
const gistSyncWarning = await maybeCheckpoint(stateManager, MODULE);
|
|
44
|
+
return withGistSyncWarning({ url: options.prUrl, target, description: `Moved to ${label}` }, gistSyncWarning);
|
|
41
45
|
}
|
|
42
46
|
case 'shelved': {
|
|
43
47
|
stateManager.batch(() => {
|
|
44
48
|
stateManager.shelvePR(options.prUrl);
|
|
45
49
|
stateManager.clearStatusOverride(options.prUrl);
|
|
46
50
|
});
|
|
47
|
-
await maybeCheckpoint(stateManager, MODULE);
|
|
48
|
-
return {
|
|
51
|
+
const gistSyncWarning = await maybeCheckpoint(stateManager, MODULE);
|
|
52
|
+
return withGistSyncWarning({
|
|
49
53
|
url: options.prUrl,
|
|
50
54
|
target,
|
|
51
55
|
description: 'Shelved — excluded from capacity and actionable items',
|
|
52
|
-
};
|
|
56
|
+
}, gistSyncWarning);
|
|
53
57
|
}
|
|
54
58
|
case 'auto': {
|
|
55
59
|
stateManager.batch(() => {
|
|
56
60
|
stateManager.clearStatusOverride(options.prUrl);
|
|
57
61
|
stateManager.unshelvePR(options.prUrl);
|
|
58
62
|
});
|
|
59
|
-
await maybeCheckpoint(stateManager, MODULE);
|
|
60
|
-
return {
|
|
63
|
+
const gistSyncWarning = await maybeCheckpoint(stateManager, MODULE);
|
|
64
|
+
return withGistSyncWarning({
|
|
61
65
|
url: options.prUrl,
|
|
62
66
|
target,
|
|
63
67
|
description: 'Reset to computed status',
|
|
64
|
-
};
|
|
68
|
+
}, gistSyncWarning);
|
|
65
69
|
}
|
|
66
70
|
default: {
|
|
67
71
|
const _exhaustive = target;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* no state mutation, runs against a public `owner/repo` slug.
|
|
12
12
|
*/
|
|
13
13
|
import { getOctokit, requireGitHubToken } from '../core/index.js';
|
|
14
|
-
import { errorMessage } from '../core/errors.js';
|
|
14
|
+
import { errorMessage, getHttpStatusCode, isRateLimitOrAuthError } from '../core/errors.js';
|
|
15
15
|
import { warn } from '../core/logger.js';
|
|
16
16
|
import { validateRepoIdentifier } from './validation.js';
|
|
17
17
|
import { computeRepoVet } from '../core/repo-vet.js';
|
|
@@ -160,13 +160,30 @@ export async function runRepoVet(options) {
|
|
|
160
160
|
const token = requireGitHubToken();
|
|
161
161
|
const octokit = getOctokit(token);
|
|
162
162
|
const now = new Date();
|
|
163
|
+
// Tracks the release-list analogue of communityHealth's `incomplete`:
|
|
164
|
+
// set when the listReleases call failed for a reason that does NOT
|
|
165
|
+
// prove absence (5xx, network), so the caller can distinguish "repo
|
|
166
|
+
// has no releases" from "couldn't check releases" (#1373).
|
|
167
|
+
let releasesIncomplete = false;
|
|
163
168
|
const [repoMetaResp, closedPRsResp, commitsResp, releasesResp, communityHealthSummary] = await Promise.all([
|
|
164
169
|
octokit.repos.get({ owner, repo }),
|
|
165
170
|
octokit.pulls.list({ owner, repo, state: 'closed', sort: 'updated', direction: 'desc', per_page: 100 }),
|
|
166
171
|
octokit.repos.listCommits({ owner, repo, per_page: 100 }),
|
|
167
|
-
octokit.repos
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
octokit.repos.listReleases({ owner, repo, per_page: 1 }).catch((err) => {
|
|
173
|
+
// Rate-limit / auth failures must abort the run, same as every
|
|
174
|
+
// sibling fetch — under throttling, a blanket catch here would
|
|
175
|
+
// silently report "no releases" for the whole vet batch (#1373).
|
|
176
|
+
if (isRateLimitOrAuthError(err))
|
|
177
|
+
throw err;
|
|
178
|
+
const empty = { data: [] };
|
|
179
|
+
// 404 is a definitive "nothing there" — genuine absence, not a gap.
|
|
180
|
+
if (getHttpStatusCode(err) === 404)
|
|
181
|
+
return empty;
|
|
182
|
+
// 5xx / network: absence is unproven. Degrade gracefully but flag it.
|
|
183
|
+
releasesIncomplete = true;
|
|
184
|
+
warn(MODULE, `release listing for ${owner}/${repo} failed: ${errorMessage(err)} — release-recency signal is incomplete`);
|
|
185
|
+
return empty;
|
|
186
|
+
}),
|
|
170
187
|
checkCommunityHealth(octokit, owner, repo),
|
|
171
188
|
]);
|
|
172
189
|
const prs = closedPRsResp.data.map((p) => ({
|
|
@@ -201,15 +218,20 @@ export async function runRepoVet(options) {
|
|
|
201
218
|
const result = computeRepoVet(input);
|
|
202
219
|
// The core function names its metadata object `repo`. Rename to `repoMeta`
|
|
203
220
|
// at the CLI boundary so the top-level slug doesn't collide with it.
|
|
204
|
-
// Also overlay the community-health `incomplete`
|
|
205
|
-
// tracks (the core type doesn't
|
|
206
|
-
// pure — only the wrapper makes
|
|
207
|
-
|
|
221
|
+
// Also overlay the community-health `incomplete` and the
|
|
222
|
+
// `releasesIncomplete` flags the wrapper tracks (the core type doesn't
|
|
223
|
+
// carry them because computeRepoVet is pure — only the wrapper makes
|
|
224
|
+
// the API calls that can fail mid-probe). Like communityHealth's
|
|
225
|
+
// incomplete flag, releasesIncomplete deliberately does NOT alter the
|
|
226
|
+
// rubric score — the score reflects the fetched signals as-is and the
|
|
227
|
+
// flag tells consumers which signal was unverified.
|
|
228
|
+
const { repo: repoMeta, communityHealth, maintainerActivity, ...rest } = result;
|
|
208
229
|
return {
|
|
209
230
|
repoSlug: options.repo,
|
|
210
231
|
fetchedAt: now.toISOString(),
|
|
211
232
|
repoMeta,
|
|
212
233
|
communityHealth: { ...communityHealth, incomplete: communityHealthSummary.incomplete },
|
|
234
|
+
maintainerActivity: { ...maintainerActivity, releasesIncomplete },
|
|
213
235
|
...rest,
|
|
214
236
|
};
|
|
215
237
|
}
|
|
@@ -11,10 +11,14 @@ import { PR_URL_PATTERN } from './validation.js';
|
|
|
11
11
|
export interface ShelveOutput {
|
|
12
12
|
shelved: boolean;
|
|
13
13
|
url: string;
|
|
14
|
+
/** Set when the post-mutation Gist checkpoint failed; the local mutation succeeded (#1370). */
|
|
15
|
+
gistSyncWarning?: string;
|
|
14
16
|
}
|
|
15
17
|
export interface UnshelveOutput {
|
|
16
18
|
unshelved: boolean;
|
|
17
19
|
url: string;
|
|
20
|
+
/** Set when the post-mutation Gist checkpoint failed; the local mutation succeeded (#1370). */
|
|
21
|
+
gistSyncWarning?: string;
|
|
18
22
|
}
|
|
19
23
|
export { PR_URL_PATTERN };
|
|
20
24
|
/**
|
package/dist/commands/shelve.js
CHANGED
|
@@ -29,8 +29,8 @@ export async function runShelve(options) {
|
|
|
29
29
|
added = stateManager.shelvePR(options.prUrl);
|
|
30
30
|
stateManager.clearStatusOverride(options.prUrl);
|
|
31
31
|
});
|
|
32
|
-
await maybeCheckpoint(stateManager, MODULE);
|
|
33
|
-
return { shelved: added, url: options.prUrl };
|
|
32
|
+
const gistSyncWarning = await maybeCheckpoint(stateManager, MODULE);
|
|
33
|
+
return { shelved: added, url: options.prUrl, ...(gistSyncWarning ? { gistSyncWarning } : {}) };
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
36
|
* Unshelve a PR, restoring it to the daily digest.
|
|
@@ -49,6 +49,6 @@ export async function runUnshelve(options) {
|
|
|
49
49
|
removed = stateManager.unshelvePR(options.prUrl);
|
|
50
50
|
stateManager.clearStatusOverride(options.prUrl);
|
|
51
51
|
});
|
|
52
|
-
await maybeCheckpoint(stateManager, MODULE);
|
|
53
|
-
return { unshelved: removed, url: options.prUrl };
|
|
52
|
+
const gistSyncWarning = await maybeCheckpoint(stateManager, MODULE);
|
|
53
|
+
return { unshelved: removed, url: options.prUrl, ...(gistSyncWarning ? { gistSyncWarning } : {}) };
|
|
54
54
|
}
|
|
@@ -49,7 +49,7 @@ import { AgentStateSchema } from './state-schema.js';
|
|
|
49
49
|
import { atomicWriteFileSync, createFreshState, migrateV1ToV2, migrateV2ToV3, migrateV3ToV4, } from './state-persistence.js';
|
|
50
50
|
import { getGistIdPath, getStateCachePath } from './paths.js';
|
|
51
51
|
import { debug, warn } from './logger.js';
|
|
52
|
-
import { GistPermissionError, GistConcurrencyError, GistCorruptError, isRateLimitError } from './errors.js';
|
|
52
|
+
import { ConfigurationError, GistPermissionError, GistConcurrencyError, GistCorruptError, isRateLimitError, } from './errors.js';
|
|
53
53
|
const MODULE = 'gist-store';
|
|
54
54
|
/**
|
|
55
55
|
* Extract the ETag header from an Octokit response, tolerating both lower-
|
|
@@ -139,6 +139,13 @@ export class GistStateStore {
|
|
|
139
139
|
return { gistId: localId, state, created: false };
|
|
140
140
|
}
|
|
141
141
|
catch (err) {
|
|
142
|
+
// A corrupt or permission-broken Gist must surface immediately
|
|
143
|
+
// (#1367): falling through to search would either re-find the same
|
|
144
|
+
// corrupt Gist or silently abandon it and create a fresh one.
|
|
145
|
+
if (err instanceof ConfigurationError)
|
|
146
|
+
throw err;
|
|
147
|
+
if (isRateLimitError(err))
|
|
148
|
+
throw err;
|
|
142
149
|
warn(MODULE, `Failed to fetch Gist ${localId}, will search/create`, err);
|
|
143
150
|
// Fall through to search
|
|
144
151
|
}
|
|
@@ -159,10 +166,22 @@ export class GistStateStore {
|
|
|
159
166
|
return { gistId: id, state, created: true };
|
|
160
167
|
}
|
|
161
168
|
catch (err) {
|
|
162
|
-
// Configuration errors (
|
|
163
|
-
|
|
169
|
+
// Configuration errors (GistPermissionError, GistCorruptError) and rate
|
|
170
|
+
// limits must surface, not degrade (#1367). A corrupt Gist especially:
|
|
171
|
+
// fetchAndCache arms this.gistId/lastFetchedEtag before the parse
|
|
172
|
+
// throws, so a degraded store could push() local or fresh state over
|
|
173
|
+
// the corrupt remote — the exact data loss #1201 exists to prevent.
|
|
174
|
+
// Rate limits propagate per the errors.ts contract; degrading would
|
|
175
|
+
// present "you are rate-limited" as a stale local cache.
|
|
176
|
+
if (err instanceof ConfigurationError)
|
|
164
177
|
throw err;
|
|
165
|
-
|
|
178
|
+
if (isRateLimitError(err))
|
|
179
|
+
throw err;
|
|
180
|
+
// All API paths failed — enter degraded mode. Disarm the remote write
|
|
181
|
+
// path first: a degraded store never verified its Gist, so it must
|
|
182
|
+
// never be able to push to one (#1367).
|
|
183
|
+
this.gistId = null;
|
|
184
|
+
this.lastFetchedEtag = null;
|
|
166
185
|
warn(MODULE, 'All Gist API paths failed, entering degraded mode', err);
|
|
167
186
|
// Try reading from local cache file
|
|
168
187
|
const cachePath = getStateCachePath();
|
|
@@ -222,6 +241,12 @@ export class GistStateStore {
|
|
|
222
241
|
return { gistId: localId, state, created: false, migrated: false };
|
|
223
242
|
}
|
|
224
243
|
catch (err) {
|
|
244
|
+
// See bootstrap(): corrupt/permission/rate-limit errors surface
|
|
245
|
+
// immediately rather than falling through (#1367).
|
|
246
|
+
if (err instanceof ConfigurationError)
|
|
247
|
+
throw err;
|
|
248
|
+
if (isRateLimitError(err))
|
|
249
|
+
throw err;
|
|
225
250
|
warn(MODULE, `bootstrapWithMigration: failed to fetch Gist ${localId}, will search/create`, err);
|
|
226
251
|
// Fall through to search
|
|
227
252
|
}
|
|
@@ -242,10 +267,16 @@ export class GistStateStore {
|
|
|
242
267
|
return { gistId: id, state, created: true, migrated: true };
|
|
243
268
|
}
|
|
244
269
|
catch (err) {
|
|
245
|
-
//
|
|
246
|
-
|
|
270
|
+
// Same surfacing contract as bootstrap() (#1367): configuration errors
|
|
271
|
+
// and rate limits rethrow; only genuine API unavailability degrades.
|
|
272
|
+
if (err instanceof ConfigurationError)
|
|
273
|
+
throw err;
|
|
274
|
+
if (isRateLimitError(err))
|
|
247
275
|
throw err;
|
|
248
|
-
// All API paths failed — enter degraded mode
|
|
276
|
+
// All API paths failed — enter degraded mode with the push path
|
|
277
|
+
// disarmed (see bootstrap()).
|
|
278
|
+
this.gistId = null;
|
|
279
|
+
this.lastFetchedEtag = null;
|
|
249
280
|
warn(MODULE, 'bootstrapWithMigration: all Gist API paths failed, entering degraded mode', err);
|
|
250
281
|
// Try reading from local cache file
|
|
251
282
|
const cachePath = getStateCachePath();
|
|
@@ -627,6 +658,10 @@ export class GistStateStore {
|
|
|
627
658
|
}
|
|
628
659
|
}
|
|
629
660
|
catch (err) {
|
|
661
|
+
// A rate-limited search must not read as "no Gist found" — bootstrap
|
|
662
|
+
// would proceed to create a duplicate Gist while throttled (#1367).
|
|
663
|
+
if (isRateLimitError(err))
|
|
664
|
+
throw err;
|
|
630
665
|
warn(MODULE, 'Failed to search Gists by description', err);
|
|
631
666
|
}
|
|
632
667
|
return null;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { guidelinesFilename, repoFromGuidelinesFilename, GUIDELINES_FILE_PREFIX,
|
|
|
8
8
|
export { PRMonitor, type PRCheckFailure, type FetchPRsResult, computeDisplayLabel, classifyCICheck, classifyFailingChecks, } from './pr-monitor.js';
|
|
9
9
|
export { IssueConversationMonitor } from './issue-conversation.js';
|
|
10
10
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
11
|
-
export { wrapUntrustedContent, extractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, type UntrustedContentMeta, } from './untrusted-content.js';
|
|
11
|
+
export { wrapUntrustedContent, extractFromFence, safeExtractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, type UntrustedContentMeta, } from './untrusted-content.js';
|
|
12
12
|
export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
13
13
|
export { parseGitHubUrl, splitRepo, isOwnRepo } from './urls.js';
|
|
14
14
|
export { daysBetween, formatRelativeTime, byDateDescending } from './dates.js';
|
package/dist/core/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { PRMonitor, computeDisplayLabel, classifyCICheck, classifyFailingChecks,
|
|
|
9
9
|
// Search/vetting now delegated to @oss-scout/core via commands/scout-bridge.ts
|
|
10
10
|
export { IssueConversationMonitor } from './issue-conversation.js';
|
|
11
11
|
export { isBotAuthor, isAcknowledgmentComment } from './comment-utils.js';
|
|
12
|
-
export { wrapUntrustedContent, extractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, } from './untrusted-content.js';
|
|
12
|
+
export { wrapUntrustedContent, extractFromFence, safeExtractFromFence, UNTRUSTED_OPEN_TAG_NAME, UNTRUSTED_CLOSE_TAG, } from './untrusted-content.js';
|
|
13
13
|
export { getOctokit, checkRateLimit } from './github.js';
|
|
14
14
|
export { parseGitHubUrl, splitRepo, isOwnRepo } from './urls.js';
|
|
15
15
|
export { daysBetween, formatRelativeTime, byDateDescending } from './dates.js';
|
|
@@ -12,7 +12,7 @@ import { getStateManager } from './state.js';
|
|
|
12
12
|
import { daysBetween } from './dates.js';
|
|
13
13
|
import { splitRepo, extractOwnerRepo, isOwnRepo } from './urls.js';
|
|
14
14
|
import { runWorkerPool, DEFAULT_CONCURRENCY } from './concurrency.js';
|
|
15
|
-
import { ConfigurationError, errorMessage } from './errors.js';
|
|
15
|
+
import { ConfigurationError, errorMessage, isRateLimitOrAuthError } from './errors.js';
|
|
16
16
|
import { debug, warn } from './logger.js';
|
|
17
17
|
const MODULE = 'issue-conversation';
|
|
18
18
|
const MAX_CONCURRENT_REQUESTS = DEFAULT_CONCURRENCY;
|
|
@@ -108,6 +108,14 @@ export class IssueConversationMonitor {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
catch (error) {
|
|
111
|
+
// Rate-limit / auth failures must propagate, not degrade to "fewer
|
|
112
|
+
// results" — under throttling every sibling analysis fails the same
|
|
113
|
+
// way and the partial result silently looks like a quiet day (#1391).
|
|
114
|
+
// runWorkerPool aborts remaining workers and rejects; daily.ts and
|
|
115
|
+
// dashboard-data.ts rethrow rate-limit/auth from their phase catches,
|
|
116
|
+
// aborting the run just like PRMonitor's 429s do.
|
|
117
|
+
if (isRateLimitOrAuthError(error))
|
|
118
|
+
throw error;
|
|
111
119
|
const msg = errorMessage(error);
|
|
112
120
|
failures.push({ issueUrl: item.html_url, error: msg });
|
|
113
121
|
warn(MODULE, `Error analyzing issue ${item.html_url}: ${msg}`);
|
|
@@ -117,7 +125,7 @@ export class IssueConversationMonitor {
|
|
|
117
125
|
warn(MODULE, `${failures.length}/${candidates.length} issue analysis call(s) failed`);
|
|
118
126
|
}
|
|
119
127
|
if (failures.length === candidates.length && candidates.length > 0) {
|
|
120
|
-
warn(MODULE, `All ${candidates.length} issue analysis call(s) failed. Possible systemic issue (
|
|
128
|
+
warn(MODULE, `All ${candidates.length} issue analysis call(s) failed. Possible systemic issue (network, GitHub availability).`);
|
|
121
129
|
}
|
|
122
130
|
// Sort: new_response first, then waiting, then acknowledged
|
|
123
131
|
const statusOrder = {
|
|
@@ -199,6 +207,11 @@ export class IssueConversationMonitor {
|
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
209
|
const labels = (item.labels || []).map((l) => l.name || '').filter(Boolean);
|
|
210
|
+
// Body excerpts stay RAW here on purpose (#1372): these objects feed the
|
|
211
|
+
// dashboard SPA and the CLI text renderers directly, and the matching
|
|
212
|
+
// above (acknowledgment / @mention) operates on raw text. The
|
|
213
|
+
// `<github-content>` fence is applied at the agent-facing serialization
|
|
214
|
+
// boundary in `toDailyOutput()` (commands/daily.ts).
|
|
202
215
|
const base = {
|
|
203
216
|
repo: repoFullName,
|
|
204
217
|
number: item.number,
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -25,6 +25,18 @@ export declare function getDataDir(): string;
|
|
|
25
25
|
* Implicitly creates the data directory via {@link getDataDir} if it does not exist.
|
|
26
26
|
*/
|
|
27
27
|
export declare function getStatePath(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Returns the legacy (pre-`~/.oss-autopilot`) state file path: `./data/state.json`
|
|
30
|
+
* relative to the working directory.
|
|
31
|
+
*
|
|
32
|
+
* Functions rather than module constants so tests can mock them per-file:
|
|
33
|
+
* as constants in state-persistence.ts they resolved to the one shared
|
|
34
|
+
* `packages/core/data/` during vitest, and the migration tests racing
|
|
35
|
+
* parallel workers on that real directory caused CI flakes (#1382).
|
|
36
|
+
*/
|
|
37
|
+
export declare function getLegacyStatePath(): string;
|
|
38
|
+
/** Legacy backup directory (`./data/backups`); see {@link getLegacyStatePath}. */
|
|
39
|
+
export declare function getLegacyBackupDir(): string;
|
|
28
40
|
/**
|
|
29
41
|
* Returns the backup directory path, creating it if it does not exist.
|
|
30
42
|
*
|
package/dist/core/paths.js
CHANGED
|
@@ -36,6 +36,22 @@ export function getDataDir() {
|
|
|
36
36
|
export function getStatePath() {
|
|
37
37
|
return path.join(getDataDir(), 'state.json');
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns the legacy (pre-`~/.oss-autopilot`) state file path: `./data/state.json`
|
|
41
|
+
* relative to the working directory.
|
|
42
|
+
*
|
|
43
|
+
* Functions rather than module constants so tests can mock them per-file:
|
|
44
|
+
* as constants in state-persistence.ts they resolved to the one shared
|
|
45
|
+
* `packages/core/data/` during vitest, and the migration tests racing
|
|
46
|
+
* parallel workers on that real directory caused CI flakes (#1382).
|
|
47
|
+
*/
|
|
48
|
+
export function getLegacyStatePath() {
|
|
49
|
+
return path.join(process.cwd(), 'data', 'state.json');
|
|
50
|
+
}
|
|
51
|
+
/** Legacy backup directory (`./data/backups`); see {@link getLegacyStatePath}. */
|
|
52
|
+
export function getLegacyBackupDir() {
|
|
53
|
+
return path.join(process.cwd(), 'data', 'backups');
|
|
54
|
+
}
|
|
39
55
|
/**
|
|
40
56
|
* Returns the backup directory path, creating it if it does not exist.
|
|
41
57
|
*
|
|
@@ -16,6 +16,7 @@ import type { Octokit } from '@octokit/rest';
|
|
|
16
16
|
export interface PRReviewEntry {
|
|
17
17
|
author: string;
|
|
18
18
|
authorAssociation: string;
|
|
19
|
+
/** `<github-content>`-fenced review body (#1372). */
|
|
19
20
|
body: string;
|
|
20
21
|
submittedAt: string;
|
|
21
22
|
}
|
|
@@ -23,6 +24,7 @@ export interface PRReviewEntry {
|
|
|
23
24
|
export interface PRReviewCommentEntry {
|
|
24
25
|
author: string;
|
|
25
26
|
authorAssociation: string;
|
|
27
|
+
/** `<github-content>`-fenced comment body (#1372). */
|
|
26
28
|
body: string;
|
|
27
29
|
path: string;
|
|
28
30
|
createdAt: string;
|
|
@@ -31,6 +33,7 @@ export interface PRReviewCommentEntry {
|
|
|
31
33
|
export interface PRIssueCommentEntry {
|
|
32
34
|
author: string;
|
|
33
35
|
authorAssociation: string;
|
|
36
|
+
/** `<github-content>`-fenced comment body (#1372). */
|
|
34
37
|
body: string;
|
|
35
38
|
createdAt: string;
|
|
36
39
|
}
|
|
@@ -62,8 +65,13 @@ export declare function fetchPRCommentBundle(octokit: Octokit, prUrl: string, gi
|
|
|
62
65
|
* `{ bundles, failures }` so the caller can decide whether to retry, surface
|
|
63
66
|
* a partial-data banner, or proceed. Rationale: extraction quality is already
|
|
64
67
|
* a partial-information problem (users contribute to many repos and many PRs),
|
|
65
|
-
* so a single 404 /
|
|
66
|
-
* from the other 4 — but the failure should still be visible (#1209 L8).
|
|
68
|
+
* so a single 404 / transient failure on one PR should not deny the host the
|
|
69
|
+
* corpus from the other 4 — but the failure should still be visible (#1209 L8).
|
|
70
|
+
*
|
|
71
|
+
* Rate-limit / auth errors are the exception: they reject the whole batch
|
|
72
|
+
* (#1391). Under throttling every remaining PR would fail the same way, so
|
|
73
|
+
* degrading to "skipped N PRs" silently masks a systemic failure the caller
|
|
74
|
+
* needs to surface (the CLI/MCP error envelope on `guidelines fetch-corpus`).
|
|
67
75
|
*/
|
|
68
76
|
export interface PRCommentBundlesBatchResult {
|
|
69
77
|
bundles: PRCommentBundle[];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { paginateAll } from './pagination.js';
|
|
2
|
+
import { wrapUntrustedContent } from './untrusted-content.js';
|
|
2
3
|
import { isBotAuthor } from './comment-utils.js';
|
|
3
4
|
import { parseGitHubUrl } from './urls.js';
|
|
4
|
-
import { ValidationError, errorMessage } from './errors.js';
|
|
5
|
+
import { ValidationError, errorMessage, isRateLimitOrAuthError } from './errors.js';
|
|
5
6
|
import { debug, warn } from './logger.js';
|
|
6
7
|
const MODULE = 'pr-comments-fetcher';
|
|
7
8
|
/** Default concurrency for {@link fetchPRCommentBundlesBatch}. */
|
|
@@ -60,6 +61,12 @@ export async function fetchPRCommentBundle(octokit, prUrl, githubUsername) {
|
|
|
60
61
|
return true;
|
|
61
62
|
};
|
|
62
63
|
const mergedAt = pr.merged_at ?? pr.closed_at ?? '';
|
|
64
|
+
// Fence every body at fetch time (#1372): the bundle's only consumer is
|
|
65
|
+
// `guidelines fetch-corpus`, whose output goes straight into the host's
|
|
66
|
+
// extract-learnings prompt — there is no human-display or string-matching
|
|
67
|
+
// consumer downstream, so fetch-time wrapping is safe here.
|
|
68
|
+
const fenceLabel = `${repoFull}#${pull_number}`;
|
|
69
|
+
const fence = (body, source, author, association) => wrapUntrustedContent(body, fenceLabel, { author, association, source });
|
|
63
70
|
return {
|
|
64
71
|
prUrl,
|
|
65
72
|
prTitle: pr.title,
|
|
@@ -70,7 +77,7 @@ export async function fetchPRCommentBundle(octokit, prUrl, githubUsername) {
|
|
|
70
77
|
.map((r) => ({
|
|
71
78
|
author: r.user?.login ?? '',
|
|
72
79
|
authorAssociation: r.author_association ?? 'NONE',
|
|
73
|
-
body: r.body ?? '',
|
|
80
|
+
body: fence(r.body ?? '', 'pr-review', r.user?.login ?? '', r.author_association ?? 'NONE'),
|
|
74
81
|
submittedAt: r.submitted_at ?? '',
|
|
75
82
|
})),
|
|
76
83
|
reviewComments: reviewComments
|
|
@@ -78,7 +85,7 @@ export async function fetchPRCommentBundle(octokit, prUrl, githubUsername) {
|
|
|
78
85
|
.map((c) => ({
|
|
79
86
|
author: c.user?.login ?? '',
|
|
80
87
|
authorAssociation: c.author_association ?? 'NONE',
|
|
81
|
-
body: c.body ?? '',
|
|
88
|
+
body: fence(c.body ?? '', 'pr-review-comment', c.user?.login ?? '', c.author_association ?? 'NONE'),
|
|
82
89
|
path: c.path ?? '',
|
|
83
90
|
createdAt: c.created_at ?? '',
|
|
84
91
|
})),
|
|
@@ -87,7 +94,7 @@ export async function fetchPRCommentBundle(octokit, prUrl, githubUsername) {
|
|
|
87
94
|
.map((c) => ({
|
|
88
95
|
author: c.user?.login ?? '',
|
|
89
96
|
authorAssociation: c.author_association ?? 'NONE',
|
|
90
|
-
body: c.body ?? '',
|
|
97
|
+
body: fence(c.body ?? '', 'pr-issue-comment', c.user?.login ?? '', c.author_association ?? 'NONE'),
|
|
91
98
|
createdAt: c.created_at ?? '',
|
|
92
99
|
})),
|
|
93
100
|
};
|
|
@@ -96,8 +103,11 @@ export async function fetchPRCommentBundlesBatch(octokit, prUrls, githubUsername
|
|
|
96
103
|
const bundles = [];
|
|
97
104
|
const failures = [];
|
|
98
105
|
const queue = [...prUrls];
|
|
106
|
+
let aborted = false;
|
|
99
107
|
async function worker() {
|
|
100
108
|
while (queue.length > 0) {
|
|
109
|
+
if (aborted)
|
|
110
|
+
return;
|
|
101
111
|
const url = queue.shift();
|
|
102
112
|
if (!url)
|
|
103
113
|
return;
|
|
@@ -106,6 +116,14 @@ export async function fetchPRCommentBundlesBatch(octokit, prUrls, githubUsername
|
|
|
106
116
|
bundles.push(bundle);
|
|
107
117
|
}
|
|
108
118
|
catch (err) {
|
|
119
|
+
// Rate-limit / auth failures abort the batch instead of degrading to
|
|
120
|
+
// a per-PR skip — see the doc comment above (#1391). The abort flag
|
|
121
|
+
// stops sibling workers from burning more rate-limited requests on
|
|
122
|
+
// results that will be discarded when Promise.all rejects.
|
|
123
|
+
if (isRateLimitOrAuthError(err)) {
|
|
124
|
+
aborted = true;
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
109
127
|
const errorMsg = errorMessage(err);
|
|
110
128
|
failures.push({ prUrl: url, error: errorMsg });
|
|
111
129
|
warn(MODULE, `Skipping ${url}: ${errorMsg}`);
|
|
@@ -45,16 +45,41 @@ export declare function migrateV3ToV4(rawState: Record<string, unknown>): Record
|
|
|
45
45
|
* Leverages Zod schema defaults to produce a complete state.
|
|
46
46
|
*/
|
|
47
47
|
export declare function createFreshState(): AgentState;
|
|
48
|
+
/**
|
|
49
|
+
* Details about a recovery that happened during {@link loadState} — set when
|
|
50
|
+
* the main state file was unreadable (JSON parse error) or structurally
|
|
51
|
+
* invalid (Zod rejection) and the loader fell back to a backup or fresh state.
|
|
52
|
+
* Surfaced through StateManager.getLoadRecovery() so commands can include the
|
|
53
|
+
* event in structured output instead of only stderr warn() lines (#1371).
|
|
54
|
+
*/
|
|
55
|
+
export interface LoadRecoveryInfo {
|
|
56
|
+
/** Where the loaded state came from after the original file was rejected. */
|
|
57
|
+
source: 'backup' | 'fresh';
|
|
58
|
+
/** Path of the preserved `.rejected-<ts>` copy, or null if preservation failed. */
|
|
59
|
+
rejectedPath: string | null;
|
|
60
|
+
/** Short summary of why the original state file was rejected. */
|
|
61
|
+
reason: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Result of {@link loadState}: the loaded state, the file's mtime for change
|
|
65
|
+
* detection, and (only when the main file was rejected) recovery details.
|
|
66
|
+
*/
|
|
67
|
+
export interface LoadStateResult {
|
|
68
|
+
state: AgentState;
|
|
69
|
+
mtimeMs: number;
|
|
70
|
+
recovery?: LoadRecoveryInfo;
|
|
71
|
+
}
|
|
48
72
|
/**
|
|
49
73
|
* Load state from file, or create initial state if none exists.
|
|
50
74
|
* If the main state file is corrupted, attempts to restore from the most recent backup.
|
|
51
75
|
* Performs migration from legacy ./data/ location if needed.
|
|
52
|
-
* @returns Object with the loaded state
|
|
76
|
+
* @returns Object with the loaded state, the file's mtime (for change detection),
|
|
77
|
+
* and recovery details when the main file was rejected.
|
|
78
|
+
* @throws Error when the state file exists but cannot be read due to a
|
|
79
|
+
* permission error (EACCES/EPERM) — that is an environment problem, not
|
|
80
|
+
* corruption, so restore-or-fresh would mask it and risk clobbering data.
|
|
53
81
|
*/
|
|
54
|
-
export declare function loadState():
|
|
55
|
-
state: AgentState;
|
|
56
|
-
mtimeMs: number;
|
|
57
|
-
};
|
|
82
|
+
export declare function loadState(): LoadStateResult;
|
|
58
83
|
/**
|
|
59
84
|
* Persist state to disk, creating a timestamped backup of the previous
|
|
60
85
|
* state file first. Retains at most 10 backup files.
|
|
@@ -79,7 +104,4 @@ export declare function saveState(state: Readonly<AgentState>, expectedMtimeMs?:
|
|
|
79
104
|
* Uses mtime comparison (single statSync call) to avoid unnecessary JSON parsing.
|
|
80
105
|
* @returns The new state and mtime if reloaded, or null if no change detected.
|
|
81
106
|
*/
|
|
82
|
-
export declare function reloadStateIfChanged(lastLoadedMtimeMs: number):
|
|
83
|
-
state: AgentState;
|
|
84
|
-
mtimeMs: number;
|
|
85
|
-
} | null;
|
|
107
|
+
export declare function reloadStateIfChanged(lastLoadedMtimeMs: number): LoadStateResult | null;
|