@oss-autopilot/core 1.16.2 → 1.17.1
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 +53 -11
- package/dist/cli.bundle.cjs +82 -69
- package/dist/cli.js +22 -10
- package/dist/commands/comments.js +38 -20
- package/dist/commands/config.d.ts +9 -2
- package/dist/commands/config.js +12 -3
- package/dist/commands/daily.d.ts +3 -1
- package/dist/commands/daily.js +126 -37
- package/dist/commands/dashboard-data.d.ts +26 -2
- package/dist/commands/dashboard-data.js +45 -19
- package/dist/commands/dashboard-server.d.ts +1 -1
- package/dist/commands/dashboard-server.js +104 -19
- package/dist/commands/dismiss.js +4 -1
- package/dist/commands/doctor.d.ts +49 -0
- package/dist/commands/doctor.js +358 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/move.d.ts +1 -2
- package/dist/commands/move.js +8 -4
- package/dist/commands/read.js +2 -1
- package/dist/commands/search.d.ts +0 -18
- package/dist/commands/search.js +38 -1
- package/dist/commands/setup.js +42 -2
- package/dist/commands/shelve.js +4 -1
- package/dist/commands/skip-add.js +1 -1
- package/dist/commands/startup.js +14 -4
- package/dist/commands/track.js +2 -1
- package/dist/commands/vet-list.d.ts +23 -2
- package/dist/commands/vet-list.js +57 -10
- package/dist/core/anti-llm-policy.d.ts +5 -0
- package/dist/core/anti-llm-policy.js +5 -0
- package/dist/core/ci-analysis.js +6 -1
- package/dist/core/config-registry.d.ts +44 -0
- package/dist/core/config-registry.js +286 -0
- package/dist/core/dashboard-data-schema.d.ts +78 -0
- package/dist/core/dashboard-data-schema.js +80 -0
- package/dist/core/errors.d.ts +14 -0
- package/dist/core/errors.js +22 -0
- package/dist/core/http-cache.d.ts +8 -1
- package/dist/core/http-cache.js +59 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +3 -1
- package/dist/core/maintainer-analysis.js +9 -3
- package/dist/core/pr-monitor.d.ts +7 -0
- package/dist/core/pr-monitor.js +45 -4
- package/dist/core/repo-score-manager.d.ts +17 -3
- package/dist/core/repo-score-manager.js +48 -19
- package/dist/core/state-persistence.d.ts +14 -1
- package/dist/core/state-persistence.js +24 -2
- package/dist/core/state-schema.d.ts +2 -0
- package/dist/core/state-schema.js +5 -0
- package/dist/core/state.d.ts +26 -2
- package/dist/core/state.js +50 -5
- package/dist/core/status-determination.d.ts +16 -0
- package/dist/core/status-determination.js +44 -11
- package/dist/formatters/json.d.ts +40 -2
- package/dist/formatters/json.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor command — a real system-health audit.
|
|
3
|
+
*
|
|
4
|
+
* Runs independent checks against the local environment and reports an
|
|
5
|
+
* aggregate `DoctorOutput` so users can diagnose common failure modes
|
|
6
|
+
* (missing token, unauthenticated gh CLI, stale bundle, corrupted state,
|
|
7
|
+
* unresolvable scout dependency, low rate-limit budget) in one go.
|
|
8
|
+
*
|
|
9
|
+
* Each `check*` function is exported individually so tests can mock its
|
|
10
|
+
* external dependencies in isolation.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { execFile } from 'child_process';
|
|
15
|
+
import { promisify } from 'util';
|
|
16
|
+
import { getGitHubTokenAsync, getOctokit, getStatePath } from '../core/index.js';
|
|
17
|
+
import { AgentStateSchema } from '../core/state-schema.js';
|
|
18
|
+
import { errorMessage } from '../core/errors.js';
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
const GH_CMD_TIMEOUT_MS = 3000;
|
|
21
|
+
// ── Individual checks ───────────────────────────────────────────────────
|
|
22
|
+
/** Extract a Node errno-style code from an unknown thrown value. */
|
|
23
|
+
function errCode(err) {
|
|
24
|
+
if (err && typeof err === 'object' && 'code' in err) {
|
|
25
|
+
const code = err.code;
|
|
26
|
+
if (typeof code === 'string')
|
|
27
|
+
return code;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
export async function checkGhCli() {
|
|
32
|
+
try {
|
|
33
|
+
const { stdout } = await execFileAsync('gh', ['--version'], { timeout: GH_CMD_TIMEOUT_MS });
|
|
34
|
+
const versionLine = stdout.split('\n')[0]?.trim() || 'gh present';
|
|
35
|
+
return { name: 'gh CLI installed', status: 'ok', message: versionLine };
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
// Distinguish "not on PATH" (ENOENT) from "present but broken" (anything
|
|
39
|
+
// else — spawn errors, timeouts, permission denied, non-zero exit). Telling
|
|
40
|
+
// users to "install gh" when gh is already installed is actively misleading.
|
|
41
|
+
const code = errCode(err);
|
|
42
|
+
if (code === 'ENOENT') {
|
|
43
|
+
return {
|
|
44
|
+
name: 'gh CLI installed',
|
|
45
|
+
status: 'warning',
|
|
46
|
+
message: 'gh CLI not found on PATH',
|
|
47
|
+
remediation: 'Install the GitHub CLI from https://cli.github.com/',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
name: 'gh CLI installed',
|
|
52
|
+
status: 'warning',
|
|
53
|
+
message: `gh CLI present but failed to run: ${errorMessage(err)}`,
|
|
54
|
+
remediation: 'Check that `gh --version` works in your shell; a timeout or permission error may indicate a broken install',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function checkGhAuth() {
|
|
59
|
+
try {
|
|
60
|
+
await execFileAsync('gh', ['auth', 'status'], { timeout: GH_CMD_TIMEOUT_MS });
|
|
61
|
+
return { name: 'gh CLI authenticated', status: 'ok', message: 'Signed in to github.com' };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
// `gh auth status` also exits non-zero on network/proxy failures and TLS
|
|
65
|
+
// issues reaching github.com — not only when the user isn't logged in.
|
|
66
|
+
// Mentioning both paths in the remediation avoids misleading users whose
|
|
67
|
+
// problem is connectivity, not auth.
|
|
68
|
+
return {
|
|
69
|
+
name: 'gh CLI authenticated',
|
|
70
|
+
status: 'warning',
|
|
71
|
+
message: `gh auth status failed: ${errorMessage(err)}`,
|
|
72
|
+
remediation: 'Run `gh auth login` to authenticate, or check network/proxy connectivity if the error looks network-related',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function checkGitHubToken(preloadedToken) {
|
|
77
|
+
const token = preloadedToken !== undefined ? preloadedToken : await getGitHubTokenAsync();
|
|
78
|
+
if (!token) {
|
|
79
|
+
return {
|
|
80
|
+
name: 'GitHub token resolvable',
|
|
81
|
+
status: 'warning',
|
|
82
|
+
message: 'No GitHub token available via $GITHUB_TOKEN or `gh auth token`',
|
|
83
|
+
remediation: 'Set $GITHUB_TOKEN or run `gh auth login`',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const octokit = getOctokit(token);
|
|
88
|
+
const { data } = await octokit.users.getAuthenticated();
|
|
89
|
+
return {
|
|
90
|
+
name: 'GitHub token resolvable',
|
|
91
|
+
status: 'ok',
|
|
92
|
+
message: `Authenticated as @${data.login}`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return {
|
|
97
|
+
name: 'GitHub token resolvable',
|
|
98
|
+
status: 'error',
|
|
99
|
+
message: `Token present but API auth failed: ${errorMessage(err)}`,
|
|
100
|
+
remediation: 'Token may be expired or revoked — run `gh auth refresh` or set a fresh $GITHUB_TOKEN',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Locate `packages/core/dist/cli.bundle.cjs` across the two runtime contexts
|
|
106
|
+
* this code can run in:
|
|
107
|
+
*
|
|
108
|
+
* - Dev (tsx): `__dirname` = `.../packages/core/src/commands`, bundle sits
|
|
109
|
+
* two levels up in `../../dist/cli.bundle.cjs`.
|
|
110
|
+
* - Bundled (cjs): `__dirname` = `.../packages/core/dist`, bundle sits at
|
|
111
|
+
* `./cli.bundle.cjs` in that same directory.
|
|
112
|
+
*/
|
|
113
|
+
function resolveBundlePaths() {
|
|
114
|
+
const devBundle = path.resolve(__dirname, '../../dist/cli.bundle.cjs');
|
|
115
|
+
const coDirBundle = path.resolve(__dirname, 'cli.bundle.cjs');
|
|
116
|
+
const devSource = path.resolve(__dirname, '../cli.ts');
|
|
117
|
+
const coDirSource = path.resolve(__dirname, '../src/cli.ts');
|
|
118
|
+
// Prefer whichever resolves an existing file; fall back to dev paths so the
|
|
119
|
+
// "missing" branches in checkBundleUpToDate still have a sensible path to
|
|
120
|
+
// report.
|
|
121
|
+
const bundlePath = fs.existsSync(devBundle) ? devBundle : fs.existsSync(coDirBundle) ? coDirBundle : devBundle;
|
|
122
|
+
const sourcePath = fs.existsSync(devSource) ? devSource : fs.existsSync(coDirSource) ? coDirSource : devSource;
|
|
123
|
+
return { bundlePath, sourcePath };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Try `fs.statSync`; if the file is merely missing (ENOENT), return null.
|
|
127
|
+
* For other fs errors (EACCES, ELOOP, ENOTDIR, …) surface the error so the
|
|
128
|
+
* caller can report it instead of misdiagnosing it as "file missing."
|
|
129
|
+
*/
|
|
130
|
+
function tryStat(p) {
|
|
131
|
+
try {
|
|
132
|
+
return { stat: fs.statSync(p) };
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
if (errCode(err) === 'ENOENT')
|
|
136
|
+
return { stat: null };
|
|
137
|
+
return { stat: null, error: err };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Bundle-freshness check only fires when the *source* file is present — that's
|
|
142
|
+
* the dev-repo workflow. npm consumers install the pre-built bundle with no
|
|
143
|
+
* source, so we just confirm the bundle exists.
|
|
144
|
+
*/
|
|
145
|
+
export function checkBundleUpToDate(options) {
|
|
146
|
+
const resolved = resolveBundlePaths();
|
|
147
|
+
const bundlePath = options?.bundlePath ?? resolved.bundlePath;
|
|
148
|
+
const sourcePath = options?.sourcePath ?? resolved.sourcePath;
|
|
149
|
+
const bundleResult = tryStat(bundlePath);
|
|
150
|
+
const sourceResult = tryStat(sourcePath);
|
|
151
|
+
if (bundleResult.error || sourceResult.error) {
|
|
152
|
+
const err = bundleResult.error ?? sourceResult.error;
|
|
153
|
+
return {
|
|
154
|
+
name: 'CLI bundle',
|
|
155
|
+
status: 'error',
|
|
156
|
+
message: `Bundle freshness check failed: ${errorMessage(err)}`,
|
|
157
|
+
remediation: 'Check filesystem permissions on packages/core/dist/',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const bundleStat = bundleResult.stat;
|
|
161
|
+
const sourceStat = sourceResult.stat;
|
|
162
|
+
if (!bundleStat && !sourceStat) {
|
|
163
|
+
return {
|
|
164
|
+
name: 'CLI bundle',
|
|
165
|
+
status: 'warning',
|
|
166
|
+
message: 'Neither cli.ts nor cli.bundle.cjs could be located',
|
|
167
|
+
remediation: 'Reinstall the package or check your install location',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (!bundleStat) {
|
|
171
|
+
return {
|
|
172
|
+
name: 'CLI bundle',
|
|
173
|
+
status: 'warning',
|
|
174
|
+
message: 'dist/cli.bundle.cjs not built yet',
|
|
175
|
+
remediation: 'Run `pnpm run bundle` to build the CLI bundle',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (sourceStat && sourceStat.mtimeMs > bundleStat.mtimeMs) {
|
|
179
|
+
return {
|
|
180
|
+
name: 'CLI bundle',
|
|
181
|
+
status: 'warning',
|
|
182
|
+
message: 'Source files are newer than dist/cli.bundle.cjs',
|
|
183
|
+
remediation: 'Run `pnpm run bundle` to rebuild',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return { name: 'CLI bundle', status: 'ok', message: 'Bundle present and up-to-date' };
|
|
187
|
+
}
|
|
188
|
+
export function checkStateFile(options) {
|
|
189
|
+
const statePath = options?.statePathOverride ?? getStatePath();
|
|
190
|
+
if (!fs.existsSync(statePath)) {
|
|
191
|
+
return {
|
|
192
|
+
name: 'state.json valid',
|
|
193
|
+
status: 'ok',
|
|
194
|
+
message: 'No state.json yet (first run — nothing to validate)',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const raw = fs.readFileSync(statePath, 'utf-8');
|
|
199
|
+
const parsed = JSON.parse(raw);
|
|
200
|
+
const result = AgentStateSchema.safeParse(parsed);
|
|
201
|
+
if (!result.success) {
|
|
202
|
+
const snippet = result.error.issues
|
|
203
|
+
.slice(0, 3)
|
|
204
|
+
.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`)
|
|
205
|
+
.join('; ');
|
|
206
|
+
return {
|
|
207
|
+
name: 'state.json valid',
|
|
208
|
+
status: 'error',
|
|
209
|
+
message: `state.json failed schema validation — ${snippet}`,
|
|
210
|
+
remediation: 'Restore from a backup in `~/.oss-autopilot/backups/` or re-run `oss-autopilot init`',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const version = typeof parsed?.version === 'number' ? parsed.version : '?';
|
|
214
|
+
return { name: 'state.json valid', status: 'ok', message: `state v${version} parses and validates` };
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
return {
|
|
218
|
+
name: 'state.json valid',
|
|
219
|
+
status: 'error',
|
|
220
|
+
message: `Failed to read/parse state.json: ${errorMessage(err)}`,
|
|
221
|
+
remediation: 'Restore from a backup in `~/.oss-autopilot/backups/`',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const defaultScoutImporter = () => import('@oss-scout/core');
|
|
226
|
+
export async function checkScoutResolvable(importer = defaultScoutImporter) {
|
|
227
|
+
// The scout package is ESM-exports-only, so `require.resolve` fails even
|
|
228
|
+
// when it's installed. `import()` goes through the ESM resolver, which
|
|
229
|
+
// honors `exports` maps correctly. Dynamic import only loads the package
|
|
230
|
+
// on demand (not at doctor module load) and is cheap — no auth side effects.
|
|
231
|
+
try {
|
|
232
|
+
await importer();
|
|
233
|
+
return { name: '@oss-scout/core resolvable', status: 'ok', message: 'Found on module path' };
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
// Three outcomes we want to distinguish:
|
|
237
|
+
// - "Package simply isn't installed" (missing from node_modules)
|
|
238
|
+
// - "Package is present but the install is corrupt" (bad exports map,
|
|
239
|
+
// unparseable source, missing internal file) — reinstall is the fix
|
|
240
|
+
// - "Package loaded but threw during init" — this is a scout bug
|
|
241
|
+
const code = errCode(err);
|
|
242
|
+
const isNotFound = code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND';
|
|
243
|
+
const isCorruptInstall = code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' ||
|
|
244
|
+
code === 'ERR_PACKAGE_IMPORT_NOT_DEFINED' ||
|
|
245
|
+
code === 'ERR_INVALID_PACKAGE_CONFIG' ||
|
|
246
|
+
err instanceof SyntaxError;
|
|
247
|
+
const reinstallRequired = isNotFound || isCorruptInstall;
|
|
248
|
+
return {
|
|
249
|
+
name: '@oss-scout/core resolvable',
|
|
250
|
+
status: 'error',
|
|
251
|
+
message: reinstallRequired
|
|
252
|
+
? `@oss-scout/core cannot be loaded (install issue): ${errorMessage(err)}`
|
|
253
|
+
: `@oss-scout/core failed to load: ${errorMessage(err)}`,
|
|
254
|
+
remediation: reinstallRequired
|
|
255
|
+
? 'Reinstall dependencies (`pnpm install` or `npm install`)'
|
|
256
|
+
: 'Scout loaded but threw during initialization — report this as a bug in oss-scout with the error above',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export async function checkGitHubRateLimit(preloadedToken) {
|
|
261
|
+
const token = preloadedToken !== undefined ? preloadedToken : await getGitHubTokenAsync();
|
|
262
|
+
if (!token) {
|
|
263
|
+
return {
|
|
264
|
+
name: 'GitHub rate limit',
|
|
265
|
+
status: 'warning',
|
|
266
|
+
message: 'Cannot check — no token available',
|
|
267
|
+
remediation: 'Set $GITHUB_TOKEN or run `gh auth login` to enable API-backed checks',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const octokit = getOctokit(token);
|
|
272
|
+
const { data } = await octokit.rateLimit.get();
|
|
273
|
+
const core = data.resources.core;
|
|
274
|
+
const resetAt = new Date(core.reset * 1000).toISOString();
|
|
275
|
+
if (core.remaining < 100) {
|
|
276
|
+
return {
|
|
277
|
+
name: 'GitHub rate limit',
|
|
278
|
+
status: 'warning',
|
|
279
|
+
message: `Only ${core.remaining}/${core.limit} core requests remaining (resets at ${resetAt})`,
|
|
280
|
+
remediation: 'Wait for the window to reset or use a token with a higher limit',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
name: 'GitHub rate limit',
|
|
285
|
+
status: 'ok',
|
|
286
|
+
message: `${core.remaining}/${core.limit} core requests remaining`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
return {
|
|
291
|
+
name: 'GitHub rate limit',
|
|
292
|
+
status: 'error',
|
|
293
|
+
message: `Failed to check rate limit: ${errorMessage(err)}`,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// ── Orchestrator ────────────────────────────────────────────────────────
|
|
298
|
+
/**
|
|
299
|
+
* Run all diagnostic checks and return a structured report.
|
|
300
|
+
*
|
|
301
|
+
* Checks are run concurrently where possible. None of the checks throw — each
|
|
302
|
+
* returns a `DoctorCheck` with its own status so a single failing check does
|
|
303
|
+
* not abort the rest of the audit.
|
|
304
|
+
*/
|
|
305
|
+
/**
|
|
306
|
+
* Run a check defensively. If it rejects or the run itself throws, convert the
|
|
307
|
+
* failure into a synthetic `DoctorCheck` with status 'error' — `doctor` is the
|
|
308
|
+
* tool users reach for when things are already broken, so it must never abort
|
|
309
|
+
* itself because one individual check crashed.
|
|
310
|
+
*/
|
|
311
|
+
async function runCheckSafely(name, runner) {
|
|
312
|
+
try {
|
|
313
|
+
return await runner();
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
return {
|
|
317
|
+
name,
|
|
318
|
+
status: 'error',
|
|
319
|
+
message: `Check crashed unexpectedly: ${errorMessage(err)}`,
|
|
320
|
+
remediation: 'This is a bug in the doctor command — please report it with the error above',
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
export async function runDoctor() {
|
|
325
|
+
// Pre-resolve the token once and pass it to the token-dependent checks.
|
|
326
|
+
// Without this, the first call enters the async `gh auth token` branch
|
|
327
|
+
// (setting `tokenFetchAttempted = true` synchronously, then yielding at the
|
|
328
|
+
// `execFile` await). A second concurrent caller sees the flag set and the
|
|
329
|
+
// cache still empty, and returns `null` immediately — producing a spurious
|
|
330
|
+
// "no token available" warning while the first caller later resolves a valid
|
|
331
|
+
// token. Empirically reproduced before this fix.
|
|
332
|
+
// Best-effort token fetch — if it throws (sync or async), downstream
|
|
333
|
+
// token-dependent checks still run and will report "no token available" on
|
|
334
|
+
// their own. `try/catch` covers the sync-throw case; `.catch` alone would
|
|
335
|
+
// not.
|
|
336
|
+
let token = null;
|
|
337
|
+
try {
|
|
338
|
+
token = await getGitHubTokenAsync();
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
token = null;
|
|
342
|
+
}
|
|
343
|
+
const checks = await Promise.all([
|
|
344
|
+
runCheckSafely('gh CLI installed', () => checkGhCli()),
|
|
345
|
+
runCheckSafely('gh CLI authenticated', () => checkGhAuth()),
|
|
346
|
+
runCheckSafely('GitHub token resolvable', () => checkGitHubToken(token)),
|
|
347
|
+
runCheckSafely('GitHub rate limit', () => checkGitHubRateLimit(token)),
|
|
348
|
+
runCheckSafely('@oss-scout/core resolvable', () => checkScoutResolvable()),
|
|
349
|
+
runCheckSafely('CLI bundle', () => checkBundleUpToDate()),
|
|
350
|
+
runCheckSafely('state.json valid', () => checkStateFile()),
|
|
351
|
+
]);
|
|
352
|
+
const summary = {
|
|
353
|
+
ok: checks.filter((c) => c.status === 'ok').length,
|
|
354
|
+
warnings: checks.filter((c) => c.status === 'warning').length,
|
|
355
|
+
errors: checks.filter((c) => c.status === 'error').length,
|
|
356
|
+
};
|
|
357
|
+
return { checks, summary };
|
|
358
|
+
}
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export { runStateUnlink } from './state-cmd.js';
|
|
|
67
67
|
export { runParseList, pruneIssueList } from './parse-list.js';
|
|
68
68
|
/** Check if new files are properly referenced/integrated. */
|
|
69
69
|
export { runCheckIntegration } from './check-integration.js';
|
|
70
|
+
/** System-health diagnostic — verifies tokens, bundle, state, scout, rate limit. */
|
|
71
|
+
export { runDoctor, type DoctorCheck, type DoctorCheckStatus, type DoctorOutput } from './doctor.js';
|
|
70
72
|
/** Detect formatters/linters configured in a local repository (#703). */
|
|
71
73
|
export { runDetectFormatters } from './detect-formatters.js';
|
|
72
74
|
/** Scan for locally cloned repos. */
|
package/dist/commands/index.js
CHANGED
|
@@ -73,6 +73,8 @@ export { runStateUnlink } from './state-cmd.js';
|
|
|
73
73
|
export { runParseList, pruneIssueList } from './parse-list.js';
|
|
74
74
|
/** Check if new files are properly referenced/integrated. */
|
|
75
75
|
export { runCheckIntegration } from './check-integration.js';
|
|
76
|
+
/** System-health diagnostic — verifies tokens, bundle, state, scout, rate limit. */
|
|
77
|
+
export { runDoctor } from './doctor.js';
|
|
76
78
|
/** Detect formatters/linters configured in a local repository (#703). */
|
|
77
79
|
export { runDetectFormatters } from './detect-formatters.js';
|
|
78
80
|
/** Scan for locally cloned repos. */
|
package/dist/commands/move.d.ts
CHANGED
|
@@ -17,8 +17,7 @@ export interface MoveOutput {
|
|
|
17
17
|
* @param options.prUrl - Full GitHub PR URL
|
|
18
18
|
* @param options.target - Target state: 'attention' | 'waiting' | 'shelved' | 'auto'
|
|
19
19
|
* @returns The move result with a human-readable description
|
|
20
|
-
* @throws {ValidationError} If the URL is not a valid GitHub PR URL
|
|
21
|
-
* @throws {Error} If the target is invalid
|
|
20
|
+
* @throws {ValidationError} If the URL is not a valid GitHub PR URL or the target is invalid
|
|
22
21
|
*/
|
|
23
22
|
export declare function runMove(options: {
|
|
24
23
|
prUrl: string;
|
package/dist/commands/move.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
* Move command — transition a PR between states:
|
|
3
3
|
* attention, waiting, shelved, or auto (reset to computed status).
|
|
4
4
|
*/
|
|
5
|
-
import { getStateManager } from '../core/index.js';
|
|
5
|
+
import { getStateManager, maybeCheckpoint } from '../core/index.js';
|
|
6
|
+
import { ValidationError } from '../core/errors.js';
|
|
6
7
|
import { PR_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
|
|
8
|
+
const MODULE = 'move';
|
|
7
9
|
export const VALID_TARGETS = ['attention', 'waiting', 'shelved', 'auto'];
|
|
8
10
|
/**
|
|
9
11
|
* Move a PR between states: attention, waiting, shelved, or auto (computed).
|
|
@@ -12,15 +14,14 @@ export const VALID_TARGETS = ['attention', 'waiting', 'shelved', 'auto'];
|
|
|
12
14
|
* @param options.prUrl - Full GitHub PR URL
|
|
13
15
|
* @param options.target - Target state: 'attention' | 'waiting' | 'shelved' | 'auto'
|
|
14
16
|
* @returns The move result with a human-readable description
|
|
15
|
-
* @throws {ValidationError} If the URL is not a valid GitHub PR URL
|
|
16
|
-
* @throws {Error} If the target is invalid
|
|
17
|
+
* @throws {ValidationError} If the URL is not a valid GitHub PR URL or the target is invalid
|
|
17
18
|
*/
|
|
18
19
|
export async function runMove(options) {
|
|
19
20
|
validateUrl(options.prUrl);
|
|
20
21
|
validateGitHubUrl(options.prUrl, PR_URL_PATTERN, 'PR');
|
|
21
22
|
const target = options.target;
|
|
22
23
|
if (!VALID_TARGETS.includes(target)) {
|
|
23
|
-
throw new
|
|
24
|
+
throw new ValidationError(`Invalid target "${options.target}". Must be one of: ${VALID_TARGETS.join(', ')}`);
|
|
24
25
|
}
|
|
25
26
|
const stateManager = getStateManager();
|
|
26
27
|
switch (target) {
|
|
@@ -35,6 +36,7 @@ export async function runMove(options) {
|
|
|
35
36
|
stateManager.setStatusOverride(options.prUrl, status, lastActivityAt);
|
|
36
37
|
stateManager.unshelvePR(options.prUrl);
|
|
37
38
|
});
|
|
39
|
+
await maybeCheckpoint(stateManager, MODULE);
|
|
38
40
|
return { url: options.prUrl, target, description: `Moved to ${label}` };
|
|
39
41
|
}
|
|
40
42
|
case 'shelved': {
|
|
@@ -42,6 +44,7 @@ export async function runMove(options) {
|
|
|
42
44
|
stateManager.shelvePR(options.prUrl);
|
|
43
45
|
stateManager.clearStatusOverride(options.prUrl);
|
|
44
46
|
});
|
|
47
|
+
await maybeCheckpoint(stateManager, MODULE);
|
|
45
48
|
return {
|
|
46
49
|
url: options.prUrl,
|
|
47
50
|
target,
|
|
@@ -53,6 +56,7 @@ export async function runMove(options) {
|
|
|
53
56
|
stateManager.clearStatusOverride(options.prUrl);
|
|
54
57
|
stateManager.unshelvePR(options.prUrl);
|
|
55
58
|
});
|
|
59
|
+
await maybeCheckpoint(stateManager, MODULE);
|
|
56
60
|
return {
|
|
57
61
|
url: options.prUrl,
|
|
58
62
|
target,
|
package/dist/commands/read.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* In v2, PR read/unread state is not tracked locally.
|
|
4
4
|
* This command is a no-op preserved for backward compatibility.
|
|
5
5
|
*/
|
|
6
|
+
import { ValidationError } from '../core/errors.js';
|
|
6
7
|
import { validateUrl } from './validation.js';
|
|
7
8
|
export async function runRead(options) {
|
|
8
9
|
if (!options.all && !options.prUrl) {
|
|
9
|
-
throw new
|
|
10
|
+
throw new ValidationError('PR URL or --all flag required');
|
|
10
11
|
}
|
|
11
12
|
if (options.prUrl) {
|
|
12
13
|
validateUrl(options.prUrl);
|
|
@@ -13,22 +13,4 @@ export declare const MAX_SEARCH_RESULTS = 100;
|
|
|
13
13
|
interface SearchOptions {
|
|
14
14
|
maxResults: number;
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Search GitHub for contributable issues using multi-phase discovery.
|
|
18
|
-
*
|
|
19
|
-
* @param options - Search configuration
|
|
20
|
-
* @param options.maxResults - Maximum number of candidates to return
|
|
21
|
-
* @returns Search results with scored candidates and exclusion lists
|
|
22
|
-
* @throws {ConfigurationError} If no GitHub token is available
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```typescript
|
|
26
|
-
* import { runSearch } from '@oss-autopilot/core/commands';
|
|
27
|
-
*
|
|
28
|
-
* const results = await runSearch({ maxResults: 10 });
|
|
29
|
-
* for (const c of results.candidates) {
|
|
30
|
-
* console.log(`${c.issue.repo}#${c.issue.number} — ${c.viabilityScore}/100`);
|
|
31
|
-
* }
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
34
16
|
export declare function runSearch(options: SearchOptions): Promise<SearchOutput>;
|
package/dist/commands/search.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { createAutopilotScout } from './scout-bridge.js';
|
|
6
6
|
import { getStateManager } from '../core/index.js';
|
|
7
|
+
import { gradeFromCandidate } from '../core/issue-grading.js';
|
|
8
|
+
import { warn } from '../core/logger.js';
|
|
9
|
+
const MODULE = 'search';
|
|
7
10
|
/**
|
|
8
11
|
* Hard cap on issue-search result count. Shared between CLI (`cli-registry.ts`),
|
|
9
12
|
* MCP tool (`tools.ts`), and MCP prompt (`prompts.ts`) so a future adjustment
|
|
@@ -28,6 +31,19 @@ export const MAX_SEARCH_RESULTS = 100;
|
|
|
28
31
|
* }
|
|
29
32
|
* ```
|
|
30
33
|
*/
|
|
34
|
+
/**
|
|
35
|
+
* Coerce scout's raw `viabilityScore` into a trustworthy 0–100 number.
|
|
36
|
+
* Scout is supposed to emit a finite integer in range, but out-of-contract
|
|
37
|
+
* values (NaN, Infinity, negative, >100, non-number) would otherwise flow
|
|
38
|
+
* straight through to consumers. Defend at the boundary — see #1043.
|
|
39
|
+
*/
|
|
40
|
+
function sanitizeViabilityScore(raw) {
|
|
41
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0 || raw > 100) {
|
|
42
|
+
warn(MODULE, `Ignoring out-of-contract viabilityScore from scout: ${JSON.stringify(raw)}`);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
return raw;
|
|
46
|
+
}
|
|
31
47
|
export async function runSearch(options) {
|
|
32
48
|
const scout = await createAutopilotScout();
|
|
33
49
|
const result = await scout.search({ maxResults: options.maxResults });
|
|
@@ -35,6 +51,26 @@ export async function runSearch(options) {
|
|
|
35
51
|
const searchOutput = {
|
|
36
52
|
candidates: result.candidates.map((c) => {
|
|
37
53
|
const repoScoreRecord = stateManager.getRepoScore(c.issue.repo);
|
|
54
|
+
// Scout's `search` does not emit per-candidate projectHealth (only
|
|
55
|
+
// `vetIssue` does). Pass a sentinel `checkFailed: true` so the grader
|
|
56
|
+
// correctly treats scout-side signals as unknown and grades purely from
|
|
57
|
+
// the autopilot-tracked repoScore. Candidates without a repoScore
|
|
58
|
+
// receive 'F' — that's an honest signal for "we haven't seen this repo
|
|
59
|
+
// before" rather than a fabricated score.
|
|
60
|
+
const grade = gradeFromCandidate({
|
|
61
|
+
repo: c.issue.repo,
|
|
62
|
+
projectHealth: { avgIssueResponseDays: null, daysSinceLastCommit: null, checkFailed: true },
|
|
63
|
+
getRepoScore: (repo) => {
|
|
64
|
+
const score = stateManager.getRepoScore(repo);
|
|
65
|
+
return score
|
|
66
|
+
? {
|
|
67
|
+
mergedPRCount: score.mergedPRCount,
|
|
68
|
+
closedWithoutMergeCount: score.closedWithoutMergeCount,
|
|
69
|
+
avgResponseDays: score.avgResponseDays ?? null,
|
|
70
|
+
}
|
|
71
|
+
: undefined;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
38
74
|
return {
|
|
39
75
|
issue: {
|
|
40
76
|
repo: c.issue.repo,
|
|
@@ -48,7 +84,8 @@ export async function runSearch(options) {
|
|
|
48
84
|
reasonsToApprove: c.reasonsToApprove,
|
|
49
85
|
reasonsToSkip: c.reasonsToSkip,
|
|
50
86
|
searchPriority: c.searchPriority,
|
|
51
|
-
viabilityScore: c.viabilityScore,
|
|
87
|
+
viabilityScore: sanitizeViabilityScore(c.viabilityScore),
|
|
88
|
+
grade,
|
|
52
89
|
repoScore: repoScoreRecord
|
|
53
90
|
? {
|
|
54
91
|
score: repoScoreRecord.score,
|
package/dist/commands/setup.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Setup command
|
|
3
3
|
* Interactive setup / configuration
|
|
4
4
|
*/
|
|
5
|
-
import { getStateManager, DEFAULT_CONFIG } from '../core/index.js';
|
|
5
|
+
import { getStateManager, DEFAULT_CONFIG, formatUnknownKeyError, getSetupKeys } from '../core/index.js';
|
|
6
6
|
import { ValidationError } from '../core/errors.js';
|
|
7
7
|
import { validateGitHubUsername } from './validation.js';
|
|
8
8
|
import { PROJECT_CATEGORIES, ISSUE_SCOPES, DIFF_TOOLS, } from '../core/types.js';
|
|
@@ -34,6 +34,18 @@ export async function runSetup(options) {
|
|
|
34
34
|
if (options.set && options.set.length > 0) {
|
|
35
35
|
const results = {};
|
|
36
36
|
const warnings = [];
|
|
37
|
+
// Pre-validate every key before mutating state. `stateManager.batch()` only
|
|
38
|
+
// defers the disk write — it does not snapshot in-memory state, so throwing
|
|
39
|
+
// mid-loop would leave earlier successful updates applied in memory (a real
|
|
40
|
+
// issue for long-running consumers like the MCP server and dashboard that
|
|
41
|
+
// share the StateManager singleton across requests).
|
|
42
|
+
const knownKeys = new Set(getSetupKeys());
|
|
43
|
+
for (const setting of options.set) {
|
|
44
|
+
const [key] = setting.split('=');
|
|
45
|
+
if (!knownKeys.has(key)) {
|
|
46
|
+
throw new ValidationError(formatUnknownKeyError(key, 'setup'));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
37
49
|
stateManager.batch(() => {
|
|
38
50
|
for (const setting of options.set) {
|
|
39
51
|
const [key, ...valueParts] = setting.split('=');
|
|
@@ -89,6 +101,34 @@ export async function runSetup(options) {
|
|
|
89
101
|
results[key] = String(stars);
|
|
90
102
|
break;
|
|
91
103
|
}
|
|
104
|
+
case 'maxIssueAgeDays': {
|
|
105
|
+
const days = parsePositiveInt(value, 'maxIssueAgeDays');
|
|
106
|
+
stateManager.updateConfig({ maxIssueAgeDays: days });
|
|
107
|
+
results[key] = String(days);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case 'minRepoScoreThreshold': {
|
|
111
|
+
const threshold = Number(value);
|
|
112
|
+
if (!Number.isInteger(threshold) || threshold < 0) {
|
|
113
|
+
throw new ValidationError(`Invalid value for minRepoScoreThreshold: "${value}". Must be a non-negative integer.`);
|
|
114
|
+
}
|
|
115
|
+
stateManager.updateConfig({ minRepoScoreThreshold: threshold });
|
|
116
|
+
results[key] = String(threshold);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'skippedIssuesPath':
|
|
120
|
+
stateManager.updateConfig({ skippedIssuesPath: value || undefined });
|
|
121
|
+
results[key] = value || '(cleared)';
|
|
122
|
+
break;
|
|
123
|
+
case 'autoFormatBeforePush': {
|
|
124
|
+
if (value !== 'true' && value !== 'false') {
|
|
125
|
+
throw new ValidationError(`Invalid value for autoFormatBeforePush: "${value}". Must be "true" or "false".`);
|
|
126
|
+
}
|
|
127
|
+
const enabled = value === 'true';
|
|
128
|
+
stateManager.updateConfig({ autoFormatBeforePush: enabled });
|
|
129
|
+
results[key] = String(enabled);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
92
132
|
case 'includeDocIssues':
|
|
93
133
|
stateManager.updateConfig({ includeDocIssues: value === 'true' });
|
|
94
134
|
results[key] = value === 'true' ? 'true' : 'false';
|
|
@@ -221,7 +261,7 @@ export async function runSetup(options) {
|
|
|
221
261
|
}
|
|
222
262
|
break;
|
|
223
263
|
default:
|
|
224
|
-
|
|
264
|
+
throw new ValidationError(formatUnknownKeyError(key, 'setup'));
|
|
225
265
|
}
|
|
226
266
|
}
|
|
227
267
|
});
|
package/dist/commands/shelve.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* which also clears status overrides. These functions match that behavior
|
|
8
8
|
* to keep the library API consistent.
|
|
9
9
|
*/
|
|
10
|
-
import { getStateManager } from '../core/index.js';
|
|
10
|
+
import { getStateManager, maybeCheckpoint } from '../core/index.js';
|
|
11
11
|
import { PR_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
|
|
12
|
+
const MODULE = 'shelve';
|
|
12
13
|
// Re-export for backward compatibility with tests
|
|
13
14
|
export { PR_URL_PATTERN };
|
|
14
15
|
/**
|
|
@@ -28,6 +29,7 @@ export async function runShelve(options) {
|
|
|
28
29
|
added = stateManager.shelvePR(options.prUrl);
|
|
29
30
|
stateManager.clearStatusOverride(options.prUrl);
|
|
30
31
|
});
|
|
32
|
+
await maybeCheckpoint(stateManager, MODULE);
|
|
31
33
|
return { shelved: added, url: options.prUrl };
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
@@ -47,5 +49,6 @@ export async function runUnshelve(options) {
|
|
|
47
49
|
removed = stateManager.unshelvePR(options.prUrl);
|
|
48
50
|
stateManager.clearStatusOverride(options.prUrl);
|
|
49
51
|
});
|
|
52
|
+
await maybeCheckpoint(stateManager, MODULE);
|
|
50
53
|
return { unshelved: removed, url: options.prUrl };
|
|
51
54
|
}
|
|
@@ -19,7 +19,7 @@ function formatUtcDate(d) {
|
|
|
19
19
|
export function runSkipAdd(options) {
|
|
20
20
|
const skipFilePath = options.skipFilePath ?? getStateManager().getState().config.skippedIssuesPath;
|
|
21
21
|
if (!skipFilePath) {
|
|
22
|
-
throw new Error('No skipped-issues path configured. Set one via `oss-autopilot
|
|
22
|
+
throw new Error('No skipped-issues path configured. Set one via `oss-autopilot setup --set skippedIssuesPath=<path>` or pass --path.');
|
|
23
23
|
}
|
|
24
24
|
if (!GITHUB_URL_RE.test(options.issueUrl)) {
|
|
25
25
|
throw new Error(`Invalid GitHub issue or PR URL: ${options.issueUrl}`);
|