@oss-autopilot/core 1.1.0 → 1.3.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.
@@ -3,6 +3,7 @@
3
3
  * Shows or updates configuration
4
4
  */
5
5
  import { getStateManager } from '../core/index.js';
6
+ import { ValidationError } from '../core/errors.js';
6
7
  import { ISSUE_SCOPES } from '../core/types.js';
7
8
  import { validateGitHubUsername } from './validation.js';
8
9
  function validateScope(value) {
@@ -105,6 +106,14 @@ export async function runConfig(options) {
105
106
  case 'issueListPath':
106
107
  stateManager.updateConfig({ issueListPath: value || undefined });
107
108
  break;
109
+ case 'scoreThreshold': {
110
+ const threshold = Number(value);
111
+ if (!Number.isInteger(threshold) || threshold < 1 || threshold > 10) {
112
+ throw new ValidationError(`Invalid value for scoreThreshold: "${value}". Must be an integer between 1 and 10.`);
113
+ }
114
+ stateManager.updateConfig({ scoreThreshold: threshold });
115
+ break;
116
+ }
108
117
  default:
109
118
  throw new Error(`Unknown config key: ${options.key}`);
110
119
  }
@@ -24,6 +24,7 @@ export interface SetupCompleteOutput {
24
24
  projectCategories: ProjectCategory[];
25
25
  preferredOrgs: string[];
26
26
  scope: IssueScope[];
27
+ scoreThreshold: number;
27
28
  };
28
29
  }
29
30
  export interface SetupPrompt {
@@ -9,11 +9,19 @@ import { PROJECT_CATEGORIES, ISSUE_SCOPES } from '../core/types.js';
9
9
  /** Parse and validate a positive integer setting value. */
10
10
  function parsePositiveInt(value, settingName) {
11
11
  const parsed = Number(value);
12
- if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
12
+ if (!Number.isInteger(parsed) || parsed < 1) {
13
13
  throw new ValidationError(`Invalid value for ${settingName}: "${value}". Must be a positive integer.`);
14
14
  }
15
15
  return parsed;
16
16
  }
17
+ /** Parse and validate an integer within a specific range [min, max]. */
18
+ function parseBoundedInt(value, settingName, min, max) {
19
+ const parsed = Number(value);
20
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
21
+ throw new ValidationError(`Invalid value for ${settingName}: "${value}". Must be an integer between ${min} and ${max}.`);
22
+ }
23
+ return parsed;
24
+ }
17
25
  /**
18
26
  * Interactive setup wizard or direct setting application.
19
27
  *
@@ -84,9 +92,15 @@ export async function runSetup(options) {
84
92
  results[key] = value !== 'false' ? 'true' : 'false';
85
93
  }
86
94
  break;
95
+ case 'scoreThreshold': {
96
+ const threshold = parseBoundedInt(value, 'scoreThreshold', 1, 10);
97
+ stateManager.updateConfig({ scoreThreshold: threshold });
98
+ results[key] = String(threshold);
99
+ break;
100
+ }
87
101
  case 'minStars': {
88
102
  const stars = Number(value);
89
- if (!Number.isFinite(stars) || !Number.isInteger(stars) || stars < 0) {
103
+ if (!Number.isInteger(stars) || stars < 0) {
90
104
  throw new ValidationError(`Invalid value for minStars: "${value}". Must be a non-negative integer.`);
91
105
  }
92
106
  stateManager.updateConfig({ minStars: stars });
@@ -225,6 +239,7 @@ export async function runSetup(options) {
225
239
  projectCategories: config.projectCategories ?? [],
226
240
  preferredOrgs: config.preferredOrgs ?? [],
227
241
  scope: config.scope ?? [],
242
+ scoreThreshold: config.scoreThreshold,
228
243
  },
229
244
  };
230
245
  }
@@ -281,6 +296,13 @@ export async function runSetup(options) {
281
296
  default: [],
282
297
  type: 'list',
283
298
  },
299
+ {
300
+ setting: 'scoreThreshold',
301
+ prompt: 'Minimum vet score (1-10) for issues to keep after vetting? Issues below this are auto-filtered.',
302
+ current: config.scoreThreshold,
303
+ default: 6,
304
+ type: 'number',
305
+ },
284
306
  {
285
307
  setting: 'aiPolicyBlocklist',
286
308
  prompt: 'Repos with anti-AI contribution policies to block (owner/repo, comma-separated)?',
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Vet-list command (#764)
3
+ * Re-vets all available issues in a curated issue list file.
4
+ */
5
+ import { type VetListOutput, type VetOutput, type VetListItemStatus } from '../formatters/json.js';
6
+ interface VetListOptions {
7
+ issueListPath?: string;
8
+ concurrency?: number;
9
+ }
10
+ /**
11
+ * Determine the list status from vetting results.
12
+ * Maps vetting recommendation + reasons to a list-level status.
13
+ */
14
+ export declare function classifyListStatus(vetResult: VetOutput): VetListItemStatus;
15
+ /**
16
+ * Re-vet all available issues in a curated issue list.
17
+ * Reads the list file, extracts available (non-done) issues,
18
+ * and vets each in parallel with concurrency control.
19
+ *
20
+ * @param options - Vet-list options
21
+ * @returns Consolidated vetting results with list status for each issue
22
+ */
23
+ export declare function runVetList(options?: VetListOptions): Promise<VetListOutput>;
24
+ export {};
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Vet-list command (#764)
3
+ * Re-vets all available issues in a curated issue list file.
4
+ */
5
+ import { IssueDiscovery, requireGitHubToken } from '../core/index.js';
6
+ import { detectIssueList } from './startup.js';
7
+ /**
8
+ * Determine the list status from vetting results.
9
+ * Maps vetting recommendation + reasons to a list-level status.
10
+ */
11
+ export function classifyListStatus(vetResult) {
12
+ const skipReasons = vetResult.reasonsToSkip.map((r) => r.toLowerCase());
13
+ if (skipReasons.some((r) => r.includes('closed')))
14
+ return 'closed';
15
+ if (skipReasons.some((r) => r.includes('claimed') || r.includes('assigned')))
16
+ return 'claimed';
17
+ if (skipReasons.some((r) => r.includes('existing pr') || r.includes('linked pr') || r.includes('pull request')))
18
+ return 'has_pr';
19
+ if (vetResult.recommendation === 'approve' || vetResult.recommendation === 'needs_review') {
20
+ return 'still_available';
21
+ }
22
+ // Default: if skipped for other reasons, still mark as available
23
+ // (the vetting result will show why it's not recommended)
24
+ return 'still_available';
25
+ }
26
+ /**
27
+ * Re-vet all available issues in a curated issue list.
28
+ * Reads the list file, extracts available (non-done) issues,
29
+ * and vets each in parallel with concurrency control.
30
+ *
31
+ * @param options - Vet-list options
32
+ * @returns Consolidated vetting results with list status for each issue
33
+ */
34
+ export async function runVetList(options = {}) {
35
+ const token = requireGitHubToken();
36
+ const concurrency = options.concurrency ?? 5;
37
+ // 1. Find and parse the issue list
38
+ let issueListPath = options.issueListPath;
39
+ if (!issueListPath) {
40
+ const detected = detectIssueList();
41
+ if (!detected) {
42
+ throw new Error('No issue list found. Provide a path with --path or configure issueListPath in settings.');
43
+ }
44
+ issueListPath = detected.path;
45
+ }
46
+ const { runParseList } = await import('./parse-list.js');
47
+ const parsed = await runParseList({ filePath: issueListPath });
48
+ if (parsed.available.length === 0) {
49
+ return {
50
+ results: [],
51
+ summary: { total: 0, stillAvailable: 0, claimed: 0, closed: 0, hasPR: 0, errors: 0 },
52
+ };
53
+ }
54
+ // 2. Vet each available issue in parallel with concurrency limit
55
+ const discovery = new IssueDiscovery(token);
56
+ const results = [];
57
+ // Simple concurrency limiter
58
+ const items = parsed.available;
59
+ let index = 0;
60
+ async function processNext() {
61
+ while (index < items.length) {
62
+ const item = items[index++];
63
+ try {
64
+ const candidate = await discovery.vetIssue(item.url);
65
+ const vetResult = {
66
+ issue: {
67
+ repo: candidate.issue.repo,
68
+ number: candidate.issue.number,
69
+ title: candidate.issue.title,
70
+ url: candidate.issue.url,
71
+ labels: candidate.issue.labels,
72
+ },
73
+ recommendation: candidate.recommendation,
74
+ reasonsToApprove: candidate.reasonsToApprove,
75
+ reasonsToSkip: candidate.reasonsToSkip,
76
+ projectHealth: candidate.projectHealth,
77
+ vettingResult: candidate.vettingResult,
78
+ };
79
+ results.push({
80
+ ...vetResult,
81
+ listStatus: classifyListStatus(vetResult),
82
+ });
83
+ }
84
+ catch (error) {
85
+ // Per-issue errors don't fail the batch
86
+ results.push({
87
+ issue: { repo: item.repo, number: item.number, title: item.title, url: item.url, labels: [] },
88
+ recommendation: 'skip',
89
+ reasonsToApprove: [],
90
+ reasonsToSkip: [`Error: ${error instanceof Error ? error.message : String(error)}`],
91
+ projectHealth: {},
92
+ vettingResult: {},
93
+ listStatus: 'error',
94
+ errorMessage: error instanceof Error ? error.message : String(error),
95
+ });
96
+ }
97
+ }
98
+ }
99
+ // Start `concurrency` workers
100
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => processNext());
101
+ await Promise.all(workers);
102
+ // 3. Compute summary
103
+ const summary = {
104
+ total: results.length,
105
+ stillAvailable: results.filter((r) => r.listStatus === 'still_available').length,
106
+ claimed: results.filter((r) => r.listStatus === 'claimed').length,
107
+ closed: results.filter((r) => r.listStatus === 'closed').length,
108
+ hasPR: results.filter((r) => r.listStatus === 'has_pr').length,
109
+ errors: results.filter((r) => r.listStatus === 'error').length,
110
+ };
111
+ return { results, summary };
112
+ }
@@ -333,11 +333,25 @@ export function computeActionMenu(actionableIssues, capacity, commentedIssues =
333
333
  }
334
334
  // The orchestration layer (commands/oss.md Action Menu section) may insert issue-list
335
335
  // options before the search item when a curated list is available.
336
- items.push({
336
+ const searchItem = {
337
337
  key: 'search',
338
338
  label: 'Search for new issues',
339
339
  description: 'Look for new contribution opportunities',
340
- });
340
+ };
341
+ if (!capacity.hasCapacity) {
342
+ const atLimit = capacity.activePRCount >= capacity.maxActivePRs;
343
+ const hasCritical = capacity.criticalIssueCount > 0;
344
+ if (atLimit && hasCritical) {
345
+ searchItem.capacityWarning = `You're at ${capacity.activePRCount}/${capacity.maxActivePRs} active PRs and have ${capacity.criticalIssueCount} critical issue(s). Resolve existing work before claiming new issues.`;
346
+ }
347
+ else if (atLimit) {
348
+ searchItem.capacityWarning = `You're at ${capacity.activePRCount}/${capacity.maxActivePRs} active PRs. Claiming a new issue will exceed your limit.`;
349
+ }
350
+ else {
351
+ searchItem.capacityWarning = `You have ${capacity.criticalIssueCount} critical issue(s) needing attention. Resolve them before claiming new issues.`;
352
+ }
353
+ }
354
+ items.push(searchItem);
341
355
  items.push({
342
356
  key: 'done',
343
357
  label: 'Done for now',
@@ -209,6 +209,7 @@ export declare const AgentConfigSchema: z.ZodObject<{
209
209
  trustedProjects: z.ZodDefault<z.ZodArray<z.ZodString>>;
210
210
  githubUsername: z.ZodDefault<z.ZodString>;
211
211
  minRepoScoreThreshold: z.ZodDefault<z.ZodNumber>;
212
+ scoreThreshold: z.ZodDefault<z.ZodNumber>;
212
213
  starredRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
213
214
  starredReposLastFetched: z.ZodOptional<z.ZodString>;
214
215
  showHealthCheck: z.ZodOptional<z.ZodBoolean>;
@@ -354,6 +355,7 @@ export declare const AgentStateSchema: z.ZodObject<{
354
355
  trustedProjects: z.ZodDefault<z.ZodArray<z.ZodString>>;
355
356
  githubUsername: z.ZodDefault<z.ZodString>;
356
357
  minRepoScoreThreshold: z.ZodDefault<z.ZodNumber>;
358
+ scoreThreshold: z.ZodDefault<z.ZodNumber>;
357
359
  starredRepos: z.ZodDefault<z.ZodArray<z.ZodString>>;
358
360
  starredReposLastFetched: z.ZodOptional<z.ZodString>;
359
361
  showHealthCheck: z.ZodOptional<z.ZodBoolean>;
@@ -135,6 +135,7 @@ export const AgentConfigSchema = z.object({
135
135
  trustedProjects: z.array(z.string()).default([]),
136
136
  githubUsername: z.string().default(''),
137
137
  minRepoScoreThreshold: z.number().default(4),
138
+ scoreThreshold: z.number().int().min(1).max(10).default(6),
138
139
  starredRepos: z.array(z.string()).default([]),
139
140
  starredReposLastFetched: z.string().optional(),
140
141
  showHealthCheck: z.boolean().optional(),
@@ -53,6 +53,8 @@ export interface ActionMenuItem {
53
53
  label: string;
54
54
  /** Explanation shown below the label. */
55
55
  description: string;
56
+ /** Present when the action would exceed the user's PR capacity limit (#765). */
57
+ capacityWarning?: string;
56
58
  }
57
59
  /**
58
60
  * Pre-computed action menu for the orchestration layer.
@@ -111,6 +113,31 @@ export interface DailyOutput {
111
113
  repoGroups: CompactRepoGroup[];
112
114
  failures: PRCheckFailure[];
113
115
  }
116
+ /**
117
+ * Compact version of DailyOutput for reduced JSON payload size (#763).
118
+ * Omits `summary` (pre-rendered markdown ~8KB that duplicates structured fields),
119
+ * `repoGroups` (derivable from digest.openPRs), and full `failures` array.
120
+ * Retains `commentedIssues` because downstream workflows (review-issue-replies.md,
121
+ * action-menu.md) consume it directly.
122
+ * Includes `failureCount` so consumers can detect partial fetch failures without
123
+ * carrying the full error payload.
124
+ */
125
+ export interface CompactDailyOutput {
126
+ digest: DailyDigestCompact;
127
+ capacity: CapacityAssessment;
128
+ briefSummary: string;
129
+ actionableIssues: CompactActionableIssue[];
130
+ actionMenu: ActionMenu;
131
+ commentedIssues: CommentedIssue[];
132
+ /** Number of PRs that failed to fetch. Non-zero indicates partial results. */
133
+ failureCount: number;
134
+ }
135
+ /**
136
+ * Strip a full DailyOutput down to the compact subset (#763).
137
+ * Omits summary, repoGroups, and full failures array. Retains a failureCount
138
+ * so consumers can detect partial fetch failures.
139
+ */
140
+ export declare function toCompactDailyOutput(output: DailyOutput): CompactDailyOutput;
114
141
  /**
115
142
  * Convert a full DailyDigest to the compact format for JSON output (#287).
116
143
  * Category arrays become PR URL arrays; full objects stay only in openPRs.
@@ -207,6 +234,19 @@ export interface StartupOutput {
207
234
  dashboardUrl?: string;
208
235
  issueList?: IssueListInfo;
209
236
  }
237
+ /**
238
+ * Compact version of StartupOutput for reduced JSON payload (#763).
239
+ * Derived from StartupOutput with CompactDailyOutput substituted for DailyOutput.
240
+ * Using Omit ensures new fields added to StartupOutput are automatically included.
241
+ */
242
+ export type CompactStartupOutput = Omit<StartupOutput, 'daily'> & {
243
+ daily?: CompactDailyOutput;
244
+ };
245
+ /**
246
+ * Convert a full StartupOutput to the compact format (#763).
247
+ * Uses destructuring to auto-forward any new fields added to StartupOutput.
248
+ */
249
+ export declare function toCompactStartupOutput(output: StartupOutput): CompactStartupOutput;
210
250
  /** A single parsed issue from a markdown list (#82) */
211
251
  export interface ParsedIssueItem {
212
252
  repo: string;
@@ -234,6 +274,23 @@ export interface CheckIntegrationOutput {
234
274
  newFiles: NewFileInfo[];
235
275
  unreferencedCount: number;
236
276
  }
277
+ /** Status of a re-vetted issue from the curated list (#764). */
278
+ export type VetListItemStatus = 'still_available' | 'claimed' | 'closed' | 'has_pr' | 'error';
279
+ /** Output of the vet-list command (#764). */
280
+ export interface VetListOutput {
281
+ results: Array<VetOutput & {
282
+ listStatus: VetListItemStatus;
283
+ errorMessage?: string;
284
+ }>;
285
+ summary: {
286
+ total: number;
287
+ stillAvailable: number;
288
+ claimed: number;
289
+ closed: number;
290
+ hasPR: number;
291
+ errors: number;
292
+ };
293
+ }
237
294
  /** Output of the vet command */
238
295
  export interface VetOutput {
239
296
  issue: {
@@ -2,6 +2,22 @@
2
2
  * JSON output formatter for CLI --json mode
3
3
  * Provides structured output that can be consumed by scripts and plugins
4
4
  */
5
+ /**
6
+ * Strip a full DailyOutput down to the compact subset (#763).
7
+ * Omits summary, repoGroups, and full failures array. Retains a failureCount
8
+ * so consumers can detect partial fetch failures.
9
+ */
10
+ export function toCompactDailyOutput(output) {
11
+ return {
12
+ digest: output.digest,
13
+ capacity: output.capacity,
14
+ briefSummary: output.briefSummary,
15
+ actionableIssues: output.actionableIssues,
16
+ actionMenu: output.actionMenu,
17
+ commentedIssues: output.commentedIssues,
18
+ failureCount: output.failures.length,
19
+ };
20
+ }
5
21
  /**
6
22
  * Convert a full DailyDigest to the compact format for JSON output (#287).
7
23
  * Category arrays become PR URL arrays; full objects stay only in openPRs.
@@ -43,6 +59,19 @@ export function compactRepoGroups(groups) {
43
59
  prUrls: group.prs.map((pr) => pr.url),
44
60
  }));
45
61
  }
62
+ /**
63
+ * Convert a full StartupOutput to the compact format (#763).
64
+ * Uses destructuring to auto-forward any new fields added to StartupOutput.
65
+ */
66
+ export function toCompactStartupOutput(output) {
67
+ const { daily, ...rest } = output;
68
+ return {
69
+ ...rest,
70
+ daily: daily ? toCompactDailyOutput(daily) : undefined,
71
+ dashboardUrl: output.dashboardUrl,
72
+ issueList: output.issueList,
73
+ };
74
+ }
46
75
  /**
47
76
  * Wrap data in a standard JSON output envelope
48
77
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-autopilot/core",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "CLI and core library for managing open source contributions",
5
5
  "type": "module",
6
6
  "bin": {