@kody-ade/kody-engine-lite 0.1.57 → 0.1.59

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
@@ -252,6 +252,17 @@ var init_logger = __esm({
252
252
  // src/config.ts
253
253
  import * as fs from "fs";
254
254
  import * as path from "path";
255
+ function needsLitellmProxy(config) {
256
+ if (config.agent.litellmUrl) return true;
257
+ if (config.agent.provider && config.agent.provider !== "anthropic") return true;
258
+ return false;
259
+ }
260
+ function getLitellmUrl(config) {
261
+ return config.agent.litellmUrl ?? LITELLM_DEFAULT_URL;
262
+ }
263
+ function providerApiKeyEnvVar(provider) {
264
+ return `${provider.toUpperCase()}_API_KEY`;
265
+ }
255
266
  function setConfigDir(dir) {
256
267
  _configDir = dir;
257
268
  _config = null;
@@ -267,7 +278,8 @@ function getProjectConfig() {
267
278
  git: { ...DEFAULT_CONFIG.git, ...raw.git },
268
279
  github: { ...DEFAULT_CONFIG.github, ...raw.github },
269
280
  paths: { ...DEFAULT_CONFIG.paths, ...raw.paths },
270
- agent: { ...DEFAULT_CONFIG.agent, ...raw.agent }
281
+ agent: { ...DEFAULT_CONFIG.agent, ...raw.agent },
282
+ contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers
271
283
  };
272
284
  } catch {
273
285
  logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
@@ -278,7 +290,7 @@ function getProjectConfig() {
278
290
  }
279
291
  return _config;
280
292
  }
281
- var DEFAULT_CONFIG, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
293
+ var DEFAULT_CONFIG, LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, TIER_TO_ANTHROPIC_IDS, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
282
294
  var init_config = __esm({
283
295
  "src/config.ts"() {
284
296
  "use strict";
@@ -306,8 +318,19 @@ var init_config = __esm({
306
318
  runner: "claude-code",
307
319
  defaultRunner: "claude",
308
320
  modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
321
+ },
322
+ contextTiers: {
323
+ enabled: true,
324
+ tokenBudget: 8e3
309
325
  }
310
326
  };
327
+ LITELLM_DEFAULT_PORT = 4e3;
328
+ LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
329
+ TIER_TO_ANTHROPIC_IDS = {
330
+ cheap: ["claude-haiku-4-5-20251001", "claude-haiku-4-5", "haiku"],
331
+ mid: ["claude-sonnet-4-6-20250514", "claude-sonnet-4-6", "sonnet"],
332
+ strong: ["claude-opus-4-6-20250514", "claude-opus-4-6", "opus"]
333
+ };
311
334
  VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
312
335
  FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1e3;
313
336
  _config = null;
@@ -775,25 +798,342 @@ var init_memory = __esm({
775
798
  }
776
799
  });
777
800
 
778
- // src/context.ts
801
+ // src/context-tiers.ts
779
802
  import * as fs4 from "fs";
780
803
  import * as path4 from "path";
804
+ import * as crypto2 from "crypto";
805
+ function estimateTokens(text) {
806
+ return Math.ceil(text.length / 4);
807
+ }
808
+ function contentHash(content) {
809
+ return crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
810
+ }
811
+ function resolveStagePolicy(stageName, stageOverrides) {
812
+ const defaults = DEFAULT_STAGE_POLICIES[stageName] ?? DEFAULT_STAGE_POLICIES.build;
813
+ const overrides = stageOverrides?.[stageName];
814
+ return overrides ? { ...defaults, ...overrides } : { ...defaults };
815
+ }
816
+ function generateL0(content, filename) {
817
+ if (!content.trim()) return "";
818
+ if (filename.endsWith(".json")) {
819
+ return generateL0Json(content);
820
+ }
821
+ const lines = content.split("\n");
822
+ const parts = [];
823
+ const headingLine = lines.find((l) => l.startsWith("#"));
824
+ if (headingLine) parts.push(headingLine);
825
+ for (const line of lines) {
826
+ if (!line.trim() || line.startsWith("#")) continue;
827
+ if (line.startsWith("-") || line.startsWith("*")) continue;
828
+ const sentence = line.split(/\.\s/)[0];
829
+ if (sentence && sentence.length > 10) {
830
+ parts.push(sentence.endsWith(".") ? sentence : sentence + ".");
831
+ break;
832
+ }
833
+ }
834
+ const result = parts.join("\n");
835
+ return result.slice(0, L0_MAX_CHARS);
836
+ }
837
+ function generateL0Json(content) {
838
+ try {
839
+ const cleaned = content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
840
+ const obj = JSON.parse(cleaned);
841
+ const fields = [];
842
+ if (obj.title) fields.push(`Title: ${obj.title}`);
843
+ if (obj.task_type) fields.push(`Type: ${obj.task_type}`);
844
+ if (obj.risk_level) fields.push(`Risk: ${obj.risk_level}`);
845
+ if (obj.estimated_complexity) fields.push(`Complexity: ${obj.estimated_complexity}`);
846
+ return fields.join(" | ") || content.slice(0, L0_MAX_CHARS);
847
+ } catch {
848
+ return content.slice(0, L0_MAX_CHARS);
849
+ }
850
+ }
851
+ function generateL1(content, filename) {
852
+ if (!content.trim()) return "";
853
+ if (filename.endsWith(".json")) {
854
+ return generateL1Json(content);
855
+ }
856
+ const lines = content.split("\n");
857
+ const parts = [];
858
+ let inSection = false;
859
+ for (const line of lines) {
860
+ if (line.startsWith("#")) {
861
+ parts.push(line);
862
+ inSection = true;
863
+ continue;
864
+ }
865
+ if (line.match(/^\s*[-*]\s/)) {
866
+ const recentBullets = parts.slice(-5).filter((p) => p.match(/^\s*[-*]\s/)).length;
867
+ if (recentBullets < 5) {
868
+ parts.push(line);
869
+ }
870
+ continue;
871
+ }
872
+ if (inSection && line.trim()) {
873
+ parts.push(line);
874
+ inSection = false;
875
+ }
876
+ }
877
+ const result = parts.join("\n");
878
+ return result.slice(0, L1_MAX_CHARS);
879
+ }
880
+ function generateL1Json(content) {
881
+ try {
882
+ const cleaned = content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
883
+ const obj = JSON.parse(cleaned);
884
+ const lines = [];
885
+ for (const [key, value] of Object.entries(obj)) {
886
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
887
+ lines.push(`- ${key}: ${value}`);
888
+ } else if (Array.isArray(value)) {
889
+ lines.push(`- ${key}: [${value.length} items] ${value.slice(0, 3).join(", ")}`);
890
+ }
891
+ }
892
+ return lines.join("\n").slice(0, L1_MAX_CHARS);
893
+ } catch {
894
+ return content.slice(0, L1_MAX_CHARS);
895
+ }
896
+ }
897
+ function readCache(cacheDir) {
898
+ const cachePath = path4.join(cacheDir, "tier-cache.json");
899
+ if (!fs4.existsSync(cachePath)) return { version: 1, entries: {} };
900
+ try {
901
+ return JSON.parse(fs4.readFileSync(cachePath, "utf-8"));
902
+ } catch {
903
+ return { version: 1, entries: {} };
904
+ }
905
+ }
906
+ function writeCache(cacheDir, cache) {
907
+ fs4.mkdirSync(cacheDir, { recursive: true });
908
+ fs4.writeFileSync(path4.join(cacheDir, "tier-cache.json"), JSON.stringify(cache, null, 2));
909
+ }
910
+ function getTieredContent(filePath, content, cacheDir) {
911
+ const hash = contentHash(content);
912
+ const key = path4.basename(filePath);
913
+ const cache = readCache(cacheDir);
914
+ if (cache.entries[key] && cache.entries[key].hash === hash) {
915
+ return cache.entries[key];
916
+ }
917
+ const tiered = {
918
+ source: filePath,
919
+ hash,
920
+ L0: generateL0(content, key),
921
+ L1: generateL1(content, key),
922
+ L2: content
923
+ };
924
+ cache.entries[key] = tiered;
925
+ writeCache(cacheDir, cache);
926
+ return tiered;
927
+ }
928
+ function invalidateCache(filePath, cacheDir) {
929
+ const key = path4.basename(filePath);
930
+ const cache = readCache(cacheDir);
931
+ if (cache.entries[key]) {
932
+ delete cache.entries[key];
933
+ writeCache(cacheDir, cache);
934
+ }
935
+ }
936
+ function selectTier(tiered, tier) {
937
+ return tiered[tier];
938
+ }
939
+ function readProjectMemoryTiered(projectDir, tier) {
940
+ const memoryDir = path4.join(projectDir, ".kody", "memory");
941
+ if (!fs4.existsSync(memoryDir)) return "";
942
+ const files = fs4.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
943
+ if (files.length === 0) return "";
944
+ const cacheDir = path4.join(memoryDir, ".tiers");
945
+ const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
946
+ const sections = [];
947
+ for (const file of files) {
948
+ const filePath = path4.join(memoryDir, file);
949
+ const content = fs4.readFileSync(filePath, "utf-8").trim();
950
+ if (!content) continue;
951
+ const tiered = getTieredContent(filePath, content, cacheDir);
952
+ const selected = selectTier(tiered, tier);
953
+ if (selected) {
954
+ sections.push(`## ${file.replace(".md", "")}
955
+ ${selected}`);
956
+ }
957
+ }
958
+ if (sections.length === 0) return "";
959
+ const header = tier === "L2" ? "# Project Memory\n" : `# Project Memory (${tierLabel2} \u2014 use Read tool for full details)
960
+ `;
961
+ return `${header}
962
+ ${sections.join("\n\n")}
963
+ `;
964
+ }
965
+ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
966
+ const cacheDir = path4.join(taskDir, ".tiers");
967
+ let context = `## Task Context
968
+ `;
969
+ context += `Task ID: ${taskId}
970
+ `;
971
+ context += `Task Directory: ${taskDir}
972
+ `;
973
+ const taskMdPath = path4.join(taskDir, "task.md");
974
+ if (fs4.existsSync(taskMdPath)) {
975
+ const content = fs4.readFileSync(taskMdPath, "utf-8");
976
+ const selected = selectContent(taskMdPath, content, cacheDir, policy.taskDescription);
977
+ const label = tierLabel("Task Description", policy.taskDescription);
978
+ context += `
979
+ ## ${label}
980
+ ${selected}
981
+ `;
982
+ }
983
+ const taskJsonPath = path4.join(taskDir, "task.json");
984
+ if (fs4.existsSync(taskJsonPath)) {
985
+ const content = fs4.readFileSync(taskJsonPath, "utf-8");
986
+ if (policy.taskClassification === "L2") {
987
+ try {
988
+ const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
989
+ context += `
990
+ ## Task Classification
991
+ `;
992
+ context += `Type: ${taskDef.task_type ?? "unknown"}
993
+ `;
994
+ context += `Title: ${taskDef.title ?? "unknown"}
995
+ `;
996
+ context += `Risk: ${taskDef.risk_level ?? "unknown"}
997
+ `;
998
+ } catch {
999
+ }
1000
+ } else {
1001
+ const selected = selectContent(taskJsonPath, content, cacheDir, policy.taskClassification);
1002
+ if (selected) {
1003
+ const label = tierLabel("Task Classification", policy.taskClassification);
1004
+ context += `
1005
+ ## ${label}
1006
+ ${selected}
1007
+ `;
1008
+ }
1009
+ }
1010
+ }
1011
+ const specPath = path4.join(taskDir, "spec.md");
1012
+ if (fs4.existsSync(specPath)) {
1013
+ const content = fs4.readFileSync(specPath, "utf-8");
1014
+ const selected = selectContent(specPath, content, cacheDir, policy.spec);
1015
+ const label = tierLabel("Spec", policy.spec);
1016
+ context += `
1017
+ ## ${label}
1018
+ ${selected}
1019
+ `;
1020
+ }
1021
+ const planPath = path4.join(taskDir, "plan.md");
1022
+ if (fs4.existsSync(planPath)) {
1023
+ const content = fs4.readFileSync(planPath, "utf-8");
1024
+ const selected = selectContent(planPath, content, cacheDir, policy.plan);
1025
+ const label = tierLabel("Plan", policy.plan);
1026
+ context += `
1027
+ ## ${label}
1028
+ ${selected}
1029
+ `;
1030
+ }
1031
+ const contextMdPath = path4.join(taskDir, "context.md");
1032
+ if (fs4.existsSync(contextMdPath)) {
1033
+ const content = fs4.readFileSync(contextMdPath, "utf-8");
1034
+ const selected = selectContent(contextMdPath, content, cacheDir, policy.accumulatedContext);
1035
+ const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
1036
+ context += `
1037
+ ## ${label}
1038
+ ${selected}
1039
+ `;
1040
+ }
1041
+ if (feedback) {
1042
+ context += `
1043
+ ## Human Feedback
1044
+ ${feedback}
1045
+ `;
1046
+ }
1047
+ return prompt.replace("{{TASK_CONTEXT}}", context);
1048
+ }
1049
+ function selectContent(filePath, content, cacheDir, tier) {
1050
+ if (tier === "L2") return content;
1051
+ const tiered = getTieredContent(filePath, content, cacheDir);
1052
+ return selectTier(tiered, tier);
1053
+ }
1054
+ function tierLabel(sectionName, tier) {
1055
+ if (tier === "L2") return sectionName;
1056
+ if (tier === "L1") return `${sectionName} (overview)`;
1057
+ return `${sectionName} (abstract)`;
1058
+ }
1059
+ var DEFAULT_STAGE_POLICIES, L0_MAX_CHARS, L1_MAX_CHARS;
1060
+ var init_context_tiers = __esm({
1061
+ "src/context-tiers.ts"() {
1062
+ "use strict";
1063
+ DEFAULT_STAGE_POLICIES = {
1064
+ taskify: {
1065
+ memory: "L1",
1066
+ taskDescription: "L2",
1067
+ taskClassification: "L0",
1068
+ spec: "L0",
1069
+ plan: "L0",
1070
+ accumulatedContext: "L0"
1071
+ },
1072
+ plan: {
1073
+ memory: "L1",
1074
+ taskDescription: "L2",
1075
+ taskClassification: "L2",
1076
+ spec: "L0",
1077
+ plan: "L0",
1078
+ accumulatedContext: "L1"
1079
+ },
1080
+ build: {
1081
+ memory: "L1",
1082
+ taskDescription: "L1",
1083
+ taskClassification: "L1",
1084
+ spec: "L1",
1085
+ plan: "L2",
1086
+ accumulatedContext: "L1"
1087
+ },
1088
+ autofix: {
1089
+ memory: "L0",
1090
+ taskDescription: "L0",
1091
+ taskClassification: "L0",
1092
+ spec: "L0",
1093
+ plan: "L1",
1094
+ accumulatedContext: "L2"
1095
+ },
1096
+ review: {
1097
+ memory: "L1",
1098
+ taskDescription: "L1",
1099
+ taskClassification: "L1",
1100
+ spec: "L0",
1101
+ plan: "L2",
1102
+ accumulatedContext: "L1"
1103
+ },
1104
+ "review-fix": {
1105
+ memory: "L0",
1106
+ taskDescription: "L0",
1107
+ taskClassification: "L0",
1108
+ spec: "L0",
1109
+ plan: "L1",
1110
+ accumulatedContext: "L2"
1111
+ }
1112
+ };
1113
+ L0_MAX_CHARS = 400;
1114
+ L1_MAX_CHARS = 1600;
1115
+ }
1116
+ });
1117
+
1118
+ // src/context.ts
1119
+ import * as fs5 from "fs";
1120
+ import * as path5 from "path";
781
1121
  function readPromptFile(stageName, projectDir) {
782
1122
  if (projectDir) {
783
- const stepFile = path4.join(projectDir, ".kody", "steps", `${stageName}.md`);
784
- if (fs4.existsSync(stepFile)) {
785
- return fs4.readFileSync(stepFile, "utf-8");
1123
+ const stepFile = path5.join(projectDir, ".kody", "steps", `${stageName}.md`);
1124
+ if (fs5.existsSync(stepFile)) {
1125
+ return fs5.readFileSync(stepFile, "utf-8");
786
1126
  }
787
1127
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
788
1128
  }
789
1129
  const scriptDir = new URL(".", import.meta.url).pathname;
790
1130
  const candidates = [
791
- path4.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
792
- path4.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
1131
+ path5.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
1132
+ path5.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
793
1133
  ];
794
1134
  for (const candidate of candidates) {
795
- if (fs4.existsSync(candidate)) {
796
- return fs4.readFileSync(candidate, "utf-8");
1135
+ if (fs5.existsSync(candidate)) {
1136
+ return fs5.readFileSync(candidate, "utf-8");
797
1137
  }
798
1138
  }
799
1139
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -805,18 +1145,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
805
1145
  `;
806
1146
  context += `Task Directory: ${taskDir}
807
1147
  `;
808
- const taskMdPath = path4.join(taskDir, "task.md");
809
- if (fs4.existsSync(taskMdPath)) {
810
- const taskMd = fs4.readFileSync(taskMdPath, "utf-8");
1148
+ const taskMdPath = path5.join(taskDir, "task.md");
1149
+ if (fs5.existsSync(taskMdPath)) {
1150
+ const taskMd = fs5.readFileSync(taskMdPath, "utf-8");
811
1151
  context += `
812
1152
  ## Task Description
813
1153
  ${taskMd}
814
1154
  `;
815
1155
  }
816
- const taskJsonPath = path4.join(taskDir, "task.json");
817
- if (fs4.existsSync(taskJsonPath)) {
1156
+ const taskJsonPath = path5.join(taskDir, "task.json");
1157
+ if (fs5.existsSync(taskJsonPath)) {
818
1158
  try {
819
- const taskDef = JSON.parse(fs4.readFileSync(taskJsonPath, "utf-8"));
1159
+ const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
820
1160
  context += `
821
1161
  ## Task Classification
822
1162
  `;
@@ -829,27 +1169,27 @@ ${taskMd}
829
1169
  } catch {
830
1170
  }
831
1171
  }
832
- const specPath = path4.join(taskDir, "spec.md");
833
- if (fs4.existsSync(specPath)) {
834
- const spec = fs4.readFileSync(specPath, "utf-8");
1172
+ const specPath = path5.join(taskDir, "spec.md");
1173
+ if (fs5.existsSync(specPath)) {
1174
+ const spec = fs5.readFileSync(specPath, "utf-8");
835
1175
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
836
1176
  context += `
837
1177
  ## Spec Summary
838
1178
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
839
1179
  `;
840
1180
  }
841
- const planPath = path4.join(taskDir, "plan.md");
842
- if (fs4.existsSync(planPath)) {
843
- const plan = fs4.readFileSync(planPath, "utf-8");
1181
+ const planPath = path5.join(taskDir, "plan.md");
1182
+ if (fs5.existsSync(planPath)) {
1183
+ const plan = fs5.readFileSync(planPath, "utf-8");
844
1184
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
845
1185
  context += `
846
1186
  ## Plan Summary
847
1187
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
848
1188
  `;
849
1189
  }
850
- const contextMdPath = path4.join(taskDir, "context.md");
851
- if (fs4.existsSync(contextMdPath)) {
852
- const accumulated = fs4.readFileSync(contextMdPath, "utf-8");
1190
+ const contextMdPath = path5.join(taskDir, "context.md");
1191
+ if (fs5.existsSync(contextMdPath)) {
1192
+ const accumulated = fs5.readFileSync(contextMdPath, "utf-8");
853
1193
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
854
1194
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
855
1195
  context += `
@@ -866,6 +1206,10 @@ ${feedback}
866
1206
  return prompt.replace("{{TASK_CONTEXT}}", context);
867
1207
  }
868
1208
  function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
1209
+ const config = getProjectConfig();
1210
+ if (config.contextTiers?.enabled) {
1211
+ return buildFullPromptTiered(stageName, taskId, taskDir, projectDir, feedback);
1212
+ }
869
1213
  const memory = readProjectMemory(projectDir);
870
1214
  const promptTemplate = readPromptFile(stageName, projectDir);
871
1215
  const prompt = injectTaskContext(promptTemplate, taskId, taskDir, feedback);
@@ -874,6 +1218,24 @@ function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
874
1218
 
875
1219
  ${prompt}` : prompt;
876
1220
  }
1221
+ function buildFullPromptTiered(stageName, taskId, taskDir, projectDir, feedback) {
1222
+ const config = getProjectConfig();
1223
+ const policy = resolveStagePolicy(stageName, config.contextTiers?.stageOverrides);
1224
+ const tokenBudget = config.contextTiers?.tokenBudget ?? 8e3;
1225
+ const memory = readProjectMemoryTiered(projectDir, policy.memory);
1226
+ const promptTemplate = readPromptFile(stageName, projectDir);
1227
+ const prompt = injectTaskContextTiered(promptTemplate, taskId, taskDir, policy, feedback);
1228
+ let assembled = memory ? `${memory}
1229
+ ---
1230
+
1231
+ ${prompt}` : prompt;
1232
+ const tokens = estimateTokens(assembled);
1233
+ if (tokens > tokenBudget) {
1234
+ const maxChars = tokenBudget * 4;
1235
+ assembled = assembled.slice(0, maxChars) + "\n...(context truncated to fit token budget)";
1236
+ }
1237
+ return assembled;
1238
+ }
877
1239
  function resolveModel(modelTier, stageName) {
878
1240
  const config = getProjectConfig();
879
1241
  if (config.agent.usePerStageRouting && stageName) {
@@ -889,6 +1251,7 @@ var init_context = __esm({
889
1251
  "use strict";
890
1252
  init_memory();
891
1253
  init_config();
1254
+ init_context_tiers();
892
1255
  DEFAULT_MODEL_MAP = {
893
1256
  cheap: "haiku",
894
1257
  mid: "sonnet",
@@ -969,8 +1332,8 @@ var init_runner_selection = __esm({
969
1332
  });
970
1333
 
971
1334
  // src/stages/agent.ts
972
- import * as fs5 from "fs";
973
- import * as path5 from "path";
1335
+ import * as fs6 from "fs";
1336
+ import * as path6 from "path";
974
1337
  function getSessionInfo(stageName, sessions) {
975
1338
  const group = SESSION_GROUP[stageName];
976
1339
  if (!group) return void 0;
@@ -1005,8 +1368,8 @@ async function executeAgentStage(ctx, def) {
1005
1368
  const runnerName = config.agent.stageRunners?.[def.name] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
1006
1369
  logger.info(` runner=${runnerName} model=${model} timeout=${def.timeout / 1e3}s`);
1007
1370
  const extraEnv = {};
1008
- if (config.agent.litellmUrl) {
1009
- extraEnv.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
1371
+ if (needsLitellmProxy(config)) {
1372
+ extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl(config);
1010
1373
  }
1011
1374
  const sessions = ctx.sessions ?? {};
1012
1375
  const sessionInfo = getSessionInfo(def.name, sessions);
@@ -1023,27 +1386,27 @@ async function executeAgentStage(ctx, def) {
1023
1386
  return { outcome: result.outcome, error: result.error, retries: 0 };
1024
1387
  }
1025
1388
  if (def.outputFile && result.output) {
1026
- fs5.writeFileSync(path5.join(ctx.taskDir, def.outputFile), result.output);
1389
+ fs6.writeFileSync(path6.join(ctx.taskDir, def.outputFile), result.output);
1027
1390
  }
1028
1391
  if (def.outputFile) {
1029
- const outputPath = path5.join(ctx.taskDir, def.outputFile);
1030
- if (!fs5.existsSync(outputPath)) {
1031
- const ext = path5.extname(def.outputFile);
1032
- const base = path5.basename(def.outputFile, ext);
1033
- const files = fs5.readdirSync(ctx.taskDir);
1392
+ const outputPath = path6.join(ctx.taskDir, def.outputFile);
1393
+ if (!fs6.existsSync(outputPath)) {
1394
+ const ext = path6.extname(def.outputFile);
1395
+ const base = path6.basename(def.outputFile, ext);
1396
+ const files = fs6.readdirSync(ctx.taskDir);
1034
1397
  const variant = files.find(
1035
1398
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
1036
1399
  );
1037
1400
  if (variant) {
1038
- fs5.renameSync(path5.join(ctx.taskDir, variant), outputPath);
1401
+ fs6.renameSync(path6.join(ctx.taskDir, variant), outputPath);
1039
1402
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
1040
1403
  }
1041
1404
  }
1042
1405
  }
1043
1406
  if (def.outputFile) {
1044
- const outputPath = path5.join(ctx.taskDir, def.outputFile);
1045
- if (fs5.existsSync(outputPath)) {
1046
- const content = fs5.readFileSync(outputPath, "utf-8");
1407
+ const outputPath = path6.join(ctx.taskDir, def.outputFile);
1408
+ if (fs6.existsSync(outputPath)) {
1409
+ const content = fs6.readFileSync(outputPath, "utf-8");
1047
1410
  const validation = validateStageOutput(def.name, content);
1048
1411
  if (!validation.valid) {
1049
1412
  if (def.name === "taskify") {
@@ -1057,7 +1420,7 @@ async function executeAgentStage(ctx, def) {
1057
1420
  const stripped = stripFences(retryResult.output);
1058
1421
  const retryValidation = validateTaskJson(stripped);
1059
1422
  if (retryValidation.valid) {
1060
- fs5.writeFileSync(outputPath, retryResult.output);
1423
+ fs6.writeFileSync(outputPath, retryResult.output);
1061
1424
  logger.info(` taskify retry produced valid JSON`);
1062
1425
  } else {
1063
1426
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -1073,7 +1436,7 @@ async function executeAgentStage(ctx, def) {
1073
1436
  return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
1074
1437
  }
1075
1438
  function appendStageContext(taskDir, stageName, output) {
1076
- const contextPath = path5.join(taskDir, "context.md");
1439
+ const contextPath = path6.join(taskDir, "context.md");
1077
1440
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
1078
1441
  let summary;
1079
1442
  if (output && output.trim()) {
@@ -1086,7 +1449,7 @@ function appendStageContext(taskDir, stageName, output) {
1086
1449
  ### ${stageName} (${timestamp2})
1087
1450
  ${summary}
1088
1451
  `;
1089
- fs5.appendFileSync(contextPath, entry);
1452
+ fs6.appendFileSync(contextPath, entry);
1090
1453
  }
1091
1454
  var SESSION_GROUP;
1092
1455
  var init_agent = __esm({
@@ -1308,8 +1671,8 @@ Error context:
1308
1671
  });
1309
1672
 
1310
1673
  // src/stages/gate.ts
1311
- import * as fs6 from "fs";
1312
- import * as path6 from "path";
1674
+ import * as fs7 from "fs";
1675
+ import * as path7 from "path";
1313
1676
  function executeGateStage(ctx, def) {
1314
1677
  if (ctx.input.dryRun) {
1315
1678
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -1352,7 +1715,7 @@ ${output}
1352
1715
  `);
1353
1716
  }
1354
1717
  }
1355
- fs6.writeFileSync(path6.join(ctx.taskDir, "verify.md"), lines.join(""));
1718
+ fs7.writeFileSync(path7.join(ctx.taskDir, "verify.md"), lines.join(""));
1356
1719
  return {
1357
1720
  outcome: verifyResult.pass ? "completed" : "failed",
1358
1721
  retries: 0
@@ -1367,8 +1730,8 @@ var init_gate = __esm({
1367
1730
  });
1368
1731
 
1369
1732
  // src/stages/verify.ts
1370
- import * as fs7 from "fs";
1371
- import * as path7 from "path";
1733
+ import * as fs8 from "fs";
1734
+ import * as path8 from "path";
1372
1735
  import { execFileSync as execFileSync6 } from "child_process";
1373
1736
  async function executeVerifyWithAutofix(ctx, def) {
1374
1737
  const maxAttempts = def.maxRetries ?? 2;
@@ -1379,8 +1742,8 @@ async function executeVerifyWithAutofix(ctx, def) {
1379
1742
  return { ...gateResult, retries: attempt };
1380
1743
  }
1381
1744
  if (attempt < maxAttempts) {
1382
- const verifyPath = path7.join(ctx.taskDir, "verify.md");
1383
- const errorOutput = fs7.existsSync(verifyPath) ? fs7.readFileSync(verifyPath, "utf-8") : "Unknown error";
1745
+ const verifyPath = path8.join(ctx.taskDir, "verify.md");
1746
+ const errorOutput = fs8.existsSync(verifyPath) ? fs8.readFileSync(verifyPath, "utf-8") : "Unknown error";
1384
1747
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
1385
1748
  const defaultRunner = getRunnerForStage(ctx, "taskify");
1386
1749
  const diagConfig = getProjectConfig();
@@ -1476,8 +1839,8 @@ var init_verify = __esm({
1476
1839
  });
1477
1840
 
1478
1841
  // src/stages/review.ts
1479
- import * as fs8 from "fs";
1480
- import * as path8 from "path";
1842
+ import * as fs9 from "fs";
1843
+ import * as path9 from "path";
1481
1844
  async function executeReviewWithFix(ctx, def) {
1482
1845
  if (ctx.input.dryRun) {
1483
1846
  return { outcome: "completed", retries: 0 };
@@ -1488,11 +1851,11 @@ async function executeReviewWithFix(ctx, def) {
1488
1851
  if (reviewResult.outcome !== "completed") {
1489
1852
  return reviewResult;
1490
1853
  }
1491
- const reviewFile = path8.join(ctx.taskDir, "review.md");
1492
- if (!fs8.existsSync(reviewFile)) {
1854
+ const reviewFile = path9.join(ctx.taskDir, "review.md");
1855
+ if (!fs9.existsSync(reviewFile)) {
1493
1856
  return { outcome: "failed", retries: 0, error: "review.md not found" };
1494
1857
  }
1495
- const content = fs8.readFileSync(reviewFile, "utf-8");
1858
+ const content = fs9.readFileSync(reviewFile, "utf-8");
1496
1859
  const hasIssues = /\bfail\b/i.test(content) && !/pass/i.test(content);
1497
1860
  if (!hasIssues) {
1498
1861
  return reviewResult;
@@ -1515,15 +1878,15 @@ var init_review = __esm({
1515
1878
  });
1516
1879
 
1517
1880
  // src/stages/ship.ts
1518
- import * as fs9 from "fs";
1519
- import * as path9 from "path";
1881
+ import * as fs10 from "fs";
1882
+ import * as path10 from "path";
1520
1883
  import { execFileSync as execFileSync7 } from "child_process";
1521
1884
  function buildPrBody(ctx) {
1522
1885
  const sections = [];
1523
- const taskJsonPath = path9.join(ctx.taskDir, "task.json");
1524
- if (fs9.existsSync(taskJsonPath)) {
1886
+ const taskJsonPath = path10.join(ctx.taskDir, "task.json");
1887
+ if (fs10.existsSync(taskJsonPath)) {
1525
1888
  try {
1526
- const raw = fs9.readFileSync(taskJsonPath, "utf-8");
1889
+ const raw = fs10.readFileSync(taskJsonPath, "utf-8");
1527
1890
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
1528
1891
  const task = JSON.parse(cleaned);
1529
1892
  if (task.description) {
@@ -1542,9 +1905,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
1542
1905
  } catch {
1543
1906
  }
1544
1907
  }
1545
- const reviewPath = path9.join(ctx.taskDir, "review.md");
1546
- if (fs9.existsSync(reviewPath)) {
1547
- const review = fs9.readFileSync(reviewPath, "utf-8");
1908
+ const reviewPath = path10.join(ctx.taskDir, "review.md");
1909
+ if (fs10.existsSync(reviewPath)) {
1910
+ const review = fs10.readFileSync(reviewPath, "utf-8");
1548
1911
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
1549
1912
  if (summaryMatch) {
1550
1913
  const summary = summaryMatch[1].trim();
@@ -1561,14 +1924,14 @@ ${summary}`);
1561
1924
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
1562
1925
  }
1563
1926
  }
1564
- const verifyPath = path9.join(ctx.taskDir, "verify.md");
1565
- if (fs9.existsSync(verifyPath)) {
1566
- const verify = fs9.readFileSync(verifyPath, "utf-8");
1927
+ const verifyPath = path10.join(ctx.taskDir, "verify.md");
1928
+ if (fs10.existsSync(verifyPath)) {
1929
+ const verify = fs10.readFileSync(verifyPath, "utf-8");
1567
1930
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
1568
1931
  }
1569
- const planPath = path9.join(ctx.taskDir, "plan.md");
1570
- if (fs9.existsSync(planPath)) {
1571
- const plan = fs9.readFileSync(planPath, "utf-8").trim();
1932
+ const planPath = path10.join(ctx.taskDir, "plan.md");
1933
+ if (fs10.existsSync(planPath)) {
1934
+ const plan = fs10.readFileSync(planPath, "utf-8").trim();
1572
1935
  if (plan) {
1573
1936
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
1574
1937
  sections.push(`
@@ -1588,13 +1951,13 @@ Closes #${ctx.input.issueNumber}`);
1588
1951
  return sections.join("\n");
1589
1952
  }
1590
1953
  function executeShipStage(ctx, _def) {
1591
- const shipPath = path9.join(ctx.taskDir, "ship.md");
1954
+ const shipPath = path10.join(ctx.taskDir, "ship.md");
1592
1955
  if (ctx.input.dryRun) {
1593
- fs9.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
1956
+ fs10.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
1594
1957
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
1595
1958
  }
1596
1959
  if (ctx.input.local && !ctx.input.issueNumber) {
1597
- fs9.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
1960
+ fs10.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
1598
1961
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
1599
1962
  }
1600
1963
  try {
@@ -1640,10 +2003,10 @@ function executeShipStage(ctx, _def) {
1640
2003
  docs: "docs",
1641
2004
  chore: "chore"
1642
2005
  };
1643
- const taskJsonPath = path9.join(ctx.taskDir, "task.json");
1644
- if (fs9.existsSync(taskJsonPath)) {
2006
+ const taskJsonPath = path10.join(ctx.taskDir, "task.json");
2007
+ if (fs10.existsSync(taskJsonPath)) {
1645
2008
  try {
1646
- const raw = fs9.readFileSync(taskJsonPath, "utf-8");
2009
+ const raw = fs10.readFileSync(taskJsonPath, "utf-8");
1647
2010
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
1648
2011
  const task = JSON.parse(cleaned);
1649
2012
  const prefix = TYPE_PREFIX[task.task_type] ?? "chore";
@@ -1653,9 +2016,9 @@ function executeShipStage(ctx, _def) {
1653
2016
  }
1654
2017
  }
1655
2018
  if (title === "Update") {
1656
- const taskMdPath = path9.join(ctx.taskDir, "task.md");
1657
- if (fs9.existsSync(taskMdPath)) {
1658
- const content = fs9.readFileSync(taskMdPath, "utf-8");
2019
+ const taskMdPath = path10.join(ctx.taskDir, "task.md");
2020
+ if (fs10.existsSync(taskMdPath)) {
2021
+ const content = fs10.readFileSync(taskMdPath, "utf-8");
1659
2022
  const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("*"));
1660
2023
  if (firstLine) title = `chore: ${firstLine.trim()}`.slice(0, 72);
1661
2024
  }
@@ -1675,7 +2038,7 @@ function executeShipStage(ctx, _def) {
1675
2038
  } catch {
1676
2039
  }
1677
2040
  }
1678
- fs9.writeFileSync(shipPath, `# Ship
2041
+ fs10.writeFileSync(shipPath, `# Ship
1679
2042
 
1680
2043
  Updated existing PR: ${existingPr.url}
1681
2044
  PR #${existingPr.number}
@@ -1689,19 +2052,19 @@ PR #${existingPr.number}
1689
2052
  } catch {
1690
2053
  }
1691
2054
  }
1692
- fs9.writeFileSync(shipPath, `# Ship
2055
+ fs10.writeFileSync(shipPath, `# Ship
1693
2056
 
1694
2057
  PR created: ${pr.url}
1695
2058
  PR #${pr.number}
1696
2059
  `);
1697
2060
  } else {
1698
- fs9.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
2061
+ fs10.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
1699
2062
  }
1700
2063
  }
1701
2064
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
1702
2065
  } catch (err) {
1703
2066
  const msg = err instanceof Error ? err.message : String(err);
1704
- fs9.writeFileSync(shipPath, `# Ship
2067
+ fs10.writeFileSync(shipPath, `# Ship
1705
2068
 
1706
2069
  Failed: ${msg}
1707
2070
  `);
@@ -1747,15 +2110,15 @@ var init_executor_registry = __esm({
1747
2110
  });
1748
2111
 
1749
2112
  // src/pipeline/questions.ts
1750
- import * as fs10 from "fs";
1751
- import * as path10 from "path";
2113
+ import * as fs11 from "fs";
2114
+ import * as path11 from "path";
1752
2115
  function checkForQuestions(ctx, stageName) {
1753
2116
  if (ctx.input.local || !ctx.input.issueNumber) return false;
1754
2117
  try {
1755
2118
  if (stageName === "taskify") {
1756
- const taskJsonPath = path10.join(ctx.taskDir, "task.json");
1757
- if (!fs10.existsSync(taskJsonPath)) return false;
1758
- const raw = fs10.readFileSync(taskJsonPath, "utf-8");
2119
+ const taskJsonPath = path11.join(ctx.taskDir, "task.json");
2120
+ if (!fs11.existsSync(taskJsonPath)) return false;
2121
+ const raw = fs11.readFileSync(taskJsonPath, "utf-8");
1759
2122
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
1760
2123
  const taskJson = JSON.parse(cleaned);
1761
2124
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -1770,9 +2133,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
1770
2133
  }
1771
2134
  }
1772
2135
  if (stageName === "plan") {
1773
- const planPath = path10.join(ctx.taskDir, "plan.md");
1774
- if (!fs10.existsSync(planPath)) return false;
1775
- const plan = fs10.readFileSync(planPath, "utf-8");
2136
+ const planPath = path11.join(ctx.taskDir, "plan.md");
2137
+ if (!fs11.existsSync(planPath)) return false;
2138
+ const plan = fs11.readFileSync(planPath, "utf-8");
1776
2139
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
1777
2140
  if (questionsMatch) {
1778
2141
  const questionsText = questionsMatch[1].trim();
@@ -1801,8 +2164,8 @@ var init_questions = __esm({
1801
2164
  });
1802
2165
 
1803
2166
  // src/pipeline/hooks.ts
1804
- import * as fs11 from "fs";
1805
- import * as path11 from "path";
2167
+ import * as fs12 from "fs";
2168
+ import * as path12 from "path";
1806
2169
  function applyPreStageLabel(ctx, def) {
1807
2170
  if (!ctx.input.issueNumber || ctx.input.local) return;
1808
2171
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
@@ -1828,9 +2191,9 @@ function autoDetectComplexity(ctx, def) {
1828
2191
  if (def.name !== "taskify") return null;
1829
2192
  if (ctx.input.complexity) return null;
1830
2193
  try {
1831
- const taskJsonPath = path11.join(ctx.taskDir, "task.json");
1832
- if (!fs11.existsSync(taskJsonPath)) return null;
1833
- const raw = fs11.readFileSync(taskJsonPath, "utf-8");
2194
+ const taskJsonPath = path12.join(ctx.taskDir, "task.json");
2195
+ if (!fs12.existsSync(taskJsonPath)) return null;
2196
+ const raw = fs12.readFileSync(taskJsonPath, "utf-8");
1834
2197
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
1835
2198
  const taskJson = JSON.parse(cleaned);
1836
2199
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -1860,8 +2223,8 @@ function checkRiskGate(ctx, def, state, complexity) {
1860
2223
  if (ctx.input.dryRun || ctx.input.local) return null;
1861
2224
  if (ctx.input.mode === "rerun") return null;
1862
2225
  if (!ctx.input.issueNumber) return null;
1863
- const planPath = path11.join(ctx.taskDir, "plan.md");
1864
- const plan = fs11.existsSync(planPath) ? fs11.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
2226
+ const planPath = path12.join(ctx.taskDir, "plan.md");
2227
+ const plan = fs12.existsSync(planPath) ? fs12.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
1865
2228
  try {
1866
2229
  postComment(
1867
2230
  ctx.input.issueNumber,
@@ -1928,22 +2291,22 @@ var init_hooks = __esm({
1928
2291
  });
1929
2292
 
1930
2293
  // src/learning/auto-learn.ts
1931
- import * as fs12 from "fs";
1932
- import * as path12 from "path";
2294
+ import * as fs13 from "fs";
2295
+ import * as path13 from "path";
1933
2296
  function stripAnsi(str) {
1934
2297
  return str.replace(/\x1b\[[0-9;]*m/g, "");
1935
2298
  }
1936
2299
  function autoLearn(ctx) {
1937
2300
  try {
1938
- const memoryDir = path12.join(ctx.projectDir, ".kody", "memory");
1939
- if (!fs12.existsSync(memoryDir)) {
1940
- fs12.mkdirSync(memoryDir, { recursive: true });
2301
+ const memoryDir = path13.join(ctx.projectDir, ".kody", "memory");
2302
+ if (!fs13.existsSync(memoryDir)) {
2303
+ fs13.mkdirSync(memoryDir, { recursive: true });
1941
2304
  }
1942
2305
  const learnings = [];
1943
2306
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1944
- const verifyPath = path12.join(ctx.taskDir, "verify.md");
1945
- if (fs12.existsSync(verifyPath)) {
1946
- const verify = stripAnsi(fs12.readFileSync(verifyPath, "utf-8"));
2307
+ const verifyPath = path13.join(ctx.taskDir, "verify.md");
2308
+ if (fs13.existsSync(verifyPath)) {
2309
+ const verify = stripAnsi(fs13.readFileSync(verifyPath, "utf-8"));
1947
2310
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
1948
2311
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
1949
2312
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -1952,18 +2315,18 @@ function autoLearn(ctx) {
1952
2315
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
1953
2316
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
1954
2317
  }
1955
- const reviewPath = path12.join(ctx.taskDir, "review.md");
1956
- if (fs12.existsSync(reviewPath)) {
1957
- const review = fs12.readFileSync(reviewPath, "utf-8");
2318
+ const reviewPath = path13.join(ctx.taskDir, "review.md");
2319
+ if (fs13.existsSync(reviewPath)) {
2320
+ const review = fs13.readFileSync(reviewPath, "utf-8");
1958
2321
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
1959
2322
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
1960
2323
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
1961
2324
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
1962
2325
  }
1963
- const taskJsonPath = path12.join(ctx.taskDir, "task.json");
1964
- if (fs12.existsSync(taskJsonPath)) {
2326
+ const taskJsonPath = path13.join(ctx.taskDir, "task.json");
2327
+ if (fs13.existsSync(taskJsonPath)) {
1965
2328
  try {
1966
- const raw = stripAnsi(fs12.readFileSync(taskJsonPath, "utf-8"));
2329
+ const raw = stripAnsi(fs13.readFileSync(taskJsonPath, "utf-8"));
1967
2330
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
1968
2331
  const task = JSON.parse(cleaned);
1969
2332
  if (task.scope && Array.isArray(task.scope)) {
@@ -1974,12 +2337,13 @@ function autoLearn(ctx) {
1974
2337
  }
1975
2338
  }
1976
2339
  if (learnings.length > 0) {
1977
- const conventionsPath = path12.join(memoryDir, "conventions.md");
2340
+ const conventionsPath = path13.join(memoryDir, "conventions.md");
1978
2341
  const entry = `
1979
2342
  ## Learned ${timestamp2} (task: ${ctx.taskId})
1980
2343
  ${learnings.join("\n")}
1981
2344
  `;
1982
- fs12.appendFileSync(conventionsPath, entry);
2345
+ fs13.appendFileSync(conventionsPath, entry);
2346
+ invalidateCache(conventionsPath, path13.join(memoryDir, ".tiers"));
1983
2347
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
1984
2348
  }
1985
2349
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
@@ -1987,13 +2351,13 @@ ${learnings.join("\n")}
1987
2351
  }
1988
2352
  }
1989
2353
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
1990
- const archPath = path12.join(memoryDir, "architecture.md");
1991
- if (fs12.existsSync(archPath)) return;
2354
+ const archPath = path13.join(memoryDir, "architecture.md");
2355
+ if (fs13.existsSync(archPath)) return;
1992
2356
  const detected = [];
1993
- const pkgPath = path12.join(projectDir, "package.json");
1994
- if (fs12.existsSync(pkgPath)) {
2357
+ const pkgPath = path13.join(projectDir, "package.json");
2358
+ if (fs13.existsSync(pkgPath)) {
1995
2359
  try {
1996
- const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
2360
+ const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
1997
2361
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1998
2362
  if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
1999
2363
  else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
@@ -2009,15 +2373,15 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
2009
2373
  if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
2010
2374
  if (pkg.type === "module") detected.push("- Module system: ESM");
2011
2375
  else detected.push("- Module system: CommonJS");
2012
- if (fs12.existsSync(path12.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
2013
- else if (fs12.existsSync(path12.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
2014
- else if (fs12.existsSync(path12.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
2376
+ if (fs13.existsSync(path13.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
2377
+ else if (fs13.existsSync(path13.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
2378
+ else if (fs13.existsSync(path13.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
2015
2379
  } catch {
2016
2380
  }
2017
2381
  }
2018
2382
  const topDirs = [];
2019
2383
  try {
2020
- const entries = fs12.readdirSync(projectDir, { withFileTypes: true });
2384
+ const entries = fs13.readdirSync(projectDir, { withFileTypes: true });
2021
2385
  for (const entry of entries) {
2022
2386
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
2023
2387
  topDirs.push(entry.name);
@@ -2026,10 +2390,10 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
2026
2390
  if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
2027
2391
  } catch {
2028
2392
  }
2029
- const srcDir = path12.join(projectDir, "src");
2030
- if (fs12.existsSync(srcDir)) {
2393
+ const srcDir = path13.join(projectDir, "src");
2394
+ if (fs13.existsSync(srcDir)) {
2031
2395
  try {
2032
- const srcEntries = fs12.readdirSync(srcDir, { withFileTypes: true });
2396
+ const srcEntries = fs13.readdirSync(srcDir, { withFileTypes: true });
2033
2397
  const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
2034
2398
  if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
2035
2399
  } catch {
@@ -2041,7 +2405,8 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
2041
2405
  ## Overview
2042
2406
  ${detected.join("\n")}
2043
2407
  `;
2044
- fs12.writeFileSync(archPath, content);
2408
+ fs13.writeFileSync(archPath, content);
2409
+ invalidateCache(archPath, path13.join(memoryDir, ".tiers"));
2045
2410
  logger.info(`Auto-detected architecture (${detected.length} items)`);
2046
2411
  }
2047
2412
  }
@@ -2049,17 +2414,18 @@ var init_auto_learn = __esm({
2049
2414
  "src/learning/auto-learn.ts"() {
2050
2415
  "use strict";
2051
2416
  init_logger();
2417
+ init_context_tiers();
2052
2418
  }
2053
2419
  });
2054
2420
 
2055
2421
  // src/retrospective.ts
2056
- import * as fs13 from "fs";
2057
- import * as path13 from "path";
2422
+ import * as fs14 from "fs";
2423
+ import * as path14 from "path";
2058
2424
  function readArtifact(taskDir, filename, maxChars) {
2059
- const p = path13.join(taskDir, filename);
2060
- if (!fs13.existsSync(p)) return null;
2425
+ const p = path14.join(taskDir, filename);
2426
+ if (!fs14.existsSync(p)) return null;
2061
2427
  try {
2062
- const content = fs13.readFileSync(p, "utf-8");
2428
+ const content = fs14.readFileSync(p, "utf-8");
2063
2429
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
2064
2430
  } catch {
2065
2431
  return null;
@@ -2112,13 +2478,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
2112
2478
  return lines.join("\n");
2113
2479
  }
2114
2480
  function getLogPath(projectDir) {
2115
- return path13.join(projectDir, ".kody", "memory", "observer-log.jsonl");
2481
+ return path14.join(projectDir, ".kody", "memory", "observer-log.jsonl");
2116
2482
  }
2117
2483
  function readPreviousRetrospectives(projectDir, limit = 10) {
2118
2484
  const logPath = getLogPath(projectDir);
2119
- if (!fs13.existsSync(logPath)) return [];
2485
+ if (!fs14.existsSync(logPath)) return [];
2120
2486
  try {
2121
- const content = fs13.readFileSync(logPath, "utf-8");
2487
+ const content = fs14.readFileSync(logPath, "utf-8");
2122
2488
  const lines = content.split("\n").filter(Boolean);
2123
2489
  const entries = [];
2124
2490
  const start = Math.max(0, lines.length - limit);
@@ -2145,11 +2511,11 @@ function formatPreviousEntries(entries) {
2145
2511
  }
2146
2512
  function appendRetrospectiveEntry(projectDir, entry) {
2147
2513
  const logPath = getLogPath(projectDir);
2148
- const dir = path13.dirname(logPath);
2149
- if (!fs13.existsSync(dir)) {
2150
- fs13.mkdirSync(dir, { recursive: true });
2514
+ const dir = path14.dirname(logPath);
2515
+ if (!fs14.existsSync(dir)) {
2516
+ fs14.mkdirSync(dir, { recursive: true });
2151
2517
  }
2152
- fs13.appendFileSync(logPath, JSON.stringify(entry) + "\n");
2518
+ fs14.appendFileSync(logPath, JSON.stringify(entry) + "\n");
2153
2519
  }
2154
2520
  async function runRetrospective(ctx, state, pipelineStartTime) {
2155
2521
  if (ctx.input.dryRun) return;
@@ -2260,14 +2626,14 @@ If no pipeline flaw is detected, set "pipelineFlaw" to null.
2260
2626
  });
2261
2627
 
2262
2628
  // src/pipeline.ts
2263
- import * as fs14 from "fs";
2264
- import * as path14 from "path";
2629
+ import * as fs15 from "fs";
2630
+ import * as path15 from "path";
2265
2631
  function ensureFeatureBranchIfNeeded(ctx) {
2266
2632
  if (ctx.input.prNumber) return;
2267
2633
  if (!ctx.input.issueNumber || ctx.input.dryRun) return;
2268
2634
  try {
2269
- const taskMdPath = path14.join(ctx.taskDir, "task.md");
2270
- const title = fs14.existsSync(taskMdPath) ? fs14.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
2635
+ const taskMdPath = path15.join(ctx.taskDir, "task.md");
2636
+ const title = fs15.existsSync(taskMdPath) ? fs15.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
2271
2637
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
2272
2638
  syncWithDefault(ctx.projectDir);
2273
2639
  } catch (err) {
@@ -2275,10 +2641,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
2275
2641
  }
2276
2642
  }
2277
2643
  function acquireLock(taskDir) {
2278
- const lockPath = path14.join(taskDir, ".lock");
2279
- if (fs14.existsSync(lockPath)) {
2644
+ const lockPath = path15.join(taskDir, ".lock");
2645
+ if (fs15.existsSync(lockPath)) {
2280
2646
  try {
2281
- const pid = parseInt(fs14.readFileSync(lockPath, "utf-8").trim(), 10);
2647
+ const pid = parseInt(fs15.readFileSync(lockPath, "utf-8").trim(), 10);
2282
2648
  try {
2283
2649
  process.kill(pid, 0);
2284
2650
  throw new Error(`Pipeline already running (PID ${pid})`);
@@ -2289,11 +2655,11 @@ function acquireLock(taskDir) {
2289
2655
  if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
2290
2656
  }
2291
2657
  }
2292
- fs14.writeFileSync(lockPath, String(process.pid));
2658
+ fs15.writeFileSync(lockPath, String(process.pid));
2293
2659
  }
2294
2660
  function releaseLock(taskDir) {
2295
2661
  try {
2296
- fs14.unlinkSync(path14.join(taskDir, ".lock"));
2662
+ fs15.unlinkSync(path15.join(taskDir, ".lock"));
2297
2663
  } catch {
2298
2664
  }
2299
2665
  }
@@ -2462,7 +2828,7 @@ var init_pipeline = __esm({
2462
2828
 
2463
2829
  // src/preflight.ts
2464
2830
  import { execFileSync as execFileSync8 } from "child_process";
2465
- import * as fs15 from "fs";
2831
+ import * as fs16 from "fs";
2466
2832
  function check(name, fn) {
2467
2833
  try {
2468
2834
  const detail = fn() ?? void 0;
@@ -2515,7 +2881,7 @@ function runPreflight() {
2515
2881
  return v;
2516
2882
  }),
2517
2883
  check("package.json", () => {
2518
- if (!fs15.existsSync("package.json")) throw new Error("not found");
2884
+ if (!fs16.existsSync("package.json")) throw new Error("not found");
2519
2885
  })
2520
2886
  ];
2521
2887
  const failed = checks.filter((c) => !c.ok);
@@ -2536,13 +2902,13 @@ var init_preflight = __esm({
2536
2902
  });
2537
2903
 
2538
2904
  // src/cli/task-resolution.ts
2539
- import * as fs16 from "fs";
2540
- import * as path15 from "path";
2905
+ import * as fs17 from "fs";
2906
+ import * as path16 from "path";
2541
2907
  import { execFileSync as execFileSync9 } from "child_process";
2542
2908
  function findLatestTaskForIssue(issueNumber, projectDir) {
2543
- const tasksDir = path15.join(projectDir, ".kody", "tasks");
2544
- if (!fs16.existsSync(tasksDir)) return null;
2545
- const allDirs = fs16.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2909
+ const tasksDir = path16.join(projectDir, ".kody", "tasks");
2910
+ if (!fs17.existsSync(tasksDir)) return null;
2911
+ const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2546
2912
  const prefix = `${issueNumber}-`;
2547
2913
  const direct = allDirs.find((d) => d.startsWith(prefix));
2548
2914
  if (direct) return direct;
@@ -2576,8 +2942,8 @@ var init_task_resolution = __esm({
2576
2942
  });
2577
2943
 
2578
2944
  // src/review-standalone.ts
2579
- import * as fs17 from "fs";
2580
- import * as path16 from "path";
2945
+ import * as fs18 from "fs";
2946
+ import * as path17 from "path";
2581
2947
  function resolveReviewTarget(input) {
2582
2948
  if (input.prs.length === 0) {
2583
2949
  return {
@@ -2601,12 +2967,12 @@ Or comment on the specific PR: \`@kody review\``
2601
2967
  }
2602
2968
  async function runStandaloneReview(input) {
2603
2969
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
2604
- const taskDir = path16.join(input.projectDir, ".kody", "tasks", taskId);
2605
- fs17.mkdirSync(taskDir, { recursive: true });
2970
+ const taskDir = path17.join(input.projectDir, ".kody", "tasks", taskId);
2971
+ fs18.mkdirSync(taskDir, { recursive: true });
2606
2972
  const taskContent = `# ${input.prTitle}
2607
2973
 
2608
2974
  ${input.prBody ?? ""}`;
2609
- fs17.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
2975
+ fs18.writeFileSync(path17.join(taskDir, "task.md"), taskContent);
2610
2976
  const reviewDef = STAGES.find((s) => s.name === "review");
2611
2977
  const ctx = {
2612
2978
  taskId,
@@ -2628,10 +2994,10 @@ ${input.prBody ?? ""}`;
2628
2994
  error: result.error ?? "Review stage failed"
2629
2995
  };
2630
2996
  }
2631
- const reviewPath = path16.join(taskDir, "review.md");
2997
+ const reviewPath = path17.join(taskDir, "review.md");
2632
2998
  let reviewContent;
2633
- if (fs17.existsSync(reviewPath)) {
2634
- reviewContent = fs17.readFileSync(reviewPath, "utf-8");
2999
+ if (fs18.existsSync(reviewPath)) {
3000
+ reviewContent = fs18.readFileSync(reviewPath, "utf-8");
2635
3001
  }
2636
3002
  return {
2637
3003
  outcome: "completed",
@@ -2723,8 +3089,9 @@ var init_args = __esm({
2723
3089
  });
2724
3090
 
2725
3091
  // src/cli/litellm.ts
2726
- import * as fs18 from "fs";
2727
- import * as path17 from "path";
3092
+ import * as fs19 from "fs";
3093
+ import * as os from "os";
3094
+ import * as path18 from "path";
2728
3095
  import { execFileSync as execFileSync10 } from "child_process";
2729
3096
  async function checkLitellmHealth(url) {
2730
3097
  try {
@@ -2734,10 +3101,31 @@ async function checkLitellmHealth(url) {
2734
3101
  return false;
2735
3102
  }
2736
3103
  }
2737
- async function tryStartLitellm(url, projectDir) {
2738
- const configPath = path17.join(projectDir, "litellm-config.yaml");
2739
- if (!fs18.existsSync(configPath)) {
2740
- logger.warn("litellm-config.yaml not found \u2014 cannot start proxy");
3104
+ function generateLitellmConfig(provider, modelMap) {
3105
+ const apiKeyVar = providerApiKeyEnvVar(provider);
3106
+ const entries = ["model_list:"];
3107
+ for (const [tier, providerModel] of Object.entries(modelMap)) {
3108
+ const anthropicIds = TIER_TO_ANTHROPIC_IDS[tier];
3109
+ if (!anthropicIds) continue;
3110
+ for (const modelName of anthropicIds) {
3111
+ entries.push(` - model_name: ${modelName}`);
3112
+ entries.push(` litellm_params:`);
3113
+ entries.push(` model: ${provider}/${providerModel}`);
3114
+ entries.push(` api_key: os.environ/${apiKeyVar}`);
3115
+ }
3116
+ }
3117
+ return entries.join("\n") + "\n";
3118
+ }
3119
+ async function tryStartLitellm(url, projectDir, generatedConfig) {
3120
+ const manualConfigPath = path18.join(projectDir, "litellm-config.yaml");
3121
+ let configPath;
3122
+ if (fs19.existsSync(manualConfigPath)) {
3123
+ configPath = manualConfigPath;
3124
+ } else if (generatedConfig) {
3125
+ configPath = path18.join(os.tmpdir(), "kody-litellm-config.yaml");
3126
+ fs19.writeFileSync(configPath, generatedConfig);
3127
+ } else {
3128
+ logger.warn("litellm-config.yaml not found and no provider configured \u2014 cannot start proxy");
2741
3129
  return null;
2742
3130
  }
2743
3131
  const portMatch = url.match(/:(\d+)/);
@@ -2768,10 +3156,10 @@ async function tryStartLitellm(url, projectDir) {
2768
3156
  cmd = "python3";
2769
3157
  args2 = ["-m", "litellm", "--config", configPath, "--port", port];
2770
3158
  }
2771
- const dotenvPath = path17.join(projectDir, ".env");
3159
+ const dotenvPath = path18.join(projectDir, ".env");
2772
3160
  const dotenvVars = {};
2773
- if (fs18.existsSync(dotenvPath)) {
2774
- for (const line of fs18.readFileSync(dotenvPath, "utf-8").split("\n")) {
3161
+ if (fs19.existsSync(dotenvPath)) {
3162
+ for (const line of fs19.readFileSync(dotenvPath, "utf-8").split("\n")) {
2775
3163
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
2776
3164
  if (match) dotenvVars[match[1]] = match[2];
2777
3165
  }
@@ -2807,12 +3195,13 @@ var init_litellm = __esm({
2807
3195
  "src/cli/litellm.ts"() {
2808
3196
  "use strict";
2809
3197
  init_logger();
3198
+ init_config();
2810
3199
  }
2811
3200
  });
2812
3201
 
2813
3202
  // src/cli/task-state.ts
2814
- import * as fs19 from "fs";
2815
- import * as path18 from "path";
3203
+ import * as fs20 from "fs";
3204
+ import * as path19 from "path";
2816
3205
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
2817
3206
  if (!existingTaskId || !existingState) {
2818
3207
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -2844,11 +3233,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
2844
3233
  function resolveForIssue(issueNumber, projectDir) {
2845
3234
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
2846
3235
  if (existingTaskId) {
2847
- const statusPath = path18.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
3236
+ const statusPath = path19.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
2848
3237
  let existingState = null;
2849
- if (fs19.existsSync(statusPath)) {
3238
+ if (fs20.existsSync(statusPath)) {
2850
3239
  try {
2851
- existingState = JSON.parse(fs19.readFileSync(statusPath, "utf-8"));
3240
+ existingState = JSON.parse(fs20.readFileSync(statusPath, "utf-8"));
2852
3241
  } catch {
2853
3242
  }
2854
3243
  }
@@ -2878,13 +3267,45 @@ var init_task_state = __esm({
2878
3267
 
2879
3268
  // src/entry.ts
2880
3269
  var entry_exports = {};
2881
- import * as fs20 from "fs";
2882
- import * as path19 from "path";
3270
+ import * as fs21 from "fs";
3271
+ import * as path20 from "path";
3272
+ async function ensureLitellmProxy(config, projectDir) {
3273
+ if (!needsLitellmProxy(config)) return null;
3274
+ const litellmUrl = getLitellmUrl(config);
3275
+ const proxyRunning = await checkLitellmHealth(litellmUrl);
3276
+ let litellmProcess = null;
3277
+ if (!proxyRunning) {
3278
+ if (config.agent.provider && config.agent.provider !== "anthropic") {
3279
+ const keyVar = providerApiKeyEnvVar(config.agent.provider);
3280
+ if (!process.env[keyVar]) {
3281
+ logger.error(`Provider '${config.agent.provider}' requires ${keyVar} environment variable`);
3282
+ process.exit(1);
3283
+ }
3284
+ }
3285
+ let generatedConfig;
3286
+ if (config.agent.provider && config.agent.provider !== "anthropic") {
3287
+ generatedConfig = generateLitellmConfig(config.agent.provider, config.agent.modelMap);
3288
+ }
3289
+ litellmProcess = await tryStartLitellm(litellmUrl, projectDir, generatedConfig);
3290
+ if (!litellmProcess) {
3291
+ logger.error("LiteLLM is configured but could not be started. Install it with: pip install 'litellm[proxy]'");
3292
+ process.exit(1);
3293
+ }
3294
+ } else {
3295
+ logger.info(`LiteLLM proxy already running at ${litellmUrl}`);
3296
+ }
3297
+ process.env.ANTHROPIC_BASE_URL = litellmUrl;
3298
+ logger.info(`ANTHROPIC_BASE_URL set to ${litellmUrl}`);
3299
+ if (!process.env.ANTHROPIC_API_KEY || !process.env.ANTHROPIC_API_KEY.startsWith("sk-ant-")) {
3300
+ process.env.ANTHROPIC_API_KEY = "sk-ant-api03-litellm-proxy-key-00000000000000000000000000000000000000000000000000000000000000000000";
3301
+ }
3302
+ return litellmProcess;
3303
+ }
2883
3304
  async function main() {
2884
3305
  const input = parseArgs();
2885
- const projectDir = input.cwd ? path19.resolve(input.cwd) : process.cwd();
3306
+ const projectDir = input.cwd ? path20.resolve(input.cwd) : process.cwd();
2886
3307
  if (input.cwd) {
2887
- if (!fs20.existsSync(projectDir)) {
3308
+ if (!fs21.existsSync(projectDir)) {
2888
3309
  console.error(`--cwd path does not exist: ${projectDir}`);
2889
3310
  process.exit(1);
2890
3311
  }
@@ -2938,8 +3359,8 @@ async function main() {
2938
3359
  process.exit(1);
2939
3360
  }
2940
3361
  }
2941
- const taskDir = path19.join(projectDir, ".kody", "tasks", taskId);
2942
- fs20.mkdirSync(taskDir, { recursive: true });
3362
+ const taskDir = path20.join(projectDir, ".kody", "tasks", taskId);
3363
+ fs21.mkdirSync(taskDir, { recursive: true });
2943
3364
  if (input.command === "status") {
2944
3365
  printStatus(taskId, taskDir);
2945
3366
  return;
@@ -2972,18 +3393,7 @@ async function main() {
2972
3393
  }
2973
3394
  }
2974
3395
  const config2 = getProjectConfig();
2975
- let litellmProcess2 = null;
2976
- if (config2.agent.litellmUrl) {
2977
- const proxyRunning = await checkLitellmHealth(config2.agent.litellmUrl);
2978
- if (!proxyRunning) {
2979
- litellmProcess2 = await tryStartLitellm(config2.agent.litellmUrl, projectDir);
2980
- if (!litellmProcess2) {
2981
- logger.error("LiteLLM is configured (litellmUrl) but could not be started. Install it with: pip install 'litellm[proxy]'");
2982
- process.exit(1);
2983
- }
2984
- }
2985
- process.env.ANTHROPIC_BASE_URL = config2.agent.litellmUrl;
2986
- }
3396
+ const litellmProcess2 = await ensureLitellmProxy(config2, projectDir);
2987
3397
  const runners2 = createRunners(config2);
2988
3398
  const defaultRunnerName2 = config2.agent.defaultRunner ?? Object.keys(runners2)[0] ?? "claude";
2989
3399
  const defaultRunner2 = runners2[defaultRunnerName2];
@@ -3027,31 +3437,31 @@ async function main() {
3027
3437
  logger.info("Preflight checks:");
3028
3438
  runPreflight();
3029
3439
  if (input.task) {
3030
- fs20.writeFileSync(path19.join(taskDir, "task.md"), input.task);
3440
+ fs21.writeFileSync(path20.join(taskDir, "task.md"), input.task);
3031
3441
  }
3032
- const taskMdPath = path19.join(taskDir, "task.md");
3033
- if (!fs20.existsSync(taskMdPath) && isPRFix && input.prNumber) {
3442
+ const taskMdPath = path20.join(taskDir, "task.md");
3443
+ if (!fs21.existsSync(taskMdPath) && isPRFix && input.prNumber) {
3034
3444
  logger.info(`Fetching PR #${input.prNumber} details as task context...`);
3035
3445
  const prDetails = getPRDetails(input.prNumber);
3036
3446
  if (prDetails) {
3037
3447
  const taskContent = `# ${prDetails.title}
3038
3448
 
3039
3449
  ${prDetails.body ?? ""}`;
3040
- fs20.writeFileSync(taskMdPath, taskContent);
3450
+ fs21.writeFileSync(taskMdPath, taskContent);
3041
3451
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
3042
3452
  }
3043
- } else if (!fs20.existsSync(taskMdPath) && input.issueNumber) {
3453
+ } else if (!fs21.existsSync(taskMdPath) && input.issueNumber) {
3044
3454
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
3045
3455
  const issue = getIssue(input.issueNumber);
3046
3456
  if (issue) {
3047
3457
  const taskContent = `# ${issue.title}
3048
3458
 
3049
3459
  ${issue.body ?? ""}`;
3050
- fs20.writeFileSync(taskMdPath, taskContent);
3460
+ fs21.writeFileSync(taskMdPath, taskContent);
3051
3461
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
3052
3462
  }
3053
3463
  }
3054
- if (!fs20.existsSync(taskMdPath)) {
3464
+ if (!fs21.existsSync(taskMdPath)) {
3055
3465
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
3056
3466
  process.exit(1);
3057
3467
  }
@@ -3073,10 +3483,10 @@ ${input.feedback}` : reviewContext;
3073
3483
  }
3074
3484
  }
3075
3485
  const config = getProjectConfig();
3076
- let litellmProcess = null;
3486
+ let litellmProcess = await ensureLitellmProxy(config, projectDir);
3077
3487
  const cleanupLitellm = () => {
3078
3488
  if (litellmProcess) {
3079
- litellmProcess.kill();
3489
+ litellmProcess.kill?.();
3080
3490
  litellmProcess = null;
3081
3491
  }
3082
3492
  };
@@ -3089,20 +3499,6 @@ ${input.feedback}` : reviewContext;
3089
3499
  cleanupLitellm();
3090
3500
  process.exit(143);
3091
3501
  });
3092
- if (config.agent.litellmUrl) {
3093
- const proxyRunning = await checkLitellmHealth(config.agent.litellmUrl);
3094
- if (!proxyRunning) {
3095
- litellmProcess = await tryStartLitellm(config.agent.litellmUrl, projectDir);
3096
- if (!litellmProcess) {
3097
- logger.error("LiteLLM is configured (litellmUrl) but could not be started. Install it with: pip install 'litellm[proxy]'");
3098
- process.exit(1);
3099
- }
3100
- } else {
3101
- logger.info(`LiteLLM proxy already running at ${config.agent.litellmUrl}`);
3102
- }
3103
- process.env.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
3104
- logger.info(`ANTHROPIC_BASE_URL set to ${config.agent.litellmUrl}`);
3105
- }
3106
3502
  const runners = createRunners(config);
3107
3503
  const defaultRunnerName = config.agent.defaultRunner ?? Object.keys(runners)[0] ?? "claude";
3108
3504
  const defaultRunner = runners[defaultRunnerName];
@@ -3148,7 +3544,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
3148
3544
  }
3149
3545
  }
3150
3546
  const state = await runPipeline(ctx);
3151
- const files = fs20.readdirSync(taskDir);
3547
+ const files = fs21.readdirSync(taskDir);
3152
3548
  console.log(`
3153
3549
  Artifacts in ${taskDir}:`);
3154
3550
  for (const f of files) {
@@ -3194,6 +3590,7 @@ var init_entry = __esm({
3194
3590
  init_litellm();
3195
3591
  init_task_resolution();
3196
3592
  init_task_state();
3593
+ init_config();
3197
3594
  main().catch(async (err) => {
3198
3595
  const msg = err instanceof Error ? err.message : String(err);
3199
3596
  console.error(msg);
@@ -3211,15 +3608,15 @@ var init_entry = __esm({
3211
3608
  });
3212
3609
 
3213
3610
  // src/bin/cli.ts
3214
- import * as fs21 from "fs";
3215
- import * as path20 from "path";
3611
+ import * as fs22 from "fs";
3612
+ import * as path21 from "path";
3216
3613
  import { execFileSync as execFileSync11 } from "child_process";
3217
3614
  import { fileURLToPath } from "url";
3218
- var __dirname = path20.dirname(fileURLToPath(import.meta.url));
3219
- var PKG_ROOT = path20.resolve(__dirname, "..", "..");
3615
+ var __dirname = path21.dirname(fileURLToPath(import.meta.url));
3616
+ var PKG_ROOT = path21.resolve(__dirname, "..", "..");
3220
3617
  function getVersion() {
3221
- const pkgPath = path20.join(PKG_ROOT, "package.json");
3222
- const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
3618
+ const pkgPath = path21.join(PKG_ROOT, "package.json");
3619
+ const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
3223
3620
  return pkg.version;
3224
3621
  }
3225
3622
  function checkCommand2(name, args2, fix) {
@@ -3235,7 +3632,7 @@ function checkCommand2(name, args2, fix) {
3235
3632
  }
3236
3633
  }
3237
3634
  function checkFile(filePath, description, fix) {
3238
- if (fs21.existsSync(filePath)) {
3635
+ if (fs22.existsSync(filePath)) {
3239
3636
  return { name: description, ok: true, detail: filePath };
3240
3637
  }
3241
3638
  return { name: description, ok: false, fix };
@@ -3307,9 +3704,9 @@ function checkGhSecret(repoSlug, secretName) {
3307
3704
  }
3308
3705
  function detectBasicConfig(cwd) {
3309
3706
  let pm = "pnpm";
3310
- if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) pm = "yarn";
3311
- else if (fs21.existsSync(path20.join(cwd, "bun.lockb"))) pm = "bun";
3312
- else if (!fs21.existsSync(path20.join(cwd, "pnpm-lock.yaml")) && fs21.existsSync(path20.join(cwd, "package-lock.json"))) pm = "npm";
3707
+ if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) pm = "yarn";
3708
+ else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) pm = "bun";
3709
+ else if (!fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml")) && fs22.existsSync(path21.join(cwd, "package-lock.json"))) pm = "npm";
3313
3710
  let defaultBranch = "main";
3314
3711
  try {
3315
3712
  const ref = execFileSync11("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
@@ -3352,7 +3749,7 @@ function detectBasicConfig(cwd) {
3352
3749
  function buildConfig(cwd, basic) {
3353
3750
  const pkg = (() => {
3354
3751
  try {
3355
- return JSON.parse(fs21.readFileSync(path20.join(cwd, "package.json"), "utf-8"));
3752
+ return JSON.parse(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
3356
3753
  } catch {
3357
3754
  return {};
3358
3755
  }
@@ -3392,35 +3789,35 @@ function initCommand(opts) {
3392
3789
  console.log(`Project: ${cwd}
3393
3790
  `);
3394
3791
  console.log("\u2500\u2500 Files \u2500\u2500");
3395
- const templatesDir = path20.join(PKG_ROOT, "templates");
3792
+ const templatesDir = path21.join(PKG_ROOT, "templates");
3396
3793
  const basic = detectBasicConfig(cwd);
3397
- const workflowSrc = path20.join(templatesDir, "kody.yml");
3398
- const workflowDest = path20.join(cwd, ".github", "workflows", "kody.yml");
3399
- if (!fs21.existsSync(workflowSrc)) {
3794
+ const workflowSrc = path21.join(templatesDir, "kody.yml");
3795
+ const workflowDest = path21.join(cwd, ".github", "workflows", "kody.yml");
3796
+ if (!fs22.existsSync(workflowSrc)) {
3400
3797
  console.error(" \u2717 Template kody.yml not found in package");
3401
3798
  process.exit(1);
3402
3799
  }
3403
- if (fs21.existsSync(workflowDest) && !opts.force) {
3800
+ if (fs22.existsSync(workflowDest) && !opts.force) {
3404
3801
  console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
3405
3802
  } else {
3406
- fs21.mkdirSync(path20.dirname(workflowDest), { recursive: true });
3407
- fs21.copyFileSync(workflowSrc, workflowDest);
3803
+ fs22.mkdirSync(path21.dirname(workflowDest), { recursive: true });
3804
+ fs22.copyFileSync(workflowSrc, workflowDest);
3408
3805
  console.log(" \u2713 .github/workflows/kody.yml");
3409
3806
  }
3410
- const configDest = path20.join(cwd, "kody.config.json");
3411
- if (!fs21.existsSync(configDest) || opts.force) {
3807
+ const configDest = path21.join(cwd, "kody.config.json");
3808
+ if (!fs22.existsSync(configDest) || opts.force) {
3412
3809
  const config = buildConfig(cwd, basic);
3413
- fs21.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
3810
+ fs22.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
3414
3811
  console.log(" \u2713 kody.config.json (auto-configured)");
3415
3812
  } else {
3416
3813
  console.log(" \u25CB kody.config.json (exists)");
3417
3814
  }
3418
- const gitignorePath = path20.join(cwd, ".gitignore");
3419
- if (fs21.existsSync(gitignorePath)) {
3420
- const content = fs21.readFileSync(gitignorePath, "utf-8");
3815
+ const gitignorePath = path21.join(cwd, ".gitignore");
3816
+ if (fs22.existsSync(gitignorePath)) {
3817
+ const content = fs22.readFileSync(gitignorePath, "utf-8");
3421
3818
  if (content.includes(".tasks/")) {
3422
3819
  const updated = content.replace(/\n?\.tasks\/\n?/g, "\n");
3423
- fs21.writeFileSync(gitignorePath, updated);
3820
+ fs22.writeFileSync(gitignorePath, updated);
3424
3821
  console.log(" \u2713 .gitignore (removed legacy .tasks/ \u2014 tasks now committed in .kody/tasks/)");
3425
3822
  } else {
3426
3823
  console.log(" \u25CB .gitignore (ok)");
@@ -3431,7 +3828,7 @@ function initCommand(opts) {
3431
3828
  checkCommand2("gh", ["--version"], "Install: https://cli.github.com"),
3432
3829
  checkCommand2("git", ["--version"], "Install git"),
3433
3830
  checkCommand2("node", ["--version"], "Install Node.js >= 22"),
3434
- checkFile(path20.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
3831
+ checkFile(path21.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
3435
3832
  ];
3436
3833
  for (const c of checks) {
3437
3834
  if (c.ok) {
@@ -3509,9 +3906,9 @@ function initCommand(opts) {
3509
3906
  }
3510
3907
  }
3511
3908
  console.log("\n\u2500\u2500 Config \u2500\u2500");
3512
- if (fs21.existsSync(configDest)) {
3909
+ if (fs22.existsSync(configDest)) {
3513
3910
  try {
3514
- const config = JSON.parse(fs21.readFileSync(configDest, "utf-8"));
3911
+ const config = JSON.parse(fs22.readFileSync(configDest, "utf-8"));
3515
3912
  const configChecks = [];
3516
3913
  if (config.github?.owner && config.github?.repo) {
3517
3914
  configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
@@ -3541,10 +3938,10 @@ function initCommand(opts) {
3541
3938
  const filesToCommit = [
3542
3939
  ".github/workflows/kody.yml",
3543
3940
  "kody.config.json"
3544
- ].filter((f) => fs21.existsSync(path20.join(cwd, f)));
3941
+ ].filter((f) => fs22.existsSync(path21.join(cwd, f)));
3545
3942
  if (filesToCommit.length > 0) {
3546
3943
  try {
3547
- const fullPaths = filesToCommit.map((f) => path20.join(cwd, f));
3944
+ const fullPaths = filesToCommit.map((f) => path21.join(cwd, f));
3548
3945
  execFileSync11("npx", ["prettier", "--write", ...fullPaths], {
3549
3946
  cwd,
3550
3947
  encoding: "utf-8",
@@ -3606,20 +4003,20 @@ function initCommand(opts) {
3606
4003
  }
3607
4004
  var STEP_STAGES = ["taskify", "plan", "build", "review", "review-fix"];
3608
4005
  function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
3609
- const srcDir = path20.join(cwd, "src");
3610
- const baseDir = fs21.existsSync(srcDir) ? srcDir : cwd;
4006
+ const srcDir = path21.join(cwd, "src");
4007
+ const baseDir = fs22.existsSync(srcDir) ? srcDir : cwd;
3611
4008
  const results = [];
3612
4009
  function walk(dir) {
3613
4010
  const entries = [];
3614
4011
  try {
3615
- for (const entry of fs21.readdirSync(dir, { withFileTypes: true })) {
4012
+ for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
3616
4013
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
3617
- const full = path20.join(dir, entry.name);
4014
+ const full = path21.join(dir, entry.name);
3618
4015
  if (entry.isDirectory()) {
3619
4016
  entries.push(...walk(full));
3620
4017
  } else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
3621
4018
  try {
3622
- const stat = fs21.statSync(full);
4019
+ const stat = fs22.statSync(full);
3623
4020
  if (stat.size >= 200 && stat.size <= 5e3) {
3624
4021
  entries.push({ filePath: full, size: stat.size });
3625
4022
  }
@@ -3633,8 +4030,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
3633
4030
  }
3634
4031
  const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
3635
4032
  for (const { filePath } of files) {
3636
- const rel = path20.relative(cwd, filePath);
3637
- const content = fs21.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
4033
+ const rel = path21.relative(cwd, filePath);
4034
+ const content = fs22.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
3638
4035
  results.push(`### File: ${rel}
3639
4036
  \`\`\`typescript
3640
4037
  ${content}
@@ -3646,9 +4043,9 @@ function ghComment(issueNumber, body, cwd) {
3646
4043
  try {
3647
4044
  let repoSlug = "";
3648
4045
  try {
3649
- const configPath = path20.join(cwd, "kody.config.json");
3650
- if (fs21.existsSync(configPath)) {
3651
- const config = JSON.parse(fs21.readFileSync(configPath, "utf-8"));
4046
+ const configPath = path21.join(cwd, "kody.config.json");
4047
+ if (fs22.existsSync(configPath)) {
4048
+ const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
3652
4049
  if (config.github?.owner && config.github?.repo) {
3653
4050
  repoSlug = `${config.github.owner}/${config.github.repo}`;
3654
4051
  }
@@ -3683,8 +4080,8 @@ function bootstrapCommand() {
3683
4080
  ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
3684
4081
  }
3685
4082
  const readIfExists = (rel, maxChars = 3e3) => {
3686
- const p = path20.join(cwd, rel);
3687
- if (fs21.existsSync(p)) return fs21.readFileSync(p, "utf-8").slice(0, maxChars);
4083
+ const p = path21.join(cwd, rel);
4084
+ if (fs22.existsSync(p)) return fs22.readFileSync(p, "utf-8").slice(0, maxChars);
3688
4085
  return null;
3689
4086
  };
3690
4087
  let repoContext = "";
@@ -3719,14 +4116,14 @@ ${sampleFiles}
3719
4116
 
3720
4117
  `;
3721
4118
  try {
3722
- const topDirs = fs21.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
4119
+ const topDirs = fs22.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
3723
4120
  repoContext += `## Top-level directories
3724
4121
  ${topDirs.join(", ")}
3725
4122
 
3726
4123
  `;
3727
- const srcDir = path20.join(cwd, "src");
3728
- if (fs21.existsSync(srcDir)) {
3729
- const srcDirs = fs21.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4124
+ const srcDir = path21.join(cwd, "src");
4125
+ if (fs22.existsSync(srcDir)) {
4126
+ const srcDirs = fs22.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
3730
4127
  if (srcDirs.length > 0) repoContext += `## src/ subdirectories
3731
4128
  ${srcDirs.join(", ")}
3732
4129
 
@@ -3736,17 +4133,17 @@ ${srcDirs.join(", ")}
3736
4133
  }
3737
4134
  const existingFiles = [];
3738
4135
  for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "vitest.config.ts", "vitest.config.mts", "jest.config.ts", "playwright.config.ts", ".eslintrc.js", "eslint.config.mjs", ".prettierrc"]) {
3739
- if (fs21.existsSync(path20.join(cwd, f))) existingFiles.push(f);
4136
+ if (fs22.existsSync(path21.join(cwd, f))) existingFiles.push(f);
3740
4137
  }
3741
4138
  if (existingFiles.length) repoContext += `## Config files present
3742
4139
  ${existingFiles.join(", ")}
3743
4140
 
3744
4141
  `;
3745
4142
  console.log("\u2500\u2500 Project Memory \u2500\u2500");
3746
- const memoryDir = path20.join(cwd, ".kody", "memory");
3747
- fs21.mkdirSync(memoryDir, { recursive: true });
3748
- const archPath = path20.join(memoryDir, "architecture.md");
3749
- const conventionsPath = path20.join(memoryDir, "conventions.md");
4143
+ const memoryDir = path21.join(cwd, ".kody", "memory");
4144
+ fs22.mkdirSync(memoryDir, { recursive: true });
4145
+ const archPath = path21.join(memoryDir, "architecture.md");
4146
+ const conventionsPath = path21.join(memoryDir, "conventions.md");
3750
4147
  const memoryPrompt = `You are analyzing a project to generate documentation for an autonomous SDLC pipeline.
3751
4148
 
3752
4149
  Given this project context, output ONLY a JSON object with EXACTLY this structure:
@@ -3787,12 +4184,12 @@ ${repoContext}`;
3787
4184
  const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3788
4185
  const parsed = JSON.parse(cleaned);
3789
4186
  if (parsed.architecture) {
3790
- fs21.writeFileSync(archPath, parsed.architecture);
4187
+ fs22.writeFileSync(archPath, parsed.architecture);
3791
4188
  const lineCount = parsed.architecture.split("\n").length;
3792
4189
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
3793
4190
  }
3794
4191
  if (parsed.conventions) {
3795
- fs21.writeFileSync(conventionsPath, parsed.conventions);
4192
+ fs22.writeFileSync(conventionsPath, parsed.conventions);
3796
4193
  const lineCount = parsed.conventions.split("\n").length;
3797
4194
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
3798
4195
  }
@@ -3801,34 +4198,34 @@ ${repoContext}`;
3801
4198
  const detected = detectArchitectureBasic(cwd);
3802
4199
  if (detected.length > 0) {
3803
4200
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3804
- fs21.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
4201
+ fs22.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
3805
4202
 
3806
4203
  ## Overview
3807
4204
  ${detected.join("\n")}
3808
4205
  `);
3809
4206
  console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
3810
4207
  }
3811
- fs21.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
4208
+ fs22.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
3812
4209
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
3813
4210
  }
3814
4211
  console.log("\n\u2500\u2500 Step Files \u2500\u2500");
3815
- const stepsDir = path20.join(cwd, ".kody", "steps");
3816
- fs21.mkdirSync(stepsDir, { recursive: true });
3817
- const arch = fs21.existsSync(archPath) ? fs21.readFileSync(archPath, "utf-8") : "";
3818
- const conv = fs21.existsSync(conventionsPath) ? fs21.readFileSync(conventionsPath, "utf-8") : "";
4212
+ const stepsDir = path21.join(cwd, ".kody", "steps");
4213
+ fs22.mkdirSync(stepsDir, { recursive: true });
4214
+ const arch = fs22.existsSync(archPath) ? fs22.readFileSync(archPath, "utf-8") : "";
4215
+ const conv = fs22.existsSync(conventionsPath) ? fs22.readFileSync(conventionsPath, "utf-8") : "";
3819
4216
  console.log(" \u23F3 Customizing step files...");
3820
4217
  let stepCount = 0;
3821
4218
  for (const stage of STEP_STAGES) {
3822
- const templatePath = path20.join(PKG_ROOT, "prompts", `${stage}.md`);
3823
- if (!fs21.existsSync(templatePath)) {
4219
+ const templatePath = path21.join(PKG_ROOT, "prompts", `${stage}.md`);
4220
+ if (!fs22.existsSync(templatePath)) {
3824
4221
  console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
3825
4222
  continue;
3826
4223
  }
3827
- const defaultPrompt = fs21.readFileSync(templatePath, "utf-8");
4224
+ const defaultPrompt = fs22.readFileSync(templatePath, "utf-8");
3828
4225
  const contextPlaceholder = "{{TASK_CONTEXT}}";
3829
4226
  const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
3830
4227
  if (placeholderIdx === -1) {
3831
- fs21.copyFileSync(templatePath, path20.join(stepsDir, `${stage}.md`));
4228
+ fs22.copyFileSync(templatePath, path21.join(stepsDir, `${stage}.md`));
3832
4229
  stepCount++;
3833
4230
  console.log(` \u2713 ${stage}.md`);
3834
4231
  continue;
@@ -3885,12 +4282,12 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
3885
4282
  let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
3886
4283
  cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
3887
4284
  const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
3888
- fs21.writeFileSync(path20.join(stepsDir, `${stage}.md`), finalPrompt);
4285
+ fs22.writeFileSync(path21.join(stepsDir, `${stage}.md`), finalPrompt);
3889
4286
  stepCount++;
3890
4287
  console.log(` \u2713 ${stage}.md`);
3891
4288
  } catch {
3892
4289
  console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
3893
- fs21.copyFileSync(templatePath, path20.join(stepsDir, `${stage}.md`));
4290
+ fs22.copyFileSync(templatePath, path21.join(stepsDir, `${stage}.md`));
3894
4291
  stepCount++;
3895
4292
  }
3896
4293
  }
@@ -3899,16 +4296,16 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
3899
4296
  const filesToCommit = [
3900
4297
  ".kody/memory/architecture.md",
3901
4298
  ".kody/memory/conventions.md"
3902
- ].filter((f) => fs21.existsSync(path20.join(cwd, f)));
4299
+ ].filter((f) => fs22.existsSync(path21.join(cwd, f)));
3903
4300
  for (const stage of STEP_STAGES) {
3904
4301
  const stepFile = `.kody/steps/${stage}.md`;
3905
- if (fs21.existsSync(path20.join(cwd, stepFile))) {
4302
+ if (fs22.existsSync(path21.join(cwd, stepFile))) {
3906
4303
  filesToCommit.push(stepFile);
3907
4304
  }
3908
4305
  }
3909
4306
  if (filesToCommit.length > 0) {
3910
4307
  try {
3911
- const fullPaths = filesToCommit.map((f) => path20.join(cwd, f));
4308
+ const fullPaths = filesToCommit.map((f) => path21.join(cwd, f));
3912
4309
  for (let pass = 0; pass < 2; pass++) {
3913
4310
  execFileSync11("npx", ["prettier", "--write", ...fullPaths], {
3914
4311
  cwd,
@@ -3935,9 +4332,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
3935
4332
  console.log(` \u2713 Pushed branch: ${branchName}`);
3936
4333
  let baseBranch = "main";
3937
4334
  try {
3938
- const configPath = path20.join(cwd, "kody.config.json");
3939
- if (fs21.existsSync(configPath)) {
3940
- const config = JSON.parse(fs21.readFileSync(configPath, "utf-8"));
4335
+ const configPath = path21.join(cwd, "kody.config.json");
4336
+ if (fs22.existsSync(configPath)) {
4337
+ const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
3941
4338
  baseBranch = config.git?.defaultBranch ?? "main";
3942
4339
  }
3943
4340
  } catch {
@@ -4004,10 +4401,10 @@ Review and merge to activate project-specific pipeline configuration.`, cwd);
4004
4401
  }
4005
4402
  function detectArchitectureBasic(cwd) {
4006
4403
  const detected = [];
4007
- const pkgPath = path20.join(cwd, "package.json");
4008
- if (fs21.existsSync(pkgPath)) {
4404
+ const pkgPath = path21.join(cwd, "package.json");
4405
+ if (fs22.existsSync(pkgPath)) {
4009
4406
  try {
4010
- const pkg = JSON.parse(fs21.readFileSync(pkgPath, "utf-8"));
4407
+ const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
4011
4408
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4012
4409
  if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
4013
4410
  else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
@@ -4023,10 +4420,10 @@ function detectArchitectureBasic(cwd) {
4023
4420
  if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
4024
4421
  if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
4025
4422
  if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
4026
- if (fs21.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
4027
- else if (fs21.existsSync(path20.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
4028
- else if (fs21.existsSync(path20.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
4029
- else if (fs21.existsSync(path20.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
4423
+ if (fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
4424
+ else if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
4425
+ else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
4426
+ else if (fs22.existsSync(path21.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
4030
4427
  } catch {
4031
4428
  }
4032
4429
  }