@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 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 checkCommand(command2, args2) {
140
+ function checkCommand2(command2, args2) {
86
141
  try {
87
- execFileSync(command2, args2, { timeout: 1e4, stdio: "pipe" });
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 checkCommand("claude", ["--version"]);
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 fs from "fs";
276
- import * as path from "path";
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 = path.join(_configDir ?? process.cwd(), "kody.config.json");
294
- if (fs.existsSync(configPath)) {
416
+ const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
417
+ if (fs8.existsSync(configPath)) {
295
418
  try {
296
- const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
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 execFileSync2 } from "child_process";
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 execFileSync2("git", args2, {
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
- logger.warn(" Failed to fetch origin");
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
- logger.warn(" Failed to fetch latest from origin");
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
- logger.warn(" Failed to fetch latest from origin");
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 execFileSync3 } from "child_process";
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 execFileSync3("gh", args2, {
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
- return JSON.parse(output);
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
- logger.error(` Failed to get issue #${issueNumber}: ${err}`);
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 stderr = err?.stderr?.toString().trim();
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
- return { title: data.title, body: data.body, headBranch: data.headRefName, baseBranch: data.baseRefName };
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
- logger.error(` Failed to get PR #${prNumber}: ${err}`);
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 fs2 from "fs";
886
- import * as path2 from "path";
1062
+ import * as fs9 from "fs";
1063
+ import * as path8 from "path";
887
1064
  function loadState(taskId, taskDir) {
888
- const p = path2.join(taskDir, "status.json");
889
- if (!fs2.existsSync(p)) return null;
1065
+ const p = path8.join(taskDir, "status.json");
1066
+ if (!fs9.existsSync(p)) return null;
890
1067
  try {
891
- const raw = JSON.parse(fs2.readFileSync(p, "utf-8"));
892
- if (raw.taskId === taskId) return raw;
893
- return null;
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 = path2.join(taskDir, "status.json");
1087
+ const target = path8.join(taskDir, "status.json");
904
1088
  const tmp = target + ".tmp";
905
- fs2.writeFileSync(tmp, JSON.stringify(updated, null, 2));
906
- fs2.renameSync(tmp, target);
907
- state.updatedAt = updated.updatedAt;
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 fs3 from "fs";
946
- import * as path3 from "path";
1131
+ import * as fs10 from "fs";
1132
+ import * as path9 from "path";
947
1133
  function readProjectMemory(projectDir) {
948
- const memoryDir = path3.join(projectDir, ".kody", "memory");
949
- if (!fs3.existsSync(memoryDir)) return "";
950
- const files = fs3.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
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 = fs3.readFileSync(path3.join(memoryDir, file), "utf-8").trim();
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 fs4 from "fs";
974
- import * as path4 from "path";
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 readCache(cacheDir) {
1069
- const cachePath = path4.join(cacheDir, "tier-cache.json");
1070
- if (!fs4.existsSync(cachePath)) return { version: 1, entries: {} };
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 = path4.join(projectDir, ".kody", "memory");
1112
- if (!fs4.existsSync(memoryDir)) return "";
1113
- const files = fs4.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
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 = path4.join(memoryDir, file);
1120
- const content = fs4.readFileSync(filePath, "utf-8").trim();
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, cacheDir);
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 = path4.join(taskDir, "task.md");
1145
- if (fs4.existsSync(taskMdPath)) {
1146
- const content = fs4.readFileSync(taskMdPath, "utf-8");
1147
- const selected = selectContent(taskMdPath, content, cacheDir, policy.taskDescription);
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 = path4.join(taskDir, "task.json");
1155
- if (fs4.existsSync(taskJsonPath)) {
1156
- const content = fs4.readFileSync(taskJsonPath, "utf-8");
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, cacheDir, policy.taskClassification);
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 = path4.join(taskDir, "spec.md");
1183
- if (fs4.existsSync(specPath)) {
1184
- const content = fs4.readFileSync(specPath, "utf-8");
1185
- const selected = selectContent(specPath, content, cacheDir, policy.spec);
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 = path4.join(taskDir, "plan.md");
1193
- if (fs4.existsSync(planPath)) {
1194
- const content = fs4.readFileSync(planPath, "utf-8");
1195
- const selected = selectContent(planPath, content, cacheDir, policy.plan);
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 = path4.join(taskDir, "context.md");
1203
- if (fs4.existsSync(contextMdPath)) {
1204
- const content = fs4.readFileSync(contextMdPath, "utf-8");
1205
- const selected = selectContent(contextMdPath, content, cacheDir, policy.accumulatedContext);
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, cacheDir, tier) {
1370
+ function selectContent(filePath, content, tier) {
1221
1371
  if (tier === "L2") return content;
1222
- const tiered = getTieredContent(filePath, content, cacheDir);
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 fs5 from "fs";
1337
- import * as path5 from "path";
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 = path5.join(projectDir, ".kody", "steps", `${stageName}.md`);
1341
- if (fs5.existsSync(stepFile)) {
1342
- return fs5.readFileSync(stepFile, "utf-8");
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
- path5.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
1349
- path5.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
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 (fs5.existsSync(candidate)) {
1353
- return fs5.readFileSync(candidate, "utf-8");
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 = path5.join(taskDir, "task.md");
1366
- if (fs5.existsSync(taskMdPath)) {
1367
- const taskMd = fs5.readFileSync(taskMdPath, "utf-8");
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 = path5.join(taskDir, "task.json");
1374
- if (fs5.existsSync(taskJsonPath)) {
1523
+ const taskJsonPath = path11.join(taskDir, "task.json");
1524
+ if (fs12.existsSync(taskJsonPath)) {
1375
1525
  try {
1376
- const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
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 = path5.join(taskDir, "spec.md");
1390
- if (fs5.existsSync(specPath)) {
1391
- const spec = fs5.readFileSync(specPath, "utf-8");
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 = path5.join(taskDir, "plan.md");
1399
- if (fs5.existsSync(planPath)) {
1400
- const plan = fs5.readFileSync(planPath, "utf-8");
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 = path5.join(taskDir, "context.md");
1408
- if (fs5.existsSync(contextMdPath)) {
1409
- const accumulated = fs5.readFileSync(contextMdPath, "utf-8");
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 = path5.join(taskDir, "task.json");
1427
- if (!fs5.existsSync(taskJsonPath)) return true;
1576
+ const taskJsonPath = path11.join(taskDir, "task.json");
1577
+ if (!fs12.existsSync(taskJsonPath)) return true;
1428
1578
  try {
1429
- const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
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 = path5.join(projectDir, ".kody", "qa-guide.md");
1550
- if (fs5.existsSync(qaGuidePath)) {
1551
- const qaGuide = fs5.readFileSync(qaGuidePath, "utf-8").trim();
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 fs6 from "fs";
1670
- import * as path6 from "path";
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
- fs6.writeFileSync(path6.join(ctx.taskDir, def.outputFile), result.output);
1833
+ fs13.writeFileSync(path12.join(ctx.taskDir, def.outputFile), result.output);
1733
1834
  }
1734
1835
  if (def.outputFile) {
1735
- const outputPath = path6.join(ctx.taskDir, def.outputFile);
1736
- if (!fs6.existsSync(outputPath)) {
1737
- const ext = path6.extname(def.outputFile);
1738
- const base = path6.basename(def.outputFile, ext);
1739
- const files = fs6.readdirSync(ctx.taskDir);
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
- fs6.renameSync(path6.join(ctx.taskDir, variant), outputPath);
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 = path6.join(ctx.taskDir, def.outputFile);
1751
- if (fs6.existsSync(outputPath)) {
1752
- const content = fs6.readFileSync(outputPath, "utf-8");
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
- fs6.writeFileSync(outputPath, retryResult.output);
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
- fs6.writeFileSync(outputPath, fallback);
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 = path6.join(taskDir, "context.md");
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
- fs6.appendFileSync(contextPath, entry);
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 execFileSync4 } from "child_process";
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 = execFileSync4(parts[0], parts.slice(1), {
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 execFileSync5 } from "child_process";
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 parsed = JSON.parse(cleaned);
1963
- const validClassifications = [
1964
- "fixable",
1965
- "infrastructure",
1966
- "pre-existing",
1967
- "retry",
1968
- "abort"
1969
- ];
1970
- if (validClassifications.includes(parsed.classification)) {
1971
- logger.info(` Diagnosis: ${parsed.classification} \u2014 ${parsed.reason}`);
1972
- return {
1973
- classification: parsed.classification,
1974
- reason: parsed.reason ?? "Unknown reason",
1975
- resolution: parsed.resolution ?? ""
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 fixable");
2089
+ logger.warn(" Diagnosis failed \u2014 defaulting to retry");
1983
2090
  return {
1984
- classification: "fixable",
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 = execFileSync5("git", ["diff", "--name-only", "--cached"], {
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 = execFileSync5("git", ["diff", "--name-only"], {
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 fs7 from "fs";
2039
- import * as path7 from "path";
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
- fs7.writeFileSync(path7.join(ctx.taskDir, "verify.md"), lines.join(""));
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 fs8 from "fs";
2098
- import * as path8 from "path";
2099
- import { execFileSync as execFileSync6 } from "child_process";
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 = path8.join(ctx.taskDir, "verify.md");
2110
- const errorOutput = fs8.existsSync(verifyPath) ? fs8.readFileSync(verifyPath, "utf-8") : "Unknown error";
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
- execFileSync6(parts[0], parts.slice(1), {
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 fs9 from "fs";
2207
- import * as path9 from "path";
2208
- import { execFileSync as execFileSync7 } from "child_process";
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 = path9.join(projectDir, ".kody", "tasks");
2211
- if (!fs9.existsSync(tasksDir)) return null;
2212
- const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
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 = execFileSync7("git", ["branch", "--show-current"], {
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 fs10 from "fs";
2247
- import * as path10 from "path";
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 = path10.join(input.projectDir, ".kody", "tasks", taskId);
2272
- fs10.mkdirSync(taskDir, { recursive: true });
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
- fs10.writeFileSync(path10.join(taskDir, "task.md"), taskContent);
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 = path10.join(taskDir, "review.md");
2412
+ const reviewPath = path16.join(taskDir, "review.md");
2304
2413
  let reviewContent;
2305
- if (fs10.existsSync(reviewPath)) {
2306
- reviewContent = fs10.readFileSync(reviewPath, "utf-8");
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 fs11 from "fs";
2346
- import * as path11 from "path";
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 = path11.join(ctx.taskDir, "review.md");
2361
- if (!fs11.existsSync(reviewFile)) {
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 = fs11.readFileSync(reviewFile, "utf-8");
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 fs12 from "fs";
2394
- import * as path12 from "path";
2395
- import { execFileSync as execFileSync8 } from "child_process";
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 = path12.join(ctx.taskDir, "task.json");
2399
- if (fs12.existsSync(taskJsonPath)) {
2507
+ const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2508
+ if (fs19.existsSync(taskJsonPath)) {
2400
2509
  try {
2401
- const raw = fs12.readFileSync(taskJsonPath, "utf-8");
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 = path12.join(ctx.taskDir, "review.md");
2421
- if (fs12.existsSync(reviewPath)) {
2422
- const review = fs12.readFileSync(reviewPath, "utf-8");
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 = path12.join(ctx.taskDir, "verify.md");
2440
- if (fs12.existsSync(verifyPath)) {
2441
- const verify = fs12.readFileSync(verifyPath, "utf-8");
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 = path12.join(ctx.taskDir, "plan.md");
2445
- if (fs12.existsSync(planPath)) {
2446
- const plan = fs12.readFileSync(planPath, "utf-8").trim();
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 = path12.join(ctx.taskDir, "ship.md");
2575
+ const shipPath = path18.join(ctx.taskDir, "ship.md");
2467
2576
  if (ctx.input.dryRun) {
2468
- fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
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
- fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
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
- execFileSync8("git", ["add", ctx.taskDir], {
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
- execFileSync8("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
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 = execFileSync8("git", ["remote", "get-url", "origin"], {
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 = path12.join(ctx.taskDir, "task.json");
2520
- if (fs12.existsSync(taskJsonPath)) {
2628
+ const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2629
+ if (fs19.existsSync(taskJsonPath)) {
2521
2630
  try {
2522
- const raw = fs12.readFileSync(taskJsonPath, "utf-8");
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 = path12.join(ctx.taskDir, "task.md");
2530
- if (fs12.existsSync(taskMdPath)) {
2531
- const content = fs12.readFileSync(taskMdPath, "utf-8");
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 (fs12.existsSync(taskJsonPath)) {
2647
+ if (fs19.existsSync(taskJsonPath)) {
2539
2648
  try {
2540
- const raw = fs12.readFileSync(taskJsonPath, "utf-8");
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
- fs12.writeFileSync(shipPath, `# Ship
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
- fs12.writeFileSync(shipPath, `# Ship
2693
+ fs19.writeFileSync(shipPath, `# Ship
2585
2694
 
2586
2695
  PR created: ${pr.url}
2587
2696
  PR #${pr.number}
2588
2697
  `);
2589
2698
  } else {
2590
- fs12.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
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
- fs12.writeFileSync(shipPath, `# Ship
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 fs13 from "fs";
2643
- import * as path13 from "path";
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 = path13.join(ctx.taskDir, "task.json");
2649
- if (!fs13.existsSync(taskJsonPath)) return false;
2650
- const raw = fs13.readFileSync(taskJsonPath, "utf-8");
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 = path13.join(ctx.taskDir, "plan.md");
2666
- if (!fs13.existsSync(planPath)) return false;
2667
- const plan = fs13.readFileSync(planPath, "utf-8");
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 fs14 from "fs";
2697
- import * as path14 from "path";
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 = path14.join(ctx.taskDir, "task.json");
2736
- if (!fs14.existsSync(taskJsonPath)) return null;
2737
- const raw = fs14.readFileSync(taskJsonPath, "utf-8");
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 = path14.join(ctx.taskDir, "plan.md");
2768
- const plan = fs14.existsSync(planPath) ? fs14.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
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 fs15 from "fs";
2836
- import * as path15 from "path";
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 = path15.join(ctx.projectDir, ".kody", "memory");
2843
- if (!fs15.existsSync(memoryDir)) {
2844
- fs15.mkdirSync(memoryDir, { recursive: true });
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 = path15.join(ctx.taskDir, "verify.md");
2849
- if (fs15.existsSync(verifyPath)) {
2850
- const verify = stripAnsi(fs15.readFileSync(verifyPath, "utf-8"));
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 = path15.join(ctx.taskDir, "review.md");
2860
- if (fs15.existsSync(reviewPath)) {
2861
- const review = fs15.readFileSync(reviewPath, "utf-8");
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 = path15.join(ctx.taskDir, "task.json");
2868
- if (fs15.existsSync(taskJsonPath)) {
2980
+ const taskJsonPath = path21.join(ctx.taskDir, "task.json");
2981
+ if (fs22.existsSync(taskJsonPath)) {
2869
2982
  try {
2870
- const raw = stripAnsi(fs15.readFileSync(taskJsonPath, "utf-8"));
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 = path15.join(memoryDir, "conventions.md");
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
- fs15.appendFileSync(conventionsPath, entry);
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 = path15.join(memoryDir, "architecture.md");
2897
- if (fs15.existsSync(archPath)) return;
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
- fs15.writeFileSync(archPath, content);
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
- init_context_tiers();
3024
+ init_architecture_detection();
2999
3025
  }
3000
3026
  });
3001
3027
 
3002
3028
  // src/retrospective.ts
3003
- import * as fs16 from "fs";
3004
- import * as path16 from "path";
3029
+ import * as fs23 from "fs";
3030
+ import * as path22 from "path";
3005
3031
  function readArtifact(taskDir, filename, maxChars) {
3006
- const p = path16.join(taskDir, filename);
3007
- if (!fs16.existsSync(p)) return null;
3032
+ const p = path22.join(taskDir, filename);
3033
+ if (!fs23.existsSync(p)) return null;
3008
3034
  try {
3009
- const content = fs16.readFileSync(p, "utf-8");
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 path16.join(projectDir, ".kody", "memory", "observer-log.jsonl");
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 (!fs16.existsSync(logPath)) return [];
3092
+ if (!fs23.existsSync(logPath)) return [];
3067
3093
  try {
3068
- const content = fs16.readFileSync(logPath, "utf-8");
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 = path16.dirname(logPath);
3096
- if (!fs16.existsSync(dir)) {
3097
- fs16.mkdirSync(dir, { recursive: true });
3121
+ const dir = path22.dirname(logPath);
3122
+ if (!fs23.existsSync(dir)) {
3123
+ fs23.mkdirSync(dir, { recursive: true });
3098
3124
  }
3099
- fs16.appendFileSync(logPath, JSON.stringify(entry) + "\n");
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 fs17 from "fs";
3211
- import * as path17 from "path";
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 = path17.join(ctx.taskDir, "task.md");
3225
- const title = fs17.existsSync(taskMdPath) ? fs17.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
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
- logger.warn(` Failed to create/sync feature branch: ${err}`);
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 = path17.join(taskDir, ".lock");
3234
- if (fs17.existsSync(lockPath)) {
3265
+ const lockPath = path23.join(taskDir, ".lock");
3266
+ if (fs24.existsSync(lockPath)) {
3235
3267
  try {
3236
- const pid = parseInt(fs17.readFileSync(lockPath, "utf-8").trim(), 10);
3237
- try {
3238
- process.kill(pid, 0);
3239
- throw new Error(`Pipeline already running (PID ${pid})`);
3240
- } catch (e) {
3241
- if (e.code !== "ESRCH") throw e;
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
- fs17.unlinkSync(path17.join(taskDir, ".lock"));
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.state = "failed";
3314
+ const updatedStages = { ...state.stages };
3264
3315
  for (const stage of STAGES) {
3265
- if (state.stages[stage.name]?.state === "running") {
3266
- state.stages[stage.name] = {
3267
- ...state.stages[stage.name],
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 execFileSync9 } from "child_process";
3439
- import * as fs18 from "fs";
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 = execFileSync9("claude", ["--version"], {
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
- execFileSync9("git", ["rev-parse", "--is-inside-work-tree"], {
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 = execFileSync9("pnpm", ["--version"], {
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 = execFileSync9("node", ["--version"], {
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 = execFileSync9("gh", ["--version"], {
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 (!fs18.existsSync("package.json")) throw new Error("not found");
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 fs19 from "fs";
3621
+ import * as fs26 from "fs";
3570
3622
  import * as os from "os";
3571
- import * as path18 from "path";
3572
- import { execFileSync as execFileSync10 } from "child_process";
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 = path18.join(os.tmpdir(), "kody-litellm-config.yaml");
3633
- fs19.writeFileSync(configPath, generatedConfig);
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
- execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
3690
+ execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
3639
3691
  litellmFound = true;
3640
3692
  } catch {
3641
3693
  try {
3642
- execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
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
- execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
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 = path18.join(projectDir, ".env");
3714
+ const dotenvPath = path24.join(projectDir, ".env");
3663
3715
  const dotenvVars = {};
3664
- if (fs19.existsSync(dotenvPath)) {
3665
- for (const line of fs19.readFileSync(dotenvPath, "utf-8").split("\n")) {
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) dotenvVars[match[1]] = match[2];
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 fs20 from "fs";
3707
- import * as path19 from "path";
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 = path19.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
3801
+ const statusPath = path25.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
3740
3802
  let existingState = null;
3741
- if (fs20.existsSync(statusPath)) {
3803
+ if (fs27.existsSync(statusPath)) {
3742
3804
  try {
3743
- existingState = JSON.parse(fs20.readFileSync(statusPath, "utf-8"));
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 execFileSync11 } from "child_process";
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 = execFileSync11("git", ["diff", file], {
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 fs21 from "fs";
3901
- import * as path20 from "path";
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 = "sk-ant-api03-litellm-proxy-key-00000000000000000000000000000000000000000000000000000000000000000000";
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 ? path20.resolve(input.cwd) : process.cwd();
4017
+ const projectDir = input.cwd ? path26.resolve(input.cwd) : process.cwd();
3956
4018
  if (input.cwd) {
3957
- if (!fs21.existsSync(projectDir)) {
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.taskId = taskAction.taskId;
3992
- input.fromStage = taskAction.fromStage;
3993
- input.command = "rerun";
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 = path20.join(projectDir, ".kody", "tasks", taskId);
4013
- fs21.mkdirSync(taskDir, { recursive: true });
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
- fs21.writeFileSync(path20.join(taskDir, "task.md"), input.task);
4193
+ fs28.writeFileSync(path26.join(taskDir, "task.md"), input.task);
4130
4194
  }
4131
- const taskMdPath = path20.join(taskDir, "task.md");
4132
- if (!fs21.existsSync(taskMdPath) && isPRFix && input.prNumber) {
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
- fs21.writeFileSync(taskMdPath, taskContent);
4203
+ fs28.writeFileSync(taskMdPath, taskContent);
4140
4204
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
4141
4205
  }
4142
- } else if (!fs21.existsSync(taskMdPath) && input.issueNumber) {
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
- fs21.writeFileSync(taskMdPath, taskContent);
4213
+ fs28.writeFileSync(taskMdPath, taskContent);
4150
4214
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
4151
4215
  }
4152
4216
  }
4153
- if (!fs21.existsSync(taskMdPath)) {
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 = fs21.readdirSync(taskDir);
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 fs22 from "fs";
4356
- import * as path21 from "path";
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
- var __dirname = path21.dirname(fileURLToPath(import.meta.url));
4360
- var PKG_ROOT = path21.resolve(__dirname, "..", "..");
4361
- function getVersion() {
4362
- const pkgPath = path21.join(PKG_ROOT, "package.json");
4363
- const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
4364
- return pkg.version;
4365
- }
4366
- function checkCommand2(name, args2, fix) {
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 = execFileSync12(name, args2, {
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 (fs22.existsSync(filePath)) {
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 = execFileSync12("gh", ["auth", "status"], {
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 = execFileSync12("git", ["remote", "get-url", "origin"], {
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
- execFileSync12("gh", ["repo", "view", repoSlug, "--json", "name"], {
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 = execFileSync12("gh", ["secret", "list", "--repo", repoSlug], {
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 (fs22.existsSync(path21.join(cwd, "yarn.lock"))) pm = "yarn";
4452
- else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) pm = "bun";
4453
- else if (!fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml")) && fs22.existsSync(path21.join(cwd, "package-lock.json"))) pm = "npm";
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 = execFileSync12("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
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
- execFileSync12("git", ["rev-parse", "--verify", "origin/dev"], {
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 = execFileSync12("git", ["remote", "get-url", "origin"], {
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(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
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
- function initCommand(opts) {
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 = path21.join(PKG_ROOT, "templates");
4631
+ const templatesDir = path2.join(pkgRoot, "templates");
4560
4632
  const basic = detectBasicConfig(cwd);
4561
- const workflowSrc = path21.join(templatesDir, "kody.yml");
4562
- const workflowDest = path21.join(cwd, ".github", "workflows", "kody.yml");
4563
- if (!fs22.existsSync(workflowSrc)) {
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 (fs22.existsSync(workflowDest) && !opts.force) {
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
- fs22.mkdirSync(path21.dirname(workflowDest), { recursive: true });
4571
- fs22.copyFileSync(workflowSrc, workflowDest);
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 = path21.join(cwd, "kody.config.json");
4575
- if (!fs22.existsSync(configDest) || opts.force) {
4646
+ const configDest = path2.join(cwd, "kody.config.json");
4647
+ if (!fs3.existsSync(configDest) || opts.force) {
4576
4648
  const config = buildConfig(cwd, basic);
4577
- fs22.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
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 = path21.join(cwd, ".gitignore");
4583
- if (fs22.existsSync(gitignorePath)) {
4584
- const content = fs22.readFileSync(gitignorePath, "utf-8");
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
- fs22.writeFileSync(gitignorePath, updated);
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
- checkCommand2("gh", ["--version"], "Install: https://cli.github.com"),
4596
- checkCommand2("git", ["--version"], "Install git"),
4597
- checkCommand2("node", ["--version"], "Install Node.js >= 22"),
4598
- checkFile(path21.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
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
- if (c.ok) {
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 (fs22.existsSync(configDest)) {
4694
+ if (fs3.existsSync(configDest)) {
4630
4695
  try {
4631
- const config = JSON.parse(fs22.readFileSync(configDest, "utf-8"));
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) => fs22.existsSync(path21.join(cwd, 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) => path21.join(cwd, f));
4665
- execFileSync12("npx", ["prettier", "--write", ...fullPaths], {
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
- execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
4677
- const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
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
- execFileSync12("git", ["commit", "-m", "chore: Add Kody Engine workflow and config\n\nAdd GitHub Actions workflow and auto-detected configuration for Kody Engine Lite."], { cwd, stdio: "pipe" });
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
- execFileSync12("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
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
- var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
4725
- function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
4726
- const srcDir = path21.join(cwd, "src");
4727
- const baseDir = fs22.existsSync(srcDir) ? srcDir : cwd;
4728
- const results = [];
4729
- function walk(dir) {
4730
- const entries = [];
4731
- try {
4732
- for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
4733
- if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
4734
- const full = path21.join(dir, entry.name);
4735
- if (entry.isDirectory()) {
4736
- entries.push(...walk(full));
4737
- } else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
4738
- try {
4739
- const stat = fs22.statSync(full);
4740
- if (stat.size >= 200 && stat.size <= 5e3) {
4741
- entries.push({ filePath: full, size: stat.size });
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
- return entries;
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 results.join("\n\n");
4879
+ return result;
4761
4880
  }
4762
- function ghComment(issueNumber, body, cwd) {
4881
+ function scanRoutes(dir, baseDir, prefix, result) {
4882
+ let entries;
4763
4883
  try {
4764
- let repoSlug = "";
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
- function bootstrapCommand(opts = { force: false }) {
4794
- const cwd = process.cwd();
4795
- const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
4796
- console.log(`
4797
- \u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
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
- const readIfExists = (rel, maxChars = 3e3) => {
4803
- const p = path21.join(cwd, rel);
4804
- if (fs22.existsSync(p)) return fs22.readFileSync(p, "utf-8").slice(0, maxChars);
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 = fs22.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
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 = path21.join(cwd, "src");
4845
- if (fs22.existsSync(srcDir)) {
4846
- const srcDirs = fs22.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
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 (fs22.existsSync(path21.join(cwd, f))) existingFiles.push(f);
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 = path21.join(cwd, ".kody", "memory");
4864
- fs22.mkdirSync(memoryDir, { recursive: true });
4865
- const archPath = path21.join(memoryDir, "architecture.md");
4866
- const conventionsPath = path21.join(memoryDir, "conventions.md");
4867
- const existingArch = fs22.existsSync(archPath) ? fs22.readFileSync(archPath, "utf-8") : "";
4868
- const existingConv = fs22.existsSync(conventionsPath) ? fs22.readFileSync(conventionsPath, "utf-8") : "";
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 = execFileSync12("claude", [
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
- fs22.writeFileSync(archPath, parsed.architecture);
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
- fs22.writeFileSync(conventionsPath, parsed.conventions);
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
- fs22.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
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
- fs22.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
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 = path21.join(cwd, ".kody", "steps");
4950
- fs22.mkdirSync(stepsDir, { recursive: true });
4951
- const arch = fs22.existsSync(archPath) ? fs22.readFileSync(archPath, "utf-8") : "";
4952
- const conv = fs22.existsSync(conventionsPath) ? fs22.readFileSync(conventionsPath, "utf-8") : "";
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 = path21.join(PKG_ROOT, "prompts", `${stage}.md`);
4957
- if (!fs22.existsSync(templatePath)) {
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 = path21.join(stepsDir, `${stage}.md`);
4962
- if (fs22.existsSync(stepOutputPath) && !opts.force) {
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 = fs22.readFileSync(templatePath, "utf-8");
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
- fs22.copyFileSync(templatePath, stepOutputPath);
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 = execFileSync12("claude", [
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
- fs22.writeFileSync(stepOutputPath, finalPrompt);
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
- fs22.copyFileSync(templatePath, stepOutputPath);
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 = path21.join(cwd, ".kody", "qa-guide.md");
5039
- if (!fs22.existsSync(qaGuidePath) || opts.force) {
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
- fs22.writeFileSync(qaGuidePath, qaGuide);
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 = path21.join(cwd, "kody.config.json");
5059
- if (fs22.existsSync(configPath)) {
5060
- const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
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
- execFileSync12("gh", [
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
- execFileSync12("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
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) => fs22.existsSync(path21.join(cwd, f)));
5134
- if (fs22.existsSync(path21.join(cwd, "skills-lock.json"))) {
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 (fs22.existsSync(path21.join(cwd, stepFile))) {
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) => path21.join(cwd, f));
5488
+ const fullPaths = filesToCommit.map((f) => path6.join(cwd, f));
5146
5489
  for (let pass = 0; pass < 2; pass++) {
5147
- execFileSync12("npx", ["prettier", "--write", ...fullPaths], {
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
- execFileSync12("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
5164
- execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
5165
- const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
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
- execFileSync12("git", ["commit", "-m", "chore: Add Kody project memory and step files\n\nBootstrap Kody Engine with project-specific architecture, conventions, and pipeline step files."], { cwd, stdio: "pipe" });
5168
- execFileSync12("git", ["push", "-u", "origin", branchName], { cwd, stdio: "pipe", timeout: 6e4 });
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 = path21.join(cwd, "kody.config.json");
5173
- if (fs22.existsSync(configPath)) {
5174
- const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
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 = execFileSync12("gh", [
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
- execFileSync12("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
5220
- const staged = execFileSync12("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
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
- execFileSync12("git", ["commit", "-m", "chore: Add Kody project memory and step files\n\nBootstrap Kody Engine with project-specific architecture, conventions, and pipeline step files."], { cwd, stdio: "pipe" });
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
- execFileSync12("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
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
- function discoverQaContext(cwd) {
5246
- const result = {
5247
- routes: [],
5248
- authFiles: [],
5249
- loginPage: null,
5250
- adminPath: null,
5251
- roles: [],
5252
- devCommand: "",
5253
- devPort: 3e3
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
- checkCommand2 as checkCommand,
5611
+ checkCommand,
5550
5612
  checkFile,
5551
5613
  checkGhAuth,
5552
5614
  checkGhRepoAccess,