@oss-autopilot/core 3.6.0 → 3.7.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.
@@ -284,6 +284,97 @@ export const commands = [
284
284
  });
285
285
  },
286
286
  },
287
+ // ── Features ───────────────────────────────────────────────────────────
288
+ // scout 0.9.0 (#97/#98/#99): feature-scoped opportunities in repos with
289
+ // 3+ merged PRs, split into quick-wins / bigger-bets buckets.
290
+ {
291
+ name: 'features',
292
+ register(program) {
293
+ program
294
+ .command('features [count]')
295
+ .description('Find feature-scoped opportunities in repos with 3+ merged PRs')
296
+ .option('--json', 'Output as JSON')
297
+ .option('--anchor-threshold <n>', 'Override featuresAnchorThreshold (1-50)')
298
+ .option('--split-ratio <r>', 'Override featuresSplitRatio (0-1)')
299
+ .action(async (count, options) => {
300
+ const { FeaturesOutputSchema } = await import('./formatters/json.js');
301
+ await executeAction(options, async () => {
302
+ const { runFeatures, MAX_FEATURES_RESULTS } = await import('./commands/features.js');
303
+ let maxResults = 10;
304
+ if (count !== undefined) {
305
+ const parsed = Number(count);
306
+ if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
307
+ throw new Error(`Invalid count "${count}". Must be a positive integer.`);
308
+ }
309
+ maxResults = parsed;
310
+ }
311
+ if (maxResults > MAX_FEATURES_RESULTS) {
312
+ console.warn(`Capping features to ${MAX_FEATURES_RESULTS} results (requested: ${maxResults})`);
313
+ maxResults = MAX_FEATURES_RESULTS;
314
+ }
315
+ let anchorThreshold;
316
+ if (options.anchorThreshold !== undefined) {
317
+ const parsed = Number(options.anchorThreshold);
318
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1 || parsed > 50) {
319
+ throw new Error(`Invalid --anchor-threshold "${options.anchorThreshold}". Must be an integer in [1, 50].`);
320
+ }
321
+ anchorThreshold = parsed;
322
+ }
323
+ let splitRatio;
324
+ if (options.splitRatio !== undefined) {
325
+ const parsed = Number(options.splitRatio);
326
+ if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1) {
327
+ throw new Error(`Invalid --split-ratio "${options.splitRatio}". Must be a number in [0, 1].`);
328
+ }
329
+ splitRatio = parsed;
330
+ }
331
+ if (!options.json) {
332
+ console.log(`\nSearching for feature opportunities (max ${maxResults})...\n`);
333
+ }
334
+ return runFeatures({ maxResults, anchorThreshold, splitRatio });
335
+ }, (data) => {
336
+ if (data.anchorRepos.length > 0) {
337
+ console.log(`Anchor repos (${data.anchorRepos.length}): ${data.anchorRepos.join(', ')}`);
338
+ console.log('');
339
+ }
340
+ if (data.quickWins.length === 0 && data.biggerBets.length === 0) {
341
+ if (data.rateLimitWarning) {
342
+ console.warn(`\n${data.rateLimitWarning}\n`);
343
+ }
344
+ else if (data.message) {
345
+ console.log(data.message);
346
+ }
347
+ else {
348
+ console.log('No feature opportunities found.');
349
+ }
350
+ return;
351
+ }
352
+ if (data.rateLimitWarning) {
353
+ console.warn(`\n${data.rateLimitWarning}\n`);
354
+ }
355
+ const printBucket = (heading, candidates) => {
356
+ if (candidates.length === 0)
357
+ return;
358
+ console.log(`${heading} (${candidates.length}):\n`);
359
+ for (const candidate of candidates) {
360
+ const { issue, recommendation, reasonsToApprove, reasonsToSkip, viabilityScore } = candidate;
361
+ console.log(`[${recommendation.toUpperCase()}] ${issue.repo}#${issue.number}: ${issue.title}`);
362
+ console.log(` URL: ${issue.url}`);
363
+ console.log(` Viability: ${viabilityScore}/100`);
364
+ if (reasonsToApprove.length > 0)
365
+ console.log(` Approve: ${reasonsToApprove.join(', ')}`);
366
+ if (reasonsToSkip.length > 0)
367
+ console.log(` Skip: ${reasonsToSkip.join(', ')}`);
368
+ console.log('---');
369
+ }
370
+ console.log('');
371
+ };
372
+ printBucket('Quick wins', data.quickWins);
373
+ printBucket('Bigger bets', data.biggerBets);
374
+ }, FeaturesOutputSchema);
375
+ });
376
+ },
377
+ },
287
378
  // ── Vet ────────────────────────────────────────────────────────────────
288
379
  {
289
380
  name: 'vet',
@@ -329,6 +420,7 @@ export const commands = [
329
420
  console.log(` Claimed: ${data.summary.claimed}`);
330
421
  console.log(` Closed: ${data.summary.closed}`);
331
422
  console.log(` Has PR: ${data.summary.hasPR}`);
423
+ console.log(` Stalled PR: ${data.summary.hasStalledPR}`);
332
424
  console.log(` Errors: ${data.summary.errors}`);
333
425
  console.log('');
334
426
  for (const result of data.results) {
@@ -337,7 +429,8 @@ export const commands = [
337
429
  : result.listStatus === 'error'
338
430
  ? '\u274c'
339
431
  : '\u26a0\ufe0f';
340
- console.log(`${status} [${result.listStatus}] ${result.issue.repo}#${result.issue.number}: ${result.issue.title}`);
432
+ const annotation = result.listStatus === 'has_stalled_pr' ? ' (stalled PR, revive opportunity)' : '';
433
+ console.log(`${status} [${result.listStatus}] ${result.issue.repo}#${result.issue.number}: ${result.issue.title}${annotation}`);
341
434
  if (result.errorMessage) {
342
435
  console.log(` Error: ${result.errorMessage}`);
343
436
  }