@oss-autopilot/core 3.2.0 → 3.4.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 +1 -1
- package/dist/cli-registry.js +39 -3
- package/dist/cli.bundle.cjs +103 -75
- package/dist/cli.js +17 -3
- package/dist/commands/check-integration.js +8 -8
- package/dist/commands/comments.js +3 -0
- package/dist/commands/config.js +14 -7
- package/dist/commands/daily-render.js +10 -5
- package/dist/commands/daily.d.ts +3 -9
- package/dist/commands/daily.js +12 -21
- package/dist/commands/dashboard-data.js +1 -1
- package/dist/commands/dashboard-lifecycle.js +1 -1
- package/dist/commands/dashboard-process.js +4 -4
- package/dist/commands/dashboard-server.js +26 -7
- package/dist/commands/dashboard.js +2 -2
- package/dist/commands/detect-formatters.js +3 -3
- package/dist/commands/doctor.js +5 -5
- package/dist/commands/guidelines.d.ts +10 -0
- package/dist/commands/guidelines.js +25 -6
- package/dist/commands/list-move-tier.js +5 -5
- package/dist/commands/local-repos.js +9 -9
- package/dist/commands/parse-list.js +10 -10
- package/dist/commands/scout-bridge.js +2 -2
- package/dist/commands/setup.js +24 -13
- package/dist/commands/skip-add.js +6 -3
- package/dist/commands/skip-file-parser.js +3 -3
- package/dist/commands/startup.js +11 -8
- package/dist/commands/state-cmd.js +1 -1
- package/dist/commands/status.js +7 -0
- package/dist/commands/validation.js +12 -3
- package/dist/commands/vet-list.js +12 -8
- package/dist/commands/vet.js +1 -2
- package/dist/core/__fixtures__/prompt-injection-payloads.d.ts +22 -0
- package/dist/core/__fixtures__/prompt-injection-payloads.js +109 -0
- package/dist/core/anti-llm-policy.js +5 -5
- package/dist/core/auth.js +12 -8
- package/dist/core/daily-logic.d.ts +13 -1
- package/dist/core/daily-logic.js +31 -4
- package/dist/core/dates.js +3 -3
- package/dist/core/errors.d.ts +29 -0
- package/dist/core/errors.js +63 -0
- package/dist/core/formatter-detection.js +9 -9
- package/dist/core/gist-state-store.d.ts +42 -3
- package/dist/core/gist-state-store.js +89 -19
- package/dist/core/guidelines-store.js +2 -2
- package/dist/core/http-cache.js +16 -7
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +6 -1
- package/dist/core/issue-conversation.js +3 -1
- package/dist/core/paths.js +4 -4
- package/dist/core/placeholder-usernames.d.ts +1 -0
- package/dist/core/placeholder-usernames.js +27 -0
- package/dist/core/pr-comments-fetcher.d.ts +14 -6
- package/dist/core/pr-comments-fetcher.js +8 -14
- package/dist/core/pr-monitor.d.ts +0 -2
- package/dist/core/pr-monitor.js +2 -25
- package/dist/core/pr-template.js +1 -1
- package/dist/core/state-persistence.d.ts +2 -2
- package/dist/core/state-persistence.js +15 -12
- package/dist/core/state-schema.js +8 -4
- package/dist/core/state.d.ts +27 -0
- package/dist/core/state.js +71 -14
- package/dist/core/untrusted-content.d.ts +48 -0
- package/dist/core/untrusted-content.js +106 -0
- package/dist/core/urls.js +2 -2
- package/dist/formatters/json.d.ts +53 -3
- package/dist/formatters/json.js +49 -14
- package/package.json +3 -3
package/dist/core/http-cache.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* for the same endpoint (e.g., star counts for two PRs in the same repo)
|
|
10
10
|
* share a single HTTP round-trip.
|
|
11
11
|
*/
|
|
12
|
-
import * as fs from 'fs';
|
|
13
|
-
import * as path from 'path';
|
|
14
|
-
import * as crypto from 'crypto';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as crypto from 'node:crypto';
|
|
15
15
|
import { getCacheDir } from './paths.js';
|
|
16
16
|
import { debug } from './logger.js';
|
|
17
17
|
import { getHttpStatusCode } from './errors.js';
|
|
@@ -76,7 +76,7 @@ export class HttpCache {
|
|
|
76
76
|
get(url) {
|
|
77
77
|
const filePath = this.pathFor(url);
|
|
78
78
|
try {
|
|
79
|
-
const raw = fs.readFileSync(filePath, '
|
|
79
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
80
80
|
const entry = JSON.parse(raw);
|
|
81
81
|
// Sanity-check: the file should contain the URL we asked for
|
|
82
82
|
if (entry.url !== url) {
|
|
@@ -85,7 +85,16 @@ export class HttpCache {
|
|
|
85
85
|
}
|
|
86
86
|
return entry;
|
|
87
87
|
}
|
|
88
|
-
catch {
|
|
88
|
+
catch (err) {
|
|
89
|
+
// ENOENT (cache miss) is the common case and not worth logging — but
|
|
90
|
+
// EISDIR / JSON parse / disk corruption all looked identical to a miss
|
|
91
|
+
// before, hiding repeated cache misses caused by a corrupt entry. Log
|
|
92
|
+
// anything that's not "file not found" so the cause becomes
|
|
93
|
+
// diagnosable in DEBUG mode (#1209 L5).
|
|
94
|
+
if (err && typeof err === 'object' && err.code !== 'ENOENT') {
|
|
95
|
+
const msg = err instanceof Error ? err.message : 'unknown error';
|
|
96
|
+
debug(MODULE, `Cache read failed for ${url} (treating as miss): ${msg}`);
|
|
97
|
+
}
|
|
89
98
|
return null;
|
|
90
99
|
}
|
|
91
100
|
}
|
|
@@ -100,7 +109,7 @@ export class HttpCache {
|
|
|
100
109
|
cachedAt: new Date().toISOString(),
|
|
101
110
|
};
|
|
102
111
|
try {
|
|
103
|
-
fs.writeFileSync(this.pathFor(url), JSON.stringify(entry), { encoding: '
|
|
112
|
+
fs.writeFileSync(this.pathFor(url), JSON.stringify(entry), { encoding: 'utf8', mode: 0o600 });
|
|
104
113
|
debug(MODULE, `Cached response for ${url}`);
|
|
105
114
|
// Best-effort size cap (#1057 M27). Runs after each write rather than on
|
|
106
115
|
// a schedule so long-lived sessions can't accumulate past the cap.
|
|
@@ -191,7 +200,7 @@ export class HttpCache {
|
|
|
191
200
|
continue;
|
|
192
201
|
const filePath = path.join(this.cacheDir, file);
|
|
193
202
|
try {
|
|
194
|
-
const raw = fs.readFileSync(filePath, '
|
|
203
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
195
204
|
const entry = JSON.parse(raw);
|
|
196
205
|
const age = now - new Date(entry.cachedAt).getTime();
|
|
197
206
|
if (age > maxAgeMs) {
|
package/dist/core/index.d.ts
CHANGED
|
@@ -8,6 +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
12
|
export { getOctokit, checkRateLimit, type RateLimitInfo } from './github.js';
|
|
12
13
|
export { parseGitHubUrl, splitRepo, isOwnRepo } from './urls.js';
|
|
13
14
|
export { daysBetween, formatRelativeTime, byDateDescending } from './dates.js';
|
|
@@ -17,7 +18,7 @@ export { DEFAULT_CONCURRENCY } from './concurrency.js';
|
|
|
17
18
|
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
|
|
18
19
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
19
20
|
export { HttpCache, getHttpCache, cachedRequest, type CacheEntry } from './http-cache.js';
|
|
20
|
-
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
21
|
+
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, buildStarFilter, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
21
22
|
export { computeContributionStats, type ContributionStats, type ComputeStatsInput } from './stats.js';
|
|
22
23
|
export { fetchPRTemplate, type PRTemplateResult } from './pr-template.js';
|
|
23
24
|
export { classifyLinkedPR, type LinkedPR, type LinkedPRClassification, type LinkedPRState, } from './linked-pr-classification.js';
|
|
@@ -25,4 +26,5 @@ export { scanForAntiLLMPolicy, type AntiLLMCategory, type AntiLLMMatch, type Ant
|
|
|
25
26
|
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, type DetectedFormatter, type FormatterDetectionResult, type CIFormatterDiagnosis, type FormatterName, } from './formatter-detection.js';
|
|
26
27
|
export { CONFIG_KEY_REGISTRY, type ConfigKeyDef, type SettableVia, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
|
|
27
28
|
export { DashboardDataSchema, DashboardStatsSchema, validateDashboardData, type DashboardDataParsed, } from './dashboard-data-schema.js';
|
|
29
|
+
export { fetchPRCommentBundle, fetchPRCommentBundlesBatch, type PRCommentBundle, type PRReviewEntry, type PRReviewCommentEntry, type PRIssueCommentEntry, } from './pr-comments-fetcher.js';
|
|
28
30
|
export * from './types.js';
|
package/dist/core/index.js
CHANGED
|
@@ -9,6 +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
13
|
export { getOctokit, checkRateLimit } from './github.js';
|
|
13
14
|
export { parseGitHubUrl, splitRepo, isOwnRepo } from './urls.js';
|
|
14
15
|
export { daysBetween, formatRelativeTime, byDateDescending } from './dates.js';
|
|
@@ -18,7 +19,7 @@ export { DEFAULT_CONCURRENCY } from './concurrency.js';
|
|
|
18
19
|
export { OssAutopilotError, ConfigurationError, ValidationError, GistPermissionError, errorMessage, getHttpStatusCode, isRateLimitError, isRateLimitOrAuthError, nonFatalCatch, resolveErrorCode, } from './errors.js';
|
|
19
20
|
export { enableDebug, isDebugEnabled, debug, info, warn, timed } from './logger.js';
|
|
20
21
|
export { HttpCache, getHttpCache, cachedRequest } from './http-cache.js';
|
|
21
|
-
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
22
|
+
export { CRITICAL_STATUSES, applyStatusOverrides, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, buildStarFilter, formatActionHint, formatBriefSummary, formatSummary, printDigest, } from './daily-logic.js';
|
|
22
23
|
export { computeContributionStats } from './stats.js';
|
|
23
24
|
export { fetchPRTemplate } from './pr-template.js';
|
|
24
25
|
export { classifyLinkedPR, } from './linked-pr-classification.js';
|
|
@@ -26,4 +27,8 @@ export { scanForAntiLLMPolicy, } from './anti-llm-policy.js';
|
|
|
26
27
|
export { detectFormatters, diagnoseCIFormatterFailure, getPreferredFormatter, } from './formatter-detection.js';
|
|
27
28
|
export { CONFIG_KEY_REGISTRY, isKnownKey, getKeyDef, getSetupKeys, getConfigKeys, suggestKey, formatUnknownKeyError, } from './config-registry.js';
|
|
28
29
|
export { DashboardDataSchema, DashboardStatsSchema, validateDashboardData, } from './dashboard-data-schema.js';
|
|
30
|
+
// PR comment bundle types — wire shape consumed by the MCP extract-learnings
|
|
31
|
+
// prompt. Re-exported so MCP doesn't have to redeclare the interface and
|
|
32
|
+
// silently drift (#1208 M5).
|
|
33
|
+
export { fetchPRCommentBundle, fetchPRCommentBundlesBatch, } from './pr-comments-fetcher.js';
|
|
29
34
|
export * from './types.js';
|
|
@@ -151,7 +151,9 @@ export class IssueConversationMonitor {
|
|
|
151
151
|
body: comment.body || '',
|
|
152
152
|
createdAt: comment.created_at,
|
|
153
153
|
isUser: author.toLowerCase() === username.toLowerCase(),
|
|
154
|
-
authorAssociation:
|
|
154
|
+
authorAssociation: typeof comment.author_association === 'string'
|
|
155
|
+
? comment.author_association
|
|
156
|
+
: '',
|
|
155
157
|
});
|
|
156
158
|
}
|
|
157
159
|
timeline.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
package/dist/core/paths.js
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Extracted from utils.ts under #1116.
|
|
8
8
|
*/
|
|
9
|
-
import * as fs from 'fs';
|
|
10
|
-
import * as path from 'path';
|
|
11
|
-
import * as os from 'os';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
12
|
/**
|
|
13
13
|
* Returns the oss-autopilot data directory path, creating it if it does not exist.
|
|
14
14
|
*
|
|
@@ -98,7 +98,7 @@ export function stateFileExists() {
|
|
|
98
98
|
export function getCLIVersion() {
|
|
99
99
|
try {
|
|
100
100
|
const pkgPath = path.join(path.dirname(process.argv[1]), '..', 'package.json');
|
|
101
|
-
return JSON.parse(fs.readFileSync(pkgPath, '
|
|
101
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
|
|
102
102
|
}
|
|
103
103
|
catch {
|
|
104
104
|
return '0.0.0';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isPlaceholderUsername(username: string): boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Known placeholder values that can end up in `config.githubUsername` from
|
|
3
|
+
* doc snippets, example configs, or aborted setup flows.
|
|
4
|
+
*
|
|
5
|
+
* Two callers consult this list:
|
|
6
|
+
* - `pr-monitor.ts` — runtime auto-repair: if a fetch is about to run with
|
|
7
|
+
* a placeholder, the configured value is replaced with the authenticated
|
|
8
|
+
* viewer's login before the search hits GitHub. Without this, the search
|
|
9
|
+
* silently returns zero results and the dashboard looks like a fresh install.
|
|
10
|
+
* - `commands/validation.ts` — write-side rejection: prevents `init`,
|
|
11
|
+
* `setup --set username=`, and the MCP `config` tool from persisting one
|
|
12
|
+
* of these values in the first place, so the auto-repair only runs as a
|
|
13
|
+
* fallback for legacy state rather than masking a fresh user error.
|
|
14
|
+
*
|
|
15
|
+
* Entries must be lowercase — `Lowercase<string>` on the source tuple makes a
|
|
16
|
+
* non-lowercase entry a compile error, keeping the case-insensitive lookup
|
|
17
|
+
* contract type-checked instead of comment-documented.
|
|
18
|
+
*/
|
|
19
|
+
const PLACEHOLDER_USERNAMES = [
|
|
20
|
+
'example-user',
|
|
21
|
+
'your-username',
|
|
22
|
+
'your-github-username',
|
|
23
|
+
];
|
|
24
|
+
const KNOWN_PLACEHOLDER_USERNAMES = new Set(PLACEHOLDER_USERNAMES);
|
|
25
|
+
export function isPlaceholderUsername(username) {
|
|
26
|
+
return KNOWN_PLACEHOLDER_USERNAMES.has(username.toLowerCase());
|
|
27
|
+
}
|
|
@@ -58,10 +58,18 @@ export declare function fetchPRCommentBundle(octokit: Octokit, prUrl: string, gi
|
|
|
58
58
|
/**
|
|
59
59
|
* Fetch comment bundles for many PRs with a small concurrency cap (default 3).
|
|
60
60
|
*
|
|
61
|
-
* Failures on individual PRs are logged and
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
61
|
+
* Failures on individual PRs are logged and recorded — the batch returns
|
|
62
|
+
* `{ bundles, failures }` so the caller can decide whether to retry, surface
|
|
63
|
+
* a partial-data banner, or proceed. Rationale: extraction quality is already
|
|
64
|
+
* a partial-information problem (users contribute to many repos and many PRs),
|
|
65
|
+
* so a single 404 / rate limit on one PR should not deny the host the corpus
|
|
66
|
+
* from the other 4 — but the failure should still be visible (#1209 L8).
|
|
66
67
|
*/
|
|
67
|
-
export
|
|
68
|
+
export interface PRCommentBundlesBatchResult {
|
|
69
|
+
bundles: PRCommentBundle[];
|
|
70
|
+
failures: Array<{
|
|
71
|
+
prUrl: string;
|
|
72
|
+
error: string;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
export declare function fetchPRCommentBundlesBatch(octokit: Octokit, prUrls: string[], githubUsername: string, concurrency?: number): Promise<PRCommentBundlesBatchResult>;
|
|
@@ -92,17 +92,9 @@ export async function fetchPRCommentBundle(octokit, prUrl, githubUsername) {
|
|
|
92
92
|
})),
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Fetch comment bundles for many PRs with a small concurrency cap (default 3).
|
|
97
|
-
*
|
|
98
|
-
* Failures on individual PRs are logged and skipped — the batch returns a
|
|
99
|
-
* shorter array rather than aborting. Rationale: extraction quality is
|
|
100
|
-
* already a partial-information problem (users contribute to many repos and
|
|
101
|
-
* many PRs), so a single 404 / rate limit on one PR should not deny the
|
|
102
|
-
* host the corpus from the other 4.
|
|
103
|
-
*/
|
|
104
95
|
export async function fetchPRCommentBundlesBatch(octokit, prUrls, githubUsername, concurrency = DEFAULT_BATCH_CONCURRENCY) {
|
|
105
|
-
const
|
|
96
|
+
const bundles = [];
|
|
97
|
+
const failures = [];
|
|
106
98
|
const queue = [...prUrls];
|
|
107
99
|
async function worker() {
|
|
108
100
|
while (queue.length > 0) {
|
|
@@ -111,15 +103,17 @@ export async function fetchPRCommentBundlesBatch(octokit, prUrls, githubUsername
|
|
|
111
103
|
return;
|
|
112
104
|
try {
|
|
113
105
|
const bundle = await fetchPRCommentBundle(octokit, url, githubUsername);
|
|
114
|
-
|
|
106
|
+
bundles.push(bundle);
|
|
115
107
|
}
|
|
116
108
|
catch (err) {
|
|
117
|
-
|
|
109
|
+
const errorMsg = errorMessage(err);
|
|
110
|
+
failures.push({ prUrl: url, error: errorMsg });
|
|
111
|
+
warn(MODULE, `Skipping ${url}: ${errorMsg}`);
|
|
118
112
|
}
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
115
|
const workers = Array.from({ length: Math.min(concurrency, prUrls.length) }, worker);
|
|
122
116
|
await Promise.all(workers);
|
|
123
|
-
debug(MODULE, `Fetched ${
|
|
124
|
-
return
|
|
117
|
+
debug(MODULE, `Fetched ${bundles.length}/${prUrls.length} comment bundles (${failures.length} failed)`);
|
|
118
|
+
return { bundles, failures };
|
|
125
119
|
}
|
|
@@ -18,8 +18,6 @@ export { computeDisplayLabel } from './display-utils.js';
|
|
|
18
18
|
export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
|
|
19
19
|
export { isConditionalChecklistItem } from './checklist-analysis.js';
|
|
20
20
|
export { determineStatus } from './status-determination.js';
|
|
21
|
-
declare function isPlaceholderUsername(username: string): boolean;
|
|
22
|
-
export { isPlaceholderUsername };
|
|
23
21
|
/**
|
|
24
22
|
* Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
|
|
25
23
|
* Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
|
package/dist/core/pr-monitor.js
CHANGED
|
@@ -16,9 +16,8 @@ import { getOctokit } from './github.js';
|
|
|
16
16
|
import { getStateManager } from './state.js';
|
|
17
17
|
import { daysBetween } from './dates.js';
|
|
18
18
|
import { parseGitHubUrl, extractOwnerRepo, isOwnRepo } from './urls.js';
|
|
19
|
-
import { DEFAULT_CONCURRENCY } from './concurrency.js';
|
|
19
|
+
import { DEFAULT_CONCURRENCY, runWorkerPool } from './concurrency.js';
|
|
20
20
|
import { determineStatus } from './status-determination.js';
|
|
21
|
-
import { runWorkerPool } from './concurrency.js';
|
|
22
21
|
import { ConfigurationError, ValidationError, errorMessage, getHttpStatusCode, isRateLimitOrAuthError, } from './errors.js';
|
|
23
22
|
import { paginateAll } from './pagination.js';
|
|
24
23
|
import { debug, warn, timed } from './logger.js';
|
|
@@ -29,34 +28,12 @@ import { analyzeChecklist } from './checklist-analysis.js';
|
|
|
29
28
|
import { extractMaintainerActionHints } from './maintainer-analysis.js';
|
|
30
29
|
import { computeDisplayLabel } from './display-utils.js';
|
|
31
30
|
import { fetchUserMergedPRCounts as fetchUserMergedPRCountsImpl, fetchUserClosedPRCounts as fetchUserClosedPRCountsImpl, fetchRecentlyClosedPRs as fetchRecentlyClosedPRsImpl, fetchRecentlyMergedPRs as fetchRecentlyMergedPRsImpl, } from './github-stats.js';
|
|
31
|
+
import { isPlaceholderUsername } from './placeholder-usernames.js';
|
|
32
32
|
// Re-export so existing consumers can still import from pr-monitor
|
|
33
33
|
export { computeDisplayLabel } from './display-utils.js';
|
|
34
34
|
export { classifyCICheck, classifyFailingChecks, getCIStatus } from './ci-analysis.js';
|
|
35
35
|
export { isConditionalChecklistItem } from './checklist-analysis.js';
|
|
36
36
|
export { determineStatus } from './status-determination.js';
|
|
37
|
-
/**
|
|
38
|
-
* Known placeholder values that can end up in `config.githubUsername` from
|
|
39
|
-
* doc snippets, example configs, or aborted setup flows. When the configured
|
|
40
|
-
* username matches one of these, the PR fetch silently returns zero results
|
|
41
|
-
* and the dashboard looks like a fresh install. Detecting these lets us
|
|
42
|
-
* auto-repair the config from the authenticated viewer before fetching.
|
|
43
|
-
*
|
|
44
|
-
* Entries must be lowercase — `Lowercase<string>` on the source tuple makes
|
|
45
|
-
* a non-lowercase entry a compile error, keeping the case-insensitive lookup
|
|
46
|
-
* contract type-checked instead of comment-documented.
|
|
47
|
-
*/
|
|
48
|
-
const PLACEHOLDER_USERNAMES = [
|
|
49
|
-
'example-user',
|
|
50
|
-
'your-username',
|
|
51
|
-
'your-github-username',
|
|
52
|
-
];
|
|
53
|
-
const KNOWN_PLACEHOLDER_USERNAMES = new Set(PLACEHOLDER_USERNAMES);
|
|
54
|
-
function isPlaceholderUsername(username) {
|
|
55
|
-
return KNOWN_PLACEHOLDER_USERNAMES.has(username.toLowerCase());
|
|
56
|
-
}
|
|
57
|
-
// Module-private on purpose: callers should only use the predicate so the
|
|
58
|
-
// `.toLowerCase()` contract can't be bypassed by reading the set directly.
|
|
59
|
-
export { isPlaceholderUsername };
|
|
60
37
|
/**
|
|
61
38
|
* Check if a PR has a merge conflict based on GitHub's mergeable flag and mergeable_state.
|
|
62
39
|
* Returns true when mergeable is explicitly false or the mergeable_state is 'dirty'.
|
package/dist/core/pr-template.js
CHANGED
|
@@ -43,7 +43,7 @@ export async function fetchPRTemplate(octokit, owner, repo) {
|
|
|
43
43
|
debug(MODULE, `${path} has no content, skipping`);
|
|
44
44
|
continue;
|
|
45
45
|
}
|
|
46
|
-
const template = Buffer.from(data.content, 'base64').toString('
|
|
46
|
+
const template = Buffer.from(data.content, 'base64').toString('utf8');
|
|
47
47
|
debug(MODULE, `Found PR template at ${path} (${template.length} chars)`);
|
|
48
48
|
return { template, source: path };
|
|
49
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* State persistence layer for the OSS Contribution Agent.
|
|
3
|
-
* Handles file I/O, locking, backup/restore, and schema migration (v1→v2→v3).
|
|
3
|
+
* Handles file I/O, locking, backup/restore, and schema migration (v1→v2→v3→v4).
|
|
4
4
|
* No module-level mutable state — functions accept/return AgentState objects.
|
|
5
5
|
*/
|
|
6
6
|
import { AgentState } from './types.js';
|
|
@@ -41,7 +41,7 @@ export declare function migrateV2ToV3(rawState: Record<string, unknown>): Record
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function migrateV3ToV4(rawState: Record<string, unknown>): Record<string, unknown>;
|
|
43
43
|
/**
|
|
44
|
-
* Create a fresh state (
|
|
44
|
+
* Create a fresh state (v4).
|
|
45
45
|
* Leverages Zod schema defaults to produce a complete state.
|
|
46
46
|
*/
|
|
47
47
|
export declare function createFreshState(): AgentState;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* State persistence layer for the OSS Contribution Agent.
|
|
3
|
-
* Handles file I/O, locking, backup/restore, and schema migration (v1→v2→v3).
|
|
3
|
+
* Handles file I/O, locking, backup/restore, and schema migration (v1→v2→v3→v4).
|
|
4
4
|
* No module-level mutable state — functions accept/return AgentState objects.
|
|
5
5
|
*/
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import * as path from 'path';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
8
|
import { AgentStateSchema } from './state-schema.js';
|
|
9
9
|
import { getStatePath, getBackupDir, getDataDir } from './paths.js';
|
|
10
10
|
import { errorMessage, ConcurrencyError } from './errors.js';
|
|
@@ -21,7 +21,7 @@ const LEGACY_BACKUP_DIR = path.join(process.cwd(), 'data', 'backups');
|
|
|
21
21
|
*/
|
|
22
22
|
function isLockStale(lockPath) {
|
|
23
23
|
try {
|
|
24
|
-
const existing = JSON.parse(fs.readFileSync(lockPath, '
|
|
24
|
+
const existing = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
25
25
|
return Date.now() - existing.timestamp > LOCK_TIMEOUT_MS;
|
|
26
26
|
}
|
|
27
27
|
catch (err) {
|
|
@@ -72,7 +72,7 @@ export function acquireLock(lockPath) {
|
|
|
72
72
|
*/
|
|
73
73
|
export function releaseLock(lockPath) {
|
|
74
74
|
try {
|
|
75
|
-
const data = JSON.parse(fs.readFileSync(lockPath, '
|
|
75
|
+
const data = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
76
76
|
if (data.pid === process.pid) {
|
|
77
77
|
fs.unlinkSync(lockPath);
|
|
78
78
|
}
|
|
@@ -170,7 +170,7 @@ export function migrateV3ToV4(rawState) {
|
|
|
170
170
|
return rawState;
|
|
171
171
|
}
|
|
172
172
|
/**
|
|
173
|
-
* Create a fresh state (
|
|
173
|
+
* Create a fresh state (v4).
|
|
174
174
|
* Leverages Zod schema defaults to produce a complete state.
|
|
175
175
|
*/
|
|
176
176
|
export function createFreshState() {
|
|
@@ -273,9 +273,9 @@ function tryRestoreFromBackup() {
|
|
|
273
273
|
for (const backupFile of backupFiles) {
|
|
274
274
|
const backupPath = path.join(backupDir, backupFile);
|
|
275
275
|
try {
|
|
276
|
-
const data = fs.readFileSync(backupPath, '
|
|
276
|
+
const data = fs.readFileSync(backupPath, 'utf8');
|
|
277
277
|
let raw = JSON.parse(data);
|
|
278
|
-
// Chain migrations: v1 → v2 → v3
|
|
278
|
+
// Chain migrations: v1 → v2 → v3 → v4
|
|
279
279
|
if (typeof raw === 'object' && raw !== null) {
|
|
280
280
|
const rawObj = raw;
|
|
281
281
|
if (rawObj.version === 1) {
|
|
@@ -306,8 +306,11 @@ function tryRestoreFromBackup() {
|
|
|
306
306
|
debug(MODULE, `Backup ${backupFile} full validation errors:`, parsed.error.issues);
|
|
307
307
|
}
|
|
308
308
|
catch (backupErr) {
|
|
309
|
-
// This backup is also corrupted, try the next one
|
|
310
|
-
warn
|
|
309
|
+
// This backup is also corrupted, try the next one. Include the error
|
|
310
|
+
// message in the warn so non-DEBUG users can diagnose without enabling
|
|
311
|
+
// DEBUG=1 (#1209 L7); the full stack still goes to debug.
|
|
312
|
+
const msg = backupErr instanceof Error ? backupErr.message : String(backupErr);
|
|
313
|
+
warn(MODULE, `Backup ${backupFile} is corrupted (${msg}), trying next...`);
|
|
311
314
|
debug(MODULE, `Backup ${backupFile} parse failed`, backupErr);
|
|
312
315
|
}
|
|
313
316
|
}
|
|
@@ -325,7 +328,7 @@ export function loadState() {
|
|
|
325
328
|
const statePath = getStatePath();
|
|
326
329
|
try {
|
|
327
330
|
if (fs.existsSync(statePath)) {
|
|
328
|
-
const data = fs.readFileSync(statePath, '
|
|
331
|
+
const data = fs.readFileSync(statePath, 'utf8');
|
|
329
332
|
let raw = JSON.parse(data);
|
|
330
333
|
// Chain migrations: v1 → v2 → v3 → v4
|
|
331
334
|
let wasMigrated = false;
|
|
@@ -491,7 +494,7 @@ export function saveState(state, expectedMtimeMs = null) {
|
|
|
491
494
|
// Create backup of existing state (best-effort, non-fatal)
|
|
492
495
|
try {
|
|
493
496
|
if (fs.existsSync(statePath)) {
|
|
494
|
-
const timestamp = new Date().toISOString().replace(/[
|
|
497
|
+
const timestamp = new Date().toISOString().replace(/[.:]/g, '-');
|
|
495
498
|
const randomSuffix = Math.random().toString(36).slice(2, 8).padEnd(6, '0');
|
|
496
499
|
const backupFile = path.join(backupDir, `state-${timestamp}-${randomSuffix}.json`);
|
|
497
500
|
fs.copyFileSync(statePath, backupFile);
|
|
@@ -45,18 +45,22 @@ export const StoredMergedPRSchema = z.object({
|
|
|
45
45
|
title: z.string(),
|
|
46
46
|
mergedAt: z.string(),
|
|
47
47
|
/** When the raw review-comment bundle for this PR was last fetched (#867). */
|
|
48
|
-
|
|
48
|
+
// ISO-8601 datetime guards against `markPRCommentsFetched(url, "garbage")`
|
|
49
|
+
// poisoning state through the stamping API (#1209 L4).
|
|
50
|
+
commentsFetchedAt: z.string().datetime().optional(),
|
|
49
51
|
/** When the host last ran LLM extraction over this PR's comment bundle (#867). */
|
|
50
|
-
learningsExtractedAt: z.string().optional(),
|
|
52
|
+
learningsExtractedAt: z.string().datetime().optional(),
|
|
51
53
|
});
|
|
52
54
|
export const StoredClosedPRSchema = z.object({
|
|
53
55
|
url: z.string(),
|
|
54
56
|
title: z.string(),
|
|
55
57
|
closedAt: z.string(),
|
|
56
58
|
/** When the raw review-comment bundle for this PR was last fetched (#867). */
|
|
57
|
-
|
|
59
|
+
// ISO-8601 datetime guards against `markPRCommentsFetched(url, "garbage")`
|
|
60
|
+
// poisoning state through the stamping API (#1209 L4).
|
|
61
|
+
commentsFetchedAt: z.string().datetime().optional(),
|
|
58
62
|
/** When the host last ran LLM extraction over this PR's comment bundle (#867). */
|
|
59
|
-
learningsExtractedAt: z.string().optional(),
|
|
63
|
+
learningsExtractedAt: z.string().datetime().optional(),
|
|
60
64
|
});
|
|
61
65
|
export const AnalyzedIssueConversationSchema = z.object({
|
|
62
66
|
url: z.string(),
|
package/dist/core/state.d.ts
CHANGED
|
@@ -26,6 +26,24 @@ export declare function maybeCheckpoint(stateManager: StateManager, callerModule
|
|
|
26
26
|
* Retains lightweight CRUD operations for config, issues, shelving, dismissal,
|
|
27
27
|
* and status overrides.
|
|
28
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* Surfaced when the in-memory cached state is no longer in sync with the
|
|
31
|
+
* canonical Gist — typically because `refreshFromGist()` failed (network
|
|
32
|
+
* blip, rate limit, expired token) or because the bootstrap fell back to
|
|
33
|
+
* the local cache file (#1193). Commands include this in their `--json`
|
|
34
|
+
* envelope so cron/dashboard consumers can react instead of silently
|
|
35
|
+
* operating on stale data.
|
|
36
|
+
*/
|
|
37
|
+
export interface StalenessInfo {
|
|
38
|
+
/** Why we're operating on cached data. Forward-compatible with future sources. */
|
|
39
|
+
source: 'cache';
|
|
40
|
+
/** Human-readable reason from the underlying error. */
|
|
41
|
+
reason: string;
|
|
42
|
+
/** ISO timestamp of the most recent successful refresh, or null if never. */
|
|
43
|
+
lastSuccessfulRefresh: string | null;
|
|
44
|
+
/** ISO timestamp when this staleness marker was first set. */
|
|
45
|
+
detectedAt: string;
|
|
46
|
+
}
|
|
29
47
|
export declare class StateManager {
|
|
30
48
|
protected state: AgentState;
|
|
31
49
|
protected inMemoryOnly: boolean;
|
|
@@ -34,6 +52,8 @@ export declare class StateManager {
|
|
|
34
52
|
private _batchDirty;
|
|
35
53
|
protected gistStore: GistStateStore | null;
|
|
36
54
|
protected gistDegraded: boolean;
|
|
55
|
+
private staleness;
|
|
56
|
+
private lastSuccessfulRefreshAt;
|
|
37
57
|
/**
|
|
38
58
|
* Create a new StateManager instance.
|
|
39
59
|
* @param inMemoryOnly - When true, state is held only in memory and never read from or
|
|
@@ -142,6 +162,13 @@ export declare class StateManager {
|
|
|
142
162
|
* Throttled to once per 30 seconds by GistStateStore. Returns true if state was refreshed.
|
|
143
163
|
*/
|
|
144
164
|
refreshFromGist(): Promise<boolean>;
|
|
165
|
+
/**
|
|
166
|
+
* Returns a staleness marker when the in-memory state diverged from the
|
|
167
|
+
* canonical Gist (refresh failure or degraded bootstrap), or `null` when
|
|
168
|
+
* state is current. Commands surface this via their `--json` warnings
|
|
169
|
+
* envelope (#1193).
|
|
170
|
+
*/
|
|
171
|
+
getStateStaleness(): StalenessInfo | null;
|
|
145
172
|
/**
|
|
146
173
|
* Store the latest daily digest and update the digest timestamp.
|
|
147
174
|
* @param digest - The daily digest to store
|
package/dist/core/state.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Thin coordinator that delegates persistence to state-persistence.ts
|
|
4
4
|
* and scoring logic to repo-score-manager.ts.
|
|
5
5
|
*/
|
|
6
|
-
import * as fs from 'fs';
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
7
|
import { AgentStateSchema } from './state-schema.js';
|
|
8
8
|
import { loadState, saveState, reloadStateIfChanged, createFreshState, atomicWriteFileSync, } from './state-persistence.js';
|
|
9
9
|
import * as repoScoring from './repo-score-manager.js';
|
|
10
10
|
import { debug, warn } from './logger.js';
|
|
11
|
-
import { errorMessage, ConfigurationError, ConcurrencyError } from './errors.js';
|
|
11
|
+
import { errorMessage, ConfigurationError, ConcurrencyError, isTransientNetworkError } from './errors.js';
|
|
12
12
|
import { GistStateStore } from './gist-state-store.js';
|
|
13
13
|
import * as guidelinesStoreModule from './guidelines-store.js';
|
|
14
14
|
import { getStatePath, getStateCachePath } from './paths.js';
|
|
@@ -53,13 +53,6 @@ export async function maybeCheckpoint(stateManager, callerModule) {
|
|
|
53
53
|
warn(callerModule, `Gist checkpoint failed (local mutation succeeded, will retry on next push): ${errorMessage(err)}`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Singleton manager for persistent agent state stored in ~/.oss-autopilot/state.json.
|
|
58
|
-
*
|
|
59
|
-
* Delegates file I/O to state-persistence.ts and scoring logic to repo-score-manager.ts.
|
|
60
|
-
* Retains lightweight CRUD operations for config, issues, shelving, dismissal,
|
|
61
|
-
* and status overrides.
|
|
62
|
-
*/
|
|
63
56
|
export class StateManager {
|
|
64
57
|
state;
|
|
65
58
|
inMemoryOnly;
|
|
@@ -68,6 +61,8 @@ export class StateManager {
|
|
|
68
61
|
_batchDirty = false;
|
|
69
62
|
gistStore = null;
|
|
70
63
|
gistDegraded = false;
|
|
64
|
+
staleness = null;
|
|
65
|
+
lastSuccessfulRefreshAt = null;
|
|
71
66
|
/**
|
|
72
67
|
* Create a new StateManager instance.
|
|
73
68
|
* @param inMemoryOnly - When true, state is held only in memory and never read from or
|
|
@@ -131,6 +126,16 @@ export class StateManager {
|
|
|
131
126
|
manager.gistStore = gistStore;
|
|
132
127
|
manager.gistDegraded = result.degraded ?? false;
|
|
133
128
|
manager.inMemoryOnly = false; // re-enable persistence
|
|
129
|
+
// Seed the staleness marker if bootstrap fell back to the local cache —
|
|
130
|
+
// a `daily` running on a cron right after this start needs to know.
|
|
131
|
+
if (result.degraded) {
|
|
132
|
+
manager.staleness = {
|
|
133
|
+
source: 'cache',
|
|
134
|
+
reason: 'initial Gist bootstrap fell back to local cache',
|
|
135
|
+
lastSuccessfulRefresh: null,
|
|
136
|
+
detectedAt: new Date().toISOString(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
134
139
|
return manager;
|
|
135
140
|
}
|
|
136
141
|
/**
|
|
@@ -340,11 +345,24 @@ export class StateManager {
|
|
|
340
345
|
async refreshFromGist() {
|
|
341
346
|
if (!this.gistStore)
|
|
342
347
|
return false;
|
|
343
|
-
const
|
|
348
|
+
const result = await this.gistStore.refreshFromGist();
|
|
349
|
+
// StateManager keeps its boolean shape (callers rely on truthy-check
|
|
350
|
+
// semantics) but consults the discriminated union internally to know
|
|
351
|
+
// whether to reload state.json from the now-fresh cache (#1209 L9).
|
|
352
|
+
const refreshed = result.status === 'refreshed';
|
|
344
353
|
if (refreshed) {
|
|
345
354
|
const raw = this.gistStore.cachedFiles.get('state.json');
|
|
346
355
|
if (!raw) {
|
|
347
356
|
warn(MODULE, 'Gist refreshed but state.json missing from cache');
|
|
357
|
+
// HTTP fetch succeeded but the Gist body is missing state.json — we
|
|
358
|
+
// still have stale in-memory data, so surface a marker rather than
|
|
359
|
+
// silently returning false (#1193 review).
|
|
360
|
+
this.staleness = {
|
|
361
|
+
source: 'cache',
|
|
362
|
+
reason: 'Gist refresh returned no state.json file',
|
|
363
|
+
lastSuccessfulRefresh: this.lastSuccessfulRefreshAt,
|
|
364
|
+
detectedAt: new Date().toISOString(),
|
|
365
|
+
};
|
|
348
366
|
return false;
|
|
349
367
|
}
|
|
350
368
|
try {
|
|
@@ -353,11 +371,41 @@ export class StateManager {
|
|
|
353
371
|
}
|
|
354
372
|
catch (err) {
|
|
355
373
|
warn(MODULE, `Failed to parse refreshed Gist state: ${errorMessage(err)}`);
|
|
374
|
+
// Same reasoning as the missing-file branch: parse failure leaves us
|
|
375
|
+
// on stale in-memory state, so flag it.
|
|
376
|
+
this.staleness = {
|
|
377
|
+
source: 'cache',
|
|
378
|
+
reason: `Gist refresh succeeded but payload was invalid: ${errorMessage(err)}`,
|
|
379
|
+
lastSuccessfulRefresh: this.lastSuccessfulRefreshAt,
|
|
380
|
+
detectedAt: new Date().toISOString(),
|
|
381
|
+
};
|
|
356
382
|
return false;
|
|
357
383
|
}
|
|
384
|
+
// Successful refresh clears any prior staleness (#1193).
|
|
385
|
+
this.lastSuccessfulRefreshAt = new Date().toISOString();
|
|
386
|
+
this.staleness = null;
|
|
387
|
+
}
|
|
388
|
+
else if (this.gistStore.lastRefreshError) {
|
|
389
|
+
// Distinguish "fetch failed" (set marker) from "throttled" (preserve
|
|
390
|
+
// any existing marker, set nothing new).
|
|
391
|
+
this.staleness = {
|
|
392
|
+
source: 'cache',
|
|
393
|
+
reason: errorMessage(this.gistStore.lastRefreshError),
|
|
394
|
+
lastSuccessfulRefresh: this.lastSuccessfulRefreshAt,
|
|
395
|
+
detectedAt: new Date().toISOString(),
|
|
396
|
+
};
|
|
358
397
|
}
|
|
359
398
|
return refreshed;
|
|
360
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Returns a staleness marker when the in-memory state diverged from the
|
|
402
|
+
* canonical Gist (refresh failure or degraded bootstrap), or `null` when
|
|
403
|
+
* state is current. Commands surface this via their `--json` warnings
|
|
404
|
+
* envelope (#1193).
|
|
405
|
+
*/
|
|
406
|
+
getStateStaleness() {
|
|
407
|
+
return this.staleness;
|
|
408
|
+
}
|
|
361
409
|
// === Dashboard Data Setters ===
|
|
362
410
|
/**
|
|
363
411
|
* Store the latest daily digest and update the digest timestamp.
|
|
@@ -847,11 +895,20 @@ export async function getStateManagerAsync(token) {
|
|
|
847
895
|
})
|
|
848
896
|
.catch((err) => {
|
|
849
897
|
asyncManagerPromise = null;
|
|
850
|
-
// Configuration errors (e.g. GistPermissionError)
|
|
898
|
+
// Configuration errors (e.g. GistPermissionError, GistCorruptError)
|
|
899
|
+
// must surface — falling back to local-only would silently split state
|
|
900
|
+
// across machines (#1202).
|
|
851
901
|
if (err instanceof ConfigurationError)
|
|
852
902
|
throw err;
|
|
853
|
-
|
|
854
|
-
|
|
903
|
+
// Only fall back on actual network/server errors. Other failures
|
|
904
|
+
// (auth, schema, concurrency conflicts) indicate the Gist mode is
|
|
905
|
+
// broken in a way the user needs to address — silently falling back
|
|
906
|
+
// would write subsequent mutations to the local file while the Gist
|
|
907
|
+
// marker stays in config, causing permanent cross-machine divergence.
|
|
908
|
+
if (!isTransientNetworkError(err))
|
|
909
|
+
throw err;
|
|
910
|
+
warn(MODULE, `Gist initialization failed (transient network error), falling back to local-only mode: ${err}`);
|
|
911
|
+
return getStateManager();
|
|
855
912
|
});
|
|
856
913
|
return asyncManagerPromise;
|
|
857
914
|
}
|
|
@@ -880,7 +937,7 @@ export async function ensureGistPersistence(token) {
|
|
|
880
937
|
return;
|
|
881
938
|
let persistence;
|
|
882
939
|
try {
|
|
883
|
-
const raw = fs.readFileSync(getStatePath(), '
|
|
940
|
+
const raw = fs.readFileSync(getStatePath(), 'utf8');
|
|
884
941
|
persistence = JSON.parse(raw)?.config?.persistence;
|
|
885
942
|
}
|
|
886
943
|
catch {
|