@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.
- package/dist/bin/cli.js +535 -135
- package/package.json +1 -1
- package/dist/agent-runner.d.ts +0 -4
- package/dist/agent-runner.js +0 -122
- package/dist/ci/parse-inputs.d.ts +0 -6
- package/dist/ci/parse-inputs.js +0 -76
- package/dist/ci/parse-safety.d.ts +0 -6
- package/dist/ci/parse-safety.js +0 -22
- package/dist/cli/args.d.ts +0 -13
- package/dist/cli/args.js +0 -42
- package/dist/cli/litellm.d.ts +0 -2
- package/dist/cli/litellm.js +0 -85
- package/dist/cli/task-resolution.d.ts +0 -2
- package/dist/cli/task-resolution.js +0 -41
- package/dist/config.d.ts +0 -49
- package/dist/config.js +0 -72
- package/dist/context.d.ts +0 -4
- package/dist/context.js +0 -83
- package/dist/definitions.d.ts +0 -3
- package/dist/definitions.js +0 -59
- package/dist/entry.d.ts +0 -1
- package/dist/entry.js +0 -236
- package/dist/git-utils.d.ts +0 -13
- package/dist/git-utils.js +0 -174
- package/dist/github-api.d.ts +0 -14
- package/dist/github-api.js +0 -114
- package/dist/kody-utils.d.ts +0 -1
- package/dist/kody-utils.js +0 -9
- package/dist/learning/auto-learn.d.ts +0 -2
- package/dist/learning/auto-learn.js +0 -169
- package/dist/logger.d.ts +0 -14
- package/dist/logger.js +0 -51
- package/dist/memory.d.ts +0 -1
- package/dist/memory.js +0 -20
- package/dist/observer.d.ts +0 -9
- package/dist/observer.js +0 -80
- package/dist/pipeline/complexity.d.ts +0 -3
- package/dist/pipeline/complexity.js +0 -12
- package/dist/pipeline/executor-registry.d.ts +0 -3
- package/dist/pipeline/executor-registry.js +0 -20
- package/dist/pipeline/hooks.d.ts +0 -17
- package/dist/pipeline/hooks.js +0 -110
- package/dist/pipeline/questions.d.ts +0 -2
- package/dist/pipeline/questions.js +0 -44
- package/dist/pipeline/runner-selection.d.ts +0 -2
- package/dist/pipeline/runner-selection.js +0 -13
- package/dist/pipeline/state.d.ts +0 -4
- package/dist/pipeline/state.js +0 -37
- package/dist/pipeline.d.ts +0 -3
- package/dist/pipeline.js +0 -213
- package/dist/preflight.d.ts +0 -1
- package/dist/preflight.js +0 -69
- package/dist/retrospective.d.ts +0 -26
- package/dist/retrospective.js +0 -211
- package/dist/stages/agent.d.ts +0 -2
- package/dist/stages/agent.js +0 -94
- package/dist/stages/gate.d.ts +0 -2
- package/dist/stages/gate.js +0 -32
- package/dist/stages/review.d.ts +0 -2
- package/dist/stages/review.js +0 -32
- package/dist/stages/ship.d.ts +0 -3
- package/dist/stages/ship.js +0 -154
- package/dist/stages/verify.d.ts +0 -2
- package/dist/stages/verify.js +0 -94
- package/dist/types.d.ts +0 -61
- package/dist/types.js +0 -1
- package/dist/validators.d.ts +0 -8
- package/dist/validators.js +0 -42
- package/dist/verify-runner.d.ts +0 -11
- 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
|
|
2468
|
-
import * as
|
|
2469
|
-
import { execFileSync as
|
|
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 =
|
|
2480
|
-
if (!
|
|
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
|
-
|
|
2689
|
+
execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
2489
2690
|
litellmFound = true;
|
|
2490
2691
|
} catch {
|
|
2491
2692
|
try {
|
|
2492
|
-
|
|
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
|
-
|
|
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 =
|
|
2713
|
+
const dotenvPath = path17.join(projectDir, ".env");
|
|
2513
2714
|
const dotenvVars = {};
|
|
2514
|
-
if (
|
|
2515
|
-
for (const line of
|
|
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
|
|
2596
|
-
import * as
|
|
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 =
|
|
2789
|
+
const statusPath = path18.join(projectDir, ".tasks", existingTaskId, "status.json");
|
|
2629
2790
|
let existingState = null;
|
|
2630
|
-
if (
|
|
2791
|
+
if (fs19.existsSync(statusPath)) {
|
|
2631
2792
|
try {
|
|
2632
|
-
existingState = JSON.parse(
|
|
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
|
|
2663
|
-
import * as
|
|
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 ?
|
|
2827
|
+
const projectDir = input.cwd ? path19.resolve(input.cwd) : process.cwd();
|
|
2667
2828
|
if (input.cwd) {
|
|
2668
|
-
if (!
|
|
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 =
|
|
2718
|
-
|
|
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
|
-
|
|
2961
|
+
fs20.writeFileSync(path19.join(taskDir, "task.md"), input.task);
|
|
2727
2962
|
}
|
|
2728
|
-
const taskMdPath =
|
|
2729
|
-
if (!
|
|
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
|
-
|
|
2971
|
+
fs20.writeFileSync(taskMdPath, taskContent);
|
|
2737
2972
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
2738
2973
|
}
|
|
2739
2974
|
}
|
|
2740
|
-
if (!
|
|
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 =
|
|
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
|
|
2887
|
-
import * as
|
|
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 =
|
|
2891
|
-
var PKG_ROOT =
|
|
3126
|
+
var __dirname = path20.dirname(fileURLToPath(import.meta.url));
|
|
3127
|
+
var PKG_ROOT = path20.resolve(__dirname, "..", "..");
|
|
2892
3128
|
function getVersion() {
|
|
2893
|
-
const pkgPath =
|
|
2894
|
-
const pkg = JSON.parse(
|
|
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 (
|
|
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 =
|
|
2983
|
-
if (
|
|
3218
|
+
const pkgPath = path20.join(cwd, "package.json");
|
|
3219
|
+
if (fs21.existsSync(pkgPath)) {
|
|
2984
3220
|
try {
|
|
2985
|
-
const pkg = JSON.parse(
|
|
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 (
|
|
3009
|
-
else if (
|
|
3010
|
-
else if (
|
|
3011
|
-
else if (
|
|
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 =
|
|
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 =
|
|
3022
|
-
if (
|
|
3257
|
+
const srcDir = path20.join(cwd, "src");
|
|
3258
|
+
if (fs21.existsSync(srcDir)) {
|
|
3023
3259
|
try {
|
|
3024
|
-
const srcEntries =
|
|
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 (
|
|
3032
|
-
if (
|
|
3033
|
-
if (
|
|
3034
|
-
if (
|
|
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 (
|
|
3041
|
-
else if (
|
|
3042
|
-
else if (!
|
|
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 =
|
|
3087
|
-
if (
|
|
3088
|
-
const content =
|
|
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 =
|
|
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 =
|
|
3120
|
-
if (
|
|
3121
|
-
const srcDirs =
|
|
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 (
|
|
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(
|
|
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(
|
|
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 =
|
|
3312
|
-
const workflowSrc =
|
|
3313
|
-
const workflowDest =
|
|
3314
|
-
if (!
|
|
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 (
|
|
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
|
-
|
|
3322
|
-
|
|
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 =
|
|
3632
|
+
const configDest = path20.join(cwd, "kody.config.json");
|
|
3326
3633
|
let smartResult = null;
|
|
3327
|
-
if (!
|
|
3634
|
+
if (!fs21.existsSync(configDest) || opts.force) {
|
|
3328
3635
|
smartResult = smartInit(cwd);
|
|
3329
|
-
|
|
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 =
|
|
3335
|
-
if (
|
|
3336
|
-
const content =
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
3735
|
+
if (fs21.existsSync(configDest)) {
|
|
3429
3736
|
try {
|
|
3430
|
-
const config = JSON.parse(
|
|
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 =
|
|
3458
|
-
|
|
3459
|
-
const archPath =
|
|
3460
|
-
const conventionsPath =
|
|
3461
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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" });
|