@kody-ade/kody-engine-lite 0.1.43 → 0.1.45

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 (2) hide show
  1. package/dist/bin/cli.js +409 -138
  2. package/package.json +1 -1
package/dist/bin/cli.js CHANGED
@@ -515,6 +515,32 @@ function postComment(issueNumber, body) {
515
515
  logger.warn(` Failed to post comment: ${err}`);
516
516
  }
517
517
  }
518
+ function getPRForBranch(branch) {
519
+ try {
520
+ const output = gh([
521
+ "pr",
522
+ "view",
523
+ branch,
524
+ "--json",
525
+ "number,url"
526
+ ]);
527
+ const data = JSON.parse(output);
528
+ return { number: data.number, url: data.url };
529
+ } catch {
530
+ return null;
531
+ }
532
+ }
533
+ function updatePR(prNumber, body) {
534
+ try {
535
+ gh(
536
+ ["pr", "edit", String(prNumber), "--body-file", "-"],
537
+ { input: body }
538
+ );
539
+ logger.info(` PR #${prNumber} body updated`);
540
+ } catch (err) {
541
+ logger.warn(` Failed to update PR #${prNumber}: ${err}`);
542
+ }
543
+ }
518
544
  function createPR(head, base, title, body) {
519
545
  try {
520
546
  const output = gh(
@@ -556,6 +582,75 @@ function setLifecycleLabel(issueNumber, phase) {
556
582
  }
557
583
  setLabel(issueNumber, `kody:${phase}`);
558
584
  }
585
+ function getPRsForIssue(issueNumber) {
586
+ try {
587
+ const output = gh([
588
+ "pr",
589
+ "list",
590
+ "--search",
591
+ `${issueNumber} in:body`,
592
+ "--json",
593
+ "number,title,url,headRefName",
594
+ "--state",
595
+ "open"
596
+ ]);
597
+ const prs = JSON.parse(output);
598
+ const branchPrs = (() => {
599
+ try {
600
+ const branchOutput = gh([
601
+ "pr",
602
+ "list",
603
+ "--json",
604
+ "number,title,url,headRefName",
605
+ "--state",
606
+ "open"
607
+ ]);
608
+ return JSON.parse(branchOutput).filter((pr) => pr.headRefName.startsWith(`${issueNumber}-`));
609
+ } catch {
610
+ return [];
611
+ }
612
+ })();
613
+ const seen = /* @__PURE__ */ new Set();
614
+ const merged = [];
615
+ for (const pr of [...prs, ...branchPrs]) {
616
+ if (!seen.has(pr.number)) {
617
+ seen.add(pr.number);
618
+ merged.push({ number: pr.number, title: pr.title, url: pr.url, headBranch: pr.headRefName });
619
+ }
620
+ }
621
+ return merged;
622
+ } catch (err) {
623
+ logger.error(` Failed to get PRs for issue #${issueNumber}: ${err}`);
624
+ return [];
625
+ }
626
+ }
627
+ function getPRDetails(prNumber) {
628
+ try {
629
+ const output = gh([
630
+ "pr",
631
+ "view",
632
+ String(prNumber),
633
+ "--json",
634
+ "title,body,headRefName,baseRefName"
635
+ ]);
636
+ const data = JSON.parse(output);
637
+ return { title: data.title, body: data.body, headBranch: data.headRefName, baseBranch: data.baseRefName };
638
+ } catch (err) {
639
+ logger.error(` Failed to get PR #${prNumber}: ${err}`);
640
+ return null;
641
+ }
642
+ }
643
+ function postPRComment(prNumber, body) {
644
+ try {
645
+ gh(
646
+ ["pr", "comment", String(prNumber), "--body-file", "-"],
647
+ { input: body }
648
+ );
649
+ logger.info(` Comment posted on PR #${prNumber}`);
650
+ } catch (err) {
651
+ logger.warn(` Failed to post PR comment: ${err}`);
652
+ }
653
+ }
559
654
  var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd;
560
655
  var init_github_api = __esm({
561
656
  "src/github-api.ts"() {
@@ -1520,21 +1615,37 @@ function executeShipStage(ctx, _def) {
1520
1615
  }
1521
1616
  }
1522
1617
  const body = buildPrBody(ctx);
1523
- const pr = createPR(head, base, title, body);
1524
- if (pr) {
1618
+ const existingPr = getPRForBranch(head);
1619
+ if (existingPr) {
1620
+ updatePR(existingPr.number, body);
1525
1621
  if (ctx.input.issueNumber && !ctx.input.local) {
1526
1622
  try {
1527
- postComment(ctx.input.issueNumber, `\u{1F389} PR created: ${pr.url}`);
1623
+ postComment(ctx.input.issueNumber, `\u2705 Fix pushed to existing PR: ${existingPr.url}`);
1528
1624
  } catch {
1529
1625
  }
1530
1626
  }
1531
1627
  fs9.writeFileSync(shipPath, `# Ship
1532
1628
 
1629
+ Updated existing PR: ${existingPr.url}
1630
+ PR #${existingPr.number}
1631
+ `);
1632
+ } else {
1633
+ const pr = createPR(head, base, title, body);
1634
+ if (pr) {
1635
+ if (ctx.input.issueNumber && !ctx.input.local) {
1636
+ try {
1637
+ postComment(ctx.input.issueNumber, `\u{1F389} PR created: ${pr.url}`);
1638
+ } catch {
1639
+ }
1640
+ }
1641
+ fs9.writeFileSync(shipPath, `# Ship
1642
+
1533
1643
  PR created: ${pr.url}
1534
1644
  PR #${pr.number}
1535
1645
  `);
1536
- } else {
1537
- fs9.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
1646
+ } else {
1647
+ fs9.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
1648
+ }
1538
1649
  }
1539
1650
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
1540
1651
  } catch (err) {
@@ -2371,6 +2482,128 @@ var init_preflight = __esm({
2371
2482
  }
2372
2483
  });
2373
2484
 
2485
+ // src/cli/task-resolution.ts
2486
+ import * as fs16 from "fs";
2487
+ import * as path15 from "path";
2488
+ import { execFileSync as execFileSync9 } from "child_process";
2489
+ function findLatestTaskForIssue(issueNumber, projectDir) {
2490
+ const tasksDir = path15.join(projectDir, ".tasks");
2491
+ if (!fs16.existsSync(tasksDir)) return null;
2492
+ const allDirs = fs16.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2493
+ const prefix = `${issueNumber}-`;
2494
+ const direct = allDirs.find((d) => d.startsWith(prefix));
2495
+ if (direct) return direct;
2496
+ try {
2497
+ const branch = execFileSync9("git", ["branch", "--show-current"], {
2498
+ encoding: "utf-8",
2499
+ cwd: projectDir,
2500
+ timeout: 5e3,
2501
+ stdio: ["pipe", "pipe", "pipe"]
2502
+ }).trim();
2503
+ const branchIssueMatch = branch.match(/^(\d+)-/);
2504
+ if (branchIssueMatch) {
2505
+ const branchIssueNum = branchIssueMatch[1];
2506
+ const branchPrefix = `${branchIssueNum}-`;
2507
+ const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
2508
+ if (fromBranch) return fromBranch;
2509
+ }
2510
+ } catch {
2511
+ }
2512
+ return null;
2513
+ }
2514
+ function generateTaskId() {
2515
+ const now = /* @__PURE__ */ new Date();
2516
+ const pad = (n) => String(n).padStart(2, "0");
2517
+ return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
2518
+ }
2519
+ var init_task_resolution = __esm({
2520
+ "src/cli/task-resolution.ts"() {
2521
+ "use strict";
2522
+ }
2523
+ });
2524
+
2525
+ // src/review-standalone.ts
2526
+ import * as fs17 from "fs";
2527
+ import * as path16 from "path";
2528
+ function resolveReviewTarget(input) {
2529
+ if (input.prs.length === 0) {
2530
+ return {
2531
+ action: "none",
2532
+ message: `Issue #${input.issueNumber} has no open PRs. Nothing to review.`
2533
+ };
2534
+ }
2535
+ if (input.prs.length === 1) {
2536
+ return { action: "review", prNumber: input.prs[0].number };
2537
+ }
2538
+ const prList = input.prs.map((pr) => ` - #${pr.number}: ${pr.title}`).join("\n");
2539
+ return {
2540
+ action: "pick",
2541
+ prs: input.prs,
2542
+ message: `\u26A0\uFE0F Issue #${input.issueNumber} has ${input.prs.length} open PRs:
2543
+ ${prList}
2544
+
2545
+ Run: \`pnpm kody review --pr-number <n>\`
2546
+ Or comment on the specific PR: \`@kody review\``
2547
+ };
2548
+ }
2549
+ async function runStandaloneReview(input) {
2550
+ const taskId = input.taskId ?? `review-${generateTaskId()}`;
2551
+ const taskDir = path16.join(input.projectDir, ".tasks", taskId);
2552
+ fs17.mkdirSync(taskDir, { recursive: true });
2553
+ const taskContent = `# ${input.prTitle}
2554
+
2555
+ ${input.prBody ?? ""}`;
2556
+ fs17.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
2557
+ const reviewDef = STAGES.find((s) => s.name === "review");
2558
+ const ctx = {
2559
+ taskId,
2560
+ taskDir,
2561
+ projectDir: input.projectDir,
2562
+ runners: input.runners,
2563
+ sessions: {},
2564
+ input: {
2565
+ mode: "full",
2566
+ local: input.local
2567
+ }
2568
+ };
2569
+ logger.info(`[review] standalone review for: ${input.prTitle}`);
2570
+ const result = await executeAgentStage(ctx, reviewDef);
2571
+ if (result.outcome !== "completed") {
2572
+ return {
2573
+ outcome: "failed",
2574
+ taskDir,
2575
+ error: result.error ?? "Review stage failed"
2576
+ };
2577
+ }
2578
+ const reviewPath = path16.join(taskDir, "review.md");
2579
+ let reviewContent;
2580
+ if (fs17.existsSync(reviewPath)) {
2581
+ reviewContent = fs17.readFileSync(reviewPath, "utf-8");
2582
+ }
2583
+ return {
2584
+ outcome: "completed",
2585
+ reviewContent,
2586
+ taskDir
2587
+ };
2588
+ }
2589
+ function formatReviewComment(reviewContent, taskId) {
2590
+ return `## \u{1F50D} Kody Review (\`${taskId}\`)
2591
+
2592
+ ${reviewContent}
2593
+
2594
+ ---
2595
+ \u{1F916} Generated by Kody`;
2596
+ }
2597
+ var init_review_standalone = __esm({
2598
+ "src/review-standalone.ts"() {
2599
+ "use strict";
2600
+ init_definitions();
2601
+ init_agent();
2602
+ init_task_resolution();
2603
+ init_logger();
2604
+ }
2605
+ });
2606
+
2374
2607
  // src/cli/args.ts
2375
2608
  function getArg(args2, flag) {
2376
2609
  const idx = args2.indexOf(flag);
@@ -2389,16 +2622,18 @@ function parseArgs() {
2389
2622
  kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
2390
2623
  kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
2391
2624
  kody fix --task-id <id> [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
2625
+ kody review [--pr-number <n>] [--issue-number <n>] [--cwd <path>] [--local]
2392
2626
  kody status --task-id <id> [--cwd <path>]
2393
2627
  kody --help`);
2394
2628
  process.exit(0);
2395
2629
  }
2396
2630
  const command2 = args2[0];
2397
- if (!["run", "rerun", "fix", "status"].includes(command2)) {
2631
+ if (!["run", "rerun", "fix", "status", "review"].includes(command2)) {
2398
2632
  console.error(`Unknown command: ${command2}`);
2399
2633
  process.exit(1);
2400
2634
  }
2401
2635
  const issueStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER;
2636
+ const prStr = getArg(args2, "--pr-number") ?? process.env.PR_NUMBER;
2402
2637
  const localFlag = hasFlag(args2, "--local");
2403
2638
  return {
2404
2639
  command: command2,
@@ -2408,6 +2643,7 @@ function parseArgs() {
2408
2643
  dryRun: hasFlag(args2, "--dry-run") || process.env.DRY_RUN === "true",
2409
2644
  cwd: getArg(args2, "--cwd"),
2410
2645
  issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
2646
+ prNumber: prStr ? parseInt(prStr, 10) : void 0,
2411
2647
  feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
2412
2648
  local: localFlag || !isCI2 && !hasFlag(args2, "--no-local"),
2413
2649
  complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
@@ -2422,9 +2658,9 @@ var init_args = __esm({
2422
2658
  });
2423
2659
 
2424
2660
  // src/cli/litellm.ts
2425
- import * as fs16 from "fs";
2426
- import * as path15 from "path";
2427
- import { execFileSync as execFileSync9 } from "child_process";
2661
+ import * as fs18 from "fs";
2662
+ import * as path17 from "path";
2663
+ import { execFileSync as execFileSync10 } from "child_process";
2428
2664
  async function checkLitellmHealth(url) {
2429
2665
  try {
2430
2666
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -2434,8 +2670,8 @@ async function checkLitellmHealth(url) {
2434
2670
  }
2435
2671
  }
2436
2672
  async function tryStartLitellm(url, projectDir) {
2437
- const configPath = path15.join(projectDir, "litellm-config.yaml");
2438
- if (!fs16.existsSync(configPath)) {
2673
+ const configPath = path17.join(projectDir, "litellm-config.yaml");
2674
+ if (!fs18.existsSync(configPath)) {
2439
2675
  logger.warn("litellm-config.yaml not found \u2014 cannot start proxy");
2440
2676
  return null;
2441
2677
  }
@@ -2443,11 +2679,11 @@ async function tryStartLitellm(url, projectDir) {
2443
2679
  const port = portMatch ? portMatch[1] : "4000";
2444
2680
  let litellmFound = false;
2445
2681
  try {
2446
- execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2682
+ execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2447
2683
  litellmFound = true;
2448
2684
  } catch {
2449
2685
  try {
2450
- execFileSync9("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
2686
+ execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
2451
2687
  litellmFound = true;
2452
2688
  } catch {
2453
2689
  }
@@ -2460,17 +2696,17 @@ async function tryStartLitellm(url, projectDir) {
2460
2696
  let cmd;
2461
2697
  let args2;
2462
2698
  try {
2463
- execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2699
+ execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2464
2700
  cmd = "litellm";
2465
2701
  args2 = ["--config", configPath, "--port", port];
2466
2702
  } catch {
2467
2703
  cmd = "python3";
2468
2704
  args2 = ["-m", "litellm", "--config", configPath, "--port", port];
2469
2705
  }
2470
- const dotenvPath = path15.join(projectDir, ".env");
2706
+ const dotenvPath = path17.join(projectDir, ".env");
2471
2707
  const dotenvVars = {};
2472
- if (fs16.existsSync(dotenvPath)) {
2473
- for (const line of fs16.readFileSync(dotenvPath, "utf-8").split("\n")) {
2708
+ if (fs18.existsSync(dotenvPath)) {
2709
+ for (const line of fs18.readFileSync(dotenvPath, "utf-8").split("\n")) {
2474
2710
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
2475
2711
  if (match) dotenvVars[match[1]] = match[2];
2476
2712
  }
@@ -2509,49 +2745,9 @@ var init_litellm = __esm({
2509
2745
  }
2510
2746
  });
2511
2747
 
2512
- // src/cli/task-resolution.ts
2513
- import * as fs17 from "fs";
2514
- import * as path16 from "path";
2515
- import { execFileSync as execFileSync10 } from "child_process";
2516
- function findLatestTaskForIssue(issueNumber, projectDir) {
2517
- const tasksDir = path16.join(projectDir, ".tasks");
2518
- if (!fs17.existsSync(tasksDir)) return null;
2519
- const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2520
- const prefix = `${issueNumber}-`;
2521
- const direct = allDirs.find((d) => d.startsWith(prefix));
2522
- if (direct) return direct;
2523
- try {
2524
- const branch = execFileSync10("git", ["branch", "--show-current"], {
2525
- encoding: "utf-8",
2526
- cwd: projectDir,
2527
- timeout: 5e3,
2528
- stdio: ["pipe", "pipe", "pipe"]
2529
- }).trim();
2530
- const branchIssueMatch = branch.match(/^(\d+)-/);
2531
- if (branchIssueMatch) {
2532
- const branchIssueNum = branchIssueMatch[1];
2533
- const branchPrefix = `${branchIssueNum}-`;
2534
- const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
2535
- if (fromBranch) return fromBranch;
2536
- }
2537
- } catch {
2538
- }
2539
- return null;
2540
- }
2541
- function generateTaskId() {
2542
- const now = /* @__PURE__ */ new Date();
2543
- const pad = (n) => String(n).padStart(2, "0");
2544
- return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
2545
- }
2546
- var init_task_resolution = __esm({
2547
- "src/cli/task-resolution.ts"() {
2548
- "use strict";
2549
- }
2550
- });
2551
-
2552
2748
  // src/cli/task-state.ts
2553
- import * as fs18 from "fs";
2554
- import * as path17 from "path";
2749
+ import * as fs19 from "fs";
2750
+ import * as path18 from "path";
2555
2751
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
2556
2752
  if (!existingTaskId || !existingState) {
2557
2753
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -2583,11 +2779,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
2583
2779
  function resolveForIssue(issueNumber, projectDir) {
2584
2780
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
2585
2781
  if (existingTaskId) {
2586
- const statusPath = path17.join(projectDir, ".tasks", existingTaskId, "status.json");
2782
+ const statusPath = path18.join(projectDir, ".tasks", existingTaskId, "status.json");
2587
2783
  let existingState = null;
2588
- if (fs18.existsSync(statusPath)) {
2784
+ if (fs19.existsSync(statusPath)) {
2589
2785
  try {
2590
- existingState = JSON.parse(fs18.readFileSync(statusPath, "utf-8"));
2786
+ existingState = JSON.parse(fs19.readFileSync(statusPath, "utf-8"));
2591
2787
  } catch {
2592
2788
  }
2593
2789
  }
@@ -2617,13 +2813,13 @@ var init_task_state = __esm({
2617
2813
 
2618
2814
  // src/entry.ts
2619
2815
  var entry_exports = {};
2620
- import * as fs19 from "fs";
2621
- import * as path18 from "path";
2816
+ import * as fs20 from "fs";
2817
+ import * as path19 from "path";
2622
2818
  async function main() {
2623
2819
  const input = parseArgs();
2624
- const projectDir = input.cwd ? path18.resolve(input.cwd) : process.cwd();
2820
+ const projectDir = input.cwd ? path19.resolve(input.cwd) : process.cwd();
2625
2821
  if (input.cwd) {
2626
- if (!fs19.existsSync(projectDir)) {
2822
+ if (!fs20.existsSync(projectDir)) {
2627
2823
  console.error(`--cwd path does not exist: ${projectDir}`);
2628
2824
  process.exit(1);
2629
2825
  }
@@ -2631,7 +2827,7 @@ async function main() {
2631
2827
  setGhCwd(projectDir);
2632
2828
  logger.info(`Working directory: ${projectDir}`);
2633
2829
  }
2634
- if (input.issueNumber) {
2830
+ if (input.issueNumber && input.command !== "review") {
2635
2831
  const taskAction = resolveForIssue(input.issueNumber, projectDir);
2636
2832
  logger.info(`Task action: ${taskAction.action}`);
2637
2833
  if (taskAction.action === "already-completed") {
@@ -2667,35 +2863,109 @@ async function main() {
2667
2863
  taskId = `${input.issueNumber}-${generateTaskId()}`;
2668
2864
  } else if (input.command === "run" && input.task) {
2669
2865
  taskId = generateTaskId();
2866
+ } else if (input.command === "review") {
2867
+ taskId = input.prNumber ? `review-pr-${input.prNumber}-${generateTaskId()}` : `review-${generateTaskId()}`;
2670
2868
  } else {
2671
2869
  console.error("--task-id is required (or provide --issue-number to auto-generate)");
2672
2870
  process.exit(1);
2673
2871
  }
2674
2872
  }
2675
- const taskDir = path18.join(projectDir, ".tasks", taskId);
2676
- fs19.mkdirSync(taskDir, { recursive: true });
2873
+ const taskDir = path19.join(projectDir, ".tasks", taskId);
2874
+ fs20.mkdirSync(taskDir, { recursive: true });
2677
2875
  if (input.command === "status") {
2678
2876
  printStatus(taskId, taskDir);
2679
2877
  return;
2680
2878
  }
2879
+ if (input.command === "review") {
2880
+ runPreflight();
2881
+ let prTitle = "Code review";
2882
+ let prBody = "";
2883
+ let prNumber = input.prNumber;
2884
+ if (!prNumber && input.issueNumber) {
2885
+ const prs = getPRsForIssue(input.issueNumber);
2886
+ const target = resolveReviewTarget({ issueNumber: input.issueNumber, prs });
2887
+ if (target.action === "none" || target.action === "pick") {
2888
+ console.log(target.message);
2889
+ if (!input.local && input.issueNumber) {
2890
+ try {
2891
+ postComment(input.issueNumber, target.message);
2892
+ } catch {
2893
+ }
2894
+ }
2895
+ process.exit(target.action === "none" ? 1 : 0);
2896
+ }
2897
+ prNumber = target.prNumber;
2898
+ }
2899
+ if (prNumber) {
2900
+ const details = getPRDetails(prNumber);
2901
+ if (details) {
2902
+ prTitle = details.title;
2903
+ prBody = details.body ?? "";
2904
+ }
2905
+ }
2906
+ const config2 = getProjectConfig();
2907
+ let litellmProcess2 = null;
2908
+ if (config2.agent.litellmUrl) {
2909
+ const proxyRunning = await checkLitellmHealth(config2.agent.litellmUrl);
2910
+ if (!proxyRunning) {
2911
+ litellmProcess2 = await tryStartLitellm(config2.agent.litellmUrl, projectDir);
2912
+ }
2913
+ if (config2.agent.litellmUrl) {
2914
+ process.env.ANTHROPIC_BASE_URL = config2.agent.litellmUrl;
2915
+ }
2916
+ }
2917
+ const runners2 = createRunners(config2);
2918
+ const defaultRunnerName2 = config2.agent.defaultRunner ?? Object.keys(runners2)[0] ?? "claude";
2919
+ const defaultRunner2 = runners2[defaultRunnerName2];
2920
+ if (!defaultRunner2) {
2921
+ console.error(`Default runner "${defaultRunnerName2}" not configured`);
2922
+ process.exit(1);
2923
+ }
2924
+ const healthy2 = await defaultRunner2.healthCheck();
2925
+ if (!healthy2) {
2926
+ console.error(`Runner "${defaultRunnerName2}" health check failed`);
2927
+ process.exit(1);
2928
+ }
2929
+ const result = await runStandaloneReview({
2930
+ projectDir,
2931
+ runners: runners2,
2932
+ prTitle,
2933
+ prBody,
2934
+ local: input.local ?? true,
2935
+ taskId
2936
+ });
2937
+ if (litellmProcess2) litellmProcess2.kill();
2938
+ if (result.outcome === "failed") {
2939
+ console.error(`Review failed: ${result.error}`);
2940
+ process.exit(1);
2941
+ }
2942
+ if (result.reviewContent) {
2943
+ console.log(result.reviewContent);
2944
+ if (!input.local && prNumber) {
2945
+ const comment = formatReviewComment(result.reviewContent, taskId);
2946
+ postPRComment(prNumber, comment);
2947
+ }
2948
+ }
2949
+ process.exit(0);
2950
+ }
2681
2951
  logger.info("Preflight checks:");
2682
2952
  runPreflight();
2683
2953
  if (input.task) {
2684
- fs19.writeFileSync(path18.join(taskDir, "task.md"), input.task);
2954
+ fs20.writeFileSync(path19.join(taskDir, "task.md"), input.task);
2685
2955
  }
2686
- const taskMdPath = path18.join(taskDir, "task.md");
2687
- if (!fs19.existsSync(taskMdPath) && input.issueNumber) {
2956
+ const taskMdPath = path19.join(taskDir, "task.md");
2957
+ if (!fs20.existsSync(taskMdPath) && input.issueNumber) {
2688
2958
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
2689
2959
  const issue = getIssue(input.issueNumber);
2690
2960
  if (issue) {
2691
2961
  const taskContent = `# ${issue.title}
2692
2962
 
2693
2963
  ${issue.body ?? ""}`;
2694
- fs19.writeFileSync(taskMdPath, taskContent);
2964
+ fs20.writeFileSync(taskMdPath, taskContent);
2695
2965
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
2696
2966
  }
2697
2967
  }
2698
- if (!fs19.existsSync(taskMdPath)) {
2968
+ if (!fs20.existsSync(taskMdPath)) {
2699
2969
  console.error("No task.md found. Provide --task, --issue-number, or ensure .tasks/<id>/task.md exists.");
2700
2970
  process.exit(1);
2701
2971
  }
@@ -2779,7 +3049,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
2779
3049
  }
2780
3050
  }
2781
3051
  const state = await runPipeline(ctx);
2782
- const files = fs19.readdirSync(taskDir);
3052
+ const files = fs20.readdirSync(taskDir);
2783
3053
  console.log(`
2784
3054
  Artifacts in ${taskDir}:`);
2785
3055
  for (const f of files) {
@@ -2820,6 +3090,7 @@ var init_entry = __esm({
2820
3090
  init_config();
2821
3091
  init_github_api();
2822
3092
  init_logger();
3093
+ init_review_standalone();
2823
3094
  init_args();
2824
3095
  init_litellm();
2825
3096
  init_task_resolution();
@@ -2841,15 +3112,15 @@ var init_entry = __esm({
2841
3112
  });
2842
3113
 
2843
3114
  // src/bin/cli.ts
2844
- import * as fs20 from "fs";
2845
- import * as path19 from "path";
3115
+ import * as fs21 from "fs";
3116
+ import * as path20 from "path";
2846
3117
  import { execFileSync as execFileSync11 } from "child_process";
2847
3118
  import { fileURLToPath } from "url";
2848
- var __dirname = path19.dirname(fileURLToPath(import.meta.url));
2849
- var PKG_ROOT = path19.resolve(__dirname, "..", "..");
3119
+ var __dirname = path20.dirname(fileURLToPath(import.meta.url));
3120
+ var PKG_ROOT = path20.resolve(__dirname, "..", "..");
2850
3121
  function getVersion() {
2851
- const pkgPath = path19.join(PKG_ROOT, "package.json");
2852
- const pkg = JSON.parse(fs20.readFileSync(pkgPath, "utf-8"));
3122
+ const pkgPath = path20.join(PKG_ROOT, "package.json");
3123
+ const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
2853
3124
  return pkg.version;
2854
3125
  }
2855
3126
  function checkCommand2(name, args2, fix) {
@@ -2865,7 +3136,7 @@ function checkCommand2(name, args2, fix) {
2865
3136
  }
2866
3137
  }
2867
3138
  function checkFile(filePath, description, fix) {
2868
- if (fs20.existsSync(filePath)) {
3139
+ if (fs21.existsSync(filePath)) {
2869
3140
  return { name: description, ok: true, detail: filePath };
2870
3141
  }
2871
3142
  return { name: description, ok: false, fix };
@@ -2937,10 +3208,10 @@ function checkGhSecret(repoSlug, secretName) {
2937
3208
  }
2938
3209
  function detectArchitecture(cwd) {
2939
3210
  const detected = [];
2940
- const pkgPath = path19.join(cwd, "package.json");
2941
- if (fs20.existsSync(pkgPath)) {
3211
+ const pkgPath = path20.join(cwd, "package.json");
3212
+ if (fs21.existsSync(pkgPath)) {
2942
3213
  try {
2943
- const pkg = JSON.parse(fs20.readFileSync(pkgPath, "utf-8"));
3214
+ const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
2944
3215
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2945
3216
  if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
2946
3217
  else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
@@ -2963,41 +3234,41 @@ function detectArchitecture(cwd) {
2963
3234
  if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
2964
3235
  if (pkg.type === "module") detected.push("- Module system: ESM");
2965
3236
  else detected.push("- Module system: CommonJS");
2966
- if (fs20.existsSync(path19.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
2967
- else if (fs20.existsSync(path19.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
2968
- else if (fs20.existsSync(path19.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
2969
- else if (fs20.existsSync(path19.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
3237
+ if (fs21.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
3238
+ else if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
3239
+ else if (fs21.existsSync(path20.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
3240
+ else if (fs21.existsSync(path20.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
2970
3241
  } catch {
2971
3242
  }
2972
3243
  }
2973
3244
  try {
2974
- const entries = fs20.readdirSync(cwd, { withFileTypes: true });
3245
+ const entries = fs21.readdirSync(cwd, { withFileTypes: true });
2975
3246
  const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
2976
3247
  if (dirs.length > 0) detected.push(`- Top-level directories: ${dirs.join(", ")}`);
2977
3248
  } catch {
2978
3249
  }
2979
- const srcDir = path19.join(cwd, "src");
2980
- if (fs20.existsSync(srcDir)) {
3250
+ const srcDir = path20.join(cwd, "src");
3251
+ if (fs21.existsSync(srcDir)) {
2981
3252
  try {
2982
- const srcEntries = fs20.readdirSync(srcDir, { withFileTypes: true });
3253
+ const srcEntries = fs21.readdirSync(srcDir, { withFileTypes: true });
2983
3254
  const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
2984
3255
  if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
2985
3256
  } catch {
2986
3257
  }
2987
3258
  }
2988
3259
  const configs = [];
2989
- if (fs20.existsSync(path19.join(cwd, "tsconfig.json"))) configs.push("tsconfig.json");
2990
- if (fs20.existsSync(path19.join(cwd, "docker-compose.yml")) || fs20.existsSync(path19.join(cwd, "docker-compose.yaml"))) configs.push("docker-compose");
2991
- if (fs20.existsSync(path19.join(cwd, "Dockerfile"))) configs.push("Dockerfile");
2992
- if (fs20.existsSync(path19.join(cwd, ".env")) || fs20.existsSync(path19.join(cwd, ".env.local"))) configs.push(".env");
3260
+ if (fs21.existsSync(path20.join(cwd, "tsconfig.json"))) configs.push("tsconfig.json");
3261
+ if (fs21.existsSync(path20.join(cwd, "docker-compose.yml")) || fs21.existsSync(path20.join(cwd, "docker-compose.yaml"))) configs.push("docker-compose");
3262
+ if (fs21.existsSync(path20.join(cwd, "Dockerfile"))) configs.push("Dockerfile");
3263
+ if (fs21.existsSync(path20.join(cwd, ".env")) || fs21.existsSync(path20.join(cwd, ".env.local"))) configs.push(".env");
2993
3264
  if (configs.length > 0) detected.push(`- Config files: ${configs.join(", ")}`);
2994
3265
  return detected;
2995
3266
  }
2996
3267
  function detectBasicConfig(cwd) {
2997
3268
  let pm = "pnpm";
2998
- if (fs20.existsSync(path19.join(cwd, "yarn.lock"))) pm = "yarn";
2999
- else if (fs20.existsSync(path19.join(cwd, "bun.lockb"))) pm = "bun";
3000
- else if (!fs20.existsSync(path19.join(cwd, "pnpm-lock.yaml")) && fs20.existsSync(path19.join(cwd, "package-lock.json"))) pm = "npm";
3269
+ if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) pm = "yarn";
3270
+ else if (fs21.existsSync(path20.join(cwd, "bun.lockb"))) pm = "bun";
3271
+ else if (!fs21.existsSync(path20.join(cwd, "pnpm-lock.yaml")) && fs21.existsSync(path20.join(cwd, "package-lock.json"))) pm = "npm";
3001
3272
  let defaultBranch = "main";
3002
3273
  try {
3003
3274
  const ref = execFileSync11("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
@@ -3041,9 +3312,9 @@ function smartInit(cwd) {
3041
3312
  const basic = detectBasicConfig(cwd);
3042
3313
  let context = "";
3043
3314
  const readIfExists = (rel, maxChars = 3e3) => {
3044
- const p = path19.join(cwd, rel);
3045
- if (fs20.existsSync(p)) {
3046
- const content = fs20.readFileSync(p, "utf-8");
3315
+ const p = path20.join(cwd, rel);
3316
+ if (fs21.existsSync(p)) {
3317
+ const content = fs21.readFileSync(p, "utf-8");
3047
3318
  return content.slice(0, maxChars);
3048
3319
  }
3049
3320
  return null;
@@ -3069,14 +3340,14 @@ ${claudeMd}
3069
3340
 
3070
3341
  `;
3071
3342
  try {
3072
- const topDirs = fs20.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
3343
+ const topDirs = fs21.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
3073
3344
  context += `## Top-level directories
3074
3345
  ${topDirs.join(", ")}
3075
3346
 
3076
3347
  `;
3077
- const srcDir = path19.join(cwd, "src");
3078
- if (fs20.existsSync(srcDir)) {
3079
- const srcDirs = fs20.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3348
+ const srcDir = path20.join(cwd, "src");
3349
+ if (fs21.existsSync(srcDir)) {
3350
+ const srcDirs = fs21.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3080
3351
  context += `## src/ subdirectories
3081
3352
  ${srcDirs.join(", ")}
3082
3353
 
@@ -3086,7 +3357,7 @@ ${srcDirs.join(", ")}
3086
3357
  }
3087
3358
  const existingFiles = [];
3088
3359
  for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "vitest.config.ts", "vitest.config.mts", "jest.config.ts", "playwright.config.ts", ".eslintrc.js", "eslint.config.mjs", ".prettierrc"]) {
3089
- if (fs20.existsSync(path19.join(cwd, f))) existingFiles.push(f);
3360
+ if (fs21.existsSync(path20.join(cwd, f))) existingFiles.push(f);
3090
3361
  }
3091
3362
  if (existingFiles.length) context += `## Config files present
3092
3363
  ${existingFiles.join(", ")}
@@ -3192,7 +3463,7 @@ ${context}`;
3192
3463
  function validateQualityCommands(cwd, config, pm) {
3193
3464
  let scripts = {};
3194
3465
  try {
3195
- const pkg = JSON.parse(fs20.readFileSync(path19.join(cwd, "package.json"), "utf-8"));
3466
+ const pkg = JSON.parse(fs21.readFileSync(path20.join(cwd, "package.json"), "utf-8"));
3196
3467
  scripts = pkg.scripts ?? {};
3197
3468
  } catch {
3198
3469
  return;
@@ -3226,7 +3497,7 @@ function validateQualityCommands(cwd, config, pm) {
3226
3497
  function buildFallbackConfig(cwd, basic) {
3227
3498
  const pkg = (() => {
3228
3499
  try {
3229
- return JSON.parse(fs20.readFileSync(path19.join(cwd, "package.json"), "utf-8"));
3500
+ return JSON.parse(fs21.readFileSync(path20.join(cwd, "package.json"), "utf-8"));
3230
3501
  } catch {
3231
3502
  return {};
3232
3503
  }
@@ -3266,34 +3537,34 @@ function initCommand(opts) {
3266
3537
  console.log(`Project: ${cwd}
3267
3538
  `);
3268
3539
  console.log("\u2500\u2500 Files \u2500\u2500");
3269
- const templatesDir = path19.join(PKG_ROOT, "templates");
3270
- const workflowSrc = path19.join(templatesDir, "kody.yml");
3271
- const workflowDest = path19.join(cwd, ".github", "workflows", "kody.yml");
3272
- if (!fs20.existsSync(workflowSrc)) {
3540
+ const templatesDir = path20.join(PKG_ROOT, "templates");
3541
+ const workflowSrc = path20.join(templatesDir, "kody.yml");
3542
+ const workflowDest = path20.join(cwd, ".github", "workflows", "kody.yml");
3543
+ if (!fs21.existsSync(workflowSrc)) {
3273
3544
  console.error(" \u2717 Template kody.yml not found in package");
3274
3545
  process.exit(1);
3275
3546
  }
3276
- if (fs20.existsSync(workflowDest) && !opts.force) {
3547
+ if (fs21.existsSync(workflowDest) && !opts.force) {
3277
3548
  console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
3278
3549
  } else {
3279
- fs20.mkdirSync(path19.dirname(workflowDest), { recursive: true });
3280
- fs20.copyFileSync(workflowSrc, workflowDest);
3550
+ fs21.mkdirSync(path20.dirname(workflowDest), { recursive: true });
3551
+ fs21.copyFileSync(workflowSrc, workflowDest);
3281
3552
  console.log(" \u2713 .github/workflows/kody.yml");
3282
3553
  }
3283
- const configDest = path19.join(cwd, "kody.config.json");
3554
+ const configDest = path20.join(cwd, "kody.config.json");
3284
3555
  let smartResult = null;
3285
- if (!fs20.existsSync(configDest) || opts.force) {
3556
+ if (!fs21.existsSync(configDest) || opts.force) {
3286
3557
  smartResult = smartInit(cwd);
3287
- fs20.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
3558
+ fs21.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
3288
3559
  console.log(" \u2713 kody.config.json (auto-configured)");
3289
3560
  } else {
3290
3561
  console.log(" \u25CB kody.config.json (exists)");
3291
3562
  }
3292
- const gitignorePath = path19.join(cwd, ".gitignore");
3293
- if (fs20.existsSync(gitignorePath)) {
3294
- const content = fs20.readFileSync(gitignorePath, "utf-8");
3563
+ const gitignorePath = path20.join(cwd, ".gitignore");
3564
+ if (fs21.existsSync(gitignorePath)) {
3565
+ const content = fs21.readFileSync(gitignorePath, "utf-8");
3295
3566
  if (!content.includes(".tasks/")) {
3296
- fs20.appendFileSync(gitignorePath, "\n.tasks/\n");
3567
+ fs21.appendFileSync(gitignorePath, "\n.tasks/\n");
3297
3568
  console.log(" \u2713 .gitignore (added .tasks/)");
3298
3569
  } else {
3299
3570
  console.log(" \u25CB .gitignore (.tasks/ already present)");
@@ -3306,7 +3577,7 @@ function initCommand(opts) {
3306
3577
  checkCommand2("git", ["--version"], "Install git"),
3307
3578
  checkCommand2("node", ["--version"], "Install Node.js >= 22"),
3308
3579
  checkCommand2("pnpm", ["--version"], "Install: npm i -g pnpm"),
3309
- checkFile(path19.join(cwd, "package.json"), "package.json", "Run: pnpm init")
3580
+ checkFile(path20.join(cwd, "package.json"), "package.json", "Run: pnpm init")
3310
3581
  ];
3311
3582
  for (const c of checks) {
3312
3583
  if (c.ok) {
@@ -3383,9 +3654,9 @@ function initCommand(opts) {
3383
3654
  }
3384
3655
  }
3385
3656
  console.log("\n\u2500\u2500 Config \u2500\u2500");
3386
- if (fs20.existsSync(configDest)) {
3657
+ if (fs21.existsSync(configDest)) {
3387
3658
  try {
3388
- const config = JSON.parse(fs20.readFileSync(configDest, "utf-8"));
3659
+ const config = JSON.parse(fs21.readFileSync(configDest, "utf-8"));
3389
3660
  const configChecks = [];
3390
3661
  if (config.github?.owner && config.github?.repo) {
3391
3662
  configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
@@ -3412,21 +3683,21 @@ function initCommand(opts) {
3412
3683
  }
3413
3684
  }
3414
3685
  console.log("\n\u2500\u2500 Project Memory \u2500\u2500");
3415
- const memoryDir = path19.join(cwd, ".kody", "memory");
3416
- fs20.mkdirSync(memoryDir, { recursive: true });
3417
- const archPath = path19.join(memoryDir, "architecture.md");
3418
- const conventionsPath = path19.join(memoryDir, "conventions.md");
3419
- if (fs20.existsSync(archPath) && !opts.force) {
3686
+ const memoryDir = path20.join(cwd, ".kody", "memory");
3687
+ fs21.mkdirSync(memoryDir, { recursive: true });
3688
+ const archPath = path20.join(memoryDir, "architecture.md");
3689
+ const conventionsPath = path20.join(memoryDir, "conventions.md");
3690
+ if (fs21.existsSync(archPath) && !opts.force) {
3420
3691
  console.log(" \u25CB .kody/memory/architecture.md (exists, use --force to regenerate)");
3421
3692
  } else if (smartResult?.architecture) {
3422
- fs20.writeFileSync(archPath, smartResult.architecture);
3693
+ fs21.writeFileSync(archPath, smartResult.architecture);
3423
3694
  const lineCount = smartResult.architecture.split("\n").length;
3424
3695
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines, LLM-generated)`);
3425
3696
  } else {
3426
3697
  const archItems = detectArchitecture(cwd);
3427
3698
  if (archItems.length > 0) {
3428
3699
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3429
- fs20.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
3700
+ fs21.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
3430
3701
 
3431
3702
  ## Overview
3432
3703
  ${archItems.join("\n")}
@@ -3436,14 +3707,14 @@ ${archItems.join("\n")}
3436
3707
  console.log(" \u25CB No architecture detected");
3437
3708
  }
3438
3709
  }
3439
- if (fs20.existsSync(conventionsPath) && !opts.force) {
3710
+ if (fs21.existsSync(conventionsPath) && !opts.force) {
3440
3711
  console.log(" \u25CB .kody/memory/conventions.md (exists, use --force to regenerate)");
3441
3712
  } else if (smartResult?.conventions) {
3442
- fs20.writeFileSync(conventionsPath, smartResult.conventions);
3713
+ fs21.writeFileSync(conventionsPath, smartResult.conventions);
3443
3714
  const lineCount = smartResult.conventions.split("\n").length;
3444
3715
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines, LLM-generated)`);
3445
3716
  } else {
3446
- fs20.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
3717
+ fs21.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
3447
3718
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
3448
3719
  }
3449
3720
  console.log("\n\u2500\u2500 Git \u2500\u2500");
@@ -3452,7 +3723,7 @@ ${archItems.join("\n")}
3452
3723
  "kody.config.json",
3453
3724
  ".kody/memory/architecture.md",
3454
3725
  ".kody/memory/conventions.md"
3455
- ].filter((f) => fs20.existsSync(path19.join(cwd, f)));
3726
+ ].filter((f) => fs21.existsSync(path20.join(cwd, f)));
3456
3727
  if (filesToCommit.length > 0) {
3457
3728
  try {
3458
3729
  execFileSync11("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine-lite",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "description": "Autonomous SDLC pipeline: Kody orchestration + Claude Code + LiteLLM",
5
5
  "license": "MIT",
6
6
  "type": "module",