@kody-ade/kody-engine-lite 0.1.103 → 0.1.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +1043 -981
- package/package.json +1 -1
- package/prompts/autofix.md +33 -20
- package/templates/kody.yml +4 -9
package/dist/bin/cli.js
CHANGED
|
@@ -9,8 +9,63 @@ 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
|
+
|
|
12
67
|
// src/agent-runner.ts
|
|
13
|
-
import { spawn, execFileSync } from "child_process";
|
|
68
|
+
import { spawn, execFileSync as execFileSync6 } from "child_process";
|
|
14
69
|
function writeStdin(child, prompt) {
|
|
15
70
|
return new Promise((resolve4, reject) => {
|
|
16
71
|
if (!child.stdin) {
|
|
@@ -82,9 +137,9 @@ async function runSubprocess(command2, args2, prompt, timeout, options) {
|
|
|
82
137
|
${errDetail}`
|
|
83
138
|
};
|
|
84
139
|
}
|
|
85
|
-
function
|
|
140
|
+
function checkCommand2(command2, args2) {
|
|
86
141
|
try {
|
|
87
|
-
|
|
142
|
+
execFileSync6(command2, args2, { timeout: 1e4, stdio: "pipe" });
|
|
88
143
|
return true;
|
|
89
144
|
} catch {
|
|
90
145
|
return false;
|
|
@@ -114,7 +169,7 @@ function createClaudeCodeRunner() {
|
|
|
114
169
|
return runSubprocess("claude", args2, prompt, timeout, options);
|
|
115
170
|
},
|
|
116
171
|
async healthCheck() {
|
|
117
|
-
return
|
|
172
|
+
return checkCommand2("claude", ["--version"]);
|
|
118
173
|
}
|
|
119
174
|
};
|
|
120
175
|
}
|
|
@@ -271,9 +326,77 @@ var init_logger = __esm({
|
|
|
271
326
|
}
|
|
272
327
|
});
|
|
273
328
|
|
|
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
|
+
|
|
274
397
|
// src/config.ts
|
|
275
|
-
import * as
|
|
276
|
-
import * as
|
|
398
|
+
import * as fs8 from "fs";
|
|
399
|
+
import * as path7 from "path";
|
|
277
400
|
function needsLitellmProxy(config) {
|
|
278
401
|
return !!(config.agent.provider && config.agent.provider !== "anthropic");
|
|
279
402
|
}
|
|
@@ -290,10 +413,16 @@ function setConfigDir(dir) {
|
|
|
290
413
|
}
|
|
291
414
|
function getProjectConfig() {
|
|
292
415
|
if (_config) return _config;
|
|
293
|
-
const configPath =
|
|
294
|
-
if (
|
|
416
|
+
const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
417
|
+
if (fs8.existsSync(configPath)) {
|
|
295
418
|
try {
|
|
296
|
-
const
|
|
419
|
+
const result = parseJsonSafe(fs8.readFileSync(configPath, "utf-8"));
|
|
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;
|
|
297
426
|
_config = {
|
|
298
427
|
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
299
428
|
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
@@ -327,6 +456,7 @@ var init_config = __esm({
|
|
|
327
456
|
"src/config.ts"() {
|
|
328
457
|
"use strict";
|
|
329
458
|
init_logger();
|
|
459
|
+
init_validators();
|
|
330
460
|
DEFAULT_CONFIG = {
|
|
331
461
|
quality: {
|
|
332
462
|
typecheck: "pnpm -s tsc --noEmit",
|
|
@@ -360,7 +490,7 @@ var init_config = __esm({
|
|
|
360
490
|
});
|
|
361
491
|
|
|
362
492
|
// src/git-utils.ts
|
|
363
|
-
import { execFileSync as
|
|
493
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
364
494
|
function getHookSafeEnv() {
|
|
365
495
|
if (!_hookSafeEnv) {
|
|
366
496
|
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
@@ -368,7 +498,7 @@ function getHookSafeEnv() {
|
|
|
368
498
|
return _hookSafeEnv;
|
|
369
499
|
}
|
|
370
500
|
function git(args2, options) {
|
|
371
|
-
return
|
|
501
|
+
return execFileSync7("git", args2, {
|
|
372
502
|
encoding: "utf-8",
|
|
373
503
|
timeout: options?.timeout ?? 3e4,
|
|
374
504
|
cwd: options?.cwd,
|
|
@@ -423,8 +553,9 @@ function ensureFeatureBranch(issueNumber, title, cwd) {
|
|
|
423
553
|
}
|
|
424
554
|
try {
|
|
425
555
|
git(["fetch", "origin"], { cwd, timeout: 3e4 });
|
|
426
|
-
} catch {
|
|
427
|
-
|
|
556
|
+
} catch (err) {
|
|
557
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
558
|
+
logger.warn(` Failed to fetch origin: ${msg}`);
|
|
428
559
|
}
|
|
429
560
|
try {
|
|
430
561
|
git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
|
|
@@ -456,8 +587,9 @@ function syncWithDefault(cwd, branch) {
|
|
|
456
587
|
if (current === defaultBranch) return;
|
|
457
588
|
try {
|
|
458
589
|
git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
|
|
459
|
-
} catch {
|
|
460
|
-
|
|
590
|
+
} catch (err) {
|
|
591
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
592
|
+
logger.warn(` Failed to fetch latest from origin: ${msg}`);
|
|
461
593
|
return;
|
|
462
594
|
}
|
|
463
595
|
try {
|
|
@@ -466,7 +598,8 @@ function syncWithDefault(cwd, branch) {
|
|
|
466
598
|
} catch {
|
|
467
599
|
try {
|
|
468
600
|
git(["merge", "--abort"], { cwd });
|
|
469
|
-
} catch {
|
|
601
|
+
} catch (abortErr) {
|
|
602
|
+
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
470
603
|
}
|
|
471
604
|
logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
|
|
472
605
|
}
|
|
@@ -477,8 +610,9 @@ function mergeDefault(cwd) {
|
|
|
477
610
|
if (current === defaultBranch) return "clean";
|
|
478
611
|
try {
|
|
479
612
|
git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
|
|
480
|
-
} catch {
|
|
481
|
-
|
|
613
|
+
} catch (err) {
|
|
614
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
615
|
+
logger.warn(` Failed to fetch latest from origin: ${msg}`);
|
|
482
616
|
return "error";
|
|
483
617
|
}
|
|
484
618
|
try {
|
|
@@ -493,7 +627,8 @@ function mergeDefault(cwd) {
|
|
|
493
627
|
}
|
|
494
628
|
try {
|
|
495
629
|
git(["merge", "--abort"], { cwd });
|
|
496
|
-
} catch {
|
|
630
|
+
} catch (abortErr) {
|
|
631
|
+
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
497
632
|
}
|
|
498
633
|
return "error";
|
|
499
634
|
}
|
|
@@ -538,7 +673,21 @@ var init_git_utils = __esm({
|
|
|
538
673
|
});
|
|
539
674
|
|
|
540
675
|
// src/github-api.ts
|
|
541
|
-
import { execFileSync as
|
|
676
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
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
|
+
}
|
|
542
691
|
function setGhCwd(cwd) {
|
|
543
692
|
_ghCwd = cwd;
|
|
544
693
|
}
|
|
@@ -548,7 +697,7 @@ function ghToken() {
|
|
|
548
697
|
function gh(args2, options) {
|
|
549
698
|
const token = ghToken();
|
|
550
699
|
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
551
|
-
return
|
|
700
|
+
return execFileSync8("gh", args2, {
|
|
552
701
|
encoding: "utf-8",
|
|
553
702
|
timeout: API_TIMEOUT_MS,
|
|
554
703
|
cwd: _ghCwd,
|
|
@@ -566,9 +715,18 @@ function getIssue(issueNumber) {
|
|
|
566
715
|
"--json",
|
|
567
716
|
"body,title"
|
|
568
717
|
]);
|
|
569
|
-
|
|
718
|
+
const parsed = JSON.parse(output);
|
|
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 };
|
|
570
724
|
} catch (err) {
|
|
571
|
-
|
|
725
|
+
if (isNotFoundError(err)) {
|
|
726
|
+
logger.info(` Issue #${issueNumber} not found`);
|
|
727
|
+
} else {
|
|
728
|
+
logger.error(` Failed to get issue #${issueNumber}: ${ghErrorMessage(err)}`);
|
|
729
|
+
}
|
|
572
730
|
return null;
|
|
573
731
|
}
|
|
574
732
|
}
|
|
@@ -609,8 +767,15 @@ function getPRForBranch(branch) {
|
|
|
609
767
|
"number,url"
|
|
610
768
|
]);
|
|
611
769
|
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
|
+
}
|
|
612
774
|
return { number: data.number, url: data.url };
|
|
613
|
-
} catch {
|
|
775
|
+
} catch (err) {
|
|
776
|
+
if (!isNotFoundError(err)) {
|
|
777
|
+
logger.warn(` Failed to check PR for branch ${branch}: ${ghErrorMessage(err)}`);
|
|
778
|
+
}
|
|
614
779
|
return null;
|
|
615
780
|
}
|
|
616
781
|
}
|
|
@@ -648,8 +813,7 @@ function createPR(head, base, title, body) {
|
|
|
648
813
|
logger.info(` PR created: ${url}`);
|
|
649
814
|
return { number, url };
|
|
650
815
|
} catch (err) {
|
|
651
|
-
const
|
|
652
|
-
const reason = stderr || (err instanceof Error ? err.message : String(err));
|
|
816
|
+
const reason = ghErrorMessage(err);
|
|
653
817
|
logger.error(` Failed to create PR: ${reason}`);
|
|
654
818
|
return null;
|
|
655
819
|
}
|
|
@@ -720,9 +884,22 @@ function getPRDetails(prNumber) {
|
|
|
720
884
|
"title,body,headRefName,baseRefName"
|
|
721
885
|
]);
|
|
722
886
|
const data = JSON.parse(output);
|
|
723
|
-
|
|
887
|
+
if (typeof data.title !== "string" || typeof data.headRefName !== "string") {
|
|
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
|
+
};
|
|
724
897
|
} catch (err) {
|
|
725
|
-
|
|
898
|
+
if (isNotFoundError(err)) {
|
|
899
|
+
logger.info(` PR #${prNumber} not found`);
|
|
900
|
+
} else {
|
|
901
|
+
logger.error(` Failed to get PR #${prNumber}: ${ghErrorMessage(err)}`);
|
|
902
|
+
}
|
|
726
903
|
return null;
|
|
727
904
|
}
|
|
728
905
|
}
|
|
@@ -764,7 +941,7 @@ function getCIFailureLogs(runId, maxLength = 8e3) {
|
|
|
764
941
|
const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
|
|
765
942
|
return `${prefix}${truncated}`;
|
|
766
943
|
} catch (err) {
|
|
767
|
-
logger.warn(` Failed to get CI failure logs for run ${runId}: ${err}`);
|
|
944
|
+
logger.warn(` Failed to get CI failure logs for run ${runId}: ${ghErrorMessage(err)}`);
|
|
768
945
|
return null;
|
|
769
946
|
}
|
|
770
947
|
}
|
|
@@ -786,7 +963,7 @@ function getLatestFailedRunForBranch(branch) {
|
|
|
786
963
|
]);
|
|
787
964
|
return output.trim() || null;
|
|
788
965
|
} catch (err) {
|
|
789
|
-
logger.warn(` Failed to get latest failed run for branch ${branch}: ${err}`);
|
|
966
|
+
logger.warn(` Failed to get latest failed run for branch ${branch}: ${ghErrorMessage(err)}`);
|
|
790
967
|
return null;
|
|
791
968
|
}
|
|
792
969
|
}
|
|
@@ -882,15 +1059,22 @@ var init_github_api = __esm({
|
|
|
882
1059
|
});
|
|
883
1060
|
|
|
884
1061
|
// src/pipeline/state.ts
|
|
885
|
-
import * as
|
|
886
|
-
import * as
|
|
1062
|
+
import * as fs9 from "fs";
|
|
1063
|
+
import * as path8 from "path";
|
|
887
1064
|
function loadState(taskId, taskDir) {
|
|
888
|
-
const p =
|
|
889
|
-
if (!
|
|
1065
|
+
const p = path8.join(taskDir, "status.json");
|
|
1066
|
+
if (!fs9.existsSync(p)) return null;
|
|
890
1067
|
try {
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
1068
|
+
const result = parseJsonSafe(
|
|
1069
|
+
fs9.readFileSync(p, "utf-8"),
|
|
1070
|
+
["taskId", "state", "stages", "createdAt", "updatedAt"]
|
|
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;
|
|
894
1078
|
} catch {
|
|
895
1079
|
return null;
|
|
896
1080
|
}
|
|
@@ -900,11 +1084,11 @@ function writeState(state, taskDir) {
|
|
|
900
1084
|
...state,
|
|
901
1085
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
902
1086
|
};
|
|
903
|
-
const target =
|
|
1087
|
+
const target = path8.join(taskDir, "status.json");
|
|
904
1088
|
const tmp = target + ".tmp";
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1089
|
+
fs9.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
1090
|
+
fs9.renameSync(tmp, target);
|
|
1091
|
+
return updated;
|
|
908
1092
|
}
|
|
909
1093
|
function initState(taskId) {
|
|
910
1094
|
const stages = {};
|
|
@@ -918,6 +1102,8 @@ var init_state = __esm({
|
|
|
918
1102
|
"src/pipeline/state.ts"() {
|
|
919
1103
|
"use strict";
|
|
920
1104
|
init_definitions();
|
|
1105
|
+
init_validators();
|
|
1106
|
+
init_logger();
|
|
921
1107
|
}
|
|
922
1108
|
});
|
|
923
1109
|
|
|
@@ -942,16 +1128,16 @@ var init_complexity = __esm({
|
|
|
942
1128
|
});
|
|
943
1129
|
|
|
944
1130
|
// src/memory.ts
|
|
945
|
-
import * as
|
|
946
|
-
import * as
|
|
1131
|
+
import * as fs10 from "fs";
|
|
1132
|
+
import * as path9 from "path";
|
|
947
1133
|
function readProjectMemory(projectDir) {
|
|
948
|
-
const memoryDir =
|
|
949
|
-
if (!
|
|
950
|
-
const files =
|
|
1134
|
+
const memoryDir = path9.join(projectDir, ".kody", "memory");
|
|
1135
|
+
if (!fs10.existsSync(memoryDir)) return "";
|
|
1136
|
+
const files = fs10.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
951
1137
|
if (files.length === 0) return "";
|
|
952
1138
|
const sections = [];
|
|
953
1139
|
for (const file of files) {
|
|
954
|
-
const content =
|
|
1140
|
+
const content = fs10.readFileSync(path9.join(memoryDir, file), "utf-8").trim();
|
|
955
1141
|
if (content) {
|
|
956
1142
|
sections.push(`## ${file.replace(".md", "")}
|
|
957
1143
|
${content}`);
|
|
@@ -970,15 +1156,11 @@ var init_memory = __esm({
|
|
|
970
1156
|
});
|
|
971
1157
|
|
|
972
1158
|
// src/context-tiers.ts
|
|
973
|
-
import * as
|
|
974
|
-
import * as
|
|
975
|
-
import * as crypto2 from "crypto";
|
|
1159
|
+
import * as fs11 from "fs";
|
|
1160
|
+
import * as path10 from "path";
|
|
976
1161
|
function estimateTokens(text) {
|
|
977
1162
|
return Math.ceil(text.length / 4);
|
|
978
1163
|
}
|
|
979
|
-
function contentHash(content) {
|
|
980
|
-
return crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
981
|
-
}
|
|
982
1164
|
function resolveStagePolicy(stageName, stageOverrides) {
|
|
983
1165
|
const defaults = DEFAULT_STAGE_POLICIES[stageName] ?? DEFAULT_STAGE_POLICIES.build;
|
|
984
1166
|
const overrides = stageOverrides?.[stageName];
|
|
@@ -1065,61 +1247,30 @@ function generateL1Json(content) {
|
|
|
1065
1247
|
return content.slice(0, L1_MAX_CHARS);
|
|
1066
1248
|
}
|
|
1067
1249
|
}
|
|
1068
|
-
function
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
try {
|
|
1072
|
-
return JSON.parse(fs4.readFileSync(cachePath, "utf-8"));
|
|
1073
|
-
} catch {
|
|
1074
|
-
return { version: 1, entries: {} };
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
function writeCache(cacheDir, cache) {
|
|
1078
|
-
fs4.mkdirSync(cacheDir, { recursive: true });
|
|
1079
|
-
fs4.writeFileSync(path4.join(cacheDir, "tier-cache.json"), JSON.stringify(cache, null, 2));
|
|
1080
|
-
}
|
|
1081
|
-
function getTieredContent(filePath, content, cacheDir) {
|
|
1082
|
-
const hash = contentHash(content);
|
|
1083
|
-
const key = path4.basename(filePath);
|
|
1084
|
-
const cache = readCache(cacheDir);
|
|
1085
|
-
if (cache.entries[key] && cache.entries[key].hash === hash) {
|
|
1086
|
-
return cache.entries[key];
|
|
1087
|
-
}
|
|
1088
|
-
const tiered = {
|
|
1250
|
+
function getTieredContent(filePath, content) {
|
|
1251
|
+
const key = path10.basename(filePath);
|
|
1252
|
+
return {
|
|
1089
1253
|
source: filePath,
|
|
1090
|
-
hash,
|
|
1091
1254
|
L0: generateL0(content, key),
|
|
1092
1255
|
L1: generateL1(content, key),
|
|
1093
1256
|
L2: content
|
|
1094
1257
|
};
|
|
1095
|
-
cache.entries[key] = tiered;
|
|
1096
|
-
writeCache(cacheDir, cache);
|
|
1097
|
-
return tiered;
|
|
1098
|
-
}
|
|
1099
|
-
function invalidateCache(filePath, cacheDir) {
|
|
1100
|
-
const key = path4.basename(filePath);
|
|
1101
|
-
const cache = readCache(cacheDir);
|
|
1102
|
-
if (cache.entries[key]) {
|
|
1103
|
-
delete cache.entries[key];
|
|
1104
|
-
writeCache(cacheDir, cache);
|
|
1105
|
-
}
|
|
1106
1258
|
}
|
|
1107
1259
|
function selectTier(tiered, tier) {
|
|
1108
1260
|
return tiered[tier];
|
|
1109
1261
|
}
|
|
1110
1262
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
1111
|
-
const memoryDir =
|
|
1112
|
-
if (!
|
|
1113
|
-
const files =
|
|
1263
|
+
const memoryDir = path10.join(projectDir, ".kody", "memory");
|
|
1264
|
+
if (!fs11.existsSync(memoryDir)) return "";
|
|
1265
|
+
const files = fs11.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
1114
1266
|
if (files.length === 0) return "";
|
|
1115
|
-
const cacheDir = path4.join(memoryDir, ".tiers");
|
|
1116
1267
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
1117
1268
|
const sections = [];
|
|
1118
1269
|
for (const file of files) {
|
|
1119
|
-
const filePath =
|
|
1120
|
-
const content =
|
|
1270
|
+
const filePath = path10.join(memoryDir, file);
|
|
1271
|
+
const content = fs11.readFileSync(filePath, "utf-8").trim();
|
|
1121
1272
|
if (!content) continue;
|
|
1122
|
-
const tiered = getTieredContent(filePath, content
|
|
1273
|
+
const tiered = getTieredContent(filePath, content);
|
|
1123
1274
|
const selected = selectTier(tiered, tier);
|
|
1124
1275
|
if (selected) {
|
|
1125
1276
|
sections.push(`## ${file.replace(".md", "")}
|
|
@@ -1134,26 +1285,25 @@ ${sections.join("\n\n")}
|
|
|
1134
1285
|
`;
|
|
1135
1286
|
}
|
|
1136
1287
|
function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
1137
|
-
const cacheDir = path4.join(taskDir, ".tiers");
|
|
1138
1288
|
let context = `## Task Context
|
|
1139
1289
|
`;
|
|
1140
1290
|
context += `Task ID: ${taskId}
|
|
1141
1291
|
`;
|
|
1142
1292
|
context += `Task Directory: ${taskDir}
|
|
1143
1293
|
`;
|
|
1144
|
-
const taskMdPath =
|
|
1145
|
-
if (
|
|
1146
|
-
const content =
|
|
1147
|
-
const selected = selectContent(taskMdPath, content,
|
|
1294
|
+
const taskMdPath = path10.join(taskDir, "task.md");
|
|
1295
|
+
if (fs11.existsSync(taskMdPath)) {
|
|
1296
|
+
const content = fs11.readFileSync(taskMdPath, "utf-8");
|
|
1297
|
+
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
1148
1298
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
1149
1299
|
context += `
|
|
1150
1300
|
## ${label}
|
|
1151
1301
|
${selected}
|
|
1152
1302
|
`;
|
|
1153
1303
|
}
|
|
1154
|
-
const taskJsonPath =
|
|
1155
|
-
if (
|
|
1156
|
-
const content =
|
|
1304
|
+
const taskJsonPath = path10.join(taskDir, "task.json");
|
|
1305
|
+
if (fs11.existsSync(taskJsonPath)) {
|
|
1306
|
+
const content = fs11.readFileSync(taskJsonPath, "utf-8");
|
|
1157
1307
|
if (policy.taskClassification === "L2") {
|
|
1158
1308
|
try {
|
|
1159
1309
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -1169,7 +1319,7 @@ ${selected}
|
|
|
1169
1319
|
} catch {
|
|
1170
1320
|
}
|
|
1171
1321
|
} else {
|
|
1172
|
-
const selected = selectContent(taskJsonPath, content,
|
|
1322
|
+
const selected = selectContent(taskJsonPath, content, policy.taskClassification);
|
|
1173
1323
|
if (selected) {
|
|
1174
1324
|
const label = tierLabel("Task Classification", policy.taskClassification);
|
|
1175
1325
|
context += `
|
|
@@ -1179,30 +1329,30 @@ ${selected}
|
|
|
1179
1329
|
}
|
|
1180
1330
|
}
|
|
1181
1331
|
}
|
|
1182
|
-
const specPath =
|
|
1183
|
-
if (
|
|
1184
|
-
const content =
|
|
1185
|
-
const selected = selectContent(specPath, content,
|
|
1332
|
+
const specPath = path10.join(taskDir, "spec.md");
|
|
1333
|
+
if (fs11.existsSync(specPath)) {
|
|
1334
|
+
const content = fs11.readFileSync(specPath, "utf-8");
|
|
1335
|
+
const selected = selectContent(specPath, content, policy.spec);
|
|
1186
1336
|
const label = tierLabel("Spec", policy.spec);
|
|
1187
1337
|
context += `
|
|
1188
1338
|
## ${label}
|
|
1189
1339
|
${selected}
|
|
1190
1340
|
`;
|
|
1191
1341
|
}
|
|
1192
|
-
const planPath =
|
|
1193
|
-
if (
|
|
1194
|
-
const content =
|
|
1195
|
-
const selected = selectContent(planPath, content,
|
|
1342
|
+
const planPath = path10.join(taskDir, "plan.md");
|
|
1343
|
+
if (fs11.existsSync(planPath)) {
|
|
1344
|
+
const content = fs11.readFileSync(planPath, "utf-8");
|
|
1345
|
+
const selected = selectContent(planPath, content, policy.plan);
|
|
1196
1346
|
const label = tierLabel("Plan", policy.plan);
|
|
1197
1347
|
context += `
|
|
1198
1348
|
## ${label}
|
|
1199
1349
|
${selected}
|
|
1200
1350
|
`;
|
|
1201
1351
|
}
|
|
1202
|
-
const contextMdPath =
|
|
1203
|
-
if (
|
|
1204
|
-
const content =
|
|
1205
|
-
const selected = selectContent(contextMdPath, content,
|
|
1352
|
+
const contextMdPath = path10.join(taskDir, "context.md");
|
|
1353
|
+
if (fs11.existsSync(contextMdPath)) {
|
|
1354
|
+
const content = fs11.readFileSync(contextMdPath, "utf-8");
|
|
1355
|
+
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
1206
1356
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
1207
1357
|
context += `
|
|
1208
1358
|
## ${label}
|
|
@@ -1217,9 +1367,9 @@ ${feedback}
|
|
|
1217
1367
|
}
|
|
1218
1368
|
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
1219
1369
|
}
|
|
1220
|
-
function selectContent(filePath, content,
|
|
1370
|
+
function selectContent(filePath, content, tier) {
|
|
1221
1371
|
if (tier === "L2") return content;
|
|
1222
|
-
const tiered = getTieredContent(filePath, content
|
|
1372
|
+
const tiered = getTieredContent(filePath, content);
|
|
1223
1373
|
return selectTier(tiered, tier);
|
|
1224
1374
|
}
|
|
1225
1375
|
function tierLabel(sectionName, tier) {
|
|
@@ -1333,24 +1483,24 @@ var init_mcp_config = __esm({
|
|
|
1333
1483
|
});
|
|
1334
1484
|
|
|
1335
1485
|
// src/context.ts
|
|
1336
|
-
import * as
|
|
1337
|
-
import * as
|
|
1486
|
+
import * as fs12 from "fs";
|
|
1487
|
+
import * as path11 from "path";
|
|
1338
1488
|
function readPromptFile(stageName, projectDir) {
|
|
1339
1489
|
if (projectDir) {
|
|
1340
|
-
const stepFile =
|
|
1341
|
-
if (
|
|
1342
|
-
return
|
|
1490
|
+
const stepFile = path11.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
1491
|
+
if (fs12.existsSync(stepFile)) {
|
|
1492
|
+
return fs12.readFileSync(stepFile, "utf-8");
|
|
1343
1493
|
}
|
|
1344
1494
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
1345
1495
|
}
|
|
1346
1496
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
1347
1497
|
const candidates = [
|
|
1348
|
-
|
|
1349
|
-
|
|
1498
|
+
path11.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
1499
|
+
path11.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
1350
1500
|
];
|
|
1351
1501
|
for (const candidate of candidates) {
|
|
1352
|
-
if (
|
|
1353
|
-
return
|
|
1502
|
+
if (fs12.existsSync(candidate)) {
|
|
1503
|
+
return fs12.readFileSync(candidate, "utf-8");
|
|
1354
1504
|
}
|
|
1355
1505
|
}
|
|
1356
1506
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -1362,18 +1512,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
1362
1512
|
`;
|
|
1363
1513
|
context += `Task Directory: ${taskDir}
|
|
1364
1514
|
`;
|
|
1365
|
-
const taskMdPath =
|
|
1366
|
-
if (
|
|
1367
|
-
const taskMd =
|
|
1515
|
+
const taskMdPath = path11.join(taskDir, "task.md");
|
|
1516
|
+
if (fs12.existsSync(taskMdPath)) {
|
|
1517
|
+
const taskMd = fs12.readFileSync(taskMdPath, "utf-8");
|
|
1368
1518
|
context += `
|
|
1369
1519
|
## Task Description
|
|
1370
1520
|
${taskMd}
|
|
1371
1521
|
`;
|
|
1372
1522
|
}
|
|
1373
|
-
const taskJsonPath =
|
|
1374
|
-
if (
|
|
1523
|
+
const taskJsonPath = path11.join(taskDir, "task.json");
|
|
1524
|
+
if (fs12.existsSync(taskJsonPath)) {
|
|
1375
1525
|
try {
|
|
1376
|
-
const taskDef = JSON.parse(
|
|
1526
|
+
const taskDef = JSON.parse(fs12.readFileSync(taskJsonPath, "utf-8"));
|
|
1377
1527
|
context += `
|
|
1378
1528
|
## Task Classification
|
|
1379
1529
|
`;
|
|
@@ -1386,27 +1536,27 @@ ${taskMd}
|
|
|
1386
1536
|
} catch {
|
|
1387
1537
|
}
|
|
1388
1538
|
}
|
|
1389
|
-
const specPath =
|
|
1390
|
-
if (
|
|
1391
|
-
const spec =
|
|
1539
|
+
const specPath = path11.join(taskDir, "spec.md");
|
|
1540
|
+
if (fs12.existsSync(specPath)) {
|
|
1541
|
+
const spec = fs12.readFileSync(specPath, "utf-8");
|
|
1392
1542
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
1393
1543
|
context += `
|
|
1394
1544
|
## Spec Summary
|
|
1395
1545
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
1396
1546
|
`;
|
|
1397
1547
|
}
|
|
1398
|
-
const planPath =
|
|
1399
|
-
if (
|
|
1400
|
-
const plan =
|
|
1548
|
+
const planPath = path11.join(taskDir, "plan.md");
|
|
1549
|
+
if (fs12.existsSync(planPath)) {
|
|
1550
|
+
const plan = fs12.readFileSync(planPath, "utf-8");
|
|
1401
1551
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
1402
1552
|
context += `
|
|
1403
1553
|
## Plan Summary
|
|
1404
1554
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
1405
1555
|
`;
|
|
1406
1556
|
}
|
|
1407
|
-
const contextMdPath =
|
|
1408
|
-
if (
|
|
1409
|
-
const accumulated =
|
|
1557
|
+
const contextMdPath = path11.join(taskDir, "context.md");
|
|
1558
|
+
if (fs12.existsSync(contextMdPath)) {
|
|
1559
|
+
const accumulated = fs12.readFileSync(contextMdPath, "utf-8");
|
|
1410
1560
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
1411
1561
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
1412
1562
|
context += `
|
|
@@ -1423,10 +1573,10 @@ ${feedback}
|
|
|
1423
1573
|
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
1424
1574
|
}
|
|
1425
1575
|
function taskHasUI(taskDir) {
|
|
1426
|
-
const taskJsonPath =
|
|
1427
|
-
if (!
|
|
1576
|
+
const taskJsonPath = path11.join(taskDir, "task.json");
|
|
1577
|
+
if (!fs12.existsSync(taskJsonPath)) return true;
|
|
1428
1578
|
try {
|
|
1429
|
-
const taskDef = JSON.parse(
|
|
1579
|
+
const taskDef = JSON.parse(fs12.readFileSync(taskJsonPath, "utf-8"));
|
|
1430
1580
|
return taskDef.hasUI !== false;
|
|
1431
1581
|
} catch {
|
|
1432
1582
|
return true;
|
|
@@ -1546,9 +1696,9 @@ ${prompt}` : prompt;
|
|
|
1546
1696
|
}
|
|
1547
1697
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
1548
1698
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
1549
|
-
const qaGuidePath =
|
|
1550
|
-
if (
|
|
1551
|
-
const qaGuide =
|
|
1699
|
+
const qaGuidePath = path11.join(projectDir, ".kody", "qa-guide.md");
|
|
1700
|
+
if (fs12.existsSync(qaGuidePath)) {
|
|
1701
|
+
const qaGuide = fs12.readFileSync(qaGuidePath, "utf-8").trim();
|
|
1552
1702
|
assembled = assembled + "\n\n" + qaGuide;
|
|
1553
1703
|
}
|
|
1554
1704
|
}
|
|
@@ -1597,55 +1747,6 @@ var init_context = __esm({
|
|
|
1597
1747
|
}
|
|
1598
1748
|
});
|
|
1599
1749
|
|
|
1600
|
-
// src/validators.ts
|
|
1601
|
-
function stripFences(content) {
|
|
1602
|
-
return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1603
|
-
}
|
|
1604
|
-
function validateTaskJson(content) {
|
|
1605
|
-
try {
|
|
1606
|
-
const parsed = JSON.parse(stripFences(content));
|
|
1607
|
-
for (const field of REQUIRED_TASK_FIELDS) {
|
|
1608
|
-
if (!(field in parsed)) {
|
|
1609
|
-
return { valid: false, error: `Missing field: ${field}` };
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
return { valid: true };
|
|
1613
|
-
} catch (err) {
|
|
1614
|
-
return {
|
|
1615
|
-
valid: false,
|
|
1616
|
-
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
1617
|
-
};
|
|
1618
|
-
}
|
|
1619
|
-
}
|
|
1620
|
-
function validatePlanMd(content) {
|
|
1621
|
-
if (content.length < 10) {
|
|
1622
|
-
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
1623
|
-
}
|
|
1624
|
-
if (!/^##\s+\w+/m.test(content)) {
|
|
1625
|
-
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
1626
|
-
}
|
|
1627
|
-
return { valid: true };
|
|
1628
|
-
}
|
|
1629
|
-
function validateReviewMd(content) {
|
|
1630
|
-
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
1631
|
-
return { valid: true };
|
|
1632
|
-
}
|
|
1633
|
-
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
1634
|
-
}
|
|
1635
|
-
var REQUIRED_TASK_FIELDS;
|
|
1636
|
-
var init_validators = __esm({
|
|
1637
|
-
"src/validators.ts"() {
|
|
1638
|
-
"use strict";
|
|
1639
|
-
REQUIRED_TASK_FIELDS = [
|
|
1640
|
-
"task_type",
|
|
1641
|
-
"title",
|
|
1642
|
-
"description",
|
|
1643
|
-
"scope",
|
|
1644
|
-
"risk_level"
|
|
1645
|
-
];
|
|
1646
|
-
}
|
|
1647
|
-
});
|
|
1648
|
-
|
|
1649
1750
|
// src/pipeline/runner-selection.ts
|
|
1650
1751
|
function getRunnerForStage(ctx, stageName) {
|
|
1651
1752
|
const config = getProjectConfig();
|
|
@@ -1666,8 +1767,8 @@ var init_runner_selection = __esm({
|
|
|
1666
1767
|
});
|
|
1667
1768
|
|
|
1668
1769
|
// src/stages/agent.ts
|
|
1669
|
-
import * as
|
|
1670
|
-
import * as
|
|
1770
|
+
import * as fs13 from "fs";
|
|
1771
|
+
import * as path12 from "path";
|
|
1671
1772
|
function getSessionInfo(stageName, sessions) {
|
|
1672
1773
|
const group = SESSION_GROUP[stageName];
|
|
1673
1774
|
if (!group) return void 0;
|
|
@@ -1729,27 +1830,27 @@ async function executeAgentStage(ctx, def) {
|
|
|
1729
1830
|
return { outcome: result.outcome, error: result.error, retries: 0 };
|
|
1730
1831
|
}
|
|
1731
1832
|
if (def.outputFile && result.output) {
|
|
1732
|
-
|
|
1833
|
+
fs13.writeFileSync(path12.join(ctx.taskDir, def.outputFile), result.output);
|
|
1733
1834
|
}
|
|
1734
1835
|
if (def.outputFile) {
|
|
1735
|
-
const outputPath =
|
|
1736
|
-
if (!
|
|
1737
|
-
const ext =
|
|
1738
|
-
const base =
|
|
1739
|
-
const files =
|
|
1836
|
+
const outputPath = path12.join(ctx.taskDir, def.outputFile);
|
|
1837
|
+
if (!fs13.existsSync(outputPath)) {
|
|
1838
|
+
const ext = path12.extname(def.outputFile);
|
|
1839
|
+
const base = path12.basename(def.outputFile, ext);
|
|
1840
|
+
const files = fs13.readdirSync(ctx.taskDir);
|
|
1740
1841
|
const variant = files.find(
|
|
1741
1842
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
1742
1843
|
);
|
|
1743
1844
|
if (variant) {
|
|
1744
|
-
|
|
1845
|
+
fs13.renameSync(path12.join(ctx.taskDir, variant), outputPath);
|
|
1745
1846
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
1746
1847
|
}
|
|
1747
1848
|
}
|
|
1748
1849
|
}
|
|
1749
1850
|
if (def.outputFile) {
|
|
1750
|
-
const outputPath =
|
|
1751
|
-
if (
|
|
1752
|
-
const content =
|
|
1851
|
+
const outputPath = path12.join(ctx.taskDir, def.outputFile);
|
|
1852
|
+
if (fs13.existsSync(outputPath)) {
|
|
1853
|
+
const content = fs13.readFileSync(outputPath, "utf-8");
|
|
1753
1854
|
const validation = validateStageOutput(def.name, content);
|
|
1754
1855
|
if (!validation.valid) {
|
|
1755
1856
|
if (def.name === "taskify") {
|
|
@@ -1763,7 +1864,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
1763
1864
|
const stripped = stripFences(retryResult.output);
|
|
1764
1865
|
const retryValidation = validateTaskJson(stripped);
|
|
1765
1866
|
if (retryValidation.valid) {
|
|
1766
|
-
|
|
1867
|
+
fs13.writeFileSync(outputPath, retryResult.output);
|
|
1767
1868
|
logger.info(` taskify retry produced valid JSON`);
|
|
1768
1869
|
} else {
|
|
1769
1870
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -1777,7 +1878,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
1777
1878
|
hasUI: true,
|
|
1778
1879
|
questions: []
|
|
1779
1880
|
}, null, 2);
|
|
1780
|
-
|
|
1881
|
+
fs13.writeFileSync(outputPath, fallback);
|
|
1781
1882
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
1782
1883
|
}
|
|
1783
1884
|
}
|
|
@@ -1791,7 +1892,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
1791
1892
|
return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
|
|
1792
1893
|
}
|
|
1793
1894
|
function appendStageContext(taskDir, stageName, output) {
|
|
1794
|
-
const contextPath =
|
|
1895
|
+
const contextPath = path12.join(taskDir, "context.md");
|
|
1795
1896
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
1796
1897
|
let summary;
|
|
1797
1898
|
if (output && output.trim()) {
|
|
@@ -1804,7 +1905,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
1804
1905
|
### ${stageName} (${timestamp2})
|
|
1805
1906
|
${summary}
|
|
1806
1907
|
`;
|
|
1807
|
-
|
|
1908
|
+
fs13.appendFileSync(contextPath, entry);
|
|
1808
1909
|
}
|
|
1809
1910
|
var SESSION_GROUP;
|
|
1810
1911
|
var init_agent = __esm({
|
|
@@ -1827,7 +1928,7 @@ var init_agent = __esm({
|
|
|
1827
1928
|
});
|
|
1828
1929
|
|
|
1829
1930
|
// src/verify-runner.ts
|
|
1830
|
-
import { execFileSync as
|
|
1931
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
1831
1932
|
function isExecError(err) {
|
|
1832
1933
|
return typeof err === "object" && err !== null;
|
|
1833
1934
|
}
|
|
@@ -1863,7 +1964,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
1863
1964
|
return { success: true, output: "", timedOut: false };
|
|
1864
1965
|
}
|
|
1865
1966
|
try {
|
|
1866
|
-
const output =
|
|
1967
|
+
const output = execFileSync9(parts[0], parts.slice(1), {
|
|
1867
1968
|
cwd,
|
|
1868
1969
|
timeout,
|
|
1869
1970
|
encoding: "utf-8",
|
|
@@ -1934,7 +2035,7 @@ var init_verify_runner = __esm({
|
|
|
1934
2035
|
});
|
|
1935
2036
|
|
|
1936
2037
|
// src/observer.ts
|
|
1937
|
-
import { execFileSync as
|
|
2038
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
1938
2039
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
1939
2040
|
const context = [
|
|
1940
2041
|
`Stage: ${stageName}`,
|
|
@@ -1959,42 +2060,48 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
1959
2060
|
);
|
|
1960
2061
|
if (result.outcome === "completed" && result.output) {
|
|
1961
2062
|
const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
1962
|
-
const
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2063
|
+
const parseResult = parseJsonSafe(cleaned, ["classification"]);
|
|
2064
|
+
if (parseResult.ok) {
|
|
2065
|
+
const { data } = parseResult;
|
|
2066
|
+
const validClassifications = [
|
|
2067
|
+
"fixable",
|
|
2068
|
+
"infrastructure",
|
|
2069
|
+
"pre-existing",
|
|
2070
|
+
"retry",
|
|
2071
|
+
"abort"
|
|
2072
|
+
];
|
|
2073
|
+
if (validClassifications.includes(data.classification)) {
|
|
2074
|
+
logger.info(` Diagnosis: ${data.classification} \u2014 ${data.reason}`);
|
|
2075
|
+
return {
|
|
2076
|
+
classification: data.classification,
|
|
2077
|
+
reason: data.reason ?? "Unknown reason",
|
|
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}`);
|
|
1977
2084
|
}
|
|
1978
2085
|
}
|
|
1979
2086
|
} catch (err) {
|
|
1980
2087
|
logger.warn(` Diagnosis error: ${err instanceof Error ? err.message : err}`);
|
|
1981
2088
|
}
|
|
1982
|
-
logger.warn(" Diagnosis failed \u2014 defaulting to
|
|
2089
|
+
logger.warn(" Diagnosis failed \u2014 defaulting to retry");
|
|
1983
2090
|
return {
|
|
1984
|
-
classification: "
|
|
1985
|
-
reason: "Could not diagnose failure",
|
|
2091
|
+
classification: "retry",
|
|
2092
|
+
reason: "Could not diagnose failure \u2014 retrying gate",
|
|
1986
2093
|
resolution: errorOutput.slice(-500)
|
|
1987
2094
|
};
|
|
1988
2095
|
}
|
|
1989
2096
|
function getModifiedFiles(projectDir) {
|
|
1990
2097
|
try {
|
|
1991
|
-
const staged =
|
|
2098
|
+
const staged = execFileSync10("git", ["diff", "--name-only", "--cached"], {
|
|
1992
2099
|
encoding: "utf-8",
|
|
1993
2100
|
cwd: projectDir,
|
|
1994
2101
|
timeout: 5e3,
|
|
1995
2102
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1996
2103
|
}).trim();
|
|
1997
|
-
const unstaged =
|
|
2104
|
+
const unstaged = execFileSync10("git", ["diff", "--name-only"], {
|
|
1998
2105
|
encoding: "utf-8",
|
|
1999
2106
|
cwd: projectDir,
|
|
2000
2107
|
timeout: 5e3,
|
|
@@ -2003,7 +2110,8 @@ function getModifiedFiles(projectDir) {
|
|
|
2003
2110
|
const all = `${staged}
|
|
2004
2111
|
${unstaged}`.split("\n").filter(Boolean);
|
|
2005
2112
|
return [...new Set(all)];
|
|
2006
|
-
} catch {
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
logger.warn(` Failed to get modified files: ${err instanceof Error ? err.message : String(err)}`);
|
|
2007
2115
|
return [];
|
|
2008
2116
|
}
|
|
2009
2117
|
}
|
|
@@ -2012,6 +2120,7 @@ var init_observer = __esm({
|
|
|
2012
2120
|
"src/observer.ts"() {
|
|
2013
2121
|
"use strict";
|
|
2014
2122
|
init_logger();
|
|
2123
|
+
init_validators();
|
|
2015
2124
|
DIAGNOSIS_PROMPT = `You are a pipeline failure diagnosis agent. Analyze the error and classify it.
|
|
2016
2125
|
|
|
2017
2126
|
Output ONLY valid JSON. No markdown fences. No explanation.
|
|
@@ -2035,8 +2144,8 @@ Error context:
|
|
|
2035
2144
|
});
|
|
2036
2145
|
|
|
2037
2146
|
// src/stages/gate.ts
|
|
2038
|
-
import * as
|
|
2039
|
-
import * as
|
|
2147
|
+
import * as fs14 from "fs";
|
|
2148
|
+
import * as path13 from "path";
|
|
2040
2149
|
function executeGateStage(ctx, def) {
|
|
2041
2150
|
if (ctx.input.dryRun) {
|
|
2042
2151
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -2079,7 +2188,7 @@ ${output}
|
|
|
2079
2188
|
`);
|
|
2080
2189
|
}
|
|
2081
2190
|
}
|
|
2082
|
-
|
|
2191
|
+
fs14.writeFileSync(path13.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
2083
2192
|
return {
|
|
2084
2193
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
2085
2194
|
retries: 0
|
|
@@ -2094,9 +2203,9 @@ var init_gate = __esm({
|
|
|
2094
2203
|
});
|
|
2095
2204
|
|
|
2096
2205
|
// src/stages/verify.ts
|
|
2097
|
-
import * as
|
|
2098
|
-
import * as
|
|
2099
|
-
import { execFileSync as
|
|
2206
|
+
import * as fs15 from "fs";
|
|
2207
|
+
import * as path14 from "path";
|
|
2208
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
2100
2209
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
2101
2210
|
const maxAttempts = def.maxRetries ?? 2;
|
|
2102
2211
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -2106,8 +2215,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
2106
2215
|
return { ...gateResult, retries: attempt };
|
|
2107
2216
|
}
|
|
2108
2217
|
if (attempt < maxAttempts) {
|
|
2109
|
-
const verifyPath =
|
|
2110
|
-
const errorOutput =
|
|
2218
|
+
const verifyPath = path14.join(ctx.taskDir, "verify.md");
|
|
2219
|
+
const errorOutput = fs15.existsSync(verifyPath) ? fs15.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
2111
2220
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
2112
2221
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
2113
2222
|
const diagConfig = getProjectConfig();
|
|
@@ -2150,7 +2259,7 @@ ${diagnosis.resolution}`);
|
|
|
2150
2259
|
const parts = parseCommand(cmd);
|
|
2151
2260
|
if (parts.length === 0) return;
|
|
2152
2261
|
try {
|
|
2153
|
-
|
|
2262
|
+
execFileSync11(parts[0], parts.slice(1), {
|
|
2154
2263
|
stdio: "pipe",
|
|
2155
2264
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
2156
2265
|
});
|
|
@@ -2203,18 +2312,18 @@ var init_verify = __esm({
|
|
|
2203
2312
|
});
|
|
2204
2313
|
|
|
2205
2314
|
// src/cli/task-resolution.ts
|
|
2206
|
-
import * as
|
|
2207
|
-
import * as
|
|
2208
|
-
import { execFileSync as
|
|
2315
|
+
import * as fs16 from "fs";
|
|
2316
|
+
import * as path15 from "path";
|
|
2317
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
2209
2318
|
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
2210
|
-
const tasksDir =
|
|
2211
|
-
if (!
|
|
2212
|
-
const allDirs =
|
|
2319
|
+
const tasksDir = path15.join(projectDir, ".kody", "tasks");
|
|
2320
|
+
if (!fs16.existsSync(tasksDir)) return null;
|
|
2321
|
+
const allDirs = fs16.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
2213
2322
|
const prefix = `${issueNumber}-`;
|
|
2214
2323
|
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
2215
2324
|
if (direct) return direct;
|
|
2216
2325
|
try {
|
|
2217
|
-
const branch =
|
|
2326
|
+
const branch = execFileSync12("git", ["branch", "--show-current"], {
|
|
2218
2327
|
encoding: "utf-8",
|
|
2219
2328
|
cwd: projectDir,
|
|
2220
2329
|
timeout: 5e3,
|
|
@@ -2243,8 +2352,8 @@ var init_task_resolution = __esm({
|
|
|
2243
2352
|
});
|
|
2244
2353
|
|
|
2245
2354
|
// src/review-standalone.ts
|
|
2246
|
-
import * as
|
|
2247
|
-
import * as
|
|
2355
|
+
import * as fs17 from "fs";
|
|
2356
|
+
import * as path16 from "path";
|
|
2248
2357
|
function resolveReviewTarget(input) {
|
|
2249
2358
|
if (input.prs.length === 0) {
|
|
2250
2359
|
return {
|
|
@@ -2268,8 +2377,8 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
2268
2377
|
}
|
|
2269
2378
|
async function runStandaloneReview(input) {
|
|
2270
2379
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
2271
|
-
const taskDir =
|
|
2272
|
-
|
|
2380
|
+
const taskDir = path16.join(input.projectDir, ".kody", "tasks", taskId);
|
|
2381
|
+
fs17.mkdirSync(taskDir, { recursive: true });
|
|
2273
2382
|
const diffInstruction = input.baseBranch ? `
|
|
2274
2383
|
|
|
2275
2384
|
## Diff Command
|
|
@@ -2278,7 +2387,7 @@ Do NOT use bare \`git diff\` \u2014 it shows only uncommitted working tree chang
|
|
|
2278
2387
|
const taskContent = `# ${input.prTitle}
|
|
2279
2388
|
|
|
2280
2389
|
${input.prBody ?? ""}${diffInstruction}`;
|
|
2281
|
-
|
|
2390
|
+
fs17.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
|
|
2282
2391
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
2283
2392
|
const ctx = {
|
|
2284
2393
|
taskId,
|
|
@@ -2300,10 +2409,10 @@ ${input.prBody ?? ""}${diffInstruction}`;
|
|
|
2300
2409
|
error: result.error ?? "Review stage failed"
|
|
2301
2410
|
};
|
|
2302
2411
|
}
|
|
2303
|
-
const reviewPath =
|
|
2412
|
+
const reviewPath = path16.join(taskDir, "review.md");
|
|
2304
2413
|
let reviewContent;
|
|
2305
|
-
if (
|
|
2306
|
-
reviewContent =
|
|
2414
|
+
if (fs17.existsSync(reviewPath)) {
|
|
2415
|
+
reviewContent = fs17.readFileSync(reviewPath, "utf-8");
|
|
2307
2416
|
}
|
|
2308
2417
|
return {
|
|
2309
2418
|
outcome: "completed",
|
|
@@ -2342,8 +2451,8 @@ var init_review_standalone = __esm({
|
|
|
2342
2451
|
});
|
|
2343
2452
|
|
|
2344
2453
|
// src/stages/review.ts
|
|
2345
|
-
import * as
|
|
2346
|
-
import * as
|
|
2454
|
+
import * as fs18 from "fs";
|
|
2455
|
+
import * as path17 from "path";
|
|
2347
2456
|
async function executeReviewWithFix(ctx, def) {
|
|
2348
2457
|
if (ctx.input.dryRun) {
|
|
2349
2458
|
return { outcome: "completed", retries: 0 };
|
|
@@ -2357,11 +2466,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
2357
2466
|
if (reviewResult.outcome !== "completed") {
|
|
2358
2467
|
return reviewResult;
|
|
2359
2468
|
}
|
|
2360
|
-
const reviewFile =
|
|
2361
|
-
if (!
|
|
2469
|
+
const reviewFile = path17.join(ctx.taskDir, "review.md");
|
|
2470
|
+
if (!fs18.existsSync(reviewFile)) {
|
|
2362
2471
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
2363
2472
|
}
|
|
2364
|
-
const content =
|
|
2473
|
+
const content = fs18.readFileSync(reviewFile, "utf-8");
|
|
2365
2474
|
if (detectReviewVerdict(content) !== "fail") {
|
|
2366
2475
|
return { ...reviewResult, retries: iteration };
|
|
2367
2476
|
}
|
|
@@ -2390,15 +2499,15 @@ var init_review = __esm({
|
|
|
2390
2499
|
});
|
|
2391
2500
|
|
|
2392
2501
|
// src/stages/ship.ts
|
|
2393
|
-
import * as
|
|
2394
|
-
import * as
|
|
2395
|
-
import { execFileSync as
|
|
2502
|
+
import * as fs19 from "fs";
|
|
2503
|
+
import * as path18 from "path";
|
|
2504
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
2396
2505
|
function buildPrBody(ctx) {
|
|
2397
2506
|
const sections = [];
|
|
2398
|
-
const taskJsonPath =
|
|
2399
|
-
if (
|
|
2507
|
+
const taskJsonPath = path18.join(ctx.taskDir, "task.json");
|
|
2508
|
+
if (fs19.existsSync(taskJsonPath)) {
|
|
2400
2509
|
try {
|
|
2401
|
-
const raw =
|
|
2510
|
+
const raw = fs19.readFileSync(taskJsonPath, "utf-8");
|
|
2402
2511
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2403
2512
|
const task = JSON.parse(cleaned);
|
|
2404
2513
|
if (task.description) {
|
|
@@ -2417,9 +2526,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
2417
2526
|
} catch {
|
|
2418
2527
|
}
|
|
2419
2528
|
}
|
|
2420
|
-
const reviewPath =
|
|
2421
|
-
if (
|
|
2422
|
-
const review =
|
|
2529
|
+
const reviewPath = path18.join(ctx.taskDir, "review.md");
|
|
2530
|
+
if (fs19.existsSync(reviewPath)) {
|
|
2531
|
+
const review = fs19.readFileSync(reviewPath, "utf-8");
|
|
2423
2532
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
2424
2533
|
if (summaryMatch) {
|
|
2425
2534
|
const summary = summaryMatch[1].trim();
|
|
@@ -2436,14 +2545,14 @@ ${summary}`);
|
|
|
2436
2545
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
2437
2546
|
}
|
|
2438
2547
|
}
|
|
2439
|
-
const verifyPath =
|
|
2440
|
-
if (
|
|
2441
|
-
const verify =
|
|
2548
|
+
const verifyPath = path18.join(ctx.taskDir, "verify.md");
|
|
2549
|
+
if (fs19.existsSync(verifyPath)) {
|
|
2550
|
+
const verify = fs19.readFileSync(verifyPath, "utf-8");
|
|
2442
2551
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
2443
2552
|
}
|
|
2444
|
-
const planPath =
|
|
2445
|
-
if (
|
|
2446
|
-
const plan =
|
|
2553
|
+
const planPath = path18.join(ctx.taskDir, "plan.md");
|
|
2554
|
+
if (fs19.existsSync(planPath)) {
|
|
2555
|
+
const plan = fs19.readFileSync(planPath, "utf-8").trim();
|
|
2447
2556
|
if (plan) {
|
|
2448
2557
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
2449
2558
|
sections.push(`
|
|
@@ -2463,25 +2572,25 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
2463
2572
|
return sections.join("\n");
|
|
2464
2573
|
}
|
|
2465
2574
|
function executeShipStage(ctx, _def) {
|
|
2466
|
-
const shipPath =
|
|
2575
|
+
const shipPath = path18.join(ctx.taskDir, "ship.md");
|
|
2467
2576
|
if (ctx.input.dryRun) {
|
|
2468
|
-
|
|
2577
|
+
fs19.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
2469
2578
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2470
2579
|
}
|
|
2471
2580
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
2472
|
-
|
|
2581
|
+
fs19.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
2473
2582
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2474
2583
|
}
|
|
2475
2584
|
try {
|
|
2476
2585
|
const head = getCurrentBranch(ctx.projectDir);
|
|
2477
2586
|
const base = getDefaultBranch(ctx.projectDir);
|
|
2478
2587
|
try {
|
|
2479
|
-
|
|
2588
|
+
execFileSync13("git", ["add", ctx.taskDir], {
|
|
2480
2589
|
cwd: ctx.projectDir,
|
|
2481
2590
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2482
2591
|
stdio: "pipe"
|
|
2483
2592
|
});
|
|
2484
|
-
|
|
2593
|
+
execFileSync13("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
2485
2594
|
cwd: ctx.projectDir,
|
|
2486
2595
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2487
2596
|
stdio: "pipe"
|
|
@@ -2495,7 +2604,7 @@ function executeShipStage(ctx, _def) {
|
|
|
2495
2604
|
let repo = config.github?.repo;
|
|
2496
2605
|
if (!owner || !repo) {
|
|
2497
2606
|
try {
|
|
2498
|
-
const remoteUrl =
|
|
2607
|
+
const remoteUrl = execFileSync13("git", ["remote", "get-url", "origin"], {
|
|
2499
2608
|
encoding: "utf-8",
|
|
2500
2609
|
cwd: ctx.projectDir
|
|
2501
2610
|
}).trim();
|
|
@@ -2516,28 +2625,28 @@ function executeShipStage(ctx, _def) {
|
|
|
2516
2625
|
chore: "chore"
|
|
2517
2626
|
};
|
|
2518
2627
|
let prefix = "chore";
|
|
2519
|
-
const taskJsonPath =
|
|
2520
|
-
if (
|
|
2628
|
+
const taskJsonPath = path18.join(ctx.taskDir, "task.json");
|
|
2629
|
+
if (fs19.existsSync(taskJsonPath)) {
|
|
2521
2630
|
try {
|
|
2522
|
-
const raw =
|
|
2631
|
+
const raw = fs19.readFileSync(taskJsonPath, "utf-8");
|
|
2523
2632
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2524
2633
|
const task = JSON.parse(cleaned);
|
|
2525
2634
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
2526
2635
|
} catch {
|
|
2527
2636
|
}
|
|
2528
2637
|
}
|
|
2529
|
-
const taskMdPath =
|
|
2530
|
-
if (
|
|
2531
|
-
const content =
|
|
2638
|
+
const taskMdPath = path18.join(ctx.taskDir, "task.md");
|
|
2639
|
+
if (fs19.existsSync(taskMdPath)) {
|
|
2640
|
+
const content = fs19.readFileSync(taskMdPath, "utf-8");
|
|
2532
2641
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
2533
2642
|
if (heading) {
|
|
2534
2643
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
2535
2644
|
}
|
|
2536
2645
|
}
|
|
2537
2646
|
if (title === "Update") {
|
|
2538
|
-
if (
|
|
2647
|
+
if (fs19.existsSync(taskJsonPath)) {
|
|
2539
2648
|
try {
|
|
2540
|
-
const raw =
|
|
2649
|
+
const raw = fs19.readFileSync(taskJsonPath, "utf-8");
|
|
2541
2650
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2542
2651
|
const task = JSON.parse(cleaned);
|
|
2543
2652
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -2560,7 +2669,7 @@ function executeShipStage(ctx, _def) {
|
|
|
2560
2669
|
} catch {
|
|
2561
2670
|
}
|
|
2562
2671
|
}
|
|
2563
|
-
|
|
2672
|
+
fs19.writeFileSync(shipPath, `# Ship
|
|
2564
2673
|
|
|
2565
2674
|
Updated existing PR: ${existingPr.url}
|
|
2566
2675
|
PR #${existingPr.number}
|
|
@@ -2581,22 +2690,26 @@ PR #${existingPr.number}
|
|
|
2581
2690
|
} catch {
|
|
2582
2691
|
}
|
|
2583
2692
|
}
|
|
2584
|
-
|
|
2693
|
+
fs19.writeFileSync(shipPath, `# Ship
|
|
2585
2694
|
|
|
2586
2695
|
PR created: ${pr.url}
|
|
2587
2696
|
PR #${pr.number}
|
|
2588
2697
|
`);
|
|
2589
2698
|
} else {
|
|
2590
|
-
|
|
2699
|
+
fs19.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
2591
2700
|
}
|
|
2592
2701
|
}
|
|
2593
2702
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2594
2703
|
} catch (err) {
|
|
2595
2704
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2596
|
-
|
|
2705
|
+
try {
|
|
2706
|
+
fs19.writeFileSync(shipPath, `# Ship
|
|
2597
2707
|
|
|
2598
2708
|
Failed: ${msg}
|
|
2599
2709
|
`);
|
|
2710
|
+
} catch {
|
|
2711
|
+
logger.warn(` Failed to write ship.md artifact`);
|
|
2712
|
+
}
|
|
2600
2713
|
return { outcome: "failed", retries: 0, error: msg };
|
|
2601
2714
|
}
|
|
2602
2715
|
}
|
|
@@ -2639,15 +2752,15 @@ var init_executor_registry = __esm({
|
|
|
2639
2752
|
});
|
|
2640
2753
|
|
|
2641
2754
|
// src/pipeline/questions.ts
|
|
2642
|
-
import * as
|
|
2643
|
-
import * as
|
|
2755
|
+
import * as fs20 from "fs";
|
|
2756
|
+
import * as path19 from "path";
|
|
2644
2757
|
function checkForQuestions(ctx, stageName) {
|
|
2645
2758
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
2646
2759
|
try {
|
|
2647
2760
|
if (stageName === "taskify") {
|
|
2648
|
-
const taskJsonPath =
|
|
2649
|
-
if (!
|
|
2650
|
-
const raw =
|
|
2761
|
+
const taskJsonPath = path19.join(ctx.taskDir, "task.json");
|
|
2762
|
+
if (!fs20.existsSync(taskJsonPath)) return false;
|
|
2763
|
+
const raw = fs20.readFileSync(taskJsonPath, "utf-8");
|
|
2651
2764
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2652
2765
|
const taskJson = JSON.parse(cleaned);
|
|
2653
2766
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -2662,9 +2775,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
2662
2775
|
}
|
|
2663
2776
|
}
|
|
2664
2777
|
if (stageName === "plan") {
|
|
2665
|
-
const planPath =
|
|
2666
|
-
if (!
|
|
2667
|
-
const plan =
|
|
2778
|
+
const planPath = path19.join(ctx.taskDir, "plan.md");
|
|
2779
|
+
if (!fs20.existsSync(planPath)) return false;
|
|
2780
|
+
const plan = fs20.readFileSync(planPath, "utf-8");
|
|
2668
2781
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
2669
2782
|
if (questionsMatch) {
|
|
2670
2783
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -2693,8 +2806,8 @@ var init_questions = __esm({
|
|
|
2693
2806
|
});
|
|
2694
2807
|
|
|
2695
2808
|
// src/pipeline/hooks.ts
|
|
2696
|
-
import * as
|
|
2697
|
-
import * as
|
|
2809
|
+
import * as fs21 from "fs";
|
|
2810
|
+
import * as path20 from "path";
|
|
2698
2811
|
function applyPreStageLabel(ctx, def) {
|
|
2699
2812
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
2700
2813
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
@@ -2732,9 +2845,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
2732
2845
|
return { complexity, activeStages };
|
|
2733
2846
|
}
|
|
2734
2847
|
try {
|
|
2735
|
-
const taskJsonPath =
|
|
2736
|
-
if (!
|
|
2737
|
-
const raw =
|
|
2848
|
+
const taskJsonPath = path20.join(ctx.taskDir, "task.json");
|
|
2849
|
+
if (!fs21.existsSync(taskJsonPath)) return null;
|
|
2850
|
+
const raw = fs21.readFileSync(taskJsonPath, "utf-8");
|
|
2738
2851
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2739
2852
|
const taskJson = JSON.parse(cleaned);
|
|
2740
2853
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -2764,8 +2877,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
2764
2877
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
2765
2878
|
if (ctx.input.mode === "rerun") return null;
|
|
2766
2879
|
if (!ctx.input.issueNumber) return null;
|
|
2767
|
-
const planPath =
|
|
2768
|
-
const plan =
|
|
2880
|
+
const planPath = path20.join(ctx.taskDir, "plan.md");
|
|
2881
|
+
const plan = fs21.existsSync(planPath) ? fs21.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
2769
2882
|
try {
|
|
2770
2883
|
postComment(
|
|
2771
2884
|
ctx.input.issueNumber,
|
|
@@ -2832,22 +2945,22 @@ var init_hooks = __esm({
|
|
|
2832
2945
|
});
|
|
2833
2946
|
|
|
2834
2947
|
// src/learning/auto-learn.ts
|
|
2835
|
-
import * as
|
|
2836
|
-
import * as
|
|
2948
|
+
import * as fs22 from "fs";
|
|
2949
|
+
import * as path21 from "path";
|
|
2837
2950
|
function stripAnsi(str) {
|
|
2838
2951
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2839
2952
|
}
|
|
2840
2953
|
function autoLearn(ctx) {
|
|
2841
2954
|
try {
|
|
2842
|
-
const memoryDir =
|
|
2843
|
-
if (!
|
|
2844
|
-
|
|
2955
|
+
const memoryDir = path21.join(ctx.projectDir, ".kody", "memory");
|
|
2956
|
+
if (!fs22.existsSync(memoryDir)) {
|
|
2957
|
+
fs22.mkdirSync(memoryDir, { recursive: true });
|
|
2845
2958
|
}
|
|
2846
2959
|
const learnings = [];
|
|
2847
2960
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2848
|
-
const verifyPath =
|
|
2849
|
-
if (
|
|
2850
|
-
const verify = stripAnsi(
|
|
2961
|
+
const verifyPath = path21.join(ctx.taskDir, "verify.md");
|
|
2962
|
+
if (fs22.existsSync(verifyPath)) {
|
|
2963
|
+
const verify = stripAnsi(fs22.readFileSync(verifyPath, "utf-8"));
|
|
2851
2964
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
2852
2965
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
2853
2966
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -2856,18 +2969,18 @@ function autoLearn(ctx) {
|
|
|
2856
2969
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
2857
2970
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
2858
2971
|
}
|
|
2859
|
-
const reviewPath =
|
|
2860
|
-
if (
|
|
2861
|
-
const review =
|
|
2972
|
+
const reviewPath = path21.join(ctx.taskDir, "review.md");
|
|
2973
|
+
if (fs22.existsSync(reviewPath)) {
|
|
2974
|
+
const review = fs22.readFileSync(reviewPath, "utf-8");
|
|
2862
2975
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
2863
2976
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
2864
2977
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
2865
2978
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
2866
2979
|
}
|
|
2867
|
-
const taskJsonPath =
|
|
2868
|
-
if (
|
|
2980
|
+
const taskJsonPath = path21.join(ctx.taskDir, "task.json");
|
|
2981
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
2869
2982
|
try {
|
|
2870
|
-
const raw = stripAnsi(
|
|
2983
|
+
const raw = stripAnsi(fs22.readFileSync(taskJsonPath, "utf-8"));
|
|
2871
2984
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2872
2985
|
const task = JSON.parse(cleaned);
|
|
2873
2986
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -2878,135 +2991,48 @@ function autoLearn(ctx) {
|
|
|
2878
2991
|
}
|
|
2879
2992
|
}
|
|
2880
2993
|
if (learnings.length > 0) {
|
|
2881
|
-
const conventionsPath =
|
|
2994
|
+
const conventionsPath = path21.join(memoryDir, "conventions.md");
|
|
2882
2995
|
const entry = `
|
|
2883
2996
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
2884
2997
|
${learnings.join("\n")}
|
|
2885
2998
|
`;
|
|
2886
|
-
|
|
2887
|
-
invalidateCache(conventionsPath, path15.join(memoryDir, ".tiers"));
|
|
2999
|
+
fs22.appendFileSync(conventionsPath, entry);
|
|
2888
3000
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
2889
3001
|
}
|
|
2890
|
-
autoLearnDecisions(ctx.taskDir, memoryDir, ctx.taskId, timestamp2);
|
|
2891
3002
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
2892
3003
|
} catch {
|
|
2893
3004
|
}
|
|
2894
3005
|
}
|
|
2895
3006
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
2896
|
-
const archPath =
|
|
2897
|
-
if (
|
|
2898
|
-
const detected =
|
|
2899
|
-
const pkgPath = path15.join(projectDir, "package.json");
|
|
2900
|
-
if (fs15.existsSync(pkgPath)) {
|
|
2901
|
-
try {
|
|
2902
|
-
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
|
|
2903
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2904
|
-
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
2905
|
-
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
2906
|
-
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
2907
|
-
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
2908
|
-
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
2909
|
-
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
2910
|
-
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
2911
|
-
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
2912
|
-
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- Database: Prisma ORM");
|
|
2913
|
-
if (allDeps.drizzle || allDeps["drizzle-orm"]) detected.push("- Database: Drizzle ORM");
|
|
2914
|
-
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
2915
|
-
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
2916
|
-
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
2917
|
-
else detected.push("- Module system: CommonJS");
|
|
2918
|
-
if (fs15.existsSync(path15.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
2919
|
-
else if (fs15.existsSync(path15.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
2920
|
-
else if (fs15.existsSync(path15.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
2921
|
-
} catch {
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
const topDirs = [];
|
|
2925
|
-
try {
|
|
2926
|
-
const entries = fs15.readdirSync(projectDir, { withFileTypes: true });
|
|
2927
|
-
for (const entry of entries) {
|
|
2928
|
-
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2929
|
-
topDirs.push(entry.name);
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
2932
|
-
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
2933
|
-
} catch {
|
|
2934
|
-
}
|
|
2935
|
-
const srcDir = path15.join(projectDir, "src");
|
|
2936
|
-
if (fs15.existsSync(srcDir)) {
|
|
2937
|
-
try {
|
|
2938
|
-
const srcEntries = fs15.readdirSync(srcDir, { withFileTypes: true });
|
|
2939
|
-
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2940
|
-
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
2941
|
-
} catch {
|
|
2942
|
-
}
|
|
2943
|
-
}
|
|
3007
|
+
const archPath = path21.join(memoryDir, "architecture.md");
|
|
3008
|
+
if (fs22.existsSync(archPath)) return;
|
|
3009
|
+
const detected = detectArchitectureBasic(projectDir);
|
|
2944
3010
|
if (detected.length > 0) {
|
|
2945
3011
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
2946
3012
|
|
|
2947
3013
|
## Overview
|
|
2948
3014
|
${detected.join("\n")}
|
|
2949
3015
|
`;
|
|
2950
|
-
|
|
2951
|
-
invalidateCache(archPath, path15.join(memoryDir, ".tiers"));
|
|
3016
|
+
fs22.writeFileSync(archPath, content);
|
|
2952
3017
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
2953
3018
|
}
|
|
2954
3019
|
}
|
|
2955
|
-
function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
|
|
2956
|
-
const reviewPath = path15.join(taskDir, "review.md");
|
|
2957
|
-
if (!fs15.existsSync(reviewPath)) return;
|
|
2958
|
-
const review = fs15.readFileSync(reviewPath, "utf-8");
|
|
2959
|
-
const decisions = [];
|
|
2960
|
-
const existingPatternRe = /(?:use|follow|reuse|match|adopt)\s+(?:the\s+)?existing\s+(.+?)(?:\.|$)/gim;
|
|
2961
|
-
for (const match of review.matchAll(existingPatternRe)) {
|
|
2962
|
-
decisions.push(`- Use existing ${match[1].trim()}`);
|
|
2963
|
-
}
|
|
2964
|
-
const insteadOfRe = /instead\s+of\s+(.+?),?\s+(?:use|prefer|adopt)\s+(.+?)(?:\.|$)/gim;
|
|
2965
|
-
for (const match of review.matchAll(insteadOfRe)) {
|
|
2966
|
-
decisions.push(`- Prefer ${match[2].trim()} over ${match[1].trim()}`);
|
|
2967
|
-
}
|
|
2968
|
-
const consistentRe = /(?:consistent\s+with|same\s+pattern\s+as|follow\s+the\s+pattern\s+(?:in|from))\s+(.+?)(?:\.|$)/gim;
|
|
2969
|
-
for (const match of review.matchAll(consistentRe)) {
|
|
2970
|
-
decisions.push(`- Follow pattern from ${match[1].trim()}`);
|
|
2971
|
-
}
|
|
2972
|
-
const avoidRe = /(?:don't|do\s+not|never|avoid)\s+(?:use\s+)?(.+?)\s+(?:for|when|in)\s+(.+?)(?:\.|$)/gim;
|
|
2973
|
-
for (const match of review.matchAll(avoidRe)) {
|
|
2974
|
-
decisions.push(`- Avoid ${match[1].trim()} for ${match[2].trim()}`);
|
|
2975
|
-
}
|
|
2976
|
-
if (decisions.length === 0) return;
|
|
2977
|
-
const decisionsPath = path15.join(memoryDir, "decisions.md");
|
|
2978
|
-
let existing = "";
|
|
2979
|
-
if (fs15.existsSync(decisionsPath)) {
|
|
2980
|
-
existing = fs15.readFileSync(decisionsPath, "utf-8");
|
|
2981
|
-
} else {
|
|
2982
|
-
existing = "# Architectural Decisions\n\nDecisions extracted from code reviews. The planning agent MUST follow these.\n";
|
|
2983
|
-
}
|
|
2984
|
-
const newDecisions = decisions.filter((d) => !existing.includes(d));
|
|
2985
|
-
if (newDecisions.length === 0) return;
|
|
2986
|
-
const entry = `
|
|
2987
|
-
## From task ${taskId} (${timestamp2})
|
|
2988
|
-
${newDecisions.join("\n")}
|
|
2989
|
-
`;
|
|
2990
|
-
fs15.appendFileSync(decisionsPath, existing ? entry : existing + entry);
|
|
2991
|
-
invalidateCache(decisionsPath, path15.join(memoryDir, ".tiers"));
|
|
2992
|
-
logger.info(`Auto-learned ${newDecisions.length} architectural decision(s)`);
|
|
2993
|
-
}
|
|
2994
3020
|
var init_auto_learn = __esm({
|
|
2995
3021
|
"src/learning/auto-learn.ts"() {
|
|
2996
3022
|
"use strict";
|
|
2997
3023
|
init_logger();
|
|
2998
|
-
|
|
3024
|
+
init_architecture_detection();
|
|
2999
3025
|
}
|
|
3000
3026
|
});
|
|
3001
3027
|
|
|
3002
3028
|
// src/retrospective.ts
|
|
3003
|
-
import * as
|
|
3004
|
-
import * as
|
|
3029
|
+
import * as fs23 from "fs";
|
|
3030
|
+
import * as path22 from "path";
|
|
3005
3031
|
function readArtifact(taskDir, filename, maxChars) {
|
|
3006
|
-
const p =
|
|
3007
|
-
if (!
|
|
3032
|
+
const p = path22.join(taskDir, filename);
|
|
3033
|
+
if (!fs23.existsSync(p)) return null;
|
|
3008
3034
|
try {
|
|
3009
|
-
const content =
|
|
3035
|
+
const content = fs23.readFileSync(p, "utf-8");
|
|
3010
3036
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
3011
3037
|
} catch {
|
|
3012
3038
|
return null;
|
|
@@ -3059,13 +3085,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
3059
3085
|
return lines.join("\n");
|
|
3060
3086
|
}
|
|
3061
3087
|
function getLogPath(projectDir) {
|
|
3062
|
-
return
|
|
3088
|
+
return path22.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
3063
3089
|
}
|
|
3064
3090
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
3065
3091
|
const logPath = getLogPath(projectDir);
|
|
3066
|
-
if (!
|
|
3092
|
+
if (!fs23.existsSync(logPath)) return [];
|
|
3067
3093
|
try {
|
|
3068
|
-
const content =
|
|
3094
|
+
const content = fs23.readFileSync(logPath, "utf-8");
|
|
3069
3095
|
const lines = content.split("\n").filter(Boolean);
|
|
3070
3096
|
const entries = [];
|
|
3071
3097
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -3092,11 +3118,11 @@ function formatPreviousEntries(entries) {
|
|
|
3092
3118
|
}
|
|
3093
3119
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
3094
3120
|
const logPath = getLogPath(projectDir);
|
|
3095
|
-
const dir =
|
|
3096
|
-
if (!
|
|
3097
|
-
|
|
3121
|
+
const dir = path22.dirname(logPath);
|
|
3122
|
+
if (!fs23.existsSync(dir)) {
|
|
3123
|
+
fs23.mkdirSync(dir, { recursive: true });
|
|
3098
3124
|
}
|
|
3099
|
-
|
|
3125
|
+
fs23.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
3100
3126
|
}
|
|
3101
3127
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
3102
3128
|
if (ctx.input.dryRun) return;
|
|
@@ -3207,8 +3233,8 @@ If no pipeline flaw is detected, set "pipelineFlaw" to null.
|
|
|
3207
3233
|
});
|
|
3208
3234
|
|
|
3209
3235
|
// src/pipeline.ts
|
|
3210
|
-
import * as
|
|
3211
|
-
import * as
|
|
3236
|
+
import * as fs24 from "fs";
|
|
3237
|
+
import * as path23 from "path";
|
|
3212
3238
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
3213
3239
|
if (ctx.input.dryRun) return;
|
|
3214
3240
|
if (ctx.input.prNumber) {
|
|
@@ -3221,34 +3247,59 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
3221
3247
|
}
|
|
3222
3248
|
if (!ctx.input.issueNumber) return;
|
|
3223
3249
|
try {
|
|
3224
|
-
const taskMdPath =
|
|
3225
|
-
const title =
|
|
3250
|
+
const taskMdPath = path23.join(ctx.taskDir, "task.md");
|
|
3251
|
+
const title = fs24.existsSync(taskMdPath) ? fs24.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
3226
3252
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
3227
3253
|
syncWithDefault(ctx.projectDir);
|
|
3228
3254
|
} catch (err) {
|
|
3229
|
-
|
|
3255
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
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
|
+
}
|
|
3230
3262
|
}
|
|
3231
3263
|
}
|
|
3232
3264
|
function acquireLock(taskDir) {
|
|
3233
|
-
const lockPath =
|
|
3234
|
-
if (
|
|
3265
|
+
const lockPath = path23.join(taskDir, ".lock");
|
|
3266
|
+
if (fs24.existsSync(lockPath)) {
|
|
3235
3267
|
try {
|
|
3236
|
-
const pid = parseInt(
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3268
|
+
const pid = parseInt(fs24.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
3269
|
+
if (!isNaN(pid)) {
|
|
3270
|
+
try {
|
|
3271
|
+
process.kill(pid, 0);
|
|
3272
|
+
throw new Error(`Pipeline already running (PID ${pid})`);
|
|
3273
|
+
} catch (e) {
|
|
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`);
|
|
3242
3279
|
}
|
|
3243
3280
|
} catch (e) {
|
|
3244
3281
|
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
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
try {
|
|
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)");
|
|
3245
3296
|
}
|
|
3297
|
+
throw err;
|
|
3246
3298
|
}
|
|
3247
|
-
fs17.writeFileSync(lockPath, String(process.pid));
|
|
3248
3299
|
}
|
|
3249
3300
|
function releaseLock(taskDir) {
|
|
3250
3301
|
try {
|
|
3251
|
-
|
|
3302
|
+
fs24.unlinkSync(path23.join(taskDir, ".lock"));
|
|
3252
3303
|
} catch {
|
|
3253
3304
|
}
|
|
3254
3305
|
}
|
|
@@ -3260,17 +3311,17 @@ async function runPipeline(ctx) {
|
|
|
3260
3311
|
try {
|
|
3261
3312
|
const state = loadState(ctx.taskId, ctx.taskDir);
|
|
3262
3313
|
if (state && state.state === "running") {
|
|
3263
|
-
state.
|
|
3314
|
+
const updatedStages = { ...state.stages };
|
|
3264
3315
|
for (const stage of STAGES) {
|
|
3265
|
-
if (
|
|
3266
|
-
|
|
3267
|
-
...
|
|
3316
|
+
if (updatedStages[stage.name]?.state === "running") {
|
|
3317
|
+
updatedStages[stage.name] = {
|
|
3318
|
+
...updatedStages[stage.name],
|
|
3268
3319
|
state: "failed",
|
|
3269
3320
|
error: "Pipeline crashed unexpectedly"
|
|
3270
3321
|
};
|
|
3271
3322
|
}
|
|
3272
3323
|
}
|
|
3273
|
-
writeState(state, ctx.taskDir);
|
|
3324
|
+
writeState({ ...state, state: "failed", stages: updatedStages }, ctx.taskDir);
|
|
3274
3325
|
}
|
|
3275
3326
|
} catch {
|
|
3276
3327
|
}
|
|
@@ -3395,7 +3446,8 @@ async function runPipelineInner(ctx) {
|
|
|
3395
3446
|
}
|
|
3396
3447
|
autoLearn(ctx);
|
|
3397
3448
|
}
|
|
3398
|
-
await runRetrospective(ctx, state, pipelineStartTime).catch(() => {
|
|
3449
|
+
await runRetrospective(ctx, state, pipelineStartTime).catch((err) => {
|
|
3450
|
+
logger.warn(` Retrospective failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3399
3451
|
});
|
|
3400
3452
|
return state;
|
|
3401
3453
|
}
|
|
@@ -3435,8 +3487,8 @@ var init_pipeline = __esm({
|
|
|
3435
3487
|
});
|
|
3436
3488
|
|
|
3437
3489
|
// src/preflight.ts
|
|
3438
|
-
import { execFileSync as
|
|
3439
|
-
import * as
|
|
3490
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
3491
|
+
import * as fs25 from "fs";
|
|
3440
3492
|
function check(name, fn) {
|
|
3441
3493
|
try {
|
|
3442
3494
|
const detail = fn() ?? void 0;
|
|
@@ -3448,7 +3500,7 @@ function check(name, fn) {
|
|
|
3448
3500
|
function runPreflight() {
|
|
3449
3501
|
const checks = [
|
|
3450
3502
|
check("claude CLI", () => {
|
|
3451
|
-
const v =
|
|
3503
|
+
const v = execFileSync14("claude", ["--version"], {
|
|
3452
3504
|
encoding: "utf-8",
|
|
3453
3505
|
timeout: 1e4,
|
|
3454
3506
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3456,14 +3508,14 @@ function runPreflight() {
|
|
|
3456
3508
|
return v;
|
|
3457
3509
|
}),
|
|
3458
3510
|
check("git repo", () => {
|
|
3459
|
-
|
|
3511
|
+
execFileSync14("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3460
3512
|
encoding: "utf-8",
|
|
3461
3513
|
timeout: 5e3,
|
|
3462
3514
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3463
3515
|
});
|
|
3464
3516
|
}),
|
|
3465
3517
|
check("pnpm", () => {
|
|
3466
|
-
const v =
|
|
3518
|
+
const v = execFileSync14("pnpm", ["--version"], {
|
|
3467
3519
|
encoding: "utf-8",
|
|
3468
3520
|
timeout: 5e3,
|
|
3469
3521
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3471,7 +3523,7 @@ function runPreflight() {
|
|
|
3471
3523
|
return v;
|
|
3472
3524
|
}),
|
|
3473
3525
|
check("node >= 18", () => {
|
|
3474
|
-
const v =
|
|
3526
|
+
const v = execFileSync14("node", ["--version"], {
|
|
3475
3527
|
encoding: "utf-8",
|
|
3476
3528
|
timeout: 5e3,
|
|
3477
3529
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3481,7 +3533,7 @@ function runPreflight() {
|
|
|
3481
3533
|
return v;
|
|
3482
3534
|
}),
|
|
3483
3535
|
check("gh CLI", () => {
|
|
3484
|
-
const v =
|
|
3536
|
+
const v = execFileSync14("gh", ["--version"], {
|
|
3485
3537
|
encoding: "utf-8",
|
|
3486
3538
|
timeout: 5e3,
|
|
3487
3539
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3489,7 +3541,7 @@ function runPreflight() {
|
|
|
3489
3541
|
return v;
|
|
3490
3542
|
}),
|
|
3491
3543
|
check("package.json", () => {
|
|
3492
|
-
if (!
|
|
3544
|
+
if (!fs25.existsSync("package.json")) throw new Error("not found");
|
|
3493
3545
|
})
|
|
3494
3546
|
];
|
|
3495
3547
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -3566,10 +3618,10 @@ var init_args = __esm({
|
|
|
3566
3618
|
});
|
|
3567
3619
|
|
|
3568
3620
|
// src/cli/litellm.ts
|
|
3569
|
-
import * as
|
|
3621
|
+
import * as fs26 from "fs";
|
|
3570
3622
|
import * as os from "os";
|
|
3571
|
-
import * as
|
|
3572
|
-
import { execFileSync as
|
|
3623
|
+
import * as path24 from "path";
|
|
3624
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
3573
3625
|
async function checkLitellmHealth(url) {
|
|
3574
3626
|
try {
|
|
3575
3627
|
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
@@ -3629,17 +3681,17 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
|
3629
3681
|
logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
|
|
3630
3682
|
return null;
|
|
3631
3683
|
}
|
|
3632
|
-
const configPath =
|
|
3633
|
-
|
|
3684
|
+
const configPath = path24.join(os.tmpdir(), "kody-litellm-config.yaml");
|
|
3685
|
+
fs26.writeFileSync(configPath, generatedConfig);
|
|
3634
3686
|
const portMatch = url.match(/:(\d+)/);
|
|
3635
3687
|
const port = portMatch ? portMatch[1] : "4000";
|
|
3636
3688
|
let litellmFound = false;
|
|
3637
3689
|
try {
|
|
3638
|
-
|
|
3690
|
+
execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
3639
3691
|
litellmFound = true;
|
|
3640
3692
|
} catch {
|
|
3641
3693
|
try {
|
|
3642
|
-
|
|
3694
|
+
execFileSync15("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
3643
3695
|
litellmFound = true;
|
|
3644
3696
|
} catch {
|
|
3645
3697
|
}
|
|
@@ -3652,19 +3704,29 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
|
3652
3704
|
let cmd;
|
|
3653
3705
|
let args2;
|
|
3654
3706
|
try {
|
|
3655
|
-
|
|
3707
|
+
execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
3656
3708
|
cmd = "litellm";
|
|
3657
3709
|
args2 = ["--config", configPath, "--port", port];
|
|
3658
3710
|
} catch {
|
|
3659
3711
|
cmd = "python3";
|
|
3660
3712
|
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
3661
3713
|
}
|
|
3662
|
-
const dotenvPath =
|
|
3714
|
+
const dotenvPath = path24.join(projectDir, ".env");
|
|
3663
3715
|
const dotenvVars = {};
|
|
3664
|
-
if (
|
|
3665
|
-
for (const
|
|
3716
|
+
if (fs26.existsSync(dotenvPath)) {
|
|
3717
|
+
for (const rawLine of fs26.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
3718
|
+
const line = rawLine.trim();
|
|
3719
|
+
if (!line || line.startsWith("#")) continue;
|
|
3666
3720
|
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
3667
|
-
if (match)
|
|
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
|
+
}
|
|
3668
3730
|
}
|
|
3669
3731
|
if (Object.keys(dotenvVars).length > 0) {
|
|
3670
3732
|
logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
|
|
@@ -3703,8 +3765,8 @@ var init_litellm = __esm({
|
|
|
3703
3765
|
});
|
|
3704
3766
|
|
|
3705
3767
|
// src/cli/task-state.ts
|
|
3706
|
-
import * as
|
|
3707
|
-
import * as
|
|
3768
|
+
import * as fs27 from "fs";
|
|
3769
|
+
import * as path25 from "path";
|
|
3708
3770
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
3709
3771
|
if (!existingTaskId || !existingState) {
|
|
3710
3772
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -3736,11 +3798,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
3736
3798
|
function resolveForIssue(issueNumber, projectDir) {
|
|
3737
3799
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
3738
3800
|
if (existingTaskId) {
|
|
3739
|
-
const statusPath =
|
|
3801
|
+
const statusPath = path25.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
3740
3802
|
let existingState = null;
|
|
3741
|
-
if (
|
|
3803
|
+
if (fs27.existsSync(statusPath)) {
|
|
3742
3804
|
try {
|
|
3743
|
-
existingState = JSON.parse(
|
|
3805
|
+
existingState = JSON.parse(fs27.readFileSync(statusPath, "utf-8"));
|
|
3744
3806
|
} catch {
|
|
3745
3807
|
}
|
|
3746
3808
|
}
|
|
@@ -3773,12 +3835,12 @@ var resolve_exports = {};
|
|
|
3773
3835
|
__export(resolve_exports, {
|
|
3774
3836
|
runResolve: () => runResolve
|
|
3775
3837
|
});
|
|
3776
|
-
import { execFileSync as
|
|
3838
|
+
import { execFileSync as execFileSync16 } from "child_process";
|
|
3777
3839
|
function getConflictContext(cwd, files) {
|
|
3778
3840
|
const parts = [];
|
|
3779
3841
|
for (const file of files.slice(0, 10)) {
|
|
3780
3842
|
try {
|
|
3781
|
-
const content =
|
|
3843
|
+
const content = execFileSync16("git", ["diff", file], {
|
|
3782
3844
|
cwd,
|
|
3783
3845
|
encoding: "utf-8",
|
|
3784
3846
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3897,8 +3959,8 @@ var init_resolve = __esm({
|
|
|
3897
3959
|
|
|
3898
3960
|
// src/entry.ts
|
|
3899
3961
|
var entry_exports = {};
|
|
3900
|
-
import * as
|
|
3901
|
-
import * as
|
|
3962
|
+
import * as fs28 from "fs";
|
|
3963
|
+
import * as path26 from "path";
|
|
3902
3964
|
async function ensureLitellmProxy(config, projectDir) {
|
|
3903
3965
|
if (!needsLitellmProxy(config)) return null;
|
|
3904
3966
|
const litellmUrl = getLitellmUrl();
|
|
@@ -3927,7 +3989,7 @@ async function ensureLitellmProxy(config, projectDir) {
|
|
|
3927
3989
|
process.env.ANTHROPIC_BASE_URL = litellmUrl;
|
|
3928
3990
|
logger.info(`ANTHROPIC_BASE_URL set to ${litellmUrl}`);
|
|
3929
3991
|
if (!process.env.ANTHROPIC_API_KEY || !process.env.ANTHROPIC_API_KEY.startsWith("sk-ant-")) {
|
|
3930
|
-
process.env.ANTHROPIC_API_KEY =
|
|
3992
|
+
process.env.ANTHROPIC_API_KEY = `sk-ant-api03-${"0".repeat(64)}`;
|
|
3931
3993
|
}
|
|
3932
3994
|
return litellmProcess;
|
|
3933
3995
|
}
|
|
@@ -3952,9 +4014,9 @@ async function runModelHealthCheck(config) {
|
|
|
3952
4014
|
}
|
|
3953
4015
|
async function main() {
|
|
3954
4016
|
const input = parseArgs();
|
|
3955
|
-
const projectDir = input.cwd ?
|
|
4017
|
+
const projectDir = input.cwd ? path26.resolve(input.cwd) : process.cwd();
|
|
3956
4018
|
if (input.cwd) {
|
|
3957
|
-
if (!
|
|
4019
|
+
if (!fs28.existsSync(projectDir)) {
|
|
3958
4020
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
3959
4021
|
process.exit(1);
|
|
3960
4022
|
}
|
|
@@ -3988,9 +4050,11 @@ async function main() {
|
|
|
3988
4050
|
process.exit(0);
|
|
3989
4051
|
}
|
|
3990
4052
|
if (taskAction.action === "resume") {
|
|
3991
|
-
input
|
|
3992
|
-
|
|
3993
|
-
|
|
4053
|
+
Object.assign(input, {
|
|
4054
|
+
taskId: taskAction.taskId,
|
|
4055
|
+
fromStage: taskAction.fromStage,
|
|
4056
|
+
command: "rerun"
|
|
4057
|
+
});
|
|
3994
4058
|
logger.info(`Resuming task ${taskAction.taskId} from ${taskAction.fromStage}`);
|
|
3995
4059
|
}
|
|
3996
4060
|
}
|
|
@@ -4009,8 +4073,8 @@ async function main() {
|
|
|
4009
4073
|
process.exit(1);
|
|
4010
4074
|
}
|
|
4011
4075
|
}
|
|
4012
|
-
const taskDir =
|
|
4013
|
-
|
|
4076
|
+
const taskDir = path26.join(projectDir, ".kody", "tasks", taskId);
|
|
4077
|
+
fs28.mkdirSync(taskDir, { recursive: true });
|
|
4014
4078
|
if (input.command === "status") {
|
|
4015
4079
|
printStatus(taskId, taskDir);
|
|
4016
4080
|
return;
|
|
@@ -4115,7 +4179,7 @@ async function main() {
|
|
|
4115
4179
|
runners: runners2,
|
|
4116
4180
|
local: input.local ?? true
|
|
4117
4181
|
});
|
|
4118
|
-
if (litellmProcess2) litellmProcess2.kill
|
|
4182
|
+
if (litellmProcess2) litellmProcess2.kill();
|
|
4119
4183
|
if (result.outcome === "failed") {
|
|
4120
4184
|
console.error(`Resolve failed: ${result.error}`);
|
|
4121
4185
|
process.exit(1);
|
|
@@ -4126,31 +4190,31 @@ async function main() {
|
|
|
4126
4190
|
logger.info("Preflight checks:");
|
|
4127
4191
|
runPreflight();
|
|
4128
4192
|
if (input.task) {
|
|
4129
|
-
|
|
4193
|
+
fs28.writeFileSync(path26.join(taskDir, "task.md"), input.task);
|
|
4130
4194
|
}
|
|
4131
|
-
const taskMdPath =
|
|
4132
|
-
if (!
|
|
4195
|
+
const taskMdPath = path26.join(taskDir, "task.md");
|
|
4196
|
+
if (!fs28.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
4133
4197
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
4134
4198
|
const prDetails = getPRDetails(input.prNumber);
|
|
4135
4199
|
if (prDetails) {
|
|
4136
4200
|
const taskContent = `# ${prDetails.title}
|
|
4137
4201
|
|
|
4138
4202
|
${prDetails.body ?? ""}`;
|
|
4139
|
-
|
|
4203
|
+
fs28.writeFileSync(taskMdPath, taskContent);
|
|
4140
4204
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
4141
4205
|
}
|
|
4142
|
-
} else if (!
|
|
4206
|
+
} else if (!fs28.existsSync(taskMdPath) && input.issueNumber) {
|
|
4143
4207
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
4144
4208
|
const issue = getIssue(input.issueNumber);
|
|
4145
4209
|
if (issue) {
|
|
4146
4210
|
const taskContent = `# ${issue.title}
|
|
4147
4211
|
|
|
4148
4212
|
${issue.body ?? ""}`;
|
|
4149
|
-
|
|
4213
|
+
fs28.writeFileSync(taskMdPath, taskContent);
|
|
4150
4214
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
4151
4215
|
}
|
|
4152
4216
|
}
|
|
4153
|
-
if (!
|
|
4217
|
+
if (!fs28.existsSync(taskMdPath)) {
|
|
4154
4218
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
4155
4219
|
process.exit(1);
|
|
4156
4220
|
}
|
|
@@ -4222,7 +4286,7 @@ ${input.feedback}`);
|
|
|
4222
4286
|
await runModelHealthCheck(config);
|
|
4223
4287
|
const cleanupLitellm = () => {
|
|
4224
4288
|
if (litellmProcess) {
|
|
4225
|
-
litellmProcess.kill
|
|
4289
|
+
litellmProcess.kill();
|
|
4226
4290
|
litellmProcess = null;
|
|
4227
4291
|
}
|
|
4228
4292
|
};
|
|
@@ -4288,7 +4352,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
4288
4352
|
}
|
|
4289
4353
|
}
|
|
4290
4354
|
const state = await runPipeline(ctx);
|
|
4291
|
-
const files =
|
|
4355
|
+
const files = fs28.readdirSync(taskDir);
|
|
4292
4356
|
console.log(`
|
|
4293
4357
|
Artifacts in ${taskDir}:`);
|
|
4294
4358
|
for (const f of files) {
|
|
@@ -4352,20 +4416,21 @@ var init_entry = __esm({
|
|
|
4352
4416
|
});
|
|
4353
4417
|
|
|
4354
4418
|
// src/bin/cli.ts
|
|
4355
|
-
import * as
|
|
4356
|
-
import * as
|
|
4357
|
-
import { execFileSync as execFileSync12 } from "child_process";
|
|
4419
|
+
import * as fs29 from "fs";
|
|
4420
|
+
import * as path27 from "path";
|
|
4358
4421
|
import { fileURLToPath } from "url";
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4422
|
+
|
|
4423
|
+
// src/bin/commands/init.ts
|
|
4424
|
+
import * as fs3 from "fs";
|
|
4425
|
+
import * as path2 from "path";
|
|
4426
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
4427
|
+
|
|
4428
|
+
// src/bin/health-checks.ts
|
|
4429
|
+
import * as fs from "fs";
|
|
4430
|
+
import { execFileSync } from "child_process";
|
|
4431
|
+
function checkCommand(name, args2, fix) {
|
|
4367
4432
|
try {
|
|
4368
|
-
const output =
|
|
4433
|
+
const output = execFileSync(name, args2, {
|
|
4369
4434
|
encoding: "utf-8",
|
|
4370
4435
|
timeout: 1e4,
|
|
4371
4436
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4376,14 +4441,14 @@ function checkCommand2(name, args2, fix) {
|
|
|
4376
4441
|
}
|
|
4377
4442
|
}
|
|
4378
4443
|
function checkFile(filePath, description, fix) {
|
|
4379
|
-
if (
|
|
4444
|
+
if (fs.existsSync(filePath)) {
|
|
4380
4445
|
return { name: description, ok: true, detail: filePath };
|
|
4381
4446
|
}
|
|
4382
4447
|
return { name: description, ok: false, fix };
|
|
4383
4448
|
}
|
|
4384
4449
|
function checkGhAuth(cwd) {
|
|
4385
4450
|
try {
|
|
4386
|
-
const output =
|
|
4451
|
+
const output = execFileSync("gh", ["auth", "status"], {
|
|
4387
4452
|
encoding: "utf-8",
|
|
4388
4453
|
timeout: 1e4,
|
|
4389
4454
|
cwd,
|
|
@@ -4401,7 +4466,7 @@ function checkGhAuth(cwd) {
|
|
|
4401
4466
|
}
|
|
4402
4467
|
function checkGhRepoAccess(cwd) {
|
|
4403
4468
|
try {
|
|
4404
|
-
const remote =
|
|
4469
|
+
const remote = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
4405
4470
|
encoding: "utf-8",
|
|
4406
4471
|
timeout: 5e3,
|
|
4407
4472
|
cwd,
|
|
@@ -4412,7 +4477,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
4412
4477
|
return { name: "GitHub repo", ok: false, fix: "Set git remote origin to a GitHub URL" };
|
|
4413
4478
|
}
|
|
4414
4479
|
const repoSlug = `${match[1]}/${match[2]}`;
|
|
4415
|
-
|
|
4480
|
+
execFileSync("gh", ["repo", "view", repoSlug, "--json", "name"], {
|
|
4416
4481
|
encoding: "utf-8",
|
|
4417
4482
|
timeout: 1e4,
|
|
4418
4483
|
cwd,
|
|
@@ -4425,7 +4490,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
4425
4490
|
}
|
|
4426
4491
|
function checkGhSecret(repoSlug, secretName) {
|
|
4427
4492
|
try {
|
|
4428
|
-
const output =
|
|
4493
|
+
const output = execFileSync("gh", ["secret", "list", "--repo", repoSlug], {
|
|
4429
4494
|
encoding: "utf-8",
|
|
4430
4495
|
timeout: 1e4,
|
|
4431
4496
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4446,14 +4511,20 @@ function checkGhSecret(repoSlug, secretName) {
|
|
|
4446
4511
|
};
|
|
4447
4512
|
}
|
|
4448
4513
|
}
|
|
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"];
|
|
4449
4520
|
function detectBasicConfig(cwd) {
|
|
4450
4521
|
let pm = "pnpm";
|
|
4451
|
-
if (
|
|
4452
|
-
else if (
|
|
4453
|
-
else if (!
|
|
4522
|
+
if (fs2.existsSync(path.join(cwd, "yarn.lock"))) pm = "yarn";
|
|
4523
|
+
else if (fs2.existsSync(path.join(cwd, "bun.lockb"))) pm = "bun";
|
|
4524
|
+
else if (!fs2.existsSync(path.join(cwd, "pnpm-lock.yaml")) && fs2.existsSync(path.join(cwd, "package-lock.json"))) pm = "npm";
|
|
4454
4525
|
let defaultBranch = "main";
|
|
4455
4526
|
try {
|
|
4456
|
-
const ref =
|
|
4527
|
+
const ref = execFileSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
4457
4528
|
encoding: "utf-8",
|
|
4458
4529
|
timeout: 5e3,
|
|
4459
4530
|
cwd,
|
|
@@ -4462,7 +4533,7 @@ function detectBasicConfig(cwd) {
|
|
|
4462
4533
|
defaultBranch = ref.replace("refs/remotes/origin/", "");
|
|
4463
4534
|
} catch {
|
|
4464
4535
|
try {
|
|
4465
|
-
|
|
4536
|
+
execFileSync2("git", ["rev-parse", "--verify", "origin/dev"], {
|
|
4466
4537
|
encoding: "utf-8",
|
|
4467
4538
|
timeout: 5e3,
|
|
4468
4539
|
cwd,
|
|
@@ -4475,7 +4546,7 @@ function detectBasicConfig(cwd) {
|
|
|
4475
4546
|
let owner = "";
|
|
4476
4547
|
let repo = "";
|
|
4477
4548
|
try {
|
|
4478
|
-
const remote =
|
|
4549
|
+
const remote = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
4479
4550
|
encoding: "utf-8",
|
|
4480
4551
|
timeout: 5e3,
|
|
4481
4552
|
cwd,
|
|
@@ -4493,7 +4564,7 @@ function detectBasicConfig(cwd) {
|
|
|
4493
4564
|
function buildConfig(cwd, basic) {
|
|
4494
4565
|
const pkg = (() => {
|
|
4495
4566
|
try {
|
|
4496
|
-
return JSON.parse(
|
|
4567
|
+
return JSON.parse(fs2.readFileSync(path.join(cwd, "package.json"), "utf-8"));
|
|
4497
4568
|
} catch {
|
|
4498
4569
|
return {};
|
|
4499
4570
|
}
|
|
@@ -4525,7 +4596,6 @@ function buildConfig(cwd, basic) {
|
|
|
4525
4596
|
if (mcp) config.mcp = mcp;
|
|
4526
4597
|
return config;
|
|
4527
4598
|
}
|
|
4528
|
-
var FRONTEND_DEPS = ["next", "react", "vue", "svelte", "nuxt", "astro", "solid-js", "angular", "@angular/core"];
|
|
4529
4599
|
function detectMcpConfig(cwd, pm, pkg) {
|
|
4530
4600
|
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
4531
4601
|
const hasFrontend = FRONTEND_DEPS.some((dep) => dep in allDeps);
|
|
@@ -4548,7 +4618,9 @@ function detectMcpConfig(cwd, pm, pkg) {
|
|
|
4548
4618
|
}
|
|
4549
4619
|
return mcp;
|
|
4550
4620
|
}
|
|
4551
|
-
|
|
4621
|
+
|
|
4622
|
+
// src/bin/commands/init.ts
|
|
4623
|
+
function initCommand(opts, pkgRoot) {
|
|
4552
4624
|
const cwd = process.cwd();
|
|
4553
4625
|
console.log(`
|
|
4554
4626
|
\u{1F527} Kody Engine Lite \u2014 Init
|
|
@@ -4556,35 +4628,35 @@ function initCommand(opts) {
|
|
|
4556
4628
|
console.log(`Project: ${cwd}
|
|
4557
4629
|
`);
|
|
4558
4630
|
console.log("\u2500\u2500 Files \u2500\u2500");
|
|
4559
|
-
const templatesDir =
|
|
4631
|
+
const templatesDir = path2.join(pkgRoot, "templates");
|
|
4560
4632
|
const basic = detectBasicConfig(cwd);
|
|
4561
|
-
const workflowSrc =
|
|
4562
|
-
const workflowDest =
|
|
4563
|
-
if (!
|
|
4633
|
+
const workflowSrc = path2.join(templatesDir, "kody.yml");
|
|
4634
|
+
const workflowDest = path2.join(cwd, ".github", "workflows", "kody.yml");
|
|
4635
|
+
if (!fs3.existsSync(workflowSrc)) {
|
|
4564
4636
|
console.error(" \u2717 Template kody.yml not found in package");
|
|
4565
4637
|
process.exit(1);
|
|
4566
4638
|
}
|
|
4567
|
-
if (
|
|
4639
|
+
if (fs3.existsSync(workflowDest) && !opts.force) {
|
|
4568
4640
|
console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
|
|
4569
4641
|
} else {
|
|
4570
|
-
|
|
4571
|
-
|
|
4642
|
+
fs3.mkdirSync(path2.dirname(workflowDest), { recursive: true });
|
|
4643
|
+
fs3.copyFileSync(workflowSrc, workflowDest);
|
|
4572
4644
|
console.log(" \u2713 .github/workflows/kody.yml");
|
|
4573
4645
|
}
|
|
4574
|
-
const configDest =
|
|
4575
|
-
if (!
|
|
4646
|
+
const configDest = path2.join(cwd, "kody.config.json");
|
|
4647
|
+
if (!fs3.existsSync(configDest) || opts.force) {
|
|
4576
4648
|
const config = buildConfig(cwd, basic);
|
|
4577
|
-
|
|
4649
|
+
fs3.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
|
|
4578
4650
|
console.log(" \u2713 kody.config.json (auto-configured)");
|
|
4579
4651
|
} else {
|
|
4580
4652
|
console.log(" \u25CB kody.config.json (exists)");
|
|
4581
4653
|
}
|
|
4582
|
-
const gitignorePath =
|
|
4583
|
-
if (
|
|
4584
|
-
const content =
|
|
4654
|
+
const gitignorePath = path2.join(cwd, ".gitignore");
|
|
4655
|
+
if (fs3.existsSync(gitignorePath)) {
|
|
4656
|
+
const content = fs3.readFileSync(gitignorePath, "utf-8");
|
|
4585
4657
|
if (content.includes(".tasks/")) {
|
|
4586
4658
|
const updated = content.replace(/\n?\.tasks\/\n?/g, "\n");
|
|
4587
|
-
|
|
4659
|
+
fs3.writeFileSync(gitignorePath, updated);
|
|
4588
4660
|
console.log(" \u2713 .gitignore (removed legacy .tasks/ \u2014 tasks now committed in .kody/tasks/)");
|
|
4589
4661
|
} else {
|
|
4590
4662
|
console.log(" \u25CB .gitignore (ok)");
|
|
@@ -4592,10 +4664,10 @@ function initCommand(opts) {
|
|
|
4592
4664
|
}
|
|
4593
4665
|
console.log("\n\u2500\u2500 Prerequisites \u2500\u2500");
|
|
4594
4666
|
const checks = [
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
checkFile(
|
|
4667
|
+
checkCommand("gh", ["--version"], "Install: https://cli.github.com"),
|
|
4668
|
+
checkCommand("git", ["--version"], "Install git"),
|
|
4669
|
+
checkCommand("node", ["--version"], "Install Node.js >= 22"),
|
|
4670
|
+
checkFile(path2.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
|
|
4599
4671
|
];
|
|
4600
4672
|
for (const c of checks) {
|
|
4601
4673
|
if (c.ok) {
|
|
@@ -4609,26 +4681,19 @@ function initCommand(opts) {
|
|
|
4609
4681
|
console.log(ghAuth.ok ? ` \u2713 ${ghAuth.name} (${ghAuth.detail})` : ` \u2717 ${ghAuth.name} \u2014 ${ghAuth.fix}`);
|
|
4610
4682
|
const ghRepo = checkGhRepoAccess(cwd);
|
|
4611
4683
|
console.log(ghRepo.ok ? ` \u2713 ${ghRepo.name} (${ghRepo.detail})` : ` \u2717 ${ghRepo.name} \u2014 ${ghRepo.fix}`);
|
|
4612
|
-
let repoSlug = "";
|
|
4613
4684
|
if (ghRepo.ok && ghRepo.detail) {
|
|
4614
|
-
repoSlug = ghRepo.detail;
|
|
4615
|
-
const secretChecks = [
|
|
4616
|
-
checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")
|
|
4617
|
-
];
|
|
4685
|
+
const repoSlug = ghRepo.detail;
|
|
4686
|
+
const secretChecks = [checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")];
|
|
4618
4687
|
for (const c of secretChecks) {
|
|
4619
|
-
|
|
4620
|
-
console.log(` \u2713 ${c.name}`);
|
|
4621
|
-
} else {
|
|
4622
|
-
console.log(` \u2717 ${c.name} \u2014 ${c.fix}`);
|
|
4623
|
-
}
|
|
4688
|
+
console.log(c.ok ? ` \u2713 ${c.name}` : ` \u2717 ${c.name} \u2014 ${c.fix}`);
|
|
4624
4689
|
}
|
|
4625
4690
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
4626
4691
|
console.log(" \u25CB Labels will be created automatically during bootstrap");
|
|
4627
4692
|
}
|
|
4628
4693
|
console.log("\n\u2500\u2500 Config \u2500\u2500");
|
|
4629
|
-
if (
|
|
4694
|
+
if (fs3.existsSync(configDest)) {
|
|
4630
4695
|
try {
|
|
4631
|
-
const config = JSON.parse(
|
|
4696
|
+
const config = JSON.parse(fs3.readFileSync(configDest, "utf-8"));
|
|
4632
4697
|
const configChecks = [];
|
|
4633
4698
|
if (config.github?.owner && config.github?.repo) {
|
|
4634
4699
|
configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
|
|
@@ -4658,11 +4723,11 @@ function initCommand(opts) {
|
|
|
4658
4723
|
const filesToCommit = [
|
|
4659
4724
|
".github/workflows/kody.yml",
|
|
4660
4725
|
"kody.config.json"
|
|
4661
|
-
].filter((f) =>
|
|
4726
|
+
].filter((f) => fs3.existsSync(path2.join(cwd, f)));
|
|
4662
4727
|
if (filesToCommit.length > 0) {
|
|
4663
4728
|
try {
|
|
4664
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
4665
|
-
|
|
4729
|
+
const fullPaths = filesToCommit.map((f) => path2.join(cwd, f));
|
|
4730
|
+
execFileSync3("npx", ["prettier", "--write", ...fullPaths], {
|
|
4666
4731
|
cwd,
|
|
4667
4732
|
encoding: "utf-8",
|
|
4668
4733
|
timeout: 3e4,
|
|
@@ -4673,13 +4738,13 @@ function initCommand(opts) {
|
|
|
4673
4738
|
}
|
|
4674
4739
|
if (filesToCommit.length > 0) {
|
|
4675
4740
|
try {
|
|
4676
|
-
|
|
4677
|
-
const staged =
|
|
4741
|
+
execFileSync3("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
4742
|
+
const staged = execFileSync3("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
4678
4743
|
if (staged) {
|
|
4679
|
-
|
|
4744
|
+
execFileSync3("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" });
|
|
4680
4745
|
console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
|
|
4681
4746
|
try {
|
|
4682
|
-
|
|
4747
|
+
execFileSync3("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
4683
4748
|
console.log(" \u2713 Pushed to origin");
|
|
4684
4749
|
} catch {
|
|
4685
4750
|
console.log(" \u25CB Push failed \u2014 run 'git push' manually");
|
|
@@ -4721,87 +4786,365 @@ function initCommand(opts) {
|
|
|
4721
4786
|
console.log("");
|
|
4722
4787
|
}
|
|
4723
4788
|
}
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
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
|
+
}
|
|
4742
4871
|
}
|
|
4743
|
-
} catch {
|
|
4744
4872
|
}
|
|
4873
|
+
} catch {
|
|
4745
4874
|
}
|
|
4746
4875
|
}
|
|
4747
|
-
} catch {
|
|
4748
4876
|
}
|
|
4749
|
-
|
|
4750
|
-
}
|
|
4751
|
-
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
4752
|
-
for (const { filePath } of files) {
|
|
4753
|
-
const rel = path21.relative(cwd, filePath);
|
|
4754
|
-
const content = fs22.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
4755
|
-
results.push(`### File: ${rel}
|
|
4756
|
-
\`\`\`typescript
|
|
4757
|
-
${content}
|
|
4758
|
-
\`\`\``);
|
|
4877
|
+
} catch {
|
|
4759
4878
|
}
|
|
4760
|
-
return
|
|
4879
|
+
return result;
|
|
4761
4880
|
}
|
|
4762
|
-
function
|
|
4881
|
+
function scanRoutes(dir, baseDir, prefix, result) {
|
|
4882
|
+
let entries;
|
|
4763
4883
|
try {
|
|
4764
|
-
|
|
4765
|
-
try {
|
|
4766
|
-
const configPath = path21.join(cwd, "kody.config.json");
|
|
4767
|
-
if (fs22.existsSync(configPath)) {
|
|
4768
|
-
const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
|
|
4769
|
-
if (config.github?.owner && config.github?.repo) {
|
|
4770
|
-
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
4771
|
-
}
|
|
4772
|
-
}
|
|
4773
|
-
} catch {
|
|
4774
|
-
}
|
|
4775
|
-
if (!repoSlug) return;
|
|
4776
|
-
execFileSync12("gh", [
|
|
4777
|
-
"issue",
|
|
4778
|
-
"comment",
|
|
4779
|
-
String(issueNumber),
|
|
4780
|
-
"--repo",
|
|
4781
|
-
repoSlug,
|
|
4782
|
-
"--body",
|
|
4783
|
-
body
|
|
4784
|
-
], {
|
|
4785
|
-
cwd,
|
|
4786
|
-
encoding: "utf-8",
|
|
4787
|
-
timeout: 15e3,
|
|
4788
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
4789
|
-
});
|
|
4884
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
4790
4885
|
} catch {
|
|
4886
|
+
return;
|
|
4791
4887
|
}
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
if (issueNumber) {
|
|
4800
|
-
ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
|
|
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;
|
|
4801
4895
|
}
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
if (
|
|
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
|
+
var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
|
|
5068
|
+
function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
5069
|
+
const srcDir = path6.join(cwd, "src");
|
|
5070
|
+
const baseDir = fs7.existsSync(srcDir) ? srcDir : cwd;
|
|
5071
|
+
const results = [];
|
|
5072
|
+
function walk(dir) {
|
|
5073
|
+
const entries = [];
|
|
5074
|
+
try {
|
|
5075
|
+
for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
|
|
5076
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
5077
|
+
const full = path6.join(dir, entry.name);
|
|
5078
|
+
if (entry.isDirectory()) {
|
|
5079
|
+
entries.push(...walk(full));
|
|
5080
|
+
} else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
|
|
5081
|
+
try {
|
|
5082
|
+
const stat = fs7.statSync(full);
|
|
5083
|
+
if (stat.size >= 200 && stat.size <= 5e3) {
|
|
5084
|
+
entries.push({ filePath: full, size: stat.size });
|
|
5085
|
+
}
|
|
5086
|
+
} catch {
|
|
5087
|
+
}
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
} catch {
|
|
5091
|
+
}
|
|
5092
|
+
return entries;
|
|
5093
|
+
}
|
|
5094
|
+
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
5095
|
+
for (const { filePath } of files) {
|
|
5096
|
+
const rel = path6.relative(cwd, filePath);
|
|
5097
|
+
const content = fs7.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
5098
|
+
results.push(`### File: ${rel}
|
|
5099
|
+
\`\`\`typescript
|
|
5100
|
+
${content}
|
|
5101
|
+
\`\`\``);
|
|
5102
|
+
}
|
|
5103
|
+
return results.join("\n\n");
|
|
5104
|
+
}
|
|
5105
|
+
function ghComment(issueNumber, body, cwd) {
|
|
5106
|
+
try {
|
|
5107
|
+
let repoSlug = "";
|
|
5108
|
+
try {
|
|
5109
|
+
const configPath = path6.join(cwd, "kody.config.json");
|
|
5110
|
+
if (fs7.existsSync(configPath)) {
|
|
5111
|
+
const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
5112
|
+
if (config.github?.owner && config.github?.repo) {
|
|
5113
|
+
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
5114
|
+
}
|
|
5115
|
+
}
|
|
5116
|
+
} catch {
|
|
5117
|
+
}
|
|
5118
|
+
if (!repoSlug) return;
|
|
5119
|
+
execFileSync5("gh", [
|
|
5120
|
+
"issue",
|
|
5121
|
+
"comment",
|
|
5122
|
+
String(issueNumber),
|
|
5123
|
+
"--repo",
|
|
5124
|
+
repoSlug,
|
|
5125
|
+
"--body",
|
|
5126
|
+
body
|
|
5127
|
+
], {
|
|
5128
|
+
cwd,
|
|
5129
|
+
encoding: "utf-8",
|
|
5130
|
+
timeout: 15e3,
|
|
5131
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5132
|
+
});
|
|
5133
|
+
} catch {
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
function bootstrapCommand(opts, pkgRoot) {
|
|
5137
|
+
const cwd = process.cwd();
|
|
5138
|
+
const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
|
|
5139
|
+
console.log(`
|
|
5140
|
+
\u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
|
|
5141
|
+
`);
|
|
5142
|
+
if (issueNumber) {
|
|
5143
|
+
ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
|
|
5144
|
+
}
|
|
5145
|
+
const readIfExists = (rel, maxChars = 3e3) => {
|
|
5146
|
+
const p = path6.join(cwd, rel);
|
|
5147
|
+
if (fs7.existsSync(p)) return fs7.readFileSync(p, "utf-8").slice(0, maxChars);
|
|
4805
5148
|
return null;
|
|
4806
5149
|
};
|
|
4807
5150
|
let repoContext = "";
|
|
@@ -4836,14 +5179,14 @@ ${sampleFiles}
|
|
|
4836
5179
|
|
|
4837
5180
|
`;
|
|
4838
5181
|
try {
|
|
4839
|
-
const topDirs =
|
|
5182
|
+
const topDirs = fs7.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
4840
5183
|
repoContext += `## Top-level directories
|
|
4841
5184
|
${topDirs.join(", ")}
|
|
4842
5185
|
|
|
4843
5186
|
`;
|
|
4844
|
-
const srcDir =
|
|
4845
|
-
if (
|
|
4846
|
-
const srcDirs =
|
|
5187
|
+
const srcDir = path6.join(cwd, "src");
|
|
5188
|
+
if (fs7.existsSync(srcDir)) {
|
|
5189
|
+
const srcDirs = fs7.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4847
5190
|
if (srcDirs.length > 0) repoContext += `## src/ subdirectories
|
|
4848
5191
|
${srcDirs.join(", ")}
|
|
4849
5192
|
|
|
@@ -4853,19 +5196,19 @@ ${srcDirs.join(", ")}
|
|
|
4853
5196
|
}
|
|
4854
5197
|
const existingFiles = [];
|
|
4855
5198
|
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"]) {
|
|
4856
|
-
if (
|
|
5199
|
+
if (fs7.existsSync(path6.join(cwd, f))) existingFiles.push(f);
|
|
4857
5200
|
}
|
|
4858
5201
|
if (existingFiles.length) repoContext += `## Config files present
|
|
4859
5202
|
${existingFiles.join(", ")}
|
|
4860
5203
|
|
|
4861
5204
|
`;
|
|
4862
5205
|
console.log("\u2500\u2500 Project Memory \u2500\u2500");
|
|
4863
|
-
const memoryDir =
|
|
4864
|
-
|
|
4865
|
-
const archPath =
|
|
4866
|
-
const conventionsPath =
|
|
4867
|
-
const existingArch =
|
|
4868
|
-
const existingConv =
|
|
5206
|
+
const memoryDir = path6.join(cwd, ".kody", "memory");
|
|
5207
|
+
fs7.mkdirSync(memoryDir, { recursive: true });
|
|
5208
|
+
const archPath = path6.join(memoryDir, "architecture.md");
|
|
5209
|
+
const conventionsPath = path6.join(memoryDir, "conventions.md");
|
|
5210
|
+
const existingArch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
|
|
5211
|
+
const existingConv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
|
|
4869
5212
|
const hasExisting = !!(existingArch || existingConv);
|
|
4870
5213
|
const extendInstruction = hasExisting && !opts.force ? `
|
|
4871
5214
|
## Existing Documentation (EXTEND, do not replace)
|
|
@@ -4906,7 +5249,7 @@ Output ONLY valid JSON. No markdown fences. No explanation.
|
|
|
4906
5249
|
${repoContext}`;
|
|
4907
5250
|
console.log(" \u23F3 Analyzing project...");
|
|
4908
5251
|
try {
|
|
4909
|
-
const output =
|
|
5252
|
+
const output = execFileSync5("claude", [
|
|
4910
5253
|
"--print",
|
|
4911
5254
|
"--model",
|
|
4912
5255
|
"haiku",
|
|
@@ -4921,12 +5264,12 @@ ${repoContext}`;
|
|
|
4921
5264
|
const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4922
5265
|
const parsed = JSON.parse(cleaned);
|
|
4923
5266
|
if (parsed.architecture) {
|
|
4924
|
-
|
|
5267
|
+
fs7.writeFileSync(archPath, parsed.architecture);
|
|
4925
5268
|
const lineCount = parsed.architecture.split("\n").length;
|
|
4926
5269
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
|
|
4927
5270
|
}
|
|
4928
5271
|
if (parsed.conventions) {
|
|
4929
|
-
|
|
5272
|
+
fs7.writeFileSync(conventionsPath, parsed.conventions);
|
|
4930
5273
|
const lineCount = parsed.conventions.split("\n").length;
|
|
4931
5274
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
|
|
4932
5275
|
}
|
|
@@ -4935,39 +5278,39 @@ ${repoContext}`;
|
|
|
4935
5278
|
const detected = detectArchitectureBasic(cwd);
|
|
4936
5279
|
if (detected.length > 0) {
|
|
4937
5280
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4938
|
-
|
|
5281
|
+
fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
4939
5282
|
|
|
4940
5283
|
## Overview
|
|
4941
5284
|
${detected.join("\n")}
|
|
4942
5285
|
`);
|
|
4943
5286
|
console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
|
|
4944
5287
|
}
|
|
4945
|
-
|
|
5288
|
+
fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
4946
5289
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
4947
5290
|
}
|
|
4948
5291
|
console.log("\n\u2500\u2500 Step Files \u2500\u2500");
|
|
4949
|
-
const stepsDir =
|
|
4950
|
-
|
|
4951
|
-
const arch =
|
|
4952
|
-
const conv =
|
|
5292
|
+
const stepsDir = path6.join(cwd, ".kody", "steps");
|
|
5293
|
+
fs7.mkdirSync(stepsDir, { recursive: true });
|
|
5294
|
+
const arch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
|
|
5295
|
+
const conv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
|
|
4953
5296
|
console.log(" \u23F3 Customizing step files...");
|
|
4954
5297
|
let stepCount = 0;
|
|
4955
5298
|
for (const stage of STEP_STAGES) {
|
|
4956
|
-
const templatePath =
|
|
4957
|
-
if (!
|
|
5299
|
+
const templatePath = path6.join(pkgRoot, "prompts", `${stage}.md`);
|
|
5300
|
+
if (!fs7.existsSync(templatePath)) {
|
|
4958
5301
|
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
4959
5302
|
continue;
|
|
4960
5303
|
}
|
|
4961
|
-
const stepOutputPath =
|
|
4962
|
-
if (
|
|
5304
|
+
const stepOutputPath = path6.join(stepsDir, `${stage}.md`);
|
|
5305
|
+
if (fs7.existsSync(stepOutputPath) && !opts.force) {
|
|
4963
5306
|
console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
|
|
4964
5307
|
continue;
|
|
4965
5308
|
}
|
|
4966
|
-
const defaultPrompt =
|
|
5309
|
+
const defaultPrompt = fs7.readFileSync(templatePath, "utf-8");
|
|
4967
5310
|
const contextPlaceholder = "{{TASK_CONTEXT}}";
|
|
4968
5311
|
const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
|
|
4969
5312
|
if (placeholderIdx === -1) {
|
|
4970
|
-
|
|
5313
|
+
fs7.copyFileSync(templatePath, stepOutputPath);
|
|
4971
5314
|
stepCount++;
|
|
4972
5315
|
console.log(` \u2713 ${stage}.md`);
|
|
4973
5316
|
continue;
|
|
@@ -5009,7 +5352,7 @@ ${repoContext}
|
|
|
5009
5352
|
|
|
5010
5353
|
REMINDER: Output the full prompt template first (unchanged), then your three appended sections. Do NOT include "${contextPlaceholder}".`;
|
|
5011
5354
|
try {
|
|
5012
|
-
const output =
|
|
5355
|
+
const output = execFileSync5("claude", [
|
|
5013
5356
|
"--print",
|
|
5014
5357
|
"--model",
|
|
5015
5358
|
"haiku",
|
|
@@ -5024,23 +5367,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5024
5367
|
let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
5025
5368
|
cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
|
|
5026
5369
|
const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
|
|
5027
|
-
|
|
5370
|
+
fs7.writeFileSync(stepOutputPath, finalPrompt);
|
|
5028
5371
|
stepCount++;
|
|
5029
5372
|
console.log(` \u2713 ${stage}.md`);
|
|
5030
5373
|
} catch {
|
|
5031
5374
|
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
5032
|
-
|
|
5375
|
+
fs7.copyFileSync(templatePath, stepOutputPath);
|
|
5033
5376
|
stepCount++;
|
|
5034
5377
|
}
|
|
5035
5378
|
}
|
|
5036
5379
|
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
5037
5380
|
console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
|
|
5038
|
-
const qaGuidePath =
|
|
5039
|
-
if (!
|
|
5381
|
+
const qaGuidePath = path6.join(cwd, ".kody", "qa-guide.md");
|
|
5382
|
+
if (!fs7.existsSync(qaGuidePath) || opts.force) {
|
|
5040
5383
|
const discovery = discoverQaContext(cwd);
|
|
5041
5384
|
if (discovery.routes.length > 0) {
|
|
5042
5385
|
const qaGuide = generateQaGuide(discovery);
|
|
5043
|
-
|
|
5386
|
+
fs7.writeFileSync(qaGuidePath, qaGuide);
|
|
5044
5387
|
console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
|
|
5045
5388
|
if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
|
|
5046
5389
|
if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
|
|
@@ -5055,9 +5398,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5055
5398
|
try {
|
|
5056
5399
|
let repoSlug = "";
|
|
5057
5400
|
try {
|
|
5058
|
-
const configPath =
|
|
5059
|
-
if (
|
|
5060
|
-
const config = JSON.parse(
|
|
5401
|
+
const configPath = path6.join(cwd, "kody.config.json");
|
|
5402
|
+
if (fs7.existsSync(configPath)) {
|
|
5403
|
+
const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
5061
5404
|
if (config.github?.owner && config.github?.repo) {
|
|
5062
5405
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
5063
5406
|
}
|
|
@@ -5084,7 +5427,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5084
5427
|
];
|
|
5085
5428
|
for (const label of labels) {
|
|
5086
5429
|
try {
|
|
5087
|
-
|
|
5430
|
+
execFileSync5("gh", [
|
|
5088
5431
|
"label",
|
|
5089
5432
|
"create",
|
|
5090
5433
|
label.name,
|
|
@@ -5104,7 +5447,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5104
5447
|
console.log(` \u2713 ${label.name}`);
|
|
5105
5448
|
} catch {
|
|
5106
5449
|
try {
|
|
5107
|
-
|
|
5450
|
+
execFileSync5("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
|
|
5108
5451
|
cwd,
|
|
5109
5452
|
encoding: "utf-8",
|
|
5110
5453
|
timeout: 1e4,
|
|
@@ -5130,21 +5473,21 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5130
5473
|
".kody/memory/conventions.md",
|
|
5131
5474
|
".kody/qa-guide.md",
|
|
5132
5475
|
...installedSkillPaths
|
|
5133
|
-
].filter((f) =>
|
|
5134
|
-
if (
|
|
5476
|
+
].filter((f) => fs7.existsSync(path6.join(cwd, f)));
|
|
5477
|
+
if (fs7.existsSync(path6.join(cwd, "skills-lock.json"))) {
|
|
5135
5478
|
filesToCommit.push("skills-lock.json");
|
|
5136
5479
|
}
|
|
5137
5480
|
for (const stage of STEP_STAGES) {
|
|
5138
5481
|
const stepFile = `.kody/steps/${stage}.md`;
|
|
5139
|
-
if (
|
|
5482
|
+
if (fs7.existsSync(path6.join(cwd, stepFile))) {
|
|
5140
5483
|
filesToCommit.push(stepFile);
|
|
5141
5484
|
}
|
|
5142
5485
|
}
|
|
5143
5486
|
if (filesToCommit.length > 0) {
|
|
5144
5487
|
try {
|
|
5145
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
5488
|
+
const fullPaths = filesToCommit.map((f) => path6.join(cwd, f));
|
|
5146
5489
|
for (let pass = 0; pass < 2; pass++) {
|
|
5147
|
-
|
|
5490
|
+
execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
|
|
5148
5491
|
cwd,
|
|
5149
5492
|
encoding: "utf-8",
|
|
5150
5493
|
timeout: 3e4,
|
|
@@ -5160,24 +5503,24 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5160
5503
|
try {
|
|
5161
5504
|
if (isCI3) {
|
|
5162
5505
|
const branchName = `kody-bootstrap-${Date.now()}`;
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
const staged =
|
|
5506
|
+
execFileSync5("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
|
|
5507
|
+
execFileSync5("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
5508
|
+
const staged = execFileSync5("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
5166
5509
|
if (staged) {
|
|
5167
|
-
|
|
5168
|
-
|
|
5510
|
+
execFileSync5("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" });
|
|
5511
|
+
execFileSync5("git", ["push", "-u", "origin", branchName], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
5169
5512
|
console.log(` \u2713 Pushed branch: ${branchName}`);
|
|
5170
5513
|
let baseBranch = "main";
|
|
5171
5514
|
try {
|
|
5172
|
-
const configPath =
|
|
5173
|
-
if (
|
|
5174
|
-
const config = JSON.parse(
|
|
5515
|
+
const configPath = path6.join(cwd, "kody.config.json");
|
|
5516
|
+
if (fs7.existsSync(configPath)) {
|
|
5517
|
+
const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
5175
5518
|
baseBranch = config.git?.defaultBranch ?? "main";
|
|
5176
5519
|
}
|
|
5177
5520
|
} catch {
|
|
5178
5521
|
}
|
|
5179
5522
|
try {
|
|
5180
|
-
const prUrl =
|
|
5523
|
+
const prUrl = execFileSync5("gh", [
|
|
5181
5524
|
"pr",
|
|
5182
5525
|
"create",
|
|
5183
5526
|
"--title",
|
|
@@ -5216,13 +5559,13 @@ Create it manually.`, cwd);
|
|
|
5216
5559
|
console.log(" \u25CB No new changes to commit");
|
|
5217
5560
|
}
|
|
5218
5561
|
} else {
|
|
5219
|
-
|
|
5220
|
-
const staged =
|
|
5562
|
+
execFileSync5("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
5563
|
+
const staged = execFileSync5("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
5221
5564
|
if (staged) {
|
|
5222
|
-
|
|
5565
|
+
execFileSync5("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" });
|
|
5223
5566
|
console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
|
|
5224
5567
|
try {
|
|
5225
|
-
|
|
5568
|
+
execFileSync5("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
5226
5569
|
console.log(" \u2713 Pushed to origin");
|
|
5227
5570
|
} catch {
|
|
5228
5571
|
console.log(" \u25CB Push failed \u2014 run 'git push' manually");
|
|
@@ -5242,303 +5585,22 @@ Create it manually.`, cwd);
|
|
|
5242
5585
|
console.log(" \u2713 Project bootstrap complete!");
|
|
5243
5586
|
console.log(" Kody now has project-specific memory and customized step files.\n");
|
|
5244
5587
|
}
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
};
|
|
5255
|
-
try {
|
|
5256
|
-
const pkg = JSON.parse(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
|
|
5257
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5258
|
-
const pm = fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs22.existsSync(path21.join(cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
5259
|
-
if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
|
|
5260
|
-
if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
|
|
5261
|
-
else if (allDeps.vite) result.devPort = 5173;
|
|
5262
|
-
} catch {
|
|
5263
|
-
}
|
|
5264
|
-
const appDirs = ["src/app", "app"];
|
|
5265
|
-
for (const appDir of appDirs) {
|
|
5266
|
-
const fullAppDir = path21.join(cwd, appDir);
|
|
5267
|
-
if (!fs22.existsSync(fullAppDir)) continue;
|
|
5268
|
-
scanRoutes(fullAppDir, appDir, "", result);
|
|
5269
|
-
break;
|
|
5270
|
-
}
|
|
5271
|
-
const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
5272
|
-
for (const p of authPatterns) {
|
|
5273
|
-
if (fs22.existsSync(path21.join(cwd, p))) result.authFiles.push(p);
|
|
5274
|
-
}
|
|
5275
|
-
const authConfigGlobs = [
|
|
5276
|
-
"src/app/api/auth",
|
|
5277
|
-
"src/auth",
|
|
5278
|
-
"src/lib/auth",
|
|
5279
|
-
"auth.config.ts",
|
|
5280
|
-
"auth.ts",
|
|
5281
|
-
"src/app/api/oauth"
|
|
5282
|
-
];
|
|
5283
|
-
for (const g of authConfigGlobs) {
|
|
5284
|
-
if (fs22.existsSync(path21.join(cwd, g))) result.authFiles.push(g);
|
|
5285
|
-
}
|
|
5286
|
-
try {
|
|
5287
|
-
const rolePaths = [
|
|
5288
|
-
"src/types",
|
|
5289
|
-
"src/lib",
|
|
5290
|
-
"src/utils",
|
|
5291
|
-
"src/constants",
|
|
5292
|
-
"src/access",
|
|
5293
|
-
"src/collections"
|
|
5294
|
-
];
|
|
5295
|
-
for (const rp of rolePaths) {
|
|
5296
|
-
const dir = path21.join(cwd, rp);
|
|
5297
|
-
if (!fs22.existsSync(dir)) continue;
|
|
5298
|
-
const files = fs22.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
5299
|
-
for (const f of files) {
|
|
5300
|
-
try {
|
|
5301
|
-
const content = fs22.readFileSync(path21.join(dir, f), "utf-8").slice(0, 5e3);
|
|
5302
|
-
const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
|
|
5303
|
-
if (roleMatches) {
|
|
5304
|
-
for (const m of roleMatches) {
|
|
5305
|
-
const val = m.match(/['"](\w+)['"]/);
|
|
5306
|
-
if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
|
|
5307
|
-
}
|
|
5308
|
-
}
|
|
5309
|
-
const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
|
|
5310
|
-
if (enumMatch) {
|
|
5311
|
-
const vals = enumMatch[1].match(/['"](\w+)['"]/g);
|
|
5312
|
-
if (vals) {
|
|
5313
|
-
for (const v of vals) {
|
|
5314
|
-
const clean = v.replace(/['"]/g, "");
|
|
5315
|
-
if (!result.roles.includes(clean)) result.roles.push(clean);
|
|
5316
|
-
}
|
|
5317
|
-
}
|
|
5318
|
-
}
|
|
5319
|
-
} catch {
|
|
5320
|
-
}
|
|
5321
|
-
}
|
|
5322
|
-
}
|
|
5323
|
-
} catch {
|
|
5324
|
-
}
|
|
5325
|
-
return result;
|
|
5326
|
-
}
|
|
5327
|
-
function scanRoutes(dir, baseDir, prefix, result) {
|
|
5328
|
-
let entries;
|
|
5329
|
-
try {
|
|
5330
|
-
entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
5331
|
-
} catch {
|
|
5332
|
-
return;
|
|
5333
|
-
}
|
|
5334
|
-
const hasPage = entries.some((e) => e.isFile() && /^page\.(tsx?|jsx?)$/.test(e.name));
|
|
5335
|
-
if (hasPage) {
|
|
5336
|
-
const routePath = prefix || "/";
|
|
5337
|
-
const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
|
|
5338
|
-
result.routes.push({ path: routePath, group });
|
|
5339
|
-
if (prefix.includes("/login")) result.loginPage = routePath;
|
|
5340
|
-
if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
|
|
5341
|
-
}
|
|
5342
|
-
for (const entry of entries) {
|
|
5343
|
-
if (!entry.isDirectory()) continue;
|
|
5344
|
-
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
5345
|
-
let segment = entry.name;
|
|
5346
|
-
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
5347
|
-
scanRoutes(path21.join(dir, entry.name), baseDir, prefix, result);
|
|
5348
|
-
continue;
|
|
5349
|
-
}
|
|
5350
|
-
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
5351
|
-
segment = `:${segment.slice(1, -1)}`;
|
|
5352
|
-
}
|
|
5353
|
-
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
5354
|
-
segment = `:${segment.slice(2, -2)}?`;
|
|
5355
|
-
}
|
|
5356
|
-
scanRoutes(path21.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
|
|
5357
|
-
}
|
|
5358
|
-
}
|
|
5359
|
-
function generateQaGuide(discovery) {
|
|
5360
|
-
const lines = ["# QA Guide", "", "## Authentication", ""];
|
|
5361
|
-
if (discovery.loginPage) {
|
|
5362
|
-
lines.push(`- Login page: \`${discovery.loginPage}\``);
|
|
5363
|
-
}
|
|
5364
|
-
lines.push(
|
|
5365
|
-
"",
|
|
5366
|
-
"### Test Accounts",
|
|
5367
|
-
"<!-- Fill in your test/preview environment credentials below -->",
|
|
5368
|
-
"| Role | Email | Password |",
|
|
5369
|
-
"|------|-------|----------|",
|
|
5370
|
-
"| Admin | admin@example.com | CHANGE_ME |",
|
|
5371
|
-
"| User | user@example.com | CHANGE_ME |",
|
|
5372
|
-
"",
|
|
5373
|
-
"### Login Steps",
|
|
5374
|
-
`1. Navigate to \`${discovery.loginPage ?? "/login"}\``,
|
|
5375
|
-
"2. Enter credentials from the test accounts table above",
|
|
5376
|
-
"3. Submit the login form",
|
|
5377
|
-
"4. Verify redirect to dashboard or home page"
|
|
5378
|
-
);
|
|
5379
|
-
if (discovery.authFiles.length > 0) {
|
|
5380
|
-
lines.push("", "### Auth Files");
|
|
5381
|
-
for (const f of discovery.authFiles) {
|
|
5382
|
-
lines.push(`- \`${f}\``);
|
|
5383
|
-
}
|
|
5384
|
-
}
|
|
5385
|
-
if (discovery.roles.length > 0) {
|
|
5386
|
-
lines.push("", "## Roles", "");
|
|
5387
|
-
for (const role of discovery.roles) {
|
|
5388
|
-
lines.push(`- \`${role}\``);
|
|
5389
|
-
}
|
|
5390
|
-
}
|
|
5391
|
-
lines.push("", "## Key Pages", "");
|
|
5392
|
-
const groups = {};
|
|
5393
|
-
for (const route of discovery.routes) {
|
|
5394
|
-
if (!groups[route.group]) groups[route.group] = [];
|
|
5395
|
-
groups[route.group].push(route.path);
|
|
5396
|
-
}
|
|
5397
|
-
for (const [group, routes] of Object.entries(groups)) {
|
|
5398
|
-
lines.push(`### ${group.charAt(0).toUpperCase() + group.slice(1)}`);
|
|
5399
|
-
const sorted = routes.sort();
|
|
5400
|
-
for (const r of sorted.slice(0, 20)) {
|
|
5401
|
-
lines.push(`- \`${r}\``);
|
|
5402
|
-
}
|
|
5403
|
-
if (sorted.length > 20) {
|
|
5404
|
-
lines.push(`- ... and ${sorted.length - 20} more`);
|
|
5405
|
-
}
|
|
5406
|
-
lines.push("");
|
|
5407
|
-
}
|
|
5408
|
-
lines.push(
|
|
5409
|
-
"## Dev Server",
|
|
5410
|
-
"",
|
|
5411
|
-
`- Command: \`${discovery.devCommand || "pnpm dev"}\``,
|
|
5412
|
-
`- URL: \`http://localhost:${discovery.devPort}\``,
|
|
5413
|
-
""
|
|
5414
|
-
);
|
|
5415
|
-
return lines.join("\n");
|
|
5416
|
-
}
|
|
5417
|
-
function detectArchitectureBasic(cwd) {
|
|
5418
|
-
const detected = [];
|
|
5419
|
-
const pkgPath = path21.join(cwd, "package.json");
|
|
5420
|
-
if (fs22.existsSync(pkgPath)) {
|
|
5421
|
-
try {
|
|
5422
|
-
const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
|
|
5423
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5424
|
-
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
5425
|
-
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
5426
|
-
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
5427
|
-
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
5428
|
-
else if (allDeps.hono) detected.push(`- Framework: Hono ${allDeps.hono}`);
|
|
5429
|
-
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
5430
|
-
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
5431
|
-
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
5432
|
-
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
5433
|
-
if (allDeps.prettier) detected.push(`- Formatting: prettier ${allDeps.prettier}`);
|
|
5434
|
-
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- ORM: Prisma");
|
|
5435
|
-
if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
|
|
5436
|
-
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
|
|
5437
|
-
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
5438
|
-
if (fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
5439
|
-
else if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
5440
|
-
else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
5441
|
-
else if (fs22.existsSync(path21.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
5442
|
-
} catch {
|
|
5443
|
-
}
|
|
5444
|
-
}
|
|
5445
|
-
return detected;
|
|
5446
|
-
}
|
|
5447
|
-
var SKILL_MAPPINGS = [
|
|
5448
|
-
{
|
|
5449
|
-
detect: (deps) => "next" in deps,
|
|
5450
|
-
skills: [
|
|
5451
|
-
{ package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
|
|
5452
|
-
]
|
|
5453
|
-
},
|
|
5454
|
-
{
|
|
5455
|
-
detect: (deps) => "react" in deps && !("next" in deps),
|
|
5456
|
-
skills: [
|
|
5457
|
-
{ package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
|
|
5458
|
-
]
|
|
5459
|
-
},
|
|
5460
|
-
{
|
|
5461
|
-
detect: (deps) => FRONTEND_DEPS.some((d) => d in deps),
|
|
5462
|
-
skills: [
|
|
5463
|
-
{ package: "microsoft/playwright-cli@playwright-cli", label: "Playwright browser automation" }
|
|
5464
|
-
]
|
|
5465
|
-
}
|
|
5466
|
-
];
|
|
5467
|
-
function detectSkillsForProject(cwd) {
|
|
5468
|
-
const pkgPath = path21.join(cwd, "package.json");
|
|
5469
|
-
if (!fs22.existsSync(pkgPath)) return [];
|
|
5470
|
-
try {
|
|
5471
|
-
const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
|
|
5472
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5473
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5474
|
-
const skills = [];
|
|
5475
|
-
for (const mapping of SKILL_MAPPINGS) {
|
|
5476
|
-
if (mapping.detect(allDeps)) {
|
|
5477
|
-
for (const skill of mapping.skills) {
|
|
5478
|
-
if (!seen.has(skill.package)) {
|
|
5479
|
-
seen.add(skill.package);
|
|
5480
|
-
skills.push(skill);
|
|
5481
|
-
}
|
|
5482
|
-
}
|
|
5483
|
-
}
|
|
5484
|
-
}
|
|
5485
|
-
return skills;
|
|
5486
|
-
} catch {
|
|
5487
|
-
return [];
|
|
5488
|
-
}
|
|
5489
|
-
}
|
|
5490
|
-
function installSkillsForProject(cwd) {
|
|
5491
|
-
const skills = detectSkillsForProject(cwd);
|
|
5492
|
-
if (skills.length === 0) {
|
|
5493
|
-
console.log(" \u25CB No skills to install (no frontend framework detected)");
|
|
5494
|
-
return [];
|
|
5495
|
-
}
|
|
5496
|
-
let installedSkills = {};
|
|
5497
|
-
const lockPath = path21.join(cwd, "skills-lock.json");
|
|
5498
|
-
if (fs22.existsSync(lockPath)) {
|
|
5499
|
-
try {
|
|
5500
|
-
const lock = JSON.parse(fs22.readFileSync(lockPath, "utf-8"));
|
|
5501
|
-
installedSkills = lock.skills ?? {};
|
|
5502
|
-
} catch {
|
|
5503
|
-
}
|
|
5504
|
-
}
|
|
5505
|
-
const installedPaths = [];
|
|
5506
|
-
for (const skill of skills) {
|
|
5507
|
-
const skillName = skill.package.split("@").pop() ?? "";
|
|
5508
|
-
if (skillName in installedSkills) {
|
|
5509
|
-
console.log(` \u25CB ${skill.label} \u2014 already installed`);
|
|
5510
|
-
const agentPath = `.agents/skills/${skillName}`;
|
|
5511
|
-
const claudePath = `.claude/skills/${skillName}`;
|
|
5512
|
-
if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
5513
|
-
if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
5514
|
-
continue;
|
|
5515
|
-
}
|
|
5516
|
-
try {
|
|
5517
|
-
console.log(` Installing: ${skill.label} (${skill.package})`);
|
|
5518
|
-
execFileSync12("npx", ["skills", "add", skill.package, "--yes"], {
|
|
5519
|
-
cwd,
|
|
5520
|
-
encoding: "utf-8",
|
|
5521
|
-
timeout: 6e4,
|
|
5522
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5523
|
-
});
|
|
5524
|
-
const skillName2 = skill.package.split("@").pop() ?? "";
|
|
5525
|
-
const agentPath = `.agents/skills/${skillName2}`;
|
|
5526
|
-
const claudePath = `.claude/skills/${skillName2}`;
|
|
5527
|
-
if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
5528
|
-
if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
5529
|
-
console.log(` \u2713 ${skill.label}`);
|
|
5530
|
-
} catch (err) {
|
|
5531
|
-
console.log(` \u2717 ${skill.label} \u2014 failed to install`);
|
|
5532
|
-
}
|
|
5533
|
-
}
|
|
5534
|
-
return installedPaths;
|
|
5588
|
+
|
|
5589
|
+
// src/bin/cli.ts
|
|
5590
|
+
init_architecture_detection();
|
|
5591
|
+
var __dirname = path27.dirname(fileURLToPath(import.meta.url));
|
|
5592
|
+
var PKG_ROOT = path27.resolve(__dirname, "..", "..");
|
|
5593
|
+
function getVersion() {
|
|
5594
|
+
const pkgPath = path27.join(PKG_ROOT, "package.json");
|
|
5595
|
+
const pkg = JSON.parse(fs29.readFileSync(pkgPath, "utf-8"));
|
|
5596
|
+
return pkg.version;
|
|
5535
5597
|
}
|
|
5536
5598
|
var args = process.argv.slice(2);
|
|
5537
5599
|
var command = args[0];
|
|
5538
5600
|
if (command === "init") {
|
|
5539
|
-
initCommand({ force: args.includes("--force") });
|
|
5601
|
+
initCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
5540
5602
|
} else if (command === "bootstrap") {
|
|
5541
|
-
bootstrapCommand({ force: args.includes("--force") });
|
|
5603
|
+
bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
5542
5604
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
5543
5605
|
console.log(getVersion());
|
|
5544
5606
|
} else {
|
|
@@ -5546,7 +5608,7 @@ if (command === "init") {
|
|
|
5546
5608
|
}
|
|
5547
5609
|
export {
|
|
5548
5610
|
buildConfig,
|
|
5549
|
-
|
|
5611
|
+
checkCommand,
|
|
5550
5612
|
checkFile,
|
|
5551
5613
|
checkGhAuth,
|
|
5552
5614
|
checkGhRepoAccess,
|