@kody-ade/kody-engine-lite 0.1.109 → 0.1.110

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.
Files changed (2) hide show
  1. package/dist/bin/cli.js +442 -419
  2. package/package.json +1 -1
package/dist/bin/cli.js CHANGED
@@ -998,6 +998,180 @@ var init_task_resolution = __esm({
998
998
  }
999
999
  });
1000
1000
 
1001
+ // src/cli/litellm.ts
1002
+ import * as fs10 from "fs";
1003
+ import * as os from "os";
1004
+ import * as path9 from "path";
1005
+ import { execFileSync as execFileSync9 } from "child_process";
1006
+ async function checkLitellmHealth(url) {
1007
+ try {
1008
+ const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
1009
+ return response.ok;
1010
+ } catch {
1011
+ return false;
1012
+ }
1013
+ }
1014
+ async function checkModelHealth(baseUrl, apiKey, model) {
1015
+ try {
1016
+ const res = await fetch(`${baseUrl}/v1/messages`, {
1017
+ method: "POST",
1018
+ headers: {
1019
+ "Content-Type": "application/json",
1020
+ "x-api-key": apiKey,
1021
+ "anthropic-version": "2023-06-01"
1022
+ },
1023
+ body: JSON.stringify({
1024
+ model,
1025
+ max_tokens: 4,
1026
+ messages: [{ role: "user", content: "Reply with: ok" }]
1027
+ }),
1028
+ signal: AbortSignal.timeout(3e4)
1029
+ });
1030
+ if (!res.ok) {
1031
+ const body2 = await res.text().catch(() => "");
1032
+ return { ok: false, error: `HTTP ${res.status}: ${body2.slice(0, 200)}` };
1033
+ }
1034
+ const body = await res.json();
1035
+ const hasAnthropicContent = Array.isArray(body.content) && body.content.some((b) => b.type === "text");
1036
+ const hasThinkingContent = Array.isArray(body.content) && body.content.some((b) => b.type === "thinking");
1037
+ const hasOpenAIContent = !!body.choices?.[0]?.message?.content;
1038
+ if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent) {
1039
+ return { ok: false, error: `Unexpected response format: ${JSON.stringify(body).slice(0, 200)}` };
1040
+ }
1041
+ return { ok: true };
1042
+ } catch (err) {
1043
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1044
+ }
1045
+ }
1046
+ function generateLitellmConfig(provider, modelMap) {
1047
+ const apiKeyVar = providerApiKeyEnvVar(provider);
1048
+ const entries = ["model_list:"];
1049
+ const seen = /* @__PURE__ */ new Set();
1050
+ for (const providerModel of Object.values(modelMap)) {
1051
+ if (seen.has(providerModel)) continue;
1052
+ seen.add(providerModel);
1053
+ entries.push(` - model_name: ${providerModel}`);
1054
+ entries.push(` litellm_params:`);
1055
+ entries.push(` model: ${provider}/${providerModel}`);
1056
+ entries.push(` api_key: os.environ/${apiKeyVar}`);
1057
+ }
1058
+ return entries.join("\n") + "\n";
1059
+ }
1060
+ function generateLitellmConfigFromStages(defaultConfig, stages) {
1061
+ const proxyModels = [];
1062
+ if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
1063
+ proxyModels.push(defaultConfig);
1064
+ }
1065
+ if (stages) {
1066
+ for (const sc of Object.values(stages)) {
1067
+ if (sc.provider !== "claude" && sc.provider !== "anthropic") {
1068
+ proxyModels.push(sc);
1069
+ }
1070
+ }
1071
+ }
1072
+ if (proxyModels.length === 0) return void 0;
1073
+ const entries = ["model_list:"];
1074
+ const seen = /* @__PURE__ */ new Set();
1075
+ for (const { provider, model } of proxyModels) {
1076
+ const key = `${provider}/${model}`;
1077
+ if (seen.has(key)) continue;
1078
+ seen.add(key);
1079
+ const apiKeyVar = providerApiKeyEnvVar(provider);
1080
+ entries.push(` - model_name: ${model}`);
1081
+ entries.push(` litellm_params:`);
1082
+ entries.push(` model: ${provider}/${model}`);
1083
+ entries.push(` api_key: os.environ/${apiKeyVar}`);
1084
+ }
1085
+ return entries.join("\n") + "\n";
1086
+ }
1087
+ async function tryStartLitellm(url, projectDir, generatedConfig) {
1088
+ if (!generatedConfig) {
1089
+ logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
1090
+ return null;
1091
+ }
1092
+ const configPath = path9.join(os.tmpdir(), "kody-litellm-config.yaml");
1093
+ fs10.writeFileSync(configPath, generatedConfig);
1094
+ const portMatch = url.match(/:(\d+)/);
1095
+ const port = portMatch ? portMatch[1] : "4000";
1096
+ let litellmFound = false;
1097
+ try {
1098
+ execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1099
+ litellmFound = true;
1100
+ } catch {
1101
+ try {
1102
+ execFileSync9("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
1103
+ litellmFound = true;
1104
+ } catch {
1105
+ }
1106
+ }
1107
+ if (!litellmFound) {
1108
+ logger.warn("litellm not installed (pip install 'litellm[proxy]')");
1109
+ return null;
1110
+ }
1111
+ logger.info(`Starting LiteLLM proxy on port ${port}...`);
1112
+ let cmd;
1113
+ let args2;
1114
+ try {
1115
+ execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1116
+ cmd = "litellm";
1117
+ args2 = ["--config", configPath, "--port", port];
1118
+ } catch {
1119
+ cmd = "python3";
1120
+ args2 = ["-m", "litellm", "--config", configPath, "--port", port];
1121
+ }
1122
+ const dotenvPath = path9.join(projectDir, ".env");
1123
+ const dotenvVars = {};
1124
+ if (fs10.existsSync(dotenvPath)) {
1125
+ for (const rawLine of fs10.readFileSync(dotenvPath, "utf-8").split("\n")) {
1126
+ const line = rawLine.trim();
1127
+ if (!line || line.startsWith("#")) continue;
1128
+ const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
1129
+ if (match) {
1130
+ let value = match[2].trim();
1131
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1132
+ value = value.slice(1, -1);
1133
+ }
1134
+ const commentIdx = value.indexOf(" #");
1135
+ if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
1136
+ if (value) dotenvVars[match[1]] = value;
1137
+ }
1138
+ }
1139
+ if (Object.keys(dotenvVars).length > 0) {
1140
+ logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
1141
+ }
1142
+ }
1143
+ const { spawn: spawn2 } = await import("child_process");
1144
+ const child = spawn2(cmd, args2, {
1145
+ stdio: ["ignore", "pipe", "pipe"],
1146
+ detached: true,
1147
+ env: { ...process.env, ...dotenvVars }
1148
+ });
1149
+ let proxyStderr = "";
1150
+ child.stderr?.on("data", (chunk) => {
1151
+ proxyStderr += chunk.toString();
1152
+ });
1153
+ for (let i = 0; i < 30; i++) {
1154
+ await new Promise((r) => setTimeout(r, 2e3));
1155
+ if (await checkLitellmHealth(url)) {
1156
+ logger.info(`LiteLLM proxy ready at ${url}`);
1157
+ return child;
1158
+ }
1159
+ }
1160
+ if (proxyStderr) {
1161
+ logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1e3)}`);
1162
+ }
1163
+ logger.warn("LiteLLM proxy failed to start within 60s");
1164
+ child.kill();
1165
+ return null;
1166
+ }
1167
+ var init_litellm = __esm({
1168
+ "src/cli/litellm.ts"() {
1169
+ "use strict";
1170
+ init_logger();
1171
+ init_config();
1172
+ }
1173
+ });
1174
+
1001
1175
  // src/cli/taskify-command.ts
1002
1176
  var taskify_command_exports = {};
1003
1177
  __export(taskify_command_exports, {
@@ -1007,8 +1181,8 @@ __export(taskify_command_exports, {
1007
1181
  taskifyCommand: () => taskifyCommand,
1008
1182
  topoSort: () => topoSort
1009
1183
  });
1010
- import * as fs10 from "fs";
1011
- import * as path9 from "path";
1184
+ import * as fs11 from "fs";
1185
+ import * as path10 from "path";
1012
1186
  import { fileURLToPath } from "url";
1013
1187
  import { execSync } from "child_process";
1014
1188
  function topoSort(tasks) {
@@ -1052,10 +1226,10 @@ function hasFlag(args2, flag) {
1052
1226
  async function runTaskifyCommand() {
1053
1227
  const args2 = process.argv.slice(3);
1054
1228
  const cwdArg = getArg(args2, "--cwd") ?? process.cwd();
1055
- const projectDir = path9.resolve(cwdArg);
1229
+ const projectDir = path10.resolve(cwdArg);
1056
1230
  const ticketId = getArg(args2, "--ticket") ?? process.env.TICKET_ID;
1057
1231
  const prdFileArg = getArg(args2, "--file") ?? process.env.PRD_FILE;
1058
- const prdFile = prdFileArg ? path9.resolve(projectDir, prdFileArg) : void 0;
1232
+ const prdFile = prdFileArg ? path10.resolve(projectDir, prdFileArg) : void 0;
1059
1233
  const issueNumberStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER ?? "";
1060
1234
  const issueNumber = issueNumberStr ? parseInt(issueNumberStr, 10) : void 0;
1061
1235
  const feedback = getArg(args2, "--feedback") ?? process.env.FEEDBACK;
@@ -1066,19 +1240,40 @@ async function runTaskifyCommand() {
1066
1240
  logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md>");
1067
1241
  process.exit(1);
1068
1242
  }
1069
- if (prdFile && !fs10.existsSync(prdFile)) {
1243
+ if (prdFile && !fs11.existsSync(prdFile)) {
1070
1244
  logger.error(`File not found: ${prdFile}`);
1071
1245
  process.exit(1);
1072
1246
  }
1073
1247
  setConfigDir(projectDir);
1074
1248
  setGhCwd(projectDir);
1075
- await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId });
1249
+ const config = getProjectConfig();
1250
+ let litellmProcess = null;
1251
+ let runnerEnv;
1252
+ if (anyStageNeedsProxy(config)) {
1253
+ const litellmUrl = getLitellmUrl();
1254
+ const proxyRunning = await checkLitellmHealth(litellmUrl);
1255
+ if (!proxyRunning) {
1256
+ let generatedConfig;
1257
+ if (config.agent.stages || config.agent.default) {
1258
+ generatedConfig = generateLitellmConfigFromStages(config.agent.default, config.agent.stages);
1259
+ } else if (config.agent.provider && config.agent.provider !== "anthropic") {
1260
+ generatedConfig = generateLitellmConfig(config.agent.provider, config.agent.modelMap);
1261
+ }
1262
+ litellmProcess = await tryStartLitellm(litellmUrl, projectDir, generatedConfig);
1263
+ }
1264
+ runnerEnv = {
1265
+ ANTHROPIC_BASE_URL: litellmUrl,
1266
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
1267
+ };
1268
+ }
1269
+ await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1270
+ litellmProcess?.kill();
1076
1271
  }
1077
1272
  async function taskifyCommand(opts) {
1078
1273
  const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
1079
1274
  const config = getProjectConfig();
1080
- const taskDir = path9.join(projectDir, ".kody", "tasks", taskId);
1081
- fs10.mkdirSync(taskDir, { recursive: true });
1275
+ const taskDir = path10.join(projectDir, ".kody", "tasks", taskId);
1276
+ fs11.mkdirSync(taskDir, { recursive: true });
1082
1277
  const mode = prdFile ? "file" : "ticket";
1083
1278
  logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile} issue=${issueNumber ?? "none"} task=${taskId}`);
1084
1279
  let mcpConfigJson;
@@ -1103,14 +1298,14 @@ Add the required MCP server config to \`kody.config.json\` and try again.`
1103
1298
  }
1104
1299
  const sc = resolveStageConfig(config, "taskify", "strong");
1105
1300
  const model = sc.model;
1106
- const fileContent = prdFile ? fs10.readFileSync(prdFile, "utf-8") : void 0;
1301
+ const fileContent = prdFile ? fs11.readFileSync(prdFile, "utf-8") : void 0;
1107
1302
  let projectContext;
1108
1303
  {
1109
1304
  const parts = [];
1110
- const memoryPath = path9.join(projectDir, ".kody", "memory.md");
1111
- if (fs10.existsSync(memoryPath)) {
1305
+ const memoryPath = path10.join(projectDir, ".kody", "memory.md");
1306
+ if (fs11.existsSync(memoryPath)) {
1112
1307
  try {
1113
- const content = fs10.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
1308
+ const content = fs11.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
1114
1309
  if (content.trim()) parts.push(`### Project Memory
1115
1310
  ${content}`);
1116
1311
  } catch {
@@ -1129,16 +1324,17 @@ ${lines.join("\n")}
1129
1324
  }
1130
1325
  const prompt = buildPrompt({ ticketId, fileContent, taskDir, feedback, projectContext });
1131
1326
  if (issueNumber && !local) {
1132
- const src = mode === "file" ? `file \`${path9.basename(prdFile)}\`` : `ticket **${ticketId}**`;
1327
+ const src = mode === "file" ? `file \`${path10.basename(prdFile)}\`` : `ticket **${ticketId}**`;
1133
1328
  postComment(issueNumber, `Kody is decomposing ${src} into tasks...`);
1134
1329
  setLifecycleLabel(issueNumber, "planning");
1135
1330
  }
1136
- fs10.writeFileSync(path9.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1331
+ fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1137
1332
  const runner = opts.runner ?? createClaudeCodeRunner();
1138
1333
  logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
1139
1334
  const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
1140
1335
  cwd: projectDir,
1141
- mcpConfigJson
1336
+ mcpConfigJson,
1337
+ env: opts.runnerEnv
1142
1338
  });
1143
1339
  if (result.outcome !== "completed") {
1144
1340
  const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
@@ -1151,8 +1347,8 @@ ${lines.join("\n")}
1151
1347
  }
1152
1348
  process.exit(1);
1153
1349
  }
1154
- const resultPath = path9.join(taskDir, RESULT_FILE);
1155
- if (!fs10.existsSync(resultPath)) {
1350
+ const resultPath = path10.join(taskDir, RESULT_FILE);
1351
+ if (!fs11.existsSync(resultPath)) {
1156
1352
  const errMsg = `Claude did not write ${RESULT_FILE}. Output:
1157
1353
 
1158
1354
  ${result.output?.slice(0, 500) ?? "(none)"}`;
@@ -1167,7 +1363,7 @@ ${errMsg}`);
1167
1363
  }
1168
1364
  let parsed;
1169
1365
  try {
1170
- parsed = JSON.parse(fs10.readFileSync(resultPath, "utf-8"));
1366
+ parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
1171
1367
  } catch {
1172
1368
  const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
1173
1369
  logger.error(`[taskify] ${errMsg}`);
@@ -1177,7 +1373,7 @@ ${errMsg}`);
1177
1373
  }
1178
1374
  process.exit(1);
1179
1375
  }
1180
- const sourceLabel = ticketId ?? (prdFile ? path9.basename(prdFile) : "spec");
1376
+ const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
1181
1377
  if (parsed.status === "questions") {
1182
1378
  handleQuestions(parsed, sourceLabel, issueNumber, local ?? false);
1183
1379
  } else if (parsed.status === "ready") {
@@ -1272,15 +1468,15 @@ function buildPrompt(opts) {
1272
1468
  const { ticketId, fileContent, taskDir, feedback, projectContext } = opts;
1273
1469
  const scriptDir = new URL(".", import.meta.url).pathname;
1274
1470
  const candidates = [
1275
- path9.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
1276
- path9.resolve(scriptDir, "..", "..", "prompts", "taskify-ticket.md"),
1277
- path9.resolve(__dirname, "..", "..", "prompts", "taskify-ticket.md"),
1278
- path9.resolve(__dirname, "..", "prompts", "taskify-ticket.md")
1471
+ path10.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
1472
+ path10.resolve(scriptDir, "..", "..", "prompts", "taskify-ticket.md"),
1473
+ path10.resolve(__dirname, "..", "..", "prompts", "taskify-ticket.md"),
1474
+ path10.resolve(__dirname, "..", "prompts", "taskify-ticket.md")
1279
1475
  ];
1280
1476
  let template = "";
1281
1477
  for (const candidate of candidates) {
1282
- if (fs10.existsSync(candidate)) {
1283
- template = fs10.readFileSync(candidate, "utf-8");
1478
+ if (fs11.existsSync(candidate)) {
1479
+ template = fs11.readFileSync(candidate, "utf-8");
1284
1480
  break;
1285
1481
  }
1286
1482
  }
@@ -1303,13 +1499,13 @@ function buildPrompt(opts) {
1303
1499
  return template;
1304
1500
  }
1305
1501
  function isTaskifyRun(taskDir) {
1306
- return fs10.existsSync(path9.join(taskDir, MARKER_FILE));
1502
+ return fs11.existsSync(path10.join(taskDir, MARKER_FILE));
1307
1503
  }
1308
1504
  function readTaskifyMarker(taskDir) {
1309
- const markerPath = path9.join(taskDir, MARKER_FILE);
1310
- if (!fs10.existsSync(markerPath)) return null;
1505
+ const markerPath = path10.join(taskDir, MARKER_FILE);
1506
+ if (!fs11.existsSync(markerPath)) return null;
1311
1507
  try {
1312
- return JSON.parse(fs10.readFileSync(markerPath, "utf-8"));
1508
+ return JSON.parse(fs11.readFileSync(markerPath, "utf-8"));
1313
1509
  } catch {
1314
1510
  return null;
1315
1511
  }
@@ -1324,7 +1520,8 @@ var init_taskify_command = __esm({
1324
1520
  init_github_api();
1325
1521
  init_logger();
1326
1522
  init_task_resolution();
1327
- __dirname = path9.dirname(fileURLToPath(import.meta.url));
1523
+ init_litellm();
1524
+ __dirname = path10.dirname(fileURLToPath(import.meta.url));
1328
1525
  AUTO_TRIGGER_THRESHOLD = 5;
1329
1526
  MAX_TASKS_GUARD = 20;
1330
1527
  TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -1340,7 +1537,7 @@ __export(parse_inputs_exports, {
1340
1537
  runCiParse: () => runCiParse,
1341
1538
  writeOutputs: () => writeOutputs
1342
1539
  });
1343
- import * as fs11 from "fs";
1540
+ import * as fs12 from "fs";
1344
1541
  function generateTimestamp() {
1345
1542
  const now = /* @__PURE__ */ new Date();
1346
1543
  const pad = (n) => String(n).padStart(2, "0");
@@ -1506,12 +1703,12 @@ function writeOutputs(result) {
1506
1703
  function output(key, value) {
1507
1704
  if (outputFile) {
1508
1705
  if (value.includes("\n")) {
1509
- fs11.appendFileSync(outputFile, `${key}<<KODY_EOF
1706
+ fs12.appendFileSync(outputFile, `${key}<<KODY_EOF
1510
1707
  ${value}
1511
1708
  KODY_EOF
1512
1709
  `);
1513
1710
  } else {
1514
- fs11.appendFileSync(outputFile, `${key}=${value}
1711
+ fs12.appendFileSync(outputFile, `${key}=${value}
1515
1712
  `);
1516
1713
  }
1517
1714
  }
@@ -1636,7 +1833,7 @@ var init_definitions = __esm({
1636
1833
  });
1637
1834
 
1638
1835
  // src/git-utils.ts
1639
- import { execFileSync as execFileSync9 } from "child_process";
1836
+ import { execFileSync as execFileSync10 } from "child_process";
1640
1837
  function getHookSafeEnv() {
1641
1838
  if (!_hookSafeEnv) {
1642
1839
  _hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
@@ -1644,7 +1841,7 @@ function getHookSafeEnv() {
1644
1841
  return _hookSafeEnv;
1645
1842
  }
1646
1843
  function git(args2, options) {
1647
- return execFileSync9("git", args2, {
1844
+ return execFileSync10("git", args2, {
1648
1845
  encoding: "utf-8",
1649
1846
  timeout: options?.timeout ?? 3e4,
1650
1847
  cwd: options?.cwd,
@@ -1830,14 +2027,14 @@ var init_git_utils = __esm({
1830
2027
  });
1831
2028
 
1832
2029
  // src/pipeline/state.ts
1833
- import * as fs12 from "fs";
1834
- import * as path10 from "path";
2030
+ import * as fs13 from "fs";
2031
+ import * as path11 from "path";
1835
2032
  function loadState(taskId, taskDir) {
1836
- const p = path10.join(taskDir, "status.json");
1837
- if (!fs12.existsSync(p)) return null;
2033
+ const p = path11.join(taskDir, "status.json");
2034
+ if (!fs13.existsSync(p)) return null;
1838
2035
  try {
1839
2036
  const result = parseJsonSafe(
1840
- fs12.readFileSync(p, "utf-8"),
2037
+ fs13.readFileSync(p, "utf-8"),
1841
2038
  ["taskId", "state", "stages", "createdAt", "updatedAt"]
1842
2039
  );
1843
2040
  if (!result.ok) {
@@ -1855,10 +2052,10 @@ function writeState(state, taskDir) {
1855
2052
  ...state,
1856
2053
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1857
2054
  };
1858
- const target = path10.join(taskDir, "status.json");
2055
+ const target = path11.join(taskDir, "status.json");
1859
2056
  const tmp = target + ".tmp";
1860
- fs12.writeFileSync(tmp, JSON.stringify(updated, null, 2));
1861
- fs12.renameSync(tmp, target);
2057
+ fs13.writeFileSync(tmp, JSON.stringify(updated, null, 2));
2058
+ fs13.renameSync(tmp, target);
1862
2059
  return updated;
1863
2060
  }
1864
2061
  function initState(taskId) {
@@ -1899,16 +2096,16 @@ var init_complexity = __esm({
1899
2096
  });
1900
2097
 
1901
2098
  // src/memory.ts
1902
- import * as fs13 from "fs";
1903
- import * as path11 from "path";
2099
+ import * as fs14 from "fs";
2100
+ import * as path12 from "path";
1904
2101
  function readProjectMemory(projectDir) {
1905
- const memoryDir = path11.join(projectDir, ".kody", "memory");
1906
- if (!fs13.existsSync(memoryDir)) return "";
1907
- const files = fs13.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
2102
+ const memoryDir = path12.join(projectDir, ".kody", "memory");
2103
+ if (!fs14.existsSync(memoryDir)) return "";
2104
+ const files = fs14.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1908
2105
  if (files.length === 0) return "";
1909
2106
  const sections = [];
1910
2107
  for (const file of files) {
1911
- const content = fs13.readFileSync(path11.join(memoryDir, file), "utf-8").trim();
2108
+ const content = fs14.readFileSync(path12.join(memoryDir, file), "utf-8").trim();
1912
2109
  if (content) {
1913
2110
  sections.push(`## ${file.replace(".md", "")}
1914
2111
  ${content}`);
@@ -1927,8 +2124,8 @@ var init_memory = __esm({
1927
2124
  });
1928
2125
 
1929
2126
  // src/context-tiers.ts
1930
- import * as fs14 from "fs";
1931
- import * as path12 from "path";
2127
+ import * as fs15 from "fs";
2128
+ import * as path13 from "path";
1932
2129
  function estimateTokens(text) {
1933
2130
  return Math.ceil(text.length / 4);
1934
2131
  }
@@ -2019,7 +2216,7 @@ function generateL1Json(content) {
2019
2216
  }
2020
2217
  }
2021
2218
  function getTieredContent(filePath, content) {
2022
- const key = path12.basename(filePath);
2219
+ const key = path13.basename(filePath);
2023
2220
  return {
2024
2221
  source: filePath,
2025
2222
  L0: generateL0(content, key),
@@ -2031,15 +2228,15 @@ function selectTier(tiered, tier) {
2031
2228
  return tiered[tier];
2032
2229
  }
2033
2230
  function readProjectMemoryTiered(projectDir, tier) {
2034
- const memoryDir = path12.join(projectDir, ".kody", "memory");
2035
- if (!fs14.existsSync(memoryDir)) return "";
2036
- const files = fs14.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
2231
+ const memoryDir = path13.join(projectDir, ".kody", "memory");
2232
+ if (!fs15.existsSync(memoryDir)) return "";
2233
+ const files = fs15.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
2037
2234
  if (files.length === 0) return "";
2038
2235
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
2039
2236
  const sections = [];
2040
2237
  for (const file of files) {
2041
- const filePath = path12.join(memoryDir, file);
2042
- const content = fs14.readFileSync(filePath, "utf-8").trim();
2238
+ const filePath = path13.join(memoryDir, file);
2239
+ const content = fs15.readFileSync(filePath, "utf-8").trim();
2043
2240
  if (!content) continue;
2044
2241
  const tiered = getTieredContent(filePath, content);
2045
2242
  const selected = selectTier(tiered, tier);
@@ -2062,9 +2259,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
2062
2259
  `;
2063
2260
  context += `Task Directory: ${taskDir}
2064
2261
  `;
2065
- const taskMdPath = path12.join(taskDir, "task.md");
2066
- if (fs14.existsSync(taskMdPath)) {
2067
- const content = fs14.readFileSync(taskMdPath, "utf-8");
2262
+ const taskMdPath = path13.join(taskDir, "task.md");
2263
+ if (fs15.existsSync(taskMdPath)) {
2264
+ const content = fs15.readFileSync(taskMdPath, "utf-8");
2068
2265
  const selected = selectContent(taskMdPath, content, policy.taskDescription);
2069
2266
  const label = tierLabel("Task Description", policy.taskDescription);
2070
2267
  context += `
@@ -2072,9 +2269,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
2072
2269
  ${selected}
2073
2270
  `;
2074
2271
  }
2075
- const taskJsonPath = path12.join(taskDir, "task.json");
2076
- if (fs14.existsSync(taskJsonPath)) {
2077
- const content = fs14.readFileSync(taskJsonPath, "utf-8");
2272
+ const taskJsonPath = path13.join(taskDir, "task.json");
2273
+ if (fs15.existsSync(taskJsonPath)) {
2274
+ const content = fs15.readFileSync(taskJsonPath, "utf-8");
2078
2275
  if (policy.taskClassification === "L2") {
2079
2276
  try {
2080
2277
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -2100,9 +2297,9 @@ ${selected}
2100
2297
  }
2101
2298
  }
2102
2299
  }
2103
- const specPath = path12.join(taskDir, "spec.md");
2104
- if (fs14.existsSync(specPath)) {
2105
- const content = fs14.readFileSync(specPath, "utf-8");
2300
+ const specPath = path13.join(taskDir, "spec.md");
2301
+ if (fs15.existsSync(specPath)) {
2302
+ const content = fs15.readFileSync(specPath, "utf-8");
2106
2303
  const selected = selectContent(specPath, content, policy.spec);
2107
2304
  const label = tierLabel("Spec", policy.spec);
2108
2305
  context += `
@@ -2110,9 +2307,9 @@ ${selected}
2110
2307
  ${selected}
2111
2308
  `;
2112
2309
  }
2113
- const planPath = path12.join(taskDir, "plan.md");
2114
- if (fs14.existsSync(planPath)) {
2115
- const content = fs14.readFileSync(planPath, "utf-8");
2310
+ const planPath = path13.join(taskDir, "plan.md");
2311
+ if (fs15.existsSync(planPath)) {
2312
+ const content = fs15.readFileSync(planPath, "utf-8");
2116
2313
  const selected = selectContent(planPath, content, policy.plan);
2117
2314
  const label = tierLabel("Plan", policy.plan);
2118
2315
  context += `
@@ -2120,9 +2317,9 @@ ${selected}
2120
2317
  ${selected}
2121
2318
  `;
2122
2319
  }
2123
- const contextMdPath = path12.join(taskDir, "context.md");
2124
- if (fs14.existsSync(contextMdPath)) {
2125
- const content = fs14.readFileSync(contextMdPath, "utf-8");
2320
+ const contextMdPath = path13.join(taskDir, "context.md");
2321
+ if (fs15.existsSync(contextMdPath)) {
2322
+ const content = fs15.readFileSync(contextMdPath, "utf-8");
2126
2323
  const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
2127
2324
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
2128
2325
  context += `
@@ -2208,24 +2405,24 @@ var init_context_tiers = __esm({
2208
2405
  });
2209
2406
 
2210
2407
  // src/context.ts
2211
- import * as fs15 from "fs";
2212
- import * as path13 from "path";
2408
+ import * as fs16 from "fs";
2409
+ import * as path14 from "path";
2213
2410
  function readPromptFile(stageName, projectDir) {
2214
2411
  if (projectDir) {
2215
- const stepFile = path13.join(projectDir, ".kody", "steps", `${stageName}.md`);
2216
- if (fs15.existsSync(stepFile)) {
2217
- return fs15.readFileSync(stepFile, "utf-8");
2412
+ const stepFile = path14.join(projectDir, ".kody", "steps", `${stageName}.md`);
2413
+ if (fs16.existsSync(stepFile)) {
2414
+ return fs16.readFileSync(stepFile, "utf-8");
2218
2415
  }
2219
2416
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
2220
2417
  }
2221
2418
  const scriptDir = new URL(".", import.meta.url).pathname;
2222
2419
  const candidates = [
2223
- path13.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
2224
- path13.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
2420
+ path14.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
2421
+ path14.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
2225
2422
  ];
2226
2423
  for (const candidate of candidates) {
2227
- if (fs15.existsSync(candidate)) {
2228
- return fs15.readFileSync(candidate, "utf-8");
2424
+ if (fs16.existsSync(candidate)) {
2425
+ return fs16.readFileSync(candidate, "utf-8");
2229
2426
  }
2230
2427
  }
2231
2428
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -2237,18 +2434,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
2237
2434
  `;
2238
2435
  context += `Task Directory: ${taskDir}
2239
2436
  `;
2240
- const taskMdPath = path13.join(taskDir, "task.md");
2241
- if (fs15.existsSync(taskMdPath)) {
2242
- const taskMd = fs15.readFileSync(taskMdPath, "utf-8");
2437
+ const taskMdPath = path14.join(taskDir, "task.md");
2438
+ if (fs16.existsSync(taskMdPath)) {
2439
+ const taskMd = fs16.readFileSync(taskMdPath, "utf-8");
2243
2440
  context += `
2244
2441
  ## Task Description
2245
2442
  ${taskMd}
2246
2443
  `;
2247
2444
  }
2248
- const taskJsonPath = path13.join(taskDir, "task.json");
2249
- if (fs15.existsSync(taskJsonPath)) {
2445
+ const taskJsonPath = path14.join(taskDir, "task.json");
2446
+ if (fs16.existsSync(taskJsonPath)) {
2250
2447
  try {
2251
- const taskDef = JSON.parse(fs15.readFileSync(taskJsonPath, "utf-8"));
2448
+ const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
2252
2449
  context += `
2253
2450
  ## Task Classification
2254
2451
  `;
@@ -2261,27 +2458,27 @@ ${taskMd}
2261
2458
  } catch {
2262
2459
  }
2263
2460
  }
2264
- const specPath = path13.join(taskDir, "spec.md");
2265
- if (fs15.existsSync(specPath)) {
2266
- const spec = fs15.readFileSync(specPath, "utf-8");
2461
+ const specPath = path14.join(taskDir, "spec.md");
2462
+ if (fs16.existsSync(specPath)) {
2463
+ const spec = fs16.readFileSync(specPath, "utf-8");
2267
2464
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
2268
2465
  context += `
2269
2466
  ## Spec Summary
2270
2467
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
2271
2468
  `;
2272
2469
  }
2273
- const planPath = path13.join(taskDir, "plan.md");
2274
- if (fs15.existsSync(planPath)) {
2275
- const plan = fs15.readFileSync(planPath, "utf-8");
2470
+ const planPath = path14.join(taskDir, "plan.md");
2471
+ if (fs16.existsSync(planPath)) {
2472
+ const plan = fs16.readFileSync(planPath, "utf-8");
2276
2473
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
2277
2474
  context += `
2278
2475
  ## Plan Summary
2279
2476
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
2280
2477
  `;
2281
2478
  }
2282
- const contextMdPath = path13.join(taskDir, "context.md");
2283
- if (fs15.existsSync(contextMdPath)) {
2284
- const accumulated = fs15.readFileSync(contextMdPath, "utf-8");
2479
+ const contextMdPath = path14.join(taskDir, "context.md");
2480
+ if (fs16.existsSync(contextMdPath)) {
2481
+ const accumulated = fs16.readFileSync(contextMdPath, "utf-8");
2285
2482
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
2286
2483
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
2287
2484
  context += `
@@ -2299,17 +2496,17 @@ ${feedback}
2299
2496
  }
2300
2497
  function inferHasUIFromScope(scope) {
2301
2498
  return scope.some((filePath) => {
2302
- const ext = path13.extname(filePath).toLowerCase();
2499
+ const ext = path14.extname(filePath).toLowerCase();
2303
2500
  if (UI_EXTENSIONS.has(ext)) return true;
2304
2501
  const normalized = filePath.replace(/\\/g, "/");
2305
2502
  return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
2306
2503
  });
2307
2504
  }
2308
2505
  function taskHasUI(taskDir) {
2309
- const taskJsonPath = path13.join(taskDir, "task.json");
2310
- if (!fs15.existsSync(taskJsonPath)) return true;
2506
+ const taskJsonPath = path14.join(taskDir, "task.json");
2507
+ if (!fs16.existsSync(taskJsonPath)) return true;
2311
2508
  try {
2312
- const taskDef = JSON.parse(fs15.readFileSync(taskJsonPath, "utf-8"));
2509
+ const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
2313
2510
  const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
2314
2511
  if (scope.length === 0) return true;
2315
2512
  return inferHasUIFromScope(scope);
@@ -2431,9 +2628,9 @@ ${prompt}` : prompt;
2431
2628
  }
2432
2629
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
2433
2630
  assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
2434
- const qaGuidePath = path13.join(projectDir, ".kody", "qa-guide.md");
2435
- if (fs15.existsSync(qaGuidePath)) {
2436
- const qaGuide = fs15.readFileSync(qaGuidePath, "utf-8").trim();
2631
+ const qaGuidePath = path14.join(projectDir, ".kody", "qa-guide.md");
2632
+ if (fs16.existsSync(qaGuidePath)) {
2633
+ const qaGuide = fs16.readFileSync(qaGuidePath, "utf-8").trim();
2437
2634
  assembled = assembled + "\n\n" + qaGuide;
2438
2635
  }
2439
2636
  }
@@ -2528,8 +2725,8 @@ var init_runner_selection = __esm({
2528
2725
  });
2529
2726
 
2530
2727
  // src/stages/agent.ts
2531
- import * as fs16 from "fs";
2532
- import * as path14 from "path";
2728
+ import * as fs17 from "fs";
2729
+ import * as path15 from "path";
2533
2730
  function getSessionInfo(stageName, sessions) {
2534
2731
  const group = SESSION_GROUP[stageName];
2535
2732
  if (!group) return void 0;
@@ -2616,27 +2813,27 @@ async function executeAgentStage(ctx, def) {
2616
2813
  }
2617
2814
  const result = lastResult;
2618
2815
  if (def.outputFile && result.output) {
2619
- fs16.writeFileSync(path14.join(ctx.taskDir, def.outputFile), result.output);
2816
+ fs17.writeFileSync(path15.join(ctx.taskDir, def.outputFile), result.output);
2620
2817
  }
2621
2818
  if (def.outputFile) {
2622
- const outputPath = path14.join(ctx.taskDir, def.outputFile);
2623
- if (!fs16.existsSync(outputPath)) {
2624
- const ext = path14.extname(def.outputFile);
2625
- const base = path14.basename(def.outputFile, ext);
2626
- const files = fs16.readdirSync(ctx.taskDir);
2819
+ const outputPath = path15.join(ctx.taskDir, def.outputFile);
2820
+ if (!fs17.existsSync(outputPath)) {
2821
+ const ext = path15.extname(def.outputFile);
2822
+ const base = path15.basename(def.outputFile, ext);
2823
+ const files = fs17.readdirSync(ctx.taskDir);
2627
2824
  const variant = files.find(
2628
2825
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
2629
2826
  );
2630
2827
  if (variant) {
2631
- fs16.renameSync(path14.join(ctx.taskDir, variant), outputPath);
2828
+ fs17.renameSync(path15.join(ctx.taskDir, variant), outputPath);
2632
2829
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
2633
2830
  }
2634
2831
  }
2635
2832
  }
2636
2833
  if (def.outputFile) {
2637
- const outputPath = path14.join(ctx.taskDir, def.outputFile);
2638
- if (fs16.existsSync(outputPath)) {
2639
- const content = fs16.readFileSync(outputPath, "utf-8");
2834
+ const outputPath = path15.join(ctx.taskDir, def.outputFile);
2835
+ if (fs17.existsSync(outputPath)) {
2836
+ const content = fs17.readFileSync(outputPath, "utf-8");
2640
2837
  const validation = validateStageOutput(def.name, content);
2641
2838
  if (!validation.valid) {
2642
2839
  if (def.name === "taskify") {
@@ -2650,7 +2847,7 @@ async function executeAgentStage(ctx, def) {
2650
2847
  const stripped = stripFences(retryResult.output);
2651
2848
  const retryValidation = validateTaskJson(stripped);
2652
2849
  if (retryValidation.valid) {
2653
- fs16.writeFileSync(outputPath, retryResult.output);
2850
+ fs17.writeFileSync(outputPath, retryResult.output);
2654
2851
  logger.info(` taskify retry produced valid JSON`);
2655
2852
  } else {
2656
2853
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -2663,7 +2860,7 @@ async function executeAgentStage(ctx, def) {
2663
2860
  risk_level: "low",
2664
2861
  questions: []
2665
2862
  }, null, 2);
2666
- fs16.writeFileSync(outputPath, fallback);
2863
+ fs17.writeFileSync(outputPath, fallback);
2667
2864
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
2668
2865
  }
2669
2866
  }
@@ -2677,7 +2874,7 @@ async function executeAgentStage(ctx, def) {
2677
2874
  return { outcome: "completed", outputFile: def.outputFile, retries };
2678
2875
  }
2679
2876
  function appendStageContext(taskDir, stageName, output) {
2680
- const contextPath = path14.join(taskDir, "context.md");
2877
+ const contextPath = path15.join(taskDir, "context.md");
2681
2878
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
2682
2879
  let summary;
2683
2880
  if (output && output.trim()) {
@@ -2690,7 +2887,7 @@ function appendStageContext(taskDir, stageName, output) {
2690
2887
  ### ${stageName} (${timestamp2})
2691
2888
  ${summary}
2692
2889
  `;
2693
- fs16.appendFileSync(contextPath, entry);
2890
+ fs17.appendFileSync(contextPath, entry);
2694
2891
  }
2695
2892
  var SESSION_GROUP;
2696
2893
  var init_agent = __esm({
@@ -2713,7 +2910,7 @@ var init_agent = __esm({
2713
2910
  });
2714
2911
 
2715
2912
  // src/verify-runner.ts
2716
- import { execFileSync as execFileSync10 } from "child_process";
2913
+ import { execFileSync as execFileSync11 } from "child_process";
2717
2914
  function isExecError(err) {
2718
2915
  return typeof err === "object" && err !== null;
2719
2916
  }
@@ -2749,7 +2946,7 @@ function runCommand(cmd, cwd, timeout) {
2749
2946
  return { success: true, output: "", timedOut: false };
2750
2947
  }
2751
2948
  try {
2752
- const output = execFileSync10(parts[0], parts.slice(1), {
2949
+ const output = execFileSync11(parts[0], parts.slice(1), {
2753
2950
  cwd,
2754
2951
  timeout,
2755
2952
  encoding: "utf-8",
@@ -2820,7 +3017,7 @@ var init_verify_runner = __esm({
2820
3017
  });
2821
3018
 
2822
3019
  // src/observer.ts
2823
- import { execFileSync as execFileSync11 } from "child_process";
3020
+ import { execFileSync as execFileSync12 } from "child_process";
2824
3021
  async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
2825
3022
  const context = [
2826
3023
  `Stage: ${stageName}`,
@@ -2880,13 +3077,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
2880
3077
  }
2881
3078
  function getModifiedFiles(projectDir) {
2882
3079
  try {
2883
- const staged = execFileSync11("git", ["diff", "--name-only", "--cached"], {
3080
+ const staged = execFileSync12("git", ["diff", "--name-only", "--cached"], {
2884
3081
  encoding: "utf-8",
2885
3082
  cwd: projectDir,
2886
3083
  timeout: 5e3,
2887
3084
  stdio: ["pipe", "pipe", "pipe"]
2888
3085
  }).trim();
2889
- const unstaged = execFileSync11("git", ["diff", "--name-only"], {
3086
+ const unstaged = execFileSync12("git", ["diff", "--name-only"], {
2890
3087
  encoding: "utf-8",
2891
3088
  cwd: projectDir,
2892
3089
  timeout: 5e3,
@@ -2929,8 +3126,8 @@ Error context:
2929
3126
  });
2930
3127
 
2931
3128
  // src/stages/gate.ts
2932
- import * as fs17 from "fs";
2933
- import * as path15 from "path";
3129
+ import * as fs18 from "fs";
3130
+ import * as path16 from "path";
2934
3131
  function executeGateStage(ctx, def) {
2935
3132
  if (ctx.input.dryRun) {
2936
3133
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -2973,7 +3170,7 @@ ${output}
2973
3170
  `);
2974
3171
  }
2975
3172
  }
2976
- fs17.writeFileSync(path15.join(ctx.taskDir, "verify.md"), lines.join(""));
3173
+ fs18.writeFileSync(path16.join(ctx.taskDir, "verify.md"), lines.join(""));
2977
3174
  return {
2978
3175
  outcome: verifyResult.pass ? "completed" : "failed",
2979
3176
  retries: 0
@@ -2988,9 +3185,9 @@ var init_gate = __esm({
2988
3185
  });
2989
3186
 
2990
3187
  // src/stages/verify.ts
2991
- import * as fs18 from "fs";
2992
- import * as path16 from "path";
2993
- import { execFileSync as execFileSync12 } from "child_process";
3188
+ import * as fs19 from "fs";
3189
+ import * as path17 from "path";
3190
+ import { execFileSync as execFileSync13 } from "child_process";
2994
3191
  async function executeVerifyWithAutofix(ctx, def) {
2995
3192
  const maxAttempts = def.maxRetries ?? 2;
2996
3193
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
@@ -3000,8 +3197,8 @@ async function executeVerifyWithAutofix(ctx, def) {
3000
3197
  return { ...gateResult, retries: attempt };
3001
3198
  }
3002
3199
  if (attempt < maxAttempts) {
3003
- const verifyPath = path16.join(ctx.taskDir, "verify.md");
3004
- const errorOutput = fs18.existsSync(verifyPath) ? fs18.readFileSync(verifyPath, "utf-8") : "Unknown error";
3200
+ const verifyPath = path17.join(ctx.taskDir, "verify.md");
3201
+ const errorOutput = fs19.existsSync(verifyPath) ? fs19.readFileSync(verifyPath, "utf-8") : "Unknown error";
3005
3202
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
3006
3203
  const defaultRunner = getRunnerForStage(ctx, "taskify");
3007
3204
  const diagConfig = getProjectConfig();
@@ -3044,7 +3241,7 @@ ${diagnosis.resolution}`);
3044
3241
  const parts = parseCommand(cmd);
3045
3242
  if (parts.length === 0) return;
3046
3243
  try {
3047
- execFileSync12(parts[0], parts.slice(1), {
3244
+ execFileSync13(parts[0], parts.slice(1), {
3048
3245
  stdio: "pipe",
3049
3246
  timeout: FIX_COMMAND_TIMEOUT_MS
3050
3247
  });
@@ -3097,8 +3294,8 @@ var init_verify = __esm({
3097
3294
  });
3098
3295
 
3099
3296
  // src/review-standalone.ts
3100
- import * as fs19 from "fs";
3101
- import * as path17 from "path";
3297
+ import * as fs20 from "fs";
3298
+ import * as path18 from "path";
3102
3299
  function resolveReviewTarget(input) {
3103
3300
  if (input.prs.length === 0) {
3104
3301
  return {
@@ -3122,8 +3319,8 @@ Or comment on the specific PR: \`@kody review\``
3122
3319
  }
3123
3320
  async function runStandaloneReview(input) {
3124
3321
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
3125
- const taskDir = path17.join(input.projectDir, ".kody", "tasks", taskId);
3126
- fs19.mkdirSync(taskDir, { recursive: true });
3322
+ const taskDir = path18.join(input.projectDir, ".kody", "tasks", taskId);
3323
+ fs20.mkdirSync(taskDir, { recursive: true });
3127
3324
  let diffInstruction = "";
3128
3325
  let filesChangedSection = "";
3129
3326
  if (input.baseBranch) {
@@ -3150,7 +3347,7 @@ ${fileList}`;
3150
3347
  const taskContent = `# ${input.prTitle}
3151
3348
 
3152
3349
  ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
3153
- fs19.writeFileSync(path17.join(taskDir, "task.md"), taskContent);
3350
+ fs20.writeFileSync(path18.join(taskDir, "task.md"), taskContent);
3154
3351
  const reviewDef = STAGES.find((s) => s.name === "review");
3155
3352
  const ctx = {
3156
3353
  taskId,
@@ -3172,10 +3369,10 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
3172
3369
  error: result.error ?? "Review stage failed"
3173
3370
  };
3174
3371
  }
3175
- const reviewPath = path17.join(taskDir, "review.md");
3372
+ const reviewPath = path18.join(taskDir, "review.md");
3176
3373
  let reviewContent;
3177
- if (fs19.existsSync(reviewPath)) {
3178
- reviewContent = fs19.readFileSync(reviewPath, "utf-8");
3374
+ if (fs20.existsSync(reviewPath)) {
3375
+ reviewContent = fs20.readFileSync(reviewPath, "utf-8");
3179
3376
  }
3180
3377
  return {
3181
3378
  outcome: "completed",
@@ -3215,8 +3412,8 @@ var init_review_standalone = __esm({
3215
3412
  });
3216
3413
 
3217
3414
  // src/stages/review.ts
3218
- import * as fs20 from "fs";
3219
- import * as path18 from "path";
3415
+ import * as fs21 from "fs";
3416
+ import * as path19 from "path";
3220
3417
  async function executeReviewWithFix(ctx, def) {
3221
3418
  if (ctx.input.dryRun) {
3222
3419
  return { outcome: "completed", retries: 0 };
@@ -3230,11 +3427,11 @@ async function executeReviewWithFix(ctx, def) {
3230
3427
  if (reviewResult.outcome !== "completed") {
3231
3428
  return reviewResult;
3232
3429
  }
3233
- const reviewFile = path18.join(ctx.taskDir, "review.md");
3234
- if (!fs20.existsSync(reviewFile)) {
3430
+ const reviewFile = path19.join(ctx.taskDir, "review.md");
3431
+ if (!fs21.existsSync(reviewFile)) {
3235
3432
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
3236
3433
  }
3237
- const content = fs20.readFileSync(reviewFile, "utf-8");
3434
+ const content = fs21.readFileSync(reviewFile, "utf-8");
3238
3435
  if (detectReviewVerdict(content) !== "fail") {
3239
3436
  return { ...reviewResult, retries: iteration };
3240
3437
  }
@@ -3263,15 +3460,15 @@ var init_review = __esm({
3263
3460
  });
3264
3461
 
3265
3462
  // src/stages/ship.ts
3266
- import * as fs21 from "fs";
3267
- import * as path19 from "path";
3268
- import { execFileSync as execFileSync13 } from "child_process";
3463
+ import * as fs22 from "fs";
3464
+ import * as path20 from "path";
3465
+ import { execFileSync as execFileSync14 } from "child_process";
3269
3466
  function buildPrBody(ctx) {
3270
3467
  const sections = [];
3271
- const taskJsonPath = path19.join(ctx.taskDir, "task.json");
3272
- if (fs21.existsSync(taskJsonPath)) {
3468
+ const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3469
+ if (fs22.existsSync(taskJsonPath)) {
3273
3470
  try {
3274
- const raw = fs21.readFileSync(taskJsonPath, "utf-8");
3471
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
3275
3472
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3276
3473
  const task = JSON.parse(cleaned);
3277
3474
  if (task.description) {
@@ -3290,9 +3487,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
3290
3487
  } catch {
3291
3488
  }
3292
3489
  }
3293
- const reviewPath = path19.join(ctx.taskDir, "review.md");
3294
- if (fs21.existsSync(reviewPath)) {
3295
- const review = fs21.readFileSync(reviewPath, "utf-8");
3490
+ const reviewPath = path20.join(ctx.taskDir, "review.md");
3491
+ if (fs22.existsSync(reviewPath)) {
3492
+ const review = fs22.readFileSync(reviewPath, "utf-8");
3296
3493
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
3297
3494
  if (summaryMatch) {
3298
3495
  const summary = summaryMatch[1].trim();
@@ -3309,14 +3506,14 @@ ${summary}`);
3309
3506
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
3310
3507
  }
3311
3508
  }
3312
- const verifyPath = path19.join(ctx.taskDir, "verify.md");
3313
- if (fs21.existsSync(verifyPath)) {
3314
- const verify = fs21.readFileSync(verifyPath, "utf-8");
3509
+ const verifyPath = path20.join(ctx.taskDir, "verify.md");
3510
+ if (fs22.existsSync(verifyPath)) {
3511
+ const verify = fs22.readFileSync(verifyPath, "utf-8");
3315
3512
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
3316
3513
  }
3317
- const planPath = path19.join(ctx.taskDir, "plan.md");
3318
- if (fs21.existsSync(planPath)) {
3319
- const plan = fs21.readFileSync(planPath, "utf-8").trim();
3514
+ const planPath = path20.join(ctx.taskDir, "plan.md");
3515
+ if (fs22.existsSync(planPath)) {
3516
+ const plan = fs22.readFileSync(planPath, "utf-8").trim();
3320
3517
  if (plan) {
3321
3518
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
3322
3519
  sections.push(`
@@ -3336,25 +3533,25 @@ Closes #${ctx.input.issueNumber}`);
3336
3533
  return sections.join("\n");
3337
3534
  }
3338
3535
  function executeShipStage(ctx, _def) {
3339
- const shipPath = path19.join(ctx.taskDir, "ship.md");
3536
+ const shipPath = path20.join(ctx.taskDir, "ship.md");
3340
3537
  if (ctx.input.dryRun) {
3341
- fs21.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
3538
+ fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
3342
3539
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3343
3540
  }
3344
3541
  if (ctx.input.local && !ctx.input.issueNumber) {
3345
- fs21.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
3542
+ fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
3346
3543
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3347
3544
  }
3348
3545
  try {
3349
3546
  const head = getCurrentBranch(ctx.projectDir);
3350
3547
  const base = getDefaultBranch(ctx.projectDir);
3351
3548
  try {
3352
- execFileSync13("git", ["add", ctx.taskDir], {
3549
+ execFileSync14("git", ["add", ctx.taskDir], {
3353
3550
  cwd: ctx.projectDir,
3354
3551
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
3355
3552
  stdio: "pipe"
3356
3553
  });
3357
- execFileSync13("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
3554
+ execFileSync14("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
3358
3555
  cwd: ctx.projectDir,
3359
3556
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
3360
3557
  stdio: "pipe"
@@ -3368,7 +3565,7 @@ function executeShipStage(ctx, _def) {
3368
3565
  let repo = config.github?.repo;
3369
3566
  if (!owner || !repo) {
3370
3567
  try {
3371
- const remoteUrl = execFileSync13("git", ["remote", "get-url", "origin"], {
3568
+ const remoteUrl = execFileSync14("git", ["remote", "get-url", "origin"], {
3372
3569
  encoding: "utf-8",
3373
3570
  cwd: ctx.projectDir
3374
3571
  }).trim();
@@ -3389,28 +3586,28 @@ function executeShipStage(ctx, _def) {
3389
3586
  chore: "chore"
3390
3587
  };
3391
3588
  let prefix = "chore";
3392
- const taskJsonPath = path19.join(ctx.taskDir, "task.json");
3393
- if (fs21.existsSync(taskJsonPath)) {
3589
+ const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3590
+ if (fs22.existsSync(taskJsonPath)) {
3394
3591
  try {
3395
- const raw = fs21.readFileSync(taskJsonPath, "utf-8");
3592
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
3396
3593
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3397
3594
  const task = JSON.parse(cleaned);
3398
3595
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
3399
3596
  } catch {
3400
3597
  }
3401
3598
  }
3402
- const taskMdPath = path19.join(ctx.taskDir, "task.md");
3403
- if (fs21.existsSync(taskMdPath)) {
3404
- const content = fs21.readFileSync(taskMdPath, "utf-8");
3599
+ const taskMdPath = path20.join(ctx.taskDir, "task.md");
3600
+ if (fs22.existsSync(taskMdPath)) {
3601
+ const content = fs22.readFileSync(taskMdPath, "utf-8");
3405
3602
  const heading = content.split("\n").find((l) => l.startsWith("# "));
3406
3603
  if (heading) {
3407
3604
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
3408
3605
  }
3409
3606
  }
3410
3607
  if (title === "Update") {
3411
- if (fs21.existsSync(taskJsonPath)) {
3608
+ if (fs22.existsSync(taskJsonPath)) {
3412
3609
  try {
3413
- const raw = fs21.readFileSync(taskJsonPath, "utf-8");
3610
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
3414
3611
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3415
3612
  const task = JSON.parse(cleaned);
3416
3613
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -3433,7 +3630,7 @@ function executeShipStage(ctx, _def) {
3433
3630
  } catch {
3434
3631
  }
3435
3632
  }
3436
- fs21.writeFileSync(shipPath, `# Ship
3633
+ fs22.writeFileSync(shipPath, `# Ship
3437
3634
 
3438
3635
  Updated existing PR: ${existingPr.url}
3439
3636
  PR #${existingPr.number}
@@ -3454,20 +3651,20 @@ PR #${existingPr.number}
3454
3651
  } catch {
3455
3652
  }
3456
3653
  }
3457
- fs21.writeFileSync(shipPath, `# Ship
3654
+ fs22.writeFileSync(shipPath, `# Ship
3458
3655
 
3459
3656
  PR created: ${pr.url}
3460
3657
  PR #${pr.number}
3461
3658
  `);
3462
3659
  } else {
3463
- fs21.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3660
+ fs22.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3464
3661
  }
3465
3662
  }
3466
3663
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3467
3664
  } catch (err) {
3468
3665
  const msg = err instanceof Error ? err.message : String(err);
3469
3666
  try {
3470
- fs21.writeFileSync(shipPath, `# Ship
3667
+ fs22.writeFileSync(shipPath, `# Ship
3471
3668
 
3472
3669
  Failed: ${msg}
3473
3670
  `);
@@ -3516,15 +3713,15 @@ var init_executor_registry = __esm({
3516
3713
  });
3517
3714
 
3518
3715
  // src/pipeline/questions.ts
3519
- import * as fs22 from "fs";
3520
- import * as path20 from "path";
3716
+ import * as fs23 from "fs";
3717
+ import * as path21 from "path";
3521
3718
  function checkForQuestions(ctx, stageName) {
3522
3719
  if (ctx.input.local || !ctx.input.issueNumber) return false;
3523
3720
  try {
3524
3721
  if (stageName === "taskify") {
3525
- const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3526
- if (!fs22.existsSync(taskJsonPath)) return false;
3527
- const raw = fs22.readFileSync(taskJsonPath, "utf-8");
3722
+ const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3723
+ if (!fs23.existsSync(taskJsonPath)) return false;
3724
+ const raw = fs23.readFileSync(taskJsonPath, "utf-8");
3528
3725
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3529
3726
  const taskJson = JSON.parse(cleaned);
3530
3727
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -3539,9 +3736,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
3539
3736
  }
3540
3737
  }
3541
3738
  if (stageName === "plan") {
3542
- const planPath = path20.join(ctx.taskDir, "plan.md");
3543
- if (!fs22.existsSync(planPath)) return false;
3544
- const plan = fs22.readFileSync(planPath, "utf-8");
3739
+ const planPath = path21.join(ctx.taskDir, "plan.md");
3740
+ if (!fs23.existsSync(planPath)) return false;
3741
+ const plan = fs23.readFileSync(planPath, "utf-8");
3545
3742
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
3546
3743
  if (questionsMatch) {
3547
3744
  const questionsText = questionsMatch[1].trim();
@@ -3570,8 +3767,8 @@ var init_questions = __esm({
3570
3767
  });
3571
3768
 
3572
3769
  // src/pipeline/hooks.ts
3573
- import * as fs23 from "fs";
3574
- import * as path21 from "path";
3770
+ import * as fs24 from "fs";
3771
+ import * as path22 from "path";
3575
3772
  function applyPreStageLabel(ctx, def) {
3576
3773
  if (!ctx.input.issueNumber || ctx.input.local) return;
3577
3774
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
@@ -3609,9 +3806,9 @@ function autoDetectComplexity(ctx, def) {
3609
3806
  return { complexity, activeStages };
3610
3807
  }
3611
3808
  try {
3612
- const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3613
- if (!fs23.existsSync(taskJsonPath)) return null;
3614
- const raw = fs23.readFileSync(taskJsonPath, "utf-8");
3809
+ const taskJsonPath = path22.join(ctx.taskDir, "task.json");
3810
+ if (!fs24.existsSync(taskJsonPath)) return null;
3811
+ const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3615
3812
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3616
3813
  const taskJson = JSON.parse(cleaned);
3617
3814
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -3641,8 +3838,8 @@ function checkRiskGate(ctx, def, state, complexity) {
3641
3838
  if (ctx.input.dryRun || ctx.input.local) return null;
3642
3839
  if (ctx.input.mode === "rerun") return null;
3643
3840
  if (!ctx.input.issueNumber) return null;
3644
- const planPath = path21.join(ctx.taskDir, "plan.md");
3645
- const plan = fs23.existsSync(planPath) ? fs23.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
3841
+ const planPath = path22.join(ctx.taskDir, "plan.md");
3842
+ const plan = fs24.existsSync(planPath) ? fs24.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
3646
3843
  try {
3647
3844
  postComment(
3648
3845
  ctx.input.issueNumber,
@@ -3709,22 +3906,22 @@ var init_hooks = __esm({
3709
3906
  });
3710
3907
 
3711
3908
  // src/learning/auto-learn.ts
3712
- import * as fs24 from "fs";
3713
- import * as path22 from "path";
3909
+ import * as fs25 from "fs";
3910
+ import * as path23 from "path";
3714
3911
  function stripAnsi(str) {
3715
3912
  return str.replace(/\x1b\[[0-9;]*m/g, "");
3716
3913
  }
3717
3914
  function autoLearn(ctx) {
3718
3915
  try {
3719
- const memoryDir = path22.join(ctx.projectDir, ".kody", "memory");
3720
- if (!fs24.existsSync(memoryDir)) {
3721
- fs24.mkdirSync(memoryDir, { recursive: true });
3916
+ const memoryDir = path23.join(ctx.projectDir, ".kody", "memory");
3917
+ if (!fs25.existsSync(memoryDir)) {
3918
+ fs25.mkdirSync(memoryDir, { recursive: true });
3722
3919
  }
3723
3920
  const learnings = [];
3724
3921
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3725
- const verifyPath = path22.join(ctx.taskDir, "verify.md");
3726
- if (fs24.existsSync(verifyPath)) {
3727
- const verify = stripAnsi(fs24.readFileSync(verifyPath, "utf-8"));
3922
+ const verifyPath = path23.join(ctx.taskDir, "verify.md");
3923
+ if (fs25.existsSync(verifyPath)) {
3924
+ const verify = stripAnsi(fs25.readFileSync(verifyPath, "utf-8"));
3728
3925
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
3729
3926
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
3730
3927
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -3733,18 +3930,18 @@ function autoLearn(ctx) {
3733
3930
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
3734
3931
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
3735
3932
  }
3736
- const reviewPath = path22.join(ctx.taskDir, "review.md");
3737
- if (fs24.existsSync(reviewPath)) {
3738
- const review = fs24.readFileSync(reviewPath, "utf-8");
3933
+ const reviewPath = path23.join(ctx.taskDir, "review.md");
3934
+ if (fs25.existsSync(reviewPath)) {
3935
+ const review = fs25.readFileSync(reviewPath, "utf-8");
3739
3936
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
3740
3937
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
3741
3938
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
3742
3939
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
3743
3940
  }
3744
- const taskJsonPath = path22.join(ctx.taskDir, "task.json");
3745
- if (fs24.existsSync(taskJsonPath)) {
3941
+ const taskJsonPath = path23.join(ctx.taskDir, "task.json");
3942
+ if (fs25.existsSync(taskJsonPath)) {
3746
3943
  try {
3747
- const raw = stripAnsi(fs24.readFileSync(taskJsonPath, "utf-8"));
3944
+ const raw = stripAnsi(fs25.readFileSync(taskJsonPath, "utf-8"));
3748
3945
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3749
3946
  const task = JSON.parse(cleaned);
3750
3947
  if (task.scope && Array.isArray(task.scope)) {
@@ -3755,12 +3952,12 @@ function autoLearn(ctx) {
3755
3952
  }
3756
3953
  }
3757
3954
  if (learnings.length > 0) {
3758
- const conventionsPath = path22.join(memoryDir, "conventions.md");
3955
+ const conventionsPath = path23.join(memoryDir, "conventions.md");
3759
3956
  const entry = `
3760
3957
  ## Learned ${timestamp2} (task: ${ctx.taskId})
3761
3958
  ${learnings.join("\n")}
3762
3959
  `;
3763
- fs24.appendFileSync(conventionsPath, entry);
3960
+ fs25.appendFileSync(conventionsPath, entry);
3764
3961
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
3765
3962
  }
3766
3963
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
@@ -3768,8 +3965,8 @@ ${learnings.join("\n")}
3768
3965
  }
3769
3966
  }
3770
3967
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3771
- const archPath = path22.join(memoryDir, "architecture.md");
3772
- if (fs24.existsSync(archPath)) return;
3968
+ const archPath = path23.join(memoryDir, "architecture.md");
3969
+ if (fs25.existsSync(archPath)) return;
3773
3970
  const detected = detectArchitectureBasic(projectDir);
3774
3971
  if (detected.length > 0) {
3775
3972
  const content = `# Architecture (auto-detected ${timestamp2})
@@ -3777,7 +3974,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3777
3974
  ## Overview
3778
3975
  ${detected.join("\n")}
3779
3976
  `;
3780
- fs24.writeFileSync(archPath, content);
3977
+ fs25.writeFileSync(archPath, content);
3781
3978
  logger.info(`Auto-detected architecture (${detected.length} items)`);
3782
3979
  }
3783
3980
  }
@@ -3790,13 +3987,13 @@ var init_auto_learn = __esm({
3790
3987
  });
3791
3988
 
3792
3989
  // src/retrospective.ts
3793
- import * as fs25 from "fs";
3794
- import * as path23 from "path";
3990
+ import * as fs26 from "fs";
3991
+ import * as path24 from "path";
3795
3992
  function readArtifact(taskDir, filename, maxChars) {
3796
- const p = path23.join(taskDir, filename);
3797
- if (!fs25.existsSync(p)) return null;
3993
+ const p = path24.join(taskDir, filename);
3994
+ if (!fs26.existsSync(p)) return null;
3798
3995
  try {
3799
- const content = fs25.readFileSync(p, "utf-8");
3996
+ const content = fs26.readFileSync(p, "utf-8");
3800
3997
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
3801
3998
  } catch {
3802
3999
  return null;
@@ -3849,13 +4046,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
3849
4046
  return lines.join("\n");
3850
4047
  }
3851
4048
  function getLogPath(projectDir) {
3852
- return path23.join(projectDir, ".kody", "memory", "observer-log.jsonl");
4049
+ return path24.join(projectDir, ".kody", "memory", "observer-log.jsonl");
3853
4050
  }
3854
4051
  function readPreviousRetrospectives(projectDir, limit = 10) {
3855
4052
  const logPath = getLogPath(projectDir);
3856
- if (!fs25.existsSync(logPath)) return [];
4053
+ if (!fs26.existsSync(logPath)) return [];
3857
4054
  try {
3858
- const content = fs25.readFileSync(logPath, "utf-8");
4055
+ const content = fs26.readFileSync(logPath, "utf-8");
3859
4056
  const lines = content.split("\n").filter(Boolean);
3860
4057
  const entries = [];
3861
4058
  const start = Math.max(0, lines.length - limit);
@@ -3882,11 +4079,11 @@ function formatPreviousEntries(entries) {
3882
4079
  }
3883
4080
  function appendRetrospectiveEntry(projectDir, entry) {
3884
4081
  const logPath = getLogPath(projectDir);
3885
- const dir = path23.dirname(logPath);
3886
- if (!fs25.existsSync(dir)) {
3887
- fs25.mkdirSync(dir, { recursive: true });
4082
+ const dir = path24.dirname(logPath);
4083
+ if (!fs26.existsSync(dir)) {
4084
+ fs26.mkdirSync(dir, { recursive: true });
3888
4085
  }
3889
- fs25.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4086
+ fs26.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3890
4087
  }
3891
4088
  async function runRetrospective(ctx, state, pipelineStartTime) {
3892
4089
  if (ctx.input.dryRun) return;
@@ -4054,8 +4251,8 @@ var init_summary = __esm({
4054
4251
  });
4055
4252
 
4056
4253
  // src/pipeline.ts
4057
- import * as fs26 from "fs";
4058
- import * as path24 from "path";
4254
+ import * as fs27 from "fs";
4255
+ import * as path25 from "path";
4059
4256
  function ensureFeatureBranchIfNeeded(ctx) {
4060
4257
  if (ctx.input.dryRun) return;
4061
4258
  if (ctx.input.prNumber) {
@@ -4068,8 +4265,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
4068
4265
  }
4069
4266
  if (!ctx.input.issueNumber) return;
4070
4267
  try {
4071
- const taskMdPath = path24.join(ctx.taskDir, "task.md");
4072
- const title = fs26.existsSync(taskMdPath) ? fs26.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
4268
+ const taskMdPath = path25.join(ctx.taskDir, "task.md");
4269
+ const title = fs27.existsSync(taskMdPath) ? fs27.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
4073
4270
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
4074
4271
  syncWithDefault(ctx.projectDir);
4075
4272
  } catch (err) {
@@ -4083,10 +4280,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
4083
4280
  }
4084
4281
  }
4085
4282
  function acquireLock(taskDir) {
4086
- const lockPath = path24.join(taskDir, ".lock");
4087
- if (fs26.existsSync(lockPath)) {
4283
+ const lockPath = path25.join(taskDir, ".lock");
4284
+ if (fs27.existsSync(lockPath)) {
4088
4285
  try {
4089
- const pid = parseInt(fs26.readFileSync(lockPath, "utf-8").trim(), 10);
4286
+ const pid = parseInt(fs27.readFileSync(lockPath, "utf-8").trim(), 10);
4090
4287
  if (!isNaN(pid)) {
4091
4288
  try {
4092
4289
  process.kill(pid, 0);
@@ -4103,14 +4300,14 @@ function acquireLock(taskDir) {
4103
4300
  logger.warn(` Corrupt lock file \u2014 overwriting`);
4104
4301
  }
4105
4302
  try {
4106
- fs26.unlinkSync(lockPath);
4303
+ fs27.unlinkSync(lockPath);
4107
4304
  } catch {
4108
4305
  }
4109
4306
  }
4110
4307
  try {
4111
- const fd = fs26.openSync(lockPath, fs26.constants.O_WRONLY | fs26.constants.O_CREAT | fs26.constants.O_EXCL);
4112
- fs26.writeSync(fd, String(process.pid));
4113
- fs26.closeSync(fd);
4308
+ const fd = fs27.openSync(lockPath, fs27.constants.O_WRONLY | fs27.constants.O_CREAT | fs27.constants.O_EXCL);
4309
+ fs27.writeSync(fd, String(process.pid));
4310
+ fs27.closeSync(fd);
4114
4311
  } catch (err) {
4115
4312
  if (err.code === "EEXIST") {
4116
4313
  throw new Error("Pipeline already running (lock acquired by another process)");
@@ -4120,7 +4317,7 @@ function acquireLock(taskDir) {
4120
4317
  }
4121
4318
  function releaseLock(taskDir) {
4122
4319
  try {
4123
- fs26.unlinkSync(path24.join(taskDir, ".lock"));
4320
+ fs27.unlinkSync(path25.join(taskDir, ".lock"));
4124
4321
  } catch {
4125
4322
  }
4126
4323
  }
@@ -4328,8 +4525,8 @@ var init_pipeline = __esm({
4328
4525
  });
4329
4526
 
4330
4527
  // src/preflight.ts
4331
- import { execFileSync as execFileSync14 } from "child_process";
4332
- import * as fs27 from "fs";
4528
+ import { execFileSync as execFileSync15 } from "child_process";
4529
+ import * as fs28 from "fs";
4333
4530
  function check(name, fn) {
4334
4531
  try {
4335
4532
  const detail = fn() ?? void 0;
@@ -4341,7 +4538,7 @@ function check(name, fn) {
4341
4538
  function runPreflight() {
4342
4539
  const checks = [
4343
4540
  check("claude CLI", () => {
4344
- const v = execFileSync14("claude", ["--version"], {
4541
+ const v = execFileSync15("claude", ["--version"], {
4345
4542
  encoding: "utf-8",
4346
4543
  timeout: 1e4,
4347
4544
  stdio: ["pipe", "pipe", "pipe"]
@@ -4349,14 +4546,14 @@ function runPreflight() {
4349
4546
  return v;
4350
4547
  }),
4351
4548
  check("git repo", () => {
4352
- execFileSync14("git", ["rev-parse", "--is-inside-work-tree"], {
4549
+ execFileSync15("git", ["rev-parse", "--is-inside-work-tree"], {
4353
4550
  encoding: "utf-8",
4354
4551
  timeout: 5e3,
4355
4552
  stdio: ["pipe", "pipe", "pipe"]
4356
4553
  });
4357
4554
  }),
4358
4555
  check("pnpm", () => {
4359
- const v = execFileSync14("pnpm", ["--version"], {
4556
+ const v = execFileSync15("pnpm", ["--version"], {
4360
4557
  encoding: "utf-8",
4361
4558
  timeout: 5e3,
4362
4559
  stdio: ["pipe", "pipe", "pipe"]
@@ -4364,7 +4561,7 @@ function runPreflight() {
4364
4561
  return v;
4365
4562
  }),
4366
4563
  check("node >= 18", () => {
4367
- const v = execFileSync14("node", ["--version"], {
4564
+ const v = execFileSync15("node", ["--version"], {
4368
4565
  encoding: "utf-8",
4369
4566
  timeout: 5e3,
4370
4567
  stdio: ["pipe", "pipe", "pipe"]
@@ -4374,7 +4571,7 @@ function runPreflight() {
4374
4571
  return v;
4375
4572
  }),
4376
4573
  check("gh CLI", () => {
4377
- const v = execFileSync14("gh", ["--version"], {
4574
+ const v = execFileSync15("gh", ["--version"], {
4378
4575
  encoding: "utf-8",
4379
4576
  timeout: 5e3,
4380
4577
  stdio: ["pipe", "pipe", "pipe"]
@@ -4382,7 +4579,7 @@ function runPreflight() {
4382
4579
  return v;
4383
4580
  }),
4384
4581
  check("package.json", () => {
4385
- if (!fs27.existsSync("package.json")) throw new Error("not found");
4582
+ if (!fs28.existsSync("package.json")) throw new Error("not found");
4386
4583
  })
4387
4584
  ];
4388
4585
  const failed = checks.filter((c) => !c.ok);
@@ -4458,180 +4655,6 @@ var init_args = __esm({
4458
4655
  }
4459
4656
  });
4460
4657
 
4461
- // src/cli/litellm.ts
4462
- import * as fs28 from "fs";
4463
- import * as os from "os";
4464
- import * as path25 from "path";
4465
- import { execFileSync as execFileSync15 } from "child_process";
4466
- async function checkLitellmHealth(url) {
4467
- try {
4468
- const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
4469
- return response.ok;
4470
- } catch {
4471
- return false;
4472
- }
4473
- }
4474
- async function checkModelHealth(baseUrl, apiKey, model) {
4475
- try {
4476
- const res = await fetch(`${baseUrl}/v1/messages`, {
4477
- method: "POST",
4478
- headers: {
4479
- "Content-Type": "application/json",
4480
- "x-api-key": apiKey,
4481
- "anthropic-version": "2023-06-01"
4482
- },
4483
- body: JSON.stringify({
4484
- model,
4485
- max_tokens: 4,
4486
- messages: [{ role: "user", content: "Reply with: ok" }]
4487
- }),
4488
- signal: AbortSignal.timeout(3e4)
4489
- });
4490
- if (!res.ok) {
4491
- const body2 = await res.text().catch(() => "");
4492
- return { ok: false, error: `HTTP ${res.status}: ${body2.slice(0, 200)}` };
4493
- }
4494
- const body = await res.json();
4495
- const hasAnthropicContent = Array.isArray(body.content) && body.content.some((b) => b.type === "text");
4496
- const hasThinkingContent = Array.isArray(body.content) && body.content.some((b) => b.type === "thinking");
4497
- const hasOpenAIContent = !!body.choices?.[0]?.message?.content;
4498
- if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent) {
4499
- return { ok: false, error: `Unexpected response format: ${JSON.stringify(body).slice(0, 200)}` };
4500
- }
4501
- return { ok: true };
4502
- } catch (err) {
4503
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
4504
- }
4505
- }
4506
- function generateLitellmConfig(provider, modelMap) {
4507
- const apiKeyVar = providerApiKeyEnvVar(provider);
4508
- const entries = ["model_list:"];
4509
- const seen = /* @__PURE__ */ new Set();
4510
- for (const providerModel of Object.values(modelMap)) {
4511
- if (seen.has(providerModel)) continue;
4512
- seen.add(providerModel);
4513
- entries.push(` - model_name: ${providerModel}`);
4514
- entries.push(` litellm_params:`);
4515
- entries.push(` model: ${provider}/${providerModel}`);
4516
- entries.push(` api_key: os.environ/${apiKeyVar}`);
4517
- }
4518
- return entries.join("\n") + "\n";
4519
- }
4520
- function generateLitellmConfigFromStages(defaultConfig, stages) {
4521
- const proxyModels = [];
4522
- if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
4523
- proxyModels.push(defaultConfig);
4524
- }
4525
- if (stages) {
4526
- for (const sc of Object.values(stages)) {
4527
- if (sc.provider !== "claude" && sc.provider !== "anthropic") {
4528
- proxyModels.push(sc);
4529
- }
4530
- }
4531
- }
4532
- if (proxyModels.length === 0) return void 0;
4533
- const entries = ["model_list:"];
4534
- const seen = /* @__PURE__ */ new Set();
4535
- for (const { provider, model } of proxyModels) {
4536
- const key = `${provider}/${model}`;
4537
- if (seen.has(key)) continue;
4538
- seen.add(key);
4539
- const apiKeyVar = providerApiKeyEnvVar(provider);
4540
- entries.push(` - model_name: ${model}`);
4541
- entries.push(` litellm_params:`);
4542
- entries.push(` model: ${provider}/${model}`);
4543
- entries.push(` api_key: os.environ/${apiKeyVar}`);
4544
- }
4545
- return entries.join("\n") + "\n";
4546
- }
4547
- async function tryStartLitellm(url, projectDir, generatedConfig) {
4548
- if (!generatedConfig) {
4549
- logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
4550
- return null;
4551
- }
4552
- const configPath = path25.join(os.tmpdir(), "kody-litellm-config.yaml");
4553
- fs28.writeFileSync(configPath, generatedConfig);
4554
- const portMatch = url.match(/:(\d+)/);
4555
- const port = portMatch ? portMatch[1] : "4000";
4556
- let litellmFound = false;
4557
- try {
4558
- execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
4559
- litellmFound = true;
4560
- } catch {
4561
- try {
4562
- execFileSync15("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
4563
- litellmFound = true;
4564
- } catch {
4565
- }
4566
- }
4567
- if (!litellmFound) {
4568
- logger.warn("litellm not installed (pip install 'litellm[proxy]')");
4569
- return null;
4570
- }
4571
- logger.info(`Starting LiteLLM proxy on port ${port}...`);
4572
- let cmd;
4573
- let args2;
4574
- try {
4575
- execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
4576
- cmd = "litellm";
4577
- args2 = ["--config", configPath, "--port", port];
4578
- } catch {
4579
- cmd = "python3";
4580
- args2 = ["-m", "litellm", "--config", configPath, "--port", port];
4581
- }
4582
- const dotenvPath = path25.join(projectDir, ".env");
4583
- const dotenvVars = {};
4584
- if (fs28.existsSync(dotenvPath)) {
4585
- for (const rawLine of fs28.readFileSync(dotenvPath, "utf-8").split("\n")) {
4586
- const line = rawLine.trim();
4587
- if (!line || line.startsWith("#")) continue;
4588
- const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
4589
- if (match) {
4590
- let value = match[2].trim();
4591
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4592
- value = value.slice(1, -1);
4593
- }
4594
- const commentIdx = value.indexOf(" #");
4595
- if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
4596
- if (value) dotenvVars[match[1]] = value;
4597
- }
4598
- }
4599
- if (Object.keys(dotenvVars).length > 0) {
4600
- logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
4601
- }
4602
- }
4603
- const { spawn: spawn2 } = await import("child_process");
4604
- const child = spawn2(cmd, args2, {
4605
- stdio: ["ignore", "pipe", "pipe"],
4606
- detached: true,
4607
- env: { ...process.env, ...dotenvVars }
4608
- });
4609
- let proxyStderr = "";
4610
- child.stderr?.on("data", (chunk) => {
4611
- proxyStderr += chunk.toString();
4612
- });
4613
- for (let i = 0; i < 30; i++) {
4614
- await new Promise((r) => setTimeout(r, 2e3));
4615
- if (await checkLitellmHealth(url)) {
4616
- logger.info(`LiteLLM proxy ready at ${url}`);
4617
- return child;
4618
- }
4619
- }
4620
- if (proxyStderr) {
4621
- logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1e3)}`);
4622
- }
4623
- logger.warn("LiteLLM proxy failed to start within 60s");
4624
- child.kill();
4625
- return null;
4626
- }
4627
- var init_litellm = __esm({
4628
- "src/cli/litellm.ts"() {
4629
- "use strict";
4630
- init_logger();
4631
- init_config();
4632
- }
4633
- });
4634
-
4635
4658
  // src/cli/task-state.ts
4636
4659
  import * as fs29 from "fs";
4637
4660
  import * as path26 from "path";