@open330/oac 2026.2.5

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 (56) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/LICENSE +21 -0
  3. package/README.md +597 -0
  4. package/dist/budget/index.d.ts +117 -0
  5. package/dist/budget/index.js +23 -0
  6. package/dist/budget/index.js.map +1 -0
  7. package/dist/chunk-4IUL7ECC.js +3152 -0
  8. package/dist/chunk-4IUL7ECC.js.map +1 -0
  9. package/dist/chunk-5GAUWC3L.js +469 -0
  10. package/dist/chunk-5GAUWC3L.js.map +1 -0
  11. package/dist/chunk-6A37SKAJ.js +58 -0
  12. package/dist/chunk-6A37SKAJ.js.map +1 -0
  13. package/dist/chunk-7C7SC4TZ.js +358 -0
  14. package/dist/chunk-7C7SC4TZ.js.map +1 -0
  15. package/dist/chunk-CJAJ4MBO.js +475 -0
  16. package/dist/chunk-CJAJ4MBO.js.map +1 -0
  17. package/dist/chunk-LQC5DLT7.js +317 -0
  18. package/dist/chunk-LQC5DLT7.js.map +1 -0
  19. package/dist/chunk-OTPXGXO7.js +2368 -0
  20. package/dist/chunk-OTPXGXO7.js.map +1 -0
  21. package/dist/chunk-QPVNC7S4.js +1833 -0
  22. package/dist/chunk-QPVNC7S4.js.map +1 -0
  23. package/dist/cli/cli.d.ts +13 -0
  24. package/dist/cli/cli.js +16 -0
  25. package/dist/cli/cli.js.map +1 -0
  26. package/dist/cli/index.d.ts +1 -0
  27. package/dist/cli/index.js +22 -0
  28. package/dist/cli/index.js.map +1 -0
  29. package/dist/completion/index.d.ts +91 -0
  30. package/dist/completion/index.js +587 -0
  31. package/dist/completion/index.js.map +1 -0
  32. package/dist/config-DequKoFA.d.ts +1468 -0
  33. package/dist/core/index.d.ts +64 -0
  34. package/dist/core/index.js +87 -0
  35. package/dist/core/index.js.map +1 -0
  36. package/dist/dashboard/index.d.ts +14 -0
  37. package/dist/dashboard/index.js +1253 -0
  38. package/dist/dashboard/index.js.map +1 -0
  39. package/dist/discovery/index.d.ts +285 -0
  40. package/dist/discovery/index.js +50 -0
  41. package/dist/discovery/index.js.map +1 -0
  42. package/dist/event-bus-KiuR6e3P.d.ts +91 -0
  43. package/dist/execution/index.d.ts +215 -0
  44. package/dist/execution/index.js +27 -0
  45. package/dist/execution/index.js.map +1 -0
  46. package/dist/repo/index.d.ts +33 -0
  47. package/dist/repo/index.js +19 -0
  48. package/dist/repo/index.js.map +1 -0
  49. package/dist/tracking/index.d.ts +357 -0
  50. package/dist/tracking/index.js +15 -0
  51. package/dist/tracking/index.js.map +1 -0
  52. package/dist/types-CYCwgojB.d.ts +34 -0
  53. package/dist/types-Ck7IucqK.d.ts +195 -0
  54. package/docs/config-reference.md +271 -0
  55. package/docs/multi-agent-support-technical-spec.md +312 -0
  56. package/package.json +82 -0
@@ -0,0 +1,3152 @@
1
+ import {
2
+ cloneRepo,
3
+ resolveRepo
4
+ } from "./chunk-CJAJ4MBO.js";
5
+ import {
6
+ CompositeScanner,
7
+ GitHubIssuesScanner,
8
+ LintScanner,
9
+ TestGapScanner,
10
+ TodoScanner,
11
+ analyzeCodebase,
12
+ buildScanners,
13
+ createBacklog,
14
+ getPendingEpics,
15
+ groupFindingsIntoEpics,
16
+ isContextStale,
17
+ loadBacklog,
18
+ loadContext,
19
+ persistBacklog,
20
+ persistContext,
21
+ rankTasks,
22
+ updateBacklog
23
+ } from "./chunk-OTPXGXO7.js";
24
+ import {
25
+ buildEpicExecutionPlan,
26
+ buildExecutionPlan,
27
+ estimateEpicTokens,
28
+ estimateTokens
29
+ } from "./chunk-5GAUWC3L.js";
30
+ import {
31
+ adapterRegistry,
32
+ createSandbox,
33
+ epicAsTask,
34
+ executeTask
35
+ } from "./chunk-QPVNC7S4.js";
36
+ import {
37
+ UNLIMITED_BUDGET,
38
+ createEventBus,
39
+ loadConfig
40
+ } from "./chunk-7C7SC4TZ.js";
41
+ import {
42
+ isRecord,
43
+ truncate
44
+ } from "./chunk-6A37SKAJ.js";
45
+ import {
46
+ contributionLogSchema,
47
+ writeContributionLog
48
+ } from "./chunk-LQC5DLT7.js";
49
+
50
+ // src/cli/cli.ts
51
+ import { Command as Command13 } from "commander";
52
+
53
+ // src/cli/commands/analyze.ts
54
+ import Table from "cli-table3";
55
+ import { Command as Command2 } from "commander";
56
+
57
+ // src/cli/github-auth.ts
58
+ import { execFileSync, spawnSync } from "child_process";
59
+ function ensureGitHubAuth() {
60
+ const githubToken = process.env.GITHUB_TOKEN?.trim();
61
+ if (githubToken) {
62
+ process.env.GITHUB_TOKEN = githubToken;
63
+ return githubToken;
64
+ }
65
+ const ghToken = process.env.GH_TOKEN?.trim();
66
+ if (ghToken) {
67
+ process.env.GITHUB_TOKEN = ghToken;
68
+ return ghToken;
69
+ }
70
+ try {
71
+ const token = execFileSync("gh", ["auth", "token"], {
72
+ timeout: 5e3,
73
+ encoding: "utf-8",
74
+ stdio: ["ignore", "pipe", "ignore"]
75
+ }).trim();
76
+ if (token.length > 0) {
77
+ process.env.GITHUB_TOKEN = token;
78
+ return token;
79
+ }
80
+ } catch {
81
+ }
82
+ return void 0;
83
+ }
84
+ function checkGitHubScopes(required = ["repo"]) {
85
+ try {
86
+ const result = spawnSync("gh", ["auth", "status"], {
87
+ timeout: 5e3,
88
+ encoding: "utf-8",
89
+ stdio: ["ignore", "pipe", "pipe"]
90
+ });
91
+ const combined = `${result.stdout ?? ""}${result.stderr ?? ""}`;
92
+ const scopeLine = combined.match(/Token scopes:\s*(.+)/);
93
+ if (!scopeLine) return [];
94
+ const scopes = scopeLine[1].split(",").map((s) => s.trim().replace(/^'|'$/g, ""));
95
+ return required.filter((r) => !scopes.includes(r));
96
+ } catch {
97
+ return [];
98
+ }
99
+ }
100
+
101
+ // src/cli/helpers.ts
102
+ import chalk, { Chalk } from "chalk";
103
+ import "commander";
104
+ import ora from "ora";
105
+ import PQueue from "p-queue";
106
+
107
+ // src/cli/config-loader.ts
108
+ import { constants as fsConstants } from "fs";
109
+ import { access, readFile } from "fs/promises";
110
+ import { resolve } from "path";
111
+ import { pathToFileURL } from "url";
112
+ var DEFINE_CONFIG_IMPORT = /@(?:open330\/oac(?:-core)?|oac\/core)/;
113
+ var DEFINE_CONFIG_IMPORT_LINE = /^\s*import\s*\{\s*defineConfig\s*\}\s*from\s*["']@(?:open330\/oac(?:-core)?|oac\/core)["'];\s*$/m;
114
+ var LEGACY_DEFINE_CONFIG_EXPORT = /export\s+default\s+defineConfig\s*\(/;
115
+ async function loadOptionalConfigFile(configPath, options = {}) {
116
+ const absolutePath = resolve(options.cwd ?? process.cwd(), configPath);
117
+ if (!await pathExists(absolutePath)) {
118
+ return null;
119
+ }
120
+ try {
121
+ const candidate = await importConfigCandidate(absolutePath);
122
+ return loadConfig(candidate);
123
+ } catch (error) {
124
+ options.onWarning?.(
125
+ `Failed to load config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`
126
+ );
127
+ return null;
128
+ }
129
+ }
130
+ async function importConfigCandidate(absolutePath) {
131
+ try {
132
+ return await importCandidate(`${pathToFileURL(absolutePath).href}?t=${Date.now()}`);
133
+ } catch (error) {
134
+ const fallbackCandidate = await importLegacyDefineConfigCandidate(absolutePath, error);
135
+ if (fallbackCandidate !== null) {
136
+ return fallbackCandidate;
137
+ }
138
+ throw error;
139
+ }
140
+ }
141
+ async function importLegacyDefineConfigCandidate(absolutePath, error) {
142
+ if (!shouldTryLegacyDefineConfigFallback(error)) {
143
+ return null;
144
+ }
145
+ const source = await readFile(absolutePath, "utf8");
146
+ const transformed = transformLegacyDefineConfigSource(source);
147
+ if (transformed === null) {
148
+ return null;
149
+ }
150
+ const encodedSource = Buffer.from(transformed, "utf8").toString("base64");
151
+ return await importCandidate(`data:text/javascript;base64,${encodedSource}`);
152
+ }
153
+ function shouldTryLegacyDefineConfigFallback(error) {
154
+ if (!(error instanceof Error)) {
155
+ return false;
156
+ }
157
+ return DEFINE_CONFIG_IMPORT.test(error.message);
158
+ }
159
+ function transformLegacyDefineConfigSource(source) {
160
+ if (!DEFINE_CONFIG_IMPORT_LINE.test(source)) {
161
+ return null;
162
+ }
163
+ if (!LEGACY_DEFINE_CONFIG_EXPORT.test(source)) {
164
+ return null;
165
+ }
166
+ return source.replace(DEFINE_CONFIG_IMPORT_LINE, "").replace(LEGACY_DEFINE_CONFIG_EXPORT, "export default (");
167
+ }
168
+ async function importCandidate(moduleSpecifier) {
169
+ const imported = await import(moduleSpecifier);
170
+ return imported.default ?? imported.config ?? imported;
171
+ }
172
+ async function pathExists(path) {
173
+ try {
174
+ await access(path, fsConstants.F_OK);
175
+ return true;
176
+ } catch {
177
+ return false;
178
+ }
179
+ }
180
+
181
+ // src/cli/helpers.ts
182
+ function getGlobalOptions(command) {
183
+ const options = command.optsWithGlobals();
184
+ return {
185
+ config: options.config ?? "oac.config.ts",
186
+ verbose: options.verbose === true,
187
+ quiet: options.quiet === true,
188
+ json: options.json === true,
189
+ color: options.color !== false
190
+ };
191
+ }
192
+ function createUi(options) {
193
+ const noColorEnv = Object.prototype.hasOwnProperty.call(process.env, "NO_COLOR");
194
+ const colorEnabled = options.color && !noColorEnv;
195
+ return new Chalk({ level: colorEnabled ? chalk.level : 0 });
196
+ }
197
+ function createSpinner(suppress, text) {
198
+ if (suppress) {
199
+ return null;
200
+ }
201
+ return ora({ text, color: "blue" }).start();
202
+ }
203
+ function parseInteger(value) {
204
+ const parsed = Number.parseInt(value, 10);
205
+ if (!Number.isFinite(parsed)) {
206
+ throw new Error(`Expected an integer but received "${value}".`);
207
+ }
208
+ return parsed;
209
+ }
210
+ function formatInteger(value) {
211
+ return new Intl.NumberFormat("en-US").format(value);
212
+ }
213
+ async function loadOptionalConfig(configPath, verbose, ui) {
214
+ return loadOptionalConfigFile(configPath, {
215
+ onWarning: verbose ? (message) => {
216
+ console.warn(ui.yellow(`[oac] ${message}`));
217
+ } : void 0
218
+ });
219
+ }
220
+ function resolveRepoInput(repoOption, config) {
221
+ const fromFlag = repoOption?.trim();
222
+ if (fromFlag) {
223
+ return fromFlag;
224
+ }
225
+ const firstConfiguredRepo = config?.repos[0];
226
+ if (typeof firstConfiguredRepo === "string") {
227
+ return firstConfiguredRepo;
228
+ }
229
+ if (firstConfiguredRepo && typeof firstConfiguredRepo === "object" && "name" in firstConfiguredRepo && typeof firstConfiguredRepo.name === "string") {
230
+ return firstConfiguredRepo.name;
231
+ }
232
+ throw new Error(
233
+ "No repository specified.\n\n Quick start: oac run --repo owner/repo\n With config: oac init (creates oac.config.ts, then just run `oac run`)\n"
234
+ );
235
+ }
236
+ function resolveProviderId(providerOption, config) {
237
+ const fromFlag = providerOption?.trim();
238
+ if (fromFlag) {
239
+ return fromFlag;
240
+ }
241
+ return config?.provider.id ?? "claude-code";
242
+ }
243
+ function resolveBudget(tokensOption, config) {
244
+ const budget = tokensOption ?? config?.budget.totalTokens ?? 1e5;
245
+ if (!Number.isFinite(budget) || budget <= 0) {
246
+ throw new Error("Token budget must be a positive number.");
247
+ }
248
+ return Math.floor(budget);
249
+ }
250
+ async function estimateTaskMap(tasks, providerId, onProgress) {
251
+ let completed = 0;
252
+ const total = tasks.length;
253
+ const queue = new PQueue({ concurrency: 10 });
254
+ const entries = await Promise.all(
255
+ tasks.map(
256
+ (task) => queue.add(async () => {
257
+ const estimate = await estimateTokens(task, providerId);
258
+ completed += 1;
259
+ onProgress?.(completed, total);
260
+ return [task.id, estimate];
261
+ })
262
+ )
263
+ );
264
+ return new Map(entries);
265
+ }
266
+
267
+ // src/cli/commands/analyze.ts
268
+ function createAnalyzeCommand() {
269
+ const command = new Command2("analyze");
270
+ command.description(
271
+ "Deep codebase analysis \u2014 build module graph and group findings into epics"
272
+ ).option("--repo <owner/repo>", "Target repository (owner/repo or GitHub URL)").option("--force", "Force re-analysis even if context is fresh", false).option("--format <format>", "Output format: table|json", "table").action(async (options, cmd) => {
273
+ const globalOptions = getGlobalOptions(cmd);
274
+ const ui = createUi(globalOptions);
275
+ const outputJson = globalOptions.json || normalizeOutputFormat(options.format) === "json";
276
+ const config = await loadOptionalConfig(globalOptions.config, globalOptions.verbose, ui);
277
+ const repoInput = resolveRepoInput(options.repo, config);
278
+ const ghToken = ensureGitHubAuth();
279
+ const resolveSpinner = createSpinner(outputJson, "Resolving repository...");
280
+ const resolvedRepo = await resolveRepo(repoInput);
281
+ resolveSpinner?.succeed(`Resolved ${resolvedRepo.fullName}`);
282
+ const cloneSpinner = createSpinner(outputJson, "Preparing local clone...");
283
+ await cloneRepo(resolvedRepo);
284
+ cloneSpinner?.succeed(`Repository ready at ${resolvedRepo.localPath}`);
285
+ const scanners = buildScannerList(config, Boolean(ghToken));
286
+ const analyzeSpinner = createSpinner(outputJson, "Analyzing codebase...");
287
+ const { codebaseMap, qualityReport } = await analyzeCodebase(resolvedRepo.localPath, {
288
+ scanners,
289
+ repoFullName: resolvedRepo.fullName,
290
+ headSha: resolvedRepo.git.headSha,
291
+ exclude: config?.discovery.exclude
292
+ });
293
+ analyzeSpinner?.succeed(
294
+ `Analyzed ${codebaseMap.modules.length} modules, ${codebaseMap.totalFiles} files, ${qualityReport.findings.length} findings`
295
+ );
296
+ const groupSpinner = createSpinner(outputJson, "Grouping findings into epics...");
297
+ const epics = groupFindingsIntoEpics(qualityReport.findings, { codebaseMap });
298
+ groupSpinner?.succeed(`Created ${epics.length} epic(s)`);
299
+ const contextDir = config?.analyze?.contextDir ?? ".oac/context";
300
+ const persistSpinner = createSpinner(outputJson, "Persisting context...");
301
+ await persistContext(resolvedRepo.localPath, codebaseMap, qualityReport, contextDir);
302
+ const backlog = createBacklog(resolvedRepo.fullName, resolvedRepo.git.headSha, epics);
303
+ await persistBacklog(resolvedRepo.localPath, backlog, contextDir);
304
+ persistSpinner?.succeed(`Context persisted to ${contextDir}/`);
305
+ if (outputJson) {
306
+ console.log(
307
+ JSON.stringify(
308
+ {
309
+ repo: resolvedRepo.fullName,
310
+ modules: codebaseMap.modules.length,
311
+ totalFiles: codebaseMap.totalFiles,
312
+ totalLoc: codebaseMap.totalLoc,
313
+ findings: qualityReport.summary,
314
+ epics: epics.map((e) => ({
315
+ id: e.id,
316
+ title: e.title,
317
+ scope: e.scope,
318
+ subtasks: e.subtasks.length,
319
+ priority: e.priority,
320
+ status: e.status
321
+ }))
322
+ },
323
+ null,
324
+ 2
325
+ )
326
+ );
327
+ return;
328
+ }
329
+ if (epics.length === 0) {
330
+ console.log(ui.yellow("No epics created \u2014 the codebase looks clean."));
331
+ return;
332
+ }
333
+ const table = new Table({
334
+ head: ["Epic", "Scope", "Subtasks", "Priority", "Status"]
335
+ });
336
+ for (const epic of epics) {
337
+ table.push([
338
+ truncate(epic.title, 55),
339
+ epic.scope,
340
+ String(epic.subtasks.length),
341
+ String(epic.priority),
342
+ epic.status
343
+ ]);
344
+ }
345
+ console.log(table.toString());
346
+ console.log("");
347
+ console.log(
348
+ ui.blue(
349
+ `${epics.length} epic(s) added to backlog. Use \`oac run --repo ${resolvedRepo.fullName}\` to execute.`
350
+ )
351
+ );
352
+ });
353
+ command.addHelpText(
354
+ "after",
355
+ `
356
+ Analyze walks the source tree, builds a module dependency graph, runs all
357
+ scanners, and groups findings into epics persisted to .oac/context/.
358
+ For a quick flat task list without persistence, use \`oac scan\`.
359
+ \`oac run\` auto-analyzes by default \u2014 you only need this command to
360
+ inspect or force-refresh the analysis independently.
361
+
362
+ Examples:
363
+ $ oac analyze --repo owner/repo
364
+ $ oac analyze --repo owner/repo --force
365
+ $ oac analyze --repo owner/repo --format json`
366
+ );
367
+ return command;
368
+ }
369
+ function normalizeOutputFormat(value) {
370
+ const normalized = value.trim().toLowerCase();
371
+ if (normalized === "table" || normalized === "json") return normalized;
372
+ throw new Error(`Unsupported --format value "${value}". Use "table" or "json".`);
373
+ }
374
+ function buildScannerList(config, hasGitHubAuth) {
375
+ return buildScanners(config, hasGitHubAuth).instances;
376
+ }
377
+
378
+ // src/cli/commands/completion.ts
379
+ import { Command as Command3 } from "commander";
380
+ var SUBCOMMANDS = [
381
+ "init",
382
+ "analyze",
383
+ "doctor",
384
+ "scan",
385
+ "plan",
386
+ "run",
387
+ "log",
388
+ "leaderboard",
389
+ "status",
390
+ "completion"
391
+ ];
392
+ var GLOBAL_OPTIONS = ["--config", "--verbose", "--quiet", "--json", "--no-color", "--help", "--version"];
393
+ var COMMAND_OPTIONS = {
394
+ run: [
395
+ "--repo",
396
+ "--tokens",
397
+ "--provider",
398
+ "--concurrency",
399
+ "--dry-run",
400
+ "--mode",
401
+ "--max-tasks",
402
+ "--timeout",
403
+ "--source",
404
+ "--retry-failed"
405
+ ],
406
+ scan: ["--repo", "--scanners", "--max-findings"],
407
+ analyze: ["--repo"],
408
+ plan: ["--repo", "--tokens", "--provider", "--max-tasks"],
409
+ log: ["--limit", "--repo", "--source", "--since"],
410
+ leaderboard: ["--limit", "--repo", "--format"],
411
+ status: ["--watch"],
412
+ init: [],
413
+ doctor: []
414
+ };
415
+ function generateBash() {
416
+ const cmds = SUBCOMMANDS.join(" ");
417
+ const global = GLOBAL_OPTIONS.join(" ");
418
+ const cases = Object.entries(COMMAND_OPTIONS).map(([cmd, opts]) => {
419
+ const all = [...opts, ...GLOBAL_OPTIONS].join(" ");
420
+ return ` ${cmd}) COMPREPLY=( $(compgen -W "${all}" -- "$cur") ) ;;`;
421
+ }).join("\n");
422
+ return `# bash completion for oac
423
+ _oac_completions() {
424
+ local cur prev cmds
425
+ cur="\${COMP_WORDS[COMP_CWORD]}"
426
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
427
+ cmds="${cmds}"
428
+
429
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
430
+ COMPREPLY=( $(compgen -W "$cmds ${global}" -- "$cur") )
431
+ return
432
+ fi
433
+
434
+ case "\${COMP_WORDS[1]}" in
435
+ ${cases}
436
+ *) COMPREPLY=( $(compgen -W "${global}" -- "$cur") ) ;;
437
+ esac
438
+ }
439
+ complete -F _oac_completions oac`;
440
+ }
441
+ function generateZsh() {
442
+ const cmds = SUBCOMMANDS.map((c) => `'${c}:${c} command'`).join(" ");
443
+ const cases = Object.entries(COMMAND_OPTIONS).map(([cmd, opts]) => {
444
+ const flags = [...opts, ...GLOBAL_OPTIONS].map((o) => `'${o}'`).join(" ");
445
+ return ` ${cmd}) _arguments ${flags} ;;`;
446
+ }).join("\n");
447
+ return `#compdef oac
448
+ _oac() {
449
+ local -a commands
450
+ commands=(${cmds})
451
+
452
+ _arguments '1:command:->cmds' '*::arg:->args'
453
+
454
+ case "$state" in
455
+ cmds) _describe 'command' commands ;;
456
+ args)
457
+ case "\${words[1]}" in
458
+ ${cases}
459
+ esac
460
+ ;;
461
+ esac
462
+ }
463
+ _oac "$@"`;
464
+ }
465
+ function generateFish() {
466
+ const lines = ["# fish completion for oac"];
467
+ for (const cmd of SUBCOMMANDS) {
468
+ lines.push(`complete -c oac -n '__fish_use_subcommand' -a '${cmd}' -d '${cmd} command'`);
469
+ }
470
+ for (const opt of GLOBAL_OPTIONS) {
471
+ const long = opt.replace(/^--/, "");
472
+ lines.push(`complete -c oac -l '${long}'`);
473
+ }
474
+ for (const [cmd, opts] of Object.entries(COMMAND_OPTIONS)) {
475
+ for (const opt of opts) {
476
+ const long = opt.replace(/^--/, "");
477
+ lines.push(
478
+ `complete -c oac -n '__fish_seen_subcommand_from ${cmd}' -l '${long}'`
479
+ );
480
+ }
481
+ }
482
+ return lines.join("\n");
483
+ }
484
+ function createCompletionCommand() {
485
+ const command = new Command3("completion");
486
+ command.description("Generate shell completion scripts").argument("<shell>", "Shell type: bash, zsh, or fish").action((shell) => {
487
+ const generators = { bash: generateBash, zsh: generateZsh, fish: generateFish };
488
+ const gen = generators[shell];
489
+ if (!gen) {
490
+ throw new Error(`Unsupported shell "${shell}". Supported: bash, zsh, fish`);
491
+ }
492
+ console.log(gen());
493
+ });
494
+ command.addHelpText(
495
+ "after",
496
+ `
497
+ Examples:
498
+ $ oac completion bash >> ~/.bashrc
499
+ $ oac completion zsh >> ~/.zshrc
500
+ $ oac completion fish > ~/.config/fish/completions/oac.fish`
501
+ );
502
+ return command;
503
+ }
504
+
505
+ // src/cli/commands/doctor.ts
506
+ import { Command as Command4 } from "commander";
507
+ var MINIMUM_NODE_VERSION = "24.0.0";
508
+ function createDoctorCommand() {
509
+ const command = new Command4("doctor");
510
+ command.description("Check local environment readiness").action(async (_options, cmd) => {
511
+ const globalOptions = getGlobalOptions(cmd);
512
+ const ui = createUi(globalOptions);
513
+ const checks = await runDoctorChecks();
514
+ const allPassed = checks.every((check) => check.status !== "fail");
515
+ if (globalOptions.json) {
516
+ console.log(
517
+ JSON.stringify(
518
+ {
519
+ checks,
520
+ allPassed
521
+ },
522
+ null,
523
+ 2
524
+ )
525
+ );
526
+ } else {
527
+ renderDoctorOutput(ui, checks, allPassed);
528
+ }
529
+ if (!allPassed) {
530
+ process.exitCode = 1;
531
+ }
532
+ });
533
+ command.addHelpText(
534
+ "after",
535
+ `
536
+ Examples:
537
+ $ oac doctor`
538
+ );
539
+ return command;
540
+ }
541
+ async function runDoctorChecks() {
542
+ const checks = [];
543
+ const nodeVersion = process.versions.node;
544
+ checks.push({
545
+ id: "node",
546
+ name: "Node.js",
547
+ requirement: `>= ${MINIMUM_NODE_VERSION}`,
548
+ value: `v${nodeVersion}`,
549
+ status: isVersionAtLeast(nodeVersion, MINIMUM_NODE_VERSION) ? "pass" : "fail",
550
+ message: `Node.js ${MINIMUM_NODE_VERSION}+ is required.`
551
+ });
552
+ const gitResult = await runCommand("git", ["--version"]);
553
+ const gitVersion = extractVersion(gitResult.stdout) ?? "--";
554
+ checks.push({
555
+ id: "git",
556
+ name: "git",
557
+ requirement: "installed",
558
+ value: gitVersion,
559
+ status: gitResult.ok ? "pass" : "fail",
560
+ message: gitResult.ok ? void 0 : explainCommandFailure("git", gitResult)
561
+ });
562
+ const githubAuthCheck = await checkGithubAuth();
563
+ checks.push(githubAuthCheck);
564
+ const claudeResult = await runCommand("claude", ["--version"]);
565
+ const claudeVersion = extractVersion(claudeResult.stdout) ?? "--";
566
+ checks.push({
567
+ id: "claude-cli",
568
+ name: "Claude CLI",
569
+ requirement: "installed",
570
+ value: claudeVersion,
571
+ status: claudeResult.ok ? "pass" : "fail",
572
+ message: claudeResult.ok ? void 0 : explainCommandFailure("claude", claudeResult)
573
+ });
574
+ const codexResult = await runCommand("codex", ["--version"]);
575
+ const codexVersion = extractVersion(codexResult.stdout) ?? "--";
576
+ checks.push({
577
+ id: "codex",
578
+ name: "Codex CLI",
579
+ requirement: "installed",
580
+ value: codexVersion,
581
+ status: codexResult.ok ? "pass" : "fail",
582
+ message: codexResult.ok ? void 0 : explainCommandFailure("codex", codexResult)
583
+ });
584
+ const opencodeResult = await runCommand("opencode", ["--version"]);
585
+ const opencodeVersion = extractVersion(opencodeResult.stdout) ?? "--";
586
+ checks.push({
587
+ id: "opencode",
588
+ name: "OpenCode CLI",
589
+ requirement: "installed",
590
+ value: opencodeVersion,
591
+ status: opencodeResult.ok ? "pass" : "fail",
592
+ message: opencodeResult.ok ? void 0 : explainCommandFailure("opencode", opencodeResult)
593
+ });
594
+ return checks;
595
+ }
596
+ async function checkGithubAuth() {
597
+ const envToken = process.env.GITHUB_TOKEN?.trim();
598
+ if (envToken) {
599
+ return {
600
+ id: "github-auth",
601
+ name: "GitHub auth",
602
+ requirement: "gh auth status or GITHUB_TOKEN",
603
+ value: `env:${maskToken(envToken)}`,
604
+ status: "pass"
605
+ };
606
+ }
607
+ const authResult = await runCommand("gh", ["auth", "status"]);
608
+ if (authResult.ok) {
609
+ const hasRepoScope = authResult.stdout.includes("repo");
610
+ return {
611
+ id: "github-auth",
612
+ name: "GitHub auth",
613
+ requirement: "gh auth status or GITHUB_TOKEN",
614
+ value: "gh auth status",
615
+ status: hasRepoScope ? "pass" : "warn",
616
+ message: hasRepoScope ? void 0 : "Missing 'repo' scope \u2014 private repos won't work. Run: gh auth refresh -s repo"
617
+ };
618
+ }
619
+ return {
620
+ id: "github-auth",
621
+ name: "GitHub auth",
622
+ requirement: "gh auth status or GITHUB_TOKEN",
623
+ value: "--",
624
+ status: "fail",
625
+ message: "No GitHub authentication detected. Set GITHUB_TOKEN or run `gh auth login`."
626
+ };
627
+ }
628
+ function renderDoctorOutput(ui, checks, allPassed) {
629
+ console.log("Checking environment...");
630
+ console.log("");
631
+ for (const check of checks) {
632
+ const iconMap = { pass: ui.green("[OK]"), warn: ui.yellow("[!]"), fail: ui.red("[X]") };
633
+ const statusMap = { pass: ui.green("PASS"), warn: ui.yellow("WARN"), fail: ui.red("FAIL") };
634
+ const icon = iconMap[check.status];
635
+ const status = statusMap[check.status];
636
+ const name = check.name.padEnd(12, " ");
637
+ const requirement = check.requirement.padEnd(30, " ");
638
+ const value = check.value.padEnd(14, " ");
639
+ console.log(` ${icon} ${name} ${requirement} ${value} ${status}`);
640
+ if (check.status === "fail" && check.message) {
641
+ console.log(` ${ui.red(check.message)}`);
642
+ }
643
+ if (check.status === "warn" && check.message) {
644
+ console.log(` ${ui.yellow(check.message)}`);
645
+ }
646
+ }
647
+ console.log("");
648
+ if (allPassed) {
649
+ console.log(ui.green("All checks passed."));
650
+ } else {
651
+ console.log(ui.red("Some checks failed."));
652
+ }
653
+ }
654
+ function extractVersion(output) {
655
+ const match = output.match(/v?(\d+\.\d+\.\d+)/i);
656
+ if (!match) {
657
+ return void 0;
658
+ }
659
+ return `v${match[1]}`;
660
+ }
661
+ function explainCommandFailure(commandName, result) {
662
+ if (result.errorCode === "ENOENT") {
663
+ return `${commandName} is not installed or not in PATH.`;
664
+ }
665
+ if (result.errorMessage) {
666
+ return result.errorMessage;
667
+ }
668
+ if (result.stderr.trim().length > 0) {
669
+ return result.stderr.trim();
670
+ }
671
+ return `${commandName} exited with code ${String(result.exitCode)}.`;
672
+ }
673
+ function maskToken(token) {
674
+ if (token.length <= 8) {
675
+ return `${token.slice(0, 2)}****`;
676
+ }
677
+ return `${token.slice(0, 4)}****${token.slice(-2)}`;
678
+ }
679
+ function isVersionAtLeast(version, minimum) {
680
+ const current = version.split(".").map((part) => Number.parseInt(part, 10));
681
+ const required = minimum.split(".").map((part) => Number.parseInt(part, 10));
682
+ const length = Math.max(current.length, required.length);
683
+ for (let index = 0; index < length; index += 1) {
684
+ const currentPart = current[index] ?? 0;
685
+ const requiredPart = required[index] ?? 0;
686
+ if (currentPart > requiredPart) {
687
+ return true;
688
+ }
689
+ if (currentPart < requiredPart) {
690
+ return false;
691
+ }
692
+ }
693
+ return true;
694
+ }
695
+ async function runCommand(command, args) {
696
+ try {
697
+ const { execa: execa3 } = await import("execa");
698
+ const result = await execa3(command, args, {
699
+ reject: false,
700
+ timeout: 3e4,
701
+ stdin: "ignore"
702
+ });
703
+ return {
704
+ ok: result.exitCode === 0,
705
+ exitCode: result.exitCode ?? null,
706
+ stdout: result.stdout,
707
+ stderr: result.stderr
708
+ };
709
+ } catch (error) {
710
+ const nodeError = error;
711
+ return {
712
+ ok: false,
713
+ exitCode: null,
714
+ stdout: "",
715
+ stderr: "",
716
+ errorCode: nodeError.code,
717
+ errorMessage: nodeError.message
718
+ };
719
+ }
720
+ }
721
+
722
+ // src/cli/commands/explain.ts
723
+ import { resolve as resolve2 } from "path";
724
+ import { Command as Command5 } from "commander";
725
+ function createExplainCommand() {
726
+ const command = new Command5("explain");
727
+ command.description("Explain why a task or epic was selected and what the agent would do").argument("<id>", "Task or epic ID (from scan / analyze / run --dry-run output)").action(async (id, _options, cmd) => {
728
+ const globalOptions = getGlobalOptions(cmd);
729
+ const ui = createUi(globalOptions);
730
+ const config = await loadOptionalConfig(globalOptions.config, globalOptions.verbose, ui);
731
+ const contextDir = config?.analyze?.contextDir;
732
+ const repoPath = resolve2(process.cwd());
733
+ const [context, backlog] = await Promise.all([
734
+ loadContext(repoPath, contextDir),
735
+ loadBacklog(repoPath, contextDir)
736
+ ]);
737
+ if (!context && !backlog) {
738
+ const message = "No analysis context found. Run `oac analyze` first.";
739
+ if (globalOptions.json) {
740
+ console.log(JSON.stringify({ error: message }, null, 2));
741
+ } else {
742
+ console.error(ui.red(message));
743
+ }
744
+ process.exitCode = 1;
745
+ return;
746
+ }
747
+ const finding = context?.qualityReport.findings.find(
748
+ (f) => f.title === id || f.filePath === id
749
+ );
750
+ const epic = backlog?.epics.find(
751
+ (e) => e.id === id || e.title.toLowerCase().includes(id.toLowerCase())
752
+ );
753
+ if (!finding && !epic) {
754
+ const message = `No task or epic matching "${id}" found in the analysis context.`;
755
+ if (globalOptions.json) {
756
+ console.log(JSON.stringify({ error: message, id }, null, 2));
757
+ } else {
758
+ console.error(ui.red(message));
759
+ console.log("");
760
+ printAvailableIds(ui, context, backlog);
761
+ }
762
+ process.exitCode = 1;
763
+ return;
764
+ }
765
+ if (globalOptions.json) {
766
+ console.log(JSON.stringify({ finding: finding ?? null, epic: epic ?? null }, null, 2));
767
+ return;
768
+ }
769
+ if (epic) {
770
+ console.log(ui.bold("Epic"));
771
+ console.log(` ${ui.blue("ID:")} ${epic.id}`);
772
+ console.log(` ${ui.blue("Title:")} ${epic.title}`);
773
+ console.log(` ${ui.blue("Scope:")} ${epic.scope}`);
774
+ console.log(` ${ui.blue("Priority:")} ${epic.priority}`);
775
+ console.log(` ${ui.blue("Status:")} ${epic.status}`);
776
+ console.log(` ${ui.blue("Tasks:")} ${epic.subtasks.length}`);
777
+ console.log("");
778
+ console.log(ui.dim("Description:"));
779
+ console.log(` ${epic.description}`);
780
+ if (epic.subtasks.length > 0) {
781
+ console.log("");
782
+ console.log(ui.dim("Task IDs:"));
783
+ for (const subtask of epic.subtasks) {
784
+ console.log(` - ${subtask.id}`);
785
+ }
786
+ }
787
+ }
788
+ if (finding) {
789
+ if (epic) console.log("");
790
+ console.log(ui.bold("Finding"));
791
+ console.log(` ${ui.blue("Title:")} ${finding.title}`);
792
+ console.log(` ${ui.blue("Source:")} ${finding.source.replace(/-/g, " ")}`);
793
+ console.log(` ${ui.blue("Scanner:")} ${finding.scannerId}`);
794
+ console.log(` ${ui.blue("Severity:")} ${colorSeverity(ui, finding.severity)}`);
795
+ console.log(` ${ui.blue("Complexity:")} ${finding.complexity}`);
796
+ console.log(` ${ui.blue("File:")} ${finding.filePath}`);
797
+ if (finding.module) {
798
+ console.log(` ${ui.blue("Module:")} ${finding.module}`);
799
+ }
800
+ if (finding.line) {
801
+ console.log(` ${ui.blue("Line:")} ${finding.line}`);
802
+ }
803
+ console.log("");
804
+ console.log(ui.dim("Description:"));
805
+ console.log(` ${finding.description}`);
806
+ console.log("");
807
+ console.log(ui.dim("What the agent would do:"));
808
+ console.log(` 1. Check out a clean branch for this task`);
809
+ console.log(` 2. Open ${finding.filePath}${finding.line ? ` at line ${finding.line}` : ""}`);
810
+ console.log(` 3. Apply the fix described above`);
811
+ console.log(` 4. Run tests and linters to verify`);
812
+ console.log(` 5. Create a PR with the changes`);
813
+ }
814
+ });
815
+ command.addHelpText(
816
+ "after",
817
+ `
818
+ Examples:
819
+ $ oac explain "Add tests for client.ts"
820
+ $ oac explain src/lib/client.ts`
821
+ );
822
+ return command;
823
+ }
824
+ function colorSeverity(ui, severity) {
825
+ if (severity === "error") return ui.red(severity);
826
+ if (severity === "warning") return ui.yellow(severity);
827
+ return ui.green(severity);
828
+ }
829
+ function printAvailableIds(ui, context, backlog) {
830
+ const findings = context?.qualityReport.findings ?? [];
831
+ const epics = backlog?.epics ?? [];
832
+ if (findings.length > 0) {
833
+ console.log(ui.dim(`Available findings (${findings.length}):`));
834
+ for (const f of findings.slice(0, 8)) {
835
+ console.log(` - ${f.title}`);
836
+ }
837
+ if (findings.length > 8) console.log(ui.dim(` ... and ${findings.length - 8} more`));
838
+ }
839
+ if (epics.length > 0) {
840
+ console.log(ui.dim(`Available epics (${epics.length}):`));
841
+ for (const e of epics.slice(0, 8)) {
842
+ console.log(` - ${e.id}: ${e.title}`);
843
+ }
844
+ if (epics.length > 8) console.log(ui.dim(` ... and ${epics.length - 8} more`));
845
+ }
846
+ }
847
+
848
+ // src/cli/commands/init.ts
849
+ import { constants as fsConstants2 } from "fs";
850
+ import { access as access2, mkdir, readFile as readFile2, writeFile } from "fs/promises";
851
+ import { resolve as resolve3 } from "path";
852
+ import { checkbox, confirm, input } from "@inquirer/prompts";
853
+ import { Command as Command6 } from "commander";
854
+ var OAC_LOGO = [
855
+ " ___ _ ___",
856
+ " / _ \\ /_\\ / __|",
857
+ "| (_) / _ \\ (__",
858
+ " \\___/_/ \\_\\___|"
859
+ ].join("\n");
860
+ var OWNER_REPO_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/;
861
+ function createInitCommand() {
862
+ const command = new Command6("init");
863
+ command.description("Initialize OAC in the current directory").option("--minimal", "Generate a bare-bones config without the interactive wizard").option("--repo <owner/repo>", "Repository in owner/repo format (required with --minimal)").action(async (options, cmd) => {
864
+ const globalOptions = getGlobalOptions(cmd);
865
+ const ui = createUi(globalOptions);
866
+ if (options.minimal) {
867
+ await runMinimalInit(options, globalOptions, ui);
868
+ return;
869
+ }
870
+ await runInteractiveInit(globalOptions, ui);
871
+ });
872
+ command.addHelpText(
873
+ "after",
874
+ `
875
+ Examples:
876
+ $ oac init Interactive wizard
877
+ $ oac init --minimal --repo owner/repo Quick start with defaults`
878
+ );
879
+ return command;
880
+ }
881
+ async function runMinimalInit(options, globalOptions, ui) {
882
+ const repoInput = options.repo?.trim();
883
+ if (!repoInput || !isValidRepoInput(repoInput)) {
884
+ const message = "--repo is required with --minimal (e.g. --repo owner/repo)";
885
+ if (globalOptions.json) {
886
+ console.log(JSON.stringify({ error: message }, null, 2));
887
+ } else {
888
+ console.error(ui.red(message));
889
+ }
890
+ process.exitCode = 1;
891
+ return;
892
+ }
893
+ const repo = normalizeRepoInput(repoInput);
894
+ const provider = "claude-code";
895
+ const budgetTokens = 1e5;
896
+ const configPath = resolve3(process.cwd(), "oac.config.ts");
897
+ const trackingDirectory = resolve3(process.cwd(), ".oac");
898
+ if (await pathExists2(configPath)) {
899
+ const message = "oac.config.ts already exists. Remove it first or run `oac init` without --minimal.";
900
+ if (globalOptions.json) {
901
+ console.log(JSON.stringify({ cancelled: true, reason: message }, null, 2));
902
+ } else {
903
+ console.log(ui.yellow(message));
904
+ }
905
+ return;
906
+ }
907
+ const configContent = buildConfigFile({
908
+ provider,
909
+ providers: [provider],
910
+ budgetTokens,
911
+ repo
912
+ });
913
+ await writeFile(configPath, configContent, "utf8");
914
+ await mkdir(trackingDirectory, { recursive: true });
915
+ await ensureGitignoreEntry(process.cwd(), ".oac/");
916
+ const summary = {
917
+ configPath,
918
+ trackingDirectory,
919
+ provider,
920
+ providers: [provider],
921
+ budgetTokens,
922
+ repo
923
+ };
924
+ if (globalOptions.json) {
925
+ console.log(JSON.stringify(summary, null, 2));
926
+ return;
927
+ }
928
+ console.log(ui.green("Created: oac.config.ts"));
929
+ console.log(ui.green("Created: .oac/"));
930
+ console.log("");
931
+ console.log("Run `oac run` to start the full pipeline, or `oac doctor` to verify setup.");
932
+ }
933
+ async function runInteractiveInit(globalOptions, ui) {
934
+ if (!globalOptions.json) {
935
+ console.log(ui.blue(OAC_LOGO));
936
+ console.log(ui.bold("Welcome to Open Agent Contribution."));
937
+ console.log("");
938
+ }
939
+ const selectedProviders = await checkbox({
940
+ message: "Select AI provider(s):",
941
+ choices: [
942
+ { name: "Claude Code", value: "claude-code", checked: true },
943
+ { name: "Codex CLI", value: "codex" },
944
+ { name: "OpenCode", value: "opencode" }
945
+ ],
946
+ validate: (value) => value.length > 0 ? true : "Select at least one provider to continue."
947
+ });
948
+ const budgetInput = await input({
949
+ message: "Monthly token budget:",
950
+ default: "100000",
951
+ validate: (value) => {
952
+ const parsed = Number.parseInt(value, 10);
953
+ if (!Number.isFinite(parsed) || parsed <= 0) {
954
+ return "Enter a positive integer.";
955
+ }
956
+ return true;
957
+ }
958
+ });
959
+ const firstRepoInput = await input({
960
+ message: "Add your first repo (owner/repo or GitHub URL):",
961
+ validate: (value) => {
962
+ if (isValidRepoInput(value)) {
963
+ return true;
964
+ }
965
+ return "Enter a valid GitHub repo like owner/repo.";
966
+ }
967
+ });
968
+ const repo = normalizeRepoInput(firstRepoInput);
969
+ const budgetTokens = Number.parseInt(budgetInput, 10);
970
+ const provider = selectedProviders[0] ?? "claude-code";
971
+ const configPath = resolve3(process.cwd(), "oac.config.ts");
972
+ const trackingDirectory = resolve3(process.cwd(), ".oac");
973
+ if (await pathExists2(configPath)) {
974
+ const shouldOverwrite = await confirm({
975
+ message: "oac.config.ts already exists. Overwrite it?",
976
+ default: false
977
+ });
978
+ if (!shouldOverwrite) {
979
+ if (globalOptions.json) {
980
+ console.log(
981
+ JSON.stringify(
982
+ {
983
+ cancelled: true,
984
+ reason: "oac.config.ts exists and overwrite was declined"
985
+ },
986
+ null,
987
+ 2
988
+ )
989
+ );
990
+ return;
991
+ }
992
+ console.log(ui.yellow("Initialization cancelled."));
993
+ return;
994
+ }
995
+ }
996
+ const configContent = buildConfigFile({
997
+ provider,
998
+ providers: selectedProviders,
999
+ budgetTokens,
1000
+ repo
1001
+ });
1002
+ await writeFile(configPath, configContent, "utf8");
1003
+ await mkdir(trackingDirectory, { recursive: true });
1004
+ await ensureGitignoreEntry(process.cwd(), ".oac/");
1005
+ const summary = {
1006
+ configPath,
1007
+ trackingDirectory,
1008
+ provider,
1009
+ providers: selectedProviders,
1010
+ budgetTokens,
1011
+ repo
1012
+ };
1013
+ if (globalOptions.json) {
1014
+ console.log(JSON.stringify(summary, null, 2));
1015
+ return;
1016
+ }
1017
+ console.log(ui.green("Created: oac.config.ts"));
1018
+ console.log(ui.green("Created: .oac/"));
1019
+ console.log("");
1020
+ console.log("Run `oac run` to start the full pipeline, or `oac doctor` to verify setup.");
1021
+ }
1022
+ function buildConfigFile(input2) {
1023
+ const enabledProviders = input2.providers.map((provider) => `"${provider}"`).join(", ");
1024
+ return `import { defineConfig } from "@open330/oac";
1025
+
1026
+ export default defineConfig({
1027
+ repos: ["${input2.repo}"],
1028
+ provider: {
1029
+ id: "${input2.provider}",
1030
+ options: {
1031
+ enabledProviders: [${enabledProviders}],
1032
+ },
1033
+ },
1034
+ budget: {
1035
+ totalTokens: ${input2.budgetTokens},
1036
+ },
1037
+ });
1038
+ `;
1039
+ }
1040
+ function normalizeRepoInput(input2) {
1041
+ const trimmed = input2.trim();
1042
+ if (OWNER_REPO_PATTERN.test(trimmed)) {
1043
+ return stripGitSuffix(trimmed);
1044
+ }
1045
+ const normalizedUrlInput = trimmed.startsWith("github.com/") ? `https://${trimmed}` : trimmed;
1046
+ try {
1047
+ const url = new URL(normalizedUrlInput);
1048
+ if (url.hostname !== "github.com") {
1049
+ return trimmed;
1050
+ }
1051
+ const segments = url.pathname.split("/").filter(Boolean);
1052
+ if (segments.length < 2) {
1053
+ return trimmed;
1054
+ }
1055
+ const owner = segments[0];
1056
+ const repo = stripGitSuffix(segments[1] ?? "");
1057
+ if (!owner || !repo) {
1058
+ return trimmed;
1059
+ }
1060
+ return `${owner}/${repo}`;
1061
+ } catch {
1062
+ return trimmed;
1063
+ }
1064
+ }
1065
+ function stripGitSuffix(value) {
1066
+ return value.endsWith(".git") ? value.slice(0, -4) : value;
1067
+ }
1068
+ function isValidRepoInput(input2) {
1069
+ const normalized = normalizeRepoInput(input2);
1070
+ return OWNER_REPO_PATTERN.test(normalized);
1071
+ }
1072
+ async function pathExists2(path) {
1073
+ try {
1074
+ await access2(path, fsConstants2.F_OK);
1075
+ return true;
1076
+ } catch {
1077
+ return false;
1078
+ }
1079
+ }
1080
+ async function ensureGitignoreEntry(dir, entry) {
1081
+ const gitignorePath = resolve3(dir, ".gitignore");
1082
+ let content = "";
1083
+ try {
1084
+ content = await readFile2(gitignorePath, "utf8");
1085
+ } catch {
1086
+ }
1087
+ if (content.split("\n").some((line) => line.trim() === entry)) {
1088
+ return;
1089
+ }
1090
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
1091
+ await writeFile(gitignorePath, `${content}${separator}${entry}
1092
+ `, "utf8");
1093
+ }
1094
+
1095
+ // src/cli/commands/leaderboard.ts
1096
+ import { readFile as readFile3, readdir } from "fs/promises";
1097
+ import { resolve as resolve4 } from "path";
1098
+ import Table2 from "cli-table3";
1099
+ import { Command as Command7 } from "commander";
1100
+ function createLeaderboardCommand() {
1101
+ const command = new Command7("leaderboard");
1102
+ command.description("Show contribution rankings").option("--limit <number>", "Max entries to show", parseInteger, 10).option("--sort <field>", "Sort by: runs, tasks, tokens, prs", "tasks").action(async (options, cmd) => {
1103
+ if (options.limit <= 0) {
1104
+ throw new Error("--limit must be a positive integer.");
1105
+ }
1106
+ const globalOptions = getGlobalOptions(cmd);
1107
+ const sortField = normalizeSortField(options.sort);
1108
+ const entries = await loadLeaderboardEntries(process.cwd());
1109
+ const sortedEntries = sortEntries(entries, sortField).slice(0, options.limit);
1110
+ if (globalOptions.json) {
1111
+ console.log(
1112
+ JSON.stringify(
1113
+ {
1114
+ total: sortedEntries.length,
1115
+ sort: sortField,
1116
+ entries: sortedEntries
1117
+ },
1118
+ null,
1119
+ 2
1120
+ )
1121
+ );
1122
+ return;
1123
+ }
1124
+ if (sortedEntries.length === 0) {
1125
+ console.log("No leaderboard data found.");
1126
+ return;
1127
+ }
1128
+ const table = new Table2({
1129
+ head: ["Rank", "User", "Tasks", "Tokens Used", "PRs Created", "PRs Merged"]
1130
+ });
1131
+ for (let index = 0; index < sortedEntries.length; index += 1) {
1132
+ const entry = sortedEntries[index];
1133
+ table.push([
1134
+ String(index + 1),
1135
+ entry.githubUsername,
1136
+ String(entry.totalTasksCompleted),
1137
+ formatInteger(entry.totalTokensDonated),
1138
+ String(entry.totalPRsCreated),
1139
+ String(entry.totalPRsMerged)
1140
+ ]);
1141
+ }
1142
+ console.log(table.toString());
1143
+ });
1144
+ command.addHelpText(
1145
+ "after",
1146
+ `
1147
+ Examples:
1148
+ $ oac leaderboard
1149
+ $ oac leaderboard --limit 20 --sort tokens`
1150
+ );
1151
+ return command;
1152
+ }
1153
+ async function loadLeaderboardEntries(repoPath) {
1154
+ const leaderboardPath = resolve4(repoPath, ".oac", "leaderboard.json");
1155
+ try {
1156
+ const leaderboardRaw = await readFile3(leaderboardPath, "utf8");
1157
+ const leaderboardPayload = JSON.parse(leaderboardRaw);
1158
+ return parseStoredLeaderboardEntries(leaderboardPayload);
1159
+ } catch (error) {
1160
+ if (!isFileNotFoundError(error)) {
1161
+ throw error;
1162
+ }
1163
+ }
1164
+ const logs = await readContributionLogs(repoPath);
1165
+ return buildEntriesFromLogs(logs);
1166
+ }
1167
+ function parseStoredLeaderboardEntries(payload) {
1168
+ if (!isRecord(payload)) {
1169
+ return [];
1170
+ }
1171
+ const entries = payload.entries;
1172
+ if (!Array.isArray(entries)) {
1173
+ return [];
1174
+ }
1175
+ const parsedEntries = [];
1176
+ for (const entry of entries) {
1177
+ if (!isRecord(entry)) {
1178
+ continue;
1179
+ }
1180
+ const githubUsername = entry.githubUsername;
1181
+ const totalRuns = entry.totalRuns;
1182
+ const totalTasksCompleted = entry.totalTasksCompleted;
1183
+ const totalTokensDonated = entry.totalTokensDonated;
1184
+ const totalPRsCreated = entry.totalPRsCreated;
1185
+ const totalPRsMerged = entry.totalPRsMerged;
1186
+ if (typeof githubUsername !== "string" || typeof totalRuns !== "number" || typeof totalTasksCompleted !== "number" || typeof totalTokensDonated !== "number" || typeof totalPRsCreated !== "number" || typeof totalPRsMerged !== "number") {
1187
+ continue;
1188
+ }
1189
+ parsedEntries.push({
1190
+ githubUsername,
1191
+ totalRuns,
1192
+ totalTasksCompleted,
1193
+ totalTokensDonated,
1194
+ totalPRsCreated,
1195
+ totalPRsMerged
1196
+ });
1197
+ }
1198
+ return parsedEntries;
1199
+ }
1200
+ async function readContributionLogs(repoPath) {
1201
+ const contributionsPath = resolve4(repoPath, ".oac", "contributions");
1202
+ let entries;
1203
+ try {
1204
+ entries = await readdir(contributionsPath, { withFileTypes: true, encoding: "utf8" });
1205
+ } catch (error) {
1206
+ if (isFileNotFoundError(error)) {
1207
+ return [];
1208
+ }
1209
+ throw error;
1210
+ }
1211
+ const fileNames = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
1212
+ const logs = await Promise.all(
1213
+ fileNames.map(async (fileName) => {
1214
+ const filePath = resolve4(contributionsPath, fileName);
1215
+ try {
1216
+ const content = await readFile3(filePath, "utf8");
1217
+ const payload = JSON.parse(content);
1218
+ const parsed = contributionLogSchema.safeParse(payload);
1219
+ return parsed.success ? parsed.data : null;
1220
+ } catch {
1221
+ return null;
1222
+ }
1223
+ })
1224
+ );
1225
+ return logs.filter((log) => log !== null);
1226
+ }
1227
+ function buildEntriesFromLogs(logs) {
1228
+ const byUser = /* @__PURE__ */ new Map();
1229
+ for (const log of logs) {
1230
+ const username = log.contributor.githubUsername;
1231
+ const existing = byUser.get(username) ?? {
1232
+ githubUsername: username,
1233
+ totalRuns: 0,
1234
+ totalTasksCompleted: 0,
1235
+ totalTokensDonated: 0,
1236
+ totalPRsCreated: 0,
1237
+ totalPRsMerged: 0
1238
+ };
1239
+ existing.totalRuns += 1;
1240
+ existing.totalTasksCompleted += log.tasks.filter((task) => task.status !== "failed").length;
1241
+ existing.totalTokensDonated += log.budget.totalTokensUsed;
1242
+ existing.totalPRsCreated += log.tasks.filter((task) => Boolean(task.pr)).length;
1243
+ existing.totalPRsMerged += log.tasks.filter((task) => task.pr?.status === "merged").length;
1244
+ byUser.set(username, existing);
1245
+ }
1246
+ return Array.from(byUser.values());
1247
+ }
1248
+ function sortEntries(entries, field) {
1249
+ return [...entries].sort((a, b) => {
1250
+ const first = sortValue(b, field) - sortValue(a, field);
1251
+ if (first !== 0) {
1252
+ return first;
1253
+ }
1254
+ if (b.totalTasksCompleted !== a.totalTasksCompleted) {
1255
+ return b.totalTasksCompleted - a.totalTasksCompleted;
1256
+ }
1257
+ if (b.totalRuns !== a.totalRuns) {
1258
+ return b.totalRuns - a.totalRuns;
1259
+ }
1260
+ return a.githubUsername.localeCompare(b.githubUsername);
1261
+ });
1262
+ }
1263
+ function normalizeSortField(value) {
1264
+ const normalized = value.trim().toLowerCase();
1265
+ if (normalized === "runs" || normalized === "tasks" || normalized === "tokens" || normalized === "prs") {
1266
+ return normalized;
1267
+ }
1268
+ throw new Error(`Unsupported --sort value "${value}". Use runs, tasks, tokens, or prs.`);
1269
+ }
1270
+ function sortValue(entry, field) {
1271
+ if (field === "runs") {
1272
+ return entry.totalRuns;
1273
+ }
1274
+ if (field === "tasks") {
1275
+ return entry.totalTasksCompleted;
1276
+ }
1277
+ if (field === "tokens") {
1278
+ return entry.totalTokensDonated;
1279
+ }
1280
+ return entry.totalPRsCreated;
1281
+ }
1282
+ function isFileNotFoundError(error) {
1283
+ if (!isRecord(error)) {
1284
+ return false;
1285
+ }
1286
+ return error.code === "ENOENT";
1287
+ }
1288
+
1289
+ // src/cli/commands/log.ts
1290
+ import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
1291
+ import { resolve as resolve5 } from "path";
1292
+ import Table3 from "cli-table3";
1293
+ import { Command as Command8 } from "commander";
1294
+ function createLogCommand() {
1295
+ const command = new Command8("log");
1296
+ command.description("View contribution history").option("--limit <number>", "Max entries to show", parseInteger, 20).option("--repo <name>", "Filter by repo name").option("--source <type>", "Filter by task source").option("--since <date>", "Filter contributions after date (ISO string)").action(async (options, cmd) => {
1297
+ if (options.limit <= 0) {
1298
+ throw new Error("--limit must be a positive integer.");
1299
+ }
1300
+ const globalOptions = getGlobalOptions(cmd);
1301
+ const sinceDate = parseSinceDate(options.since);
1302
+ const repoFilter = options.repo?.trim();
1303
+ const sourceFilter = options.source?.trim().toLowerCase();
1304
+ const logs = await readContributionLogs2(process.cwd());
1305
+ const filteredLogs = logs.filter((log) => repoFilter ? log.repo.fullName === repoFilter : true).filter(
1306
+ (log) => sourceFilter ? log.tasks.some((task) => task.source === sourceFilter) : true
1307
+ ).filter((log) => sinceDate ? Date.parse(log.timestamp) >= sinceDate.getTime() : true).sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp)).slice(0, options.limit);
1308
+ if (globalOptions.json) {
1309
+ console.log(
1310
+ JSON.stringify(
1311
+ {
1312
+ total: filteredLogs.length,
1313
+ entries: filteredLogs
1314
+ },
1315
+ null,
1316
+ 2
1317
+ )
1318
+ );
1319
+ return;
1320
+ }
1321
+ if (filteredLogs.length === 0) {
1322
+ console.log("No contribution logs found.");
1323
+ return;
1324
+ }
1325
+ const table = new Table3({
1326
+ head: ["Date", "Repo", "Tasks", "Tokens", "PRs", "Source"]
1327
+ });
1328
+ for (const log of filteredLogs) {
1329
+ table.push([
1330
+ formatDate(log.timestamp),
1331
+ log.repo.fullName,
1332
+ String(log.tasks.length),
1333
+ formatInteger(log.budget.totalTokensUsed),
1334
+ String(log.tasks.filter((task) => Boolean(task.pr)).length),
1335
+ summarizeSources(log)
1336
+ ]);
1337
+ }
1338
+ console.log(table.toString());
1339
+ });
1340
+ command.addHelpText(
1341
+ "after",
1342
+ `
1343
+ Examples:
1344
+ $ oac log
1345
+ $ oac log --limit 10 --repo owner/repo
1346
+ $ oac log --source lint --since 2025-01-01`
1347
+ );
1348
+ return command;
1349
+ }
1350
+ async function readContributionLogs2(repoPath) {
1351
+ const contributionsPath = resolve5(repoPath, ".oac", "contributions");
1352
+ let entries;
1353
+ try {
1354
+ entries = await readdir2(contributionsPath, { withFileTypes: true, encoding: "utf8" });
1355
+ } catch (error) {
1356
+ if (isFileNotFoundError2(error)) {
1357
+ return [];
1358
+ }
1359
+ throw error;
1360
+ }
1361
+ const files = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
1362
+ const logs = await Promise.all(
1363
+ files.map(async (fileName) => {
1364
+ const filePath = resolve5(contributionsPath, fileName);
1365
+ try {
1366
+ const content = await readFile4(filePath, "utf8");
1367
+ const payload = JSON.parse(content);
1368
+ const parsed = contributionLogSchema.safeParse(payload);
1369
+ return parsed.success ? parsed.data : null;
1370
+ } catch {
1371
+ return null;
1372
+ }
1373
+ })
1374
+ );
1375
+ return logs.filter((log) => log !== null);
1376
+ }
1377
+ function parseSinceDate(value) {
1378
+ if (!value) {
1379
+ return null;
1380
+ }
1381
+ const parsed = Date.parse(value);
1382
+ if (!Number.isFinite(parsed)) {
1383
+ throw new Error(`Invalid --since value "${value}". Expected an ISO date string.`);
1384
+ }
1385
+ return new Date(parsed);
1386
+ }
1387
+ function summarizeSources(log) {
1388
+ const uniqueSources = [...new Set(log.tasks.map((task) => task.source))].sort(
1389
+ (a, b) => a.localeCompare(b)
1390
+ );
1391
+ if (uniqueSources.length === 0) {
1392
+ return "-";
1393
+ }
1394
+ return uniqueSources.join(", ");
1395
+ }
1396
+ function formatDate(timestamp) {
1397
+ const date = new Date(timestamp);
1398
+ if (Number.isNaN(date.getTime())) {
1399
+ return timestamp;
1400
+ }
1401
+ return date.toISOString();
1402
+ }
1403
+ function isFileNotFoundError2(error) {
1404
+ if (typeof error !== "object" || error === null) {
1405
+ return false;
1406
+ }
1407
+ const code = error.code;
1408
+ return code === "ENOENT";
1409
+ }
1410
+
1411
+ // src/cli/commands/plan.ts
1412
+ import Table4 from "cli-table3";
1413
+ import { Command as Command9 } from "commander";
1414
+ function createPlanCommand() {
1415
+ const command = new Command9("plan");
1416
+ command.description("Build an execution plan from discovered tasks").option("--repo <owner/repo>", "Target repository (owner/repo or GitHub URL)").option("--tokens <number>", "Token budget for planning", parseInteger).option("--provider <id>", "Agent provider id").action(async (options, cmd) => {
1417
+ const globalOptions = getGlobalOptions(cmd);
1418
+ const ui = createUi(globalOptions);
1419
+ const outputJson = globalOptions.json;
1420
+ const config = await loadOptionalConfig(globalOptions.config, globalOptions.verbose, ui);
1421
+ const repoInput = resolveRepoInput(options.repo, config);
1422
+ const providerId = resolveProviderId(options.provider, config);
1423
+ const totalBudget = resolveBudget(options.tokens, config);
1424
+ const minPriority = config?.discovery.minPriority ?? 20;
1425
+ const scannerSelection = selectScannersFromConfig(config);
1426
+ ensureGitHubAuth();
1427
+ const resolveSpinner = createSpinner(outputJson, "Resolving repository...");
1428
+ const resolvedRepo = await resolveRepo(repoInput);
1429
+ resolveSpinner?.succeed(`Resolved ${resolvedRepo.fullName}`);
1430
+ const cloneSpinner = createSpinner(outputJson, "Preparing local clone...");
1431
+ await cloneRepo(resolvedRepo);
1432
+ cloneSpinner?.succeed(`Repository ready at ${resolvedRepo.localPath}`);
1433
+ const scanSpinner = createSpinner(
1434
+ outputJson,
1435
+ `Running scanners: ${scannerSelection.enabled.join(", ")}`
1436
+ );
1437
+ const scannedTasks = await scannerSelection.scanner.scan(resolvedRepo.localPath, {
1438
+ exclude: config?.discovery.exclude,
1439
+ maxTasks: config?.discovery.maxTasks,
1440
+ repo: resolvedRepo
1441
+ });
1442
+ scanSpinner?.succeed(`Discovered ${scannedTasks.length} raw task(s)`);
1443
+ const rankedTasks = rankTasks(scannedTasks).filter((task) => task.priority >= minPriority);
1444
+ const estimateSpinner = createSpinner(
1445
+ outputJson,
1446
+ `Estimating tokens for ${rankedTasks.length} task(s)...`
1447
+ );
1448
+ const estimates = await estimateTaskMap(rankedTasks, providerId);
1449
+ estimateSpinner?.succeed("Token estimation completed");
1450
+ const plan = buildExecutionPlan(rankedTasks, estimates, totalBudget);
1451
+ if (outputJson) {
1452
+ console.log(
1453
+ JSON.stringify(
1454
+ {
1455
+ repo: resolvedRepo.fullName,
1456
+ provider: providerId,
1457
+ budget: totalBudget,
1458
+ tasksDiscovered: rankedTasks.length,
1459
+ plan
1460
+ },
1461
+ null,
1462
+ 2
1463
+ )
1464
+ );
1465
+ return;
1466
+ }
1467
+ renderPlan(ui, {
1468
+ repo: resolvedRepo.fullName,
1469
+ provider: providerId,
1470
+ budget: totalBudget,
1471
+ plan
1472
+ });
1473
+ });
1474
+ command.addHelpText(
1475
+ "after",
1476
+ `
1477
+ Examples:
1478
+ $ oac plan --repo owner/repo --tokens 100000
1479
+ $ oac plan --repo owner/repo --provider codex`
1480
+ );
1481
+ return command;
1482
+ }
1483
+ function selectScannersFromConfig(config) {
1484
+ const enabled = [];
1485
+ if (config?.discovery.scanners.lint !== false) {
1486
+ enabled.push("lint");
1487
+ }
1488
+ if (config?.discovery.scanners.todo !== false) {
1489
+ enabled.push("todo");
1490
+ }
1491
+ if (enabled.length === 0) {
1492
+ enabled.push("lint", "todo");
1493
+ }
1494
+ const uniqueEnabled = [...new Set(enabled)];
1495
+ const scannerInstances = uniqueEnabled.map(
1496
+ (scannerName) => scannerName === "lint" ? new LintScanner() : new TodoScanner()
1497
+ );
1498
+ return {
1499
+ enabled: uniqueEnabled,
1500
+ scanner: new CompositeScanner(scannerInstances)
1501
+ };
1502
+ }
1503
+ function renderPlan(ui, data) {
1504
+ const table = new Table4({
1505
+ head: ["#", "Task", "Est. Tokens", "Cumulative", "Confidence"]
1506
+ });
1507
+ for (let index = 0; index < data.plan.selectedTasks.length; index += 1) {
1508
+ const entry = data.plan.selectedTasks[index];
1509
+ table.push([
1510
+ String(index + 1),
1511
+ truncate(entry.task.title, 56),
1512
+ formatInteger(entry.estimate.totalEstimatedTokens),
1513
+ formatInteger(entry.cumulativeBudgetUsed),
1514
+ entry.estimate.confidence.toFixed(2)
1515
+ ]);
1516
+ }
1517
+ console.log(ui.bold(`Execution Plan for ${data.repo}`));
1518
+ console.log(`Provider: ${data.provider}`);
1519
+ console.log("");
1520
+ if (data.plan.selectedTasks.length > 0) {
1521
+ console.log(table.toString());
1522
+ console.log("");
1523
+ } else {
1524
+ console.log(ui.yellow("No tasks selected for execution."));
1525
+ console.log("");
1526
+ }
1527
+ const effectiveBudget = data.plan.totalBudget - data.plan.reserveTokens;
1528
+ const budgetUsed = data.plan.selectedTasks[data.plan.selectedTasks.length - 1]?.cumulativeBudgetUsed ?? 0;
1529
+ console.log(
1530
+ `Budget used: ${formatInteger(budgetUsed)} / ${formatInteger(effectiveBudget)} (effective)`
1531
+ );
1532
+ console.log(`Reserve: ${formatInteger(data.plan.reserveTokens)} (10%)`);
1533
+ console.log(`Remaining: ${formatInteger(data.plan.remainingTokens)}`);
1534
+ if (data.plan.deferredTasks.length > 0) {
1535
+ console.log("");
1536
+ console.log(ui.yellow(`Deferred (${data.plan.deferredTasks.length}):`));
1537
+ for (const deferred of data.plan.deferredTasks) {
1538
+ const reason = deferred.reason.replaceAll("_", " ");
1539
+ console.log(
1540
+ ` - ${truncate(deferred.task.title, 72)} (${formatInteger(
1541
+ deferred.estimate.totalEstimatedTokens
1542
+ )} tokens, ${reason})`
1543
+ );
1544
+ }
1545
+ }
1546
+ }
1547
+
1548
+ // src/cli/commands/run/index.ts
1549
+ import { Command as Command10 } from "commander";
1550
+
1551
+ // src/cli/commands/run/pipeline.ts
1552
+ import { randomUUID } from "crypto";
1553
+
1554
+ // src/cli/commands/run/epic.ts
1555
+ import Table6 from "cli-table3";
1556
+ import PQueue3 from "p-queue";
1557
+
1558
+ // src/cli/commands/run/pr.ts
1559
+ import { execa } from "execa";
1560
+
1561
+ // src/cli/commands/run/types.ts
1562
+ var DEFAULT_TIMEOUT_SECONDS = 300;
1563
+ var DEFAULT_CONCURRENCY = 2;
1564
+ var PR_CREATION_TIMEOUT_MS = 12e4;
1565
+ var ConfigError = class extends Error {
1566
+ constructor(message) {
1567
+ super(message);
1568
+ this.name = "ConfigError";
1569
+ }
1570
+ };
1571
+ var EXIT_SUCCESS = 0;
1572
+ var EXIT_GENERAL_ERROR = 1;
1573
+ var EXIT_CONFIG_ERROR = 2;
1574
+ var EXIT_ALL_FAILED = 3;
1575
+ var EXIT_PARTIAL_SUCCESS = 4;
1576
+ function resolveExitCode(results) {
1577
+ if (results.length === 0) return EXIT_SUCCESS;
1578
+ const succeeded = results.filter((r) => r.execution.success).length;
1579
+ if (succeeded === results.length) return EXIT_SUCCESS;
1580
+ if (succeeded === 0) return EXIT_ALL_FAILED;
1581
+ return EXIT_PARTIAL_SUCCESS;
1582
+ }
1583
+ function formatBudgetDisplay(budget) {
1584
+ if (budget >= UNLIMITED_BUDGET) {
1585
+ return "unlimited";
1586
+ }
1587
+ return formatInteger(budget);
1588
+ }
1589
+ function formatDuration(seconds) {
1590
+ if (!Number.isFinite(seconds) || seconds < 0) {
1591
+ return "0s";
1592
+ }
1593
+ if (seconds < 60) {
1594
+ return `${seconds.toFixed(1)}s`;
1595
+ }
1596
+ const minutes = Math.floor(seconds / 60);
1597
+ const remainingSeconds = Math.round(seconds % 60);
1598
+ return `${minutes}m ${remainingSeconds}s`;
1599
+ }
1600
+
1601
+ // src/cli/commands/run/pr.ts
1602
+ async function createPullRequest(input2) {
1603
+ if (!input2.sandbox) {
1604
+ return void 0;
1605
+ }
1606
+ const { branchName, sandboxPath } = input2.sandbox;
1607
+ try {
1608
+ const ghEnv = { ...process.env };
1609
+ if (input2.ghToken) {
1610
+ ghEnv.GH_TOKEN = input2.ghToken;
1611
+ ghEnv.GITHUB_TOKEN = input2.ghToken;
1612
+ }
1613
+ await execa("git", ["push", "--set-upstream", "origin", branchName], {
1614
+ cwd: sandboxPath,
1615
+ env: ghEnv,
1616
+ timeout: PR_CREATION_TIMEOUT_MS
1617
+ });
1618
+ const prTitle = `[OAC] ${input2.task.title}`;
1619
+ const prBodyLines = [
1620
+ "## Summary",
1621
+ "",
1622
+ input2.task.description || `Automated contribution for task "${input2.task.title}".`,
1623
+ ""
1624
+ ];
1625
+ if (input2.task.linkedIssue) {
1626
+ prBodyLines.push(`Closes #${input2.task.linkedIssue.number}`, "");
1627
+ }
1628
+ prBodyLines.push(
1629
+ "## Context",
1630
+ "",
1631
+ `- **Task source:** ${input2.task.source}`,
1632
+ `- **Complexity:** ${input2.task.complexity}`,
1633
+ `- **Tokens used:** ${input2.execution.totalTokensUsed}`,
1634
+ `- **Files changed:** ${input2.execution.filesChanged.length}`
1635
+ );
1636
+ if (input2.task.linkedIssue) {
1637
+ prBodyLines.push(`- **Resolves:** #${input2.task.linkedIssue.number}`);
1638
+ }
1639
+ prBodyLines.push(
1640
+ "",
1641
+ "---",
1642
+ "*This PR was automatically generated by [OAC](https://github.com/Open330/open-agent-contribution).*"
1643
+ );
1644
+ const prBody = prBodyLines.join("\n");
1645
+ const ghResult = await execa(
1646
+ "gh",
1647
+ [
1648
+ "pr",
1649
+ "create",
1650
+ "--repo",
1651
+ input2.repoFullName,
1652
+ "--title",
1653
+ prTitle,
1654
+ "--body",
1655
+ prBody,
1656
+ "--head",
1657
+ branchName,
1658
+ "--base",
1659
+ input2.baseBranch
1660
+ ],
1661
+ { cwd: sandboxPath, env: ghEnv, timeout: PR_CREATION_TIMEOUT_MS }
1662
+ );
1663
+ const prUrl = ghResult.stdout.trim();
1664
+ const prNumberMatch = prUrl.match(/\/pull\/(\d+)/);
1665
+ const prNumber = prNumberMatch ? Number.parseInt(prNumberMatch[1], 10) : 0;
1666
+ return {
1667
+ number: prNumber,
1668
+ url: prUrl,
1669
+ status: "open"
1670
+ };
1671
+ } catch (error) {
1672
+ const message = error instanceof Error ? error.message : String(error);
1673
+ console.warn(`[oac] PR creation failed: ${message}`);
1674
+ return void 0;
1675
+ }
1676
+ }
1677
+
1678
+ // src/cli/commands/run/task.ts
1679
+ import Table5 from "cli-table3";
1680
+ import { execa as execa2 } from "execa";
1681
+ import PQueue2 from "p-queue";
1682
+ async function discoverTasks(ctx, options, config, ghToken, resolvedRepo) {
1683
+ const scannerSelection = selectScannersFromConfig2(config, Boolean(ghToken));
1684
+ const minPriority = config?.discovery.minPriority ?? 20;
1685
+ const maxTasks = options.maxTasks ?? void 0;
1686
+ const scanSpinner = createSpinner(
1687
+ ctx.suppressOutput,
1688
+ `Running scanners: ${scannerSelection.enabled.join(", ")}`
1689
+ );
1690
+ const scannedTasks = await scannerSelection.scanner.scan(resolvedRepo.localPath, {
1691
+ exclude: config?.discovery.exclude,
1692
+ maxTasks: config?.discovery.maxTasks,
1693
+ repo: resolvedRepo
1694
+ });
1695
+ scanSpinner?.succeed(`Discovered ${scannedTasks.length} raw task(s)`);
1696
+ let candidateTasks = rankTasks(scannedTasks).filter((task) => task.priority >= minPriority);
1697
+ if (options.source) {
1698
+ candidateTasks = candidateTasks.filter((task) => task.source === options.source);
1699
+ }
1700
+ if (typeof maxTasks === "number") {
1701
+ candidateTasks = candidateTasks.slice(0, maxTasks);
1702
+ }
1703
+ const estimateSpinner = createSpinner(
1704
+ ctx.suppressOutput,
1705
+ `Estimating tokens for ${candidateTasks.length} task(s)...`
1706
+ );
1707
+ const estimates = candidateTasks.length > 0 ? await estimateTaskMap(
1708
+ candidateTasks,
1709
+ resolveProviderId(options.provider, config),
1710
+ (done, total) => {
1711
+ if (estimateSpinner) {
1712
+ const pct = Math.round(done / total * 100);
1713
+ estimateSpinner.text = `Estimating tokens... (${done}/${total} \u2014 ${pct}%)`;
1714
+ }
1715
+ }
1716
+ ) : /* @__PURE__ */ new Map();
1717
+ if (candidateTasks.length > 0) estimateSpinner?.succeed("Token estimation completed");
1718
+ else estimateSpinner?.stop();
1719
+ const plan = buildExecutionPlan(candidateTasks, estimates, resolveBudget(options.tokens, config));
1720
+ return { ...resolvedRepo, candidateTasks, plan, fullName: resolvedRepo.fullName };
1721
+ }
1722
+ function printEmptySummary(ctx, repoName, providerId, totalBudget) {
1723
+ const emptySummary = {
1724
+ runId: ctx.runId,
1725
+ repo: repoName,
1726
+ provider: providerId,
1727
+ dryRun: Boolean(ctx.options.dryRun),
1728
+ selectedTasks: 0,
1729
+ deferredTasks: 0,
1730
+ tasksCompleted: 0,
1731
+ tasksFailed: 0,
1732
+ prsCreated: 0,
1733
+ tokensUsed: 0,
1734
+ tokensBudgeted: totalBudget
1735
+ };
1736
+ if (ctx.outputJson) {
1737
+ console.log(JSON.stringify({ summary: emptySummary, plan: null }, null, 2));
1738
+ } else {
1739
+ console.log(ctx.ui.yellow("No tasks discovered for execution."));
1740
+ }
1741
+ }
1742
+ function printDryRunSummary(ctx, repoName, providerId, totalBudget, plan) {
1743
+ const dryRunSummary = {
1744
+ runId: ctx.runId,
1745
+ repo: repoName,
1746
+ provider: providerId,
1747
+ dryRun: true,
1748
+ selectedTasks: plan.selectedTasks.length,
1749
+ deferredTasks: plan.deferredTasks.length,
1750
+ tasksCompleted: 0,
1751
+ tasksFailed: 0,
1752
+ prsCreated: 0,
1753
+ tokensUsed: 0,
1754
+ tokensBudgeted: totalBudget
1755
+ };
1756
+ if (ctx.outputJson) {
1757
+ console.log(JSON.stringify({ summary: dryRunSummary, plan }, null, 2));
1758
+ } else {
1759
+ renderSelectedPlanTable(ctx.ui, plan, totalBudget);
1760
+ console.log("");
1761
+ renderDryRunDiff(ctx.ui, plan);
1762
+ console.log(ctx.ui.blue("Dry run complete. No tasks were executed."));
1763
+ }
1764
+ }
1765
+ function renderDryRunDiff(ui, plan) {
1766
+ if (plan.selectedTasks.length === 0) return;
1767
+ console.log(ui.bold("Planned changes:"));
1768
+ console.log("");
1769
+ for (const entry of plan.selectedTasks) {
1770
+ const { task } = entry;
1771
+ const sourceLabel = task.source.replace(/-/g, " ");
1772
+ const complexityColor = task.complexity === "trivial" || task.complexity === "simple" ? ui.green : task.complexity === "moderate" ? ui.yellow : ui.red;
1773
+ console.log(`${ui.green("+")} ${ui.bold(task.title)}`);
1774
+ console.log(` ${ui.dim(`source: ${sourceLabel} complexity: `)}${complexityColor(task.complexity)}`);
1775
+ if (task.targetFiles.length > 0) {
1776
+ for (const file of task.targetFiles.slice(0, 5)) {
1777
+ console.log(` ${ui.yellow("~")} ${file}`);
1778
+ }
1779
+ if (task.targetFiles.length > 5) {
1780
+ console.log(` ${ui.dim(` ... and ${task.targetFiles.length - 5} more files`)}`);
1781
+ }
1782
+ }
1783
+ if (task.description) {
1784
+ const preview = task.description.length > 120 ? `${task.description.slice(0, 117)}...` : task.description;
1785
+ console.log(` ${ui.dim(preview)}`);
1786
+ }
1787
+ console.log("");
1788
+ }
1789
+ }
1790
+ async function executePlan(ctx, params) {
1791
+ const { plan, providerId, resolvedRepo, concurrency, timeoutSeconds, mode, ghToken } = params;
1792
+ const { adapter } = await resolveAdapter(providerId);
1793
+ if (!ctx.suppressOutput && ctx.globalOptions.verbose) {
1794
+ const avail = await adapter.checkAvailability();
1795
+ console.log(
1796
+ ctx.ui.green(`[oac] Using ${adapter.name} v${avail.version ?? "unknown"} for execution.`)
1797
+ );
1798
+ }
1799
+ const executionSpinner = createSpinner(
1800
+ ctx.suppressOutput,
1801
+ `Executing ${plan.selectedTasks.length} planned task(s)...`
1802
+ );
1803
+ let completedCount = 0;
1804
+ const taskQueue = new PQueue2({ concurrency });
1805
+ const executedTasks = await Promise.all(
1806
+ plan.selectedTasks.map(
1807
+ (entry) => taskQueue.add(async () => {
1808
+ const result = await executeWithAgent({
1809
+ task: entry.task,
1810
+ estimate: entry.estimate,
1811
+ adapter,
1812
+ repoPath: resolvedRepo.localPath,
1813
+ baseBranch: resolvedRepo.meta.defaultBranch,
1814
+ timeoutSeconds
1815
+ });
1816
+ const { execution, sandbox } = result;
1817
+ completedCount += 1;
1818
+ if (executionSpinner) {
1819
+ const total = plan.selectedTasks.length;
1820
+ const pct = Math.round(completedCount / total * 100);
1821
+ executionSpinner.text = `Executing tasks... (${completedCount}/${total} \u2014 ${pct}%)`;
1822
+ }
1823
+ return { task: entry.task, estimate: entry.estimate, execution, sandbox };
1824
+ })
1825
+ )
1826
+ );
1827
+ executionSpinner?.succeed("Execution stage finished");
1828
+ const completionSpinner = createSpinner(ctx.suppressOutput, "Completing task outputs...");
1829
+ const completionQueue = new PQueue2({ concurrency });
1830
+ const completedTasks = await Promise.all(
1831
+ executedTasks.map(
1832
+ (result) => completionQueue.add(async () => {
1833
+ if (mode === "direct-commit" || !result.execution.success) return result;
1834
+ const pr = await createPullRequest({
1835
+ task: result.task,
1836
+ execution: result.execution,
1837
+ sandbox: result.sandbox,
1838
+ repoFullName: resolvedRepo.fullName,
1839
+ baseBranch: resolvedRepo.meta.defaultBranch,
1840
+ ghToken
1841
+ });
1842
+ return pr ? { ...result, pr } : result;
1843
+ })
1844
+ )
1845
+ );
1846
+ completionSpinner?.succeed("Completion stage finished");
1847
+ return completedTasks;
1848
+ }
1849
+ function printFinalSummary(ctx, params) {
1850
+ const { plan, resolvedRepo, providerId, totalBudget, completedTasks } = params;
1851
+ const tasksCompleted = completedTasks.filter((t) => t.execution.success).length;
1852
+ const tasksFailed = completedTasks.length - tasksCompleted;
1853
+ const prsCreated = completedTasks.filter((t) => Boolean(t.pr)).length;
1854
+ const tokensUsed = completedTasks.reduce((sum, t) => sum + t.execution.totalTokensUsed, 0);
1855
+ const runDurationSeconds = (Date.now() - ctx.runStartedAt) / 1e3;
1856
+ const summary = {
1857
+ runId: ctx.runId,
1858
+ repo: resolvedRepo.fullName,
1859
+ provider: providerId,
1860
+ dryRun: false,
1861
+ selectedTasks: plan.selectedTasks.length,
1862
+ deferredTasks: plan.deferredTasks.length,
1863
+ tasksCompleted,
1864
+ tasksFailed,
1865
+ prsCreated,
1866
+ tokensUsed,
1867
+ tokensBudgeted: totalBudget,
1868
+ logPath: params.logPath
1869
+ };
1870
+ if (ctx.outputJson) {
1871
+ console.log(JSON.stringify({ summary, plan, tasks: completedTasks }, null, 2));
1872
+ return;
1873
+ }
1874
+ if (!ctx.globalOptions.quiet) {
1875
+ renderTaskResults(ctx.ui, completedTasks);
1876
+ }
1877
+ console.log("");
1878
+ console.log(ctx.ui.bold("Run Summary"));
1879
+ console.log(` Tasks completed: ${tasksCompleted}/${completedTasks.length}`);
1880
+ console.log(` Tasks failed: ${tasksFailed}`);
1881
+ console.log(` PRs created: ${prsCreated}`);
1882
+ console.log(
1883
+ ` Tokens used: ${formatInteger(tokensUsed)} / ${formatBudgetDisplay(totalBudget)}`
1884
+ );
1885
+ console.log(` Duration: ${formatDuration(runDurationSeconds)}`);
1886
+ if (params.logPath) {
1887
+ console.log(` Log: ${params.logPath}`);
1888
+ }
1889
+ const failedTasks = completedTasks.filter((t) => !t.execution.success);
1890
+ if (failedTasks.length > 0) {
1891
+ console.log("");
1892
+ console.log(ctx.ui.red(`Failed Tasks (${failedTasks.length}):`));
1893
+ for (const t of failedTasks) {
1894
+ const reason = t.execution.error ? `: ${truncate(t.execution.error, 120)}` : "";
1895
+ console.log(` ${ctx.ui.red("\u2717")} ${truncate(t.task.title, 60)}${reason}`);
1896
+ }
1897
+ }
1898
+ }
1899
+ function selectScannersFromConfig2(config, hasGitHubAuth) {
1900
+ const { names, composite } = buildScanners(config, hasGitHubAuth);
1901
+ return { enabled: names, scanner: composite };
1902
+ }
1903
+ async function executeWithAgent(input2) {
1904
+ const startedAt = Date.now();
1905
+ const taskSlug = input2.task.id.replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").slice(0, 30);
1906
+ const branchName = `oac/${Date.now()}-${taskSlug}`;
1907
+ const sandbox = await createSandbox(input2.repoPath, branchName, input2.baseBranch);
1908
+ const eventBus = createEventBus();
1909
+ const sandboxInfo = {
1910
+ branchName,
1911
+ sandboxPath: sandbox.path,
1912
+ cleanup: sandbox.cleanup
1913
+ };
1914
+ try {
1915
+ const result = await executeTask(input2.adapter, input2.task, sandbox, eventBus, {
1916
+ tokenBudget: input2.estimate.totalEstimatedTokens,
1917
+ timeoutMs: input2.timeoutSeconds * 1e3
1918
+ });
1919
+ const commitResult = await commitSandboxChanges(sandbox.path, input2.task);
1920
+ const filesChanged = commitResult.filesChanged.length > 0 ? commitResult.filesChanged : result.filesChanged.length > 0 ? result.filesChanged : [];
1921
+ return {
1922
+ execution: {
1923
+ success: result.success || commitResult.hasChanges,
1924
+ exitCode: result.exitCode,
1925
+ totalTokensUsed: result.totalTokensUsed,
1926
+ filesChanged,
1927
+ duration: result.duration > 0 ? result.duration / 1e3 : (Date.now() - startedAt) / 1e3,
1928
+ error: result.error
1929
+ },
1930
+ sandbox: sandboxInfo
1931
+ };
1932
+ } catch (error) {
1933
+ const commitResult = await commitSandboxChanges(sandbox.path, input2.task);
1934
+ if (commitResult.hasChanges) {
1935
+ return {
1936
+ execution: {
1937
+ success: true,
1938
+ exitCode: 0,
1939
+ totalTokensUsed: 0,
1940
+ filesChanged: commitResult.filesChanged,
1941
+ duration: (Date.now() - startedAt) / 1e3
1942
+ },
1943
+ sandbox: sandboxInfo
1944
+ };
1945
+ }
1946
+ const message = error instanceof Error ? error.message : String(error);
1947
+ return {
1948
+ execution: {
1949
+ success: false,
1950
+ exitCode: 1,
1951
+ totalTokensUsed: 0,
1952
+ filesChanged: [],
1953
+ duration: (Date.now() - startedAt) / 1e3,
1954
+ error: message
1955
+ },
1956
+ sandbox: sandboxInfo
1957
+ };
1958
+ }
1959
+ }
1960
+ async function commitSandboxChanges(sandboxPath, task) {
1961
+ try {
1962
+ const statusResult = await execa2("git", ["status", "--porcelain"], { cwd: sandboxPath });
1963
+ if (!statusResult.stdout.trim()) {
1964
+ return { hasChanges: false, filesChanged: [] };
1965
+ }
1966
+ await execa2("git", ["add", "-A"], { cwd: sandboxPath });
1967
+ await execa2(
1968
+ "git",
1969
+ ["commit", "-m", `[OAC] ${task.title}
1970
+
1971
+ Automated contribution by OAC using Codex CLI.`],
1972
+ { cwd: sandboxPath }
1973
+ );
1974
+ const diffResult = await execa2("git", ["diff", "--name-only", "HEAD~1", "HEAD"], {
1975
+ cwd: sandboxPath
1976
+ });
1977
+ const changedFiles = diffResult.stdout.trim().split("\n").filter(Boolean);
1978
+ return { hasChanges: true, filesChanged: changedFiles };
1979
+ } catch {
1980
+ return { hasChanges: false, filesChanged: [] };
1981
+ }
1982
+ }
1983
+ async function resolveAdapter(providerId) {
1984
+ const normalizedId = adapterRegistry.resolveId(providerId);
1985
+ const factory = adapterRegistry.get(providerId);
1986
+ if (!factory) {
1987
+ const supported = adapterRegistry.registeredIds().join(", ");
1988
+ throw new Error(
1989
+ `Unknown provider "${providerId}". Supported providers: ${supported}.
1990
+ Run \`oac doctor\` to check your environment setup.`
1991
+ );
1992
+ }
1993
+ const adapter = factory();
1994
+ const availability = await adapter.checkAvailability();
1995
+ if (!availability.available) {
1996
+ throw new Error(
1997
+ `Agent CLI "${normalizedId}" is not available: ${availability.error ?? "unknown reason"}.
1998
+ Install the ${normalizedId} CLI or switch providers.
1999
+ Run \`oac doctor\` for setup instructions.`
2000
+ );
2001
+ }
2002
+ return { adapter };
2003
+ }
2004
+ function renderSelectedPlanTable(ui, plan, budget) {
2005
+ const table = new Table5({
2006
+ head: ["#", "Task", "Est. Tokens", "Cumulative", "Confidence"]
2007
+ });
2008
+ for (let index = 0; index < plan.selectedTasks.length; index += 1) {
2009
+ const entry = plan.selectedTasks[index];
2010
+ table.push([
2011
+ String(index + 1),
2012
+ truncate(entry.task.title, 56),
2013
+ formatInteger(entry.estimate.totalEstimatedTokens),
2014
+ formatInteger(entry.cumulativeBudgetUsed),
2015
+ entry.estimate.confidence.toFixed(2)
2016
+ ]);
2017
+ }
2018
+ if (plan.selectedTasks.length > 0) {
2019
+ console.log(table.toString());
2020
+ } else {
2021
+ console.log(ui.yellow("No tasks selected for execution."));
2022
+ }
2023
+ console.log("");
2024
+ console.log(
2025
+ `Budget used: ${formatInteger(
2026
+ plan.selectedTasks[plan.selectedTasks.length - 1]?.cumulativeBudgetUsed ?? 0
2027
+ )} / ${formatBudgetDisplay(budget - plan.reserveTokens)} (effective)`
2028
+ );
2029
+ console.log(`Reserve: ${formatBudgetDisplay(plan.reserveTokens)} (10%)`);
2030
+ console.log(`Remaining: ${formatBudgetDisplay(plan.remainingTokens)}`);
2031
+ if (plan.deferredTasks.length > 0) {
2032
+ console.log("");
2033
+ console.log(ui.yellow(`Deferred (${plan.deferredTasks.length}):`));
2034
+ for (const deferred of plan.deferredTasks) {
2035
+ console.log(
2036
+ ` - ${truncate(deferred.task.title, 72)} (${formatInteger(
2037
+ deferred.estimate.totalEstimatedTokens
2038
+ )} tokens, ${deferred.reason.replaceAll("_", " ")})`
2039
+ );
2040
+ }
2041
+ }
2042
+ }
2043
+ function renderTaskResults(ui, taskResults) {
2044
+ for (let index = 0; index < taskResults.length; index += 1) {
2045
+ const result = taskResults[index];
2046
+ const icon = result.execution.success ? ui.green("[OK]") : ui.red("[X]");
2047
+ const status = result.execution.success ? ui.green("SUCCESS") : ui.red("FAILED");
2048
+ console.log(`${icon} [${index + 1}/${taskResults.length}] ${result.task.title}`);
2049
+ console.log(
2050
+ ` ${status} | tokens ${formatInteger(result.execution.totalTokensUsed)} | duration ${formatDuration(
2051
+ result.execution.duration
2052
+ )}`
2053
+ );
2054
+ if (result.pr) {
2055
+ console.log(` PR #${result.pr.number}: ${result.pr.url}`);
2056
+ }
2057
+ if (result.execution.error) {
2058
+ console.log(` Error: ${result.execution.error}`);
2059
+ }
2060
+ }
2061
+ }
2062
+
2063
+ // src/cli/commands/run/tracking.ts
2064
+ async function writeTracking(ctx, params) {
2065
+ const { resolvedRepo, providerId, totalBudget, candidateTasks, completedTasks } = params;
2066
+ const runDurationSeconds = (Date.now() - ctx.runStartedAt) / 1e3;
2067
+ const contributionLog = buildContributionLog({
2068
+ runId: ctx.runId,
2069
+ repoFullName: resolvedRepo.fullName,
2070
+ repoHeadSha: resolvedRepo.git.headSha,
2071
+ defaultBranch: resolvedRepo.meta.defaultBranch,
2072
+ repoOwner: resolvedRepo.owner,
2073
+ providerId,
2074
+ totalBudget,
2075
+ runDurationSeconds,
2076
+ discoveredTasks: candidateTasks.length,
2077
+ taskResults: completedTasks
2078
+ });
2079
+ const trackingSpinner = createSpinner(ctx.suppressOutput, "Writing contribution log...");
2080
+ try {
2081
+ const logPath = await writeContributionLog(contributionLog, resolvedRepo.localPath);
2082
+ trackingSpinner?.succeed(`Contribution log written: ${logPath}`);
2083
+ return logPath;
2084
+ } catch (error) {
2085
+ trackingSpinner?.fail("Failed to write contribution log");
2086
+ if (ctx.globalOptions.verbose && !ctx.suppressOutput) {
2087
+ const message = error instanceof Error ? error.message : String(error);
2088
+ console.warn(ctx.ui.yellow(`[oac] Tracking failed: ${message}`));
2089
+ }
2090
+ return void 0;
2091
+ }
2092
+ }
2093
+ function buildContributionLog(input2) {
2094
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2095
+ const contributor = resolveGithubUsername(input2.repoOwner);
2096
+ const contributionTasks = input2.taskResults.map((result) => ({
2097
+ taskId: result.task.id,
2098
+ title: result.task.title,
2099
+ source: result.task.source,
2100
+ complexity: result.task.complexity,
2101
+ status: deriveTaskStatus(result.execution),
2102
+ tokensUsed: Math.max(0, Math.floor(result.execution.totalTokensUsed)),
2103
+ duration: Math.max(0, result.execution.duration),
2104
+ filesChanged: result.execution.filesChanged,
2105
+ pr: result.pr,
2106
+ linkedIssue: result.task.linkedIssue ? {
2107
+ number: result.task.linkedIssue.number,
2108
+ url: result.task.linkedIssue.url
2109
+ } : void 0,
2110
+ error: result.execution.error
2111
+ }));
2112
+ const tasksSucceeded = contributionTasks.filter((task) => task.status !== "failed").length;
2113
+ const tasksFailed = contributionTasks.length - tasksSucceeded;
2114
+ const totalTokensUsed = contributionTasks.reduce((sum, task) => sum + task.tokensUsed, 0);
2115
+ const totalFilesChanged = contributionTasks.reduce(
2116
+ (sum, task) => sum + task.filesChanged.length,
2117
+ 0
2118
+ );
2119
+ return {
2120
+ version: "1.0",
2121
+ runId: input2.runId,
2122
+ timestamp,
2123
+ contributor: {
2124
+ githubUsername: contributor,
2125
+ email: process.env.GIT_AUTHOR_EMAIL ?? void 0
2126
+ },
2127
+ repo: {
2128
+ fullName: input2.repoFullName,
2129
+ headSha: input2.repoHeadSha,
2130
+ defaultBranch: input2.defaultBranch
2131
+ },
2132
+ budget: {
2133
+ provider: input2.providerId,
2134
+ totalTokensBudgeted: input2.totalBudget,
2135
+ totalTokensUsed
2136
+ },
2137
+ tasks: contributionTasks,
2138
+ metrics: {
2139
+ tasksDiscovered: input2.discoveredTasks,
2140
+ tasksAttempted: contributionTasks.length,
2141
+ tasksSucceeded,
2142
+ tasksFailed,
2143
+ totalDuration: Math.max(0, input2.runDurationSeconds),
2144
+ totalFilesChanged,
2145
+ totalLinesAdded: 0,
2146
+ totalLinesRemoved: 0
2147
+ }
2148
+ };
2149
+ }
2150
+ function deriveTaskStatus(execution) {
2151
+ if (execution.success) {
2152
+ return "success";
2153
+ }
2154
+ if (execution.filesChanged.length > 0) {
2155
+ return "partial";
2156
+ }
2157
+ return "failed";
2158
+ }
2159
+ function resolveGithubUsername(fallback) {
2160
+ const candidates = [
2161
+ process.env.GITHUB_USER,
2162
+ process.env.GITHUB_USERNAME,
2163
+ process.env.USER,
2164
+ process.env.LOGNAME,
2165
+ fallback,
2166
+ "oac-user"
2167
+ ];
2168
+ for (const candidate of candidates) {
2169
+ const normalized = sanitizeGithubUsername(candidate ?? "");
2170
+ if (normalized) {
2171
+ return normalized;
2172
+ }
2173
+ }
2174
+ return "oac-user";
2175
+ }
2176
+ function sanitizeGithubUsername(value) {
2177
+ const trimmed = value.trim();
2178
+ if (!trimmed) {
2179
+ return null;
2180
+ }
2181
+ const cleaned = trimmed.replace(/[^A-Za-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2182
+ if (cleaned.length === 0 || cleaned.length > 39) {
2183
+ return null;
2184
+ }
2185
+ if (!/^(?!-)[A-Za-z0-9-]+(?<!-)$/.test(cleaned)) {
2186
+ return null;
2187
+ }
2188
+ return cleaned;
2189
+ }
2190
+
2191
+ // src/cli/commands/run/epic.ts
2192
+ async function tryLoadOrAnalyzeEpics(ctx, params) {
2193
+ const { resolvedRepo, config, ghToken, contextDir, staleAfterMs } = params;
2194
+ const existingBacklog = await loadBacklog(resolvedRepo.localPath, contextDir);
2195
+ if (existingBacklog) {
2196
+ const pending = getPendingEpics(existingBacklog);
2197
+ if (pending.length > 0) {
2198
+ const context = await loadContext(resolvedRepo.localPath, contextDir);
2199
+ if (context && !isContextStale(context.codebaseMap, staleAfterMs)) {
2200
+ if (!ctx.suppressOutput) {
2201
+ console.log(ctx.ui.blue(`[oac] Loaded ${pending.length} pending epic(s) from backlog.`));
2202
+ }
2203
+ return pending;
2204
+ }
2205
+ }
2206
+ }
2207
+ if (!params.autoAnalyze) {
2208
+ return null;
2209
+ }
2210
+ const analyzeSpinner = createSpinner(ctx.suppressOutput, "Auto-analyzing codebase...");
2211
+ const scanners = buildScannerList2(config, Boolean(ghToken));
2212
+ const { codebaseMap, qualityReport } = await analyzeCodebase(resolvedRepo.localPath, {
2213
+ scanners,
2214
+ repoFullName: resolvedRepo.fullName,
2215
+ headSha: resolvedRepo.git.headSha,
2216
+ exclude: config?.discovery.exclude
2217
+ });
2218
+ analyzeSpinner?.succeed(
2219
+ `Analyzed ${codebaseMap.modules.length} modules, ${codebaseMap.totalFiles} files, ${qualityReport.findings.length} findings`
2220
+ );
2221
+ if (qualityReport.findings.length === 0) {
2222
+ return null;
2223
+ }
2224
+ const groupSpinner = createSpinner(ctx.suppressOutput, "Grouping findings into epics...");
2225
+ const epics = groupFindingsIntoEpics(qualityReport.findings, { codebaseMap });
2226
+ groupSpinner?.succeed(`Created ${epics.length} epic(s)`);
2227
+ const persistSpinner = createSpinner(ctx.suppressOutput, "Persisting context...");
2228
+ await persistContext(resolvedRepo.localPath, codebaseMap, qualityReport, contextDir);
2229
+ const backlog = createBacklog(resolvedRepo.fullName, resolvedRepo.git.headSha, epics);
2230
+ await persistBacklog(resolvedRepo.localPath, backlog, contextDir);
2231
+ persistSpinner?.succeed(`Context persisted to ${contextDir}/`);
2232
+ return getPendingEpics(backlog);
2233
+ }
2234
+ function buildScannerList2(config, hasGitHubAuth) {
2235
+ return buildScanners(config, hasGitHubAuth).instances;
2236
+ }
2237
+ function makeStubEstimate(taskId, providerId, tokens) {
2238
+ return {
2239
+ taskId,
2240
+ providerId,
2241
+ contextTokens: 0,
2242
+ promptTokens: 0,
2243
+ expectedOutputTokens: 0,
2244
+ totalEstimatedTokens: tokens,
2245
+ confidence: 0.7,
2246
+ feasible: true
2247
+ };
2248
+ }
2249
+ async function executeEpicEntry(entry, params) {
2250
+ const { adapter, resolvedRepo, providerId, timeoutSeconds, mode, ghToken } = params;
2251
+ const task = epicAsTask(entry.epic);
2252
+ const estimate = makeStubEstimate(task.id, providerId, entry.estimatedTokens);
2253
+ const result = await executeWithAgent({
2254
+ task,
2255
+ estimate,
2256
+ adapter,
2257
+ repoPath: resolvedRepo.localPath,
2258
+ baseBranch: resolvedRepo.meta.defaultBranch,
2259
+ timeoutSeconds
2260
+ });
2261
+ const { execution, sandbox } = result;
2262
+ let pr;
2263
+ if (mode !== "direct-commit" && execution.success && sandbox) {
2264
+ pr = await createPullRequest({
2265
+ task,
2266
+ execution,
2267
+ sandbox,
2268
+ repoFullName: resolvedRepo.fullName,
2269
+ baseBranch: resolvedRepo.meta.defaultBranch,
2270
+ ghToken
2271
+ }) ?? void 0;
2272
+ }
2273
+ return { task, estimate, execution, sandbox, pr };
2274
+ }
2275
+ async function runEpicPipeline(ctx, params) {
2276
+ const {
2277
+ epics,
2278
+ resolvedRepo,
2279
+ providerId,
2280
+ totalBudget,
2281
+ concurrency,
2282
+ timeoutSeconds,
2283
+ mode,
2284
+ ghToken,
2285
+ contextDir
2286
+ } = params;
2287
+ const estimateSpinner = createSpinner(
2288
+ ctx.suppressOutput,
2289
+ `Estimating tokens for ${epics.length} epic(s)...`
2290
+ );
2291
+ let estimatedCount = 0;
2292
+ for (const epic of epics) {
2293
+ if (epic.estimatedTokens === 0) {
2294
+ epic.estimatedTokens = await estimateEpicTokens(epic, providerId);
2295
+ }
2296
+ estimatedCount += 1;
2297
+ if (estimateSpinner) {
2298
+ const pct = Math.round(estimatedCount / epics.length * 100);
2299
+ estimateSpinner.text = `Estimating epic tokens... (${estimatedCount}/${epics.length} \u2014 ${pct}%)`;
2300
+ }
2301
+ }
2302
+ estimateSpinner?.succeed("Epic token estimation completed");
2303
+ const epicPlan = buildEpicExecutionPlan(epics, totalBudget);
2304
+ if (!ctx.suppressOutput) {
2305
+ console.log(
2306
+ ctx.ui.blue(
2307
+ `[oac] Selected ${epicPlan.selectedEpics.length} epic(s) for execution, ${epicPlan.deferredEpics.length} deferred.`
2308
+ )
2309
+ );
2310
+ }
2311
+ if (ctx.options.dryRun) {
2312
+ printEpicDryRun(ctx, epicPlan, totalBudget);
2313
+ return [];
2314
+ }
2315
+ const { adapter } = await resolveAdapter(providerId);
2316
+ let epicCompletedCount = 0;
2317
+ const epicTotal = epicPlan.selectedEpics.length;
2318
+ const executionSpinner = createSpinner(
2319
+ ctx.suppressOutput,
2320
+ `Executing ${epicTotal} epic(s)...`
2321
+ );
2322
+ const epicQueue = new PQueue3({ concurrency });
2323
+ const allTaskResults = await Promise.all(
2324
+ epicPlan.selectedEpics.map(
2325
+ (entry) => epicQueue.add(async () => {
2326
+ if (!ctx.suppressOutput) {
2327
+ console.log(
2328
+ ctx.ui.blue(
2329
+ `
2330
+ [oac] Executing epic: ${entry.epic.title} (${entry.epic.subtasks.length} subtasks)`
2331
+ )
2332
+ );
2333
+ }
2334
+ const result = await executeEpicEntry(entry, {
2335
+ adapter,
2336
+ resolvedRepo,
2337
+ providerId,
2338
+ timeoutSeconds,
2339
+ mode,
2340
+ ghToken
2341
+ });
2342
+ epicCompletedCount += 1;
2343
+ if (executionSpinner) {
2344
+ const pct = Math.round(epicCompletedCount / epicTotal * 100);
2345
+ executionSpinner.text = `Executing epics... (${epicCompletedCount}/${epicTotal} \u2014 ${pct}%)`;
2346
+ }
2347
+ if (!ctx.suppressOutput) {
2348
+ const icon = result.execution.success ? ctx.ui.green("[OK]") : ctx.ui.red("[X]");
2349
+ console.log(`${icon} ${entry.epic.title}`);
2350
+ if (result.pr) console.log(` PR #${result.pr.number}: ${result.pr.url}`);
2351
+ }
2352
+ return result;
2353
+ })
2354
+ )
2355
+ );
2356
+ executionSpinner?.succeed("Epic execution finished");
2357
+ const completedIds = allTaskResults.filter((r) => r.execution.success).map((r) => r.task.id);
2358
+ const existingBacklog = await loadBacklog(resolvedRepo.localPath, contextDir);
2359
+ if (existingBacklog && completedIds.length > 0) {
2360
+ const updated = updateBacklog(existingBacklog, [], completedIds);
2361
+ await persistBacklog(resolvedRepo.localPath, updated, contextDir);
2362
+ }
2363
+ await writeTracking(ctx, {
2364
+ resolvedRepo,
2365
+ providerId,
2366
+ totalBudget,
2367
+ candidateTasks: allTaskResults.map((r) => r.task),
2368
+ completedTasks: allTaskResults
2369
+ });
2370
+ printEpicSummary(ctx, epicPlan, allTaskResults, resolvedRepo.fullName, providerId, totalBudget);
2371
+ return allTaskResults;
2372
+ }
2373
+ function printEpicDryRun(ctx, epicPlan, totalBudget) {
2374
+ if (ctx.outputJson) {
2375
+ console.log(
2376
+ JSON.stringify(
2377
+ { summary: { runId: ctx.runId, dryRun: true, epics: epicPlan }, plan: epicPlan },
2378
+ null,
2379
+ 2
2380
+ )
2381
+ );
2382
+ } else {
2383
+ renderEpicPlanTable(ctx.ui, epicPlan, totalBudget);
2384
+ console.log("");
2385
+ console.log(ctx.ui.blue("Dry run complete. No epics were executed."));
2386
+ }
2387
+ }
2388
+ function printEpicSummary(ctx, epicPlan, results, repoName, providerId, totalBudget) {
2389
+ const completed = results.filter((t) => t.execution.success).length;
2390
+ const failed = results.length - completed;
2391
+ const prsCreated = results.filter((t) => Boolean(t.pr)).length;
2392
+ const tokensUsed = results.reduce((sum, t) => sum + t.execution.totalTokensUsed, 0);
2393
+ const duration = (Date.now() - ctx.runStartedAt) / 1e3;
2394
+ if (ctx.outputJson) {
2395
+ console.log(
2396
+ JSON.stringify(
2397
+ {
2398
+ summary: {
2399
+ runId: ctx.runId,
2400
+ repo: repoName,
2401
+ provider: providerId,
2402
+ dryRun: false,
2403
+ selectedEpics: epicPlan.selectedEpics.length,
2404
+ deferredEpics: epicPlan.deferredEpics.length,
2405
+ epicsCompleted: completed,
2406
+ epicsFailed: failed,
2407
+ prsCreated,
2408
+ tokensUsed,
2409
+ tokensBudgeted: totalBudget
2410
+ },
2411
+ epics: results
2412
+ },
2413
+ null,
2414
+ 2
2415
+ )
2416
+ );
2417
+ return;
2418
+ }
2419
+ console.log("");
2420
+ console.log(ctx.ui.bold("Run Summary (Epic Mode)"));
2421
+ console.log(` Epics completed: ${completed}/${results.length}`);
2422
+ console.log(` Epics failed: ${failed}`);
2423
+ console.log(` PRs created: ${prsCreated}`);
2424
+ console.log(
2425
+ ` Tokens used: ${formatInteger(tokensUsed)} / ${formatBudgetDisplay(totalBudget)}`
2426
+ );
2427
+ console.log(` Duration: ${formatDuration(duration)}`);
2428
+ const failedEpics = results.filter((t) => !t.execution.success);
2429
+ if (failedEpics.length > 0) {
2430
+ console.log("");
2431
+ console.log(ctx.ui.red(`Failed Epics (${failedEpics.length}):`));
2432
+ for (const t of failedEpics) {
2433
+ const reason = t.execution.error ? `: ${truncate(t.execution.error, 120)}` : "";
2434
+ console.log(` ${ctx.ui.red("\u2717")} ${truncate(t.task.title, 60)}${reason}`);
2435
+ }
2436
+ }
2437
+ }
2438
+ function renderEpicPlanTable(ui, plan, budget) {
2439
+ const table = new Table6({
2440
+ head: ["#", "Epic", "Scope", "Subtasks", "Est. Tokens", "Priority"]
2441
+ });
2442
+ for (let i = 0; i < plan.selectedEpics.length; i++) {
2443
+ const entry = plan.selectedEpics[i];
2444
+ table.push([
2445
+ String(i + 1),
2446
+ truncate(entry.epic.title, 45),
2447
+ entry.epic.scope,
2448
+ String(entry.epic.subtasks.length),
2449
+ formatInteger(entry.estimatedTokens),
2450
+ String(entry.epic.priority)
2451
+ ]);
2452
+ }
2453
+ if (plan.selectedEpics.length > 0) {
2454
+ console.log(table.toString());
2455
+ } else {
2456
+ console.log(ui.yellow("No epics selected for execution."));
2457
+ }
2458
+ if (plan.deferredEpics.length > 0) {
2459
+ console.log("");
2460
+ console.log(ui.yellow(`Deferred (${plan.deferredEpics.length}):`));
2461
+ for (const deferred of plan.deferredEpics) {
2462
+ console.log(
2463
+ ` - ${truncate(deferred.epic.title, 60)} (${formatInteger(deferred.estimatedTokens)} tokens)`
2464
+ );
2465
+ }
2466
+ }
2467
+ }
2468
+
2469
+ // src/cli/commands/run/retry.ts
2470
+ import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
2471
+ import { resolve as resolve6 } from "path";
2472
+ async function readMostRecentContributionLog(repoPath) {
2473
+ const contributionsPath = resolve6(repoPath, ".oac", "contributions");
2474
+ let entries;
2475
+ try {
2476
+ const dirEntries = await readdir3(contributionsPath, { withFileTypes: true, encoding: "utf8" });
2477
+ entries = dirEntries.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name).sort((a, b) => b.localeCompare(a));
2478
+ } catch {
2479
+ return void 0;
2480
+ }
2481
+ for (const fileName of entries) {
2482
+ try {
2483
+ const content = await readFile5(resolve6(contributionsPath, fileName), "utf8");
2484
+ const parsed = contributionLogSchema.safeParse(JSON.parse(content));
2485
+ if (parsed.success) return parsed.data;
2486
+ } catch {
2487
+ continue;
2488
+ }
2489
+ }
2490
+ return void 0;
2491
+ }
2492
+ function taskFromContributionEntry(entry) {
2493
+ return {
2494
+ id: entry.taskId,
2495
+ source: entry.source,
2496
+ title: entry.title,
2497
+ description: `Retry of failed task: ${entry.title}`,
2498
+ targetFiles: entry.filesChanged,
2499
+ priority: 100,
2500
+ // high priority — user explicitly chose to retry
2501
+ complexity: entry.complexity,
2502
+ executionMode: "new-pr",
2503
+ metadata: { retryOf: entry.taskId },
2504
+ discoveredAt: (/* @__PURE__ */ new Date()).toISOString()
2505
+ };
2506
+ }
2507
+ async function runRetryPipeline(ctx, params) {
2508
+ const { resolvedRepo, providerId, totalBudget, concurrency, timeoutSeconds, mode, ghToken } = params;
2509
+ const retrySpinner = createSpinner(ctx.suppressOutput, "Loading most recent contribution log...");
2510
+ const log = await readMostRecentContributionLog(resolvedRepo.localPath);
2511
+ if (!log) {
2512
+ retrySpinner?.fail("No contribution logs found in .oac/contributions/");
2513
+ if (!ctx.suppressOutput) {
2514
+ console.log(ctx.ui.yellow("[oac] Run the pipeline at least once before using --retry-failed."));
2515
+ }
2516
+ return [];
2517
+ }
2518
+ const failedEntries = log.tasks.filter((t) => t.status === "failed");
2519
+ if (failedEntries.length === 0) {
2520
+ retrySpinner?.succeed("No failed tasks in the most recent run \u2014 nothing to retry.");
2521
+ return [];
2522
+ }
2523
+ retrySpinner?.succeed(
2524
+ `Found ${failedEntries.length} failed task(s) from run ${log.runId.slice(0, 8)}`
2525
+ );
2526
+ const retryTasks = failedEntries.map(taskFromContributionEntry);
2527
+ const estimates = await estimateTaskMap(retryTasks, providerId);
2528
+ const plan = buildExecutionPlan(retryTasks, estimates, totalBudget);
2529
+ if (plan.selectedTasks.length === 0) {
2530
+ if (!ctx.suppressOutput) {
2531
+ console.log(ctx.ui.yellow("[oac] No retry tasks could be selected within the budget."));
2532
+ }
2533
+ return [];
2534
+ }
2535
+ if (!ctx.suppressOutput) {
2536
+ console.log(
2537
+ ctx.ui.blue(
2538
+ `
2539
+ [oac] Retrying ${plan.selectedTasks.length} failed task(s) (budget: ${formatInteger(totalBudget)} tokens)`
2540
+ )
2541
+ );
2542
+ }
2543
+ const completedTasks = await executePlan(ctx, {
2544
+ plan,
2545
+ providerId,
2546
+ resolvedRepo,
2547
+ concurrency,
2548
+ timeoutSeconds,
2549
+ mode,
2550
+ ghToken
2551
+ });
2552
+ await writeTracking(ctx, {
2553
+ resolvedRepo,
2554
+ providerId,
2555
+ totalBudget,
2556
+ candidateTasks: retryTasks,
2557
+ completedTasks
2558
+ });
2559
+ printFinalSummary(ctx, {
2560
+ plan,
2561
+ resolvedRepo,
2562
+ providerId,
2563
+ totalBudget,
2564
+ completedTasks
2565
+ });
2566
+ return completedTasks;
2567
+ }
2568
+
2569
+ // src/cli/commands/run/pipeline.ts
2570
+ async function runPipeline(options, globalOptions, ui) {
2571
+ const ctx = {
2572
+ options,
2573
+ globalOptions,
2574
+ ui,
2575
+ outputJson: globalOptions.json,
2576
+ suppressOutput: globalOptions.json || globalOptions.quiet,
2577
+ runId: randomUUID(),
2578
+ runStartedAt: Date.now()
2579
+ };
2580
+ const config = await loadOptionalConfig(globalOptions.config, globalOptions.verbose, ui);
2581
+ const providerId = resolveProviderId(options.provider, config);
2582
+ const totalBudget = resolveBudget(options.tokens, config);
2583
+ const mode = resolveMode(options.mode, config);
2584
+ const concurrency = resolveConcurrency(options.concurrency, config);
2585
+ const timeoutSeconds = resolveTimeout(options.timeout, config);
2586
+ const ghToken = ensureGitHubAuth();
2587
+ printGitHubAuthWarnings(ctx, ghToken);
2588
+ printRunHeader(ctx, totalBudget, concurrency);
2589
+ const repoInput = resolveRepoInput(options.repo, config);
2590
+ const resolveSpinner = createSpinner(ctx.suppressOutput, "Resolving repository...");
2591
+ const resolvedRepo = await resolveRepo(repoInput);
2592
+ resolveSpinner?.succeed(`Resolved ${resolvedRepo.fullName}`);
2593
+ const cloneSpinner = createSpinner(ctx.suppressOutput, "Preparing local clone...");
2594
+ await cloneRepo(resolvedRepo);
2595
+ cloneSpinner?.succeed(`Repository ready at ${resolvedRepo.localPath}`);
2596
+ if (options.retryFailed) {
2597
+ const retryResults = await runRetryPipeline(ctx, {
2598
+ resolvedRepo,
2599
+ providerId,
2600
+ totalBudget,
2601
+ concurrency,
2602
+ timeoutSeconds,
2603
+ mode,
2604
+ ghToken
2605
+ });
2606
+ process.exitCode = resolveExitCode(retryResults);
2607
+ return;
2608
+ }
2609
+ const autoAnalyze = config?.analyze?.autoAnalyze ?? true;
2610
+ const contextDir = config?.analyze?.contextDir ?? ".oac/context";
2611
+ const staleAfterMs = config?.analyze?.staleAfterMs ?? 864e5;
2612
+ const epics = await tryLoadOrAnalyzeEpics(ctx, {
2613
+ resolvedRepo,
2614
+ config,
2615
+ ghToken,
2616
+ autoAnalyze,
2617
+ contextDir,
2618
+ staleAfterMs
2619
+ });
2620
+ if (epics && epics.length > 0) {
2621
+ const epicResults = await runEpicPipeline(ctx, {
2622
+ epics,
2623
+ resolvedRepo,
2624
+ config,
2625
+ providerId,
2626
+ totalBudget,
2627
+ concurrency,
2628
+ timeoutSeconds,
2629
+ mode,
2630
+ ghToken,
2631
+ contextDir
2632
+ });
2633
+ process.exitCode = resolveExitCode(epicResults);
2634
+ return;
2635
+ }
2636
+ const { candidateTasks, plan } = await discoverTasks(ctx, options, config, ghToken, resolvedRepo);
2637
+ if (candidateTasks.length === 0) {
2638
+ printEmptySummary(ctx, resolvedRepo.fullName, providerId, totalBudget);
2639
+ return;
2640
+ }
2641
+ if (options.dryRun) {
2642
+ printDryRunSummary(ctx, resolvedRepo.fullName, providerId, totalBudget, plan);
2643
+ return;
2644
+ }
2645
+ const completedTasks = await executePlan(ctx, {
2646
+ plan,
2647
+ providerId,
2648
+ resolvedRepo,
2649
+ concurrency,
2650
+ timeoutSeconds,
2651
+ mode,
2652
+ ghToken
2653
+ });
2654
+ await writeTracking(ctx, {
2655
+ resolvedRepo,
2656
+ providerId,
2657
+ totalBudget,
2658
+ candidateTasks,
2659
+ completedTasks
2660
+ });
2661
+ printFinalSummary(ctx, {
2662
+ plan,
2663
+ resolvedRepo,
2664
+ providerId,
2665
+ totalBudget,
2666
+ completedTasks
2667
+ });
2668
+ process.exitCode = resolveExitCode(completedTasks);
2669
+ }
2670
+ function printGitHubAuthWarnings(ctx, ghToken) {
2671
+ if (ctx.suppressOutput) return;
2672
+ if (!ghToken) {
2673
+ console.log(
2674
+ ctx.ui.yellow("[oac] Warning: GitHub auth not detected. Run `gh auth login` first.")
2675
+ );
2676
+ console.log(
2677
+ ctx.ui.yellow("[oac] For private repos, ensure the 'repo' scope: gh auth refresh -s repo")
2678
+ );
2679
+ } else {
2680
+ const missingScopes = checkGitHubScopes(["repo"]);
2681
+ if (missingScopes.length > 0) {
2682
+ console.log(
2683
+ ctx.ui.yellow(
2684
+ `[oac] Warning: GitHub token missing scope(s): ${missingScopes.join(", ")}. Private repos may fail.`
2685
+ )
2686
+ );
2687
+ console.log(ctx.ui.yellow("[oac] Fix with: gh auth refresh -s repo"));
2688
+ }
2689
+ }
2690
+ }
2691
+ function printRunHeader(ctx, totalBudget, concurrency) {
2692
+ if (ctx.suppressOutput) return;
2693
+ console.log(
2694
+ ctx.ui.blue(
2695
+ `Starting OAC run (budget: ${formatBudgetDisplay(totalBudget)} tokens, concurrency: ${concurrency})`
2696
+ )
2697
+ );
2698
+ }
2699
+ function validateRunOptions(options) {
2700
+ if (typeof options.concurrency === "number" && options.concurrency <= 0) {
2701
+ throw new ConfigError("--concurrency must be greater than zero.");
2702
+ }
2703
+ if (typeof options.timeout === "number" && options.timeout <= 0) {
2704
+ throw new ConfigError("--timeout must be greater than zero.");
2705
+ }
2706
+ if (typeof options.maxTasks === "number" && options.maxTasks <= 0) {
2707
+ throw new ConfigError("--max-tasks must be greater than zero when provided.");
2708
+ }
2709
+ }
2710
+ function resolveMode(modeOption, config) {
2711
+ const candidate = (modeOption ?? config?.execution.mode ?? "new-pr").trim();
2712
+ if (candidate === "new-pr" || candidate === "update-pr" || candidate === "direct-commit") {
2713
+ return candidate;
2714
+ }
2715
+ throw new ConfigError(`Invalid --mode value "${candidate}".`);
2716
+ }
2717
+ function resolveConcurrency(concurrencyOption, config) {
2718
+ const configuredConcurrency = typeof concurrencyOption === "number" ? concurrencyOption : config?.execution.concurrency ?? DEFAULT_CONCURRENCY;
2719
+ if (!Number.isFinite(configuredConcurrency) || configuredConcurrency <= 0) {
2720
+ throw new ConfigError("Concurrency must be a positive integer.");
2721
+ }
2722
+ return Math.floor(configuredConcurrency);
2723
+ }
2724
+ function resolveTimeout(timeoutOption, config) {
2725
+ const configuredTimeout = typeof timeoutOption === "number" ? timeoutOption : config?.execution.taskTimeout ?? DEFAULT_TIMEOUT_SECONDS;
2726
+ if (!Number.isFinite(configuredTimeout) || configuredTimeout <= 0) {
2727
+ throw new ConfigError("Timeout must be a positive integer.");
2728
+ }
2729
+ return Math.floor(configuredTimeout);
2730
+ }
2731
+
2732
+ // src/cli/commands/run/index.ts
2733
+ function parseTokens(value) {
2734
+ if (value.toLowerCase() === "unlimited") {
2735
+ return UNLIMITED_BUDGET;
2736
+ }
2737
+ return parseInteger(value);
2738
+ }
2739
+ function createRunCommand() {
2740
+ const command = new Command10("run");
2741
+ command.alias("r").description(
2742
+ "Run the full OAC pipeline \u2014 analyze, plan, and execute in one command"
2743
+ ).option("--repo <owner/repo>", "Target repository (owner/repo or GitHub URL)").option("--tokens <value>", 'Token budget (number or "unlimited")', parseTokens).option("--provider <id>", "Agent provider id").option("--concurrency <number>", "Maximum parallel task executions", parseInteger).option("--dry-run", "Show plan without executing tasks", false).option("--mode <mode>", "Execution mode: new-pr|update-pr|direct-commit").option("--max-tasks <number>", "Maximum number of discovered tasks to consider", parseInteger).option("--timeout <seconds>", "Per-task timeout in seconds", parseInteger).option("--source <source>", "Filter tasks by source: lint, todo, github-issue, test-gap").option("--retry-failed", "Re-run only failed tasks from the most recent run", false).action(async (options, cmd) => {
2744
+ const globalOptions = getGlobalOptions(cmd);
2745
+ const ui = createUi(globalOptions);
2746
+ validateRunOptions(options);
2747
+ await runPipeline(options, globalOptions, ui);
2748
+ });
2749
+ command.addHelpText(
2750
+ "after",
2751
+ `
2752
+ This is the primary command. It auto-analyzes the codebase, groups findings
2753
+ into epics, and executes them \u2014 no separate scan/analyze step required.
2754
+
2755
+ If no oac.config.ts exists, pass --repo to get started immediately:
2756
+ $ oac run --repo owner/repo
2757
+
2758
+ Examples:
2759
+ $ oac run --repo owner/repo --tokens 50000
2760
+ $ oac run --repo owner/repo --provider codex --concurrency 4
2761
+ $ oac run --repo owner/repo --dry-run
2762
+ $ oac run --repo owner/repo --source lint --max-tasks 10
2763
+ $ oac run --repo owner/repo --retry-failed
2764
+
2765
+ Exit Codes:
2766
+ 0 All tasks/epics completed successfully (or dry-run)
2767
+ 1 Unexpected / unhandled error
2768
+ 2 Configuration or validation error (bad flags, missing repo)
2769
+ 3 All selected tasks/epics failed
2770
+ 4 Partial success \u2014 some tasks succeeded, others failed`
2771
+ );
2772
+ return command;
2773
+ }
2774
+
2775
+ // src/cli/commands/scan.ts
2776
+ import Table7 from "cli-table3";
2777
+ import { Command as Command11 } from "commander";
2778
+ var SUPPORTED_SCANNERS = ["lint", "todo", "github-issues", "test-gap"];
2779
+ function createScanCommand() {
2780
+ const command = new Command11("scan");
2781
+ command.description("Quick task discovery \u2014 list individual issues ranked by priority").option("--repo <owner/repo>", "Target repository (owner/repo or GitHub URL)").option("--scanners <names>", "Comma-separated scanner filter (lint,todo)").option("--min-priority <number>", "Minimum priority threshold (0-100)", parseInteger, 20).option("--format <format>", "Output format: table|json", "table").action(async (options, cmd) => {
2782
+ const globalOptions = getGlobalOptions(cmd);
2783
+ const ui = createUi(globalOptions);
2784
+ const outputFormat = normalizeOutputFormat2(options.format);
2785
+ const outputJson = globalOptions.json || outputFormat === "json";
2786
+ if (options.minPriority < 0 || options.minPriority > 100) {
2787
+ throw new Error("--min-priority must be between 0 and 100.");
2788
+ }
2789
+ const config = await loadOptionalConfig(globalOptions.config, globalOptions.verbose, ui);
2790
+ const repoInput = resolveRepoInput(options.repo, config);
2791
+ const ghToken = ensureGitHubAuth();
2792
+ const scannerSelection = selectScanners(options.scanners, config, Boolean(ghToken));
2793
+ if (!outputJson && scannerSelection.unknown.length > 0) {
2794
+ console.log(
2795
+ ui.yellow(
2796
+ `Ignoring unsupported scanner(s): ${scannerSelection.unknown.join(", ")}. Supported scanners: ${SUPPORTED_SCANNERS.join(", ")}.`
2797
+ )
2798
+ );
2799
+ }
2800
+ const resolveSpinner = createSpinner(outputJson, "Resolving repository...");
2801
+ const resolvedRepo = await resolveRepo(repoInput);
2802
+ resolveSpinner?.succeed(`Resolved ${resolvedRepo.fullName}`);
2803
+ const cloneSpinner = createSpinner(outputJson, "Preparing local clone...");
2804
+ await cloneRepo(resolvedRepo);
2805
+ cloneSpinner?.succeed(`Repository ready at ${resolvedRepo.localPath}`);
2806
+ const scanSpinner = createSpinner(
2807
+ outputJson,
2808
+ `Running scanners: ${scannerSelection.enabled.join(", ")}`
2809
+ );
2810
+ const scannedTasks = await scannerSelection.scanner.scan(resolvedRepo.localPath, {
2811
+ exclude: config?.discovery.exclude,
2812
+ maxTasks: config?.discovery.maxTasks,
2813
+ repo: resolvedRepo
2814
+ });
2815
+ scanSpinner?.succeed(`Scanned ${resolvedRepo.fullName}`);
2816
+ const rankedTasks = rankTasks(scannedTasks).filter(
2817
+ (task) => task.priority >= options.minPriority
2818
+ );
2819
+ if (outputJson) {
2820
+ console.log(
2821
+ JSON.stringify(
2822
+ {
2823
+ repo: resolvedRepo.fullName,
2824
+ scanners: scannerSelection.enabled,
2825
+ minPriority: options.minPriority,
2826
+ totalTasks: rankedTasks.length,
2827
+ tasks: rankedTasks
2828
+ },
2829
+ null,
2830
+ 2
2831
+ )
2832
+ );
2833
+ return;
2834
+ }
2835
+ if (rankedTasks.length === 0) {
2836
+ console.log(ui.yellow("No tasks discovered for the selected criteria."));
2837
+ return;
2838
+ }
2839
+ const table = new Table7({
2840
+ head: ["ID", "Title", "Source", "Priority", "Complexity"]
2841
+ });
2842
+ for (const task of rankedTasks) {
2843
+ table.push([
2844
+ task.id,
2845
+ truncate(task.title, 60),
2846
+ task.source,
2847
+ String(task.priority),
2848
+ task.complexity
2849
+ ]);
2850
+ }
2851
+ console.log(table.toString());
2852
+ console.log("");
2853
+ console.log(
2854
+ ui.blue(
2855
+ `Found ${rankedTasks.length} task(s). Use \`oac plan --repo ${resolvedRepo.fullName} --tokens <n>\` to build an execution plan.`
2856
+ )
2857
+ );
2858
+ });
2859
+ command.addHelpText(
2860
+ "after",
2861
+ `
2862
+ Scan runs lightweight scanners and outputs a flat list of ranked tasks.
2863
+ For deeper analysis that groups findings into epics, use \`oac analyze\`.
2864
+ To run the full pipeline (analyze + execute), use \`oac run\`.
2865
+
2866
+ Examples:
2867
+ $ oac scan --repo owner/repo
2868
+ $ oac scan --repo owner/repo --scanners lint,todo
2869
+ $ oac scan --repo owner/repo --min-priority 50 --format json`
2870
+ );
2871
+ return command;
2872
+ }
2873
+ function normalizeOutputFormat2(value) {
2874
+ const normalized = value.trim().toLowerCase();
2875
+ if (normalized === "table" || normalized === "json") {
2876
+ return normalized;
2877
+ }
2878
+ throw new Error(`Unsupported --format value "${value}". Use "table" or "json".`);
2879
+ }
2880
+ function selectScanners(scannerOption, config, hasGitHubAuth = false) {
2881
+ const defaultScanners = ["lint", "todo", "test-gap"];
2882
+ if (hasGitHubAuth) {
2883
+ defaultScanners.push("github-issues");
2884
+ }
2885
+ const requested = scannerOption ? parseCsv(scannerOption) : scannersFromConfig(config, hasGitHubAuth) ?? defaultScanners;
2886
+ const enabled = [];
2887
+ const unknown = [];
2888
+ for (const scannerName of requested) {
2889
+ const normalized = scannerName.toLowerCase();
2890
+ if (normalized === "lint" || normalized === "todo" || normalized === "github-issues" || normalized === "test-gap") {
2891
+ enabled.push(normalized);
2892
+ } else {
2893
+ unknown.push(scannerName);
2894
+ }
2895
+ }
2896
+ const uniqueEnabled = [...new Set(enabled)];
2897
+ if (uniqueEnabled.length === 0) {
2898
+ throw new Error(
2899
+ `No supported scanners selected. Supported scanners: ${SUPPORTED_SCANNERS.join(", ")}.`
2900
+ );
2901
+ }
2902
+ const scannerInstances = uniqueEnabled.map((name) => {
2903
+ if (name === "lint") return new LintScanner();
2904
+ if (name === "github-issues") return new GitHubIssuesScanner();
2905
+ if (name === "test-gap") return new TestGapScanner();
2906
+ return new TodoScanner();
2907
+ });
2908
+ return {
2909
+ enabled: uniqueEnabled,
2910
+ unknown,
2911
+ scanner: new CompositeScanner(scannerInstances)
2912
+ };
2913
+ }
2914
+ function scannersFromConfig(config, hasGitHubAuth = false) {
2915
+ if (!config) {
2916
+ return null;
2917
+ }
2918
+ const configured = [];
2919
+ if (config.discovery.scanners.lint) {
2920
+ configured.push("lint");
2921
+ }
2922
+ if (config.discovery.scanners.todo) {
2923
+ configured.push("todo");
2924
+ }
2925
+ if (config.discovery.scanners.testGap) {
2926
+ configured.push("test-gap");
2927
+ }
2928
+ if (hasGitHubAuth) {
2929
+ configured.push("github-issues");
2930
+ }
2931
+ if (configured.length === 0) {
2932
+ return null;
2933
+ }
2934
+ return configured;
2935
+ }
2936
+ function parseCsv(value) {
2937
+ return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
2938
+ }
2939
+
2940
+ // src/cli/commands/status.ts
2941
+ import { readFile as readFile6 } from "fs/promises";
2942
+ import { resolve as resolve7 } from "path";
2943
+ import { Command as Command12 } from "commander";
2944
+ var WATCH_INTERVAL_MS = 2e3;
2945
+ function createStatusCommand() {
2946
+ const command = new Command12("status");
2947
+ command.description("Show current job status").option("--watch", "Poll every 2 seconds", false).action(async (options, cmd) => {
2948
+ const globalOptions = getGlobalOptions(cmd);
2949
+ const render = async () => {
2950
+ const status = await readRunStatus(process.cwd());
2951
+ renderStatusOutput(status, globalOptions.json);
2952
+ };
2953
+ await render();
2954
+ if (!options.watch) {
2955
+ return;
2956
+ }
2957
+ const intervalId = setInterval(() => {
2958
+ console.clear();
2959
+ void render().catch((error) => {
2960
+ const message = error instanceof Error ? error.message : String(error);
2961
+ console.error(message);
2962
+ process.exitCode = 1;
2963
+ });
2964
+ }, WATCH_INTERVAL_MS);
2965
+ process.on("SIGINT", () => {
2966
+ clearInterval(intervalId);
2967
+ console.log("\nWatch mode stopped.");
2968
+ process.exit(0);
2969
+ });
2970
+ });
2971
+ command.addHelpText(
2972
+ "after",
2973
+ `
2974
+ Examples:
2975
+ $ oac status
2976
+ $ oac status --watch`
2977
+ );
2978
+ return command;
2979
+ }
2980
+ async function readRunStatus(repoPath) {
2981
+ const statusPath = resolve7(repoPath, ".oac", "status.json");
2982
+ try {
2983
+ const raw = await readFile6(statusPath, "utf8");
2984
+ const payload = JSON.parse(raw);
2985
+ return parseRunStatus(payload);
2986
+ } catch (error) {
2987
+ if (isFileNotFoundError3(error)) {
2988
+ return null;
2989
+ }
2990
+ throw error;
2991
+ }
2992
+ }
2993
+ function parseRunStatus(payload) {
2994
+ if (!isRecord(payload)) {
2995
+ throw new Error("Invalid .oac/status.json format.");
2996
+ }
2997
+ const runId = payload.runId;
2998
+ const startedAt = payload.startedAt;
2999
+ const agent = payload.agent;
3000
+ const tasks = payload.tasks;
3001
+ if (typeof runId !== "string" || typeof startedAt !== "string" || typeof agent !== "string" || !Array.isArray(tasks)) {
3002
+ throw new Error("Invalid .oac/status.json format.");
3003
+ }
3004
+ return {
3005
+ runId,
3006
+ startedAt,
3007
+ agent,
3008
+ tasks: tasks.map((task, index) => parseRunStatusTask(task, index))
3009
+ };
3010
+ }
3011
+ function parseRunStatusTask(task, index) {
3012
+ if (!isRecord(task)) {
3013
+ throw new Error(`Invalid task at index ${String(index)} in .oac/status.json.`);
3014
+ }
3015
+ const taskId = task.taskId;
3016
+ const title = task.title;
3017
+ const status = task.status;
3018
+ const startedAt = task.startedAt;
3019
+ const completedAt = task.completedAt;
3020
+ const error = task.error;
3021
+ if (typeof taskId !== "string" || typeof title !== "string" || status !== "pending" && status !== "running" && status !== "completed" && status !== "failed") {
3022
+ throw new Error(`Invalid task at index ${String(index)} in .oac/status.json.`);
3023
+ }
3024
+ if (startedAt !== void 0 && typeof startedAt !== "string") {
3025
+ throw new Error(`Invalid task at index ${String(index)} in .oac/status.json.`);
3026
+ }
3027
+ if (completedAt !== void 0 && typeof completedAt !== "string") {
3028
+ throw new Error(`Invalid task at index ${String(index)} in .oac/status.json.`);
3029
+ }
3030
+ if (error !== void 0 && typeof error !== "string") {
3031
+ throw new Error(`Invalid task at index ${String(index)} in .oac/status.json.`);
3032
+ }
3033
+ return {
3034
+ taskId,
3035
+ title,
3036
+ status,
3037
+ startedAt,
3038
+ completedAt,
3039
+ error
3040
+ };
3041
+ }
3042
+ function renderStatusOutput(status, outputJson) {
3043
+ if (outputJson) {
3044
+ if (!status) {
3045
+ console.log(
3046
+ JSON.stringify(
3047
+ {
3048
+ active: false,
3049
+ message: "No active runs"
3050
+ },
3051
+ null,
3052
+ 2
3053
+ )
3054
+ );
3055
+ return;
3056
+ }
3057
+ console.log(
3058
+ JSON.stringify(
3059
+ {
3060
+ active: true,
3061
+ status
3062
+ },
3063
+ null,
3064
+ 2
3065
+ )
3066
+ );
3067
+ return;
3068
+ }
3069
+ if (!status) {
3070
+ console.log("No active runs");
3071
+ return;
3072
+ }
3073
+ const runningTasks = status.tasks.filter((task) => task.status === "running");
3074
+ const completedTasks = status.tasks.filter((task) => task.status === "completed");
3075
+ const failedTasks = status.tasks.filter((task) => task.status === "failed");
3076
+ console.log(`Run ID: ${status.runId}`);
3077
+ console.log(`Start Time: ${status.startedAt}`);
3078
+ console.log(`Agent: ${status.agent}`);
3079
+ console.log(
3080
+ `Tasks In Progress (${String(runningTasks.length)}): ${formatTaskList(runningTasks)}`
3081
+ );
3082
+ console.log(
3083
+ `Completed Tasks (${String(completedTasks.length)}): ${formatTaskList(completedTasks)}`
3084
+ );
3085
+ if (failedTasks.length === 0) {
3086
+ console.log("Errors: none");
3087
+ return;
3088
+ }
3089
+ console.log(`Errors (${String(failedTasks.length)}):`);
3090
+ for (const task of failedTasks) {
3091
+ console.log(`- ${task.taskId}: ${task.error ?? "Unknown error"}`);
3092
+ }
3093
+ }
3094
+ function formatTaskList(tasks) {
3095
+ if (tasks.length === 0) {
3096
+ return "-";
3097
+ }
3098
+ return tasks.map((task) => `${task.taskId} (${task.title})`).join(", ");
3099
+ }
3100
+ function isFileNotFoundError3(error) {
3101
+ if (!isRecord(error)) {
3102
+ return false;
3103
+ }
3104
+ return error.code === "ENOENT";
3105
+ }
3106
+
3107
+ // src/cli/cli.ts
3108
+ function registerCommands(program) {
3109
+ program.addCommand(createInitCommand());
3110
+ program.addCommand(createAnalyzeCommand());
3111
+ program.addCommand(createDoctorCommand());
3112
+ program.addCommand(createScanCommand());
3113
+ program.addCommand(createPlanCommand());
3114
+ program.addCommand(createRunCommand());
3115
+ program.addCommand(createLogCommand());
3116
+ program.addCommand(createLeaderboardCommand());
3117
+ program.addCommand(createStatusCommand());
3118
+ program.addCommand(createCompletionCommand());
3119
+ program.addCommand(createExplainCommand());
3120
+ }
3121
+ async function createCliProgram() {
3122
+ const version = true ? "2026.2.5" : "0.0.0";
3123
+ const program = new Command13();
3124
+ program.name("oac").description("Open Agent Contribution CLI").version(version).option("--config <path>", "Config file path", "oac.config.ts").option("--verbose", "Enable verbose logging", false).option("--quiet", "Suppress non-error output", false).option("--json", "Output machine-readable JSON", false).option("--no-color", "Disable ANSI colors");
3125
+ registerCommands(program);
3126
+ program.addHelpText(
3127
+ "after",
3128
+ `
3129
+ Getting Started:
3130
+ $ oac init Set up your project configuration
3131
+ $ oac doctor Verify your environment is ready
3132
+ $ oac analyze Analyze codebase for contribution opportunities
3133
+ $ oac run Run the full contribution pipeline
3134
+
3135
+ Documentation: https://github.com/Open330/open-agent-contribution
3136
+ `
3137
+ );
3138
+ return program;
3139
+ }
3140
+ async function runCli(argv = process.argv) {
3141
+ const program = await createCliProgram();
3142
+ await program.parseAsync([...argv]);
3143
+ }
3144
+
3145
+ export {
3146
+ ConfigError,
3147
+ EXIT_GENERAL_ERROR,
3148
+ EXIT_CONFIG_ERROR,
3149
+ createCliProgram,
3150
+ runCli
3151
+ };
3152
+ //# sourceMappingURL=chunk-4IUL7ECC.js.map