@kody-ade/kody-engine-lite 0.1.44 → 0.1.46

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 (70) hide show
  1. package/dist/bin/cli.js +535 -135
  2. package/package.json +1 -1
  3. package/dist/agent-runner.d.ts +0 -4
  4. package/dist/agent-runner.js +0 -122
  5. package/dist/ci/parse-inputs.d.ts +0 -6
  6. package/dist/ci/parse-inputs.js +0 -76
  7. package/dist/ci/parse-safety.d.ts +0 -6
  8. package/dist/ci/parse-safety.js +0 -22
  9. package/dist/cli/args.d.ts +0 -13
  10. package/dist/cli/args.js +0 -42
  11. package/dist/cli/litellm.d.ts +0 -2
  12. package/dist/cli/litellm.js +0 -85
  13. package/dist/cli/task-resolution.d.ts +0 -2
  14. package/dist/cli/task-resolution.js +0 -41
  15. package/dist/config.d.ts +0 -49
  16. package/dist/config.js +0 -72
  17. package/dist/context.d.ts +0 -4
  18. package/dist/context.js +0 -83
  19. package/dist/definitions.d.ts +0 -3
  20. package/dist/definitions.js +0 -59
  21. package/dist/entry.d.ts +0 -1
  22. package/dist/entry.js +0 -236
  23. package/dist/git-utils.d.ts +0 -13
  24. package/dist/git-utils.js +0 -174
  25. package/dist/github-api.d.ts +0 -14
  26. package/dist/github-api.js +0 -114
  27. package/dist/kody-utils.d.ts +0 -1
  28. package/dist/kody-utils.js +0 -9
  29. package/dist/learning/auto-learn.d.ts +0 -2
  30. package/dist/learning/auto-learn.js +0 -169
  31. package/dist/logger.d.ts +0 -14
  32. package/dist/logger.js +0 -51
  33. package/dist/memory.d.ts +0 -1
  34. package/dist/memory.js +0 -20
  35. package/dist/observer.d.ts +0 -9
  36. package/dist/observer.js +0 -80
  37. package/dist/pipeline/complexity.d.ts +0 -3
  38. package/dist/pipeline/complexity.js +0 -12
  39. package/dist/pipeline/executor-registry.d.ts +0 -3
  40. package/dist/pipeline/executor-registry.js +0 -20
  41. package/dist/pipeline/hooks.d.ts +0 -17
  42. package/dist/pipeline/hooks.js +0 -110
  43. package/dist/pipeline/questions.d.ts +0 -2
  44. package/dist/pipeline/questions.js +0 -44
  45. package/dist/pipeline/runner-selection.d.ts +0 -2
  46. package/dist/pipeline/runner-selection.js +0 -13
  47. package/dist/pipeline/state.d.ts +0 -4
  48. package/dist/pipeline/state.js +0 -37
  49. package/dist/pipeline.d.ts +0 -3
  50. package/dist/pipeline.js +0 -213
  51. package/dist/preflight.d.ts +0 -1
  52. package/dist/preflight.js +0 -69
  53. package/dist/retrospective.d.ts +0 -26
  54. package/dist/retrospective.js +0 -211
  55. package/dist/stages/agent.d.ts +0 -2
  56. package/dist/stages/agent.js +0 -94
  57. package/dist/stages/gate.d.ts +0 -2
  58. package/dist/stages/gate.js +0 -32
  59. package/dist/stages/review.d.ts +0 -2
  60. package/dist/stages/review.js +0 -32
  61. package/dist/stages/ship.d.ts +0 -3
  62. package/dist/stages/ship.js +0 -154
  63. package/dist/stages/verify.d.ts +0 -2
  64. package/dist/stages/verify.js +0 -94
  65. package/dist/types.d.ts +0 -61
  66. package/dist/types.js +0 -1
  67. package/dist/validators.d.ts +0 -8
  68. package/dist/validators.js +0 -42
  69. package/dist/verify-runner.d.ts +0 -11
  70. package/dist/verify-runner.js +0 -110
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"() {
@@ -683,7 +752,14 @@ var init_memory = __esm({
683
752
  // src/context.ts
684
753
  import * as fs4 from "fs";
685
754
  import * as path4 from "path";
686
- function readPromptFile(stageName) {
755
+ function readPromptFile(stageName, projectDir) {
756
+ if (projectDir) {
757
+ const stepFile = path4.join(projectDir, ".kody", "steps", `${stageName}.md`);
758
+ if (fs4.existsSync(stepFile)) {
759
+ return fs4.readFileSync(stepFile, "utf-8");
760
+ }
761
+ console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
762
+ }
687
763
  const scriptDir = new URL(".", import.meta.url).pathname;
688
764
  const candidates = [
689
765
  path4.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
@@ -765,7 +841,7 @@ ${feedback}
765
841
  }
766
842
  function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
767
843
  const memory = readProjectMemory(projectDir);
768
- const promptTemplate = readPromptFile(stageName);
844
+ const promptTemplate = readPromptFile(stageName, projectDir);
769
845
  const prompt = injectTaskContext(promptTemplate, taskId, taskDir, feedback);
770
846
  return memory ? `${memory}
771
847
  ---
@@ -2413,6 +2489,128 @@ var init_preflight = __esm({
2413
2489
  }
2414
2490
  });
2415
2491
 
2492
+ // src/cli/task-resolution.ts
2493
+ import * as fs16 from "fs";
2494
+ import * as path15 from "path";
2495
+ import { execFileSync as execFileSync9 } from "child_process";
2496
+ function findLatestTaskForIssue(issueNumber, projectDir) {
2497
+ const tasksDir = path15.join(projectDir, ".tasks");
2498
+ if (!fs16.existsSync(tasksDir)) return null;
2499
+ const allDirs = fs16.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2500
+ const prefix = `${issueNumber}-`;
2501
+ const direct = allDirs.find((d) => d.startsWith(prefix));
2502
+ if (direct) return direct;
2503
+ try {
2504
+ const branch = execFileSync9("git", ["branch", "--show-current"], {
2505
+ encoding: "utf-8",
2506
+ cwd: projectDir,
2507
+ timeout: 5e3,
2508
+ stdio: ["pipe", "pipe", "pipe"]
2509
+ }).trim();
2510
+ const branchIssueMatch = branch.match(/^(\d+)-/);
2511
+ if (branchIssueMatch) {
2512
+ const branchIssueNum = branchIssueMatch[1];
2513
+ const branchPrefix = `${branchIssueNum}-`;
2514
+ const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
2515
+ if (fromBranch) return fromBranch;
2516
+ }
2517
+ } catch {
2518
+ }
2519
+ return null;
2520
+ }
2521
+ function generateTaskId() {
2522
+ const now = /* @__PURE__ */ new Date();
2523
+ const pad = (n) => String(n).padStart(2, "0");
2524
+ return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
2525
+ }
2526
+ var init_task_resolution = __esm({
2527
+ "src/cli/task-resolution.ts"() {
2528
+ "use strict";
2529
+ }
2530
+ });
2531
+
2532
+ // src/review-standalone.ts
2533
+ import * as fs17 from "fs";
2534
+ import * as path16 from "path";
2535
+ function resolveReviewTarget(input) {
2536
+ if (input.prs.length === 0) {
2537
+ return {
2538
+ action: "none",
2539
+ message: `Issue #${input.issueNumber} has no open PRs. Nothing to review.`
2540
+ };
2541
+ }
2542
+ if (input.prs.length === 1) {
2543
+ return { action: "review", prNumber: input.prs[0].number };
2544
+ }
2545
+ const prList = input.prs.map((pr) => ` - #${pr.number}: ${pr.title}`).join("\n");
2546
+ return {
2547
+ action: "pick",
2548
+ prs: input.prs,
2549
+ message: `\u26A0\uFE0F Issue #${input.issueNumber} has ${input.prs.length} open PRs:
2550
+ ${prList}
2551
+
2552
+ Run: \`pnpm kody review --pr-number <n>\`
2553
+ Or comment on the specific PR: \`@kody review\``
2554
+ };
2555
+ }
2556
+ async function runStandaloneReview(input) {
2557
+ const taskId = input.taskId ?? `review-${generateTaskId()}`;
2558
+ const taskDir = path16.join(input.projectDir, ".tasks", taskId);
2559
+ fs17.mkdirSync(taskDir, { recursive: true });
2560
+ const taskContent = `# ${input.prTitle}
2561
+
2562
+ ${input.prBody ?? ""}`;
2563
+ fs17.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
2564
+ const reviewDef = STAGES.find((s) => s.name === "review");
2565
+ const ctx = {
2566
+ taskId,
2567
+ taskDir,
2568
+ projectDir: input.projectDir,
2569
+ runners: input.runners,
2570
+ sessions: {},
2571
+ input: {
2572
+ mode: "full",
2573
+ local: input.local
2574
+ }
2575
+ };
2576
+ logger.info(`[review] standalone review for: ${input.prTitle}`);
2577
+ const result = await executeAgentStage(ctx, reviewDef);
2578
+ if (result.outcome !== "completed") {
2579
+ return {
2580
+ outcome: "failed",
2581
+ taskDir,
2582
+ error: result.error ?? "Review stage failed"
2583
+ };
2584
+ }
2585
+ const reviewPath = path16.join(taskDir, "review.md");
2586
+ let reviewContent;
2587
+ if (fs17.existsSync(reviewPath)) {
2588
+ reviewContent = fs17.readFileSync(reviewPath, "utf-8");
2589
+ }
2590
+ return {
2591
+ outcome: "completed",
2592
+ reviewContent,
2593
+ taskDir
2594
+ };
2595
+ }
2596
+ function formatReviewComment(reviewContent, taskId) {
2597
+ return `## \u{1F50D} Kody Review (\`${taskId}\`)
2598
+
2599
+ ${reviewContent}
2600
+
2601
+ ---
2602
+ \u{1F916} Generated by Kody`;
2603
+ }
2604
+ var init_review_standalone = __esm({
2605
+ "src/review-standalone.ts"() {
2606
+ "use strict";
2607
+ init_definitions();
2608
+ init_agent();
2609
+ init_task_resolution();
2610
+ init_logger();
2611
+ }
2612
+ });
2613
+
2416
2614
  // src/cli/args.ts
2417
2615
  function getArg(args2, flag) {
2418
2616
  const idx = args2.indexOf(flag);
@@ -2431,16 +2629,18 @@ function parseArgs() {
2431
2629
  kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
2432
2630
  kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
2433
2631
  kody fix --task-id <id> [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
2632
+ kody review [--pr-number <n>] [--issue-number <n>] [--cwd <path>] [--local]
2434
2633
  kody status --task-id <id> [--cwd <path>]
2435
2634
  kody --help`);
2436
2635
  process.exit(0);
2437
2636
  }
2438
2637
  const command2 = args2[0];
2439
- if (!["run", "rerun", "fix", "status"].includes(command2)) {
2638
+ if (!["run", "rerun", "fix", "status", "review"].includes(command2)) {
2440
2639
  console.error(`Unknown command: ${command2}`);
2441
2640
  process.exit(1);
2442
2641
  }
2443
2642
  const issueStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER;
2643
+ const prStr = getArg(args2, "--pr-number") ?? process.env.PR_NUMBER;
2444
2644
  const localFlag = hasFlag(args2, "--local");
2445
2645
  return {
2446
2646
  command: command2,
@@ -2450,6 +2650,7 @@ function parseArgs() {
2450
2650
  dryRun: hasFlag(args2, "--dry-run") || process.env.DRY_RUN === "true",
2451
2651
  cwd: getArg(args2, "--cwd"),
2452
2652
  issueNumber: issueStr ? parseInt(issueStr, 10) : void 0,
2653
+ prNumber: prStr ? parseInt(prStr, 10) : void 0,
2453
2654
  feedback: getArg(args2, "--feedback") ?? process.env.FEEDBACK,
2454
2655
  local: localFlag || !isCI2 && !hasFlag(args2, "--no-local"),
2455
2656
  complexity: getArg(args2, "--complexity") ?? process.env.COMPLEXITY
@@ -2464,9 +2665,9 @@ var init_args = __esm({
2464
2665
  });
2465
2666
 
2466
2667
  // src/cli/litellm.ts
2467
- import * as fs16 from "fs";
2468
- import * as path15 from "path";
2469
- import { execFileSync as execFileSync9 } from "child_process";
2668
+ import * as fs18 from "fs";
2669
+ import * as path17 from "path";
2670
+ import { execFileSync as execFileSync10 } from "child_process";
2470
2671
  async function checkLitellmHealth(url) {
2471
2672
  try {
2472
2673
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -2476,8 +2677,8 @@ async function checkLitellmHealth(url) {
2476
2677
  }
2477
2678
  }
2478
2679
  async function tryStartLitellm(url, projectDir) {
2479
- const configPath = path15.join(projectDir, "litellm-config.yaml");
2480
- if (!fs16.existsSync(configPath)) {
2680
+ const configPath = path17.join(projectDir, "litellm-config.yaml");
2681
+ if (!fs18.existsSync(configPath)) {
2481
2682
  logger.warn("litellm-config.yaml not found \u2014 cannot start proxy");
2482
2683
  return null;
2483
2684
  }
@@ -2485,11 +2686,11 @@ async function tryStartLitellm(url, projectDir) {
2485
2686
  const port = portMatch ? portMatch[1] : "4000";
2486
2687
  let litellmFound = false;
2487
2688
  try {
2488
- execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2689
+ execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2489
2690
  litellmFound = true;
2490
2691
  } catch {
2491
2692
  try {
2492
- execFileSync9("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
2693
+ execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
2493
2694
  litellmFound = true;
2494
2695
  } catch {
2495
2696
  }
@@ -2502,17 +2703,17 @@ async function tryStartLitellm(url, projectDir) {
2502
2703
  let cmd;
2503
2704
  let args2;
2504
2705
  try {
2505
- execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2706
+ execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2506
2707
  cmd = "litellm";
2507
2708
  args2 = ["--config", configPath, "--port", port];
2508
2709
  } catch {
2509
2710
  cmd = "python3";
2510
2711
  args2 = ["-m", "litellm", "--config", configPath, "--port", port];
2511
2712
  }
2512
- const dotenvPath = path15.join(projectDir, ".env");
2713
+ const dotenvPath = path17.join(projectDir, ".env");
2513
2714
  const dotenvVars = {};
2514
- if (fs16.existsSync(dotenvPath)) {
2515
- for (const line of fs16.readFileSync(dotenvPath, "utf-8").split("\n")) {
2715
+ if (fs18.existsSync(dotenvPath)) {
2716
+ for (const line of fs18.readFileSync(dotenvPath, "utf-8").split("\n")) {
2516
2717
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
2517
2718
  if (match) dotenvVars[match[1]] = match[2];
2518
2719
  }
@@ -2551,49 +2752,9 @@ var init_litellm = __esm({
2551
2752
  }
2552
2753
  });
2553
2754
 
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
2755
  // src/cli/task-state.ts
2595
- import * as fs18 from "fs";
2596
- import * as path17 from "path";
2756
+ import * as fs19 from "fs";
2757
+ import * as path18 from "path";
2597
2758
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
2598
2759
  if (!existingTaskId || !existingState) {
2599
2760
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -2625,11 +2786,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
2625
2786
  function resolveForIssue(issueNumber, projectDir) {
2626
2787
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
2627
2788
  if (existingTaskId) {
2628
- const statusPath = path17.join(projectDir, ".tasks", existingTaskId, "status.json");
2789
+ const statusPath = path18.join(projectDir, ".tasks", existingTaskId, "status.json");
2629
2790
  let existingState = null;
2630
- if (fs18.existsSync(statusPath)) {
2791
+ if (fs19.existsSync(statusPath)) {
2631
2792
  try {
2632
- existingState = JSON.parse(fs18.readFileSync(statusPath, "utf-8"));
2793
+ existingState = JSON.parse(fs19.readFileSync(statusPath, "utf-8"));
2633
2794
  } catch {
2634
2795
  }
2635
2796
  }
@@ -2659,13 +2820,13 @@ var init_task_state = __esm({
2659
2820
 
2660
2821
  // src/entry.ts
2661
2822
  var entry_exports = {};
2662
- import * as fs19 from "fs";
2663
- import * as path18 from "path";
2823
+ import * as fs20 from "fs";
2824
+ import * as path19 from "path";
2664
2825
  async function main() {
2665
2826
  const input = parseArgs();
2666
- const projectDir = input.cwd ? path18.resolve(input.cwd) : process.cwd();
2827
+ const projectDir = input.cwd ? path19.resolve(input.cwd) : process.cwd();
2667
2828
  if (input.cwd) {
2668
- if (!fs19.existsSync(projectDir)) {
2829
+ if (!fs20.existsSync(projectDir)) {
2669
2830
  console.error(`--cwd path does not exist: ${projectDir}`);
2670
2831
  process.exit(1);
2671
2832
  }
@@ -2673,7 +2834,7 @@ async function main() {
2673
2834
  setGhCwd(projectDir);
2674
2835
  logger.info(`Working directory: ${projectDir}`);
2675
2836
  }
2676
- if (input.issueNumber) {
2837
+ if (input.issueNumber && input.command !== "review") {
2677
2838
  const taskAction = resolveForIssue(input.issueNumber, projectDir);
2678
2839
  logger.info(`Task action: ${taskAction.action}`);
2679
2840
  if (taskAction.action === "already-completed") {
@@ -2709,35 +2870,109 @@ async function main() {
2709
2870
  taskId = `${input.issueNumber}-${generateTaskId()}`;
2710
2871
  } else if (input.command === "run" && input.task) {
2711
2872
  taskId = generateTaskId();
2873
+ } else if (input.command === "review") {
2874
+ taskId = input.prNumber ? `review-pr-${input.prNumber}-${generateTaskId()}` : `review-${generateTaskId()}`;
2712
2875
  } else {
2713
2876
  console.error("--task-id is required (or provide --issue-number to auto-generate)");
2714
2877
  process.exit(1);
2715
2878
  }
2716
2879
  }
2717
- const taskDir = path18.join(projectDir, ".tasks", taskId);
2718
- fs19.mkdirSync(taskDir, { recursive: true });
2880
+ const taskDir = path19.join(projectDir, ".tasks", taskId);
2881
+ fs20.mkdirSync(taskDir, { recursive: true });
2719
2882
  if (input.command === "status") {
2720
2883
  printStatus(taskId, taskDir);
2721
2884
  return;
2722
2885
  }
2886
+ if (input.command === "review") {
2887
+ runPreflight();
2888
+ let prTitle = "Code review";
2889
+ let prBody = "";
2890
+ let prNumber = input.prNumber;
2891
+ if (!prNumber && input.issueNumber) {
2892
+ const prs = getPRsForIssue(input.issueNumber);
2893
+ const target = resolveReviewTarget({ issueNumber: input.issueNumber, prs });
2894
+ if (target.action === "none" || target.action === "pick") {
2895
+ console.log(target.message);
2896
+ if (!input.local && input.issueNumber) {
2897
+ try {
2898
+ postComment(input.issueNumber, target.message);
2899
+ } catch {
2900
+ }
2901
+ }
2902
+ process.exit(target.action === "none" ? 1 : 0);
2903
+ }
2904
+ prNumber = target.prNumber;
2905
+ }
2906
+ if (prNumber) {
2907
+ const details = getPRDetails(prNumber);
2908
+ if (details) {
2909
+ prTitle = details.title;
2910
+ prBody = details.body ?? "";
2911
+ }
2912
+ }
2913
+ const config2 = getProjectConfig();
2914
+ let litellmProcess2 = null;
2915
+ if (config2.agent.litellmUrl) {
2916
+ const proxyRunning = await checkLitellmHealth(config2.agent.litellmUrl);
2917
+ if (!proxyRunning) {
2918
+ litellmProcess2 = await tryStartLitellm(config2.agent.litellmUrl, projectDir);
2919
+ }
2920
+ if (config2.agent.litellmUrl) {
2921
+ process.env.ANTHROPIC_BASE_URL = config2.agent.litellmUrl;
2922
+ }
2923
+ }
2924
+ const runners2 = createRunners(config2);
2925
+ const defaultRunnerName2 = config2.agent.defaultRunner ?? Object.keys(runners2)[0] ?? "claude";
2926
+ const defaultRunner2 = runners2[defaultRunnerName2];
2927
+ if (!defaultRunner2) {
2928
+ console.error(`Default runner "${defaultRunnerName2}" not configured`);
2929
+ process.exit(1);
2930
+ }
2931
+ const healthy2 = await defaultRunner2.healthCheck();
2932
+ if (!healthy2) {
2933
+ console.error(`Runner "${defaultRunnerName2}" health check failed`);
2934
+ process.exit(1);
2935
+ }
2936
+ const result = await runStandaloneReview({
2937
+ projectDir,
2938
+ runners: runners2,
2939
+ prTitle,
2940
+ prBody,
2941
+ local: input.local ?? true,
2942
+ taskId
2943
+ });
2944
+ if (litellmProcess2) litellmProcess2.kill();
2945
+ if (result.outcome === "failed") {
2946
+ console.error(`Review failed: ${result.error}`);
2947
+ process.exit(1);
2948
+ }
2949
+ if (result.reviewContent) {
2950
+ console.log(result.reviewContent);
2951
+ if (!input.local && prNumber) {
2952
+ const comment = formatReviewComment(result.reviewContent, taskId);
2953
+ postPRComment(prNumber, comment);
2954
+ }
2955
+ }
2956
+ process.exit(0);
2957
+ }
2723
2958
  logger.info("Preflight checks:");
2724
2959
  runPreflight();
2725
2960
  if (input.task) {
2726
- fs19.writeFileSync(path18.join(taskDir, "task.md"), input.task);
2961
+ fs20.writeFileSync(path19.join(taskDir, "task.md"), input.task);
2727
2962
  }
2728
- const taskMdPath = path18.join(taskDir, "task.md");
2729
- if (!fs19.existsSync(taskMdPath) && input.issueNumber) {
2963
+ const taskMdPath = path19.join(taskDir, "task.md");
2964
+ if (!fs20.existsSync(taskMdPath) && input.issueNumber) {
2730
2965
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
2731
2966
  const issue = getIssue(input.issueNumber);
2732
2967
  if (issue) {
2733
2968
  const taskContent = `# ${issue.title}
2734
2969
 
2735
2970
  ${issue.body ?? ""}`;
2736
- fs19.writeFileSync(taskMdPath, taskContent);
2971
+ fs20.writeFileSync(taskMdPath, taskContent);
2737
2972
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
2738
2973
  }
2739
2974
  }
2740
- if (!fs19.existsSync(taskMdPath)) {
2975
+ if (!fs20.existsSync(taskMdPath)) {
2741
2976
  console.error("No task.md found. Provide --task, --issue-number, or ensure .tasks/<id>/task.md exists.");
2742
2977
  process.exit(1);
2743
2978
  }
@@ -2821,7 +3056,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
2821
3056
  }
2822
3057
  }
2823
3058
  const state = await runPipeline(ctx);
2824
- const files = fs19.readdirSync(taskDir);
3059
+ const files = fs20.readdirSync(taskDir);
2825
3060
  console.log(`
2826
3061
  Artifacts in ${taskDir}:`);
2827
3062
  for (const f of files) {
@@ -2862,6 +3097,7 @@ var init_entry = __esm({
2862
3097
  init_config();
2863
3098
  init_github_api();
2864
3099
  init_logger();
3100
+ init_review_standalone();
2865
3101
  init_args();
2866
3102
  init_litellm();
2867
3103
  init_task_resolution();
@@ -2883,15 +3119,15 @@ var init_entry = __esm({
2883
3119
  });
2884
3120
 
2885
3121
  // src/bin/cli.ts
2886
- import * as fs20 from "fs";
2887
- import * as path19 from "path";
3122
+ import * as fs21 from "fs";
3123
+ import * as path20 from "path";
2888
3124
  import { execFileSync as execFileSync11 } from "child_process";
2889
3125
  import { fileURLToPath } from "url";
2890
- var __dirname = path19.dirname(fileURLToPath(import.meta.url));
2891
- var PKG_ROOT = path19.resolve(__dirname, "..", "..");
3126
+ var __dirname = path20.dirname(fileURLToPath(import.meta.url));
3127
+ var PKG_ROOT = path20.resolve(__dirname, "..", "..");
2892
3128
  function getVersion() {
2893
- const pkgPath = path19.join(PKG_ROOT, "package.json");
2894
- const pkg = JSON.parse(fs20.readFileSync(pkgPath, "utf-8"));
3129
+ const pkgPath = path20.join(PKG_ROOT, "package.json");
3130
+ const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
2895
3131
  return pkg.version;
2896
3132
  }
2897
3133
  function checkCommand2(name, args2, fix) {
@@ -2907,7 +3143,7 @@ function checkCommand2(name, args2, fix) {
2907
3143
  }
2908
3144
  }
2909
3145
  function checkFile(filePath, description, fix) {
2910
- if (fs20.existsSync(filePath)) {
3146
+ if (fs21.existsSync(filePath)) {
2911
3147
  return { name: description, ok: true, detail: filePath };
2912
3148
  }
2913
3149
  return { name: description, ok: false, fix };
@@ -2979,10 +3215,10 @@ function checkGhSecret(repoSlug, secretName) {
2979
3215
  }
2980
3216
  function detectArchitecture(cwd) {
2981
3217
  const detected = [];
2982
- const pkgPath = path19.join(cwd, "package.json");
2983
- if (fs20.existsSync(pkgPath)) {
3218
+ const pkgPath = path20.join(cwd, "package.json");
3219
+ if (fs21.existsSync(pkgPath)) {
2984
3220
  try {
2985
- const pkg = JSON.parse(fs20.readFileSync(pkgPath, "utf-8"));
3221
+ const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
2986
3222
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2987
3223
  if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
2988
3224
  else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
@@ -3005,41 +3241,112 @@ function detectArchitecture(cwd) {
3005
3241
  if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
3006
3242
  if (pkg.type === "module") detected.push("- Module system: ESM");
3007
3243
  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");
3244
+ if (fs21.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
3245
+ else if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
3246
+ else if (fs21.existsSync(path20.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
3247
+ else if (fs21.existsSync(path20.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
3012
3248
  } catch {
3013
3249
  }
3014
3250
  }
3015
3251
  try {
3016
- const entries = fs20.readdirSync(cwd, { withFileTypes: true });
3252
+ const entries = fs21.readdirSync(cwd, { withFileTypes: true });
3017
3253
  const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
3018
3254
  if (dirs.length > 0) detected.push(`- Top-level directories: ${dirs.join(", ")}`);
3019
3255
  } catch {
3020
3256
  }
3021
- const srcDir = path19.join(cwd, "src");
3022
- if (fs20.existsSync(srcDir)) {
3257
+ const srcDir = path20.join(cwd, "src");
3258
+ if (fs21.existsSync(srcDir)) {
3023
3259
  try {
3024
- const srcEntries = fs20.readdirSync(srcDir, { withFileTypes: true });
3260
+ const srcEntries = fs21.readdirSync(srcDir, { withFileTypes: true });
3025
3261
  const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
3026
3262
  if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
3027
3263
  } catch {
3028
3264
  }
3029
3265
  }
3030
3266
  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");
3267
+ if (fs21.existsSync(path20.join(cwd, "tsconfig.json"))) configs.push("tsconfig.json");
3268
+ if (fs21.existsSync(path20.join(cwd, "docker-compose.yml")) || fs21.existsSync(path20.join(cwd, "docker-compose.yaml"))) configs.push("docker-compose");
3269
+ if (fs21.existsSync(path20.join(cwd, "Dockerfile"))) configs.push("Dockerfile");
3270
+ if (fs21.existsSync(path20.join(cwd, ".env")) || fs21.existsSync(path20.join(cwd, ".env.local"))) configs.push(".env");
3035
3271
  if (configs.length > 0) detected.push(`- Config files: ${configs.join(", ")}`);
3036
3272
  return detected;
3037
3273
  }
3274
+ var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
3275
+ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
3276
+ const srcDir = path20.join(cwd, "src");
3277
+ const baseDir = fs21.existsSync(srcDir) ? srcDir : cwd;
3278
+ const results = [];
3279
+ function walk(dir) {
3280
+ const entries = [];
3281
+ try {
3282
+ for (const entry of fs21.readdirSync(dir, { withFileTypes: true })) {
3283
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
3284
+ const full = path20.join(dir, entry.name);
3285
+ if (entry.isDirectory()) {
3286
+ entries.push(...walk(full));
3287
+ } else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
3288
+ try {
3289
+ const stat = fs21.statSync(full);
3290
+ if (stat.size >= 200 && stat.size <= 5e3) {
3291
+ entries.push({ filePath: full, size: stat.size });
3292
+ }
3293
+ } catch {
3294
+ }
3295
+ }
3296
+ }
3297
+ } catch {
3298
+ }
3299
+ return entries;
3300
+ }
3301
+ const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
3302
+ for (const { filePath } of files) {
3303
+ const rel = path20.relative(cwd, filePath);
3304
+ const content = fs21.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
3305
+ results.push(`### File: ${rel}
3306
+ \`\`\`typescript
3307
+ ${content}
3308
+ \`\`\``);
3309
+ }
3310
+ return results.join("\n\n");
3311
+ }
3312
+ function buildStepCustomizationPrompt(stageName, defaultPrompt, repoContext, architecture, conventions) {
3313
+ return `You are customizing a Kody pipeline prompt for a specific repository.
3314
+
3315
+ ## Your Task
3316
+ Take the default prompt template below and produce a CUSTOMIZED version tailored to this specific repository.
3317
+
3318
+ ## Rules
3319
+ 1. KEEP the entire original prompt intact \u2014 its role definition, rules, output format, and {{TASK_CONTEXT}} placeholder. Do not remove or rephrase any existing content.
3320
+ 2. APPEND three new sections after the original content but BEFORE the {{TASK_CONTEXT}} line:
3321
+ - ## Repo Patterns \u2014 Real code examples from this repo that demonstrate the patterns to follow. Include specific file paths, function signatures, and brief code snippets. Show what GOOD looks like in this repo.
3322
+ - ## Improvement Areas \u2014 Gaps, anti-patterns, or inconsistencies found in the codebase that this stage should address when touching related code. Be specific with file paths and what to fix. Do NOT refactor unrelated code \u2014 only improve what the task touches.
3323
+ - ## Acceptance Criteria \u2014 A concrete checklist (using markdown checkboxes) that defines "done" for this stage in this specific repo.
3324
+ 3. Be SPECIFIC \u2014 reference actual file paths, function names, and conventions from the repo context provided below.
3325
+ 4. Keep each appended section concise (10-20 lines max).
3326
+ 5. Output ONLY the complete customized prompt markdown. No explanation before or after.
3327
+
3328
+ ## Stage Being Customized
3329
+ Stage: ${stageName}
3330
+
3331
+ ## Default Prompt Template
3332
+ ${defaultPrompt}
3333
+
3334
+ ## Repository Context
3335
+
3336
+ ### Architecture
3337
+ ${architecture}
3338
+
3339
+ ### Conventions
3340
+ ${conventions}
3341
+
3342
+ ### Project Details
3343
+ ${repoContext}`;
3344
+ }
3038
3345
  function detectBasicConfig(cwd) {
3039
3346
  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";
3347
+ if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) pm = "yarn";
3348
+ else if (fs21.existsSync(path20.join(cwd, "bun.lockb"))) pm = "bun";
3349
+ else if (!fs21.existsSync(path20.join(cwd, "pnpm-lock.yaml")) && fs21.existsSync(path20.join(cwd, "package-lock.json"))) pm = "npm";
3043
3350
  let defaultBranch = "main";
3044
3351
  try {
3045
3352
  const ref = execFileSync11("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
@@ -3083,9 +3390,9 @@ function smartInit(cwd) {
3083
3390
  const basic = detectBasicConfig(cwd);
3084
3391
  let context = "";
3085
3392
  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");
3393
+ const p = path20.join(cwd, rel);
3394
+ if (fs21.existsSync(p)) {
3395
+ const content = fs21.readFileSync(p, "utf-8");
3089
3396
  return content.slice(0, maxChars);
3090
3397
  }
3091
3398
  return null;
@@ -3111,14 +3418,14 @@ ${claudeMd}
3111
3418
 
3112
3419
  `;
3113
3420
  try {
3114
- const topDirs = fs20.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
3421
+ const topDirs = fs21.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
3115
3422
  context += `## Top-level directories
3116
3423
  ${topDirs.join(", ")}
3117
3424
 
3118
3425
  `;
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);
3426
+ const srcDir = path20.join(cwd, "src");
3427
+ if (fs21.existsSync(srcDir)) {
3428
+ const srcDirs = fs21.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3122
3429
  context += `## src/ subdirectories
3123
3430
  ${srcDirs.join(", ")}
3124
3431
 
@@ -3128,7 +3435,7 @@ ${srcDirs.join(", ")}
3128
3435
  }
3129
3436
  const existingFiles = [];
3130
3437
  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);
3438
+ if (fs21.existsSync(path20.join(cwd, f))) existingFiles.push(f);
3132
3439
  }
3133
3440
  if (existingFiles.length) context += `## Config files present
3134
3441
  ${existingFiles.join(", ")}
@@ -3234,7 +3541,7 @@ ${context}`;
3234
3541
  function validateQualityCommands(cwd, config, pm) {
3235
3542
  let scripts = {};
3236
3543
  try {
3237
- const pkg = JSON.parse(fs20.readFileSync(path19.join(cwd, "package.json"), "utf-8"));
3544
+ const pkg = JSON.parse(fs21.readFileSync(path20.join(cwd, "package.json"), "utf-8"));
3238
3545
  scripts = pkg.scripts ?? {};
3239
3546
  } catch {
3240
3547
  return;
@@ -3268,7 +3575,7 @@ function validateQualityCommands(cwd, config, pm) {
3268
3575
  function buildFallbackConfig(cwd, basic) {
3269
3576
  const pkg = (() => {
3270
3577
  try {
3271
- return JSON.parse(fs20.readFileSync(path19.join(cwd, "package.json"), "utf-8"));
3578
+ return JSON.parse(fs21.readFileSync(path20.join(cwd, "package.json"), "utf-8"));
3272
3579
  } catch {
3273
3580
  return {};
3274
3581
  }
@@ -3308,34 +3615,34 @@ function initCommand(opts) {
3308
3615
  console.log(`Project: ${cwd}
3309
3616
  `);
3310
3617
  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)) {
3618
+ const templatesDir = path20.join(PKG_ROOT, "templates");
3619
+ const workflowSrc = path20.join(templatesDir, "kody.yml");
3620
+ const workflowDest = path20.join(cwd, ".github", "workflows", "kody.yml");
3621
+ if (!fs21.existsSync(workflowSrc)) {
3315
3622
  console.error(" \u2717 Template kody.yml not found in package");
3316
3623
  process.exit(1);
3317
3624
  }
3318
- if (fs20.existsSync(workflowDest) && !opts.force) {
3625
+ if (fs21.existsSync(workflowDest) && !opts.force) {
3319
3626
  console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
3320
3627
  } else {
3321
- fs20.mkdirSync(path19.dirname(workflowDest), { recursive: true });
3322
- fs20.copyFileSync(workflowSrc, workflowDest);
3628
+ fs21.mkdirSync(path20.dirname(workflowDest), { recursive: true });
3629
+ fs21.copyFileSync(workflowSrc, workflowDest);
3323
3630
  console.log(" \u2713 .github/workflows/kody.yml");
3324
3631
  }
3325
- const configDest = path19.join(cwd, "kody.config.json");
3632
+ const configDest = path20.join(cwd, "kody.config.json");
3326
3633
  let smartResult = null;
3327
- if (!fs20.existsSync(configDest) || opts.force) {
3634
+ if (!fs21.existsSync(configDest) || opts.force) {
3328
3635
  smartResult = smartInit(cwd);
3329
- fs20.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
3636
+ fs21.writeFileSync(configDest, JSON.stringify(smartResult.config, null, 2) + "\n");
3330
3637
  console.log(" \u2713 kody.config.json (auto-configured)");
3331
3638
  } else {
3332
3639
  console.log(" \u25CB kody.config.json (exists)");
3333
3640
  }
3334
- const gitignorePath = path19.join(cwd, ".gitignore");
3335
- if (fs20.existsSync(gitignorePath)) {
3336
- const content = fs20.readFileSync(gitignorePath, "utf-8");
3641
+ const gitignorePath = path20.join(cwd, ".gitignore");
3642
+ if (fs21.existsSync(gitignorePath)) {
3643
+ const content = fs21.readFileSync(gitignorePath, "utf-8");
3337
3644
  if (!content.includes(".tasks/")) {
3338
- fs20.appendFileSync(gitignorePath, "\n.tasks/\n");
3645
+ fs21.appendFileSync(gitignorePath, "\n.tasks/\n");
3339
3646
  console.log(" \u2713 .gitignore (added .tasks/)");
3340
3647
  } else {
3341
3648
  console.log(" \u25CB .gitignore (.tasks/ already present)");
@@ -3348,7 +3655,7 @@ function initCommand(opts) {
3348
3655
  checkCommand2("git", ["--version"], "Install git"),
3349
3656
  checkCommand2("node", ["--version"], "Install Node.js >= 22"),
3350
3657
  checkCommand2("pnpm", ["--version"], "Install: npm i -g pnpm"),
3351
- checkFile(path19.join(cwd, "package.json"), "package.json", "Run: pnpm init")
3658
+ checkFile(path20.join(cwd, "package.json"), "package.json", "Run: pnpm init")
3352
3659
  ];
3353
3660
  for (const c of checks) {
3354
3661
  if (c.ok) {
@@ -3425,9 +3732,9 @@ function initCommand(opts) {
3425
3732
  }
3426
3733
  }
3427
3734
  console.log("\n\u2500\u2500 Config \u2500\u2500");
3428
- if (fs20.existsSync(configDest)) {
3735
+ if (fs21.existsSync(configDest)) {
3429
3736
  try {
3430
- const config = JSON.parse(fs20.readFileSync(configDest, "utf-8"));
3737
+ const config = JSON.parse(fs21.readFileSync(configDest, "utf-8"));
3431
3738
  const configChecks = [];
3432
3739
  if (config.github?.owner && config.github?.repo) {
3433
3740
  configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
@@ -3454,21 +3761,21 @@ function initCommand(opts) {
3454
3761
  }
3455
3762
  }
3456
3763
  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) {
3764
+ const memoryDir = path20.join(cwd, ".kody", "memory");
3765
+ fs21.mkdirSync(memoryDir, { recursive: true });
3766
+ const archPath = path20.join(memoryDir, "architecture.md");
3767
+ const conventionsPath = path20.join(memoryDir, "conventions.md");
3768
+ if (fs21.existsSync(archPath) && !opts.force) {
3462
3769
  console.log(" \u25CB .kody/memory/architecture.md (exists, use --force to regenerate)");
3463
3770
  } else if (smartResult?.architecture) {
3464
- fs20.writeFileSync(archPath, smartResult.architecture);
3771
+ fs21.writeFileSync(archPath, smartResult.architecture);
3465
3772
  const lineCount = smartResult.architecture.split("\n").length;
3466
3773
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines, LLM-generated)`);
3467
3774
  } else {
3468
3775
  const archItems = detectArchitecture(cwd);
3469
3776
  if (archItems.length > 0) {
3470
3777
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3471
- fs20.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
3778
+ fs21.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
3472
3779
 
3473
3780
  ## Overview
3474
3781
  ${archItems.join("\n")}
@@ -3478,23 +3785,116 @@ ${archItems.join("\n")}
3478
3785
  console.log(" \u25CB No architecture detected");
3479
3786
  }
3480
3787
  }
3481
- if (fs20.existsSync(conventionsPath) && !opts.force) {
3788
+ if (fs21.existsSync(conventionsPath) && !opts.force) {
3482
3789
  console.log(" \u25CB .kody/memory/conventions.md (exists, use --force to regenerate)");
3483
3790
  } else if (smartResult?.conventions) {
3484
- fs20.writeFileSync(conventionsPath, smartResult.conventions);
3791
+ fs21.writeFileSync(conventionsPath, smartResult.conventions);
3485
3792
  const lineCount = smartResult.conventions.split("\n").length;
3486
3793
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines, LLM-generated)`);
3487
3794
  } else {
3488
- fs20.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
3795
+ fs21.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
3489
3796
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
3490
3797
  }
3798
+ console.log("\n\u2500\u2500 Step Files \u2500\u2500");
3799
+ const stepsDir = path20.join(cwd, ".kody", "steps");
3800
+ const stepsExist = fs21.existsSync(stepsDir) && fs21.readdirSync(stepsDir).some((f) => f.endsWith(".md"));
3801
+ if (stepsExist && !opts.force) {
3802
+ console.log(" \u25CB .kody/steps/ (exists, use --force to regenerate)");
3803
+ } else {
3804
+ fs21.mkdirSync(stepsDir, { recursive: true });
3805
+ const readIfExistsForSteps = (rel, maxChars = 3e3) => {
3806
+ const p = path20.join(cwd, rel);
3807
+ if (fs21.existsSync(p)) return fs21.readFileSync(p, "utf-8").slice(0, maxChars);
3808
+ return null;
3809
+ };
3810
+ let repoContext = "";
3811
+ const pkgForSteps = readIfExistsForSteps("package.json");
3812
+ if (pkgForSteps) repoContext += `## package.json
3813
+ ${pkgForSteps}
3814
+
3815
+ `;
3816
+ const readmeForSteps = readIfExistsForSteps("README.md", 2e3);
3817
+ if (readmeForSteps) repoContext += `## README.md
3818
+ ${readmeForSteps}
3819
+
3820
+ `;
3821
+ const claudeMdForSteps = readIfExistsForSteps("CLAUDE.md", 3e3);
3822
+ if (claudeMdForSteps) repoContext += `## CLAUDE.md
3823
+ ${claudeMdForSteps}
3824
+
3825
+ `;
3826
+ const agentsMdForSteps = readIfExistsForSteps("AGENTS.md", 3e3);
3827
+ if (agentsMdForSteps) repoContext += `## AGENTS.md
3828
+ ${agentsMdForSteps}
3829
+
3830
+ `;
3831
+ const sampleFiles = gatherSampleSourceFiles(cwd);
3832
+ if (sampleFiles) repoContext += `## Sample Source Files
3833
+ ${sampleFiles}
3834
+
3835
+ `;
3836
+ try {
3837
+ const srcEntries = fs21.readdirSync(path20.join(cwd, "src"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3838
+ if (srcEntries.length > 0) repoContext += `## src/ structure
3839
+ ${srcEntries.join(", ")}
3840
+
3841
+ `;
3842
+ } catch {
3843
+ }
3844
+ const arch = fs21.existsSync(archPath) ? fs21.readFileSync(archPath, "utf-8") : "";
3845
+ const conv = fs21.existsSync(conventionsPath) ? fs21.readFileSync(conventionsPath, "utf-8") : "";
3846
+ console.log(" \u23F3 Customizing step files with Claude (sonnet)...");
3847
+ let stepCount = 0;
3848
+ for (const stage of STEP_STAGES) {
3849
+ const templatePath = path20.join(PKG_ROOT, "prompts", `${stage}.md`);
3850
+ if (!fs21.existsSync(templatePath)) {
3851
+ console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
3852
+ continue;
3853
+ }
3854
+ const defaultPrompt = fs21.readFileSync(templatePath, "utf-8");
3855
+ const customizationPrompt = buildStepCustomizationPrompt(stage, defaultPrompt, repoContext, arch, conv);
3856
+ try {
3857
+ const output = execFileSync11("claude", [
3858
+ "--print",
3859
+ "--model",
3860
+ "sonnet",
3861
+ "--dangerously-skip-permissions",
3862
+ customizationPrompt
3863
+ ], {
3864
+ encoding: "utf-8",
3865
+ timeout: 12e4,
3866
+ cwd,
3867
+ stdio: ["pipe", "pipe", "pipe"]
3868
+ }).trim();
3869
+ if (!output.includes("{{TASK_CONTEXT}}")) {
3870
+ console.log(` \u26A0 ${stage}.md \u2014 AI dropped {{TASK_CONTEXT}}, using default template`);
3871
+ fs21.writeFileSync(path20.join(stepsDir, `${stage}.md`), defaultPrompt);
3872
+ } else {
3873
+ fs21.writeFileSync(path20.join(stepsDir, `${stage}.md`), output);
3874
+ }
3875
+ stepCount++;
3876
+ console.log(` \u2713 ${stage}.md`);
3877
+ } catch (err) {
3878
+ console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
3879
+ fs21.copyFileSync(templatePath, path20.join(stepsDir, `${stage}.md`));
3880
+ stepCount++;
3881
+ }
3882
+ }
3883
+ console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
3884
+ }
3491
3885
  console.log("\n\u2500\u2500 Git \u2500\u2500");
3492
3886
  const filesToCommit = [
3493
3887
  ".github/workflows/kody.yml",
3494
3888
  "kody.config.json",
3495
3889
  ".kody/memory/architecture.md",
3496
3890
  ".kody/memory/conventions.md"
3497
- ].filter((f) => fs20.existsSync(path19.join(cwd, f)));
3891
+ ].filter((f) => fs21.existsSync(path20.join(cwd, f)));
3892
+ for (const stage of STEP_STAGES) {
3893
+ const stepFile = `.kody/steps/${stage}.md`;
3894
+ if (fs21.existsSync(path20.join(cwd, stepFile))) {
3895
+ filesToCommit.push(stepFile);
3896
+ }
3897
+ }
3498
3898
  if (filesToCommit.length > 0) {
3499
3899
  try {
3500
3900
  execFileSync11("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });