@oss-autopilot/core 1.17.4 → 3.0.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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-registry.js +417 -326
  3. package/dist/cli.bundle.cjs +99 -96
  4. package/dist/commands/daily-render.d.ts +39 -0
  5. package/dist/commands/daily-render.js +189 -0
  6. package/dist/commands/dashboard-data.js +9 -3
  7. package/dist/commands/index.d.ts +4 -8
  8. package/dist/commands/index.js +3 -5
  9. package/dist/commands/list-move-tier.d.ts +46 -0
  10. package/dist/commands/list-move-tier.js +192 -0
  11. package/dist/commands/pr-template.js +2 -1
  12. package/dist/commands/state-cmd.d.ts +10 -1
  13. package/dist/commands/state-cmd.js +22 -3
  14. package/dist/commands/track.d.ts +7 -28
  15. package/dist/commands/track.js +8 -30
  16. package/dist/core/auth.d.ts +50 -0
  17. package/dist/core/auth.js +160 -0
  18. package/dist/core/concurrency.d.ts +7 -0
  19. package/dist/core/concurrency.js +9 -0
  20. package/dist/core/daily-logic.d.ts +10 -42
  21. package/dist/core/daily-logic.js +14 -201
  22. package/dist/core/dates.d.ts +37 -0
  23. package/dist/core/dates.js +60 -0
  24. package/dist/core/errors.d.ts +14 -0
  25. package/dist/core/errors.js +22 -0
  26. package/dist/core/gist-state-store.d.ts +48 -2
  27. package/dist/core/gist-state-store.js +120 -24
  28. package/dist/core/github-stats.js +1 -1
  29. package/dist/core/http-cache.js +1 -1
  30. package/dist/core/index.d.ts +5 -1
  31. package/dist/core/index.js +5 -1
  32. package/dist/core/issue-conversation.js +3 -2
  33. package/dist/core/paths.d.ts +68 -0
  34. package/dist/core/paths.js +106 -0
  35. package/dist/core/pr-monitor.js +3 -1
  36. package/dist/core/repo-score-manager.js +1 -1
  37. package/dist/core/state-persistence.js +1 -1
  38. package/dist/core/state.d.ts +16 -2
  39. package/dist/core/state.js +42 -7
  40. package/dist/core/types.d.ts +57 -0
  41. package/dist/core/urls.d.ts +63 -0
  42. package/dist/core/urls.js +101 -0
  43. package/dist/formatters/json.d.ts +464 -74
  44. package/dist/formatters/json.js +380 -0
  45. package/package.json +3 -3
  46. package/dist/commands/read.d.ts +0 -18
  47. package/dist/commands/read.js +0 -20
  48. package/dist/core/utils.d.ts +0 -303
  49. package/dist/core/utils.js +0 -529
@@ -9,7 +9,7 @@
9
9
  * so that only the invoked command's dependencies are evaluated.
10
10
  */
11
11
  import { errorMessage, resolveErrorCode } from './core/errors.js';
12
- import { outputJson, outputJsonError } from './formatters/json.js';
12
+ import { outputJson, outputJsonError, outputJsonValidated } from './formatters/json.js';
13
13
  /** Shared error handler for CLI action catch blocks. */
14
14
  function handleCommandError(err, json) {
15
15
  const msg = errorMessage(err);
@@ -30,11 +30,26 @@ function handleCommandError(err, json) {
30
30
  * modes (e.g. `--compact`, `--markdown`, `--badge`) inline their own
31
31
  * branching rather than squeezing through this helper.
32
32
  */
33
- async function executeAction(options, run, display) {
33
+ async function executeAction(options, run, display,
34
+ /** Optional Zod schema. When provided, the JSON output is validated against
35
+ * it (#1105). On mismatch, executeAction throws and the standard error
36
+ * envelope fires — surfacing a contract drift instead of silently shipping
37
+ * a broken shape.
38
+ *
39
+ * Typed as `ZodType<unknown>` because validation is a runtime concern: the
40
+ * schema runs `safeParse(data: unknown)`, and Zod-inferred union types do
41
+ * not always structurally match hand-written TS union interfaces. The test
42
+ * harness still catches drift via `safeParse`. */
43
+ schema) {
34
44
  try {
35
45
  const data = await run();
36
46
  if (options.json) {
37
- outputJson(data);
47
+ if (schema) {
48
+ outputJsonValidated(schema, data);
49
+ }
50
+ else {
51
+ outputJson(data);
52
+ }
38
53
  }
39
54
  else {
40
55
  await display(data);
@@ -69,11 +84,12 @@ export const commands = [
69
84
  const { runDaily } = await import('./commands/daily.js');
70
85
  const data = await runDaily();
71
86
  if (options.compact) {
72
- const { toCompactDailyOutput } = await import('./formatters/json.js');
73
- outputJson(toCompactDailyOutput(data));
87
+ const { toCompactDailyOutput, outputJsonValidated, CompactDailyOutputSchema } = await import('./formatters/json.js');
88
+ outputJsonValidated(CompactDailyOutputSchema, toCompactDailyOutput(data));
74
89
  }
75
90
  else {
76
- outputJson(data);
91
+ const { outputJsonValidated, DailyOutputSchema } = await import('./formatters/json.js');
92
+ outputJsonValidated(DailyOutputSchema, data);
77
93
  }
78
94
  }
79
95
  else {
@@ -98,24 +114,27 @@ export const commands = [
98
114
  .description('Show current status and stats')
99
115
  .option('--json', 'Output as JSON')
100
116
  .option('--offline', 'Show cache-freshness metadata (lastUpdated). Status always reads local state — no GitHub API calls are made either way.')
101
- .action((options) => executeAction(options, async () => {
102
- const { runStatus } = await import('./commands/status.js');
103
- return runStatus({ offline: options.offline });
104
- }, (data) => {
105
- console.log('\n\ud83d\udcca OSS Status\n');
106
- console.log(`Merged PRs: ${data.stats.mergedPRs}`);
107
- console.log(`Closed PRs: ${data.stats.closedPRs}`);
108
- console.log(`Merge Rate: ${data.stats.mergeRate}`);
109
- console.log(`Needs Response: ${data.stats.needsResponse}`);
110
- if (data.offline) {
111
- console.log(`\nLast Updated: ${data.lastUpdated || 'Never'}`);
112
- console.log('(Status reads from local state — no GitHub API calls)');
113
- }
114
- else {
115
- console.log(`\nLast Run: ${data.lastRunAt || 'Never'}`);
116
- }
117
- console.log('\nRun with --json for structured output');
118
- }));
117
+ .action(async (options) => {
118
+ const { StatusOutputSchema } = await import('./formatters/json.js');
119
+ await executeAction(options, async () => {
120
+ const { runStatus } = await import('./commands/status.js');
121
+ return runStatus({ offline: options.offline });
122
+ }, (data) => {
123
+ console.log('\n\ud83d\udcca OSS Status\n');
124
+ console.log(`Merged PRs: ${data.stats.mergedPRs}`);
125
+ console.log(`Closed PRs: ${data.stats.closedPRs}`);
126
+ console.log(`Merge Rate: ${data.stats.mergeRate}`);
127
+ console.log(`Needs Response: ${data.stats.needsResponse}`);
128
+ if (data.offline) {
129
+ console.log(`\nLast Updated: ${data.lastUpdated || 'Never'}`);
130
+ console.log('(Status reads from local state — no GitHub API calls)');
131
+ }
132
+ else {
133
+ console.log(`\nLast Run: ${data.lastRunAt || 'Never'}`);
134
+ }
135
+ console.log('\nRun with --json for structured output');
136
+ }, StatusOutputSchema);
137
+ });
119
138
  },
120
139
  },
121
140
  // ── State ──────────────────────────────────────────────────────────────
@@ -124,10 +143,12 @@ export const commands = [
124
143
  register(program) {
125
144
  program
126
145
  .command('state')
127
- .description('Manage state persistence (local/gist)')
146
+ .description('Manage state persistence (local/gist). Gist mode uses ETag-based optimistic concurrency: ' +
147
+ 'a concurrent push from another machine surfaces as a CONCURRENCY error rather than silently overwriting (#1115).')
128
148
  .option('--show', 'Display current persistence mode and Gist ID')
129
149
  .option('--sync', 'Force push state to Gist (no-op if not in Gist mode)')
130
150
  .option('--unlink', 'Switch from Gist back to local persistence')
151
+ .option('--validate', 'When used with --show, also report stored PR entries with unparseable URLs')
131
152
  .option('--json', 'Output as JSON')
132
153
  .action(async (options) => {
133
154
  if (options.unlink) {
@@ -152,13 +173,25 @@ export const commands = [
152
173
  }
153
174
  else {
154
175
  // Default: --show
155
- await executeAction(options, async () => (await import('./commands/state-cmd.js')).runStateShow(), (data) => {
176
+ await executeAction(options, async () => (await import('./commands/state-cmd.js')).runStateShow({ validate: !!options.validate }), (data) => {
156
177
  console.log(`\nPersistence: ${data.persistence}`);
157
178
  if (data.gistId)
158
179
  console.log(`Gist ID: ${data.gistId}`);
159
180
  if (data.gistDegraded)
160
181
  console.log('Status: DEGRADED (using local cache)');
161
- console.log(`Last run: ${data.lastRunAt ?? 'Never'}\n`);
182
+ console.log(`Last run: ${data.lastRunAt ?? 'Never'}`);
183
+ if (data.invalidEntries) {
184
+ if (data.invalidEntries.length === 0) {
185
+ console.log('Validation: no invalid PR URLs in stored state.');
186
+ }
187
+ else {
188
+ console.log(`Validation: ${data.invalidEntries.length} stored PR(s) with invalid URLs:`);
189
+ for (const e of data.invalidEntries) {
190
+ console.log(` [${e.kind}] ${e.url} ${e.title}`);
191
+ }
192
+ }
193
+ }
194
+ console.log('');
162
195
  });
163
196
  }
164
197
  });
@@ -172,50 +205,53 @@ export const commands = [
172
205
  .command('search [count]')
173
206
  .description('Search for new issues to work on')
174
207
  .option('--json', 'Output as JSON')
175
- .action((count, options) => executeAction(options, async () => {
176
- const { runSearch, MAX_SEARCH_RESULTS } = await import('./commands/search.js');
177
- let maxResults = 5;
178
- if (count !== undefined) {
179
- const parsed = Number(count);
180
- if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
181
- throw new Error(`Invalid count "${count}". Must be a positive integer.`);
182
- }
183
- maxResults = parsed;
184
- }
185
- if (maxResults > MAX_SEARCH_RESULTS) {
186
- console.warn(`Capping search to ${MAX_SEARCH_RESULTS} results (requested: ${maxResults})`);
187
- maxResults = MAX_SEARCH_RESULTS;
188
- }
189
- if (!options.json) {
190
- console.log(`\nSearching for issues (max ${maxResults})...\n`);
191
- }
192
- return runSearch({ maxResults });
193
- }, (data) => {
194
- if (data.candidates.length === 0) {
208
+ .action(async (count, options) => {
209
+ const { SearchOutputSchema } = await import('./formatters/json.js');
210
+ await executeAction(options, async () => {
211
+ const { runSearch, MAX_SEARCH_RESULTS } = await import('./commands/search.js');
212
+ let maxResults = 5;
213
+ if (count !== undefined) {
214
+ const parsed = Number(count);
215
+ if (!Number.isFinite(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
216
+ throw new Error(`Invalid count "${count}". Must be a positive integer.`);
217
+ }
218
+ maxResults = parsed;
219
+ }
220
+ if (maxResults > MAX_SEARCH_RESULTS) {
221
+ console.warn(`Capping search to ${MAX_SEARCH_RESULTS} results (requested: ${maxResults})`);
222
+ maxResults = MAX_SEARCH_RESULTS;
223
+ }
224
+ if (!options.json) {
225
+ console.log(`\nSearching for issues (max ${maxResults})...\n`);
226
+ }
227
+ return runSearch({ maxResults });
228
+ }, (data) => {
229
+ if (data.candidates.length === 0) {
230
+ if (data.rateLimitWarning) {
231
+ console.warn(`\n${data.rateLimitWarning}\n`);
232
+ }
233
+ else {
234
+ console.log('No matching issues found.');
235
+ }
236
+ return;
237
+ }
195
238
  if (data.rateLimitWarning) {
196
239
  console.warn(`\n${data.rateLimitWarning}\n`);
197
240
  }
198
- else {
199
- console.log('No matching issues found.');
241
+ console.log(`Found ${data.candidates.length} candidates:\n`);
242
+ for (const candidate of data.candidates) {
243
+ const { issue, recommendation, reasonsToApprove, reasonsToSkip, viabilityScore } = candidate;
244
+ console.log(`[${recommendation.toUpperCase()}] ${issue.repo}#${issue.number}: ${issue.title}`);
245
+ console.log(` URL: ${issue.url}`);
246
+ console.log(` Viability: ${viabilityScore}/100`);
247
+ if (reasonsToApprove.length > 0)
248
+ console.log(` Approve: ${reasonsToApprove.join(', ')}`);
249
+ if (reasonsToSkip.length > 0)
250
+ console.log(` Skip: ${reasonsToSkip.join(', ')}`);
251
+ console.log('---');
200
252
  }
201
- return;
202
- }
203
- if (data.rateLimitWarning) {
204
- console.warn(`\n${data.rateLimitWarning}\n`);
205
- }
206
- console.log(`Found ${data.candidates.length} candidates:\n`);
207
- for (const candidate of data.candidates) {
208
- const { issue, recommendation, reasonsToApprove, reasonsToSkip, viabilityScore } = candidate;
209
- console.log(`[${recommendation.toUpperCase()}] ${issue.repo}#${issue.number}: ${issue.title}`);
210
- console.log(` URL: ${issue.url}`);
211
- console.log(` Viability: ${viabilityScore}/100`);
212
- if (reasonsToApprove.length > 0)
213
- console.log(` Approve: ${reasonsToApprove.join(', ')}`);
214
- if (reasonsToSkip.length > 0)
215
- console.log(` Skip: ${reasonsToSkip.join(', ')}`);
216
- console.log('---');
217
- }
218
- }));
253
+ }, SearchOutputSchema);
254
+ });
219
255
  },
220
256
  },
221
257
  // ── Vet ────────────────────────────────────────────────────────────────
@@ -292,16 +328,57 @@ export const commands = [
292
328
  .description('Append an issue URL to the skipped-issues file (idempotent)')
293
329
  .option('--path <file>', 'Skipped-issues file path (falls back to config.skippedIssuesPath)')
294
330
  .option('--json', 'Output as JSON')
295
- .action((issueUrl, options) => executeAction(options, async () => (await import('./commands/skip-add.js')).runSkipAdd({ issueUrl, skipFilePath: options.path }), (data) => {
296
- if (data.added) {
297
- console.log(`Added to skip list: ${data.url} (${data.date})`);
298
- console.log(` File: ${data.path}`);
299
- }
300
- else {
301
- console.log(`Already on skip list: ${data.url}`);
302
- console.log(` File: ${data.path}`);
303
- }
304
- }));
331
+ .action(async (issueUrl, options) => {
332
+ const { SkipAddOutputSchema } = await import('./formatters/json.js');
333
+ await executeAction(options, async () => (await import('./commands/skip-add.js')).runSkipAdd({ issueUrl, skipFilePath: options.path }), (data) => {
334
+ if (data.added) {
335
+ console.log(`Added to skip list: ${data.url} (${data.date})`);
336
+ console.log(` File: ${data.path}`);
337
+ }
338
+ else {
339
+ console.log(`Already on skip list: ${data.url}`);
340
+ console.log(` File: ${data.path}`);
341
+ }
342
+ }, SkipAddOutputSchema);
343
+ });
344
+ },
345
+ },
346
+ // ── List Move Tier ─────────────────────────────────────────────────────
347
+ {
348
+ name: 'list-move-tier',
349
+ localOnly: true,
350
+ register(program) {
351
+ program
352
+ .command('list-move-tier <issue-url>')
353
+ .description('Move an issue between Pursue / Maybe / Skip sections of a curated list (#1107)')
354
+ .requiredOption('--tier <tier>', 'Target tier: pursue, maybe, or skip')
355
+ .requiredOption('--list-path <file>', 'Path to the markdown issue list')
356
+ .option('--json', 'Output as JSON')
357
+ .action(async (issueUrl, options) => {
358
+ const { ListMoveTierOutputSchema } = await import('./formatters/json.js');
359
+ await executeAction(options, async () => {
360
+ const tier = String(options.tier).toLowerCase();
361
+ if (tier !== 'pursue' && tier !== 'maybe' && tier !== 'skip') {
362
+ throw new Error(`Invalid --tier "${options.tier}". Must be one of: pursue, maybe, skip.`);
363
+ }
364
+ return (await import('./commands/list-move-tier.js')).runListMoveTier({
365
+ issueUrl,
366
+ tier,
367
+ listPath: options.listPath,
368
+ });
369
+ }, (data) => {
370
+ if (data.moved) {
371
+ const fromLabel = data.fromTier ? ` (from ${data.fromTier})` : '';
372
+ const countLabel = data.count > 1 ? ` × ${data.count}` : '';
373
+ console.log(`Moved ${data.url} to ${data.toTier}${fromLabel}${countLabel}`);
374
+ console.log(` File: ${data.filePath}`);
375
+ }
376
+ else {
377
+ console.log(`No move: ${data.url} — ${data.reason ?? 'unchanged'}`);
378
+ console.log(` File: ${data.filePath}`);
379
+ }
380
+ }, ListMoveTierOutputSchema);
381
+ });
305
382
  },
306
383
  },
307
384
  // ── Track ──────────────────────────────────────────────────────────────
@@ -319,37 +396,9 @@ export const commands = [
319
396
  }));
320
397
  },
321
398
  },
322
- // ── Untrack ────────────────────────────────────────────────────────────
323
- {
324
- name: 'untrack',
325
- localOnly: true,
326
- register(program) {
327
- program
328
- .command('untrack <pr-url>')
329
- .description('[DEPRECATED] No-op in v2. Use `shelve` to hide a PR from the daily digest.')
330
- .option('--json', 'Output as JSON')
331
- .action((prUrl, options) => executeAction(options, async () => (await import('./commands/track.js')).runUntrack({ prUrl }), () => {
332
- // Stderr so scripts piping stdout don't see the deprecation notice.
333
- console.error('[DEPRECATED] `untrack` is a no-op in v2. PRs are fetched fresh on each daily run —');
334
- console.error('there is no local tracking list to remove from. Use `shelve` to hide a PR instead.');
335
- }));
336
- },
337
- },
338
- // ── Read ───────────────────────────────────────────────────────────────
339
- {
340
- name: 'read',
341
- localOnly: true,
342
- register(program) {
343
- program
344
- .command('read [pr-url]')
345
- .description('Mark PR comments as read')
346
- .option('--all', 'Mark all PRs as read')
347
- .option('--json', 'Output as JSON')
348
- .action((prUrl, options) => executeAction(options, async () => (await import('./commands/read.js')).runRead({ prUrl, all: options.all }), () => {
349
- console.log('Note: In v2, PR read state is not tracked locally. PRs are fetched fresh on each daily run.');
350
- }));
351
- },
352
- },
399
+ // The v1→v2 `untrack` and `read` stubs were removed in v4 (#1133). Use
400
+ // `shelve`/`unshelve` to hide PRs from the daily digest. Scripts that hard-
401
+ // coded these commands now get an "unknown command" error from commander.
353
402
  // ── Comments ───────────────────────────────────────────────────────────
354
403
  {
355
404
  name: 'comments',
@@ -360,7 +409,7 @@ export const commands = [
360
409
  .option('--bots', 'Include bot comments')
361
410
  .option('--json', 'Output as JSON')
362
411
  .action((prUrl, options) => executeAction(options, async () => (await import('./commands/comments.js')).runComments({ prUrl, showBots: options.bots }), async (data) => {
363
- const { formatRelativeTime } = await import('./core/utils.js');
412
+ const { formatRelativeTime } = await import('./core/dates.js');
364
413
  console.log(`\nFetching comments for: ${prUrl}\n`);
365
414
  console.log(`## ${data.pr.title}\n`);
366
415
  console.log(`**Status:** ${data.pr.state} | **Mergeable:** ${data.pr.mergeable ?? 'checking...'}`);
@@ -415,23 +464,26 @@ export const commands = [
415
464
  .description('Post a comment to a PR or issue')
416
465
  .option('--stdin', 'Read message from stdin')
417
466
  .option('--json', 'Output as JSON')
418
- .action((url, messageParts, options) => executeAction(options, async () => {
419
- let message;
420
- if (options.stdin) {
421
- const chunks = [];
422
- for await (const chunk of process.stdin) {
423
- chunks.push(chunk);
424
- }
425
- message = Buffer.concat(chunks).toString('utf-8').trim();
426
- }
427
- else {
428
- message = messageParts.join(' ');
429
- }
430
- const { runPost } = await import('./commands/comments.js');
431
- return runPost({ url, message });
432
- }, (data) => {
433
- console.log(`Comment posted: ${data.commentUrl}`);
434
- }));
467
+ .action(async (url, messageParts, options) => {
468
+ const { PostOutputSchema } = await import('./formatters/json.js');
469
+ await executeAction(options, async () => {
470
+ let message;
471
+ if (options.stdin) {
472
+ const chunks = [];
473
+ for await (const chunk of process.stdin) {
474
+ chunks.push(chunk);
475
+ }
476
+ message = Buffer.concat(chunks).toString('utf-8').trim();
477
+ }
478
+ else {
479
+ message = messageParts.join(' ');
480
+ }
481
+ const { runPost } = await import('./commands/comments.js');
482
+ return runPost({ url, message });
483
+ }, (data) => {
484
+ console.log(`Comment posted: ${data.commentUrl}`);
485
+ }, PostOutputSchema);
486
+ });
435
487
  },
436
488
  },
437
489
  // ── Claim ──────────────────────────────────────────────────────────────
@@ -442,13 +494,16 @@ export const commands = [
442
494
  .command('claim <issue-url> [message...]')
443
495
  .description('Claim an issue by posting a comment')
444
496
  .option('--json', 'Output as JSON')
445
- .action((issueUrl, messageParts, options) => executeAction(options, async () => {
446
- const { runClaim } = await import('./commands/comments.js');
447
- const message = messageParts.length > 0 ? messageParts.join(' ') : undefined;
448
- return runClaim({ issueUrl, message });
449
- }, (data) => {
450
- console.log(`Issue claimed: ${data.commentUrl}`);
451
- }));
497
+ .action(async (issueUrl, messageParts, options) => {
498
+ const { ClaimOutputSchema } = await import('./formatters/json.js');
499
+ await executeAction(options, async () => {
500
+ const { runClaim } = await import('./commands/comments.js');
501
+ const message = messageParts.length > 0 ? messageParts.join(' ') : undefined;
502
+ return runClaim({ issueUrl, message });
503
+ }, (data) => {
504
+ console.log(`Issue claimed: ${data.commentUrl}`);
505
+ }, ClaimOutputSchema);
506
+ });
452
507
  },
453
508
  },
454
509
  // ── Config ─────────────────────────────────────────────────────────────
@@ -461,28 +516,31 @@ export const commands = [
461
516
  .description('Show or update configuration')
462
517
  .option('--json', 'Output as JSON')
463
518
  .option('--list-keys', 'List every known config key with descriptions')
464
- .action((key, value, options) => executeAction(options, async () => (await import('./commands/config.js')).runConfig({
465
- key,
466
- value,
467
- listKeys: options.listKeys,
468
- }), (data) => {
469
- if ('keys' in data) {
470
- console.log('\nConfig keys\n');
471
- for (const def of data.keys) {
472
- const flag = def.settableVia === 'auto' ? '(auto)' : `[${def.settableVia}]`;
473
- console.log(` ${def.key.padEnd(28)} ${flag.padEnd(10)} ${def.description}`);
474
- console.log(` ${''.padEnd(28)} ${''.padEnd(10)} value: ${def.valueHint}`);
475
- }
476
- console.log('');
477
- }
478
- else if ('config' in data) {
479
- console.log('\n\u2699\ufe0f Current Configuration:\n');
480
- console.log(JSON.stringify(data.config, null, 2));
481
- }
482
- else {
483
- console.log(`Set ${data.key} to: ${data.value}`);
484
- }
485
- }));
519
+ .action(async (key, value, options) => {
520
+ const { ConfigCommandOutputSchema } = await import('./formatters/json.js');
521
+ await executeAction(options, async () => (await import('./commands/config.js')).runConfig({
522
+ key,
523
+ value,
524
+ listKeys: options.listKeys,
525
+ }), (data) => {
526
+ if ('keys' in data) {
527
+ console.log('\nConfig keys\n');
528
+ for (const def of data.keys) {
529
+ const flag = def.settableVia === 'auto' ? '(auto)' : `[${def.settableVia}]`;
530
+ console.log(` ${def.key.padEnd(28)} ${flag.padEnd(10)} ${def.description}`);
531
+ console.log(` ${''.padEnd(28)} ${''.padEnd(10)} value: ${def.valueHint}`);
532
+ }
533
+ console.log('');
534
+ }
535
+ else if ('config' in data) {
536
+ console.log('\n\u2699\ufe0f Current Configuration:\n');
537
+ console.log(JSON.stringify(data.config, null, 2));
538
+ }
539
+ else {
540
+ console.log(`Set ${data.key} to: ${data.value}`);
541
+ }
542
+ }, ConfigCommandOutputSchema);
543
+ });
486
544
  },
487
545
  },
488
546
  // ── Init ───────────────────────────────────────────────────────────────
@@ -493,10 +551,13 @@ export const commands = [
493
551
  .command('init <username>')
494
552
  .description('Initialize with your GitHub username and import open PRs')
495
553
  .option('--json', 'Output as JSON')
496
- .action((username, options) => executeAction(options, async () => (await import('./commands/init.js')).runInit({ username }), (data) => {
497
- console.log(`\nUsername set to @${data.username}.`);
498
- console.log('Run `oss-autopilot daily` to fetch your open PRs from GitHub.');
499
- }));
554
+ .action(async (username, options) => {
555
+ const { InitOutputSchema } = await import('./formatters/json.js');
556
+ await executeAction(options, async () => (await import('./commands/init.js')).runInit({ username }), (data) => {
557
+ console.log(`\nUsername set to @${data.username}.`);
558
+ console.log('Run `oss-autopilot daily` to fetch your open PRs from GitHub.');
559
+ }, InitOutputSchema);
560
+ });
500
561
  },
501
562
  },
502
563
  // ── Setup ──────────────────────────────────────────────────────────────
@@ -510,56 +571,59 @@ export const commands = [
510
571
  .option('--reset', 'Re-run setup even if already complete')
511
572
  .option('--set <settings...>', 'Set specific values (key=value)')
512
573
  .option('--json', 'Output as JSON')
513
- .action((options) => executeAction(options, async () => (await import('./commands/setup.js')).runSetup({ reset: options.reset, set: options.set }), (data) => {
514
- if ('success' in data) {
515
- // --set mode
516
- for (const [key, value] of Object.entries(data.settings)) {
517
- console.log(`\u2713 ${key}: ${value}`);
518
- }
519
- if (data.warnings) {
520
- for (const w of data.warnings) {
521
- console.warn(w);
574
+ .action(async (options) => {
575
+ const { SetupOutputSchema } = await import('./formatters/json.js');
576
+ await executeAction(options, async () => (await import('./commands/setup.js')).runSetup({ reset: options.reset, set: options.set }), (data) => {
577
+ if ('success' in data) {
578
+ // --set mode
579
+ for (const [key, value] of Object.entries(data.settings)) {
580
+ console.log(`\u2713 ${key}: ${value}`);
581
+ }
582
+ if (data.warnings) {
583
+ for (const w of data.warnings) {
584
+ console.warn(w);
585
+ }
522
586
  }
523
587
  }
524
- }
525
- else if ('setupComplete' in data && data.setupComplete) {
526
- // Already complete
527
- console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
528
- console.log('\u2713 Setup already complete!\n');
529
- console.log('Current settings:');
530
- console.log(` GitHub username: ${data.config.githubUsername || '(not set)'}`);
531
- console.log(` Max active PRs: ${data.config.maxActivePRs}`);
532
- console.log(` Dormant threshold: ${data.config.dormantThresholdDays} days`);
533
- console.log(` Approaching dormant: ${data.config.approachingDormantDays} days`);
534
- console.log(` Languages: ${data.config.languages.join(', ')}`);
535
- console.log(` Labels: ${data.config.labels.join(', ')}`);
536
- console.log(`\nRun 'setup --reset' to reconfigure.`);
537
- }
538
- else if ('setupRequired' in data) {
539
- // Needs setup
540
- console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
541
- console.log('SETUP_REQUIRED');
542
- console.log('---');
543
- console.log('Please configure the following settings:\n');
544
- for (const prompt of data.prompts) {
545
- console.log(`SETTING: ${prompt.setting}`);
546
- console.log(`PROMPT: ${prompt.prompt}`);
547
- const currentVal = Array.isArray(prompt.current) ? prompt.current.join(', ') : prompt.current;
548
- console.log(`CURRENT: ${currentVal ?? '(not set)'}`);
549
- if (prompt.required)
550
- console.log('REQUIRED: true');
551
- if (prompt.default !== undefined) {
552
- const defaultVal = Array.isArray(prompt.default) ? prompt.default.join(', ') : prompt.default;
553
- console.log(`DEFAULT: ${defaultVal}`);
588
+ else if ('setupComplete' in data && data.setupComplete) {
589
+ // Already complete
590
+ console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
591
+ console.log('\u2713 Setup already complete!\n');
592
+ console.log('Current settings:');
593
+ console.log(` GitHub username: ${data.config.githubUsername || '(not set)'}`);
594
+ console.log(` Max active PRs: ${data.config.maxActivePRs}`);
595
+ console.log(` Dormant threshold: ${data.config.dormantThresholdDays} days`);
596
+ console.log(` Approaching dormant: ${data.config.approachingDormantDays} days`);
597
+ console.log(` Languages: ${data.config.languages.join(', ')}`);
598
+ console.log(` Labels: ${data.config.labels.join(', ')}`);
599
+ console.log(`\nRun 'setup --reset' to reconfigure.`);
600
+ }
601
+ else if ('setupRequired' in data) {
602
+ // Needs setup
603
+ console.log('\n\u2699\ufe0f OSS Autopilot Setup\n');
604
+ console.log('SETUP_REQUIRED');
605
+ console.log('---');
606
+ console.log('Please configure the following settings:\n');
607
+ for (const prompt of data.prompts) {
608
+ console.log(`SETTING: ${prompt.setting}`);
609
+ console.log(`PROMPT: ${prompt.prompt}`);
610
+ const currentVal = Array.isArray(prompt.current) ? prompt.current.join(', ') : prompt.current;
611
+ console.log(`CURRENT: ${currentVal ?? '(not set)'}`);
612
+ if (prompt.required)
613
+ console.log('REQUIRED: true');
614
+ if (prompt.default !== undefined) {
615
+ const defaultVal = Array.isArray(prompt.default) ? prompt.default.join(', ') : prompt.default;
616
+ console.log(`DEFAULT: ${defaultVal}`);
617
+ }
618
+ if (prompt.type)
619
+ console.log(`TYPE: ${prompt.type}`);
620
+ console.log('');
554
621
  }
555
- if (prompt.type)
556
- console.log(`TYPE: ${prompt.type}`);
557
- console.log('');
622
+ console.log('---');
623
+ console.log('END_SETUP_PROMPTS');
558
624
  }
559
- console.log('---');
560
- console.log('END_SETUP_PROMPTS');
561
- }
562
- }));
625
+ }, SetupOutputSchema);
626
+ });
563
627
  },
564
628
  },
565
629
  // ── Check Setup ────────────────────────────────────────────────────────
@@ -571,15 +635,18 @@ export const commands = [
571
635
  .command('checkSetup')
572
636
  .description('Check if setup is complete')
573
637
  .option('--json', 'Output as JSON')
574
- .action((options) => executeAction(options, async () => (await import('./commands/setup.js')).runCheckSetup(), (data) => {
575
- if (data.setupComplete) {
576
- console.log('SETUP_COMPLETE');
577
- console.log(`username=${data.username}`);
578
- }
579
- else {
580
- console.log('SETUP_INCOMPLETE');
581
- }
582
- }));
638
+ .action(async (options) => {
639
+ const { CheckSetupOutputSchema } = await import('./formatters/json.js');
640
+ await executeAction(options, async () => (await import('./commands/setup.js')).runCheckSetup(), (data) => {
641
+ if (data.setupComplete) {
642
+ console.log('SETUP_COMPLETE');
643
+ console.log(`username=${data.username}`);
644
+ }
645
+ else {
646
+ console.log('SETUP_INCOMPLETE');
647
+ }
648
+ }, CheckSetupOutputSchema);
649
+ });
583
650
  },
584
651
  },
585
652
  // ── Dashboard Serve ────────────────────────────────────────────────────
@@ -618,24 +685,27 @@ export const commands = [
618
685
  .command('parse-issue-list <path>')
619
686
  .description('Parse a markdown issue list into structured JSON')
620
687
  .option('--json', 'Output as JSON')
621
- .action((filePath, options) => executeAction(options, async () => (await import('./commands/parse-list.js')).runParseList({ filePath }), async (data) => {
622
- const path = await import('path');
623
- const resolvedPath = path.resolve(filePath);
624
- console.log(`\n\ud83d\udccb Issue List: ${resolvedPath}\n`);
625
- console.log(`Available: ${data.availableCount} | Completed: ${data.completedCount}\n`);
626
- if (data.available.length > 0) {
627
- console.log('--- Available ---');
628
- for (const item of data.available) {
629
- console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
688
+ .action(async (filePath, options) => {
689
+ const { ParseIssueListOutputSchema } = await import('./formatters/json.js');
690
+ await executeAction(options, async () => (await import('./commands/parse-list.js')).runParseList({ filePath }), async (data) => {
691
+ const path = await import('path');
692
+ const resolvedPath = path.resolve(filePath);
693
+ console.log(`\n\ud83d\udccb Issue List: ${resolvedPath}\n`);
694
+ console.log(`Available: ${data.availableCount} | Completed: ${data.completedCount}\n`);
695
+ if (data.available.length > 0) {
696
+ console.log('--- Available ---');
697
+ for (const item of data.available) {
698
+ console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
699
+ }
630
700
  }
631
- }
632
- if (data.completed.length > 0) {
633
- console.log('\n--- Completed ---');
634
- for (const item of data.completed) {
635
- console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
701
+ if (data.completed.length > 0) {
702
+ console.log('\n--- Completed ---');
703
+ for (const item of data.completed) {
704
+ console.log(` [${item.tier}] ${item.repo}#${item.number}: ${item.title}`);
705
+ }
636
706
  }
637
- }
638
- }));
707
+ }, ParseIssueListOutputSchema);
708
+ });
639
709
  },
640
710
  },
641
711
  // ── Check Integration ──────────────────────────────────────────────────
@@ -649,27 +719,30 @@ export const commands = [
649
719
  .description('Detect new files on this branch that no other file references')
650
720
  .option('--base <branch>', 'Base branch to compare against', 'main')
651
721
  .option('--json', 'Output as JSON')
652
- .action((options) => executeAction(options, async () => (await import('./commands/check-integration.js')).runCheckIntegration({ base: options.base }), (data) => {
653
- if (data.newFiles.length === 0) {
654
- console.log('\nNo new code files to check.');
655
- return;
656
- }
657
- console.log(`\n\ud83d\udd0d Integration Check (base: ${options.base})\n`);
658
- console.log(`New files: ${data.newFiles.length} | Unreferenced: ${data.unreferencedCount}\n`);
659
- for (const file of data.newFiles) {
660
- const status = file.isIntegrated ? '\u2705' : '\u26a0\ufe0f';
661
- console.log(`${status} ${file.path}`);
662
- if (file.isIntegrated) {
663
- console.log(` Referenced by: ${file.referencedBy.join(', ')}`);
722
+ .action(async (options) => {
723
+ const { CheckIntegrationOutputSchema } = await import('./formatters/json.js');
724
+ await executeAction(options, async () => (await import('./commands/check-integration.js')).runCheckIntegration({ base: options.base }), (data) => {
725
+ if (data.newFiles.length === 0) {
726
+ console.log('\nNo new code files to check.');
727
+ return;
664
728
  }
665
- else {
666
- console.log(' Not referenced by any file');
667
- if (file.suggestedEntryPoints && file.suggestedEntryPoints.length > 0) {
668
- console.log(` Suggested entry points: ${file.suggestedEntryPoints.join(', ')}`);
729
+ console.log(`\n\ud83d\udd0d Integration Check (base: ${options.base})\n`);
730
+ console.log(`New files: ${data.newFiles.length} | Unreferenced: ${data.unreferencedCount}\n`);
731
+ for (const file of data.newFiles) {
732
+ const status = file.isIntegrated ? '\u2705' : '\u26a0\ufe0f';
733
+ console.log(`${status} ${file.path}`);
734
+ if (file.isIntegrated) {
735
+ console.log(` Referenced by: ${file.referencedBy.join(', ')}`);
736
+ }
737
+ else {
738
+ console.log(' Not referenced by any file');
739
+ if (file.suggestedEntryPoints && file.suggestedEntryPoints.length > 0) {
740
+ console.log(` Suggested entry points: ${file.suggestedEntryPoints.join(', ')}`);
741
+ }
669
742
  }
670
743
  }
671
- }
672
- }));
744
+ }, CheckIntegrationOutputSchema);
745
+ });
673
746
  },
674
747
  },
675
748
  // ── Doctor ─────────────────────────────────────────────────────────────
@@ -681,17 +754,20 @@ export const commands = [
681
754
  .command('doctor')
682
755
  .description('Run a system-health diagnostic (token, bundle, state, scout, rate limit)')
683
756
  .option('--json', 'Output as JSON')
684
- .action((options) => executeAction(options, async () => (await import('./commands/doctor.js')).runDoctor(), (data) => {
685
- console.log('\nSystem health\n');
686
- for (const check of data.checks) {
687
- const icon = check.status === 'ok' ? '[OK] ' : check.status === 'warning' ? '[WARN] ' : '[ERR] ';
688
- console.log(`${icon}${check.name}: ${check.message}`);
689
- if (check.remediation) {
690
- console.log(` ↳ ${check.remediation}`);
757
+ .action(async (options) => {
758
+ const { DoctorOutputSchema } = await import('./formatters/json.js');
759
+ await executeAction(options, async () => (await import('./commands/doctor.js')).runDoctor(), (data) => {
760
+ console.log('\nSystem health\n');
761
+ for (const check of data.checks) {
762
+ const icon = check.status === 'ok' ? '[OK] ' : check.status === 'warning' ? '[WARN] ' : '[ERR] ';
763
+ console.log(`${icon}${check.name}: ${check.message}`);
764
+ if (check.remediation) {
765
+ console.log(` ↳ ${check.remediation}`);
766
+ }
691
767
  }
692
- }
693
- console.log(`\n${data.summary.ok} ok / ${data.summary.warnings} warning / ${data.summary.errors} error\n`);
694
- }));
768
+ console.log(`\n${data.summary.ok} ok / ${data.summary.warnings} warning / ${data.summary.errors} error\n`);
769
+ }, DoctorOutputSchema);
770
+ });
695
771
  },
696
772
  },
697
773
  // ── Local Repos ────────────────────────────────────────────────────────
@@ -705,19 +781,22 @@ export const commands = [
705
781
  .option('--scan', 'Force re-scan (ignores cache)')
706
782
  .option('--paths <dirs...>', 'Directories to scan')
707
783
  .option('--json', 'Output as JSON')
708
- .action((options) => executeAction(options, async () => (await import('./commands/local-repos.js')).runLocalRepos({
709
- scan: options.scan,
710
- paths: options.paths,
711
- }), (data) => {
712
- if (data.fromCache) {
713
- console.log(`\n\ud83d\udcc1 Local Repos (cached ${data.cachedAt})\n`);
714
- printRepos(data.repos);
715
- }
716
- else {
717
- console.log(`Found ${Object.keys(data.repos).length} repos:\n`);
718
- printRepos(data.repos);
719
- }
720
- }));
784
+ .action(async (options) => {
785
+ const { LocalReposOutputSchema } = await import('./formatters/json.js');
786
+ await executeAction(options, async () => (await import('./commands/local-repos.js')).runLocalRepos({
787
+ scan: options.scan,
788
+ paths: options.paths,
789
+ }), (data) => {
790
+ if (data.fromCache) {
791
+ console.log(`\n\ud83d\udcc1 Local Repos (cached ${data.cachedAt})\n`);
792
+ printRepos(data.repos);
793
+ }
794
+ else {
795
+ console.log(`Found ${Object.keys(data.repos).length} repos:\n`);
796
+ printRepos(data.repos);
797
+ }
798
+ }, LocalReposOutputSchema);
799
+ });
721
800
  },
722
801
  },
723
802
  // ── Startup ────────────────────────────────────────────────────────────
@@ -859,17 +938,20 @@ export const commands = [
859
938
  .command('override <pr-url> <status>')
860
939
  .description('Manually override PR status (needs_addressing or waiting_on_maintainer)')
861
940
  .option('--json', 'Output as JSON')
862
- .action((prUrl, status, options) => executeAction(options, async () => {
863
- const validStatuses = ['needs_addressing', 'waiting_on_maintainer'];
864
- if (!validStatuses.includes(status)) {
865
- throw new Error(`Invalid status "${status}". Must be one of: ${validStatuses.join(', ')}`);
866
- }
867
- const target = status === 'needs_addressing' ? 'attention' : 'waiting';
868
- const { runMove } = await import('./commands/move.js');
869
- return runMove({ prUrl, target });
870
- }, (data) => {
871
- console.log(data.description);
872
- }));
941
+ .action(async (prUrl, status, options) => {
942
+ const { MoveOutputSchema } = await import('./formatters/json.js');
943
+ await executeAction(options, async () => {
944
+ const validStatuses = ['needs_addressing', 'waiting_on_maintainer'];
945
+ if (!validStatuses.includes(status)) {
946
+ throw new Error(`Invalid status "${status}". Must be one of: ${validStatuses.join(', ')}`);
947
+ }
948
+ const target = status === 'needs_addressing' ? 'attention' : 'waiting';
949
+ const { runMove } = await import('./commands/move.js');
950
+ return runMove({ prUrl, target });
951
+ }, (data) => {
952
+ console.log(data.description);
953
+ }, MoveOutputSchema);
954
+ });
873
955
  },
874
956
  },
875
957
  // ── Clear Override ────────────────────────────────────────────────────
@@ -881,9 +963,12 @@ export const commands = [
881
963
  .command('clear-override <pr-url>')
882
964
  .description('Clear a manual status override for a PR')
883
965
  .option('--json', 'Output as JSON')
884
- .action((prUrl, options) => executeAction(options, async () => (await import('./commands/move.js')).runMove({ prUrl, target: 'auto' }), (data) => {
885
- console.log(data.description);
886
- }));
966
+ .action(async (prUrl, options) => {
967
+ const { MoveOutputSchema } = await import('./formatters/json.js');
968
+ await executeAction(options, async () => (await import('./commands/move.js')).runMove({ prUrl, target: 'auto' }), (data) => {
969
+ console.log(data.description);
970
+ }, MoveOutputSchema);
971
+ });
887
972
  },
888
973
  },
889
974
  // ── PR Template ──────────────────────────────────────────────────────
@@ -894,18 +979,21 @@ export const commands = [
894
979
  .command('pr-template <repo>')
895
980
  .description("Fetch a repository's PR description template")
896
981
  .option('--json', 'Output as JSON')
897
- .action((repo, options) => executeAction(options, async () => (await import('./commands/pr-template.js')).runPRTemplate({ repo }), (data) => {
898
- if (data.template) {
899
- console.log(`\nPR template found at: ${data.source}\n`);
900
- console.log(data.template);
901
- }
902
- else if (data.error) {
903
- console.error(`\nWarning: Could not check for PR template: ${data.error}`);
904
- }
905
- else {
906
- console.log('\nNo PR template found for this repository.');
907
- }
908
- }));
982
+ .action(async (repo, options) => {
983
+ const { PRTemplateOutputSchema } = await import('./formatters/json.js');
984
+ await executeAction(options, async () => (await import('./commands/pr-template.js')).runPRTemplate({ repo }), (data) => {
985
+ if (data.template) {
986
+ console.log(`\nPR template found at: ${data.source}\n`);
987
+ console.log(data.template);
988
+ }
989
+ else if (data.error) {
990
+ console.error(`\nWarning: Could not check for PR template: ${data.error}`);
991
+ }
992
+ else {
993
+ console.log('\nNo PR template found for this repository.');
994
+ }
995
+ }, PRTemplateOutputSchema);
996
+ });
909
997
  },
910
998
  },
911
999
  // ── Detect Formatters ────────────────────────────────────────────────
@@ -918,39 +1006,42 @@ export const commands = [
918
1006
  .description('Detect formatters and linters configured in a repository')
919
1007
  .option('--ci-log <path>', 'Analyze CI log file for formatting failures')
920
1008
  .option('--json', 'Output as JSON')
921
- .action((repoPath, options) => executeAction(options, async () => (await import('./commands/detect-formatters.js')).runDetectFormatters({
922
- repoPath,
923
- ciLog: options.ciLog,
924
- }), (data) => {
925
- if (data.formatters.length === 0) {
926
- console.log('\nNo formatters detected.');
927
- }
928
- else {
929
- console.log(`\nDetected ${data.formatters.length} formatter(s):\n`);
930
- for (const f of data.formatters) {
931
- console.log(` ${f.name} (${f.configPath})`);
932
- console.log(` Fix: ${f.fixCommand}`);
933
- console.log(` Check: ${f.checkCommand}`);
1009
+ .action(async (repoPath, options) => {
1010
+ const { DetectFormattersOutputSchema } = await import('./formatters/json.js');
1011
+ await executeAction(options, async () => (await import('./commands/detect-formatters.js')).runDetectFormatters({
1012
+ repoPath,
1013
+ ciLog: options.ciLog,
1014
+ }), (data) => {
1015
+ if (data.formatters.length === 0) {
1016
+ console.log('\nNo formatters detected.');
934
1017
  }
935
- }
936
- if (data.packageJsonScripts.length > 0) {
937
- console.log('\npackage.json scripts:');
938
- for (const s of data.packageJsonScripts) {
939
- console.log(` ${s.name}: ${s.command}`);
1018
+ else {
1019
+ console.log(`\nDetected ${data.formatters.length} formatter(s):\n`);
1020
+ for (const f of data.formatters) {
1021
+ console.log(` ${f.name} (${f.configPath})`);
1022
+ console.log(` Fix: ${f.fixCommand}`);
1023
+ console.log(` Check: ${f.checkCommand}`);
1024
+ }
940
1025
  }
941
- }
942
- if (data.ciDiagnosis) {
943
- console.log('');
944
- if (data.ciDiagnosis.isFormattingFailure) {
945
- console.log(`CI Diagnosis: Formatting failure detected (${data.ciDiagnosis.formatter})`);
946
- console.log(` Fix: ${data.ciDiagnosis.fixCommand}`);
947
- console.log(` Evidence: ${data.ciDiagnosis.evidence.join(', ')}`);
1026
+ if (data.packageJsonScripts.length > 0) {
1027
+ console.log('\npackage.json scripts:');
1028
+ for (const s of data.packageJsonScripts) {
1029
+ console.log(` ${s.name}: ${s.command}`);
1030
+ }
948
1031
  }
949
- else {
950
- console.log('CI Diagnosis: No formatting failure detected.');
1032
+ if (data.ciDiagnosis) {
1033
+ console.log('');
1034
+ if (data.ciDiagnosis.isFormattingFailure) {
1035
+ console.log(`CI Diagnosis: Formatting failure detected (${data.ciDiagnosis.formatter})`);
1036
+ console.log(` Fix: ${data.ciDiagnosis.fixCommand}`);
1037
+ console.log(` Evidence: ${data.ciDiagnosis.evidence.join(', ')}`);
1038
+ }
1039
+ else {
1040
+ console.log('CI Diagnosis: No formatting failure detected.');
1041
+ }
951
1042
  }
952
- }
953
- }));
1043
+ }, DetectFormattersOutputSchema);
1044
+ });
954
1045
  },
955
1046
  },
956
1047
  // ── Stats ─────────────────────────────────────────────────────────────