@oss-autopilot/core 1.16.2 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-registry.js +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 +7 -3
- 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 +16 -3
- 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
package/dist/commands/startup.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
|
-
import { getStateManager,
|
|
12
|
+
import { getStateManager, getGitHubTokenAsync, getCLIVersion, detectGitHubUsername } from '../core/index.js';
|
|
13
13
|
import { errorMessage } from '../core/errors.js';
|
|
14
14
|
import { warn } from '../core/logger.js';
|
|
15
15
|
import { executeDailyCheck } from './daily.js';
|
|
@@ -207,8 +207,12 @@ export async function runStartup() {
|
|
|
207
207
|
return { version, setupComplete: false };
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
-
// 2. Check auth
|
|
211
|
-
|
|
210
|
+
// 2. Check auth — use the async variant so the `gh auth token` CLI fallback
|
|
211
|
+
// fires for users who ran `gh auth login` but never exported $GITHUB_TOKEN.
|
|
212
|
+
// The sync `getGitHubToken()` reads only the env var, matching the `preAction`
|
|
213
|
+
// token check that the CLI's `localOnly: true` flag on `startup` deliberately
|
|
214
|
+
// skips — the mismatch produced a spurious `authError` for valid users.
|
|
215
|
+
const token = await getGitHubTokenAsync();
|
|
212
216
|
if (!token) {
|
|
213
217
|
return {
|
|
214
218
|
version,
|
package/dist/commands/track.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* a PR from the daily digest.
|
|
14
14
|
*/
|
|
15
15
|
import { getOctokit, requireGitHubToken } from '../core/index.js';
|
|
16
|
+
import { ValidationError } from '../core/errors.js';
|
|
16
17
|
import { validateUrl, PR_URL_PATTERN, validateGitHubUrl } from './validation.js';
|
|
17
18
|
import { parseGitHubUrl } from '../core/utils.js';
|
|
18
19
|
/**
|
|
@@ -34,7 +35,7 @@ export async function runTrack(options) {
|
|
|
34
35
|
const octokit = getOctokit(token);
|
|
35
36
|
const parsed = parseGitHubUrl(options.prUrl);
|
|
36
37
|
if (!parsed || parsed.type !== 'pull') {
|
|
37
|
-
throw new
|
|
38
|
+
throw new ValidationError(`Invalid PR URL: ${options.prUrl}`);
|
|
38
39
|
}
|
|
39
40
|
const { owner, repo, number } = parsed;
|
|
40
41
|
const { data: ghPR } = await octokit.pulls.get({ owner, repo, pull_number: number });
|
|
@@ -8,11 +8,32 @@ interface VetListOptions {
|
|
|
8
8
|
concurrency?: number;
|
|
9
9
|
prune?: boolean;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Scout-side enumerated skip reasons (#1043). If scout emits a `skipReason`
|
|
13
|
+
* field on its vet candidate, we route on that directly rather than doing
|
|
14
|
+
* fragile substring matches against free-text. The strings scout uses today
|
|
15
|
+
* (e.g. "Issue is closed", "Issue is already claimed") would silently stop
|
|
16
|
+
* matching on a rewording — the enum makes the data contract explicit.
|
|
17
|
+
*
|
|
18
|
+
* Scout has not yet landed the enum emitter; this PR lands the reader so the
|
|
19
|
+
* switchover is drop-in when scout ships it. The substring fallback below
|
|
20
|
+
* covers the transition period.
|
|
21
|
+
*/
|
|
22
|
+
export type ScoutSkipReason = 'issue_closed' | 'has_linked_pr' | 'claimed' | 'score_too_low' | 'anti_llm_policy' | 'other';
|
|
23
|
+
/**
|
|
24
|
+
* Defensively extract a `skipReason` from a scout candidate. Scout's types
|
|
25
|
+
* don't expose it yet, and we want to ignore any value scout emits that
|
|
26
|
+
* isn't one of the expected enum members (forward-compat + poison guard).
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractSkipReason(candidate: unknown): ScoutSkipReason | undefined;
|
|
11
29
|
/**
|
|
12
30
|
* Determine the list status from vetting results.
|
|
13
|
-
*
|
|
31
|
+
*
|
|
32
|
+
* Prefers scout's structured `skipReason` enum when present; falls back to
|
|
33
|
+
* substring matching on the free-text `reasonsToSkip` for the transition
|
|
34
|
+
* period before scout emits the enum. See #1043.
|
|
14
35
|
*/
|
|
15
|
-
export declare function classifyListStatus(vetResult: VetOutput): VetListItemStatus;
|
|
36
|
+
export declare function classifyListStatus(vetResult: VetOutput, skipReason?: ScoutSkipReason): VetListItemStatus;
|
|
16
37
|
/**
|
|
17
38
|
* Re-vet all available issues in a curated issue list.
|
|
18
39
|
* Reads the list file, extracts available (non-done) issues,
|
|
@@ -9,18 +9,65 @@ import { detectIssueList } from './startup.js';
|
|
|
9
9
|
import { computeSuccessGrade, gradeFromCandidate } from '../core/issue-grading.js';
|
|
10
10
|
import { getStateManager } from '../core/index.js';
|
|
11
11
|
const UNKNOWN_GRADE = computeSuccessGrade({ avgResponseDays: null, mergeRate: null, daysSinceLastCommit: null });
|
|
12
|
+
const KNOWN_SKIP_REASONS = new Set([
|
|
13
|
+
'issue_closed',
|
|
14
|
+
'has_linked_pr',
|
|
15
|
+
'claimed',
|
|
16
|
+
'score_too_low',
|
|
17
|
+
'anti_llm_policy',
|
|
18
|
+
'other',
|
|
19
|
+
]);
|
|
20
|
+
function mapSkipReasonToStatus(reason) {
|
|
21
|
+
switch (reason) {
|
|
22
|
+
case 'issue_closed':
|
|
23
|
+
return 'closed';
|
|
24
|
+
case 'claimed':
|
|
25
|
+
return 'claimed';
|
|
26
|
+
case 'has_linked_pr':
|
|
27
|
+
return 'has_pr';
|
|
28
|
+
case 'score_too_low':
|
|
29
|
+
case 'anti_llm_policy':
|
|
30
|
+
case 'other':
|
|
31
|
+
return null; // fall through to recommendation / default
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Defensively extract a `skipReason` from a scout candidate. Scout's types
|
|
36
|
+
* don't expose it yet, and we want to ignore any value scout emits that
|
|
37
|
+
* isn't one of the expected enum members (forward-compat + poison guard).
|
|
38
|
+
*/
|
|
39
|
+
export function extractSkipReason(candidate) {
|
|
40
|
+
if (typeof candidate !== 'object' || candidate === null)
|
|
41
|
+
return undefined;
|
|
42
|
+
const raw = candidate.skipReason;
|
|
43
|
+
if (typeof raw !== 'string')
|
|
44
|
+
return undefined;
|
|
45
|
+
return KNOWN_SKIP_REASONS.has(raw) ? raw : undefined;
|
|
46
|
+
}
|
|
12
47
|
/**
|
|
13
48
|
* Determine the list status from vetting results.
|
|
14
|
-
*
|
|
49
|
+
*
|
|
50
|
+
* Prefers scout's structured `skipReason` enum when present; falls back to
|
|
51
|
+
* substring matching on the free-text `reasonsToSkip` for the transition
|
|
52
|
+
* period before scout emits the enum. See #1043.
|
|
15
53
|
*/
|
|
16
|
-
export function classifyListStatus(vetResult) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
54
|
+
export function classifyListStatus(vetResult, skipReason) {
|
|
55
|
+
if (skipReason) {
|
|
56
|
+
const fromEnum = mapSkipReasonToStatus(skipReason);
|
|
57
|
+
if (fromEnum)
|
|
58
|
+
return fromEnum;
|
|
59
|
+
// skipReason was set but maps to 'other' / low-score / policy — let the
|
|
60
|
+
// recommendation-based branches below decide final status.
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const skipReasons = vetResult.reasonsToSkip.map((r) => r.toLowerCase());
|
|
64
|
+
if (skipReasons.some((r) => r.includes('closed')))
|
|
65
|
+
return 'closed';
|
|
66
|
+
if (skipReasons.some((r) => r.includes('claimed') || r.includes('assigned')))
|
|
67
|
+
return 'claimed';
|
|
68
|
+
if (skipReasons.some((r) => r.includes('existing pr') || r.includes('linked pr') || r.includes('pull request')))
|
|
69
|
+
return 'has_pr';
|
|
70
|
+
}
|
|
24
71
|
if (vetResult.recommendation === 'approve' || vetResult.recommendation === 'needs_review') {
|
|
25
72
|
return 'still_available';
|
|
26
73
|
}
|
|
@@ -87,7 +134,7 @@ export async function runVetList(options = {}) {
|
|
|
87
134
|
};
|
|
88
135
|
results.push({
|
|
89
136
|
...vetResult,
|
|
90
|
-
listStatus: classifyListStatus(vetResult),
|
|
137
|
+
listStatus: classifyListStatus(vetResult, extractSkipReason(candidate)),
|
|
91
138
|
});
|
|
92
139
|
}
|
|
93
140
|
catch (error) {
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* contribution surface without recourse. We only match on phrases
|
|
17
17
|
* that combine a rejection keyword (no / reject / will be closed /
|
|
18
18
|
* don't accept) with an AI/LLM noun.
|
|
19
|
+
*
|
|
20
|
+
* **User-facing reference:** `docs/anti-llm-policy.md` — explains the
|
|
21
|
+
* three categories, example phrases per category, and the false-positive-
|
|
22
|
+
* resistance design (why "AI division will be closed at end of Q4"
|
|
23
|
+
* does NOT match).
|
|
19
24
|
*/
|
|
20
25
|
export type AntiLLMCategory = 'explicit_ban' | 'tool_ban' | 'reject_framing';
|
|
21
26
|
export interface AntiLLMMatch {
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* contribution surface without recourse. We only match on phrases
|
|
17
17
|
* that combine a rejection keyword (no / reject / will be closed /
|
|
18
18
|
* don't accept) with an AI/LLM noun.
|
|
19
|
+
*
|
|
20
|
+
* **User-facing reference:** `docs/anti-llm-policy.md` — explains the
|
|
21
|
+
* three categories, example phrases per category, and the false-positive-
|
|
22
|
+
* resistance design (why "AI division will be closed at end of Q4"
|
|
23
|
+
* does NOT match).
|
|
19
24
|
*/
|
|
20
25
|
const PATTERNS = [
|
|
21
26
|
// Explicit "no X" bans against AI/LLM nouns.
|
package/dist/core/ci-analysis.js
CHANGED
|
@@ -21,7 +21,12 @@ const FORK_LIMITATION_PATTERNS = [
|
|
|
21
21
|
/chromatic/i,
|
|
22
22
|
/percy/i,
|
|
23
23
|
/cloudflare pages/i,
|
|
24
|
-
|
|
24
|
+
// Tightened from plain `\binternal\b` (#1057 M34). Still catches the
|
|
25
|
+
// known "Facebook Internal", "Meta Internal", and "Internal - <thing>"
|
|
26
|
+
// fork-limitation checks (#675), but the negative lookahead prevents
|
|
27
|
+
// misclassifying legitimate test checks like "Internal API tests" or
|
|
28
|
+
// "Internal Integration Tests".
|
|
29
|
+
/\binternal\b(?!\s+(?:api|test|integration|smoke|unit|e2e|regression|functional))/i,
|
|
25
30
|
];
|
|
26
31
|
/**
|
|
27
32
|
* Known CI check name patterns that indicate authorization gates (#81).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for user-configurable state.json config keys.
|
|
3
|
+
*
|
|
4
|
+
* Keys fall into two CLI surfaces:
|
|
5
|
+
* - `oss-autopilot setup --set key=value` — direct scalar / list-replace sets
|
|
6
|
+
* - `oss-autopilot config <key> <value>` — list mutators (add-/remove-) and aliases
|
|
7
|
+
*
|
|
8
|
+
* A key may be settable via one, the other, or both. `auto` means the field is
|
|
9
|
+
* populated by internal code (e.g. starredRepos is fetched from GitHub), never
|
|
10
|
+
* by a user command — it's listed here so `scout-bridge.ts` reads are auditable.
|
|
11
|
+
*
|
|
12
|
+
* When adding a new user-configurable state.json field:
|
|
13
|
+
* 1. Add the field to `AgentConfigSchema` (state-schema.ts).
|
|
14
|
+
* 2. Add an entry here.
|
|
15
|
+
* 3. Wire the handler in `commands/setup.ts` and/or `commands/config.ts`.
|
|
16
|
+
* 4. The registry test (`config-registry.test.ts`) asserts both commands
|
|
17
|
+
* handle every non-`auto` key.
|
|
18
|
+
*/
|
|
19
|
+
export type SettableVia = 'setup' | 'config' | 'both' | 'auto';
|
|
20
|
+
export interface ConfigKeyDef {
|
|
21
|
+
/** The key as users type it (may differ from the underlying state field, e.g. `dormantDays` → `dormantThresholdDays`). */
|
|
22
|
+
key: string;
|
|
23
|
+
/** One-line human description — shown by `config --list-keys`. */
|
|
24
|
+
description: string;
|
|
25
|
+
/** Which CLI surface accepts this key. */
|
|
26
|
+
settableVia: SettableVia;
|
|
27
|
+
/** Short hint for the expected value shape (e.g. `"number"`, `"owner/repo"`, `"comma-separated list"`). */
|
|
28
|
+
valueHint: string;
|
|
29
|
+
}
|
|
30
|
+
export declare const CONFIG_KEY_REGISTRY: readonly ConfigKeyDef[];
|
|
31
|
+
export declare function isKnownKey(key: string): boolean;
|
|
32
|
+
export declare function getKeyDef(key: string): ConfigKeyDef | undefined;
|
|
33
|
+
/** Keys accepted by the `setup --set` command (includes `both`). */
|
|
34
|
+
export declare function getSetupKeys(): readonly string[];
|
|
35
|
+
/** Keys accepted by the `config <key> <value>` command (includes `both`). */
|
|
36
|
+
export declare function getConfigKeys(): readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Find the closest known key for a typo, limited to keys accepted by the given
|
|
39
|
+
* CLI surface. Returns `undefined` when no key is close enough (threshold ≤2
|
|
40
|
+
* edits, case-insensitive) — better to say nothing than suggest a wild guess.
|
|
41
|
+
*/
|
|
42
|
+
export declare function suggestKey(key: string, surface: 'setup' | 'config'): string | undefined;
|
|
43
|
+
/** Format an "unknown key" error, appending a did-you-mean suggestion when confident. */
|
|
44
|
+
export declare function formatUnknownKeyError(key: string, surface: 'setup' | 'config'): string;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for user-configurable state.json config keys.
|
|
3
|
+
*
|
|
4
|
+
* Keys fall into two CLI surfaces:
|
|
5
|
+
* - `oss-autopilot setup --set key=value` — direct scalar / list-replace sets
|
|
6
|
+
* - `oss-autopilot config <key> <value>` — list mutators (add-/remove-) and aliases
|
|
7
|
+
*
|
|
8
|
+
* A key may be settable via one, the other, or both. `auto` means the field is
|
|
9
|
+
* populated by internal code (e.g. starredRepos is fetched from GitHub), never
|
|
10
|
+
* by a user command — it's listed here so `scout-bridge.ts` reads are auditable.
|
|
11
|
+
*
|
|
12
|
+
* When adding a new user-configurable state.json field:
|
|
13
|
+
* 1. Add the field to `AgentConfigSchema` (state-schema.ts).
|
|
14
|
+
* 2. Add an entry here.
|
|
15
|
+
* 3. Wire the handler in `commands/setup.ts` and/or `commands/config.ts`.
|
|
16
|
+
* 4. The registry test (`config-registry.test.ts`) asserts both commands
|
|
17
|
+
* handle every non-`auto` key.
|
|
18
|
+
*/
|
|
19
|
+
export const CONFIG_KEY_REGISTRY = [
|
|
20
|
+
// ── Identity ─────────────────────────────────────────────────────────
|
|
21
|
+
{
|
|
22
|
+
key: 'username',
|
|
23
|
+
description: 'Your GitHub username.',
|
|
24
|
+
settableVia: 'both',
|
|
25
|
+
valueHint: 'string',
|
|
26
|
+
},
|
|
27
|
+
// ── Capacity / dormancy ──────────────────────────────────────────────
|
|
28
|
+
{
|
|
29
|
+
key: 'maxActivePRs',
|
|
30
|
+
description: 'Soft cap on how many active PRs you want to juggle at once.',
|
|
31
|
+
settableVia: 'setup',
|
|
32
|
+
valueHint: 'positive integer',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'dormantDays',
|
|
36
|
+
description: 'Alias for dormantThresholdDays: days of inactivity before a PR is considered dormant.',
|
|
37
|
+
settableVia: 'setup',
|
|
38
|
+
valueHint: 'positive integer',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'approachingDays',
|
|
42
|
+
description: 'Alias for approachingDormantDays: days before dormancy threshold at which to warn.',
|
|
43
|
+
settableVia: 'setup',
|
|
44
|
+
valueHint: 'positive integer',
|
|
45
|
+
},
|
|
46
|
+
// ── Issue discovery ──────────────────────────────────────────────────
|
|
47
|
+
{
|
|
48
|
+
key: 'languages',
|
|
49
|
+
description: 'Programming languages to filter issue discovery by (whole-list replace).',
|
|
50
|
+
settableVia: 'setup',
|
|
51
|
+
valueHint: 'comma-separated list',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: 'labels',
|
|
55
|
+
description: 'Issue labels to search for (whole-list replace).',
|
|
56
|
+
settableVia: 'setup',
|
|
57
|
+
valueHint: 'comma-separated list',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'scope',
|
|
61
|
+
description: 'Issue complexity scope(s) — beginner, intermediate, advanced.',
|
|
62
|
+
settableVia: 'setup',
|
|
63
|
+
valueHint: 'comma-separated list of: beginner,intermediate,advanced',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'minStars',
|
|
67
|
+
description: 'Minimum stargazers required for a repo to surface during discovery.',
|
|
68
|
+
settableVia: 'setup',
|
|
69
|
+
valueHint: 'non-negative integer',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: 'includeDocIssues',
|
|
73
|
+
description: 'Whether documentation-only issues should appear in discovery.',
|
|
74
|
+
settableVia: 'setup',
|
|
75
|
+
valueHint: 'true|false',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: 'maxIssueAgeDays',
|
|
79
|
+
description: 'Maximum age (in days) for an issue to be considered in discovery.',
|
|
80
|
+
settableVia: 'setup',
|
|
81
|
+
valueHint: 'positive integer',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: 'minRepoScoreThreshold',
|
|
85
|
+
description: 'Minimum repo maintainer-health score required for discovery (0–10).',
|
|
86
|
+
settableVia: 'setup',
|
|
87
|
+
valueHint: 'non-negative integer',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: 'projectCategories',
|
|
91
|
+
description: 'Project categories to prioritize (whole-list replace).',
|
|
92
|
+
settableVia: 'setup',
|
|
93
|
+
valueHint: 'comma-separated list of: nonprofit,devtools,infrastructure,web-frameworks,data-ml,education',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: 'preferredOrgs',
|
|
97
|
+
description: 'GitHub orgs to prioritize during discovery (whole-list replace).',
|
|
98
|
+
settableVia: 'setup',
|
|
99
|
+
valueHint: 'comma-separated list',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: 'aiPolicyBlocklist',
|
|
103
|
+
description: 'Repos (owner/repo) with anti-AI contribution policies to block from discovery.',
|
|
104
|
+
settableVia: 'setup',
|
|
105
|
+
valueHint: 'comma-separated list of owner/repo',
|
|
106
|
+
},
|
|
107
|
+
// ── Exclusion list mutators (config-only) ────────────────────────────
|
|
108
|
+
{
|
|
109
|
+
key: 'add-language',
|
|
110
|
+
description: 'Append a language to the discovery languages list.',
|
|
111
|
+
settableVia: 'config',
|
|
112
|
+
valueHint: 'string',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: 'add-label',
|
|
116
|
+
description: 'Append a label to the discovery labels list.',
|
|
117
|
+
settableVia: 'config',
|
|
118
|
+
valueHint: 'string',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: 'remove-label',
|
|
122
|
+
description: 'Remove a label from the discovery labels list.',
|
|
123
|
+
settableVia: 'config',
|
|
124
|
+
valueHint: 'string (must already be present)',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: 'add-scope',
|
|
128
|
+
description: 'Append a scope to the discovery scope list.',
|
|
129
|
+
settableVia: 'config',
|
|
130
|
+
valueHint: 'one of: beginner,intermediate,advanced',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: 'remove-scope',
|
|
134
|
+
description: 'Remove a scope from the discovery scope list.',
|
|
135
|
+
settableVia: 'config',
|
|
136
|
+
valueHint: 'one of: beginner,intermediate,advanced',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: 'exclude-repo',
|
|
140
|
+
description: 'Exclude a specific repo (owner/repo) from discovery.',
|
|
141
|
+
settableVia: 'config',
|
|
142
|
+
valueHint: 'owner/repo',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: 'exclude-org',
|
|
146
|
+
description: 'Exclude an entire org from discovery.',
|
|
147
|
+
settableVia: 'config',
|
|
148
|
+
valueHint: 'org name (no slash)',
|
|
149
|
+
},
|
|
150
|
+
// ── Tooling ──────────────────────────────────────────────────────────
|
|
151
|
+
{
|
|
152
|
+
key: 'issueListPath',
|
|
153
|
+
description: 'Path to a text file of extra issue URLs to surface.',
|
|
154
|
+
settableVia: 'both',
|
|
155
|
+
valueHint: 'filesystem path',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: 'skippedIssuesPath',
|
|
159
|
+
description: 'Path to the skipped-issues file (auto-culls entries older than 90 days).',
|
|
160
|
+
settableVia: 'setup',
|
|
161
|
+
valueHint: 'filesystem path',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
key: 'diffTool',
|
|
165
|
+
description: 'Default diff renderer for reviews.',
|
|
166
|
+
settableVia: 'both',
|
|
167
|
+
valueHint: 'one of: inline,sourcetree,vscode,custom',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
key: 'diffToolCustomCommand',
|
|
171
|
+
description: 'Shell command template used when diffTool=custom.',
|
|
172
|
+
settableVia: 'both',
|
|
173
|
+
valueHint: 'shell command with {old}/{new} placeholders',
|
|
174
|
+
},
|
|
175
|
+
// ── Behavior ─────────────────────────────────────────────────────────
|
|
176
|
+
{
|
|
177
|
+
key: 'squashByDefault',
|
|
178
|
+
description: 'Default merge strategy — squash-merge, prompt, or standard merge.',
|
|
179
|
+
settableVia: 'setup',
|
|
180
|
+
valueHint: 'true|false|ask',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
key: 'persistence',
|
|
184
|
+
description: 'Where to store state.json — local file or GitHub Gist.',
|
|
185
|
+
settableVia: 'setup',
|
|
186
|
+
valueHint: 'one of: local,gist',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
key: 'autoFormatBeforePush',
|
|
190
|
+
description: 'Opt-in: run the project formatter and append a `style:` commit before every `git push`. Off by default because formatting commits surprise OSS maintainers; the hook also skips automatically when the branch tracks a fork upstream.',
|
|
191
|
+
settableVia: 'setup',
|
|
192
|
+
valueHint: 'true|false',
|
|
193
|
+
},
|
|
194
|
+
// ── Setup-only completion flag ──────────────────────────────────────
|
|
195
|
+
{
|
|
196
|
+
key: 'complete',
|
|
197
|
+
description: 'Internal marker that initial setup has finished. Normally set by the wizard — `setup --set complete=true` is a manual override.',
|
|
198
|
+
settableVia: 'setup',
|
|
199
|
+
valueHint: 'true',
|
|
200
|
+
},
|
|
201
|
+
// ── Auto-managed (listed for auditability; not user-settable) ────────
|
|
202
|
+
{
|
|
203
|
+
key: 'starredRepos',
|
|
204
|
+
description: 'Cache of the user’s starred repos. Refreshed automatically during discovery.',
|
|
205
|
+
settableVia: 'auto',
|
|
206
|
+
valueHint: '(managed internally)',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
key: 'starredReposLastFetched',
|
|
210
|
+
description: 'Timestamp of the last starredRepos refresh.',
|
|
211
|
+
settableVia: 'auto',
|
|
212
|
+
valueHint: '(managed internally)',
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
const KEY_INDEX = new Map(CONFIG_KEY_REGISTRY.map((def) => [def.key, def]));
|
|
216
|
+
export function isKnownKey(key) {
|
|
217
|
+
return KEY_INDEX.has(key);
|
|
218
|
+
}
|
|
219
|
+
export function getKeyDef(key) {
|
|
220
|
+
return KEY_INDEX.get(key);
|
|
221
|
+
}
|
|
222
|
+
/** Keys accepted by the `setup --set` command (includes `both`). */
|
|
223
|
+
export function getSetupKeys() {
|
|
224
|
+
return CONFIG_KEY_REGISTRY.filter((d) => d.settableVia === 'setup' || d.settableVia === 'both').map((d) => d.key);
|
|
225
|
+
}
|
|
226
|
+
/** Keys accepted by the `config <key> <value>` command (includes `both`). */
|
|
227
|
+
export function getConfigKeys() {
|
|
228
|
+
return CONFIG_KEY_REGISTRY.filter((d) => d.settableVia === 'config' || d.settableVia === 'both').map((d) => d.key);
|
|
229
|
+
}
|
|
230
|
+
/** Classic iterative Levenshtein. O(n*m) time, O(min(n,m)) space. */
|
|
231
|
+
function levenshtein(a, b) {
|
|
232
|
+
if (a === b)
|
|
233
|
+
return 0;
|
|
234
|
+
if (a.length === 0)
|
|
235
|
+
return b.length;
|
|
236
|
+
if (b.length === 0)
|
|
237
|
+
return a.length;
|
|
238
|
+
// Ensure b is the shorter (minimizes row width).
|
|
239
|
+
if (a.length < b.length)
|
|
240
|
+
[a, b] = [b, a];
|
|
241
|
+
let prev = new Array(b.length + 1);
|
|
242
|
+
let curr = new Array(b.length + 1);
|
|
243
|
+
for (let j = 0; j <= b.length; j++)
|
|
244
|
+
prev[j] = j;
|
|
245
|
+
for (let i = 1; i <= a.length; i++) {
|
|
246
|
+
curr[0] = i;
|
|
247
|
+
for (let j = 1; j <= b.length; j++) {
|
|
248
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
249
|
+
curr[j] = Math.min(prev[j] + 1, // deletion
|
|
250
|
+
curr[j - 1] + 1, // insertion
|
|
251
|
+
prev[j - 1] + cost);
|
|
252
|
+
}
|
|
253
|
+
[prev, curr] = [curr, prev];
|
|
254
|
+
}
|
|
255
|
+
return prev[b.length];
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Find the closest known key for a typo, limited to keys accepted by the given
|
|
259
|
+
* CLI surface. Returns `undefined` when no key is close enough (threshold ≤2
|
|
260
|
+
* edits, case-insensitive) — better to say nothing than suggest a wild guess.
|
|
261
|
+
*/
|
|
262
|
+
export function suggestKey(key, surface) {
|
|
263
|
+
const candidates = surface === 'setup' ? getSetupKeys() : getConfigKeys();
|
|
264
|
+
const lower = key.toLowerCase();
|
|
265
|
+
let best;
|
|
266
|
+
for (const candidate of candidates) {
|
|
267
|
+
const d = levenshtein(lower, candidate.toLowerCase());
|
|
268
|
+
if (best === undefined || d < best.distance) {
|
|
269
|
+
best = { key: candidate, distance: d };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!best)
|
|
273
|
+
return undefined;
|
|
274
|
+
// Allow up to 2 edits, or 3 when the typo is long (≥8 chars) — forgives one swap + one drop.
|
|
275
|
+
const threshold = key.length >= 8 ? 3 : 2;
|
|
276
|
+
return best.distance <= threshold ? best.key : undefined;
|
|
277
|
+
}
|
|
278
|
+
/** Format an "unknown key" error, appending a did-you-mean suggestion when confident. */
|
|
279
|
+
export function formatUnknownKeyError(key, surface) {
|
|
280
|
+
const suggestion = suggestKey(key, surface);
|
|
281
|
+
const base = surface === 'setup' ? `Unknown setting "${key}"` : `Unknown config key "${key}"`;
|
|
282
|
+
if (suggestion) {
|
|
283
|
+
return `${base}. Did you mean "${suggestion}"? Run \`oss-autopilot config --list-keys\` to see all keys.`;
|
|
284
|
+
}
|
|
285
|
+
return `${base}. Run \`oss-autopilot config --list-keys\` to see all keys.`;
|
|
286
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime schema for the dashboard server's `GET /api/data` response (#1050).
|
|
3
|
+
*
|
|
4
|
+
* The server (`commands/dashboard-data.ts`) and client (`packages/dashboard/`)
|
|
5
|
+
* run in different processes. TypeScript can't cross the process boundary —
|
|
6
|
+
* if the server removes or renames a field, the client hits runtime
|
|
7
|
+
* `undefined` with no diagnostic. This schema is the shared runtime contract:
|
|
8
|
+
* the server can optionally self-check outgoing payloads, and the dashboard
|
|
9
|
+
* validates every `/api/data` response before committing to state.
|
|
10
|
+
*
|
|
11
|
+
* Intentional scope: this schema validates **top-level presence and primitive
|
|
12
|
+
* shape** (required vs optional fields, container types like array/object),
|
|
13
|
+
* but uses `z.array(z.unknown())` for nested PR/issue arrays rather than
|
|
14
|
+
* pinning every field. The top-level surface is the only place drift tends
|
|
15
|
+
* to go silently undetected — nested fields already blow up loudly at the
|
|
16
|
+
* render site when they change. Exhaustive nested validation would turn the
|
|
17
|
+
* schema into a maintenance burden.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
export declare const DashboardStatsSchema: z.ZodObject<{
|
|
21
|
+
activePRs: z.ZodNumber;
|
|
22
|
+
shelvedPRs: z.ZodNumber;
|
|
23
|
+
mergedPRs: z.ZodNumber;
|
|
24
|
+
closedPRs: z.ZodNumber;
|
|
25
|
+
mergeRate: z.ZodString;
|
|
26
|
+
availableIssues: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
export declare const DashboardDataSchema: z.ZodObject<{
|
|
29
|
+
stats: z.ZodObject<{
|
|
30
|
+
activePRs: z.ZodNumber;
|
|
31
|
+
shelvedPRs: z.ZodNumber;
|
|
32
|
+
mergedPRs: z.ZodNumber;
|
|
33
|
+
closedPRs: z.ZodNumber;
|
|
34
|
+
mergeRate: z.ZodString;
|
|
35
|
+
availableIssues: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
prsByRepo: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
38
|
+
active: z.ZodNumber;
|
|
39
|
+
merged: z.ZodNumber;
|
|
40
|
+
closed: z.ZodNumber;
|
|
41
|
+
}, z.core.$strip>>;
|
|
42
|
+
topRepos: z.ZodArray<z.ZodObject<{
|
|
43
|
+
repo: z.ZodString;
|
|
44
|
+
active: z.ZodNumber;
|
|
45
|
+
merged: z.ZodNumber;
|
|
46
|
+
closed: z.ZodNumber;
|
|
47
|
+
}, z.core.$strip>>;
|
|
48
|
+
monthlyMerged: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
49
|
+
monthlyOpened: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
50
|
+
monthlyClosed: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
51
|
+
activePRs: z.ZodArray<z.ZodUnknown>;
|
|
52
|
+
shelvedPRUrls: z.ZodArray<z.ZodString>;
|
|
53
|
+
recentlyMergedPRs: z.ZodArray<z.ZodUnknown>;
|
|
54
|
+
recentlyClosedPRs: z.ZodArray<z.ZodUnknown>;
|
|
55
|
+
autoUnshelvedPRs: z.ZodArray<z.ZodUnknown>;
|
|
56
|
+
commentedIssues: z.ZodArray<z.ZodUnknown>;
|
|
57
|
+
issueResponses: z.ZodArray<z.ZodUnknown>;
|
|
58
|
+
allMergedPRs: z.ZodArray<z.ZodUnknown>;
|
|
59
|
+
allClosedPRs: z.ZodArray<z.ZodUnknown>;
|
|
60
|
+
repoMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
61
|
+
vettedIssues: z.ZodOptional<z.ZodNullable<z.ZodUnknown>>;
|
|
62
|
+
offline: z.ZodOptional<z.ZodBoolean>;
|
|
63
|
+
lastUpdated: z.ZodOptional<z.ZodString>;
|
|
64
|
+
partialFailures: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
export type DashboardDataParsed = z.infer<typeof DashboardDataSchema>;
|
|
67
|
+
/**
|
|
68
|
+
* Validate a raw `/api/data` payload. Returns `{ok: true, data}` on success or
|
|
69
|
+
* `{ok: false, message}` with a condensed Zod error string on failure. Never
|
|
70
|
+
* throws. The dashboard's `useDashboard` hook surfaces the message in the UI.
|
|
71
|
+
*/
|
|
72
|
+
export declare function validateDashboardData(raw: unknown): {
|
|
73
|
+
ok: true;
|
|
74
|
+
data: DashboardDataParsed;
|
|
75
|
+
} | {
|
|
76
|
+
ok: false;
|
|
77
|
+
message: string;
|
|
78
|
+
};
|