@kody-ade/kody-engine-lite 0.1.104 → 0.1.105
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/README.md +1 -1
- package/dist/bin/cli.js +759 -1037
- package/package.json +1 -1
- package/prompts/autofix.md +20 -33
- package/prompts/review-fix.md +8 -12
- package/prompts/taskify.md +2 -19
- package/templates/kody.yml +26 -17
- 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
|
@@ -9,63 +9,8 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/bin/architecture-detection.ts
|
|
13
|
-
import * as fs4 from "fs";
|
|
14
|
-
import * as path3 from "path";
|
|
15
|
-
function detectArchitectureBasic(cwd) {
|
|
16
|
-
const detected = [];
|
|
17
|
-
const pkgPath = path3.join(cwd, "package.json");
|
|
18
|
-
if (fs4.existsSync(pkgPath)) {
|
|
19
|
-
try {
|
|
20
|
-
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
21
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
22
|
-
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
23
|
-
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
24
|
-
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
25
|
-
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
26
|
-
else if (allDeps.hono) detected.push(`- Framework: Hono ${allDeps.hono}`);
|
|
27
|
-
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
28
|
-
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
29
|
-
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
30
|
-
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
31
|
-
if (allDeps.prettier) detected.push(`- Formatting: prettier ${allDeps.prettier}`);
|
|
32
|
-
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- ORM: Prisma");
|
|
33
|
-
if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
|
|
34
|
-
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
|
|
35
|
-
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
36
|
-
if (fs4.existsSync(path3.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
37
|
-
else if (fs4.existsSync(path3.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
38
|
-
else if (fs4.existsSync(path3.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
39
|
-
else if (fs4.existsSync(path3.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
40
|
-
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
41
|
-
else detected.push("- Module system: CommonJS");
|
|
42
|
-
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
43
|
-
} catch {
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
try {
|
|
47
|
-
const topDirs = fs4.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
48
|
-
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
49
|
-
} catch {
|
|
50
|
-
}
|
|
51
|
-
const srcDir = path3.join(cwd, "src");
|
|
52
|
-
if (fs4.existsSync(srcDir)) {
|
|
53
|
-
try {
|
|
54
|
-
const srcDirs = fs4.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
55
|
-
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
56
|
-
} catch {
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return detected;
|
|
60
|
-
}
|
|
61
|
-
var init_architecture_detection = __esm({
|
|
62
|
-
"src/bin/architecture-detection.ts"() {
|
|
63
|
-
"use strict";
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
12
|
// src/agent-runner.ts
|
|
68
|
-
import { spawn, execFileSync
|
|
13
|
+
import { spawn, execFileSync } from "child_process";
|
|
69
14
|
function writeStdin(child, prompt) {
|
|
70
15
|
return new Promise((resolve4, reject) => {
|
|
71
16
|
if (!child.stdin) {
|
|
@@ -137,9 +82,9 @@ async function runSubprocess(command2, args2, prompt, timeout, options) {
|
|
|
137
82
|
${errDetail}`
|
|
138
83
|
};
|
|
139
84
|
}
|
|
140
|
-
function
|
|
85
|
+
function checkCommand(command2, args2) {
|
|
141
86
|
try {
|
|
142
|
-
|
|
87
|
+
execFileSync(command2, args2, { timeout: 1e4, stdio: "pipe" });
|
|
143
88
|
return true;
|
|
144
89
|
} catch {
|
|
145
90
|
return false;
|
|
@@ -169,7 +114,7 @@ function createClaudeCodeRunner() {
|
|
|
169
114
|
return runSubprocess("claude", args2, prompt, timeout, options);
|
|
170
115
|
},
|
|
171
116
|
async healthCheck() {
|
|
172
|
-
return
|
|
117
|
+
return checkCommand("claude", ["--version"]);
|
|
173
118
|
}
|
|
174
119
|
};
|
|
175
120
|
}
|
|
@@ -326,77 +271,9 @@ var init_logger = __esm({
|
|
|
326
271
|
}
|
|
327
272
|
});
|
|
328
273
|
|
|
329
|
-
// src/validators.ts
|
|
330
|
-
function stripFences(content) {
|
|
331
|
-
return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
332
|
-
}
|
|
333
|
-
function parseJsonSafe(raw, requiredFields) {
|
|
334
|
-
let parsed;
|
|
335
|
-
try {
|
|
336
|
-
parsed = JSON.parse(raw);
|
|
337
|
-
} catch (err) {
|
|
338
|
-
return { ok: false, error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
339
|
-
}
|
|
340
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
341
|
-
return { ok: false, error: `Expected JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}` };
|
|
342
|
-
}
|
|
343
|
-
if (requiredFields) {
|
|
344
|
-
for (const field of requiredFields) {
|
|
345
|
-
if (!(field in parsed)) {
|
|
346
|
-
return { ok: false, error: `Missing required field: ${field}` };
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return { ok: true, data: parsed };
|
|
351
|
-
}
|
|
352
|
-
function validateTaskJson(content) {
|
|
353
|
-
try {
|
|
354
|
-
const parsed = JSON.parse(stripFences(content));
|
|
355
|
-
for (const field of REQUIRED_TASK_FIELDS) {
|
|
356
|
-
if (!(field in parsed)) {
|
|
357
|
-
return { valid: false, error: `Missing field: ${field}` };
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
return { valid: true };
|
|
361
|
-
} catch (err) {
|
|
362
|
-
return {
|
|
363
|
-
valid: false,
|
|
364
|
-
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
function validatePlanMd(content) {
|
|
369
|
-
if (content.length < 10) {
|
|
370
|
-
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
371
|
-
}
|
|
372
|
-
if (!/^##\s+\w+/m.test(content)) {
|
|
373
|
-
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
374
|
-
}
|
|
375
|
-
return { valid: true };
|
|
376
|
-
}
|
|
377
|
-
function validateReviewMd(content) {
|
|
378
|
-
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
379
|
-
return { valid: true };
|
|
380
|
-
}
|
|
381
|
-
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
382
|
-
}
|
|
383
|
-
var REQUIRED_TASK_FIELDS;
|
|
384
|
-
var init_validators = __esm({
|
|
385
|
-
"src/validators.ts"() {
|
|
386
|
-
"use strict";
|
|
387
|
-
REQUIRED_TASK_FIELDS = [
|
|
388
|
-
"task_type",
|
|
389
|
-
"title",
|
|
390
|
-
"description",
|
|
391
|
-
"scope",
|
|
392
|
-
"risk_level"
|
|
393
|
-
];
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
|
|
397
274
|
// src/config.ts
|
|
398
|
-
import * as
|
|
399
|
-
import * as
|
|
275
|
+
import * as fs from "fs";
|
|
276
|
+
import * as path from "path";
|
|
400
277
|
function needsLitellmProxy(config) {
|
|
401
278
|
return !!(config.agent.provider && config.agent.provider !== "anthropic");
|
|
402
279
|
}
|
|
@@ -413,16 +290,10 @@ function setConfigDir(dir) {
|
|
|
413
290
|
}
|
|
414
291
|
function getProjectConfig() {
|
|
415
292
|
if (_config) return _config;
|
|
416
|
-
const configPath =
|
|
417
|
-
if (
|
|
293
|
+
const configPath = path.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
294
|
+
if (fs.existsSync(configPath)) {
|
|
418
295
|
try {
|
|
419
|
-
const
|
|
420
|
-
if (!result.ok) {
|
|
421
|
-
logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
|
|
422
|
-
_config = { ...DEFAULT_CONFIG };
|
|
423
|
-
return _config;
|
|
424
|
-
}
|
|
425
|
-
const raw = result.data;
|
|
296
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
426
297
|
_config = {
|
|
427
298
|
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
428
299
|
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
@@ -434,13 +305,7 @@ function getProjectConfig() {
|
|
|
434
305
|
},
|
|
435
306
|
timeouts: raw.timeouts ?? void 0,
|
|
436
307
|
contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
|
|
437
|
-
mcp: raw.mcp ? {
|
|
438
|
-
servers: {},
|
|
439
|
-
stages: ["build", "verify", "review", "review-fix"],
|
|
440
|
-
...raw.mcp,
|
|
441
|
-
// Auto-enable when devServer is configured (user can still set enabled: false to override)
|
|
442
|
-
enabled: raw.mcp.enabled ?? !!raw.mcp.devServer
|
|
443
|
-
} : void 0
|
|
308
|
+
mcp: raw.mcp ? { enabled: false, servers: {}, stages: ["build", "verify", "review", "review-fix"], ...raw.mcp } : void 0
|
|
444
309
|
};
|
|
445
310
|
} catch {
|
|
446
311
|
logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
|
|
@@ -456,7 +321,6 @@ var init_config = __esm({
|
|
|
456
321
|
"src/config.ts"() {
|
|
457
322
|
"use strict";
|
|
458
323
|
init_logger();
|
|
459
|
-
init_validators();
|
|
460
324
|
DEFAULT_CONFIG = {
|
|
461
325
|
quality: {
|
|
462
326
|
typecheck: "pnpm -s tsc --noEmit",
|
|
@@ -490,7 +354,7 @@ var init_config = __esm({
|
|
|
490
354
|
});
|
|
491
355
|
|
|
492
356
|
// src/git-utils.ts
|
|
493
|
-
import { execFileSync as
|
|
357
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
494
358
|
function getHookSafeEnv() {
|
|
495
359
|
if (!_hookSafeEnv) {
|
|
496
360
|
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
@@ -498,7 +362,7 @@ function getHookSafeEnv() {
|
|
|
498
362
|
return _hookSafeEnv;
|
|
499
363
|
}
|
|
500
364
|
function git(args2, options) {
|
|
501
|
-
return
|
|
365
|
+
return execFileSync2("git", args2, {
|
|
502
366
|
encoding: "utf-8",
|
|
503
367
|
timeout: options?.timeout ?? 3e4,
|
|
504
368
|
cwd: options?.cwd,
|
|
@@ -553,9 +417,8 @@ function ensureFeatureBranch(issueNumber, title, cwd) {
|
|
|
553
417
|
}
|
|
554
418
|
try {
|
|
555
419
|
git(["fetch", "origin"], { cwd, timeout: 3e4 });
|
|
556
|
-
} catch
|
|
557
|
-
|
|
558
|
-
logger.warn(` Failed to fetch origin: ${msg}`);
|
|
420
|
+
} catch {
|
|
421
|
+
logger.warn(" Failed to fetch origin");
|
|
559
422
|
}
|
|
560
423
|
try {
|
|
561
424
|
git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
|
|
@@ -587,9 +450,8 @@ function syncWithDefault(cwd, branch) {
|
|
|
587
450
|
if (current === defaultBranch) return;
|
|
588
451
|
try {
|
|
589
452
|
git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
|
|
590
|
-
} catch
|
|
591
|
-
|
|
592
|
-
logger.warn(` Failed to fetch latest from origin: ${msg}`);
|
|
453
|
+
} catch {
|
|
454
|
+
logger.warn(" Failed to fetch latest from origin");
|
|
593
455
|
return;
|
|
594
456
|
}
|
|
595
457
|
try {
|
|
@@ -598,8 +460,7 @@ function syncWithDefault(cwd, branch) {
|
|
|
598
460
|
} catch {
|
|
599
461
|
try {
|
|
600
462
|
git(["merge", "--abort"], { cwd });
|
|
601
|
-
} catch
|
|
602
|
-
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
463
|
+
} catch {
|
|
603
464
|
}
|
|
604
465
|
logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
|
|
605
466
|
}
|
|
@@ -610,9 +471,8 @@ function mergeDefault(cwd) {
|
|
|
610
471
|
if (current === defaultBranch) return "clean";
|
|
611
472
|
try {
|
|
612
473
|
git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
|
|
613
|
-
} catch
|
|
614
|
-
|
|
615
|
-
logger.warn(` Failed to fetch latest from origin: ${msg}`);
|
|
474
|
+
} catch {
|
|
475
|
+
logger.warn(" Failed to fetch latest from origin");
|
|
616
476
|
return "error";
|
|
617
477
|
}
|
|
618
478
|
try {
|
|
@@ -627,8 +487,7 @@ function mergeDefault(cwd) {
|
|
|
627
487
|
}
|
|
628
488
|
try {
|
|
629
489
|
git(["merge", "--abort"], { cwd });
|
|
630
|
-
} catch
|
|
631
|
-
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
490
|
+
} catch {
|
|
632
491
|
}
|
|
633
492
|
return "error";
|
|
634
493
|
}
|
|
@@ -673,21 +532,7 @@ var init_git_utils = __esm({
|
|
|
673
532
|
});
|
|
674
533
|
|
|
675
534
|
// src/github-api.ts
|
|
676
|
-
import { execFileSync as
|
|
677
|
-
function isGhExecError(err) {
|
|
678
|
-
return typeof err === "object" && err !== null;
|
|
679
|
-
}
|
|
680
|
-
function ghErrorMessage(err) {
|
|
681
|
-
if (isGhExecError(err)) {
|
|
682
|
-
const stderr = err.stderr?.toString().trim();
|
|
683
|
-
if (stderr) return stderr;
|
|
684
|
-
}
|
|
685
|
-
return err instanceof Error ? err.message : String(err);
|
|
686
|
-
}
|
|
687
|
-
function isNotFoundError(err) {
|
|
688
|
-
const msg = ghErrorMessage(err).toLowerCase();
|
|
689
|
-
return msg.includes("not found") || msg.includes("no pull requests") || msg.includes("could not resolve");
|
|
690
|
-
}
|
|
535
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
691
536
|
function setGhCwd(cwd) {
|
|
692
537
|
_ghCwd = cwd;
|
|
693
538
|
}
|
|
@@ -697,7 +542,7 @@ function ghToken() {
|
|
|
697
542
|
function gh(args2, options) {
|
|
698
543
|
const token = ghToken();
|
|
699
544
|
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
700
|
-
return
|
|
545
|
+
return execFileSync3("gh", args2, {
|
|
701
546
|
encoding: "utf-8",
|
|
702
547
|
timeout: API_TIMEOUT_MS,
|
|
703
548
|
cwd: _ghCwd,
|
|
@@ -715,18 +560,9 @@ function getIssue(issueNumber) {
|
|
|
715
560
|
"--json",
|
|
716
561
|
"body,title"
|
|
717
562
|
]);
|
|
718
|
-
|
|
719
|
-
if (!parsed || typeof parsed.title !== "string") {
|
|
720
|
-
logger.warn(` Issue #${issueNumber}: unexpected response shape`);
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
return { body: parsed.body ?? "", title: parsed.title };
|
|
563
|
+
return JSON.parse(output);
|
|
724
564
|
} catch (err) {
|
|
725
|
-
|
|
726
|
-
logger.info(` Issue #${issueNumber} not found`);
|
|
727
|
-
} else {
|
|
728
|
-
logger.error(` Failed to get issue #${issueNumber}: ${ghErrorMessage(err)}`);
|
|
729
|
-
}
|
|
565
|
+
logger.error(` Failed to get issue #${issueNumber}: ${err}`);
|
|
730
566
|
return null;
|
|
731
567
|
}
|
|
732
568
|
}
|
|
@@ -767,15 +603,8 @@ function getPRForBranch(branch) {
|
|
|
767
603
|
"number,url"
|
|
768
604
|
]);
|
|
769
605
|
const data = JSON.parse(output);
|
|
770
|
-
if (typeof data.number !== "number" || typeof data.url !== "string") {
|
|
771
|
-
logger.warn(` PR for branch ${branch}: unexpected response shape`);
|
|
772
|
-
return null;
|
|
773
|
-
}
|
|
774
606
|
return { number: data.number, url: data.url };
|
|
775
|
-
} catch
|
|
776
|
-
if (!isNotFoundError(err)) {
|
|
777
|
-
logger.warn(` Failed to check PR for branch ${branch}: ${ghErrorMessage(err)}`);
|
|
778
|
-
}
|
|
607
|
+
} catch {
|
|
779
608
|
return null;
|
|
780
609
|
}
|
|
781
610
|
}
|
|
@@ -813,7 +642,8 @@ function createPR(head, base, title, body) {
|
|
|
813
642
|
logger.info(` PR created: ${url}`);
|
|
814
643
|
return { number, url };
|
|
815
644
|
} catch (err) {
|
|
816
|
-
const
|
|
645
|
+
const stderr = err?.stderr?.toString().trim();
|
|
646
|
+
const reason = stderr || (err instanceof Error ? err.message : String(err));
|
|
817
647
|
logger.error(` Failed to create PR: ${reason}`);
|
|
818
648
|
return null;
|
|
819
649
|
}
|
|
@@ -884,22 +714,9 @@ function getPRDetails(prNumber) {
|
|
|
884
714
|
"title,body,headRefName,baseRefName"
|
|
885
715
|
]);
|
|
886
716
|
const data = JSON.parse(output);
|
|
887
|
-
|
|
888
|
-
logger.warn(` PR #${prNumber}: unexpected response shape`);
|
|
889
|
-
return null;
|
|
890
|
-
}
|
|
891
|
-
return {
|
|
892
|
-
title: data.title,
|
|
893
|
-
body: data.body ?? "",
|
|
894
|
-
headBranch: data.headRefName,
|
|
895
|
-
baseBranch: data.baseRefName ?? "main"
|
|
896
|
-
};
|
|
717
|
+
return { title: data.title, body: data.body, headBranch: data.headRefName, baseBranch: data.baseRefName };
|
|
897
718
|
} catch (err) {
|
|
898
|
-
|
|
899
|
-
logger.info(` PR #${prNumber} not found`);
|
|
900
|
-
} else {
|
|
901
|
-
logger.error(` Failed to get PR #${prNumber}: ${ghErrorMessage(err)}`);
|
|
902
|
-
}
|
|
719
|
+
logger.error(` Failed to get PR #${prNumber}: ${err}`);
|
|
903
720
|
return null;
|
|
904
721
|
}
|
|
905
722
|
}
|
|
@@ -941,7 +758,7 @@ function getCIFailureLogs(runId, maxLength = 8e3) {
|
|
|
941
758
|
const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
|
|
942
759
|
return `${prefix}${truncated}`;
|
|
943
760
|
} catch (err) {
|
|
944
|
-
logger.warn(` Failed to get CI failure logs for run ${runId}: ${
|
|
761
|
+
logger.warn(` Failed to get CI failure logs for run ${runId}: ${err}`);
|
|
945
762
|
return null;
|
|
946
763
|
}
|
|
947
764
|
}
|
|
@@ -963,7 +780,7 @@ function getLatestFailedRunForBranch(branch) {
|
|
|
963
780
|
]);
|
|
964
781
|
return output.trim() || null;
|
|
965
782
|
} catch (err) {
|
|
966
|
-
logger.warn(` Failed to get latest failed run for branch ${branch}: ${
|
|
783
|
+
logger.warn(` Failed to get latest failed run for branch ${branch}: ${err}`);
|
|
967
784
|
return null;
|
|
968
785
|
}
|
|
969
786
|
}
|
|
@@ -1044,7 +861,7 @@ var init_github_api = __esm({
|
|
|
1044
861
|
"use strict";
|
|
1045
862
|
init_logger();
|
|
1046
863
|
API_TIMEOUT_MS = 3e4;
|
|
1047
|
-
LIFECYCLE_LABELS = ["planning", "building", "review", "
|
|
864
|
+
LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
|
|
1048
865
|
KODY_MARKERS = [
|
|
1049
866
|
"Kody Review",
|
|
1050
867
|
"\u{1F916} Generated by Kody",
|
|
@@ -1059,22 +876,15 @@ var init_github_api = __esm({
|
|
|
1059
876
|
});
|
|
1060
877
|
|
|
1061
878
|
// src/pipeline/state.ts
|
|
1062
|
-
import * as
|
|
1063
|
-
import * as
|
|
879
|
+
import * as fs2 from "fs";
|
|
880
|
+
import * as path2 from "path";
|
|
1064
881
|
function loadState(taskId, taskDir) {
|
|
1065
|
-
const p =
|
|
1066
|
-
if (!
|
|
882
|
+
const p = path2.join(taskDir, "status.json");
|
|
883
|
+
if (!fs2.existsSync(p)) return null;
|
|
1067
884
|
try {
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
);
|
|
1072
|
-
if (!result.ok) {
|
|
1073
|
-
logger.warn(` Corrupt status.json: ${result.error}`);
|
|
1074
|
-
return null;
|
|
1075
|
-
}
|
|
1076
|
-
if (result.data.taskId !== taskId) return null;
|
|
1077
|
-
return result.data;
|
|
885
|
+
const raw = JSON.parse(fs2.readFileSync(p, "utf-8"));
|
|
886
|
+
if (raw.taskId === taskId) return raw;
|
|
887
|
+
return null;
|
|
1078
888
|
} catch {
|
|
1079
889
|
return null;
|
|
1080
890
|
}
|
|
@@ -1084,11 +894,11 @@ function writeState(state, taskDir) {
|
|
|
1084
894
|
...state,
|
|
1085
895
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1086
896
|
};
|
|
1087
|
-
const target =
|
|
897
|
+
const target = path2.join(taskDir, "status.json");
|
|
1088
898
|
const tmp = target + ".tmp";
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
899
|
+
fs2.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
900
|
+
fs2.renameSync(tmp, target);
|
|
901
|
+
state.updatedAt = updated.updatedAt;
|
|
1092
902
|
}
|
|
1093
903
|
function initState(taskId) {
|
|
1094
904
|
const stages = {};
|
|
@@ -1102,8 +912,6 @@ var init_state = __esm({
|
|
|
1102
912
|
"src/pipeline/state.ts"() {
|
|
1103
913
|
"use strict";
|
|
1104
914
|
init_definitions();
|
|
1105
|
-
init_validators();
|
|
1106
|
-
init_logger();
|
|
1107
915
|
}
|
|
1108
916
|
});
|
|
1109
917
|
|
|
@@ -1128,16 +936,16 @@ var init_complexity = __esm({
|
|
|
1128
936
|
});
|
|
1129
937
|
|
|
1130
938
|
// src/memory.ts
|
|
1131
|
-
import * as
|
|
1132
|
-
import * as
|
|
939
|
+
import * as fs3 from "fs";
|
|
940
|
+
import * as path3 from "path";
|
|
1133
941
|
function readProjectMemory(projectDir) {
|
|
1134
|
-
const memoryDir =
|
|
1135
|
-
if (!
|
|
1136
|
-
const files =
|
|
942
|
+
const memoryDir = path3.join(projectDir, ".kody", "memory");
|
|
943
|
+
if (!fs3.existsSync(memoryDir)) return "";
|
|
944
|
+
const files = fs3.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
1137
945
|
if (files.length === 0) return "";
|
|
1138
946
|
const sections = [];
|
|
1139
947
|
for (const file of files) {
|
|
1140
|
-
const content =
|
|
948
|
+
const content = fs3.readFileSync(path3.join(memoryDir, file), "utf-8").trim();
|
|
1141
949
|
if (content) {
|
|
1142
950
|
sections.push(`## ${file.replace(".md", "")}
|
|
1143
951
|
${content}`);
|
|
@@ -1156,11 +964,15 @@ var init_memory = __esm({
|
|
|
1156
964
|
});
|
|
1157
965
|
|
|
1158
966
|
// src/context-tiers.ts
|
|
1159
|
-
import * as
|
|
1160
|
-
import * as
|
|
967
|
+
import * as fs4 from "fs";
|
|
968
|
+
import * as path4 from "path";
|
|
969
|
+
import * as crypto2 from "crypto";
|
|
1161
970
|
function estimateTokens(text) {
|
|
1162
971
|
return Math.ceil(text.length / 4);
|
|
1163
972
|
}
|
|
973
|
+
function contentHash(content) {
|
|
974
|
+
return crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
975
|
+
}
|
|
1164
976
|
function resolveStagePolicy(stageName, stageOverrides) {
|
|
1165
977
|
const defaults = DEFAULT_STAGE_POLICIES[stageName] ?? DEFAULT_STAGE_POLICIES.build;
|
|
1166
978
|
const overrides = stageOverrides?.[stageName];
|
|
@@ -1247,30 +1059,61 @@ function generateL1Json(content) {
|
|
|
1247
1059
|
return content.slice(0, L1_MAX_CHARS);
|
|
1248
1060
|
}
|
|
1249
1061
|
}
|
|
1250
|
-
function
|
|
1251
|
-
const
|
|
1252
|
-
return {
|
|
1062
|
+
function readCache(cacheDir) {
|
|
1063
|
+
const cachePath = path4.join(cacheDir, "tier-cache.json");
|
|
1064
|
+
if (!fs4.existsSync(cachePath)) return { version: 1, entries: {} };
|
|
1065
|
+
try {
|
|
1066
|
+
return JSON.parse(fs4.readFileSync(cachePath, "utf-8"));
|
|
1067
|
+
} catch {
|
|
1068
|
+
return { version: 1, entries: {} };
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function writeCache(cacheDir, cache) {
|
|
1072
|
+
fs4.mkdirSync(cacheDir, { recursive: true });
|
|
1073
|
+
fs4.writeFileSync(path4.join(cacheDir, "tier-cache.json"), JSON.stringify(cache, null, 2));
|
|
1074
|
+
}
|
|
1075
|
+
function getTieredContent(filePath, content, cacheDir) {
|
|
1076
|
+
const hash = contentHash(content);
|
|
1077
|
+
const key = path4.basename(filePath);
|
|
1078
|
+
const cache = readCache(cacheDir);
|
|
1079
|
+
if (cache.entries[key] && cache.entries[key].hash === hash) {
|
|
1080
|
+
return cache.entries[key];
|
|
1081
|
+
}
|
|
1082
|
+
const tiered = {
|
|
1253
1083
|
source: filePath,
|
|
1084
|
+
hash,
|
|
1254
1085
|
L0: generateL0(content, key),
|
|
1255
1086
|
L1: generateL1(content, key),
|
|
1256
1087
|
L2: content
|
|
1257
1088
|
};
|
|
1089
|
+
cache.entries[key] = tiered;
|
|
1090
|
+
writeCache(cacheDir, cache);
|
|
1091
|
+
return tiered;
|
|
1092
|
+
}
|
|
1093
|
+
function invalidateCache(filePath, cacheDir) {
|
|
1094
|
+
const key = path4.basename(filePath);
|
|
1095
|
+
const cache = readCache(cacheDir);
|
|
1096
|
+
if (cache.entries[key]) {
|
|
1097
|
+
delete cache.entries[key];
|
|
1098
|
+
writeCache(cacheDir, cache);
|
|
1099
|
+
}
|
|
1258
1100
|
}
|
|
1259
1101
|
function selectTier(tiered, tier) {
|
|
1260
1102
|
return tiered[tier];
|
|
1261
1103
|
}
|
|
1262
1104
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
1263
|
-
const memoryDir =
|
|
1264
|
-
if (!
|
|
1265
|
-
const files =
|
|
1105
|
+
const memoryDir = path4.join(projectDir, ".kody", "memory");
|
|
1106
|
+
if (!fs4.existsSync(memoryDir)) return "";
|
|
1107
|
+
const files = fs4.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
1266
1108
|
if (files.length === 0) return "";
|
|
1109
|
+
const cacheDir = path4.join(memoryDir, ".tiers");
|
|
1267
1110
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
1268
1111
|
const sections = [];
|
|
1269
1112
|
for (const file of files) {
|
|
1270
|
-
const filePath =
|
|
1271
|
-
const content =
|
|
1113
|
+
const filePath = path4.join(memoryDir, file);
|
|
1114
|
+
const content = fs4.readFileSync(filePath, "utf-8").trim();
|
|
1272
1115
|
if (!content) continue;
|
|
1273
|
-
const tiered = getTieredContent(filePath, content);
|
|
1116
|
+
const tiered = getTieredContent(filePath, content, cacheDir);
|
|
1274
1117
|
const selected = selectTier(tiered, tier);
|
|
1275
1118
|
if (selected) {
|
|
1276
1119
|
sections.push(`## ${file.replace(".md", "")}
|
|
@@ -1285,25 +1128,26 @@ ${sections.join("\n\n")}
|
|
|
1285
1128
|
`;
|
|
1286
1129
|
}
|
|
1287
1130
|
function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
1131
|
+
const cacheDir = path4.join(taskDir, ".tiers");
|
|
1288
1132
|
let context = `## Task Context
|
|
1289
1133
|
`;
|
|
1290
1134
|
context += `Task ID: ${taskId}
|
|
1291
1135
|
`;
|
|
1292
1136
|
context += `Task Directory: ${taskDir}
|
|
1293
1137
|
`;
|
|
1294
|
-
const taskMdPath =
|
|
1295
|
-
if (
|
|
1296
|
-
const content =
|
|
1297
|
-
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
1138
|
+
const taskMdPath = path4.join(taskDir, "task.md");
|
|
1139
|
+
if (fs4.existsSync(taskMdPath)) {
|
|
1140
|
+
const content = fs4.readFileSync(taskMdPath, "utf-8");
|
|
1141
|
+
const selected = selectContent(taskMdPath, content, cacheDir, policy.taskDescription);
|
|
1298
1142
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
1299
1143
|
context += `
|
|
1300
1144
|
## ${label}
|
|
1301
1145
|
${selected}
|
|
1302
1146
|
`;
|
|
1303
1147
|
}
|
|
1304
|
-
const taskJsonPath =
|
|
1305
|
-
if (
|
|
1306
|
-
const content =
|
|
1148
|
+
const taskJsonPath = path4.join(taskDir, "task.json");
|
|
1149
|
+
if (fs4.existsSync(taskJsonPath)) {
|
|
1150
|
+
const content = fs4.readFileSync(taskJsonPath, "utf-8");
|
|
1307
1151
|
if (policy.taskClassification === "L2") {
|
|
1308
1152
|
try {
|
|
1309
1153
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -1319,7 +1163,7 @@ ${selected}
|
|
|
1319
1163
|
} catch {
|
|
1320
1164
|
}
|
|
1321
1165
|
} else {
|
|
1322
|
-
const selected = selectContent(taskJsonPath, content, policy.taskClassification);
|
|
1166
|
+
const selected = selectContent(taskJsonPath, content, cacheDir, policy.taskClassification);
|
|
1323
1167
|
if (selected) {
|
|
1324
1168
|
const label = tierLabel("Task Classification", policy.taskClassification);
|
|
1325
1169
|
context += `
|
|
@@ -1329,30 +1173,30 @@ ${selected}
|
|
|
1329
1173
|
}
|
|
1330
1174
|
}
|
|
1331
1175
|
}
|
|
1332
|
-
const specPath =
|
|
1333
|
-
if (
|
|
1334
|
-
const content =
|
|
1335
|
-
const selected = selectContent(specPath, content, policy.spec);
|
|
1176
|
+
const specPath = path4.join(taskDir, "spec.md");
|
|
1177
|
+
if (fs4.existsSync(specPath)) {
|
|
1178
|
+
const content = fs4.readFileSync(specPath, "utf-8");
|
|
1179
|
+
const selected = selectContent(specPath, content, cacheDir, policy.spec);
|
|
1336
1180
|
const label = tierLabel("Spec", policy.spec);
|
|
1337
1181
|
context += `
|
|
1338
1182
|
## ${label}
|
|
1339
1183
|
${selected}
|
|
1340
1184
|
`;
|
|
1341
1185
|
}
|
|
1342
|
-
const planPath =
|
|
1343
|
-
if (
|
|
1344
|
-
const content =
|
|
1345
|
-
const selected = selectContent(planPath, content, policy.plan);
|
|
1186
|
+
const planPath = path4.join(taskDir, "plan.md");
|
|
1187
|
+
if (fs4.existsSync(planPath)) {
|
|
1188
|
+
const content = fs4.readFileSync(planPath, "utf-8");
|
|
1189
|
+
const selected = selectContent(planPath, content, cacheDir, policy.plan);
|
|
1346
1190
|
const label = tierLabel("Plan", policy.plan);
|
|
1347
1191
|
context += `
|
|
1348
1192
|
## ${label}
|
|
1349
1193
|
${selected}
|
|
1350
1194
|
`;
|
|
1351
1195
|
}
|
|
1352
|
-
const contextMdPath =
|
|
1353
|
-
if (
|
|
1354
|
-
const content =
|
|
1355
|
-
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
1196
|
+
const contextMdPath = path4.join(taskDir, "context.md");
|
|
1197
|
+
if (fs4.existsSync(contextMdPath)) {
|
|
1198
|
+
const content = fs4.readFileSync(contextMdPath, "utf-8");
|
|
1199
|
+
const selected = selectContent(contextMdPath, content, cacheDir, policy.accumulatedContext);
|
|
1356
1200
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
1357
1201
|
context += `
|
|
1358
1202
|
## ${label}
|
|
@@ -1367,9 +1211,9 @@ ${feedback}
|
|
|
1367
1211
|
}
|
|
1368
1212
|
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
1369
1213
|
}
|
|
1370
|
-
function selectContent(filePath, content, tier) {
|
|
1214
|
+
function selectContent(filePath, content, cacheDir, tier) {
|
|
1371
1215
|
if (tier === "L2") return content;
|
|
1372
|
-
const tiered = getTieredContent(filePath, content);
|
|
1216
|
+
const tiered = getTieredContent(filePath, content, cacheDir);
|
|
1373
1217
|
return selectTier(tiered, tier);
|
|
1374
1218
|
}
|
|
1375
1219
|
function tierLabel(sectionName, tier) {
|
|
@@ -1437,20 +1281,6 @@ var init_context_tiers = __esm({
|
|
|
1437
1281
|
});
|
|
1438
1282
|
|
|
1439
1283
|
// src/mcp-config.ts
|
|
1440
|
-
function withPlaywrightIfNeeded(mcpConfig, hasUI) {
|
|
1441
|
-
if (!mcpConfig?.enabled || !hasUI) return mcpConfig;
|
|
1442
|
-
const hasPlaywright = Object.keys(mcpConfig.servers).some(
|
|
1443
|
-
(name) => name.toLowerCase().includes("playwright")
|
|
1444
|
-
);
|
|
1445
|
-
if (hasPlaywright) return mcpConfig;
|
|
1446
|
-
return {
|
|
1447
|
-
...mcpConfig,
|
|
1448
|
-
servers: {
|
|
1449
|
-
...mcpConfig.servers,
|
|
1450
|
-
playwright: PLAYWRIGHT_SERVER
|
|
1451
|
-
}
|
|
1452
|
-
};
|
|
1453
|
-
}
|
|
1454
1284
|
function buildMcpConfigJson(mcpConfig) {
|
|
1455
1285
|
if (!mcpConfig?.enabled) return void 0;
|
|
1456
1286
|
if (Object.keys(mcpConfig.servers).length === 0) return void 0;
|
|
@@ -1467,40 +1297,37 @@ function buildMcpConfigJson(mcpConfig) {
|
|
|
1467
1297
|
}
|
|
1468
1298
|
function isMcpEnabledForStage(stageName, mcpConfig) {
|
|
1469
1299
|
if (!mcpConfig?.enabled) return false;
|
|
1300
|
+
if (Object.keys(mcpConfig.servers).length === 0) return false;
|
|
1470
1301
|
const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
|
|
1471
1302
|
return allowedStages.includes(stageName);
|
|
1472
1303
|
}
|
|
1473
|
-
var DEFAULT_MCP_STAGES
|
|
1304
|
+
var DEFAULT_MCP_STAGES;
|
|
1474
1305
|
var init_mcp_config = __esm({
|
|
1475
1306
|
"src/mcp-config.ts"() {
|
|
1476
1307
|
"use strict";
|
|
1477
1308
|
DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
|
|
1478
|
-
PLAYWRIGHT_SERVER = {
|
|
1479
|
-
command: "npx",
|
|
1480
|
-
args: ["-y", "@anthropic-ai/mcp-playwright"]
|
|
1481
|
-
};
|
|
1482
1309
|
}
|
|
1483
1310
|
});
|
|
1484
1311
|
|
|
1485
1312
|
// src/context.ts
|
|
1486
|
-
import * as
|
|
1487
|
-
import * as
|
|
1313
|
+
import * as fs5 from "fs";
|
|
1314
|
+
import * as path5 from "path";
|
|
1488
1315
|
function readPromptFile(stageName, projectDir) {
|
|
1489
1316
|
if (projectDir) {
|
|
1490
|
-
const stepFile =
|
|
1491
|
-
if (
|
|
1492
|
-
return
|
|
1317
|
+
const stepFile = path5.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
1318
|
+
if (fs5.existsSync(stepFile)) {
|
|
1319
|
+
return fs5.readFileSync(stepFile, "utf-8");
|
|
1493
1320
|
}
|
|
1494
1321
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
1495
1322
|
}
|
|
1496
1323
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
1497
1324
|
const candidates = [
|
|
1498
|
-
|
|
1499
|
-
|
|
1325
|
+
path5.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
1326
|
+
path5.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
1500
1327
|
];
|
|
1501
1328
|
for (const candidate of candidates) {
|
|
1502
|
-
if (
|
|
1503
|
-
return
|
|
1329
|
+
if (fs5.existsSync(candidate)) {
|
|
1330
|
+
return fs5.readFileSync(candidate, "utf-8");
|
|
1504
1331
|
}
|
|
1505
1332
|
}
|
|
1506
1333
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -1512,18 +1339,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
1512
1339
|
`;
|
|
1513
1340
|
context += `Task Directory: ${taskDir}
|
|
1514
1341
|
`;
|
|
1515
|
-
const taskMdPath =
|
|
1516
|
-
if (
|
|
1517
|
-
const taskMd =
|
|
1342
|
+
const taskMdPath = path5.join(taskDir, "task.md");
|
|
1343
|
+
if (fs5.existsSync(taskMdPath)) {
|
|
1344
|
+
const taskMd = fs5.readFileSync(taskMdPath, "utf-8");
|
|
1518
1345
|
context += `
|
|
1519
1346
|
## Task Description
|
|
1520
1347
|
${taskMd}
|
|
1521
1348
|
`;
|
|
1522
1349
|
}
|
|
1523
|
-
const taskJsonPath =
|
|
1524
|
-
if (
|
|
1350
|
+
const taskJsonPath = path5.join(taskDir, "task.json");
|
|
1351
|
+
if (fs5.existsSync(taskJsonPath)) {
|
|
1525
1352
|
try {
|
|
1526
|
-
const taskDef = JSON.parse(
|
|
1353
|
+
const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
|
|
1527
1354
|
context += `
|
|
1528
1355
|
## Task Classification
|
|
1529
1356
|
`;
|
|
@@ -1536,27 +1363,27 @@ ${taskMd}
|
|
|
1536
1363
|
} catch {
|
|
1537
1364
|
}
|
|
1538
1365
|
}
|
|
1539
|
-
const specPath =
|
|
1540
|
-
if (
|
|
1541
|
-
const spec =
|
|
1366
|
+
const specPath = path5.join(taskDir, "spec.md");
|
|
1367
|
+
if (fs5.existsSync(specPath)) {
|
|
1368
|
+
const spec = fs5.readFileSync(specPath, "utf-8");
|
|
1542
1369
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
1543
1370
|
context += `
|
|
1544
1371
|
## Spec Summary
|
|
1545
1372
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
1546
1373
|
`;
|
|
1547
1374
|
}
|
|
1548
|
-
const planPath =
|
|
1549
|
-
if (
|
|
1550
|
-
const plan =
|
|
1375
|
+
const planPath = path5.join(taskDir, "plan.md");
|
|
1376
|
+
if (fs5.existsSync(planPath)) {
|
|
1377
|
+
const plan = fs5.readFileSync(planPath, "utf-8");
|
|
1551
1378
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
1552
1379
|
context += `
|
|
1553
1380
|
## Plan Summary
|
|
1554
1381
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
1555
1382
|
`;
|
|
1556
1383
|
}
|
|
1557
|
-
const contextMdPath =
|
|
1558
|
-
if (
|
|
1559
|
-
const accumulated =
|
|
1384
|
+
const contextMdPath = path5.join(taskDir, "context.md");
|
|
1385
|
+
if (fs5.existsSync(contextMdPath)) {
|
|
1386
|
+
const accumulated = fs5.readFileSync(contextMdPath, "utf-8");
|
|
1560
1387
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
1561
1388
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
1562
1389
|
context += `
|
|
@@ -1573,10 +1400,10 @@ ${feedback}
|
|
|
1573
1400
|
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
1574
1401
|
}
|
|
1575
1402
|
function taskHasUI(taskDir) {
|
|
1576
|
-
const taskJsonPath =
|
|
1577
|
-
if (!
|
|
1403
|
+
const taskJsonPath = path5.join(taskDir, "task.json");
|
|
1404
|
+
if (!fs5.existsSync(taskJsonPath)) return true;
|
|
1578
1405
|
try {
|
|
1579
|
-
const taskDef = JSON.parse(
|
|
1406
|
+
const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
|
|
1580
1407
|
return taskDef.hasUI !== false;
|
|
1581
1408
|
} catch {
|
|
1582
1409
|
return true;
|
|
@@ -1696,11 +1523,6 @@ ${prompt}` : prompt;
|
|
|
1696
1523
|
}
|
|
1697
1524
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
1698
1525
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
1699
|
-
const qaGuidePath = path11.join(projectDir, ".kody", "qa-guide.md");
|
|
1700
|
-
if (fs12.existsSync(qaGuidePath)) {
|
|
1701
|
-
const qaGuide = fs12.readFileSync(qaGuidePath, "utf-8").trim();
|
|
1702
|
-
assembled = assembled + "\n\n" + qaGuide;
|
|
1703
|
-
}
|
|
1704
1526
|
}
|
|
1705
1527
|
return assembled;
|
|
1706
1528
|
}
|
|
@@ -1747,6 +1569,55 @@ var init_context = __esm({
|
|
|
1747
1569
|
}
|
|
1748
1570
|
});
|
|
1749
1571
|
|
|
1572
|
+
// src/validators.ts
|
|
1573
|
+
function stripFences(content) {
|
|
1574
|
+
return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1575
|
+
}
|
|
1576
|
+
function validateTaskJson(content) {
|
|
1577
|
+
try {
|
|
1578
|
+
const parsed = JSON.parse(stripFences(content));
|
|
1579
|
+
for (const field of REQUIRED_TASK_FIELDS) {
|
|
1580
|
+
if (!(field in parsed)) {
|
|
1581
|
+
return { valid: false, error: `Missing field: ${field}` };
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return { valid: true };
|
|
1585
|
+
} catch (err) {
|
|
1586
|
+
return {
|
|
1587
|
+
valid: false,
|
|
1588
|
+
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function validatePlanMd(content) {
|
|
1593
|
+
if (content.length < 10) {
|
|
1594
|
+
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
1595
|
+
}
|
|
1596
|
+
if (!/^##\s+\w+/m.test(content)) {
|
|
1597
|
+
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
1598
|
+
}
|
|
1599
|
+
return { valid: true };
|
|
1600
|
+
}
|
|
1601
|
+
function validateReviewMd(content) {
|
|
1602
|
+
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
1603
|
+
return { valid: true };
|
|
1604
|
+
}
|
|
1605
|
+
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
1606
|
+
}
|
|
1607
|
+
var REQUIRED_TASK_FIELDS;
|
|
1608
|
+
var init_validators = __esm({
|
|
1609
|
+
"src/validators.ts"() {
|
|
1610
|
+
"use strict";
|
|
1611
|
+
REQUIRED_TASK_FIELDS = [
|
|
1612
|
+
"task_type",
|
|
1613
|
+
"title",
|
|
1614
|
+
"description",
|
|
1615
|
+
"scope",
|
|
1616
|
+
"risk_level"
|
|
1617
|
+
];
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1750
1621
|
// src/pipeline/runner-selection.ts
|
|
1751
1622
|
function getRunnerForStage(ctx, stageName) {
|
|
1752
1623
|
const config = getProjectConfig();
|
|
@@ -1767,8 +1638,8 @@ var init_runner_selection = __esm({
|
|
|
1767
1638
|
});
|
|
1768
1639
|
|
|
1769
1640
|
// src/stages/agent.ts
|
|
1770
|
-
import * as
|
|
1771
|
-
import * as
|
|
1641
|
+
import * as fs6 from "fs";
|
|
1642
|
+
import * as path6 from "path";
|
|
1772
1643
|
function getSessionInfo(stageName, sessions) {
|
|
1773
1644
|
const group = SESSION_GROUP[stageName];
|
|
1774
1645
|
if (!group) return void 0;
|
|
@@ -1814,8 +1685,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
1814
1685
|
if (sessionInfo) {
|
|
1815
1686
|
logger.info(` session: ${SESSION_GROUP[def.name]} (${sessionInfo.resumeSession ? "resume" : "new"})`);
|
|
1816
1687
|
}
|
|
1817
|
-
const
|
|
1818
|
-
const mcpConfigJson = buildMcpConfigJson(mcpForStage);
|
|
1688
|
+
const mcpConfigJson = isMcpEnabledForStage(def.name, config.mcp) ? buildMcpConfigJson(config.mcp) : void 0;
|
|
1819
1689
|
if (mcpConfigJson) {
|
|
1820
1690
|
logger.info(` MCP servers enabled for ${def.name}`);
|
|
1821
1691
|
}
|
|
@@ -1830,27 +1700,27 @@ async function executeAgentStage(ctx, def) {
|
|
|
1830
1700
|
return { outcome: result.outcome, error: result.error, retries: 0 };
|
|
1831
1701
|
}
|
|
1832
1702
|
if (def.outputFile && result.output) {
|
|
1833
|
-
|
|
1703
|
+
fs6.writeFileSync(path6.join(ctx.taskDir, def.outputFile), result.output);
|
|
1834
1704
|
}
|
|
1835
1705
|
if (def.outputFile) {
|
|
1836
|
-
const outputPath =
|
|
1837
|
-
if (!
|
|
1838
|
-
const ext =
|
|
1839
|
-
const base =
|
|
1840
|
-
const files =
|
|
1706
|
+
const outputPath = path6.join(ctx.taskDir, def.outputFile);
|
|
1707
|
+
if (!fs6.existsSync(outputPath)) {
|
|
1708
|
+
const ext = path6.extname(def.outputFile);
|
|
1709
|
+
const base = path6.basename(def.outputFile, ext);
|
|
1710
|
+
const files = fs6.readdirSync(ctx.taskDir);
|
|
1841
1711
|
const variant = files.find(
|
|
1842
1712
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
1843
1713
|
);
|
|
1844
1714
|
if (variant) {
|
|
1845
|
-
|
|
1715
|
+
fs6.renameSync(path6.join(ctx.taskDir, variant), outputPath);
|
|
1846
1716
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
1847
1717
|
}
|
|
1848
1718
|
}
|
|
1849
1719
|
}
|
|
1850
1720
|
if (def.outputFile) {
|
|
1851
|
-
const outputPath =
|
|
1852
|
-
if (
|
|
1853
|
-
const content =
|
|
1721
|
+
const outputPath = path6.join(ctx.taskDir, def.outputFile);
|
|
1722
|
+
if (fs6.existsSync(outputPath)) {
|
|
1723
|
+
const content = fs6.readFileSync(outputPath, "utf-8");
|
|
1854
1724
|
const validation = validateStageOutput(def.name, content);
|
|
1855
1725
|
if (!validation.valid) {
|
|
1856
1726
|
if (def.name === "taskify") {
|
|
@@ -1864,7 +1734,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
1864
1734
|
const stripped = stripFences(retryResult.output);
|
|
1865
1735
|
const retryValidation = validateTaskJson(stripped);
|
|
1866
1736
|
if (retryValidation.valid) {
|
|
1867
|
-
|
|
1737
|
+
fs6.writeFileSync(outputPath, retryResult.output);
|
|
1868
1738
|
logger.info(` taskify retry produced valid JSON`);
|
|
1869
1739
|
} else {
|
|
1870
1740
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -1875,10 +1745,10 @@ async function executeAgentStage(ctx, def) {
|
|
|
1875
1745
|
description: plainText.slice(0, 500),
|
|
1876
1746
|
scope: [],
|
|
1877
1747
|
risk_level: "low",
|
|
1878
|
-
hasUI:
|
|
1748
|
+
hasUI: false,
|
|
1879
1749
|
questions: []
|
|
1880
1750
|
}, null, 2);
|
|
1881
|
-
|
|
1751
|
+
fs6.writeFileSync(outputPath, fallback);
|
|
1882
1752
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
1883
1753
|
}
|
|
1884
1754
|
}
|
|
@@ -1892,7 +1762,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
1892
1762
|
return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
|
|
1893
1763
|
}
|
|
1894
1764
|
function appendStageContext(taskDir, stageName, output) {
|
|
1895
|
-
const contextPath =
|
|
1765
|
+
const contextPath = path6.join(taskDir, "context.md");
|
|
1896
1766
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
1897
1767
|
let summary;
|
|
1898
1768
|
if (output && output.trim()) {
|
|
@@ -1905,7 +1775,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
1905
1775
|
### ${stageName} (${timestamp2})
|
|
1906
1776
|
${summary}
|
|
1907
1777
|
`;
|
|
1908
|
-
|
|
1778
|
+
fs6.appendFileSync(contextPath, entry);
|
|
1909
1779
|
}
|
|
1910
1780
|
var SESSION_GROUP;
|
|
1911
1781
|
var init_agent = __esm({
|
|
@@ -1928,7 +1798,7 @@ var init_agent = __esm({
|
|
|
1928
1798
|
});
|
|
1929
1799
|
|
|
1930
1800
|
// src/verify-runner.ts
|
|
1931
|
-
import { execFileSync as
|
|
1801
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
1932
1802
|
function isExecError(err) {
|
|
1933
1803
|
return typeof err === "object" && err !== null;
|
|
1934
1804
|
}
|
|
@@ -1964,7 +1834,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
1964
1834
|
return { success: true, output: "", timedOut: false };
|
|
1965
1835
|
}
|
|
1966
1836
|
try {
|
|
1967
|
-
const output =
|
|
1837
|
+
const output = execFileSync4(parts[0], parts.slice(1), {
|
|
1968
1838
|
cwd,
|
|
1969
1839
|
timeout,
|
|
1970
1840
|
encoding: "utf-8",
|
|
@@ -2035,7 +1905,7 @@ var init_verify_runner = __esm({
|
|
|
2035
1905
|
});
|
|
2036
1906
|
|
|
2037
1907
|
// src/observer.ts
|
|
2038
|
-
import { execFileSync as
|
|
1908
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
2039
1909
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
2040
1910
|
const context = [
|
|
2041
1911
|
`Stage: ${stageName}`,
|
|
@@ -2060,48 +1930,42 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
2060
1930
|
);
|
|
2061
1931
|
if (result.outcome === "completed" && result.output) {
|
|
2062
1932
|
const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
resolution: data.resolution ?? ""
|
|
2079
|
-
};
|
|
2080
|
-
}
|
|
2081
|
-
logger.warn(` Diagnosis returned invalid classification: ${data.classification}`);
|
|
2082
|
-
} else {
|
|
2083
|
-
logger.warn(` Diagnosis JSON invalid: ${parseResult.error}`);
|
|
1933
|
+
const parsed = JSON.parse(cleaned);
|
|
1934
|
+
const validClassifications = [
|
|
1935
|
+
"fixable",
|
|
1936
|
+
"infrastructure",
|
|
1937
|
+
"pre-existing",
|
|
1938
|
+
"retry",
|
|
1939
|
+
"abort"
|
|
1940
|
+
];
|
|
1941
|
+
if (validClassifications.includes(parsed.classification)) {
|
|
1942
|
+
logger.info(` Diagnosis: ${parsed.classification} \u2014 ${parsed.reason}`);
|
|
1943
|
+
return {
|
|
1944
|
+
classification: parsed.classification,
|
|
1945
|
+
reason: parsed.reason ?? "Unknown reason",
|
|
1946
|
+
resolution: parsed.resolution ?? ""
|
|
1947
|
+
};
|
|
2084
1948
|
}
|
|
2085
1949
|
}
|
|
2086
1950
|
} catch (err) {
|
|
2087
1951
|
logger.warn(` Diagnosis error: ${err instanceof Error ? err.message : err}`);
|
|
2088
1952
|
}
|
|
2089
|
-
logger.warn(" Diagnosis failed \u2014 defaulting to
|
|
1953
|
+
logger.warn(" Diagnosis failed \u2014 defaulting to fixable");
|
|
2090
1954
|
return {
|
|
2091
|
-
classification: "
|
|
2092
|
-
reason: "Could not diagnose failure
|
|
1955
|
+
classification: "fixable",
|
|
1956
|
+
reason: "Could not diagnose failure",
|
|
2093
1957
|
resolution: errorOutput.slice(-500)
|
|
2094
1958
|
};
|
|
2095
1959
|
}
|
|
2096
1960
|
function getModifiedFiles(projectDir) {
|
|
2097
1961
|
try {
|
|
2098
|
-
const staged =
|
|
1962
|
+
const staged = execFileSync5("git", ["diff", "--name-only", "--cached"], {
|
|
2099
1963
|
encoding: "utf-8",
|
|
2100
1964
|
cwd: projectDir,
|
|
2101
1965
|
timeout: 5e3,
|
|
2102
1966
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2103
1967
|
}).trim();
|
|
2104
|
-
const unstaged =
|
|
1968
|
+
const unstaged = execFileSync5("git", ["diff", "--name-only"], {
|
|
2105
1969
|
encoding: "utf-8",
|
|
2106
1970
|
cwd: projectDir,
|
|
2107
1971
|
timeout: 5e3,
|
|
@@ -2110,8 +1974,7 @@ function getModifiedFiles(projectDir) {
|
|
|
2110
1974
|
const all = `${staged}
|
|
2111
1975
|
${unstaged}`.split("\n").filter(Boolean);
|
|
2112
1976
|
return [...new Set(all)];
|
|
2113
|
-
} catch
|
|
2114
|
-
logger.warn(` Failed to get modified files: ${err instanceof Error ? err.message : String(err)}`);
|
|
1977
|
+
} catch {
|
|
2115
1978
|
return [];
|
|
2116
1979
|
}
|
|
2117
1980
|
}
|
|
@@ -2120,7 +1983,6 @@ var init_observer = __esm({
|
|
|
2120
1983
|
"src/observer.ts"() {
|
|
2121
1984
|
"use strict";
|
|
2122
1985
|
init_logger();
|
|
2123
|
-
init_validators();
|
|
2124
1986
|
DIAGNOSIS_PROMPT = `You are a pipeline failure diagnosis agent. Analyze the error and classify it.
|
|
2125
1987
|
|
|
2126
1988
|
Output ONLY valid JSON. No markdown fences. No explanation.
|
|
@@ -2144,8 +2006,8 @@ Error context:
|
|
|
2144
2006
|
});
|
|
2145
2007
|
|
|
2146
2008
|
// src/stages/gate.ts
|
|
2147
|
-
import * as
|
|
2148
|
-
import * as
|
|
2009
|
+
import * as fs7 from "fs";
|
|
2010
|
+
import * as path7 from "path";
|
|
2149
2011
|
function executeGateStage(ctx, def) {
|
|
2150
2012
|
if (ctx.input.dryRun) {
|
|
2151
2013
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -2188,7 +2050,7 @@ ${output}
|
|
|
2188
2050
|
`);
|
|
2189
2051
|
}
|
|
2190
2052
|
}
|
|
2191
|
-
|
|
2053
|
+
fs7.writeFileSync(path7.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
2192
2054
|
return {
|
|
2193
2055
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
2194
2056
|
retries: 0
|
|
@@ -2203,9 +2065,9 @@ var init_gate = __esm({
|
|
|
2203
2065
|
});
|
|
2204
2066
|
|
|
2205
2067
|
// src/stages/verify.ts
|
|
2206
|
-
import * as
|
|
2207
|
-
import * as
|
|
2208
|
-
import { execFileSync as
|
|
2068
|
+
import * as fs8 from "fs";
|
|
2069
|
+
import * as path8 from "path";
|
|
2070
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
2209
2071
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
2210
2072
|
const maxAttempts = def.maxRetries ?? 2;
|
|
2211
2073
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -2215,8 +2077,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
2215
2077
|
return { ...gateResult, retries: attempt };
|
|
2216
2078
|
}
|
|
2217
2079
|
if (attempt < maxAttempts) {
|
|
2218
|
-
const verifyPath =
|
|
2219
|
-
const errorOutput =
|
|
2080
|
+
const verifyPath = path8.join(ctx.taskDir, "verify.md");
|
|
2081
|
+
const errorOutput = fs8.existsSync(verifyPath) ? fs8.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
2220
2082
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
2221
2083
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
2222
2084
|
const diagConfig = getProjectConfig();
|
|
@@ -2259,7 +2121,7 @@ ${diagnosis.resolution}`);
|
|
|
2259
2121
|
const parts = parseCommand(cmd);
|
|
2260
2122
|
if (parts.length === 0) return;
|
|
2261
2123
|
try {
|
|
2262
|
-
|
|
2124
|
+
execFileSync6(parts[0], parts.slice(1), {
|
|
2263
2125
|
stdio: "pipe",
|
|
2264
2126
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
2265
2127
|
});
|
|
@@ -2312,18 +2174,18 @@ var init_verify = __esm({
|
|
|
2312
2174
|
});
|
|
2313
2175
|
|
|
2314
2176
|
// src/cli/task-resolution.ts
|
|
2315
|
-
import * as
|
|
2316
|
-
import * as
|
|
2317
|
-
import { execFileSync as
|
|
2177
|
+
import * as fs9 from "fs";
|
|
2178
|
+
import * as path9 from "path";
|
|
2179
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
2318
2180
|
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
2319
|
-
const tasksDir =
|
|
2320
|
-
if (!
|
|
2321
|
-
const allDirs =
|
|
2181
|
+
const tasksDir = path9.join(projectDir, ".kody", "tasks");
|
|
2182
|
+
if (!fs9.existsSync(tasksDir)) return null;
|
|
2183
|
+
const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
2322
2184
|
const prefix = `${issueNumber}-`;
|
|
2323
2185
|
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
2324
2186
|
if (direct) return direct;
|
|
2325
2187
|
try {
|
|
2326
|
-
const branch =
|
|
2188
|
+
const branch = execFileSync7("git", ["branch", "--show-current"], {
|
|
2327
2189
|
encoding: "utf-8",
|
|
2328
2190
|
cwd: projectDir,
|
|
2329
2191
|
timeout: 5e3,
|
|
@@ -2352,8 +2214,8 @@ var init_task_resolution = __esm({
|
|
|
2352
2214
|
});
|
|
2353
2215
|
|
|
2354
2216
|
// src/review-standalone.ts
|
|
2355
|
-
import * as
|
|
2356
|
-
import * as
|
|
2217
|
+
import * as fs10 from "fs";
|
|
2218
|
+
import * as path10 from "path";
|
|
2357
2219
|
function resolveReviewTarget(input) {
|
|
2358
2220
|
if (input.prs.length === 0) {
|
|
2359
2221
|
return {
|
|
@@ -2377,8 +2239,8 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
2377
2239
|
}
|
|
2378
2240
|
async function runStandaloneReview(input) {
|
|
2379
2241
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
2380
|
-
const taskDir =
|
|
2381
|
-
|
|
2242
|
+
const taskDir = path10.join(input.projectDir, ".kody", "tasks", taskId);
|
|
2243
|
+
fs10.mkdirSync(taskDir, { recursive: true });
|
|
2382
2244
|
const diffInstruction = input.baseBranch ? `
|
|
2383
2245
|
|
|
2384
2246
|
## Diff Command
|
|
@@ -2387,7 +2249,7 @@ Do NOT use bare \`git diff\` \u2014 it shows only uncommitted working tree chang
|
|
|
2387
2249
|
const taskContent = `# ${input.prTitle}
|
|
2388
2250
|
|
|
2389
2251
|
${input.prBody ?? ""}${diffInstruction}`;
|
|
2390
|
-
|
|
2252
|
+
fs10.writeFileSync(path10.join(taskDir, "task.md"), taskContent);
|
|
2391
2253
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
2392
2254
|
const ctx = {
|
|
2393
2255
|
taskId,
|
|
@@ -2409,10 +2271,10 @@ ${input.prBody ?? ""}${diffInstruction}`;
|
|
|
2409
2271
|
error: result.error ?? "Review stage failed"
|
|
2410
2272
|
};
|
|
2411
2273
|
}
|
|
2412
|
-
const reviewPath =
|
|
2274
|
+
const reviewPath = path10.join(taskDir, "review.md");
|
|
2413
2275
|
let reviewContent;
|
|
2414
|
-
if (
|
|
2415
|
-
reviewContent =
|
|
2276
|
+
if (fs10.existsSync(reviewPath)) {
|
|
2277
|
+
reviewContent = fs10.readFileSync(reviewPath, "utf-8");
|
|
2416
2278
|
}
|
|
2417
2279
|
return {
|
|
2418
2280
|
outcome: "completed",
|
|
@@ -2451,8 +2313,8 @@ var init_review_standalone = __esm({
|
|
|
2451
2313
|
});
|
|
2452
2314
|
|
|
2453
2315
|
// src/stages/review.ts
|
|
2454
|
-
import * as
|
|
2455
|
-
import * as
|
|
2316
|
+
import * as fs11 from "fs";
|
|
2317
|
+
import * as path11 from "path";
|
|
2456
2318
|
async function executeReviewWithFix(ctx, def) {
|
|
2457
2319
|
if (ctx.input.dryRun) {
|
|
2458
2320
|
return { outcome: "completed", retries: 0 };
|
|
@@ -2466,11 +2328,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
2466
2328
|
if (reviewResult.outcome !== "completed") {
|
|
2467
2329
|
return reviewResult;
|
|
2468
2330
|
}
|
|
2469
|
-
const reviewFile =
|
|
2470
|
-
if (!
|
|
2331
|
+
const reviewFile = path11.join(ctx.taskDir, "review.md");
|
|
2332
|
+
if (!fs11.existsSync(reviewFile)) {
|
|
2471
2333
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
2472
2334
|
}
|
|
2473
|
-
const content =
|
|
2335
|
+
const content = fs11.readFileSync(reviewFile, "utf-8");
|
|
2474
2336
|
if (detectReviewVerdict(content) !== "fail") {
|
|
2475
2337
|
return { ...reviewResult, retries: iteration };
|
|
2476
2338
|
}
|
|
@@ -2499,15 +2361,15 @@ var init_review = __esm({
|
|
|
2499
2361
|
});
|
|
2500
2362
|
|
|
2501
2363
|
// src/stages/ship.ts
|
|
2502
|
-
import * as
|
|
2503
|
-
import * as
|
|
2504
|
-
import { execFileSync as
|
|
2364
|
+
import * as fs12 from "fs";
|
|
2365
|
+
import * as path12 from "path";
|
|
2366
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
2505
2367
|
function buildPrBody(ctx) {
|
|
2506
2368
|
const sections = [];
|
|
2507
|
-
const taskJsonPath =
|
|
2508
|
-
if (
|
|
2369
|
+
const taskJsonPath = path12.join(ctx.taskDir, "task.json");
|
|
2370
|
+
if (fs12.existsSync(taskJsonPath)) {
|
|
2509
2371
|
try {
|
|
2510
|
-
const raw =
|
|
2372
|
+
const raw = fs12.readFileSync(taskJsonPath, "utf-8");
|
|
2511
2373
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2512
2374
|
const task = JSON.parse(cleaned);
|
|
2513
2375
|
if (task.description) {
|
|
@@ -2526,9 +2388,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
2526
2388
|
} catch {
|
|
2527
2389
|
}
|
|
2528
2390
|
}
|
|
2529
|
-
const reviewPath =
|
|
2530
|
-
if (
|
|
2531
|
-
const review =
|
|
2391
|
+
const reviewPath = path12.join(ctx.taskDir, "review.md");
|
|
2392
|
+
if (fs12.existsSync(reviewPath)) {
|
|
2393
|
+
const review = fs12.readFileSync(reviewPath, "utf-8");
|
|
2532
2394
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
2533
2395
|
if (summaryMatch) {
|
|
2534
2396
|
const summary = summaryMatch[1].trim();
|
|
@@ -2545,14 +2407,14 @@ ${summary}`);
|
|
|
2545
2407
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
2546
2408
|
}
|
|
2547
2409
|
}
|
|
2548
|
-
const verifyPath =
|
|
2549
|
-
if (
|
|
2550
|
-
const verify =
|
|
2410
|
+
const verifyPath = path12.join(ctx.taskDir, "verify.md");
|
|
2411
|
+
if (fs12.existsSync(verifyPath)) {
|
|
2412
|
+
const verify = fs12.readFileSync(verifyPath, "utf-8");
|
|
2551
2413
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
2552
2414
|
}
|
|
2553
|
-
const planPath =
|
|
2554
|
-
if (
|
|
2555
|
-
const plan =
|
|
2415
|
+
const planPath = path12.join(ctx.taskDir, "plan.md");
|
|
2416
|
+
if (fs12.existsSync(planPath)) {
|
|
2417
|
+
const plan = fs12.readFileSync(planPath, "utf-8").trim();
|
|
2556
2418
|
if (plan) {
|
|
2557
2419
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
2558
2420
|
sections.push(`
|
|
@@ -2572,25 +2434,25 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
2572
2434
|
return sections.join("\n");
|
|
2573
2435
|
}
|
|
2574
2436
|
function executeShipStage(ctx, _def) {
|
|
2575
|
-
const shipPath =
|
|
2437
|
+
const shipPath = path12.join(ctx.taskDir, "ship.md");
|
|
2576
2438
|
if (ctx.input.dryRun) {
|
|
2577
|
-
|
|
2439
|
+
fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
2578
2440
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2579
2441
|
}
|
|
2580
2442
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
2581
|
-
|
|
2443
|
+
fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
2582
2444
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2583
2445
|
}
|
|
2584
2446
|
try {
|
|
2585
2447
|
const head = getCurrentBranch(ctx.projectDir);
|
|
2586
2448
|
const base = getDefaultBranch(ctx.projectDir);
|
|
2587
2449
|
try {
|
|
2588
|
-
|
|
2450
|
+
execFileSync8("git", ["add", ctx.taskDir], {
|
|
2589
2451
|
cwd: ctx.projectDir,
|
|
2590
2452
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2591
2453
|
stdio: "pipe"
|
|
2592
2454
|
});
|
|
2593
|
-
|
|
2455
|
+
execFileSync8("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
2594
2456
|
cwd: ctx.projectDir,
|
|
2595
2457
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2596
2458
|
stdio: "pipe"
|
|
@@ -2604,7 +2466,7 @@ function executeShipStage(ctx, _def) {
|
|
|
2604
2466
|
let repo = config.github?.repo;
|
|
2605
2467
|
if (!owner || !repo) {
|
|
2606
2468
|
try {
|
|
2607
|
-
const remoteUrl =
|
|
2469
|
+
const remoteUrl = execFileSync8("git", ["remote", "get-url", "origin"], {
|
|
2608
2470
|
encoding: "utf-8",
|
|
2609
2471
|
cwd: ctx.projectDir
|
|
2610
2472
|
}).trim();
|
|
@@ -2625,28 +2487,28 @@ function executeShipStage(ctx, _def) {
|
|
|
2625
2487
|
chore: "chore"
|
|
2626
2488
|
};
|
|
2627
2489
|
let prefix = "chore";
|
|
2628
|
-
const taskJsonPath =
|
|
2629
|
-
if (
|
|
2490
|
+
const taskJsonPath = path12.join(ctx.taskDir, "task.json");
|
|
2491
|
+
if (fs12.existsSync(taskJsonPath)) {
|
|
2630
2492
|
try {
|
|
2631
|
-
const raw =
|
|
2493
|
+
const raw = fs12.readFileSync(taskJsonPath, "utf-8");
|
|
2632
2494
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2633
2495
|
const task = JSON.parse(cleaned);
|
|
2634
2496
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
2635
2497
|
} catch {
|
|
2636
2498
|
}
|
|
2637
2499
|
}
|
|
2638
|
-
const taskMdPath =
|
|
2639
|
-
if (
|
|
2640
|
-
const content =
|
|
2500
|
+
const taskMdPath = path12.join(ctx.taskDir, "task.md");
|
|
2501
|
+
if (fs12.existsSync(taskMdPath)) {
|
|
2502
|
+
const content = fs12.readFileSync(taskMdPath, "utf-8");
|
|
2641
2503
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
2642
2504
|
if (heading) {
|
|
2643
2505
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
2644
2506
|
}
|
|
2645
2507
|
}
|
|
2646
2508
|
if (title === "Update") {
|
|
2647
|
-
if (
|
|
2509
|
+
if (fs12.existsSync(taskJsonPath)) {
|
|
2648
2510
|
try {
|
|
2649
|
-
const raw =
|
|
2511
|
+
const raw = fs12.readFileSync(taskJsonPath, "utf-8");
|
|
2650
2512
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2651
2513
|
const task = JSON.parse(cleaned);
|
|
2652
2514
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -2669,7 +2531,7 @@ function executeShipStage(ctx, _def) {
|
|
|
2669
2531
|
} catch {
|
|
2670
2532
|
}
|
|
2671
2533
|
}
|
|
2672
|
-
|
|
2534
|
+
fs12.writeFileSync(shipPath, `# Ship
|
|
2673
2535
|
|
|
2674
2536
|
Updated existing PR: ${existingPr.url}
|
|
2675
2537
|
PR #${existingPr.number}
|
|
@@ -2690,26 +2552,22 @@ PR #${existingPr.number}
|
|
|
2690
2552
|
} catch {
|
|
2691
2553
|
}
|
|
2692
2554
|
}
|
|
2693
|
-
|
|
2555
|
+
fs12.writeFileSync(shipPath, `# Ship
|
|
2694
2556
|
|
|
2695
2557
|
PR created: ${pr.url}
|
|
2696
2558
|
PR #${pr.number}
|
|
2697
2559
|
`);
|
|
2698
2560
|
} else {
|
|
2699
|
-
|
|
2561
|
+
fs12.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
2700
2562
|
}
|
|
2701
2563
|
}
|
|
2702
2564
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2703
2565
|
} catch (err) {
|
|
2704
2566
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2705
|
-
|
|
2706
|
-
fs19.writeFileSync(shipPath, `# Ship
|
|
2567
|
+
fs12.writeFileSync(shipPath, `# Ship
|
|
2707
2568
|
|
|
2708
2569
|
Failed: ${msg}
|
|
2709
2570
|
`);
|
|
2710
|
-
} catch {
|
|
2711
|
-
logger.warn(` Failed to write ship.md artifact`);
|
|
2712
|
-
}
|
|
2713
2571
|
return { outcome: "failed", retries: 0, error: msg };
|
|
2714
2572
|
}
|
|
2715
2573
|
}
|
|
@@ -2752,15 +2610,15 @@ var init_executor_registry = __esm({
|
|
|
2752
2610
|
});
|
|
2753
2611
|
|
|
2754
2612
|
// src/pipeline/questions.ts
|
|
2755
|
-
import * as
|
|
2756
|
-
import * as
|
|
2613
|
+
import * as fs13 from "fs";
|
|
2614
|
+
import * as path13 from "path";
|
|
2757
2615
|
function checkForQuestions(ctx, stageName) {
|
|
2758
2616
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
2759
2617
|
try {
|
|
2760
2618
|
if (stageName === "taskify") {
|
|
2761
|
-
const taskJsonPath =
|
|
2762
|
-
if (!
|
|
2763
|
-
const raw =
|
|
2619
|
+
const taskJsonPath = path13.join(ctx.taskDir, "task.json");
|
|
2620
|
+
if (!fs13.existsSync(taskJsonPath)) return false;
|
|
2621
|
+
const raw = fs13.readFileSync(taskJsonPath, "utf-8");
|
|
2764
2622
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2765
2623
|
const taskJson = JSON.parse(cleaned);
|
|
2766
2624
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -2775,9 +2633,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
2775
2633
|
}
|
|
2776
2634
|
}
|
|
2777
2635
|
if (stageName === "plan") {
|
|
2778
|
-
const planPath =
|
|
2779
|
-
if (!
|
|
2780
|
-
const plan =
|
|
2636
|
+
const planPath = path13.join(ctx.taskDir, "plan.md");
|
|
2637
|
+
if (!fs13.existsSync(planPath)) return false;
|
|
2638
|
+
const plan = fs13.readFileSync(planPath, "utf-8");
|
|
2781
2639
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
2782
2640
|
if (questionsMatch) {
|
|
2783
2641
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -2806,13 +2664,12 @@ var init_questions = __esm({
|
|
|
2806
2664
|
});
|
|
2807
2665
|
|
|
2808
2666
|
// src/pipeline/hooks.ts
|
|
2809
|
-
import * as
|
|
2810
|
-
import * as
|
|
2667
|
+
import * as fs14 from "fs";
|
|
2668
|
+
import * as path14 from "path";
|
|
2811
2669
|
function applyPreStageLabel(ctx, def) {
|
|
2812
2670
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
2813
2671
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
2814
2672
|
if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
|
|
2815
|
-
if (def.name === "ship") setLifecycleLabel(ctx.input.issueNumber, "shipping");
|
|
2816
2673
|
}
|
|
2817
2674
|
function checkQuestionsAfterStage(ctx, def, state) {
|
|
2818
2675
|
if (def.name !== "taskify" && def.name !== "plan") return null;
|
|
@@ -2845,9 +2702,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
2845
2702
|
return { complexity, activeStages };
|
|
2846
2703
|
}
|
|
2847
2704
|
try {
|
|
2848
|
-
const taskJsonPath =
|
|
2849
|
-
if (!
|
|
2850
|
-
const raw =
|
|
2705
|
+
const taskJsonPath = path14.join(ctx.taskDir, "task.json");
|
|
2706
|
+
if (!fs14.existsSync(taskJsonPath)) return null;
|
|
2707
|
+
const raw = fs14.readFileSync(taskJsonPath, "utf-8");
|
|
2851
2708
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2852
2709
|
const taskJson = JSON.parse(cleaned);
|
|
2853
2710
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -2877,8 +2734,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
2877
2734
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
2878
2735
|
if (ctx.input.mode === "rerun") return null;
|
|
2879
2736
|
if (!ctx.input.issueNumber) return null;
|
|
2880
|
-
const planPath =
|
|
2881
|
-
const plan =
|
|
2737
|
+
const planPath = path14.join(ctx.taskDir, "plan.md");
|
|
2738
|
+
const plan = fs14.existsSync(planPath) ? fs14.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
2882
2739
|
try {
|
|
2883
2740
|
postComment(
|
|
2884
2741
|
ctx.input.issueNumber,
|
|
@@ -2945,22 +2802,22 @@ var init_hooks = __esm({
|
|
|
2945
2802
|
});
|
|
2946
2803
|
|
|
2947
2804
|
// src/learning/auto-learn.ts
|
|
2948
|
-
import * as
|
|
2949
|
-
import * as
|
|
2805
|
+
import * as fs15 from "fs";
|
|
2806
|
+
import * as path15 from "path";
|
|
2950
2807
|
function stripAnsi(str) {
|
|
2951
2808
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2952
2809
|
}
|
|
2953
2810
|
function autoLearn(ctx) {
|
|
2954
2811
|
try {
|
|
2955
|
-
const memoryDir =
|
|
2956
|
-
if (!
|
|
2957
|
-
|
|
2812
|
+
const memoryDir = path15.join(ctx.projectDir, ".kody", "memory");
|
|
2813
|
+
if (!fs15.existsSync(memoryDir)) {
|
|
2814
|
+
fs15.mkdirSync(memoryDir, { recursive: true });
|
|
2958
2815
|
}
|
|
2959
2816
|
const learnings = [];
|
|
2960
2817
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2961
|
-
const verifyPath =
|
|
2962
|
-
if (
|
|
2963
|
-
const verify = stripAnsi(
|
|
2818
|
+
const verifyPath = path15.join(ctx.taskDir, "verify.md");
|
|
2819
|
+
if (fs15.existsSync(verifyPath)) {
|
|
2820
|
+
const verify = stripAnsi(fs15.readFileSync(verifyPath, "utf-8"));
|
|
2964
2821
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
2965
2822
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
2966
2823
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -2969,18 +2826,18 @@ function autoLearn(ctx) {
|
|
|
2969
2826
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
2970
2827
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
2971
2828
|
}
|
|
2972
|
-
const reviewPath =
|
|
2973
|
-
if (
|
|
2974
|
-
const review =
|
|
2829
|
+
const reviewPath = path15.join(ctx.taskDir, "review.md");
|
|
2830
|
+
if (fs15.existsSync(reviewPath)) {
|
|
2831
|
+
const review = fs15.readFileSync(reviewPath, "utf-8");
|
|
2975
2832
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
2976
2833
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
2977
2834
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
2978
2835
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
2979
2836
|
}
|
|
2980
|
-
const taskJsonPath =
|
|
2981
|
-
if (
|
|
2837
|
+
const taskJsonPath = path15.join(ctx.taskDir, "task.json");
|
|
2838
|
+
if (fs15.existsSync(taskJsonPath)) {
|
|
2982
2839
|
try {
|
|
2983
|
-
const raw = stripAnsi(
|
|
2840
|
+
const raw = stripAnsi(fs15.readFileSync(taskJsonPath, "utf-8"));
|
|
2984
2841
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2985
2842
|
const task = JSON.parse(cleaned);
|
|
2986
2843
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -2991,48 +2848,135 @@ function autoLearn(ctx) {
|
|
|
2991
2848
|
}
|
|
2992
2849
|
}
|
|
2993
2850
|
if (learnings.length > 0) {
|
|
2994
|
-
const conventionsPath =
|
|
2851
|
+
const conventionsPath = path15.join(memoryDir, "conventions.md");
|
|
2995
2852
|
const entry = `
|
|
2996
2853
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
2997
2854
|
${learnings.join("\n")}
|
|
2998
2855
|
`;
|
|
2999
|
-
|
|
2856
|
+
fs15.appendFileSync(conventionsPath, entry);
|
|
2857
|
+
invalidateCache(conventionsPath, path15.join(memoryDir, ".tiers"));
|
|
3000
2858
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
3001
2859
|
}
|
|
2860
|
+
autoLearnDecisions(ctx.taskDir, memoryDir, ctx.taskId, timestamp2);
|
|
3002
2861
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
3003
2862
|
} catch {
|
|
3004
2863
|
}
|
|
3005
2864
|
}
|
|
3006
2865
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
3007
|
-
const archPath =
|
|
3008
|
-
if (
|
|
3009
|
-
const detected =
|
|
2866
|
+
const archPath = path15.join(memoryDir, "architecture.md");
|
|
2867
|
+
if (fs15.existsSync(archPath)) return;
|
|
2868
|
+
const detected = [];
|
|
2869
|
+
const pkgPath = path15.join(projectDir, "package.json");
|
|
2870
|
+
if (fs15.existsSync(pkgPath)) {
|
|
2871
|
+
try {
|
|
2872
|
+
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
|
|
2873
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2874
|
+
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
2875
|
+
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
2876
|
+
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
2877
|
+
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
2878
|
+
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
2879
|
+
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
2880
|
+
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
2881
|
+
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
2882
|
+
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- Database: Prisma ORM");
|
|
2883
|
+
if (allDeps.drizzle || allDeps["drizzle-orm"]) detected.push("- Database: Drizzle ORM");
|
|
2884
|
+
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
2885
|
+
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
2886
|
+
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
2887
|
+
else detected.push("- Module system: CommonJS");
|
|
2888
|
+
if (fs15.existsSync(path15.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
2889
|
+
else if (fs15.existsSync(path15.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
2890
|
+
else if (fs15.existsSync(path15.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
2891
|
+
} catch {
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
const topDirs = [];
|
|
2895
|
+
try {
|
|
2896
|
+
const entries = fs15.readdirSync(projectDir, { withFileTypes: true });
|
|
2897
|
+
for (const entry of entries) {
|
|
2898
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2899
|
+
topDirs.push(entry.name);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
2903
|
+
} catch {
|
|
2904
|
+
}
|
|
2905
|
+
const srcDir = path15.join(projectDir, "src");
|
|
2906
|
+
if (fs15.existsSync(srcDir)) {
|
|
2907
|
+
try {
|
|
2908
|
+
const srcEntries = fs15.readdirSync(srcDir, { withFileTypes: true });
|
|
2909
|
+
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2910
|
+
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
2911
|
+
} catch {
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
3010
2914
|
if (detected.length > 0) {
|
|
3011
2915
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
3012
2916
|
|
|
3013
2917
|
## Overview
|
|
3014
2918
|
${detected.join("\n")}
|
|
3015
2919
|
`;
|
|
3016
|
-
|
|
2920
|
+
fs15.writeFileSync(archPath, content);
|
|
2921
|
+
invalidateCache(archPath, path15.join(memoryDir, ".tiers"));
|
|
3017
2922
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
3018
2923
|
}
|
|
3019
2924
|
}
|
|
2925
|
+
function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
|
|
2926
|
+
const reviewPath = path15.join(taskDir, "review.md");
|
|
2927
|
+
if (!fs15.existsSync(reviewPath)) return;
|
|
2928
|
+
const review = fs15.readFileSync(reviewPath, "utf-8");
|
|
2929
|
+
const decisions = [];
|
|
2930
|
+
const existingPatternRe = /(?:use|follow|reuse|match|adopt)\s+(?:the\s+)?existing\s+(.+?)(?:\.|$)/gim;
|
|
2931
|
+
for (const match of review.matchAll(existingPatternRe)) {
|
|
2932
|
+
decisions.push(`- Use existing ${match[1].trim()}`);
|
|
2933
|
+
}
|
|
2934
|
+
const insteadOfRe = /instead\s+of\s+(.+?),?\s+(?:use|prefer|adopt)\s+(.+?)(?:\.|$)/gim;
|
|
2935
|
+
for (const match of review.matchAll(insteadOfRe)) {
|
|
2936
|
+
decisions.push(`- Prefer ${match[2].trim()} over ${match[1].trim()}`);
|
|
2937
|
+
}
|
|
2938
|
+
const consistentRe = /(?:consistent\s+with|same\s+pattern\s+as|follow\s+the\s+pattern\s+(?:in|from))\s+(.+?)(?:\.|$)/gim;
|
|
2939
|
+
for (const match of review.matchAll(consistentRe)) {
|
|
2940
|
+
decisions.push(`- Follow pattern from ${match[1].trim()}`);
|
|
2941
|
+
}
|
|
2942
|
+
const avoidRe = /(?:don't|do\s+not|never|avoid)\s+(?:use\s+)?(.+?)\s+(?:for|when|in)\s+(.+?)(?:\.|$)/gim;
|
|
2943
|
+
for (const match of review.matchAll(avoidRe)) {
|
|
2944
|
+
decisions.push(`- Avoid ${match[1].trim()} for ${match[2].trim()}`);
|
|
2945
|
+
}
|
|
2946
|
+
if (decisions.length === 0) return;
|
|
2947
|
+
const decisionsPath = path15.join(memoryDir, "decisions.md");
|
|
2948
|
+
let existing = "";
|
|
2949
|
+
if (fs15.existsSync(decisionsPath)) {
|
|
2950
|
+
existing = fs15.readFileSync(decisionsPath, "utf-8");
|
|
2951
|
+
} else {
|
|
2952
|
+
existing = "# Architectural Decisions\n\nDecisions extracted from code reviews. The planning agent MUST follow these.\n";
|
|
2953
|
+
}
|
|
2954
|
+
const newDecisions = decisions.filter((d) => !existing.includes(d));
|
|
2955
|
+
if (newDecisions.length === 0) return;
|
|
2956
|
+
const entry = `
|
|
2957
|
+
## From task ${taskId} (${timestamp2})
|
|
2958
|
+
${newDecisions.join("\n")}
|
|
2959
|
+
`;
|
|
2960
|
+
fs15.appendFileSync(decisionsPath, existing ? entry : existing + entry);
|
|
2961
|
+
invalidateCache(decisionsPath, path15.join(memoryDir, ".tiers"));
|
|
2962
|
+
logger.info(`Auto-learned ${newDecisions.length} architectural decision(s)`);
|
|
2963
|
+
}
|
|
3020
2964
|
var init_auto_learn = __esm({
|
|
3021
2965
|
"src/learning/auto-learn.ts"() {
|
|
3022
2966
|
"use strict";
|
|
3023
2967
|
init_logger();
|
|
3024
|
-
|
|
2968
|
+
init_context_tiers();
|
|
3025
2969
|
}
|
|
3026
2970
|
});
|
|
3027
2971
|
|
|
3028
2972
|
// src/retrospective.ts
|
|
3029
|
-
import * as
|
|
3030
|
-
import * as
|
|
2973
|
+
import * as fs16 from "fs";
|
|
2974
|
+
import * as path16 from "path";
|
|
3031
2975
|
function readArtifact(taskDir, filename, maxChars) {
|
|
3032
|
-
const p =
|
|
3033
|
-
if (!
|
|
2976
|
+
const p = path16.join(taskDir, filename);
|
|
2977
|
+
if (!fs16.existsSync(p)) return null;
|
|
3034
2978
|
try {
|
|
3035
|
-
const content =
|
|
2979
|
+
const content = fs16.readFileSync(p, "utf-8");
|
|
3036
2980
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
3037
2981
|
} catch {
|
|
3038
2982
|
return null;
|
|
@@ -3085,13 +3029,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
3085
3029
|
return lines.join("\n");
|
|
3086
3030
|
}
|
|
3087
3031
|
function getLogPath(projectDir) {
|
|
3088
|
-
return
|
|
3032
|
+
return path16.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
3089
3033
|
}
|
|
3090
3034
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
3091
3035
|
const logPath = getLogPath(projectDir);
|
|
3092
|
-
if (!
|
|
3036
|
+
if (!fs16.existsSync(logPath)) return [];
|
|
3093
3037
|
try {
|
|
3094
|
-
const content =
|
|
3038
|
+
const content = fs16.readFileSync(logPath, "utf-8");
|
|
3095
3039
|
const lines = content.split("\n").filter(Boolean);
|
|
3096
3040
|
const entries = [];
|
|
3097
3041
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -3118,11 +3062,11 @@ function formatPreviousEntries(entries) {
|
|
|
3118
3062
|
}
|
|
3119
3063
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
3120
3064
|
const logPath = getLogPath(projectDir);
|
|
3121
|
-
const dir =
|
|
3122
|
-
if (!
|
|
3123
|
-
|
|
3065
|
+
const dir = path16.dirname(logPath);
|
|
3066
|
+
if (!fs16.existsSync(dir)) {
|
|
3067
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
3124
3068
|
}
|
|
3125
|
-
|
|
3069
|
+
fs16.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
3126
3070
|
}
|
|
3127
3071
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
3128
3072
|
if (ctx.input.dryRun) return;
|
|
@@ -3233,8 +3177,8 @@ If no pipeline flaw is detected, set "pipelineFlaw" to null.
|
|
|
3233
3177
|
});
|
|
3234
3178
|
|
|
3235
3179
|
// src/pipeline.ts
|
|
3236
|
-
import * as
|
|
3237
|
-
import * as
|
|
3180
|
+
import * as fs17 from "fs";
|
|
3181
|
+
import * as path17 from "path";
|
|
3238
3182
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
3239
3183
|
if (ctx.input.dryRun) return;
|
|
3240
3184
|
if (ctx.input.prNumber) {
|
|
@@ -3247,59 +3191,34 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
3247
3191
|
}
|
|
3248
3192
|
if (!ctx.input.issueNumber) return;
|
|
3249
3193
|
try {
|
|
3250
|
-
const taskMdPath =
|
|
3251
|
-
const title =
|
|
3194
|
+
const taskMdPath = path17.join(ctx.taskDir, "task.md");
|
|
3195
|
+
const title = fs17.existsSync(taskMdPath) ? fs17.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
3252
3196
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
3253
3197
|
syncWithDefault(ctx.projectDir);
|
|
3254
3198
|
} catch (err) {
|
|
3255
|
-
|
|
3256
|
-
if (msg.includes("not a git repository")) {
|
|
3257
|
-
logger.warn(` Not a git repository \u2014 skipping feature branch setup`);
|
|
3258
|
-
} else {
|
|
3259
|
-
logger.error(` Failed to create/sync feature branch: ${msg}`);
|
|
3260
|
-
throw new Error(`Feature branch setup failed: ${msg}`);
|
|
3261
|
-
}
|
|
3199
|
+
logger.warn(` Failed to create/sync feature branch: ${err}`);
|
|
3262
3200
|
}
|
|
3263
3201
|
}
|
|
3264
3202
|
function acquireLock(taskDir) {
|
|
3265
|
-
const lockPath =
|
|
3266
|
-
if (
|
|
3203
|
+
const lockPath = path17.join(taskDir, ".lock");
|
|
3204
|
+
if (fs17.existsSync(lockPath)) {
|
|
3267
3205
|
try {
|
|
3268
|
-
const pid = parseInt(
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
if (e.code !== "ESRCH") throw e;
|
|
3275
|
-
logger.info(` Removing stale lock (PID ${pid} no longer running)`);
|
|
3276
|
-
}
|
|
3277
|
-
} else {
|
|
3278
|
-
logger.warn(` Corrupt lock file (non-numeric PID) \u2014 overwriting`);
|
|
3206
|
+
const pid = parseInt(fs17.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
3207
|
+
try {
|
|
3208
|
+
process.kill(pid, 0);
|
|
3209
|
+
throw new Error(`Pipeline already running (PID ${pid})`);
|
|
3210
|
+
} catch (e) {
|
|
3211
|
+
if (e.code !== "ESRCH") throw e;
|
|
3279
3212
|
}
|
|
3280
3213
|
} catch (e) {
|
|
3281
3214
|
if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
|
|
3282
|
-
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
3283
|
-
}
|
|
3284
|
-
try {
|
|
3285
|
-
fs24.unlinkSync(lockPath);
|
|
3286
|
-
} catch {
|
|
3287
3215
|
}
|
|
3288
3216
|
}
|
|
3289
|
-
|
|
3290
|
-
const fd = fs24.openSync(lockPath, fs24.constants.O_WRONLY | fs24.constants.O_CREAT | fs24.constants.O_EXCL);
|
|
3291
|
-
fs24.writeSync(fd, String(process.pid));
|
|
3292
|
-
fs24.closeSync(fd);
|
|
3293
|
-
} catch (err) {
|
|
3294
|
-
if (err.code === "EEXIST") {
|
|
3295
|
-
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
3296
|
-
}
|
|
3297
|
-
throw err;
|
|
3298
|
-
}
|
|
3217
|
+
fs17.writeFileSync(lockPath, String(process.pid));
|
|
3299
3218
|
}
|
|
3300
3219
|
function releaseLock(taskDir) {
|
|
3301
3220
|
try {
|
|
3302
|
-
|
|
3221
|
+
fs17.unlinkSync(path17.join(taskDir, ".lock"));
|
|
3303
3222
|
} catch {
|
|
3304
3223
|
}
|
|
3305
3224
|
}
|
|
@@ -3311,17 +3230,17 @@ async function runPipeline(ctx) {
|
|
|
3311
3230
|
try {
|
|
3312
3231
|
const state = loadState(ctx.taskId, ctx.taskDir);
|
|
3313
3232
|
if (state && state.state === "running") {
|
|
3314
|
-
|
|
3233
|
+
state.state = "failed";
|
|
3315
3234
|
for (const stage of STAGES) {
|
|
3316
|
-
if (
|
|
3317
|
-
|
|
3318
|
-
...
|
|
3235
|
+
if (state.stages[stage.name]?.state === "running") {
|
|
3236
|
+
state.stages[stage.name] = {
|
|
3237
|
+
...state.stages[stage.name],
|
|
3319
3238
|
state: "failed",
|
|
3320
3239
|
error: "Pipeline crashed unexpectedly"
|
|
3321
3240
|
};
|
|
3322
3241
|
}
|
|
3323
3242
|
}
|
|
3324
|
-
writeState(
|
|
3243
|
+
writeState(state, ctx.taskDir);
|
|
3325
3244
|
}
|
|
3326
3245
|
} catch {
|
|
3327
3246
|
}
|
|
@@ -3446,8 +3365,7 @@ async function runPipelineInner(ctx) {
|
|
|
3446
3365
|
}
|
|
3447
3366
|
autoLearn(ctx);
|
|
3448
3367
|
}
|
|
3449
|
-
await runRetrospective(ctx, state, pipelineStartTime).catch((
|
|
3450
|
-
logger.warn(` Retrospective failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3368
|
+
await runRetrospective(ctx, state, pipelineStartTime).catch(() => {
|
|
3451
3369
|
});
|
|
3452
3370
|
return state;
|
|
3453
3371
|
}
|
|
@@ -3487,8 +3405,8 @@ var init_pipeline = __esm({
|
|
|
3487
3405
|
});
|
|
3488
3406
|
|
|
3489
3407
|
// src/preflight.ts
|
|
3490
|
-
import { execFileSync as
|
|
3491
|
-
import * as
|
|
3408
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
3409
|
+
import * as fs18 from "fs";
|
|
3492
3410
|
function check(name, fn) {
|
|
3493
3411
|
try {
|
|
3494
3412
|
const detail = fn() ?? void 0;
|
|
@@ -3500,7 +3418,7 @@ function check(name, fn) {
|
|
|
3500
3418
|
function runPreflight() {
|
|
3501
3419
|
const checks = [
|
|
3502
3420
|
check("claude CLI", () => {
|
|
3503
|
-
const v =
|
|
3421
|
+
const v = execFileSync9("claude", ["--version"], {
|
|
3504
3422
|
encoding: "utf-8",
|
|
3505
3423
|
timeout: 1e4,
|
|
3506
3424
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3508,14 +3426,14 @@ function runPreflight() {
|
|
|
3508
3426
|
return v;
|
|
3509
3427
|
}),
|
|
3510
3428
|
check("git repo", () => {
|
|
3511
|
-
|
|
3429
|
+
execFileSync9("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3512
3430
|
encoding: "utf-8",
|
|
3513
3431
|
timeout: 5e3,
|
|
3514
3432
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3515
3433
|
});
|
|
3516
3434
|
}),
|
|
3517
3435
|
check("pnpm", () => {
|
|
3518
|
-
const v =
|
|
3436
|
+
const v = execFileSync9("pnpm", ["--version"], {
|
|
3519
3437
|
encoding: "utf-8",
|
|
3520
3438
|
timeout: 5e3,
|
|
3521
3439
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3523,7 +3441,7 @@ function runPreflight() {
|
|
|
3523
3441
|
return v;
|
|
3524
3442
|
}),
|
|
3525
3443
|
check("node >= 18", () => {
|
|
3526
|
-
const v =
|
|
3444
|
+
const v = execFileSync9("node", ["--version"], {
|
|
3527
3445
|
encoding: "utf-8",
|
|
3528
3446
|
timeout: 5e3,
|
|
3529
3447
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3533,7 +3451,7 @@ function runPreflight() {
|
|
|
3533
3451
|
return v;
|
|
3534
3452
|
}),
|
|
3535
3453
|
check("gh CLI", () => {
|
|
3536
|
-
const v =
|
|
3454
|
+
const v = execFileSync9("gh", ["--version"], {
|
|
3537
3455
|
encoding: "utf-8",
|
|
3538
3456
|
timeout: 5e3,
|
|
3539
3457
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3541,7 +3459,7 @@ function runPreflight() {
|
|
|
3541
3459
|
return v;
|
|
3542
3460
|
}),
|
|
3543
3461
|
check("package.json", () => {
|
|
3544
|
-
if (!
|
|
3462
|
+
if (!fs18.existsSync("package.json")) throw new Error("not found");
|
|
3545
3463
|
})
|
|
3546
3464
|
];
|
|
3547
3465
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -3618,10 +3536,10 @@ var init_args = __esm({
|
|
|
3618
3536
|
});
|
|
3619
3537
|
|
|
3620
3538
|
// src/cli/litellm.ts
|
|
3621
|
-
import * as
|
|
3539
|
+
import * as fs19 from "fs";
|
|
3622
3540
|
import * as os from "os";
|
|
3623
|
-
import * as
|
|
3624
|
-
import { execFileSync as
|
|
3541
|
+
import * as path18 from "path";
|
|
3542
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
3625
3543
|
async function checkLitellmHealth(url) {
|
|
3626
3544
|
try {
|
|
3627
3545
|
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
@@ -3681,17 +3599,17 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
|
3681
3599
|
logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
|
|
3682
3600
|
return null;
|
|
3683
3601
|
}
|
|
3684
|
-
const configPath =
|
|
3685
|
-
|
|
3602
|
+
const configPath = path18.join(os.tmpdir(), "kody-litellm-config.yaml");
|
|
3603
|
+
fs19.writeFileSync(configPath, generatedConfig);
|
|
3686
3604
|
const portMatch = url.match(/:(\d+)/);
|
|
3687
3605
|
const port = portMatch ? portMatch[1] : "4000";
|
|
3688
3606
|
let litellmFound = false;
|
|
3689
3607
|
try {
|
|
3690
|
-
|
|
3608
|
+
execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
3691
3609
|
litellmFound = true;
|
|
3692
3610
|
} catch {
|
|
3693
3611
|
try {
|
|
3694
|
-
|
|
3612
|
+
execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
3695
3613
|
litellmFound = true;
|
|
3696
3614
|
} catch {
|
|
3697
3615
|
}
|
|
@@ -3704,29 +3622,19 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
|
3704
3622
|
let cmd;
|
|
3705
3623
|
let args2;
|
|
3706
3624
|
try {
|
|
3707
|
-
|
|
3625
|
+
execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
3708
3626
|
cmd = "litellm";
|
|
3709
3627
|
args2 = ["--config", configPath, "--port", port];
|
|
3710
3628
|
} catch {
|
|
3711
3629
|
cmd = "python3";
|
|
3712
3630
|
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
3713
3631
|
}
|
|
3714
|
-
const dotenvPath =
|
|
3632
|
+
const dotenvPath = path18.join(projectDir, ".env");
|
|
3715
3633
|
const dotenvVars = {};
|
|
3716
|
-
if (
|
|
3717
|
-
for (const
|
|
3718
|
-
const line = rawLine.trim();
|
|
3719
|
-
if (!line || line.startsWith("#")) continue;
|
|
3634
|
+
if (fs19.existsSync(dotenvPath)) {
|
|
3635
|
+
for (const line of fs19.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
3720
3636
|
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
3721
|
-
if (match)
|
|
3722
|
-
let value = match[2].trim();
|
|
3723
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3724
|
-
value = value.slice(1, -1);
|
|
3725
|
-
}
|
|
3726
|
-
const commentIdx = value.indexOf(" #");
|
|
3727
|
-
if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
|
|
3728
|
-
if (value) dotenvVars[match[1]] = value;
|
|
3729
|
-
}
|
|
3637
|
+
if (match) dotenvVars[match[1]] = match[2];
|
|
3730
3638
|
}
|
|
3731
3639
|
if (Object.keys(dotenvVars).length > 0) {
|
|
3732
3640
|
logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
|
|
@@ -3765,8 +3673,8 @@ var init_litellm = __esm({
|
|
|
3765
3673
|
});
|
|
3766
3674
|
|
|
3767
3675
|
// src/cli/task-state.ts
|
|
3768
|
-
import * as
|
|
3769
|
-
import * as
|
|
3676
|
+
import * as fs20 from "fs";
|
|
3677
|
+
import * as path19 from "path";
|
|
3770
3678
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
3771
3679
|
if (!existingTaskId || !existingState) {
|
|
3772
3680
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -3798,11 +3706,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
3798
3706
|
function resolveForIssue(issueNumber, projectDir) {
|
|
3799
3707
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
3800
3708
|
if (existingTaskId) {
|
|
3801
|
-
const statusPath =
|
|
3709
|
+
const statusPath = path19.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
3802
3710
|
let existingState = null;
|
|
3803
|
-
if (
|
|
3711
|
+
if (fs20.existsSync(statusPath)) {
|
|
3804
3712
|
try {
|
|
3805
|
-
existingState = JSON.parse(
|
|
3713
|
+
existingState = JSON.parse(fs20.readFileSync(statusPath, "utf-8"));
|
|
3806
3714
|
} catch {
|
|
3807
3715
|
}
|
|
3808
3716
|
}
|
|
@@ -3835,12 +3743,12 @@ var resolve_exports = {};
|
|
|
3835
3743
|
__export(resolve_exports, {
|
|
3836
3744
|
runResolve: () => runResolve
|
|
3837
3745
|
});
|
|
3838
|
-
import { execFileSync as
|
|
3746
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
3839
3747
|
function getConflictContext(cwd, files) {
|
|
3840
3748
|
const parts = [];
|
|
3841
3749
|
for (const file of files.slice(0, 10)) {
|
|
3842
3750
|
try {
|
|
3843
|
-
const content =
|
|
3751
|
+
const content = execFileSync11("git", ["diff", file], {
|
|
3844
3752
|
cwd,
|
|
3845
3753
|
encoding: "utf-8",
|
|
3846
3754
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3959,8 +3867,8 @@ var init_resolve = __esm({
|
|
|
3959
3867
|
|
|
3960
3868
|
// src/entry.ts
|
|
3961
3869
|
var entry_exports = {};
|
|
3962
|
-
import * as
|
|
3963
|
-
import * as
|
|
3870
|
+
import * as fs21 from "fs";
|
|
3871
|
+
import * as path20 from "path";
|
|
3964
3872
|
async function ensureLitellmProxy(config, projectDir) {
|
|
3965
3873
|
if (!needsLitellmProxy(config)) return null;
|
|
3966
3874
|
const litellmUrl = getLitellmUrl();
|
|
@@ -3989,7 +3897,7 @@ async function ensureLitellmProxy(config, projectDir) {
|
|
|
3989
3897
|
process.env.ANTHROPIC_BASE_URL = litellmUrl;
|
|
3990
3898
|
logger.info(`ANTHROPIC_BASE_URL set to ${litellmUrl}`);
|
|
3991
3899
|
if (!process.env.ANTHROPIC_API_KEY || !process.env.ANTHROPIC_API_KEY.startsWith("sk-ant-")) {
|
|
3992
|
-
process.env.ANTHROPIC_API_KEY =
|
|
3900
|
+
process.env.ANTHROPIC_API_KEY = "sk-ant-api03-litellm-proxy-key-00000000000000000000000000000000000000000000000000000000000000000000";
|
|
3993
3901
|
}
|
|
3994
3902
|
return litellmProcess;
|
|
3995
3903
|
}
|
|
@@ -4014,9 +3922,9 @@ async function runModelHealthCheck(config) {
|
|
|
4014
3922
|
}
|
|
4015
3923
|
async function main() {
|
|
4016
3924
|
const input = parseArgs();
|
|
4017
|
-
const projectDir = input.cwd ?
|
|
3925
|
+
const projectDir = input.cwd ? path20.resolve(input.cwd) : process.cwd();
|
|
4018
3926
|
if (input.cwd) {
|
|
4019
|
-
if (!
|
|
3927
|
+
if (!fs21.existsSync(projectDir)) {
|
|
4020
3928
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
4021
3929
|
process.exit(1);
|
|
4022
3930
|
}
|
|
@@ -4050,11 +3958,9 @@ async function main() {
|
|
|
4050
3958
|
process.exit(0);
|
|
4051
3959
|
}
|
|
4052
3960
|
if (taskAction.action === "resume") {
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
command: "rerun"
|
|
4057
|
-
});
|
|
3961
|
+
input.taskId = taskAction.taskId;
|
|
3962
|
+
input.fromStage = taskAction.fromStage;
|
|
3963
|
+
input.command = "rerun";
|
|
4058
3964
|
logger.info(`Resuming task ${taskAction.taskId} from ${taskAction.fromStage}`);
|
|
4059
3965
|
}
|
|
4060
3966
|
}
|
|
@@ -4073,8 +3979,8 @@ async function main() {
|
|
|
4073
3979
|
process.exit(1);
|
|
4074
3980
|
}
|
|
4075
3981
|
}
|
|
4076
|
-
const taskDir =
|
|
4077
|
-
|
|
3982
|
+
const taskDir = path20.join(projectDir, ".kody", "tasks", taskId);
|
|
3983
|
+
fs21.mkdirSync(taskDir, { recursive: true });
|
|
4078
3984
|
if (input.command === "status") {
|
|
4079
3985
|
printStatus(taskId, taskDir);
|
|
4080
3986
|
return;
|
|
@@ -4179,7 +4085,7 @@ async function main() {
|
|
|
4179
4085
|
runners: runners2,
|
|
4180
4086
|
local: input.local ?? true
|
|
4181
4087
|
});
|
|
4182
|
-
if (litellmProcess2) litellmProcess2.kill();
|
|
4088
|
+
if (litellmProcess2) litellmProcess2.kill?.();
|
|
4183
4089
|
if (result.outcome === "failed") {
|
|
4184
4090
|
console.error(`Resolve failed: ${result.error}`);
|
|
4185
4091
|
process.exit(1);
|
|
@@ -4190,31 +4096,31 @@ async function main() {
|
|
|
4190
4096
|
logger.info("Preflight checks:");
|
|
4191
4097
|
runPreflight();
|
|
4192
4098
|
if (input.task) {
|
|
4193
|
-
|
|
4099
|
+
fs21.writeFileSync(path20.join(taskDir, "task.md"), input.task);
|
|
4194
4100
|
}
|
|
4195
|
-
const taskMdPath =
|
|
4196
|
-
if (!
|
|
4101
|
+
const taskMdPath = path20.join(taskDir, "task.md");
|
|
4102
|
+
if (!fs21.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
4197
4103
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
4198
4104
|
const prDetails = getPRDetails(input.prNumber);
|
|
4199
4105
|
if (prDetails) {
|
|
4200
4106
|
const taskContent = `# ${prDetails.title}
|
|
4201
4107
|
|
|
4202
4108
|
${prDetails.body ?? ""}`;
|
|
4203
|
-
|
|
4109
|
+
fs21.writeFileSync(taskMdPath, taskContent);
|
|
4204
4110
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
4205
4111
|
}
|
|
4206
|
-
} else if (!
|
|
4112
|
+
} else if (!fs21.existsSync(taskMdPath) && input.issueNumber) {
|
|
4207
4113
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
4208
4114
|
const issue = getIssue(input.issueNumber);
|
|
4209
4115
|
if (issue) {
|
|
4210
4116
|
const taskContent = `# ${issue.title}
|
|
4211
4117
|
|
|
4212
4118
|
${issue.body ?? ""}`;
|
|
4213
|
-
|
|
4119
|
+
fs21.writeFileSync(taskMdPath, taskContent);
|
|
4214
4120
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
4215
4121
|
}
|
|
4216
4122
|
}
|
|
4217
|
-
if (!
|
|
4123
|
+
if (!fs21.existsSync(taskMdPath)) {
|
|
4218
4124
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
4219
4125
|
process.exit(1);
|
|
4220
4126
|
}
|
|
@@ -4286,7 +4192,7 @@ ${input.feedback}`);
|
|
|
4286
4192
|
await runModelHealthCheck(config);
|
|
4287
4193
|
const cleanupLitellm = () => {
|
|
4288
4194
|
if (litellmProcess) {
|
|
4289
|
-
litellmProcess.kill();
|
|
4195
|
+
litellmProcess.kill?.();
|
|
4290
4196
|
litellmProcess = null;
|
|
4291
4197
|
}
|
|
4292
4198
|
};
|
|
@@ -4352,7 +4258,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
4352
4258
|
}
|
|
4353
4259
|
}
|
|
4354
4260
|
const state = await runPipeline(ctx);
|
|
4355
|
-
const files =
|
|
4261
|
+
const files = fs21.readdirSync(taskDir);
|
|
4356
4262
|
console.log(`
|
|
4357
4263
|
Artifacts in ${taskDir}:`);
|
|
4358
4264
|
for (const f of files) {
|
|
@@ -4416,21 +4322,20 @@ var init_entry = __esm({
|
|
|
4416
4322
|
});
|
|
4417
4323
|
|
|
4418
4324
|
// src/bin/cli.ts
|
|
4419
|
-
import * as
|
|
4420
|
-
import * as
|
|
4325
|
+
import * as fs22 from "fs";
|
|
4326
|
+
import * as path21 from "path";
|
|
4327
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
4421
4328
|
import { fileURLToPath } from "url";
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
import { execFileSync } from "child_process";
|
|
4431
|
-
function checkCommand(name, args2, fix) {
|
|
4329
|
+
var __dirname = path21.dirname(fileURLToPath(import.meta.url));
|
|
4330
|
+
var PKG_ROOT = path21.resolve(__dirname, "..", "..");
|
|
4331
|
+
function getVersion() {
|
|
4332
|
+
const pkgPath = path21.join(PKG_ROOT, "package.json");
|
|
4333
|
+
const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
|
|
4334
|
+
return pkg.version;
|
|
4335
|
+
}
|
|
4336
|
+
function checkCommand2(name, args2, fix) {
|
|
4432
4337
|
try {
|
|
4433
|
-
const output =
|
|
4338
|
+
const output = execFileSync12(name, args2, {
|
|
4434
4339
|
encoding: "utf-8",
|
|
4435
4340
|
timeout: 1e4,
|
|
4436
4341
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4441,14 +4346,14 @@ function checkCommand(name, args2, fix) {
|
|
|
4441
4346
|
}
|
|
4442
4347
|
}
|
|
4443
4348
|
function checkFile(filePath, description, fix) {
|
|
4444
|
-
if (
|
|
4349
|
+
if (fs22.existsSync(filePath)) {
|
|
4445
4350
|
return { name: description, ok: true, detail: filePath };
|
|
4446
4351
|
}
|
|
4447
4352
|
return { name: description, ok: false, fix };
|
|
4448
4353
|
}
|
|
4449
4354
|
function checkGhAuth(cwd) {
|
|
4450
4355
|
try {
|
|
4451
|
-
const output =
|
|
4356
|
+
const output = execFileSync12("gh", ["auth", "status"], {
|
|
4452
4357
|
encoding: "utf-8",
|
|
4453
4358
|
timeout: 1e4,
|
|
4454
4359
|
cwd,
|
|
@@ -4466,7 +4371,7 @@ function checkGhAuth(cwd) {
|
|
|
4466
4371
|
}
|
|
4467
4372
|
function checkGhRepoAccess(cwd) {
|
|
4468
4373
|
try {
|
|
4469
|
-
const remote =
|
|
4374
|
+
const remote = execFileSync12("git", ["remote", "get-url", "origin"], {
|
|
4470
4375
|
encoding: "utf-8",
|
|
4471
4376
|
timeout: 5e3,
|
|
4472
4377
|
cwd,
|
|
@@ -4477,7 +4382,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
4477
4382
|
return { name: "GitHub repo", ok: false, fix: "Set git remote origin to a GitHub URL" };
|
|
4478
4383
|
}
|
|
4479
4384
|
const repoSlug = `${match[1]}/${match[2]}`;
|
|
4480
|
-
|
|
4385
|
+
execFileSync12("gh", ["repo", "view", repoSlug, "--json", "name"], {
|
|
4481
4386
|
encoding: "utf-8",
|
|
4482
4387
|
timeout: 1e4,
|
|
4483
4388
|
cwd,
|
|
@@ -4490,7 +4395,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
4490
4395
|
}
|
|
4491
4396
|
function checkGhSecret(repoSlug, secretName) {
|
|
4492
4397
|
try {
|
|
4493
|
-
const output =
|
|
4398
|
+
const output = execFileSync12("gh", ["secret", "list", "--repo", repoSlug], {
|
|
4494
4399
|
encoding: "utf-8",
|
|
4495
4400
|
timeout: 1e4,
|
|
4496
4401
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4511,20 +4416,14 @@ function checkGhSecret(repoSlug, secretName) {
|
|
|
4511
4416
|
};
|
|
4512
4417
|
}
|
|
4513
4418
|
}
|
|
4514
|
-
|
|
4515
|
-
// src/bin/config-detection.ts
|
|
4516
|
-
import * as fs2 from "fs";
|
|
4517
|
-
import * as path from "path";
|
|
4518
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
4519
|
-
var FRONTEND_DEPS = ["next", "react", "vue", "svelte", "nuxt", "astro", "solid-js", "angular", "@angular/core"];
|
|
4520
4419
|
function detectBasicConfig(cwd) {
|
|
4521
4420
|
let pm = "pnpm";
|
|
4522
|
-
if (
|
|
4523
|
-
else if (
|
|
4524
|
-
else if (!
|
|
4421
|
+
if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) pm = "yarn";
|
|
4422
|
+
else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) pm = "bun";
|
|
4423
|
+
else if (!fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml")) && fs22.existsSync(path21.join(cwd, "package-lock.json"))) pm = "npm";
|
|
4525
4424
|
let defaultBranch = "main";
|
|
4526
4425
|
try {
|
|
4527
|
-
const ref =
|
|
4426
|
+
const ref = execFileSync12("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
4528
4427
|
encoding: "utf-8",
|
|
4529
4428
|
timeout: 5e3,
|
|
4530
4429
|
cwd,
|
|
@@ -4533,7 +4432,7 @@ function detectBasicConfig(cwd) {
|
|
|
4533
4432
|
defaultBranch = ref.replace("refs/remotes/origin/", "");
|
|
4534
4433
|
} catch {
|
|
4535
4434
|
try {
|
|
4536
|
-
|
|
4435
|
+
execFileSync12("git", ["rev-parse", "--verify", "origin/dev"], {
|
|
4537
4436
|
encoding: "utf-8",
|
|
4538
4437
|
timeout: 5e3,
|
|
4539
4438
|
cwd,
|
|
@@ -4546,7 +4445,7 @@ function detectBasicConfig(cwd) {
|
|
|
4546
4445
|
let owner = "";
|
|
4547
4446
|
let repo = "";
|
|
4548
4447
|
try {
|
|
4549
|
-
const remote =
|
|
4448
|
+
const remote = execFileSync12("git", ["remote", "get-url", "origin"], {
|
|
4550
4449
|
encoding: "utf-8",
|
|
4551
4450
|
timeout: 5e3,
|
|
4552
4451
|
cwd,
|
|
@@ -4564,7 +4463,7 @@ function detectBasicConfig(cwd) {
|
|
|
4564
4463
|
function buildConfig(cwd, basic) {
|
|
4565
4464
|
const pkg = (() => {
|
|
4566
4465
|
try {
|
|
4567
|
-
return JSON.parse(
|
|
4466
|
+
return JSON.parse(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
|
|
4568
4467
|
} catch {
|
|
4569
4468
|
return {};
|
|
4570
4469
|
}
|
|
@@ -4596,6 +4495,7 @@ function buildConfig(cwd, basic) {
|
|
|
4596
4495
|
if (mcp) config.mcp = mcp;
|
|
4597
4496
|
return config;
|
|
4598
4497
|
}
|
|
4498
|
+
var FRONTEND_DEPS = ["next", "react", "vue", "svelte", "nuxt", "astro", "solid-js", "angular", "@angular/core"];
|
|
4599
4499
|
function detectMcpConfig(cwd, pm, pkg) {
|
|
4600
4500
|
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
4601
4501
|
const hasFrontend = FRONTEND_DEPS.some((dep) => dep in allDeps);
|
|
@@ -4607,7 +4507,12 @@ function detectMcpConfig(cwd, pm, pkg) {
|
|
|
4607
4507
|
const defaultPort = isNext ? 3e3 : isVite ? 5173 : 3e3;
|
|
4608
4508
|
const mcp = {
|
|
4609
4509
|
enabled: true,
|
|
4610
|
-
servers: {
|
|
4510
|
+
servers: {
|
|
4511
|
+
playwright: {
|
|
4512
|
+
command: "npx",
|
|
4513
|
+
args: ["@playwright/mcp@latest"]
|
|
4514
|
+
}
|
|
4515
|
+
},
|
|
4611
4516
|
stages: ["build", "review"]
|
|
4612
4517
|
};
|
|
4613
4518
|
if (hasDevScript) {
|
|
@@ -4618,9 +4523,7 @@ function detectMcpConfig(cwd, pm, pkg) {
|
|
|
4618
4523
|
}
|
|
4619
4524
|
return mcp;
|
|
4620
4525
|
}
|
|
4621
|
-
|
|
4622
|
-
// src/bin/commands/init.ts
|
|
4623
|
-
function initCommand(opts, pkgRoot) {
|
|
4526
|
+
function initCommand(opts) {
|
|
4624
4527
|
const cwd = process.cwd();
|
|
4625
4528
|
console.log(`
|
|
4626
4529
|
\u{1F527} Kody Engine Lite \u2014 Init
|
|
@@ -4628,35 +4531,35 @@ function initCommand(opts, pkgRoot) {
|
|
|
4628
4531
|
console.log(`Project: ${cwd}
|
|
4629
4532
|
`);
|
|
4630
4533
|
console.log("\u2500\u2500 Files \u2500\u2500");
|
|
4631
|
-
const templatesDir =
|
|
4534
|
+
const templatesDir = path21.join(PKG_ROOT, "templates");
|
|
4632
4535
|
const basic = detectBasicConfig(cwd);
|
|
4633
|
-
const workflowSrc =
|
|
4634
|
-
const workflowDest =
|
|
4635
|
-
if (!
|
|
4536
|
+
const workflowSrc = path21.join(templatesDir, "kody.yml");
|
|
4537
|
+
const workflowDest = path21.join(cwd, ".github", "workflows", "kody.yml");
|
|
4538
|
+
if (!fs22.existsSync(workflowSrc)) {
|
|
4636
4539
|
console.error(" \u2717 Template kody.yml not found in package");
|
|
4637
4540
|
process.exit(1);
|
|
4638
4541
|
}
|
|
4639
|
-
if (
|
|
4542
|
+
if (fs22.existsSync(workflowDest) && !opts.force) {
|
|
4640
4543
|
console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
|
|
4641
4544
|
} else {
|
|
4642
|
-
|
|
4643
|
-
|
|
4545
|
+
fs22.mkdirSync(path21.dirname(workflowDest), { recursive: true });
|
|
4546
|
+
fs22.copyFileSync(workflowSrc, workflowDest);
|
|
4644
4547
|
console.log(" \u2713 .github/workflows/kody.yml");
|
|
4645
4548
|
}
|
|
4646
|
-
const configDest =
|
|
4647
|
-
if (!
|
|
4549
|
+
const configDest = path21.join(cwd, "kody.config.json");
|
|
4550
|
+
if (!fs22.existsSync(configDest) || opts.force) {
|
|
4648
4551
|
const config = buildConfig(cwd, basic);
|
|
4649
|
-
|
|
4552
|
+
fs22.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
|
|
4650
4553
|
console.log(" \u2713 kody.config.json (auto-configured)");
|
|
4651
4554
|
} else {
|
|
4652
4555
|
console.log(" \u25CB kody.config.json (exists)");
|
|
4653
4556
|
}
|
|
4654
|
-
const gitignorePath =
|
|
4655
|
-
if (
|
|
4656
|
-
const content =
|
|
4557
|
+
const gitignorePath = path21.join(cwd, ".gitignore");
|
|
4558
|
+
if (fs22.existsSync(gitignorePath)) {
|
|
4559
|
+
const content = fs22.readFileSync(gitignorePath, "utf-8");
|
|
4657
4560
|
if (content.includes(".tasks/")) {
|
|
4658
4561
|
const updated = content.replace(/\n?\.tasks\/\n?/g, "\n");
|
|
4659
|
-
|
|
4562
|
+
fs22.writeFileSync(gitignorePath, updated);
|
|
4660
4563
|
console.log(" \u2713 .gitignore (removed legacy .tasks/ \u2014 tasks now committed in .kody/tasks/)");
|
|
4661
4564
|
} else {
|
|
4662
4565
|
console.log(" \u25CB .gitignore (ok)");
|
|
@@ -4664,10 +4567,10 @@ function initCommand(opts, pkgRoot) {
|
|
|
4664
4567
|
}
|
|
4665
4568
|
console.log("\n\u2500\u2500 Prerequisites \u2500\u2500");
|
|
4666
4569
|
const checks = [
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
checkFile(
|
|
4570
|
+
checkCommand2("gh", ["--version"], "Install: https://cli.github.com"),
|
|
4571
|
+
checkCommand2("git", ["--version"], "Install git"),
|
|
4572
|
+
checkCommand2("node", ["--version"], "Install Node.js >= 22"),
|
|
4573
|
+
checkFile(path21.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
|
|
4671
4574
|
];
|
|
4672
4575
|
for (const c of checks) {
|
|
4673
4576
|
if (c.ok) {
|
|
@@ -4681,19 +4584,26 @@ function initCommand(opts, pkgRoot) {
|
|
|
4681
4584
|
console.log(ghAuth.ok ? ` \u2713 ${ghAuth.name} (${ghAuth.detail})` : ` \u2717 ${ghAuth.name} \u2014 ${ghAuth.fix}`);
|
|
4682
4585
|
const ghRepo = checkGhRepoAccess(cwd);
|
|
4683
4586
|
console.log(ghRepo.ok ? ` \u2713 ${ghRepo.name} (${ghRepo.detail})` : ` \u2717 ${ghRepo.name} \u2014 ${ghRepo.fix}`);
|
|
4587
|
+
let repoSlug = "";
|
|
4684
4588
|
if (ghRepo.ok && ghRepo.detail) {
|
|
4685
|
-
|
|
4686
|
-
const secretChecks = [
|
|
4589
|
+
repoSlug = ghRepo.detail;
|
|
4590
|
+
const secretChecks = [
|
|
4591
|
+
checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")
|
|
4592
|
+
];
|
|
4687
4593
|
for (const c of secretChecks) {
|
|
4688
|
-
|
|
4594
|
+
if (c.ok) {
|
|
4595
|
+
console.log(` \u2713 ${c.name}`);
|
|
4596
|
+
} else {
|
|
4597
|
+
console.log(` \u2717 ${c.name} \u2014 ${c.fix}`);
|
|
4598
|
+
}
|
|
4689
4599
|
}
|
|
4690
4600
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
4691
4601
|
console.log(" \u25CB Labels will be created automatically during bootstrap");
|
|
4692
4602
|
}
|
|
4693
4603
|
console.log("\n\u2500\u2500 Config \u2500\u2500");
|
|
4694
|
-
if (
|
|
4604
|
+
if (fs22.existsSync(configDest)) {
|
|
4695
4605
|
try {
|
|
4696
|
-
const config = JSON.parse(
|
|
4606
|
+
const config = JSON.parse(fs22.readFileSync(configDest, "utf-8"));
|
|
4697
4607
|
const configChecks = [];
|
|
4698
4608
|
if (config.github?.owner && config.github?.repo) {
|
|
4699
4609
|
configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
|
|
@@ -4723,11 +4633,11 @@ function initCommand(opts, pkgRoot) {
|
|
|
4723
4633
|
const filesToCommit = [
|
|
4724
4634
|
".github/workflows/kody.yml",
|
|
4725
4635
|
"kody.config.json"
|
|
4726
|
-
].filter((f) =>
|
|
4636
|
+
].filter((f) => fs22.existsSync(path21.join(cwd, f)));
|
|
4727
4637
|
if (filesToCommit.length > 0) {
|
|
4728
4638
|
try {
|
|
4729
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
4730
|
-
|
|
4639
|
+
const fullPaths = filesToCommit.map((f) => path21.join(cwd, f));
|
|
4640
|
+
execFileSync12("npx", ["prettier", "--write", ...fullPaths], {
|
|
4731
4641
|
cwd,
|
|
4732
4642
|
encoding: "utf-8",
|
|
4733
4643
|
timeout: 3e4,
|
|
@@ -4738,13 +4648,13 @@ function initCommand(opts, pkgRoot) {
|
|
|
4738
4648
|
}
|
|
4739
4649
|
if (filesToCommit.length > 0) {
|
|
4740
4650
|
try {
|
|
4741
|
-
|
|
4742
|
-
const staged =
|
|
4651
|
+
execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
4652
|
+
const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
4743
4653
|
if (staged) {
|
|
4744
|
-
|
|
4654
|
+
execFileSync12("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" });
|
|
4745
4655
|
console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
|
|
4746
4656
|
try {
|
|
4747
|
-
|
|
4657
|
+
execFileSync12("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
4748
4658
|
console.log(" \u2713 Pushed to origin");
|
|
4749
4659
|
} catch {
|
|
4750
4660
|
console.log(" \u25CB Push failed \u2014 run 'git push' manually");
|
|
@@ -4786,300 +4696,22 @@ function initCommand(opts, pkgRoot) {
|
|
|
4786
4696
|
console.log("");
|
|
4787
4697
|
}
|
|
4788
4698
|
}
|
|
4789
|
-
|
|
4790
|
-
// src/bin/commands/bootstrap.ts
|
|
4791
|
-
init_architecture_detection();
|
|
4792
|
-
import * as fs7 from "fs";
|
|
4793
|
-
import * as path6 from "path";
|
|
4794
|
-
import { execFileSync as execFileSync5 } from "child_process";
|
|
4795
|
-
|
|
4796
|
-
// src/bin/qa-guide.ts
|
|
4797
|
-
import * as fs5 from "fs";
|
|
4798
|
-
import * as path4 from "path";
|
|
4799
|
-
function discoverQaContext(cwd) {
|
|
4800
|
-
const result = {
|
|
4801
|
-
routes: [],
|
|
4802
|
-
authFiles: [],
|
|
4803
|
-
loginPage: null,
|
|
4804
|
-
adminPath: null,
|
|
4805
|
-
roles: [],
|
|
4806
|
-
devCommand: "",
|
|
4807
|
-
devPort: 3e3
|
|
4808
|
-
};
|
|
4809
|
-
try {
|
|
4810
|
-
const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
|
|
4811
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4812
|
-
const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
4813
|
-
if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
|
|
4814
|
-
if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
|
|
4815
|
-
else if (allDeps.vite) result.devPort = 5173;
|
|
4816
|
-
} catch {
|
|
4817
|
-
}
|
|
4818
|
-
const appDirs = ["src/app", "app"];
|
|
4819
|
-
for (const appDir of appDirs) {
|
|
4820
|
-
const fullAppDir = path4.join(cwd, appDir);
|
|
4821
|
-
if (!fs5.existsSync(fullAppDir)) continue;
|
|
4822
|
-
scanRoutes(fullAppDir, appDir, "", result);
|
|
4823
|
-
break;
|
|
4824
|
-
}
|
|
4825
|
-
const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
4826
|
-
for (const p of authPatterns) {
|
|
4827
|
-
if (fs5.existsSync(path4.join(cwd, p))) result.authFiles.push(p);
|
|
4828
|
-
}
|
|
4829
|
-
const authConfigGlobs = [
|
|
4830
|
-
"src/app/api/auth",
|
|
4831
|
-
"src/auth",
|
|
4832
|
-
"src/lib/auth",
|
|
4833
|
-
"auth.config.ts",
|
|
4834
|
-
"auth.ts",
|
|
4835
|
-
"src/app/api/oauth"
|
|
4836
|
-
];
|
|
4837
|
-
for (const g of authConfigGlobs) {
|
|
4838
|
-
if (fs5.existsSync(path4.join(cwd, g))) result.authFiles.push(g);
|
|
4839
|
-
}
|
|
4840
|
-
try {
|
|
4841
|
-
const rolePaths = [
|
|
4842
|
-
"src/types",
|
|
4843
|
-
"src/lib",
|
|
4844
|
-
"src/utils",
|
|
4845
|
-
"src/constants",
|
|
4846
|
-
"src/access",
|
|
4847
|
-
"src/collections"
|
|
4848
|
-
];
|
|
4849
|
-
for (const rp of rolePaths) {
|
|
4850
|
-
const dir = path4.join(cwd, rp);
|
|
4851
|
-
if (!fs5.existsSync(dir)) continue;
|
|
4852
|
-
const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
4853
|
-
for (const f of files) {
|
|
4854
|
-
try {
|
|
4855
|
-
const content = fs5.readFileSync(path4.join(dir, f), "utf-8").slice(0, 5e3);
|
|
4856
|
-
const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
|
|
4857
|
-
if (roleMatches) {
|
|
4858
|
-
for (const m of roleMatches) {
|
|
4859
|
-
const val = m.match(/['"](\w+)['"]/);
|
|
4860
|
-
if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
|
|
4861
|
-
}
|
|
4862
|
-
}
|
|
4863
|
-
const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
|
|
4864
|
-
if (enumMatch) {
|
|
4865
|
-
const vals = enumMatch[1].match(/['"](\w+)['"]/g);
|
|
4866
|
-
if (vals) {
|
|
4867
|
-
for (const v of vals) {
|
|
4868
|
-
const clean = v.replace(/['"]/g, "");
|
|
4869
|
-
if (!result.roles.includes(clean)) result.roles.push(clean);
|
|
4870
|
-
}
|
|
4871
|
-
}
|
|
4872
|
-
}
|
|
4873
|
-
} catch {
|
|
4874
|
-
}
|
|
4875
|
-
}
|
|
4876
|
-
}
|
|
4877
|
-
} catch {
|
|
4878
|
-
}
|
|
4879
|
-
return result;
|
|
4880
|
-
}
|
|
4881
|
-
function scanRoutes(dir, baseDir, prefix, result) {
|
|
4882
|
-
let entries;
|
|
4883
|
-
try {
|
|
4884
|
-
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
4885
|
-
} catch {
|
|
4886
|
-
return;
|
|
4887
|
-
}
|
|
4888
|
-
const hasPage = entries.some((e) => e.isFile() && /^page\.(tsx?|jsx?)$/.test(e.name));
|
|
4889
|
-
if (hasPage) {
|
|
4890
|
-
const routePath = prefix || "/";
|
|
4891
|
-
const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
|
|
4892
|
-
result.routes.push({ path: routePath, group });
|
|
4893
|
-
if (prefix.includes("/login")) result.loginPage = routePath;
|
|
4894
|
-
if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
|
|
4895
|
-
}
|
|
4896
|
-
for (const entry of entries) {
|
|
4897
|
-
if (!entry.isDirectory()) continue;
|
|
4898
|
-
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
4899
|
-
let segment = entry.name;
|
|
4900
|
-
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
4901
|
-
scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result);
|
|
4902
|
-
continue;
|
|
4903
|
-
}
|
|
4904
|
-
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
4905
|
-
segment = `:${segment.slice(1, -1)}`;
|
|
4906
|
-
}
|
|
4907
|
-
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
4908
|
-
segment = `:${segment.slice(2, -2)}?`;
|
|
4909
|
-
}
|
|
4910
|
-
scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
|
|
4911
|
-
}
|
|
4912
|
-
}
|
|
4913
|
-
function generateQaGuide(discovery) {
|
|
4914
|
-
const lines = ["# QA Guide", "", "## Authentication", ""];
|
|
4915
|
-
if (discovery.loginPage) {
|
|
4916
|
-
lines.push(`- Login page: \`${discovery.loginPage}\``);
|
|
4917
|
-
}
|
|
4918
|
-
lines.push(
|
|
4919
|
-
"",
|
|
4920
|
-
"### Test Accounts",
|
|
4921
|
-
"<!-- Fill in your test/preview environment credentials below -->",
|
|
4922
|
-
"| Role | Email | Password |",
|
|
4923
|
-
"|------|-------|----------|",
|
|
4924
|
-
"| Admin | admin@example.com | CHANGE_ME |",
|
|
4925
|
-
"| User | user@example.com | CHANGE_ME |",
|
|
4926
|
-
"",
|
|
4927
|
-
"### Login Steps",
|
|
4928
|
-
`1. Navigate to \`${discovery.loginPage ?? "/login"}\``,
|
|
4929
|
-
"2. Enter credentials from the test accounts table above",
|
|
4930
|
-
"3. Submit the login form",
|
|
4931
|
-
"4. Verify redirect to dashboard or home page"
|
|
4932
|
-
);
|
|
4933
|
-
if (discovery.authFiles.length > 0) {
|
|
4934
|
-
lines.push("", "### Auth Files");
|
|
4935
|
-
for (const f of discovery.authFiles) {
|
|
4936
|
-
lines.push(`- \`${f}\``);
|
|
4937
|
-
}
|
|
4938
|
-
}
|
|
4939
|
-
if (discovery.roles.length > 0) {
|
|
4940
|
-
lines.push("", "## Roles", "");
|
|
4941
|
-
for (const role of discovery.roles) {
|
|
4942
|
-
lines.push(`- \`${role}\``);
|
|
4943
|
-
}
|
|
4944
|
-
}
|
|
4945
|
-
lines.push("", "## Key Pages", "");
|
|
4946
|
-
const groups = {};
|
|
4947
|
-
for (const route of discovery.routes) {
|
|
4948
|
-
if (!groups[route.group]) groups[route.group] = [];
|
|
4949
|
-
groups[route.group].push(route.path);
|
|
4950
|
-
}
|
|
4951
|
-
for (const [group, routes] of Object.entries(groups)) {
|
|
4952
|
-
lines.push(`### ${group.charAt(0).toUpperCase() + group.slice(1)}`);
|
|
4953
|
-
const sorted = routes.sort();
|
|
4954
|
-
for (const r of sorted.slice(0, 20)) {
|
|
4955
|
-
lines.push(`- \`${r}\``);
|
|
4956
|
-
}
|
|
4957
|
-
if (sorted.length > 20) {
|
|
4958
|
-
lines.push(`- ... and ${sorted.length - 20} more`);
|
|
4959
|
-
}
|
|
4960
|
-
lines.push("");
|
|
4961
|
-
}
|
|
4962
|
-
lines.push(
|
|
4963
|
-
"## Dev Server",
|
|
4964
|
-
"",
|
|
4965
|
-
`- Command: \`${discovery.devCommand || "pnpm dev"}\``,
|
|
4966
|
-
`- URL: \`http://localhost:${discovery.devPort}\``,
|
|
4967
|
-
""
|
|
4968
|
-
);
|
|
4969
|
-
return lines.join("\n");
|
|
4970
|
-
}
|
|
4971
|
-
|
|
4972
|
-
// src/bin/skills.ts
|
|
4973
|
-
import * as fs6 from "fs";
|
|
4974
|
-
import * as path5 from "path";
|
|
4975
|
-
import { execFileSync as execFileSync4 } from "child_process";
|
|
4976
|
-
var SKILL_MAPPINGS = [
|
|
4977
|
-
{
|
|
4978
|
-
detect: (deps) => "next" in deps,
|
|
4979
|
-
skills: [
|
|
4980
|
-
{ package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
|
|
4981
|
-
]
|
|
4982
|
-
},
|
|
4983
|
-
{
|
|
4984
|
-
detect: (deps) => "react" in deps && !("next" in deps),
|
|
4985
|
-
skills: [
|
|
4986
|
-
{ package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
|
|
4987
|
-
]
|
|
4988
|
-
},
|
|
4989
|
-
{
|
|
4990
|
-
detect: (deps) => FRONTEND_DEPS.some((d) => d in deps),
|
|
4991
|
-
skills: [
|
|
4992
|
-
{ package: "microsoft/playwright-cli@playwright-cli", label: "Playwright browser automation" }
|
|
4993
|
-
]
|
|
4994
|
-
}
|
|
4995
|
-
];
|
|
4996
|
-
function detectSkillsForProject(cwd) {
|
|
4997
|
-
const pkgPath = path5.join(cwd, "package.json");
|
|
4998
|
-
if (!fs6.existsSync(pkgPath)) return [];
|
|
4999
|
-
try {
|
|
5000
|
-
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
5001
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5002
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5003
|
-
const skills = [];
|
|
5004
|
-
for (const mapping of SKILL_MAPPINGS) {
|
|
5005
|
-
if (mapping.detect(allDeps)) {
|
|
5006
|
-
for (const skill of mapping.skills) {
|
|
5007
|
-
if (!seen.has(skill.package)) {
|
|
5008
|
-
seen.add(skill.package);
|
|
5009
|
-
skills.push(skill);
|
|
5010
|
-
}
|
|
5011
|
-
}
|
|
5012
|
-
}
|
|
5013
|
-
}
|
|
5014
|
-
return skills;
|
|
5015
|
-
} catch {
|
|
5016
|
-
return [];
|
|
5017
|
-
}
|
|
5018
|
-
}
|
|
5019
|
-
function installSkillsForProject(cwd) {
|
|
5020
|
-
const skills = detectSkillsForProject(cwd);
|
|
5021
|
-
if (skills.length === 0) {
|
|
5022
|
-
console.log(" \u25CB No skills to install (no frontend framework detected)");
|
|
5023
|
-
return [];
|
|
5024
|
-
}
|
|
5025
|
-
let installedSkills = {};
|
|
5026
|
-
const lockPath = path5.join(cwd, "skills-lock.json");
|
|
5027
|
-
if (fs6.existsSync(lockPath)) {
|
|
5028
|
-
try {
|
|
5029
|
-
const lock = JSON.parse(fs6.readFileSync(lockPath, "utf-8"));
|
|
5030
|
-
installedSkills = lock.skills ?? {};
|
|
5031
|
-
} catch {
|
|
5032
|
-
}
|
|
5033
|
-
}
|
|
5034
|
-
const installedPaths = [];
|
|
5035
|
-
for (const skill of skills) {
|
|
5036
|
-
const skillName = skill.package.split("@").pop() ?? "";
|
|
5037
|
-
if (skillName in installedSkills) {
|
|
5038
|
-
console.log(` \u25CB ${skill.label} \u2014 already installed`);
|
|
5039
|
-
const agentPath = `.agents/skills/${skillName}`;
|
|
5040
|
-
const claudePath = `.claude/skills/${skillName}`;
|
|
5041
|
-
if (fs6.existsSync(path5.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
5042
|
-
if (fs6.existsSync(path5.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
5043
|
-
continue;
|
|
5044
|
-
}
|
|
5045
|
-
try {
|
|
5046
|
-
console.log(` Installing: ${skill.label} (${skill.package})`);
|
|
5047
|
-
execFileSync4("npx", ["skills", "add", skill.package, "--yes"], {
|
|
5048
|
-
cwd,
|
|
5049
|
-
encoding: "utf-8",
|
|
5050
|
-
timeout: 6e4,
|
|
5051
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5052
|
-
});
|
|
5053
|
-
const installedName = skill.package.split("@").pop() ?? "";
|
|
5054
|
-
const agentPath = `.agents/skills/${installedName}`;
|
|
5055
|
-
const claudePath = `.claude/skills/${installedName}`;
|
|
5056
|
-
if (fs6.existsSync(path5.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
5057
|
-
if (fs6.existsSync(path5.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
5058
|
-
console.log(` \u2713 ${skill.label}`);
|
|
5059
|
-
} catch {
|
|
5060
|
-
console.log(` \u2717 ${skill.label} \u2014 failed to install`);
|
|
5061
|
-
}
|
|
5062
|
-
}
|
|
5063
|
-
return installedPaths;
|
|
5064
|
-
}
|
|
5065
|
-
|
|
5066
|
-
// src/bin/commands/bootstrap.ts
|
|
5067
4699
|
var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
|
|
5068
4700
|
function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
5069
|
-
const srcDir =
|
|
5070
|
-
const baseDir =
|
|
4701
|
+
const srcDir = path21.join(cwd, "src");
|
|
4702
|
+
const baseDir = fs22.existsSync(srcDir) ? srcDir : cwd;
|
|
5071
4703
|
const results = [];
|
|
5072
4704
|
function walk(dir) {
|
|
5073
4705
|
const entries = [];
|
|
5074
4706
|
try {
|
|
5075
|
-
for (const entry of
|
|
4707
|
+
for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
|
|
5076
4708
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
5077
|
-
const full =
|
|
4709
|
+
const full = path21.join(dir, entry.name);
|
|
5078
4710
|
if (entry.isDirectory()) {
|
|
5079
4711
|
entries.push(...walk(full));
|
|
5080
4712
|
} else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
|
|
5081
4713
|
try {
|
|
5082
|
-
const stat =
|
|
4714
|
+
const stat = fs22.statSync(full);
|
|
5083
4715
|
if (stat.size >= 200 && stat.size <= 5e3) {
|
|
5084
4716
|
entries.push({ filePath: full, size: stat.size });
|
|
5085
4717
|
}
|
|
@@ -5093,8 +4725,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
|
5093
4725
|
}
|
|
5094
4726
|
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
5095
4727
|
for (const { filePath } of files) {
|
|
5096
|
-
const rel =
|
|
5097
|
-
const content =
|
|
4728
|
+
const rel = path21.relative(cwd, filePath);
|
|
4729
|
+
const content = fs22.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
5098
4730
|
results.push(`### File: ${rel}
|
|
5099
4731
|
\`\`\`typescript
|
|
5100
4732
|
${content}
|
|
@@ -5106,9 +4738,9 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
5106
4738
|
try {
|
|
5107
4739
|
let repoSlug = "";
|
|
5108
4740
|
try {
|
|
5109
|
-
const configPath =
|
|
5110
|
-
if (
|
|
5111
|
-
const config = JSON.parse(
|
|
4741
|
+
const configPath = path21.join(cwd, "kody.config.json");
|
|
4742
|
+
if (fs22.existsSync(configPath)) {
|
|
4743
|
+
const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
|
|
5112
4744
|
if (config.github?.owner && config.github?.repo) {
|
|
5113
4745
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
5114
4746
|
}
|
|
@@ -5116,7 +4748,7 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
5116
4748
|
} catch {
|
|
5117
4749
|
}
|
|
5118
4750
|
if (!repoSlug) return;
|
|
5119
|
-
|
|
4751
|
+
execFileSync12("gh", [
|
|
5120
4752
|
"issue",
|
|
5121
4753
|
"comment",
|
|
5122
4754
|
String(issueNumber),
|
|
@@ -5133,7 +4765,7 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
5133
4765
|
} catch {
|
|
5134
4766
|
}
|
|
5135
4767
|
}
|
|
5136
|
-
function bootstrapCommand(opts
|
|
4768
|
+
function bootstrapCommand(opts = { force: false }) {
|
|
5137
4769
|
const cwd = process.cwd();
|
|
5138
4770
|
const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
|
|
5139
4771
|
console.log(`
|
|
@@ -5143,8 +4775,8 @@ function bootstrapCommand(opts, pkgRoot) {
|
|
|
5143
4775
|
ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
|
|
5144
4776
|
}
|
|
5145
4777
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
5146
|
-
const p =
|
|
5147
|
-
if (
|
|
4778
|
+
const p = path21.join(cwd, rel);
|
|
4779
|
+
if (fs22.existsSync(p)) return fs22.readFileSync(p, "utf-8").slice(0, maxChars);
|
|
5148
4780
|
return null;
|
|
5149
4781
|
};
|
|
5150
4782
|
let repoContext = "";
|
|
@@ -5179,14 +4811,14 @@ ${sampleFiles}
|
|
|
5179
4811
|
|
|
5180
4812
|
`;
|
|
5181
4813
|
try {
|
|
5182
|
-
const topDirs =
|
|
4814
|
+
const topDirs = fs22.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
5183
4815
|
repoContext += `## Top-level directories
|
|
5184
4816
|
${topDirs.join(", ")}
|
|
5185
4817
|
|
|
5186
4818
|
`;
|
|
5187
|
-
const srcDir =
|
|
5188
|
-
if (
|
|
5189
|
-
const srcDirs =
|
|
4819
|
+
const srcDir = path21.join(cwd, "src");
|
|
4820
|
+
if (fs22.existsSync(srcDir)) {
|
|
4821
|
+
const srcDirs = fs22.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
5190
4822
|
if (srcDirs.length > 0) repoContext += `## src/ subdirectories
|
|
5191
4823
|
${srcDirs.join(", ")}
|
|
5192
4824
|
|
|
@@ -5196,19 +4828,19 @@ ${srcDirs.join(", ")}
|
|
|
5196
4828
|
}
|
|
5197
4829
|
const existingFiles = [];
|
|
5198
4830
|
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"]) {
|
|
5199
|
-
if (
|
|
4831
|
+
if (fs22.existsSync(path21.join(cwd, f))) existingFiles.push(f);
|
|
5200
4832
|
}
|
|
5201
4833
|
if (existingFiles.length) repoContext += `## Config files present
|
|
5202
4834
|
${existingFiles.join(", ")}
|
|
5203
4835
|
|
|
5204
4836
|
`;
|
|
5205
4837
|
console.log("\u2500\u2500 Project Memory \u2500\u2500");
|
|
5206
|
-
const memoryDir =
|
|
5207
|
-
|
|
5208
|
-
const archPath =
|
|
5209
|
-
const conventionsPath =
|
|
5210
|
-
const existingArch =
|
|
5211
|
-
const existingConv =
|
|
4838
|
+
const memoryDir = path21.join(cwd, ".kody", "memory");
|
|
4839
|
+
fs22.mkdirSync(memoryDir, { recursive: true });
|
|
4840
|
+
const archPath = path21.join(memoryDir, "architecture.md");
|
|
4841
|
+
const conventionsPath = path21.join(memoryDir, "conventions.md");
|
|
4842
|
+
const existingArch = fs22.existsSync(archPath) ? fs22.readFileSync(archPath, "utf-8") : "";
|
|
4843
|
+
const existingConv = fs22.existsSync(conventionsPath) ? fs22.readFileSync(conventionsPath, "utf-8") : "";
|
|
5212
4844
|
const hasExisting = !!(existingArch || existingConv);
|
|
5213
4845
|
const extendInstruction = hasExisting && !opts.force ? `
|
|
5214
4846
|
## Existing Documentation (EXTEND, do not replace)
|
|
@@ -5249,7 +4881,7 @@ Output ONLY valid JSON. No markdown fences. No explanation.
|
|
|
5249
4881
|
${repoContext}`;
|
|
5250
4882
|
console.log(" \u23F3 Analyzing project...");
|
|
5251
4883
|
try {
|
|
5252
|
-
const output =
|
|
4884
|
+
const output = execFileSync12("claude", [
|
|
5253
4885
|
"--print",
|
|
5254
4886
|
"--model",
|
|
5255
4887
|
"haiku",
|
|
@@ -5264,12 +4896,12 @@ ${repoContext}`;
|
|
|
5264
4896
|
const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
5265
4897
|
const parsed = JSON.parse(cleaned);
|
|
5266
4898
|
if (parsed.architecture) {
|
|
5267
|
-
|
|
4899
|
+
fs22.writeFileSync(archPath, parsed.architecture);
|
|
5268
4900
|
const lineCount = parsed.architecture.split("\n").length;
|
|
5269
4901
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
|
|
5270
4902
|
}
|
|
5271
4903
|
if (parsed.conventions) {
|
|
5272
|
-
|
|
4904
|
+
fs22.writeFileSync(conventionsPath, parsed.conventions);
|
|
5273
4905
|
const lineCount = parsed.conventions.split("\n").length;
|
|
5274
4906
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
|
|
5275
4907
|
}
|
|
@@ -5278,39 +4910,39 @@ ${repoContext}`;
|
|
|
5278
4910
|
const detected = detectArchitectureBasic(cwd);
|
|
5279
4911
|
if (detected.length > 0) {
|
|
5280
4912
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5281
|
-
|
|
4913
|
+
fs22.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
5282
4914
|
|
|
5283
4915
|
## Overview
|
|
5284
4916
|
${detected.join("\n")}
|
|
5285
4917
|
`);
|
|
5286
4918
|
console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
|
|
5287
4919
|
}
|
|
5288
|
-
|
|
4920
|
+
fs22.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
5289
4921
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
5290
4922
|
}
|
|
5291
4923
|
console.log("\n\u2500\u2500 Step Files \u2500\u2500");
|
|
5292
|
-
const stepsDir =
|
|
5293
|
-
|
|
5294
|
-
const arch =
|
|
5295
|
-
const conv =
|
|
4924
|
+
const stepsDir = path21.join(cwd, ".kody", "steps");
|
|
4925
|
+
fs22.mkdirSync(stepsDir, { recursive: true });
|
|
4926
|
+
const arch = fs22.existsSync(archPath) ? fs22.readFileSync(archPath, "utf-8") : "";
|
|
4927
|
+
const conv = fs22.existsSync(conventionsPath) ? fs22.readFileSync(conventionsPath, "utf-8") : "";
|
|
5296
4928
|
console.log(" \u23F3 Customizing step files...");
|
|
5297
4929
|
let stepCount = 0;
|
|
5298
4930
|
for (const stage of STEP_STAGES) {
|
|
5299
|
-
const templatePath =
|
|
5300
|
-
if (!
|
|
4931
|
+
const templatePath = path21.join(PKG_ROOT, "prompts", `${stage}.md`);
|
|
4932
|
+
if (!fs22.existsSync(templatePath)) {
|
|
5301
4933
|
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
5302
4934
|
continue;
|
|
5303
4935
|
}
|
|
5304
|
-
const stepOutputPath =
|
|
5305
|
-
if (
|
|
4936
|
+
const stepOutputPath = path21.join(stepsDir, `${stage}.md`);
|
|
4937
|
+
if (fs22.existsSync(stepOutputPath) && !opts.force) {
|
|
5306
4938
|
console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
|
|
5307
4939
|
continue;
|
|
5308
4940
|
}
|
|
5309
|
-
const defaultPrompt =
|
|
4941
|
+
const defaultPrompt = fs22.readFileSync(templatePath, "utf-8");
|
|
5310
4942
|
const contextPlaceholder = "{{TASK_CONTEXT}}";
|
|
5311
4943
|
const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
|
|
5312
4944
|
if (placeholderIdx === -1) {
|
|
5313
|
-
|
|
4945
|
+
fs22.copyFileSync(templatePath, stepOutputPath);
|
|
5314
4946
|
stepCount++;
|
|
5315
4947
|
console.log(` \u2713 ${stage}.md`);
|
|
5316
4948
|
continue;
|
|
@@ -5352,7 +4984,7 @@ ${repoContext}
|
|
|
5352
4984
|
|
|
5353
4985
|
REMINDER: Output the full prompt template first (unchanged), then your three appended sections. Do NOT include "${contextPlaceholder}".`;
|
|
5354
4986
|
try {
|
|
5355
|
-
const output =
|
|
4987
|
+
const output = execFileSync12("claude", [
|
|
5356
4988
|
"--print",
|
|
5357
4989
|
"--model",
|
|
5358
4990
|
"haiku",
|
|
@@ -5367,40 +4999,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5367
4999
|
let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
5368
5000
|
cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
|
|
5369
5001
|
const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
|
|
5370
|
-
|
|
5002
|
+
fs22.writeFileSync(stepOutputPath, finalPrompt);
|
|
5371
5003
|
stepCount++;
|
|
5372
5004
|
console.log(` \u2713 ${stage}.md`);
|
|
5373
5005
|
} catch {
|
|
5374
5006
|
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
5375
|
-
|
|
5007
|
+
fs22.copyFileSync(templatePath, stepOutputPath);
|
|
5376
5008
|
stepCount++;
|
|
5377
5009
|
}
|
|
5378
5010
|
}
|
|
5379
5011
|
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
5380
|
-
console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
|
|
5381
|
-
const qaGuidePath = path6.join(cwd, ".kody", "qa-guide.md");
|
|
5382
|
-
if (!fs7.existsSync(qaGuidePath) || opts.force) {
|
|
5383
|
-
const discovery = discoverQaContext(cwd);
|
|
5384
|
-
if (discovery.routes.length > 0) {
|
|
5385
|
-
const qaGuide = generateQaGuide(discovery);
|
|
5386
|
-
fs7.writeFileSync(qaGuidePath, qaGuide);
|
|
5387
|
-
console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
|
|
5388
|
-
if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
|
|
5389
|
-
if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
|
|
5390
|
-
console.log(" \u2139 Add QA_ADMIN_EMAIL, QA_ADMIN_PASSWORD, QA_USER_EMAIL, QA_USER_PASSWORD as GitHub secrets");
|
|
5391
|
-
} else {
|
|
5392
|
-
console.log(" \u25CB No routes detected \u2014 skipping QA guide");
|
|
5393
|
-
}
|
|
5394
|
-
} else {
|
|
5395
|
-
console.log(" \u25CB .kody/qa-guide.md already exists (use --force to regenerate)");
|
|
5396
|
-
}
|
|
5397
5012
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
5398
5013
|
try {
|
|
5399
5014
|
let repoSlug = "";
|
|
5400
5015
|
try {
|
|
5401
|
-
const configPath =
|
|
5402
|
-
if (
|
|
5403
|
-
const config = JSON.parse(
|
|
5016
|
+
const configPath = path21.join(cwd, "kody.config.json");
|
|
5017
|
+
if (fs22.existsSync(configPath)) {
|
|
5018
|
+
const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
|
|
5404
5019
|
if (config.github?.owner && config.github?.repo) {
|
|
5405
5020
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
5406
5021
|
}
|
|
@@ -5412,7 +5027,6 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5412
5027
|
{ name: "kody:planning", color: "c5def5", description: "Kody is analyzing and planning" },
|
|
5413
5028
|
{ name: "kody:building", color: "0e8a16", description: "Kody is building code" },
|
|
5414
5029
|
{ name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
|
|
5415
|
-
{ name: "kody:shipping", color: "1d76db", description: "Kody is creating the pull request" },
|
|
5416
5030
|
{ name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
|
|
5417
5031
|
{ name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
|
|
5418
5032
|
{ name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
|
|
@@ -5427,7 +5041,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5427
5041
|
];
|
|
5428
5042
|
for (const label of labels) {
|
|
5429
5043
|
try {
|
|
5430
|
-
|
|
5044
|
+
execFileSync12("gh", [
|
|
5431
5045
|
"label",
|
|
5432
5046
|
"create",
|
|
5433
5047
|
label.name,
|
|
@@ -5447,7 +5061,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5447
5061
|
console.log(` \u2713 ${label.name}`);
|
|
5448
5062
|
} catch {
|
|
5449
5063
|
try {
|
|
5450
|
-
|
|
5064
|
+
execFileSync12("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
|
|
5451
5065
|
cwd,
|
|
5452
5066
|
encoding: "utf-8",
|
|
5453
5067
|
timeout: 1e4,
|
|
@@ -5471,23 +5085,22 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5471
5085
|
const filesToCommit = [
|
|
5472
5086
|
".kody/memory/architecture.md",
|
|
5473
5087
|
".kody/memory/conventions.md",
|
|
5474
|
-
".kody/qa-guide.md",
|
|
5475
5088
|
...installedSkillPaths
|
|
5476
|
-
].filter((f) =>
|
|
5477
|
-
if (
|
|
5089
|
+
].filter((f) => fs22.existsSync(path21.join(cwd, f)));
|
|
5090
|
+
if (fs22.existsSync(path21.join(cwd, "skills-lock.json"))) {
|
|
5478
5091
|
filesToCommit.push("skills-lock.json");
|
|
5479
5092
|
}
|
|
5480
5093
|
for (const stage of STEP_STAGES) {
|
|
5481
5094
|
const stepFile = `.kody/steps/${stage}.md`;
|
|
5482
|
-
if (
|
|
5095
|
+
if (fs22.existsSync(path21.join(cwd, stepFile))) {
|
|
5483
5096
|
filesToCommit.push(stepFile);
|
|
5484
5097
|
}
|
|
5485
5098
|
}
|
|
5486
5099
|
if (filesToCommit.length > 0) {
|
|
5487
5100
|
try {
|
|
5488
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
5101
|
+
const fullPaths = filesToCommit.map((f) => path21.join(cwd, f));
|
|
5489
5102
|
for (let pass = 0; pass < 2; pass++) {
|
|
5490
|
-
|
|
5103
|
+
execFileSync12("npx", ["prettier", "--write", ...fullPaths], {
|
|
5491
5104
|
cwd,
|
|
5492
5105
|
encoding: "utf-8",
|
|
5493
5106
|
timeout: 3e4,
|
|
@@ -5503,24 +5116,24 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5503
5116
|
try {
|
|
5504
5117
|
if (isCI3) {
|
|
5505
5118
|
const branchName = `kody-bootstrap-${Date.now()}`;
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
const staged =
|
|
5119
|
+
execFileSync12("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
|
|
5120
|
+
execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
5121
|
+
const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
5509
5122
|
if (staged) {
|
|
5510
|
-
|
|
5511
|
-
|
|
5123
|
+
execFileSync12("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" });
|
|
5124
|
+
execFileSync12("git", ["push", "-u", "origin", branchName], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
5512
5125
|
console.log(` \u2713 Pushed branch: ${branchName}`);
|
|
5513
5126
|
let baseBranch = "main";
|
|
5514
5127
|
try {
|
|
5515
|
-
const configPath =
|
|
5516
|
-
if (
|
|
5517
|
-
const config = JSON.parse(
|
|
5128
|
+
const configPath = path21.join(cwd, "kody.config.json");
|
|
5129
|
+
if (fs22.existsSync(configPath)) {
|
|
5130
|
+
const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
|
|
5518
5131
|
baseBranch = config.git?.defaultBranch ?? "main";
|
|
5519
5132
|
}
|
|
5520
5133
|
} catch {
|
|
5521
5134
|
}
|
|
5522
5135
|
try {
|
|
5523
|
-
const prUrl =
|
|
5136
|
+
const prUrl = execFileSync12("gh", [
|
|
5524
5137
|
"pr",
|
|
5525
5138
|
"create",
|
|
5526
5139
|
"--title",
|
|
@@ -5559,13 +5172,13 @@ Create it manually.`, cwd);
|
|
|
5559
5172
|
console.log(" \u25CB No new changes to commit");
|
|
5560
5173
|
}
|
|
5561
5174
|
} else {
|
|
5562
|
-
|
|
5563
|
-
const staged =
|
|
5175
|
+
execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
5176
|
+
const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
5564
5177
|
if (staged) {
|
|
5565
|
-
|
|
5178
|
+
execFileSync12("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" });
|
|
5566
5179
|
console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
|
|
5567
5180
|
try {
|
|
5568
|
-
|
|
5181
|
+
execFileSync12("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
5569
5182
|
console.log(" \u2713 Pushed to origin");
|
|
5570
5183
|
} catch {
|
|
5571
5184
|
console.log(" \u25CB Push failed \u2014 run 'git push' manually");
|
|
@@ -5585,22 +5198,131 @@ Create it manually.`, cwd);
|
|
|
5585
5198
|
console.log(" \u2713 Project bootstrap complete!");
|
|
5586
5199
|
console.log(" Kody now has project-specific memory and customized step files.\n");
|
|
5587
5200
|
}
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5201
|
+
function detectArchitectureBasic(cwd) {
|
|
5202
|
+
const detected = [];
|
|
5203
|
+
const pkgPath = path21.join(cwd, "package.json");
|
|
5204
|
+
if (fs22.existsSync(pkgPath)) {
|
|
5205
|
+
try {
|
|
5206
|
+
const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
|
|
5207
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5208
|
+
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
5209
|
+
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
5210
|
+
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
5211
|
+
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
5212
|
+
else if (allDeps.hono) detected.push(`- Framework: Hono ${allDeps.hono}`);
|
|
5213
|
+
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
5214
|
+
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
5215
|
+
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
5216
|
+
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
5217
|
+
if (allDeps.prettier) detected.push(`- Formatting: prettier ${allDeps.prettier}`);
|
|
5218
|
+
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- ORM: Prisma");
|
|
5219
|
+
if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
|
|
5220
|
+
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
|
|
5221
|
+
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
5222
|
+
if (fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
5223
|
+
else if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
5224
|
+
else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
5225
|
+
else if (fs22.existsSync(path21.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
5226
|
+
} catch {
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
return detected;
|
|
5230
|
+
}
|
|
5231
|
+
var SKILL_MAPPINGS = [
|
|
5232
|
+
{
|
|
5233
|
+
detect: (deps) => "next" in deps,
|
|
5234
|
+
skills: [
|
|
5235
|
+
{ package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
|
|
5236
|
+
]
|
|
5237
|
+
},
|
|
5238
|
+
{
|
|
5239
|
+
detect: (deps) => "react" in deps && !("next" in deps),
|
|
5240
|
+
skills: [
|
|
5241
|
+
{ package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
|
|
5242
|
+
]
|
|
5243
|
+
},
|
|
5244
|
+
{
|
|
5245
|
+
detect: (deps) => FRONTEND_DEPS.some((d) => d in deps),
|
|
5246
|
+
skills: [
|
|
5247
|
+
{ package: "microsoft/playwright-cli@playwright-cli", label: "Playwright browser automation" }
|
|
5248
|
+
]
|
|
5249
|
+
}
|
|
5250
|
+
];
|
|
5251
|
+
function detectSkillsForProject(cwd) {
|
|
5252
|
+
const pkgPath = path21.join(cwd, "package.json");
|
|
5253
|
+
if (!fs22.existsSync(pkgPath)) return [];
|
|
5254
|
+
try {
|
|
5255
|
+
const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
|
|
5256
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5257
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5258
|
+
const skills = [];
|
|
5259
|
+
for (const mapping of SKILL_MAPPINGS) {
|
|
5260
|
+
if (mapping.detect(allDeps)) {
|
|
5261
|
+
for (const skill of mapping.skills) {
|
|
5262
|
+
if (!seen.has(skill.package)) {
|
|
5263
|
+
seen.add(skill.package);
|
|
5264
|
+
skills.push(skill);
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
return skills;
|
|
5270
|
+
} catch {
|
|
5271
|
+
return [];
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
function installSkillsForProject(cwd) {
|
|
5275
|
+
const skills = detectSkillsForProject(cwd);
|
|
5276
|
+
if (skills.length === 0) {
|
|
5277
|
+
console.log(" \u25CB No skills to install (no frontend framework detected)");
|
|
5278
|
+
return [];
|
|
5279
|
+
}
|
|
5280
|
+
let installedSkills = {};
|
|
5281
|
+
const lockPath = path21.join(cwd, "skills-lock.json");
|
|
5282
|
+
if (fs22.existsSync(lockPath)) {
|
|
5283
|
+
try {
|
|
5284
|
+
const lock = JSON.parse(fs22.readFileSync(lockPath, "utf-8"));
|
|
5285
|
+
installedSkills = lock.skills ?? {};
|
|
5286
|
+
} catch {
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
const installedPaths = [];
|
|
5290
|
+
for (const skill of skills) {
|
|
5291
|
+
const skillName = skill.package.split("@").pop() ?? "";
|
|
5292
|
+
if (skillName in installedSkills) {
|
|
5293
|
+
console.log(` \u25CB ${skill.label} \u2014 already installed`);
|
|
5294
|
+
const agentPath = `.agents/skills/${skillName}`;
|
|
5295
|
+
const claudePath = `.claude/skills/${skillName}`;
|
|
5296
|
+
if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
5297
|
+
if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
5298
|
+
continue;
|
|
5299
|
+
}
|
|
5300
|
+
try {
|
|
5301
|
+
console.log(` Installing: ${skill.label} (${skill.package})`);
|
|
5302
|
+
execFileSync12("npx", ["skills", "add", skill.package, "--yes"], {
|
|
5303
|
+
cwd,
|
|
5304
|
+
encoding: "utf-8",
|
|
5305
|
+
timeout: 6e4,
|
|
5306
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5307
|
+
});
|
|
5308
|
+
const skillName2 = skill.package.split("@").pop() ?? "";
|
|
5309
|
+
const agentPath = `.agents/skills/${skillName2}`;
|
|
5310
|
+
const claudePath = `.claude/skills/${skillName2}`;
|
|
5311
|
+
if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
5312
|
+
if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
5313
|
+
console.log(` \u2713 ${skill.label}`);
|
|
5314
|
+
} catch (err) {
|
|
5315
|
+
console.log(` \u2717 ${skill.label} \u2014 failed to install`);
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
return installedPaths;
|
|
5597
5319
|
}
|
|
5598
5320
|
var args = process.argv.slice(2);
|
|
5599
5321
|
var command = args[0];
|
|
5600
5322
|
if (command === "init") {
|
|
5601
|
-
initCommand({ force: args.includes("--force") }
|
|
5323
|
+
initCommand({ force: args.includes("--force") });
|
|
5602
5324
|
} else if (command === "bootstrap") {
|
|
5603
|
-
bootstrapCommand({ force: args.includes("--force") }
|
|
5325
|
+
bootstrapCommand({ force: args.includes("--force") });
|
|
5604
5326
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
5605
5327
|
console.log(getVersion());
|
|
5606
5328
|
} else {
|
|
@@ -5608,7 +5330,7 @@ if (command === "init") {
|
|
|
5608
5330
|
}
|
|
5609
5331
|
export {
|
|
5610
5332
|
buildConfig,
|
|
5611
|
-
checkCommand,
|
|
5333
|
+
checkCommand2 as checkCommand,
|
|
5612
5334
|
checkFile,
|
|
5613
5335
|
checkGhAuth,
|
|
5614
5336
|
checkGhRepoAccess,
|