@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.
- package/dist/bin/cli.js +362 -133
- 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
|
|
2468
|
-
import * as
|
|
2469
|
-
import { execFileSync as
|
|
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 =
|
|
2480
|
-
if (!
|
|
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
|
-
|
|
2682
|
+
execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
2489
2683
|
litellmFound = true;
|
|
2490
2684
|
} catch {
|
|
2491
2685
|
try {
|
|
2492
|
-
|
|
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
|
-
|
|
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 =
|
|
2706
|
+
const dotenvPath = path17.join(projectDir, ".env");
|
|
2513
2707
|
const dotenvVars = {};
|
|
2514
|
-
if (
|
|
2515
|
-
for (const line of
|
|
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
|
|
2596
|
-
import * as
|
|
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 =
|
|
2782
|
+
const statusPath = path18.join(projectDir, ".tasks", existingTaskId, "status.json");
|
|
2629
2783
|
let existingState = null;
|
|
2630
|
-
if (
|
|
2784
|
+
if (fs19.existsSync(statusPath)) {
|
|
2631
2785
|
try {
|
|
2632
|
-
existingState = JSON.parse(
|
|
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
|
|
2663
|
-
import * as
|
|
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 ?
|
|
2820
|
+
const projectDir = input.cwd ? path19.resolve(input.cwd) : process.cwd();
|
|
2667
2821
|
if (input.cwd) {
|
|
2668
|
-
if (!
|
|
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 =
|
|
2718
|
-
|
|
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
|
-
|
|
2954
|
+
fs20.writeFileSync(path19.join(taskDir, "task.md"), input.task);
|
|
2727
2955
|
}
|
|
2728
|
-
const taskMdPath =
|
|
2729
|
-
if (!
|
|
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
|
-
|
|
2964
|
+
fs20.writeFileSync(taskMdPath, taskContent);
|
|
2737
2965
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
2738
2966
|
}
|
|
2739
2967
|
}
|
|
2740
|
-
if (!
|
|
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 =
|
|
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
|
|
2887
|
-
import * as
|
|
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 =
|
|
2891
|
-
var PKG_ROOT =
|
|
3119
|
+
var __dirname = path20.dirname(fileURLToPath(import.meta.url));
|
|
3120
|
+
var PKG_ROOT = path20.resolve(__dirname, "..", "..");
|
|
2892
3121
|
function getVersion() {
|
|
2893
|
-
const pkgPath =
|
|
2894
|
-
const pkg = JSON.parse(
|
|
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 (
|
|
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 =
|
|
2983
|
-
if (
|
|
3211
|
+
const pkgPath = path20.join(cwd, "package.json");
|
|
3212
|
+
if (fs21.existsSync(pkgPath)) {
|
|
2984
3213
|
try {
|
|
2985
|
-
const pkg = JSON.parse(
|
|
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 (
|
|
3009
|
-
else if (
|
|
3010
|
-
else if (
|
|
3011
|
-
else if (
|
|
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 =
|
|
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 =
|
|
3022
|
-
if (
|
|
3250
|
+
const srcDir = path20.join(cwd, "src");
|
|
3251
|
+
if (fs21.existsSync(srcDir)) {
|
|
3023
3252
|
try {
|
|
3024
|
-
const srcEntries =
|
|
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 (
|
|
3032
|
-
if (
|
|
3033
|
-
if (
|
|
3034
|
-
if (
|
|
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 (
|
|
3041
|
-
else if (
|
|
3042
|
-
else if (!
|
|
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 =
|
|
3087
|
-
if (
|
|
3088
|
-
const content =
|
|
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 =
|
|
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 =
|
|
3120
|
-
if (
|
|
3121
|
-
const srcDirs =
|
|
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 (
|
|
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(
|
|
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(
|
|
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 =
|
|
3312
|
-
const workflowSrc =
|
|
3313
|
-
const workflowDest =
|
|
3314
|
-
if (!
|
|
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 (
|
|
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
|
-
|
|
3322
|
-
|
|
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 =
|
|
3554
|
+
const configDest = path20.join(cwd, "kody.config.json");
|
|
3326
3555
|
let smartResult = null;
|
|
3327
|
-
if (!
|
|
3556
|
+
if (!fs21.existsSync(configDest) || opts.force) {
|
|
3328
3557
|
smartResult = smartInit(cwd);
|
|
3329
|
-
|
|
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 =
|
|
3335
|
-
if (
|
|
3336
|
-
const content =
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
3657
|
+
if (fs21.existsSync(configDest)) {
|
|
3429
3658
|
try {
|
|
3430
|
-
const config = JSON.parse(
|
|
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 =
|
|
3458
|
-
|
|
3459
|
-
const archPath =
|
|
3460
|
-
const conventionsPath =
|
|
3461
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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" });
|