@kody-ade/kody-engine-lite 0.1.44 → 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 +362 -133
  2. package/package.json +1 -1
package/dist/bin/cli.js CHANGED
@@ -582,6 +582,75 @@ function setLifecycleLabel(issueNumber, phase) {
582
582
  }
583
583
  setLabel(issueNumber, `kody:${phase}`);
584
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
+ }
585
654
  var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd;
586
655
  var init_github_api = __esm({
587
656
  "src/github-api.ts"() {
@@ -2413,6 +2482,128 @@ var init_preflight = __esm({
2413
2482
  }
2414
2483
  });
2415
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
+
2416
2607
  // src/cli/args.ts
2417
2608
  function getArg(args2, flag) {
2418
2609
  const idx = args2.indexOf(flag);
@@ -2431,16 +2622,18 @@ function parseArgs() {
2431
2622
  kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
2432
2623
  kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
2433
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]
2434
2626
  kody status --task-id <id> [--cwd <path>]
2435
2627
  kody --help`);
2436
2628
  process.exit(0);
2437
2629
  }
2438
2630
  const command2 = args2[0];
2439
- if (!["run", "rerun", "fix", "status"].includes(command2)) {
2631
+ if (!["run", "rerun", "fix", "status", "review"].includes(command2)) {
2440
2632
  console.error(`Unknown command: ${command2}`);
2441
2633
  process.exit(1);
2442
2634
  }
2443
2635
  const issueStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER;
2636
+ const prStr = getArg(args2, "--pr-number") ?? process.env.PR_NUMBER;
2444
2637
  const localFlag = hasFlag(args2, "--local");
2445
2638
  return {
2446
2639
  command: command2,
@@ -2450,6 +2643,7 @@ function parseArgs() {
2450
2643
  dryRun: hasFlag(args2, "--dry-run") || process.env.DRY_RUN === "true",
2451
2644
  cwd: getArg(args2, "--cwd"),
2452
2645
  issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
2646
+ prNumber: prStr ? parseInt(prStr, 10) : void 0,
2453
2647
  feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
2454
2648
  local: localFlag || !isCI2 && !hasFlag(args2, "--no-local"),
2455
2649
  complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
@@ -2464,9 +2658,9 @@ var init_args = __esm({
2464
2658
  });
2465
2659
 
2466
2660
  // src/cli/litellm.ts
2467
- import * as fs16 from "fs";
2468
- import * as path15 from "path";
2469
- 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";
2470
2664
  async function checkLitellmHealth(url) {
2471
2665
  try {
2472
2666
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -2476,8 +2670,8 @@ async function checkLitellmHealth(url) {
2476
2670
  }
2477
2671
  }
2478
2672
  async function tryStartLitellm(url, projectDir) {
2479
- const configPath = path15.join(projectDir, "litellm-config.yaml");
2480
- if (!fs16.existsSync(configPath)) {
2673
+ const configPath = path17.join(projectDir, "litellm-config.yaml");
2674
+ if (!fs18.existsSync(configPath)) {
2481
2675
  logger.warn("litellm-config.yaml not found \u2014 cannot start proxy");
2482
2676
  return null;
2483
2677
  }
@@ -2485,11 +2679,11 @@ async function tryStartLitellm(url, projectDir) {
2485
2679
  const port = portMatch ? portMatch[1] : "4000";
2486
2680
  let litellmFound = false;
2487
2681
  try {
2488
- execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2682
+ execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2489
2683
  litellmFound = true;
2490
2684
  } catch {
2491
2685
  try {
2492
- execFileSync9("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
2686
+ execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
2493
2687
  litellmFound = true;
2494
2688
  } catch {
2495
2689
  }
@@ -2502,17 +2696,17 @@ async function tryStartLitellm(url, projectDir) {
2502
2696
  let cmd;
2503
2697
  let args2;
2504
2698
  try {
2505
- execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2699
+ execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2506
2700
  cmd = "litellm";
2507
2701
  args2 = ["--config", configPath, "--port", port];
2508
2702
  } catch {
2509
2703
  cmd = "python3";
2510
2704
  args2 = ["-m", "litellm", "--config", configPath, "--port", port];
2511
2705
  }
2512
- const dotenvPath = path15.join(projectDir, ".env");
2706
+ const dotenvPath = path17.join(projectDir, ".env");
2513
2707
  const dotenvVars = {};
2514
- if (fs16.existsSync(dotenvPath)) {
2515
- 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")) {
2516
2710
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
2517
2711
  if (match) dotenvVars[match[1]] = match[2];
2518
2712
  }
@@ -2551,49 +2745,9 @@ var init_litellm = __esm({
2551
2745
  }
2552
2746
  });
2553
2747
 
2554
- // src/cli/task-resolution.ts
2555
- import * as fs17 from "fs";
2556
- import * as path16 from "path";
2557
- import { execFileSync as execFileSync10 } from "child_process";
2558
- function findLatestTaskForIssue(issueNumber, projectDir) {
2559
- const tasksDir = path16.join(projectDir, ".tasks");
2560
- if (!fs17.existsSync(tasksDir)) return null;
2561
- const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2562
- const prefix = `${issueNumber}-`;
2563
- const direct = allDirs.find((d) => d.startsWith(prefix));
2564
- if (direct) return direct;
2565
- try {
2566
- const branch = execFileSync10("git", ["branch", "--show-current"], {
2567
- encoding: "utf-8",
2568
- cwd: projectDir,
2569
- timeout: 5e3,
2570
- stdio: ["pipe", "pipe", "pipe"]
2571
- }).trim();
2572
- const branchIssueMatch = branch.match(/^(\d+)-/);
2573
- if (branchIssueMatch) {
2574
- const branchIssueNum = branchIssueMatch[1];
2575
- const branchPrefix = `${branchIssueNum}-`;
2576
- const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
2577
- if (fromBranch) return fromBranch;
2578
- }
2579
- } catch {
2580
- }
2581
- return null;
2582
- }
2583
- function generateTaskId() {
2584
- const now = /* @__PURE__ */ new Date();
2585
- const pad = (n) => String(n).padStart(2, "0");
2586
- return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
2587
- }
2588
- var init_task_resolution = __esm({
2589
- "src/cli/task-resolution.ts"() {
2590
- "use strict";
2591
- }
2592
- });
2593
-
2594
2748
  // src/cli/task-state.ts
2595
- import * as fs18 from "fs";
2596
- import * as path17 from "path";
2749
+ import * as fs19 from "fs";
2750
+ import * as path18 from "path";
2597
2751
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
2598
2752
  if (!existingTaskId || !existingState) {
2599
2753
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -2625,11 +2779,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
2625
2779
  function resolveForIssue(issueNumber, projectDir) {
2626
2780
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
2627
2781
  if (existingTaskId) {
2628
- const statusPath = path17.join(projectDir, ".tasks", existingTaskId, "status.json");
2782
+ const statusPath = path18.join(projectDir, ".tasks", existingTaskId, "status.json");
2629
2783
  let existingState = null;
2630
- if (fs18.existsSync(statusPath)) {
2784
+ if (fs19.existsSync(statusPath)) {
2631
2785
  try {
2632
- existingState = JSON.parse(fs18.readFileSync(statusPath, "utf-8"));
2786
+ existingState = JSON.parse(fs19.readFileSync(statusPath, "utf-8"));
2633
2787
  } catch {
2634
2788
  }
2635
2789
  }
@@ -2659,13 +2813,13 @@ var init_task_state = __esm({
2659
2813
 
2660
2814
  // src/entry.ts
2661
2815
  var entry_exports = {};
2662
- import * as fs19 from "fs";
2663
- import * as path18 from "path";
2816
+ import * as fs20 from "fs";
2817
+ import * as path19 from "path";
2664
2818
  async function main() {
2665
2819
  const input = parseArgs();
2666
- const projectDir = input.cwd ? path18.resolve(input.cwd) : process.cwd();
2820
+ const projectDir = input.cwd ? path19.resolve(input.cwd) : process.cwd();
2667
2821
  if (input.cwd) {
2668
- if (!fs19.existsSync(projectDir)) {
2822
+ if (!fs20.existsSync(projectDir)) {
2669
2823
  console.error(`--cwd path does not exist: ${projectDir}`);
2670
2824
  process.exit(1);
2671
2825
  }
@@ -2673,7 +2827,7 @@ async function main() {
2673
2827
  setGhCwd(projectDir);
2674
2828
  logger.info(`Working directory: ${projectDir}`);
2675
2829
  }
2676
- if (input.issueNumber) {
2830
+ if (input.issueNumber && input.command !== "review") {
2677
2831
  const taskAction = resolveForIssue(input.issueNumber, projectDir);
2678
2832
  logger.info(`Task action: ${taskAction.action}`);
2679
2833
  if (taskAction.action === "already-completed") {
@@ -2709,35 +2863,109 @@ async function main() {
2709
2863
  taskId = `${input.issueNumber}-${generateTaskId()}`;
2710
2864
  } else if (input.command === "run" && input.task) {
2711
2865
  taskId = generateTaskId();
2866
+ } else if (input.command === "review") {
2867
+ taskId = input.prNumber ? `review-pr-${input.prNumber}-${generateTaskId()}` : `review-${generateTaskId()}`;
2712
2868
  } else {
2713
2869
  console.error("--task-id is required (or provide --issue-number to auto-generate)");
2714
2870
  process.exit(1);
2715
2871
  }
2716
2872
  }
2717
- const taskDir = path18.join(projectDir, ".tasks", taskId);
2718
- fs19.mkdirSync(taskDir, { recursive: true });
2873
+ const taskDir = path19.join(projectDir, ".tasks", taskId);
2874
+ fs20.mkdirSync(taskDir, { recursive: true });
2719
2875
  if (input.command === "status") {
2720
2876
  printStatus(taskId, taskDir);
2721
2877
  return;
2722
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
+ }
2723
2951
  logger.info("Preflight checks:");
2724
2952
  runPreflight();
2725
2953
  if (input.task) {
2726
- fs19.writeFileSync(path18.join(taskDir, "task.md"), input.task);
2954
+ fs20.writeFileSync(path19.join(taskDir, "task.md"), input.task);
2727
2955
  }
2728
- const taskMdPath = path18.join(taskDir, "task.md");
2729
- if (!fs19.existsSync(taskMdPath) && input.issueNumber) {
2956
+ const taskMdPath = path19.join(taskDir, "task.md");
2957
+ if (!fs20.existsSync(taskMdPath) && input.issueNumber) {
2730
2958
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
2731
2959
  const issue = getIssue(input.issueNumber);
2732
2960
  if (issue) {
2733
2961
  const taskContent = `# ${issue.title}
2734
2962
 
2735
2963
  ${issue.body ?? ""}`;
2736
- fs19.writeFileSync(taskMdPath, taskContent);
2964
+ fs20.writeFileSync(taskMdPath, taskContent);
2737
2965
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
2738
2966
  }
2739
2967
  }
2740
- if (!fs19.existsSync(taskMdPath)) {
2968
+ if (!fs20.existsSync(taskMdPath)) {
2741
2969
  console.error("No task.md found. Provide --task, --issue-number, or ensure .tasks/<id>/task.md exists.");
2742
2970
  process.exit(1);
2743
2971
  }
@@ -2821,7 +3049,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
2821
3049
  }
2822
3050
  }
2823
3051
  const state = await runPipeline(ctx);
2824
- const files = fs19.readdirSync(taskDir);
3052
+ const files = fs20.readdirSync(taskDir);
2825
3053
  console.log(`
2826
3054
  Artifacts in ${taskDir}:`);
2827
3055
  for (const f of files) {
@@ -2862,6 +3090,7 @@ var init_entry = __esm({
2862
3090
  init_config();
2863
3091
  init_github_api();
2864
3092
  init_logger();
3093
+ init_review_standalone();
2865
3094
  init_args();
2866
3095
  init_litellm();
2867
3096
  init_task_resolution();
@@ -2883,15 +3112,15 @@ var init_entry = __esm({
2883
3112
  });
2884
3113
 
2885
3114
  // src/bin/cli.ts
2886
- import * as fs20 from "fs";
2887
- import * as path19 from "path";
3115
+ import * as fs21 from "fs";
3116
+ import * as path20 from "path";
2888
3117
  import { execFileSync as execFileSync11 } from "child_process";
2889
3118
  import { fileURLToPath } from "url";
2890
- var __dirname = path19.dirname(fileURLToPath(import.meta.url));
2891
- var PKG_ROOT = path19.resolve(__dirname, "..", "..");
3119
+ var __dirname = path20.dirname(fileURLToPath(import.meta.url));
3120
+ var PKG_ROOT = path20.resolve(__dirname, "..", "..");
2892
3121
  function getVersion() {
2893
- const pkgPath = path19.join(PKG_ROOT, "package.json");
2894
- 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"));
2895
3124
  return pkg.version;
2896
3125
  }
2897
3126
  function checkCommand2(name, args2, fix) {
@@ -2907,7 +3136,7 @@ function checkCommand2(name, args2, fix) {
2907
3136
  }
2908
3137
  }
2909
3138
  function checkFile(filePath, description, fix) {
2910
- if (fs20.existsSync(filePath)) {
3139
+ if (fs21.existsSync(filePath)) {
2911
3140
  return { name: description, ok: true, detail: filePath };
2912
3141
  }
2913
3142
  return { name: description, ok: false, fix };
@@ -2979,10 +3208,10 @@ function checkGhSecret(repoSlug, secretName) {
2979
3208
  }
2980
3209
  function detectArchitecture(cwd) {
2981
3210
  const detected = [];
2982
- const pkgPath = path19.join(cwd, "package.json");
2983
- if (fs20.existsSync(pkgPath)) {
3211
+ const pkgPath = path20.join(cwd, "package.json");
3212
+ if (fs21.existsSync(pkgPath)) {
2984
3213
  try {
2985
- const pkg = JSON.parse(fs20.readFileSync(pkgPath, "utf-8"));
3214
+ const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
2986
3215
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2987
3216
  if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
2988
3217
  else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
@@ -3005,41 +3234,41 @@ function detectArchitecture(cwd) {
3005
3234
  if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
3006
3235
  if (pkg.type === "module") detected.push("- Module system: ESM");
3007
3236
  else detected.push("- Module system: CommonJS");
3008
- if (fs20.existsSync(path19.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
3009
- else if (fs20.existsSync(path19.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
3010
- else if (fs20.existsSync(path19.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
3011
- 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");
3012
3241
  } catch {
3013
3242
  }
3014
3243
  }
3015
3244
  try {
3016
- const entries = fs20.readdirSync(cwd, { withFileTypes: true });
3245
+ const entries = fs21.readdirSync(cwd, { withFileTypes: true });
3017
3246
  const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
3018
3247
  if (dirs.length > 0) detected.push(`- Top-level directories: ${dirs.join(", ")}`);
3019
3248
  } catch {
3020
3249
  }
3021
- const srcDir = path19.join(cwd, "src");
3022
- if (fs20.existsSync(srcDir)) {
3250
+ const srcDir = path20.join(cwd, "src");
3251
+ if (fs21.existsSync(srcDir)) {
3023
3252
  try {
3024
- const srcEntries = fs20.readdirSync(srcDir, { withFileTypes: true });
3253
+ const srcEntries = fs21.readdirSync(srcDir, { withFileTypes: true });
3025
3254
  const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
3026
3255
  if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
3027
3256
  } catch {
3028
3257
  }
3029
3258
  }
3030
3259
  const configs = [];
3031
- if (fs20.existsSync(path19.join(cwd, "tsconfig.json"))) configs.push("tsconfig.json");
3032
- if (fs20.existsSync(path19.join(cwd, "docker-compose.yml")) || fs20.existsSync(path19.join(cwd, "docker-compose.yaml"))) configs.push("docker-compose");
3033
- if (fs20.existsSync(path19.join(cwd, "Dockerfile"))) configs.push("Dockerfile");
3034
- 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");
3035
3264
  if (configs.length > 0) detected.push(`- Config files: ${configs.join(", ")}`);
3036
3265
  return detected;
3037
3266
  }
3038
3267
  function detectBasicConfig(cwd) {
3039
3268
  let pm = "pnpm";
3040
- if (fs20.existsSync(path19.join(cwd, "yarn.lock"))) pm = "yarn";
3041
- else if (fs20.existsSync(path19.join(cwd, "bun.lockb"))) pm = "bun";
3042
- 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";
3043
3272
  let defaultBranch = "main";
3044
3273
  try {
3045
3274
  const ref = execFileSync11("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
@@ -3083,9 +3312,9 @@ function smartInit(cwd) {
3083
3312
  const basic = detectBasicConfig(cwd);
3084
3313
  let context = "";
3085
3314
  const readIfExists = (rel, maxChars = 3e3) => {
3086
- const p = path19.join(cwd, rel);
3087
- if (fs20.existsSync(p)) {
3088
- 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");
3089
3318
  return content.slice(0, maxChars);
3090
3319
  }
3091
3320
  return null;
@@ -3111,14 +3340,14 @@ ${claudeMd}
3111
3340
 
3112
3341
  `;
3113
3342
  try {
3114
- 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);
3115
3344
  context += `## Top-level directories
3116
3345
  ${topDirs.join(", ")}
3117
3346
 
3118
3347
  `;
3119
- const srcDir = path19.join(cwd, "src");
3120
- if (fs20.existsSync(srcDir)) {
3121
- 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);
3122
3351
  context += `## src/ subdirectories
3123
3352
  ${srcDirs.join(", ")}
3124
3353
 
@@ -3128,7 +3357,7 @@ ${srcDirs.join(", ")}
3128
3357
  }
3129
3358
  const existingFiles = [];
3130
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"]) {
3131
- if (fs20.existsSync(path19.join(cwd, f))) existingFiles.push(f);
3360
+ if (fs21.existsSync(path20.join(cwd, f))) existingFiles.push(f);
3132
3361
  }
3133
3362
  if (existingFiles.length) context += `## Config files present
3134
3363
  ${existingFiles.join(", ")}
@@ -3234,7 +3463,7 @@ ${context}`;
3234
3463
  function validateQualityCommands(cwd, config, pm) {
3235
3464
  let scripts = {};
3236
3465
  try {
3237
- 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"));
3238
3467
  scripts = pkg.scripts ?? {};
3239
3468
  } catch {
3240
3469
  return;
@@ -3268,7 +3497,7 @@ function validateQualityCommands(cwd, config, pm) {
3268
3497
  function buildFallbackConfig(cwd, basic) {
3269
3498
  const pkg = (() => {
3270
3499
  try {
3271
- 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"));
3272
3501
  } catch {
3273
3502
  return {};
3274
3503
  }
@@ -3308,34 +3537,34 @@ function initCommand(opts) {
3308
3537
  console.log(`Project: ${cwd}
3309
3538
  `);
3310
3539
  console.log("\u2500\u2500 Files \u2500\u2500");
3311
- const templatesDir = path19.join(PKG_ROOT, "templates");
3312
- const workflowSrc = path19.join(templatesDir, "kody.yml");
3313
- const workflowDest = path19.join(cwd, ".github", "workflows", "kody.yml");
3314
- 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)) {
3315
3544
  console.error(" \u2717 Template kody.yml not found in package");
3316
3545
  process.exit(1);
3317
3546
  }
3318
- if (fs20.existsSync(workflowDest) && !opts.force) {
3547
+ if (fs21.existsSync(workflowDest) && !opts.force) {
3319
3548
  console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
3320
3549
  } else {
3321
- fs20.mkdirSync(path19.dirname(workflowDest), { recursive: true });
3322
- fs20.copyFileSync(workflowSrc, workflowDest);
3550
+ fs21.mkdirSync(path20.dirname(workflowDest), { recursive: true });
3551
+ fs21.copyFileSync(workflowSrc, workflowDest);
3323
3552
  console.log(" \u2713 .github/workflows/kody.yml");
3324
3553
  }
3325
- const configDest = path19.join(cwd, "kody.config.json");
3554
+ const configDest = path20.join(cwd, "kody.config.json");
3326
3555
  let smartResult = null;
3327
- if (!fs20.existsSync(configDest) || opts.force) {
3556
+ if (!fs21.existsSync(configDest) || opts.force) {
3328
3557
  smartResult = smartInit(cwd);
3329
- fs20.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
3558
+ fs21.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
3330
3559
  console.log(" \u2713 kody.config.json (auto-configured)");
3331
3560
  } else {
3332
3561
  console.log(" \u25CB kody.config.json (exists)");
3333
3562
  }
3334
- const gitignorePath = path19.join(cwd, ".gitignore");
3335
- if (fs20.existsSync(gitignorePath)) {
3336
- 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");
3337
3566
  if (!content.includes(".tasks/")) {
3338
- fs20.appendFileSync(gitignorePath, "\n.tasks/\n");
3567
+ fs21.appendFileSync(gitignorePath, "\n.tasks/\n");
3339
3568
  console.log(" \u2713 .gitignore (added .tasks/)");
3340
3569
  } else {
3341
3570
  console.log(" \u25CB .gitignore (.tasks/ already present)");
@@ -3348,7 +3577,7 @@ function initCommand(opts) {
3348
3577
  checkCommand2("git", ["--version"], "Install git"),
3349
3578
  checkCommand2("node", ["--version"], "Install Node.js >= 22"),
3350
3579
  checkCommand2("pnpm", ["--version"], "Install: npm i -g pnpm"),
3351
- checkFile(path19.join(cwd, "package.json"), "package.json", "Run: pnpm init")
3580
+ checkFile(path20.join(cwd, "package.json"), "package.json", "Run: pnpm init")
3352
3581
  ];
3353
3582
  for (const c of checks) {
3354
3583
  if (c.ok) {
@@ -3425,9 +3654,9 @@ function initCommand(opts) {
3425
3654
  }
3426
3655
  }
3427
3656
  console.log("\n\u2500\u2500 Config \u2500\u2500");
3428
- if (fs20.existsSync(configDest)) {
3657
+ if (fs21.existsSync(configDest)) {
3429
3658
  try {
3430
- const config = JSON.parse(fs20.readFileSync(configDest, "utf-8"));
3659
+ const config = JSON.parse(fs21.readFileSync(configDest, "utf-8"));
3431
3660
  const configChecks = [];
3432
3661
  if (config.github?.owner && config.github?.repo) {
3433
3662
  configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
@@ -3454,21 +3683,21 @@ function initCommand(opts) {
3454
3683
  }
3455
3684
  }
3456
3685
  console.log("\n\u2500\u2500 Project Memory \u2500\u2500");
3457
- const memoryDir = path19.join(cwd, ".kody", "memory");
3458
- fs20.mkdirSync(memoryDir, { recursive: true });
3459
- const archPath = path19.join(memoryDir, "architecture.md");
3460
- const conventionsPath = path19.join(memoryDir, "conventions.md");
3461
- 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) {
3462
3691
  console.log(" \u25CB .kody/memory/architecture.md (exists, use --force to regenerate)");
3463
3692
  } else if (smartResult?.architecture) {
3464
- fs20.writeFileSync(archPath, smartResult.architecture);
3693
+ fs21.writeFileSync(archPath, smartResult.architecture);
3465
3694
  const lineCount = smartResult.architecture.split("\n").length;
3466
3695
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines, LLM-generated)`);
3467
3696
  } else {
3468
3697
  const archItems = detectArchitecture(cwd);
3469
3698
  if (archItems.length > 0) {
3470
3699
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3471
- fs20.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
3700
+ fs21.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
3472
3701
 
3473
3702
  ## Overview
3474
3703
  ${archItems.join("\n")}
@@ -3478,14 +3707,14 @@ ${archItems.join("\n")}
3478
3707
  console.log(" \u25CB No architecture detected");
3479
3708
  }
3480
3709
  }
3481
- if (fs20.existsSync(conventionsPath) && !opts.force) {
3710
+ if (fs21.existsSync(conventionsPath) && !opts.force) {
3482
3711
  console.log(" \u25CB .kody/memory/conventions.md (exists, use --force to regenerate)");
3483
3712
  } else if (smartResult?.conventions) {
3484
- fs20.writeFileSync(conventionsPath, smartResult.conventions);
3713
+ fs21.writeFileSync(conventionsPath, smartResult.conventions);
3485
3714
  const lineCount = smartResult.conventions.split("\n").length;
3486
3715
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines, LLM-generated)`);
3487
3716
  } else {
3488
- 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");
3489
3718
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
3490
3719
  }
3491
3720
  console.log("\n\u2500\u2500 Git \u2500\u2500");
@@ -3494,7 +3723,7 @@ ${archItems.join("\n")}
3494
3723
  "kody.config.json",
3495
3724
  ".kody/memory/architecture.md",
3496
3725
  ".kody/memory/conventions.md"
3497
- ].filter((f) => fs20.existsSync(path19.join(cwd, f)));
3726
+ ].filter((f) => fs21.existsSync(path20.join(cwd, f)));
3498
3727
  if (filesToCommit.length > 0) {
3499
3728
  try {
3500
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.44",
3
+ "version": "0.1.45",
4
4
  "description": "Autonomous SDLC pipeline: Kody orchestration + Claude Code + LiteLLM",
5
5
  "license": "MIT",
6
6
  "type": "module",