@kody-ade/kody-engine-lite 0.1.55 → 0.1.57
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 +474 -458
- package/package.json +1 -1
- package/templates/kody.yml +22 -12
package/dist/bin/cli.js
CHANGED
|
@@ -1101,7 +1101,6 @@ var init_agent = __esm({
|
|
|
1101
1101
|
taskify: "explore",
|
|
1102
1102
|
plan: "explore",
|
|
1103
1103
|
build: "build",
|
|
1104
|
-
autofix: "build",
|
|
1105
1104
|
"review-fix": "build",
|
|
1106
1105
|
review: "review"
|
|
1107
1106
|
};
|
|
@@ -2979,13 +2978,11 @@ async function main() {
|
|
|
2979
2978
|
if (!proxyRunning) {
|
|
2980
2979
|
litellmProcess2 = await tryStartLitellm(config2.agent.litellmUrl, projectDir);
|
|
2981
2980
|
if (!litellmProcess2) {
|
|
2982
|
-
logger.
|
|
2983
|
-
|
|
2981
|
+
logger.error("LiteLLM is configured (litellmUrl) but could not be started. Install it with: pip install 'litellm[proxy]'");
|
|
2982
|
+
process.exit(1);
|
|
2984
2983
|
}
|
|
2985
2984
|
}
|
|
2986
|
-
|
|
2987
|
-
process.env.ANTHROPIC_BASE_URL = config2.agent.litellmUrl;
|
|
2988
|
-
}
|
|
2985
|
+
process.env.ANTHROPIC_BASE_URL = config2.agent.litellmUrl;
|
|
2989
2986
|
}
|
|
2990
2987
|
const runners2 = createRunners(config2);
|
|
2991
2988
|
const defaultRunnerName2 = config2.agent.defaultRunner ?? Object.keys(runners2)[0] ?? "claude";
|
|
@@ -3097,16 +3094,14 @@ ${input.feedback}` : reviewContext;
|
|
|
3097
3094
|
if (!proxyRunning) {
|
|
3098
3095
|
litellmProcess = await tryStartLitellm(config.agent.litellmUrl, projectDir);
|
|
3099
3096
|
if (!litellmProcess) {
|
|
3100
|
-
logger.
|
|
3101
|
-
|
|
3097
|
+
logger.error("LiteLLM is configured (litellmUrl) but could not be started. Install it with: pip install 'litellm[proxy]'");
|
|
3098
|
+
process.exit(1);
|
|
3102
3099
|
}
|
|
3103
3100
|
} else {
|
|
3104
3101
|
logger.info(`LiteLLM proxy already running at ${config.agent.litellmUrl}`);
|
|
3105
3102
|
}
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
logger.info(`ANTHROPIC_BASE_URL set to ${config.agent.litellmUrl}`);
|
|
3109
|
-
}
|
|
3103
|
+
process.env.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
|
|
3104
|
+
logger.info(`ANTHROPIC_BASE_URL set to ${config.agent.litellmUrl}`);
|
|
3110
3105
|
}
|
|
3111
3106
|
const runners = createRunners(config);
|
|
3112
3107
|
const defaultRunnerName = config.agent.defaultRunner ?? Object.keys(runners)[0] ?? "claude";
|
|
@@ -3310,135 +3305,6 @@ function checkGhSecret(repoSlug, secretName) {
|
|
|
3310
3305
|
};
|
|
3311
3306
|
}
|
|
3312
3307
|
}
|
|
3313
|
-
function detectArchitecture(cwd) {
|
|
3314
|
-
const detected = [];
|
|
3315
|
-
const pkgPath = path20.join(cwd, "package.json");
|
|
3316
|
-
if (fs21.existsSync(pkgPath)) {
|
|
3317
|
-
try {
|
|
3318
|
-
const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
|
|
3319
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3320
|
-
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
3321
|
-
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
3322
|
-
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
3323
|
-
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
3324
|
-
else if (allDeps.hono) detected.push(`- Framework: Hono ${allDeps.hono}`);
|
|
3325
|
-
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
3326
|
-
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
3327
|
-
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
3328
|
-
else if (allDeps.mocha) detected.push(`- Testing: mocha ${allDeps.mocha}`);
|
|
3329
|
-
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
3330
|
-
if (allDeps.prettier) detected.push(`- Formatting: prettier ${allDeps.prettier}`);
|
|
3331
|
-
if (allDeps.biome || allDeps["@biomejs/biome"]) detected.push("- Formatting: biome");
|
|
3332
|
-
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- ORM: Prisma");
|
|
3333
|
-
if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
|
|
3334
|
-
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
3335
|
-
if (allDeps.mongodb || allDeps.mongoose) detected.push("- Database: MongoDB");
|
|
3336
|
-
if (allDeps.redis || allDeps.ioredis) detected.push("- Cache: Redis");
|
|
3337
|
-
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
|
|
3338
|
-
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
3339
|
-
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
3340
|
-
else detected.push("- Module system: CommonJS");
|
|
3341
|
-
if (fs21.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
3342
|
-
else if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
3343
|
-
else if (fs21.existsSync(path20.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
3344
|
-
else if (fs21.existsSync(path20.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
3345
|
-
} catch {
|
|
3346
|
-
}
|
|
3347
|
-
}
|
|
3348
|
-
try {
|
|
3349
|
-
const entries = fs21.readdirSync(cwd, { withFileTypes: true });
|
|
3350
|
-
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
3351
|
-
if (dirs.length > 0) detected.push(`- Top-level directories: ${dirs.join(", ")}`);
|
|
3352
|
-
} catch {
|
|
3353
|
-
}
|
|
3354
|
-
const srcDir = path20.join(cwd, "src");
|
|
3355
|
-
if (fs21.existsSync(srcDir)) {
|
|
3356
|
-
try {
|
|
3357
|
-
const srcEntries = fs21.readdirSync(srcDir, { withFileTypes: true });
|
|
3358
|
-
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3359
|
-
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
3360
|
-
} catch {
|
|
3361
|
-
}
|
|
3362
|
-
}
|
|
3363
|
-
const configs = [];
|
|
3364
|
-
if (fs21.existsSync(path20.join(cwd, "tsconfig.json"))) configs.push("tsconfig.json");
|
|
3365
|
-
if (fs21.existsSync(path20.join(cwd, "docker-compose.yml")) || fs21.existsSync(path20.join(cwd, "docker-compose.yaml"))) configs.push("docker-compose");
|
|
3366
|
-
if (fs21.existsSync(path20.join(cwd, "Dockerfile"))) configs.push("Dockerfile");
|
|
3367
|
-
if (fs21.existsSync(path20.join(cwd, ".env")) || fs21.existsSync(path20.join(cwd, ".env.local"))) configs.push(".env");
|
|
3368
|
-
if (configs.length > 0) detected.push(`- Config files: ${configs.join(", ")}`);
|
|
3369
|
-
return detected;
|
|
3370
|
-
}
|
|
3371
|
-
var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
|
|
3372
|
-
function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
3373
|
-
const srcDir = path20.join(cwd, "src");
|
|
3374
|
-
const baseDir = fs21.existsSync(srcDir) ? srcDir : cwd;
|
|
3375
|
-
const results = [];
|
|
3376
|
-
function walk(dir) {
|
|
3377
|
-
const entries = [];
|
|
3378
|
-
try {
|
|
3379
|
-
for (const entry of fs21.readdirSync(dir, { withFileTypes: true })) {
|
|
3380
|
-
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
3381
|
-
const full = path20.join(dir, entry.name);
|
|
3382
|
-
if (entry.isDirectory()) {
|
|
3383
|
-
entries.push(...walk(full));
|
|
3384
|
-
} else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
|
|
3385
|
-
try {
|
|
3386
|
-
const stat = fs21.statSync(full);
|
|
3387
|
-
if (stat.size >= 200 && stat.size <= 5e3) {
|
|
3388
|
-
entries.push({ filePath: full, size: stat.size });
|
|
3389
|
-
}
|
|
3390
|
-
} catch {
|
|
3391
|
-
}
|
|
3392
|
-
}
|
|
3393
|
-
}
|
|
3394
|
-
} catch {
|
|
3395
|
-
}
|
|
3396
|
-
return entries;
|
|
3397
|
-
}
|
|
3398
|
-
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
3399
|
-
for (const { filePath } of files) {
|
|
3400
|
-
const rel = path20.relative(cwd, filePath);
|
|
3401
|
-
const content = fs21.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
3402
|
-
results.push(`### File: ${rel}
|
|
3403
|
-
\`\`\`typescript
|
|
3404
|
-
${content}
|
|
3405
|
-
\`\`\``);
|
|
3406
|
-
}
|
|
3407
|
-
return results.join("\n\n");
|
|
3408
|
-
}
|
|
3409
|
-
function buildStepCustomizationPrompt(stageName, defaultPrompt, repoContext, architecture, conventions) {
|
|
3410
|
-
return `You are customizing a Kody pipeline prompt for a specific repository.
|
|
3411
|
-
|
|
3412
|
-
## Your Task
|
|
3413
|
-
Take the default prompt template below and produce a CUSTOMIZED version tailored to this specific repository.
|
|
3414
|
-
|
|
3415
|
-
## Rules
|
|
3416
|
-
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.
|
|
3417
|
-
2. APPEND three new sections after the original content but BEFORE the {{TASK_CONTEXT}} line:
|
|
3418
|
-
- ## 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.
|
|
3419
|
-
- ## 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.
|
|
3420
|
-
- ## Acceptance Criteria \u2014 A concrete checklist (using markdown checkboxes) that defines "done" for this stage in this specific repo.
|
|
3421
|
-
3. Be SPECIFIC \u2014 reference actual file paths, function names, and conventions from the repo context provided below.
|
|
3422
|
-
4. Keep each appended section concise (10-20 lines max).
|
|
3423
|
-
5. Output ONLY the complete customized prompt markdown. No explanation before or after.
|
|
3424
|
-
|
|
3425
|
-
## Stage Being Customized
|
|
3426
|
-
Stage: ${stageName}
|
|
3427
|
-
|
|
3428
|
-
## Default Prompt Template
|
|
3429
|
-
${defaultPrompt}
|
|
3430
|
-
|
|
3431
|
-
## Repository Context
|
|
3432
|
-
|
|
3433
|
-
### Architecture
|
|
3434
|
-
${architecture}
|
|
3435
|
-
|
|
3436
|
-
### Conventions
|
|
3437
|
-
${conventions}
|
|
3438
|
-
|
|
3439
|
-
### Project Details
|
|
3440
|
-
${repoContext}`;
|
|
3441
|
-
}
|
|
3442
3308
|
function detectBasicConfig(cwd) {
|
|
3443
3309
|
let pm = "pnpm";
|
|
3444
3310
|
if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) pm = "yarn";
|
|
@@ -3483,193 +3349,7 @@ function detectBasicConfig(cwd) {
|
|
|
3483
3349
|
}
|
|
3484
3350
|
return { defaultBranch, owner, repo, pm };
|
|
3485
3351
|
}
|
|
3486
|
-
function
|
|
3487
|
-
const basic = detectBasicConfig(cwd);
|
|
3488
|
-
let context = "";
|
|
3489
|
-
const readIfExists = (rel, maxChars = 3e3) => {
|
|
3490
|
-
const p = path20.join(cwd, rel);
|
|
3491
|
-
if (fs21.existsSync(p)) {
|
|
3492
|
-
const content = fs21.readFileSync(p, "utf-8");
|
|
3493
|
-
return content.slice(0, maxChars);
|
|
3494
|
-
}
|
|
3495
|
-
return null;
|
|
3496
|
-
};
|
|
3497
|
-
const pkgJson = readIfExists("package.json");
|
|
3498
|
-
if (pkgJson) context += `## package.json
|
|
3499
|
-
${pkgJson}
|
|
3500
|
-
|
|
3501
|
-
`;
|
|
3502
|
-
const tsconfig = readIfExists("tsconfig.json", 1e3);
|
|
3503
|
-
if (tsconfig) context += `## tsconfig.json
|
|
3504
|
-
${tsconfig}
|
|
3505
|
-
|
|
3506
|
-
`;
|
|
3507
|
-
const readme = readIfExists("README.md", 2e3);
|
|
3508
|
-
if (readme) context += `## README.md (first 2000 chars)
|
|
3509
|
-
${readme}
|
|
3510
|
-
|
|
3511
|
-
`;
|
|
3512
|
-
const claudeMd = readIfExists("CLAUDE.md", 3e3);
|
|
3513
|
-
if (claudeMd) context += `## CLAUDE.md
|
|
3514
|
-
${claudeMd}
|
|
3515
|
-
|
|
3516
|
-
`;
|
|
3517
|
-
try {
|
|
3518
|
-
const topDirs = fs21.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
3519
|
-
context += `## Top-level directories
|
|
3520
|
-
${topDirs.join(", ")}
|
|
3521
|
-
|
|
3522
|
-
`;
|
|
3523
|
-
const srcDir = path20.join(cwd, "src");
|
|
3524
|
-
if (fs21.existsSync(srcDir)) {
|
|
3525
|
-
const srcDirs = fs21.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3526
|
-
context += `## src/ subdirectories
|
|
3527
|
-
${srcDirs.join(", ")}
|
|
3528
|
-
|
|
3529
|
-
`;
|
|
3530
|
-
}
|
|
3531
|
-
} catch {
|
|
3532
|
-
}
|
|
3533
|
-
const existingFiles = [];
|
|
3534
|
-
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"]) {
|
|
3535
|
-
if (fs21.existsSync(path20.join(cwd, f))) existingFiles.push(f);
|
|
3536
|
-
}
|
|
3537
|
-
if (existingFiles.length) context += `## Config files present
|
|
3538
|
-
${existingFiles.join(", ")}
|
|
3539
|
-
|
|
3540
|
-
`;
|
|
3541
|
-
context += `## Detected: package manager=${basic.pm}, default branch=${basic.defaultBranch}, github=${basic.owner}/${basic.repo}
|
|
3542
|
-
`;
|
|
3543
|
-
const prompt = `You are analyzing a project to configure Kody (an autonomous SDLC pipeline).
|
|
3544
|
-
|
|
3545
|
-
Given this project context, output ONLY a JSON object with EXACTLY this structure:
|
|
3546
|
-
|
|
3547
|
-
{
|
|
3548
|
-
"config": {
|
|
3549
|
-
"quality": {
|
|
3550
|
-
"typecheck": "${basic.pm} <script or command>",
|
|
3551
|
-
"lint": "${basic.pm} <script or command>",
|
|
3552
|
-
"lintFix": "${basic.pm} <script or command>",
|
|
3553
|
-
"format": "${basic.pm} <script or command>",
|
|
3554
|
-
"formatFix": "${basic.pm} <script or command>",
|
|
3555
|
-
"testUnit": "${basic.pm} <script or command>"
|
|
3556
|
-
},
|
|
3557
|
-
"git": { "defaultBranch": "${basic.defaultBranch}" },
|
|
3558
|
-
"github": { "owner": "${basic.owner}", "repo": "${basic.repo}" },
|
|
3559
|
-
"paths": { "taskDir": ".kody/tasks" },
|
|
3560
|
-
"agent": {
|
|
3561
|
-
"runner": "${"claude-code"}",
|
|
3562
|
-
"defaultRunner": "${"claude"}",
|
|
3563
|
-
"modelMap": { "cheap": "haiku", "mid": "sonnet", "strong": "opus" }
|
|
3564
|
-
}
|
|
3565
|
-
},
|
|
3566
|
-
"architecture": "# Architecture\\n\\n<markdown content>",
|
|
3567
|
-
"conventions": "# Conventions\\n\\n<markdown content>"
|
|
3568
|
-
}
|
|
3569
|
-
|
|
3570
|
-
CRITICAL rules for config.quality:
|
|
3571
|
-
- Every command MUST start with "${basic.pm}" (e.g., "${basic.pm} typecheck", "${basic.pm} lint")
|
|
3572
|
-
- Look at the package.json "scripts" section to find the correct script names
|
|
3573
|
-
- testUnit must run ONLY unit tests \u2014 exclude integration and e2e tests. If there's a "test:unit" script use it. Otherwise use "test" but add exclude flags for int/e2e.
|
|
3574
|
-
- If a script doesn't exist and can't be inferred, set the value to ""
|
|
3575
|
-
- Do NOT invent commands that don't exist in package.json scripts
|
|
3576
|
-
|
|
3577
|
-
Rules for architecture (markdown string):
|
|
3578
|
-
- Be specific about THIS project
|
|
3579
|
-
- Include: framework, language, database, testing, key directories, data flow
|
|
3580
|
-
- Reference CLAUDE.md and .ai-docs/ if they exist
|
|
3581
|
-
- Keep under 50 lines
|
|
3582
|
-
|
|
3583
|
-
Rules for conventions (markdown string):
|
|
3584
|
-
- Extract actual patterns from the project
|
|
3585
|
-
- If CLAUDE.md exists, reference it
|
|
3586
|
-
- If .ai-docs/ exists, reference it
|
|
3587
|
-
- Keep under 30 lines
|
|
3588
|
-
|
|
3589
|
-
Output ONLY valid JSON. No markdown fences. No explanation before or after.
|
|
3590
|
-
|
|
3591
|
-
${context}`;
|
|
3592
|
-
console.log(" \u23F3 Analyzing project with Claude Code...");
|
|
3593
|
-
try {
|
|
3594
|
-
const output = execFileSync11("claude", [
|
|
3595
|
-
"--print",
|
|
3596
|
-
"--model",
|
|
3597
|
-
"haiku",
|
|
3598
|
-
"--dangerously-skip-permissions",
|
|
3599
|
-
prompt
|
|
3600
|
-
], {
|
|
3601
|
-
encoding: "utf-8",
|
|
3602
|
-
timeout: 12e4,
|
|
3603
|
-
cwd,
|
|
3604
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
3605
|
-
}).trim();
|
|
3606
|
-
const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3607
|
-
const parsed = JSON.parse(cleaned);
|
|
3608
|
-
const config = parsed.config ?? {};
|
|
3609
|
-
if (!config.git) config.git = {};
|
|
3610
|
-
if (!config.github) config.github = {};
|
|
3611
|
-
if (!config.paths) config.paths = {};
|
|
3612
|
-
if (!config.agent) config.agent = {};
|
|
3613
|
-
config["$schema"] = "https://raw.githubusercontent.com/aharonyaircohen/Kody-Engine-Lite/main/kody.config.schema.json";
|
|
3614
|
-
config.git.defaultBranch = config.git.defaultBranch || basic.defaultBranch;
|
|
3615
|
-
config.github.owner = config.github.owner || basic.owner;
|
|
3616
|
-
config.github.repo = config.github.repo || basic.repo;
|
|
3617
|
-
config.paths.taskDir = config.paths.taskDir || ".kody/tasks";
|
|
3618
|
-
config.agent.runner = config.agent.runner || "claude-code";
|
|
3619
|
-
config.agent.defaultRunner = config.agent.defaultRunner || "claude";
|
|
3620
|
-
if (!config.agent.modelMap) {
|
|
3621
|
-
config.agent.modelMap = { cheap: "haiku", mid: "sonnet", strong: "opus" };
|
|
3622
|
-
}
|
|
3623
|
-
validateQualityCommands(cwd, config, basic.pm);
|
|
3624
|
-
return {
|
|
3625
|
-
config,
|
|
3626
|
-
architecture: parsed.architecture ?? "",
|
|
3627
|
-
conventions: parsed.conventions ?? ""
|
|
3628
|
-
};
|
|
3629
|
-
} catch (err) {
|
|
3630
|
-
console.log(" \u26A0 Smart detection failed, falling back to basic detection");
|
|
3631
|
-
return {
|
|
3632
|
-
config: buildFallbackConfig(cwd, basic),
|
|
3633
|
-
architecture: "",
|
|
3634
|
-
conventions: ""
|
|
3635
|
-
};
|
|
3636
|
-
}
|
|
3637
|
-
}
|
|
3638
|
-
function validateQualityCommands(cwd, config, pm) {
|
|
3639
|
-
let scripts = {};
|
|
3640
|
-
try {
|
|
3641
|
-
const pkg = JSON.parse(fs21.readFileSync(path20.join(cwd, "package.json"), "utf-8"));
|
|
3642
|
-
scripts = pkg.scripts ?? {};
|
|
3643
|
-
} catch {
|
|
3644
|
-
return;
|
|
3645
|
-
}
|
|
3646
|
-
const quality = config.quality ?? {};
|
|
3647
|
-
const overrides = [
|
|
3648
|
-
{ key: "typecheck", preferred: ["typecheck", "type-check"] },
|
|
3649
|
-
{ key: "lint", preferred: ["lint"] },
|
|
3650
|
-
{ key: "lintFix", preferred: ["lint:fix", "lint-fix"] },
|
|
3651
|
-
{ key: "format", preferred: ["format:check", "format-check", "prettier:check"] },
|
|
3652
|
-
{ key: "formatFix", preferred: ["format", "format:fix", "format-fix"] },
|
|
3653
|
-
{ key: "testUnit", preferred: ["test:unit", "test-unit", "test:ci"] }
|
|
3654
|
-
];
|
|
3655
|
-
for (const { key, preferred } of overrides) {
|
|
3656
|
-
const match = preferred.find((s) => scripts[s]);
|
|
3657
|
-
if (match) {
|
|
3658
|
-
const correct = `${pm} ${match}`;
|
|
3659
|
-
if (quality[key] !== correct) {
|
|
3660
|
-
quality[key] = correct;
|
|
3661
|
-
}
|
|
3662
|
-
}
|
|
3663
|
-
if (quality[key]) {
|
|
3664
|
-
const scriptName = quality[key].replace(`${pm} `, "");
|
|
3665
|
-
if (scriptName && !scripts[scriptName] && !scriptName.includes(" ")) {
|
|
3666
|
-
quality[key] = "";
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3670
|
-
config.quality = quality;
|
|
3671
|
-
}
|
|
3672
|
-
function buildFallbackConfig(cwd, basic) {
|
|
3352
|
+
function buildConfig(cwd, basic) {
|
|
3673
3353
|
const pkg = (() => {
|
|
3674
3354
|
try {
|
|
3675
3355
|
return JSON.parse(fs21.readFileSync(path20.join(cwd, "package.json"), "utf-8"));
|
|
@@ -3713,6 +3393,7 @@ function initCommand(opts) {
|
|
|
3713
3393
|
`);
|
|
3714
3394
|
console.log("\u2500\u2500 Files \u2500\u2500");
|
|
3715
3395
|
const templatesDir = path20.join(PKG_ROOT, "templates");
|
|
3396
|
+
const basic = detectBasicConfig(cwd);
|
|
3716
3397
|
const workflowSrc = path20.join(templatesDir, "kody.yml");
|
|
3717
3398
|
const workflowDest = path20.join(cwd, ".github", "workflows", "kody.yml");
|
|
3718
3399
|
if (!fs21.existsSync(workflowSrc)) {
|
|
@@ -3727,10 +3408,9 @@ function initCommand(opts) {
|
|
|
3727
3408
|
console.log(" \u2713 .github/workflows/kody.yml");
|
|
3728
3409
|
}
|
|
3729
3410
|
const configDest = path20.join(cwd, "kody.config.json");
|
|
3730
|
-
let smartResult = null;
|
|
3731
3411
|
if (!fs21.existsSync(configDest) || opts.force) {
|
|
3732
|
-
|
|
3733
|
-
fs21.writeFileSync(configDest, JSON.stringify(
|
|
3412
|
+
const config = buildConfig(cwd, basic);
|
|
3413
|
+
fs21.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
|
|
3734
3414
|
console.log(" \u2713 kody.config.json (auto-configured)");
|
|
3735
3415
|
} else {
|
|
3736
3416
|
console.log(" \u25CB kody.config.json (exists)");
|
|
@@ -3748,12 +3428,10 @@ function initCommand(opts) {
|
|
|
3748
3428
|
}
|
|
3749
3429
|
console.log("\n\u2500\u2500 Prerequisites \u2500\u2500");
|
|
3750
3430
|
const checks = [
|
|
3751
|
-
checkCommand2("claude", ["--version"], "Install: npm i -g @anthropic-ai/claude-code"),
|
|
3752
3431
|
checkCommand2("gh", ["--version"], "Install: https://cli.github.com"),
|
|
3753
3432
|
checkCommand2("git", ["--version"], "Install git"),
|
|
3754
3433
|
checkCommand2("node", ["--version"], "Install Node.js >= 22"),
|
|
3755
|
-
|
|
3756
|
-
checkFile(path20.join(cwd, "package.json"), "package.json", "Run: pnpm init")
|
|
3434
|
+
checkFile(path20.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
|
|
3757
3435
|
];
|
|
3758
3436
|
for (const c of checks) {
|
|
3759
3437
|
if (c.ok) {
|
|
@@ -3767,8 +3445,9 @@ function initCommand(opts) {
|
|
|
3767
3445
|
console.log(ghAuth.ok ? ` \u2713 ${ghAuth.name} (${ghAuth.detail})` : ` \u2717 ${ghAuth.name} \u2014 ${ghAuth.fix}`);
|
|
3768
3446
|
const ghRepo = checkGhRepoAccess(cwd);
|
|
3769
3447
|
console.log(ghRepo.ok ? ` \u2713 ${ghRepo.name} (${ghRepo.detail})` : ` \u2717 ${ghRepo.name} \u2014 ${ghRepo.fix}`);
|
|
3448
|
+
let repoSlug = "";
|
|
3770
3449
|
if (ghRepo.ok && ghRepo.detail) {
|
|
3771
|
-
|
|
3450
|
+
repoSlug = ghRepo.detail;
|
|
3772
3451
|
const secretChecks = [
|
|
3773
3452
|
checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")
|
|
3774
3453
|
];
|
|
@@ -3858,133 +3537,366 @@ function initCommand(opts) {
|
|
|
3858
3537
|
console.log(" \u2717 kody.config.json \u2014 invalid JSON");
|
|
3859
3538
|
}
|
|
3860
3539
|
}
|
|
3861
|
-
console.log("\n\u2500\u2500
|
|
3540
|
+
console.log("\n\u2500\u2500 Git \u2500\u2500");
|
|
3541
|
+
const filesToCommit = [
|
|
3542
|
+
".github/workflows/kody.yml",
|
|
3543
|
+
"kody.config.json"
|
|
3544
|
+
].filter((f) => fs21.existsSync(path20.join(cwd, f)));
|
|
3545
|
+
if (filesToCommit.length > 0) {
|
|
3546
|
+
try {
|
|
3547
|
+
const fullPaths = filesToCommit.map((f) => path20.join(cwd, f));
|
|
3548
|
+
execFileSync11("npx", ["prettier", "--write", ...fullPaths], {
|
|
3549
|
+
cwd,
|
|
3550
|
+
encoding: "utf-8",
|
|
3551
|
+
timeout: 3e4,
|
|
3552
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3553
|
+
});
|
|
3554
|
+
} catch {
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
if (filesToCommit.length > 0) {
|
|
3558
|
+
try {
|
|
3559
|
+
execFileSync11("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
3560
|
+
const staged = execFileSync11("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
3561
|
+
if (staged) {
|
|
3562
|
+
execFileSync11("git", ["commit", "-m", "chore: Add Kody Engine workflow and config\n\nAdd GitHub Actions workflow and auto-detected configuration for Kody Engine Lite."], { cwd, stdio: "pipe" });
|
|
3563
|
+
console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
|
|
3564
|
+
try {
|
|
3565
|
+
execFileSync11("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
3566
|
+
console.log(" \u2713 Pushed to origin");
|
|
3567
|
+
} catch {
|
|
3568
|
+
console.log(" \u25CB Push failed \u2014 run 'git push' manually");
|
|
3569
|
+
}
|
|
3570
|
+
} else {
|
|
3571
|
+
console.log(" \u25CB No new changes to commit");
|
|
3572
|
+
}
|
|
3573
|
+
} catch (err) {
|
|
3574
|
+
console.log(` \u25CB Git commit skipped: ${err instanceof Error ? err.message : err}`);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
const allChecks = [...checks, ghAuth, ghRepo];
|
|
3578
|
+
const failed = allChecks.filter((c) => !c.ok);
|
|
3579
|
+
console.log("\n\u2500\u2500 Summary \u2500\u2500");
|
|
3580
|
+
if (failed.length === 0) {
|
|
3581
|
+
console.log(" \u2713 All checks passed! Ready to use.");
|
|
3582
|
+
console.log(`
|
|
3583
|
+
\u2500\u2500 Getting Started \u2500\u2500
|
|
3584
|
+
|
|
3585
|
+
1. Bootstrap (optional but recommended):
|
|
3586
|
+
Create a GitHub issue comment with '@kody bootstrap'
|
|
3587
|
+
\u2192 Kody will analyze your repo and generate project-specific config
|
|
3588
|
+
|
|
3589
|
+
2. First task:
|
|
3590
|
+
Create a GitHub issue describing work to do, then comment '@kody'
|
|
3591
|
+
\u2192 Kody picks it up and runs the full pipeline
|
|
3592
|
+
|
|
3593
|
+
Commands:
|
|
3594
|
+
@kody Run full pipeline on an issue
|
|
3595
|
+
@kody bootstrap Analyze repo, generate memory + step files
|
|
3596
|
+
@kody fix Fix build failures
|
|
3597
|
+
@kody review Review a PR
|
|
3598
|
+
`);
|
|
3599
|
+
} else {
|
|
3600
|
+
console.log(` \u26A0 ${failed.length} issue(s) to fix:`);
|
|
3601
|
+
for (const c of failed) {
|
|
3602
|
+
console.log(` \u2022 ${c.name}: ${c.fix}`);
|
|
3603
|
+
}
|
|
3604
|
+
console.log("");
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
var STEP_STAGES = ["taskify", "plan", "build", "review", "review-fix"];
|
|
3608
|
+
function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
3609
|
+
const srcDir = path20.join(cwd, "src");
|
|
3610
|
+
const baseDir = fs21.existsSync(srcDir) ? srcDir : cwd;
|
|
3611
|
+
const results = [];
|
|
3612
|
+
function walk(dir) {
|
|
3613
|
+
const entries = [];
|
|
3614
|
+
try {
|
|
3615
|
+
for (const entry of fs21.readdirSync(dir, { withFileTypes: true })) {
|
|
3616
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
3617
|
+
const full = path20.join(dir, entry.name);
|
|
3618
|
+
if (entry.isDirectory()) {
|
|
3619
|
+
entries.push(...walk(full));
|
|
3620
|
+
} else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
|
|
3621
|
+
try {
|
|
3622
|
+
const stat = fs21.statSync(full);
|
|
3623
|
+
if (stat.size >= 200 && stat.size <= 5e3) {
|
|
3624
|
+
entries.push({ filePath: full, size: stat.size });
|
|
3625
|
+
}
|
|
3626
|
+
} catch {
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
} catch {
|
|
3631
|
+
}
|
|
3632
|
+
return entries;
|
|
3633
|
+
}
|
|
3634
|
+
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
3635
|
+
for (const { filePath } of files) {
|
|
3636
|
+
const rel = path20.relative(cwd, filePath);
|
|
3637
|
+
const content = fs21.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
3638
|
+
results.push(`### File: ${rel}
|
|
3639
|
+
\`\`\`typescript
|
|
3640
|
+
${content}
|
|
3641
|
+
\`\`\``);
|
|
3642
|
+
}
|
|
3643
|
+
return results.join("\n\n");
|
|
3644
|
+
}
|
|
3645
|
+
function ghComment(issueNumber, body, cwd) {
|
|
3646
|
+
try {
|
|
3647
|
+
let repoSlug = "";
|
|
3648
|
+
try {
|
|
3649
|
+
const configPath = path20.join(cwd, "kody.config.json");
|
|
3650
|
+
if (fs21.existsSync(configPath)) {
|
|
3651
|
+
const config = JSON.parse(fs21.readFileSync(configPath, "utf-8"));
|
|
3652
|
+
if (config.github?.owner && config.github?.repo) {
|
|
3653
|
+
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
} catch {
|
|
3657
|
+
}
|
|
3658
|
+
if (!repoSlug) return;
|
|
3659
|
+
execFileSync11("gh", [
|
|
3660
|
+
"issue",
|
|
3661
|
+
"comment",
|
|
3662
|
+
String(issueNumber),
|
|
3663
|
+
"--repo",
|
|
3664
|
+
repoSlug,
|
|
3665
|
+
"--body",
|
|
3666
|
+
body
|
|
3667
|
+
], {
|
|
3668
|
+
cwd,
|
|
3669
|
+
encoding: "utf-8",
|
|
3670
|
+
timeout: 15e3,
|
|
3671
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3672
|
+
});
|
|
3673
|
+
} catch {
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
function bootstrapCommand() {
|
|
3677
|
+
const cwd = process.cwd();
|
|
3678
|
+
const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
|
|
3679
|
+
console.log(`
|
|
3680
|
+
\u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
|
|
3681
|
+
`);
|
|
3682
|
+
if (issueNumber) {
|
|
3683
|
+
ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
|
|
3684
|
+
}
|
|
3685
|
+
const readIfExists = (rel, maxChars = 3e3) => {
|
|
3686
|
+
const p = path20.join(cwd, rel);
|
|
3687
|
+
if (fs21.existsSync(p)) return fs21.readFileSync(p, "utf-8").slice(0, maxChars);
|
|
3688
|
+
return null;
|
|
3689
|
+
};
|
|
3690
|
+
let repoContext = "";
|
|
3691
|
+
const pkgJson = readIfExists("package.json");
|
|
3692
|
+
if (pkgJson) repoContext += `## package.json
|
|
3693
|
+
${pkgJson}
|
|
3694
|
+
|
|
3695
|
+
`;
|
|
3696
|
+
const tsconfig = readIfExists("tsconfig.json", 1e3);
|
|
3697
|
+
if (tsconfig) repoContext += `## tsconfig.json
|
|
3698
|
+
${tsconfig}
|
|
3699
|
+
|
|
3700
|
+
`;
|
|
3701
|
+
const readme = readIfExists("README.md", 2e3);
|
|
3702
|
+
if (readme) repoContext += `## README.md (first 2000 chars)
|
|
3703
|
+
${readme}
|
|
3704
|
+
|
|
3705
|
+
`;
|
|
3706
|
+
const claudeMd = readIfExists("CLAUDE.md", 3e3);
|
|
3707
|
+
if (claudeMd) repoContext += `## CLAUDE.md
|
|
3708
|
+
${claudeMd}
|
|
3709
|
+
|
|
3710
|
+
`;
|
|
3711
|
+
const agentsMd = readIfExists("AGENTS.md", 3e3);
|
|
3712
|
+
if (agentsMd) repoContext += `## AGENTS.md
|
|
3713
|
+
${agentsMd}
|
|
3714
|
+
|
|
3715
|
+
`;
|
|
3716
|
+
const sampleFiles = gatherSampleSourceFiles(cwd);
|
|
3717
|
+
if (sampleFiles) repoContext += `## Sample Source Files
|
|
3718
|
+
${sampleFiles}
|
|
3719
|
+
|
|
3720
|
+
`;
|
|
3721
|
+
try {
|
|
3722
|
+
const topDirs = fs21.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
3723
|
+
repoContext += `## Top-level directories
|
|
3724
|
+
${topDirs.join(", ")}
|
|
3725
|
+
|
|
3726
|
+
`;
|
|
3727
|
+
const srcDir = path20.join(cwd, "src");
|
|
3728
|
+
if (fs21.existsSync(srcDir)) {
|
|
3729
|
+
const srcDirs = fs21.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3730
|
+
if (srcDirs.length > 0) repoContext += `## src/ subdirectories
|
|
3731
|
+
${srcDirs.join(", ")}
|
|
3732
|
+
|
|
3733
|
+
`;
|
|
3734
|
+
}
|
|
3735
|
+
} catch {
|
|
3736
|
+
}
|
|
3737
|
+
const existingFiles = [];
|
|
3738
|
+
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"]) {
|
|
3739
|
+
if (fs21.existsSync(path20.join(cwd, f))) existingFiles.push(f);
|
|
3740
|
+
}
|
|
3741
|
+
if (existingFiles.length) repoContext += `## Config files present
|
|
3742
|
+
${existingFiles.join(", ")}
|
|
3743
|
+
|
|
3744
|
+
`;
|
|
3745
|
+
console.log("\u2500\u2500 Project Memory \u2500\u2500");
|
|
3862
3746
|
const memoryDir = path20.join(cwd, ".kody", "memory");
|
|
3863
3747
|
fs21.mkdirSync(memoryDir, { recursive: true });
|
|
3864
3748
|
const archPath = path20.join(memoryDir, "architecture.md");
|
|
3865
3749
|
const conventionsPath = path20.join(memoryDir, "conventions.md");
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3750
|
+
const memoryPrompt = `You are analyzing a project to generate documentation for an autonomous SDLC pipeline.
|
|
3751
|
+
|
|
3752
|
+
Given this project context, output ONLY a JSON object with EXACTLY this structure:
|
|
3753
|
+
|
|
3754
|
+
{
|
|
3755
|
+
"architecture": "# Architecture\\n\\n<markdown content>",
|
|
3756
|
+
"conventions": "# Conventions\\n\\n<markdown content>"
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
Rules for architecture (markdown string):
|
|
3760
|
+
- Be specific about THIS project
|
|
3761
|
+
- Include: framework, language, database, testing, key directories, data flow
|
|
3762
|
+
- Reference CLAUDE.md and .ai-docs/ if they exist
|
|
3763
|
+
- Keep under 50 lines
|
|
3764
|
+
|
|
3765
|
+
Rules for conventions (markdown string):
|
|
3766
|
+
- Extract actual patterns from the project
|
|
3767
|
+
- If CLAUDE.md exists, reference it
|
|
3768
|
+
- Keep under 30 lines
|
|
3769
|
+
|
|
3770
|
+
Output ONLY valid JSON. No markdown fences. No explanation.
|
|
3771
|
+
|
|
3772
|
+
${repoContext}`;
|
|
3773
|
+
console.log(" \u23F3 Analyzing project...");
|
|
3774
|
+
try {
|
|
3775
|
+
const output = execFileSync11("claude", [
|
|
3776
|
+
"--print",
|
|
3777
|
+
"--model",
|
|
3778
|
+
"haiku",
|
|
3779
|
+
"--dangerously-skip-permissions",
|
|
3780
|
+
memoryPrompt
|
|
3781
|
+
], {
|
|
3782
|
+
encoding: "utf-8",
|
|
3783
|
+
timeout: 9e4,
|
|
3784
|
+
cwd,
|
|
3785
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3786
|
+
}).trim();
|
|
3787
|
+
const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3788
|
+
const parsed = JSON.parse(cleaned);
|
|
3789
|
+
if (parsed.architecture) {
|
|
3790
|
+
fs21.writeFileSync(archPath, parsed.architecture);
|
|
3791
|
+
const lineCount = parsed.architecture.split("\n").length;
|
|
3792
|
+
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
|
|
3793
|
+
}
|
|
3794
|
+
if (parsed.conventions) {
|
|
3795
|
+
fs21.writeFileSync(conventionsPath, parsed.conventions);
|
|
3796
|
+
const lineCount = parsed.conventions.split("\n").length;
|
|
3797
|
+
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
|
|
3798
|
+
}
|
|
3799
|
+
} catch {
|
|
3800
|
+
console.log(" \u26A0 LLM analysis failed \u2014 creating basic memory files");
|
|
3801
|
+
const detected = detectArchitectureBasic(cwd);
|
|
3802
|
+
if (detected.length > 0) {
|
|
3875
3803
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3876
3804
|
fs21.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
3877
3805
|
|
|
3878
3806
|
## Overview
|
|
3879
|
-
${
|
|
3807
|
+
${detected.join("\n")}
|
|
3880
3808
|
`);
|
|
3881
|
-
console.log(` \u2713 .kody/memory/architecture.md (${
|
|
3882
|
-
} else {
|
|
3883
|
-
console.log(" \u25CB No architecture detected");
|
|
3809
|
+
console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
|
|
3884
3810
|
}
|
|
3885
|
-
}
|
|
3886
|
-
if (fs21.existsSync(conventionsPath) && !opts.force) {
|
|
3887
|
-
console.log(" \u25CB .kody/memory/conventions.md (exists, use --force to regenerate)");
|
|
3888
|
-
} else if (smartResult?.conventions) {
|
|
3889
|
-
fs21.writeFileSync(conventionsPath, smartResult.conventions);
|
|
3890
|
-
const lineCount = smartResult.conventions.split("\n").length;
|
|
3891
|
-
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines, LLM-generated)`);
|
|
3892
|
-
} else {
|
|
3893
3811
|
fs21.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
3894
3812
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
3895
3813
|
}
|
|
3896
3814
|
console.log("\n\u2500\u2500 Step Files \u2500\u2500");
|
|
3897
3815
|
const stepsDir = path20.join(cwd, ".kody", "steps");
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
const
|
|
3910
|
-
|
|
3911
|
-
|
|
3816
|
+
fs21.mkdirSync(stepsDir, { recursive: true });
|
|
3817
|
+
const arch = fs21.existsSync(archPath) ? fs21.readFileSync(archPath, "utf-8") : "";
|
|
3818
|
+
const conv = fs21.existsSync(conventionsPath) ? fs21.readFileSync(conventionsPath, "utf-8") : "";
|
|
3819
|
+
console.log(" \u23F3 Customizing step files...");
|
|
3820
|
+
let stepCount = 0;
|
|
3821
|
+
for (const stage of STEP_STAGES) {
|
|
3822
|
+
const templatePath = path20.join(PKG_ROOT, "prompts", `${stage}.md`);
|
|
3823
|
+
if (!fs21.existsSync(templatePath)) {
|
|
3824
|
+
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
3825
|
+
continue;
|
|
3826
|
+
}
|
|
3827
|
+
const defaultPrompt = fs21.readFileSync(templatePath, "utf-8");
|
|
3828
|
+
const contextPlaceholder = "{{TASK_CONTEXT}}";
|
|
3829
|
+
const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
|
|
3830
|
+
if (placeholderIdx === -1) {
|
|
3831
|
+
fs21.copyFileSync(templatePath, path20.join(stepsDir, `${stage}.md`));
|
|
3832
|
+
stepCount++;
|
|
3833
|
+
console.log(` \u2713 ${stage}.md`);
|
|
3834
|
+
continue;
|
|
3835
|
+
}
|
|
3836
|
+
const beforePlaceholder = defaultPrompt.slice(0, placeholderIdx).trimEnd();
|
|
3837
|
+
const afterPlaceholder = defaultPrompt.slice(placeholderIdx);
|
|
3838
|
+
const customizationPrompt = `You are customizing a Kody pipeline prompt for a specific repository.
|
|
3912
3839
|
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
if (readmeForSteps) repoContext += `## README.md
|
|
3916
|
-
${readmeForSteps}
|
|
3840
|
+
## Your Task
|
|
3841
|
+
Take the prompt template below and APPEND repository-specific sections to it.
|
|
3917
3842
|
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3843
|
+
## Rules
|
|
3844
|
+
1. Output the ENTIRE original prompt template UNCHANGED first \u2014 copy it exactly, character for character.
|
|
3845
|
+
2. Then APPEND these three new sections at the end:
|
|
3846
|
+
- ## 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.
|
|
3847
|
+
- ## Improvement Areas \u2014 Gaps or anti-patterns found in the codebase. Be specific with file paths.
|
|
3848
|
+
- ## Acceptance Criteria \u2014 A concrete checklist (markdown checkboxes) for "done" in this repo.
|
|
3849
|
+
3. Be SPECIFIC \u2014 reference actual file paths, function names, and conventions from the repo context.
|
|
3850
|
+
4. Keep each appended section concise (10-20 lines max).
|
|
3851
|
+
5. Output ONLY the customized prompt markdown. No explanation before or after.
|
|
3852
|
+
6. Do NOT include the text "${contextPlaceholder}" \u2014 it will be appended automatically after your output.
|
|
3922
3853
|
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
if (agentsMdForSteps) repoContext += `## AGENTS.md
|
|
3926
|
-
${agentsMdForSteps}
|
|
3854
|
+
## Stage Being Customized
|
|
3855
|
+
Stage: ${stage}
|
|
3927
3856
|
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
if (sampleFiles) repoContext += `## Sample Source Files
|
|
3931
|
-
${sampleFiles}
|
|
3857
|
+
## Prompt Template (output this EXACTLY, then append your sections)
|
|
3858
|
+
${beforePlaceholder}
|
|
3932
3859
|
|
|
3933
|
-
|
|
3934
|
-
try {
|
|
3935
|
-
const srcEntries = fs21.readdirSync(path20.join(cwd, "src"), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3936
|
-
if (srcEntries.length > 0) repoContext += `## src/ structure
|
|
3937
|
-
${srcEntries.join(", ")}
|
|
3860
|
+
## Repository Context
|
|
3938
3861
|
|
|
3939
|
-
|
|
3862
|
+
### Architecture
|
|
3863
|
+
${arch}
|
|
3864
|
+
|
|
3865
|
+
### Conventions
|
|
3866
|
+
${conv}
|
|
3867
|
+
|
|
3868
|
+
### Project Details
|
|
3869
|
+
${repoContext}
|
|
3870
|
+
|
|
3871
|
+
REMINDER: Output the full prompt template first (unchanged), then your three appended sections. Do NOT include "${contextPlaceholder}".`;
|
|
3872
|
+
try {
|
|
3873
|
+
const output = execFileSync11("claude", [
|
|
3874
|
+
"--print",
|
|
3875
|
+
"--model",
|
|
3876
|
+
"haiku",
|
|
3877
|
+
"--dangerously-skip-permissions",
|
|
3878
|
+
customizationPrompt
|
|
3879
|
+
], {
|
|
3880
|
+
encoding: "utf-8",
|
|
3881
|
+
timeout: 9e4,
|
|
3882
|
+
cwd,
|
|
3883
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3884
|
+
}).trim();
|
|
3885
|
+
let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
3886
|
+
cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
|
|
3887
|
+
const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
|
|
3888
|
+
fs21.writeFileSync(path20.join(stepsDir, `${stage}.md`), finalPrompt);
|
|
3889
|
+
stepCount++;
|
|
3890
|
+
console.log(` \u2713 ${stage}.md`);
|
|
3940
3891
|
} catch {
|
|
3892
|
+
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
3893
|
+
fs21.copyFileSync(templatePath, path20.join(stepsDir, `${stage}.md`));
|
|
3894
|
+
stepCount++;
|
|
3941
3895
|
}
|
|
3942
|
-
const arch = fs21.existsSync(archPath) ? fs21.readFileSync(archPath, "utf-8") : "";
|
|
3943
|
-
const conv = fs21.existsSync(conventionsPath) ? fs21.readFileSync(conventionsPath, "utf-8") : "";
|
|
3944
|
-
console.log(" \u23F3 Customizing step files with Claude (sonnet)...");
|
|
3945
|
-
let stepCount = 0;
|
|
3946
|
-
for (const stage of STEP_STAGES) {
|
|
3947
|
-
const templatePath = path20.join(PKG_ROOT, "prompts", `${stage}.md`);
|
|
3948
|
-
if (!fs21.existsSync(templatePath)) {
|
|
3949
|
-
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
3950
|
-
continue;
|
|
3951
|
-
}
|
|
3952
|
-
const defaultPrompt = fs21.readFileSync(templatePath, "utf-8");
|
|
3953
|
-
const customizationPrompt = buildStepCustomizationPrompt(stage, defaultPrompt, repoContext, arch, conv);
|
|
3954
|
-
try {
|
|
3955
|
-
const output = execFileSync11("claude", [
|
|
3956
|
-
"--print",
|
|
3957
|
-
"--model",
|
|
3958
|
-
"sonnet",
|
|
3959
|
-
"--dangerously-skip-permissions",
|
|
3960
|
-
customizationPrompt
|
|
3961
|
-
], {
|
|
3962
|
-
encoding: "utf-8",
|
|
3963
|
-
timeout: 12e4,
|
|
3964
|
-
cwd,
|
|
3965
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
3966
|
-
}).trim();
|
|
3967
|
-
const cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
3968
|
-
if (!cleaned.includes("{{TASK_CONTEXT}}")) {
|
|
3969
|
-
console.log(` \u26A0 ${stage}.md \u2014 AI dropped {{TASK_CONTEXT}}, using default template`);
|
|
3970
|
-
fs21.writeFileSync(path20.join(stepsDir, `${stage}.md`), defaultPrompt);
|
|
3971
|
-
} else {
|
|
3972
|
-
fs21.writeFileSync(path20.join(stepsDir, `${stage}.md`), cleaned);
|
|
3973
|
-
}
|
|
3974
|
-
stepCount++;
|
|
3975
|
-
console.log(` \u2713 ${stage}.md`);
|
|
3976
|
-
} catch (err) {
|
|
3977
|
-
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
3978
|
-
fs21.copyFileSync(templatePath, path20.join(stepsDir, `${stage}.md`));
|
|
3979
|
-
stepCount++;
|
|
3980
|
-
}
|
|
3981
|
-
}
|
|
3982
|
-
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
3983
3896
|
}
|
|
3897
|
+
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
3984
3898
|
console.log("\n\u2500\u2500 Git \u2500\u2500");
|
|
3985
3899
|
const filesToCommit = [
|
|
3986
|
-
".github/workflows/kody.yml",
|
|
3987
|
-
"kody.config.json",
|
|
3988
3900
|
".kody/memory/architecture.md",
|
|
3989
3901
|
".kody/memory/conventions.md"
|
|
3990
3902
|
].filter((f) => fs21.existsSync(path20.join(cwd, f)));
|
|
@@ -3996,44 +3908,148 @@ ${srcEntries.join(", ")}
|
|
|
3996
3908
|
}
|
|
3997
3909
|
if (filesToCommit.length > 0) {
|
|
3998
3910
|
try {
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
3911
|
+
const fullPaths = filesToCommit.map((f) => path20.join(cwd, f));
|
|
3912
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
3913
|
+
execFileSync11("npx", ["prettier", "--write", ...fullPaths], {
|
|
3914
|
+
cwd,
|
|
3915
|
+
encoding: "utf-8",
|
|
3916
|
+
timeout: 3e4,
|
|
3917
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
console.log(" \u2713 Formatted files with Prettier");
|
|
3921
|
+
} catch {
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
const isCI3 = !!process.env.GITHUB_ACTIONS;
|
|
3925
|
+
if (filesToCommit.length > 0) {
|
|
3926
|
+
try {
|
|
3927
|
+
if (isCI3) {
|
|
3928
|
+
const branchName = `kody/bootstrap-${Date.now()}`;
|
|
3929
|
+
execFileSync11("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
|
|
3930
|
+
execFileSync11("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
3931
|
+
const staged = execFileSync11("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
3932
|
+
if (staged) {
|
|
3933
|
+
execFileSync11("git", ["commit", "-m", "chore: Add Kody project memory and step files\n\nBootstrap Kody Engine with project-specific architecture, conventions, and pipeline step files."], { cwd, stdio: "pipe" });
|
|
3934
|
+
execFileSync11("git", ["push", "-u", "origin", branchName], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
3935
|
+
console.log(` \u2713 Pushed branch: ${branchName}`);
|
|
3936
|
+
let baseBranch = "main";
|
|
3937
|
+
try {
|
|
3938
|
+
const configPath = path20.join(cwd, "kody.config.json");
|
|
3939
|
+
if (fs21.existsSync(configPath)) {
|
|
3940
|
+
const config = JSON.parse(fs21.readFileSync(configPath, "utf-8"));
|
|
3941
|
+
baseBranch = config.git?.defaultBranch ?? "main";
|
|
3942
|
+
}
|
|
3943
|
+
} catch {
|
|
3944
|
+
}
|
|
3945
|
+
try {
|
|
3946
|
+
const prUrl = execFileSync11("gh", [
|
|
3947
|
+
"pr",
|
|
3948
|
+
"create",
|
|
3949
|
+
"--title",
|
|
3950
|
+
"chore: Bootstrap Kody Engine",
|
|
3951
|
+
"--body",
|
|
3952
|
+
"## Summary\n\n- Add project memory (architecture + conventions)\n- Add customized pipeline step files\n\nGenerated by `@kody bootstrap`.",
|
|
3953
|
+
"--base",
|
|
3954
|
+
baseBranch,
|
|
3955
|
+
"--head",
|
|
3956
|
+
branchName
|
|
3957
|
+
], {
|
|
3958
|
+
cwd,
|
|
3959
|
+
encoding: "utf-8",
|
|
3960
|
+
timeout: 3e4,
|
|
3961
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3962
|
+
}).trim();
|
|
3963
|
+
console.log(` \u2713 Created PR: ${prUrl}`);
|
|
3964
|
+
if (issueNumber) {
|
|
3965
|
+
ghComment(issueNumber, `\u2705 **Bootstrap complete** \u2014 PR created: ${prUrl}
|
|
3966
|
+
|
|
3967
|
+
Review and merge to activate project-specific pipeline configuration.`, cwd);
|
|
3968
|
+
}
|
|
3969
|
+
} catch (prErr) {
|
|
3970
|
+
console.log(` \u25CB PR creation failed: ${prErr instanceof Error ? prErr.message : prErr}`);
|
|
3971
|
+
if (issueNumber) {
|
|
3972
|
+
ghComment(issueNumber, `\u26A0\uFE0F **Bootstrap complete** \u2014 files generated and pushed to branch \`${branchName}\`, but PR creation failed. Create it manually.`, cwd);
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
} else {
|
|
3976
|
+
console.log(" \u25CB No new changes to commit");
|
|
4009
3977
|
}
|
|
4010
3978
|
} else {
|
|
4011
|
-
|
|
3979
|
+
execFileSync11("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
3980
|
+
const staged = execFileSync11("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
3981
|
+
if (staged) {
|
|
3982
|
+
execFileSync11("git", ["commit", "-m", "chore: Add Kody project memory and step files\n\nBootstrap Kody Engine with project-specific architecture, conventions, and pipeline step files."], { cwd, stdio: "pipe" });
|
|
3983
|
+
console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
|
|
3984
|
+
try {
|
|
3985
|
+
execFileSync11("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
3986
|
+
console.log(" \u2713 Pushed to origin");
|
|
3987
|
+
} catch {
|
|
3988
|
+
console.log(" \u25CB Push failed \u2014 run 'git push' manually");
|
|
3989
|
+
}
|
|
3990
|
+
} else {
|
|
3991
|
+
console.log(" \u25CB No new changes to commit");
|
|
3992
|
+
}
|
|
4012
3993
|
}
|
|
4013
3994
|
} catch (err) {
|
|
4014
3995
|
console.log(` \u25CB Git commit skipped: ${err instanceof Error ? err.message : err}`);
|
|
3996
|
+
if (issueNumber) {
|
|
3997
|
+
ghComment(issueNumber, `\u274C **Bootstrap failed** \u2014 git operation error: ${err instanceof Error ? err.message : err}`, cwd);
|
|
3998
|
+
}
|
|
4015
3999
|
}
|
|
4016
4000
|
}
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
console.log("
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4001
|
+
console.log("\n\u2500\u2500 Done \u2500\u2500");
|
|
4002
|
+
console.log(" \u2713 Project bootstrap complete!");
|
|
4003
|
+
console.log(" Kody now has project-specific memory and customized step files.\n");
|
|
4004
|
+
}
|
|
4005
|
+
function detectArchitectureBasic(cwd) {
|
|
4006
|
+
const detected = [];
|
|
4007
|
+
const pkgPath = path20.join(cwd, "package.json");
|
|
4008
|
+
if (fs21.existsSync(pkgPath)) {
|
|
4009
|
+
try {
|
|
4010
|
+
const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
|
|
4011
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4012
|
+
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
4013
|
+
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
4014
|
+
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
4015
|
+
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
4016
|
+
else if (allDeps.hono) detected.push(`- Framework: Hono ${allDeps.hono}`);
|
|
4017
|
+
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
4018
|
+
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
4019
|
+
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
4020
|
+
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
4021
|
+
if (allDeps.prettier) detected.push(`- Formatting: prettier ${allDeps.prettier}`);
|
|
4022
|
+
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- ORM: Prisma");
|
|
4023
|
+
if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
|
|
4024
|
+
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
|
|
4025
|
+
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
4026
|
+
if (fs21.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
4027
|
+
else if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
4028
|
+
else if (fs21.existsSync(path20.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
4029
|
+
else if (fs21.existsSync(path20.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
4030
|
+
} catch {
|
|
4027
4031
|
}
|
|
4028
4032
|
}
|
|
4029
|
-
|
|
4033
|
+
return detected;
|
|
4030
4034
|
}
|
|
4031
4035
|
var args = process.argv.slice(2);
|
|
4032
4036
|
var command = args[0];
|
|
4033
4037
|
if (command === "init") {
|
|
4034
4038
|
initCommand({ force: args.includes("--force") });
|
|
4039
|
+
} else if (command === "bootstrap") {
|
|
4040
|
+
bootstrapCommand();
|
|
4035
4041
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
4036
4042
|
console.log(getVersion());
|
|
4037
4043
|
} else {
|
|
4038
4044
|
Promise.resolve().then(() => init_entry());
|
|
4039
4045
|
}
|
|
4046
|
+
export {
|
|
4047
|
+
buildConfig,
|
|
4048
|
+
checkCommand2 as checkCommand,
|
|
4049
|
+
checkFile,
|
|
4050
|
+
checkGhAuth,
|
|
4051
|
+
checkGhRepoAccess,
|
|
4052
|
+
checkGhSecret,
|
|
4053
|
+
detectArchitectureBasic,
|
|
4054
|
+
detectBasicConfig
|
|
4055
|
+
};
|
package/package.json
CHANGED
package/templates/kody.yml
CHANGED
|
@@ -109,7 +109,7 @@ jobs:
|
|
|
109
109
|
|
|
110
110
|
# Validate mode
|
|
111
111
|
case "$MODE" in
|
|
112
|
-
full|rerun|fix|status|approve|review) ;;
|
|
112
|
+
full|rerun|fix|status|approve|review|bootstrap) ;;
|
|
113
113
|
*)
|
|
114
114
|
# If first arg isn't a mode, it might be a task-id or nothing
|
|
115
115
|
if [ -n "$MODE" ] && [ "$MODE" != "" ]; then
|
|
@@ -139,6 +139,11 @@ jobs:
|
|
|
139
139
|
# Leave TASK_ID empty — entry.ts finds latest task for issue
|
|
140
140
|
fi
|
|
141
141
|
|
|
142
|
+
# Bootstrap mode: set task-id and skip normal pipeline
|
|
143
|
+
if [ "$MODE" = "bootstrap" ]; then
|
|
144
|
+
TASK_ID="bootstrap-$(date +%y%m%d-%H%M%S)"
|
|
145
|
+
fi
|
|
146
|
+
|
|
142
147
|
# Auto-generate task-id if not provided (only for full mode)
|
|
143
148
|
if [ -z "$TASK_ID" ] && [ "$MODE" = "full" ]; then
|
|
144
149
|
TASK_ID="${ISSUE_NUM}-$(date +%y%m%d-%H%M%S)"
|
|
@@ -237,17 +242,22 @@ jobs:
|
|
|
237
242
|
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
|
238
243
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
239
244
|
run: |
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
245
|
+
if [ "$MODE" = "bootstrap" ]; then
|
|
246
|
+
echo "Running bootstrap..."
|
|
247
|
+
kody-engine-lite bootstrap
|
|
248
|
+
else
|
|
249
|
+
CMD="run"
|
|
250
|
+
[ "$MODE" = "rerun" ] && CMD="rerun"
|
|
251
|
+
[ "$MODE" = "fix" ] && CMD="fix"
|
|
252
|
+
[ "$MODE" = "review" ] && CMD="review"
|
|
253
|
+
ARGS="--issue-number $ISSUE_NUMBER"
|
|
254
|
+
[ -n "$TASK_ID" ] && ARGS="$ARGS --task-id $TASK_ID"
|
|
255
|
+
[ -n "$PR_NUMBER" ] && ARGS="$ARGS --pr-number $PR_NUMBER"
|
|
256
|
+
[ -n "$FROM_STAGE" ] && ARGS="$ARGS --from $FROM_STAGE"
|
|
257
|
+
# FEEDBACK is passed via env var, not CLI arg (avoids shell escaping issues)
|
|
258
|
+
[ "$DRY_RUN" = "true" ] && ARGS="$ARGS --dry-run"
|
|
259
|
+
kody-engine-lite $CMD $ARGS
|
|
260
|
+
fi
|
|
251
261
|
|
|
252
262
|
- name: Pipeline summary
|
|
253
263
|
if: always()
|