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

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 +463 -420
  2. package/package.json +1 -1
package/dist/bin/cli.js CHANGED
@@ -984,7 +984,24 @@ function resolveTaskIdFromComments(issueNumber) {
984
984
  return null;
985
985
  }
986
986
  }
987
+ function findPausedTaskifyForIssue(issueNumber, projectDir) {
988
+ const tasksDir = path8.join(projectDir, ".kody", "tasks");
989
+ if (!fs9.existsSync(tasksDir)) return null;
990
+ const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
991
+ for (const dir of allDirs) {
992
+ const markerPath = path8.join(tasksDir, dir, "taskify.marker");
993
+ if (!fs9.existsSync(markerPath)) continue;
994
+ try {
995
+ const marker = JSON.parse(fs9.readFileSync(markerPath, "utf-8"));
996
+ if (marker.issueNumber === issueNumber) return dir;
997
+ } catch {
998
+ }
999
+ }
1000
+ return null;
1001
+ }
987
1002
  function resolveTaskIdForCommand(issueNumber, projectDir) {
1003
+ const fromTaskify = findPausedTaskifyForIssue(issueNumber, projectDir);
1004
+ if (fromTaskify) return fromTaskify;
988
1005
  const fromTasks = findLatestTaskForIssue(issueNumber, projectDir);
989
1006
  if (fromTasks) return fromTasks;
990
1007
  const fromComments = resolveTaskIdFromComments(issueNumber);
@@ -998,6 +1015,180 @@ var init_task_resolution = __esm({
998
1015
  }
999
1016
  });
1000
1017
 
1018
+ // src/cli/litellm.ts
1019
+ import * as fs10 from "fs";
1020
+ import * as os from "os";
1021
+ import * as path9 from "path";
1022
+ import { execFileSync as execFileSync9 } from "child_process";
1023
+ async function checkLitellmHealth(url) {
1024
+ try {
1025
+ const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
1026
+ return response.ok;
1027
+ } catch {
1028
+ return false;
1029
+ }
1030
+ }
1031
+ async function checkModelHealth(baseUrl, apiKey, model) {
1032
+ try {
1033
+ const res = await fetch(`${baseUrl}/v1/messages`, {
1034
+ method: "POST",
1035
+ headers: {
1036
+ "Content-Type": "application/json",
1037
+ "x-api-key": apiKey,
1038
+ "anthropic-version": "2023-06-01"
1039
+ },
1040
+ body: JSON.stringify({
1041
+ model,
1042
+ max_tokens: 4,
1043
+ messages: [{ role: "user", content: "Reply with: ok" }]
1044
+ }),
1045
+ signal: AbortSignal.timeout(3e4)
1046
+ });
1047
+ if (!res.ok) {
1048
+ const body2 = await res.text().catch(() => "");
1049
+ return { ok: false, error: `HTTP ${res.status}: ${body2.slice(0, 200)}` };
1050
+ }
1051
+ const body = await res.json();
1052
+ const hasAnthropicContent = Array.isArray(body.content) && body.content.some((b) => b.type === "text");
1053
+ const hasThinkingContent = Array.isArray(body.content) && body.content.some((b) => b.type === "thinking");
1054
+ const hasOpenAIContent = !!body.choices?.[0]?.message?.content;
1055
+ if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent) {
1056
+ return { ok: false, error: `Unexpected response format: ${JSON.stringify(body).slice(0, 200)}` };
1057
+ }
1058
+ return { ok: true };
1059
+ } catch (err) {
1060
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1061
+ }
1062
+ }
1063
+ function generateLitellmConfig(provider, modelMap) {
1064
+ const apiKeyVar = providerApiKeyEnvVar(provider);
1065
+ const entries = ["model_list:"];
1066
+ const seen = /* @__PURE__ */ new Set();
1067
+ for (const providerModel of Object.values(modelMap)) {
1068
+ if (seen.has(providerModel)) continue;
1069
+ seen.add(providerModel);
1070
+ entries.push(` - model_name: ${providerModel}`);
1071
+ entries.push(` litellm_params:`);
1072
+ entries.push(` model: ${provider}/${providerModel}`);
1073
+ entries.push(` api_key: os.environ/${apiKeyVar}`);
1074
+ }
1075
+ return entries.join("\n") + "\n";
1076
+ }
1077
+ function generateLitellmConfigFromStages(defaultConfig, stages) {
1078
+ const proxyModels = [];
1079
+ if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
1080
+ proxyModels.push(defaultConfig);
1081
+ }
1082
+ if (stages) {
1083
+ for (const sc of Object.values(stages)) {
1084
+ if (sc.provider !== "claude" && sc.provider !== "anthropic") {
1085
+ proxyModels.push(sc);
1086
+ }
1087
+ }
1088
+ }
1089
+ if (proxyModels.length === 0) return void 0;
1090
+ const entries = ["model_list:"];
1091
+ const seen = /* @__PURE__ */ new Set();
1092
+ for (const { provider, model } of proxyModels) {
1093
+ const key = `${provider}/${model}`;
1094
+ if (seen.has(key)) continue;
1095
+ seen.add(key);
1096
+ const apiKeyVar = providerApiKeyEnvVar(provider);
1097
+ entries.push(` - model_name: ${model}`);
1098
+ entries.push(` litellm_params:`);
1099
+ entries.push(` model: ${provider}/${model}`);
1100
+ entries.push(` api_key: os.environ/${apiKeyVar}`);
1101
+ }
1102
+ return entries.join("\n") + "\n";
1103
+ }
1104
+ async function tryStartLitellm(url, projectDir, generatedConfig) {
1105
+ if (!generatedConfig) {
1106
+ logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
1107
+ return null;
1108
+ }
1109
+ const configPath = path9.join(os.tmpdir(), "kody-litellm-config.yaml");
1110
+ fs10.writeFileSync(configPath, generatedConfig);
1111
+ const portMatch = url.match(/:(\d+)/);
1112
+ const port = portMatch ? portMatch[1] : "4000";
1113
+ let litellmFound = false;
1114
+ try {
1115
+ execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1116
+ litellmFound = true;
1117
+ } catch {
1118
+ try {
1119
+ execFileSync9("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
1120
+ litellmFound = true;
1121
+ } catch {
1122
+ }
1123
+ }
1124
+ if (!litellmFound) {
1125
+ logger.warn("litellm not installed (pip install 'litellm[proxy]')");
1126
+ return null;
1127
+ }
1128
+ logger.info(`Starting LiteLLM proxy on port ${port}...`);
1129
+ let cmd;
1130
+ let args2;
1131
+ try {
1132
+ execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1133
+ cmd = "litellm";
1134
+ args2 = ["--config", configPath, "--port", port];
1135
+ } catch {
1136
+ cmd = "python3";
1137
+ args2 = ["-m", "litellm", "--config", configPath, "--port", port];
1138
+ }
1139
+ const dotenvPath = path9.join(projectDir, ".env");
1140
+ const dotenvVars = {};
1141
+ if (fs10.existsSync(dotenvPath)) {
1142
+ for (const rawLine of fs10.readFileSync(dotenvPath, "utf-8").split("\n")) {
1143
+ const line = rawLine.trim();
1144
+ if (!line || line.startsWith("#")) continue;
1145
+ const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
1146
+ if (match) {
1147
+ let value = match[2].trim();
1148
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1149
+ value = value.slice(1, -1);
1150
+ }
1151
+ const commentIdx = value.indexOf(" #");
1152
+ if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
1153
+ if (value) dotenvVars[match[1]] = value;
1154
+ }
1155
+ }
1156
+ if (Object.keys(dotenvVars).length > 0) {
1157
+ logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
1158
+ }
1159
+ }
1160
+ const { spawn: spawn2 } = await import("child_process");
1161
+ const child = spawn2(cmd, args2, {
1162
+ stdio: ["ignore", "pipe", "pipe"],
1163
+ detached: true,
1164
+ env: { ...process.env, ...dotenvVars }
1165
+ });
1166
+ let proxyStderr = "";
1167
+ child.stderr?.on("data", (chunk) => {
1168
+ proxyStderr += chunk.toString();
1169
+ });
1170
+ for (let i = 0; i < 30; i++) {
1171
+ await new Promise((r) => setTimeout(r, 2e3));
1172
+ if (await checkLitellmHealth(url)) {
1173
+ logger.info(`LiteLLM proxy ready at ${url}`);
1174
+ return child;
1175
+ }
1176
+ }
1177
+ if (proxyStderr) {
1178
+ logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1e3)}`);
1179
+ }
1180
+ logger.warn("LiteLLM proxy failed to start within 60s");
1181
+ child.kill();
1182
+ return null;
1183
+ }
1184
+ var init_litellm = __esm({
1185
+ "src/cli/litellm.ts"() {
1186
+ "use strict";
1187
+ init_logger();
1188
+ init_config();
1189
+ }
1190
+ });
1191
+
1001
1192
  // src/cli/taskify-command.ts
1002
1193
  var taskify_command_exports = {};
1003
1194
  __export(taskify_command_exports, {
@@ -1007,8 +1198,8 @@ __export(taskify_command_exports, {
1007
1198
  taskifyCommand: () => taskifyCommand,
1008
1199
  topoSort: () => topoSort
1009
1200
  });
1010
- import * as fs10 from "fs";
1011
- import * as path9 from "path";
1201
+ import * as fs11 from "fs";
1202
+ import * as path10 from "path";
1012
1203
  import { fileURLToPath } from "url";
1013
1204
  import { execSync } from "child_process";
1014
1205
  function topoSort(tasks) {
@@ -1052,10 +1243,10 @@ function hasFlag(args2, flag) {
1052
1243
  async function runTaskifyCommand() {
1053
1244
  const args2 = process.argv.slice(3);
1054
1245
  const cwdArg = getArg(args2, "--cwd") ?? process.cwd();
1055
- const projectDir = path9.resolve(cwdArg);
1246
+ const projectDir = path10.resolve(cwdArg);
1056
1247
  const ticketId = getArg(args2, "--ticket") ?? process.env.TICKET_ID;
1057
1248
  const prdFileArg = getArg(args2, "--file") ?? process.env.PRD_FILE;
1058
- const prdFile = prdFileArg ? path9.resolve(projectDir, prdFileArg) : void 0;
1249
+ const prdFile = prdFileArg ? path10.resolve(projectDir, prdFileArg) : void 0;
1059
1250
  const issueNumberStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER ?? "";
1060
1251
  const issueNumber = issueNumberStr ? parseInt(issueNumberStr, 10) : void 0;
1061
1252
  const feedback = getArg(args2, "--feedback") ?? process.env.FEEDBACK;
@@ -1066,19 +1257,40 @@ async function runTaskifyCommand() {
1066
1257
  logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md>");
1067
1258
  process.exit(1);
1068
1259
  }
1069
- if (prdFile && !fs10.existsSync(prdFile)) {
1260
+ if (prdFile && !fs11.existsSync(prdFile)) {
1070
1261
  logger.error(`File not found: ${prdFile}`);
1071
1262
  process.exit(1);
1072
1263
  }
1073
1264
  setConfigDir(projectDir);
1074
1265
  setGhCwd(projectDir);
1075
- await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId });
1266
+ const config = getProjectConfig();
1267
+ let litellmProcess = null;
1268
+ let runnerEnv;
1269
+ if (anyStageNeedsProxy(config)) {
1270
+ const litellmUrl = getLitellmUrl();
1271
+ const proxyRunning = await checkLitellmHealth(litellmUrl);
1272
+ if (!proxyRunning) {
1273
+ let generatedConfig;
1274
+ if (config.agent.stages || config.agent.default) {
1275
+ generatedConfig = generateLitellmConfigFromStages(config.agent.default, config.agent.stages);
1276
+ } else if (config.agent.provider && config.agent.provider !== "anthropic") {
1277
+ generatedConfig = generateLitellmConfig(config.agent.provider, config.agent.modelMap);
1278
+ }
1279
+ litellmProcess = await tryStartLitellm(litellmUrl, projectDir, generatedConfig);
1280
+ }
1281
+ runnerEnv = {
1282
+ ANTHROPIC_BASE_URL: litellmUrl,
1283
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
1284
+ };
1285
+ }
1286
+ await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1287
+ litellmProcess?.kill();
1076
1288
  }
1077
1289
  async function taskifyCommand(opts) {
1078
1290
  const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
1079
1291
  const config = getProjectConfig();
1080
- const taskDir = path9.join(projectDir, ".kody", "tasks", taskId);
1081
- fs10.mkdirSync(taskDir, { recursive: true });
1292
+ const taskDir = path10.join(projectDir, ".kody", "tasks", taskId);
1293
+ fs11.mkdirSync(taskDir, { recursive: true });
1082
1294
  const mode = prdFile ? "file" : "ticket";
1083
1295
  logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile} issue=${issueNumber ?? "none"} task=${taskId}`);
1084
1296
  let mcpConfigJson;
@@ -1103,14 +1315,14 @@ Add the required MCP server config to \`kody.config.json\` and try again.`
1103
1315
  }
1104
1316
  const sc = resolveStageConfig(config, "taskify", "strong");
1105
1317
  const model = sc.model;
1106
- const fileContent = prdFile ? fs10.readFileSync(prdFile, "utf-8") : void 0;
1318
+ const fileContent = prdFile ? fs11.readFileSync(prdFile, "utf-8") : void 0;
1107
1319
  let projectContext;
1108
1320
  {
1109
1321
  const parts = [];
1110
- const memoryPath = path9.join(projectDir, ".kody", "memory.md");
1111
- if (fs10.existsSync(memoryPath)) {
1322
+ const memoryPath = path10.join(projectDir, ".kody", "memory.md");
1323
+ if (fs11.existsSync(memoryPath)) {
1112
1324
  try {
1113
- const content = fs10.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
1325
+ const content = fs11.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
1114
1326
  if (content.trim()) parts.push(`### Project Memory
1115
1327
  ${content}`);
1116
1328
  } catch {
@@ -1129,16 +1341,20 @@ ${lines.join("\n")}
1129
1341
  }
1130
1342
  const prompt = buildPrompt({ ticketId, fileContent, taskDir, feedback, projectContext });
1131
1343
  if (issueNumber && !local) {
1132
- const src = mode === "file" ? `file \`${path9.basename(prdFile)}\`` : `ticket **${ticketId}**`;
1133
- postComment(issueNumber, `Kody is decomposing ${src} into tasks...`);
1344
+ const src = mode === "file" ? `file \`${path10.basename(prdFile)}\`` : `ticket **${ticketId}**`;
1345
+ const runUrl = process.env.RUN_URL ? ` ([logs](${process.env.RUN_URL}))` : "";
1346
+ postComment(issueNumber, `\u{1F680} Kody pipeline started: \`${taskId}\`${runUrl}
1347
+
1348
+ Kody is decomposing ${src} into tasks...`);
1134
1349
  setLifecycleLabel(issueNumber, "planning");
1135
1350
  }
1136
- fs10.writeFileSync(path9.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1351
+ fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1137
1352
  const runner = opts.runner ?? createClaudeCodeRunner();
1138
1353
  logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
1139
1354
  const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
1140
1355
  cwd: projectDir,
1141
- mcpConfigJson
1356
+ mcpConfigJson,
1357
+ env: opts.runnerEnv
1142
1358
  });
1143
1359
  if (result.outcome !== "completed") {
1144
1360
  const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
@@ -1151,8 +1367,8 @@ ${lines.join("\n")}
1151
1367
  }
1152
1368
  process.exit(1);
1153
1369
  }
1154
- const resultPath = path9.join(taskDir, RESULT_FILE);
1155
- if (!fs10.existsSync(resultPath)) {
1370
+ const resultPath = path10.join(taskDir, RESULT_FILE);
1371
+ if (!fs11.existsSync(resultPath)) {
1156
1372
  const errMsg = `Claude did not write ${RESULT_FILE}. Output:
1157
1373
 
1158
1374
  ${result.output?.slice(0, 500) ?? "(none)"}`;
@@ -1167,7 +1383,7 @@ ${errMsg}`);
1167
1383
  }
1168
1384
  let parsed;
1169
1385
  try {
1170
- parsed = JSON.parse(fs10.readFileSync(resultPath, "utf-8"));
1386
+ parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
1171
1387
  } catch {
1172
1388
  const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
1173
1389
  logger.error(`[taskify] ${errMsg}`);
@@ -1177,7 +1393,7 @@ ${errMsg}`);
1177
1393
  }
1178
1394
  process.exit(1);
1179
1395
  }
1180
- const sourceLabel = ticketId ?? (prdFile ? path9.basename(prdFile) : "spec");
1396
+ const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
1181
1397
  if (parsed.status === "questions") {
1182
1398
  handleQuestions(parsed, sourceLabel, issueNumber, local ?? false);
1183
1399
  } else if (parsed.status === "ready") {
@@ -1272,15 +1488,15 @@ function buildPrompt(opts) {
1272
1488
  const { ticketId, fileContent, taskDir, feedback, projectContext } = opts;
1273
1489
  const scriptDir = new URL(".", import.meta.url).pathname;
1274
1490
  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")
1491
+ path10.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
1492
+ path10.resolve(scriptDir, "..", "..", "prompts", "taskify-ticket.md"),
1493
+ path10.resolve(__dirname, "..", "..", "prompts", "taskify-ticket.md"),
1494
+ path10.resolve(__dirname, "..", "prompts", "taskify-ticket.md")
1279
1495
  ];
1280
1496
  let template = "";
1281
1497
  for (const candidate of candidates) {
1282
- if (fs10.existsSync(candidate)) {
1283
- template = fs10.readFileSync(candidate, "utf-8");
1498
+ if (fs11.existsSync(candidate)) {
1499
+ template = fs11.readFileSync(candidate, "utf-8");
1284
1500
  break;
1285
1501
  }
1286
1502
  }
@@ -1303,13 +1519,13 @@ function buildPrompt(opts) {
1303
1519
  return template;
1304
1520
  }
1305
1521
  function isTaskifyRun(taskDir) {
1306
- return fs10.existsSync(path9.join(taskDir, MARKER_FILE));
1522
+ return fs11.existsSync(path10.join(taskDir, MARKER_FILE));
1307
1523
  }
1308
1524
  function readTaskifyMarker(taskDir) {
1309
- const markerPath = path9.join(taskDir, MARKER_FILE);
1310
- if (!fs10.existsSync(markerPath)) return null;
1525
+ const markerPath = path10.join(taskDir, MARKER_FILE);
1526
+ if (!fs11.existsSync(markerPath)) return null;
1311
1527
  try {
1312
- return JSON.parse(fs10.readFileSync(markerPath, "utf-8"));
1528
+ return JSON.parse(fs11.readFileSync(markerPath, "utf-8"));
1313
1529
  } catch {
1314
1530
  return null;
1315
1531
  }
@@ -1324,7 +1540,8 @@ var init_taskify_command = __esm({
1324
1540
  init_github_api();
1325
1541
  init_logger();
1326
1542
  init_task_resolution();
1327
- __dirname = path9.dirname(fileURLToPath(import.meta.url));
1543
+ init_litellm();
1544
+ __dirname = path10.dirname(fileURLToPath(import.meta.url));
1328
1545
  AUTO_TRIGGER_THRESHOLD = 5;
1329
1546
  MAX_TASKS_GUARD = 20;
1330
1547
  TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -1340,7 +1557,7 @@ __export(parse_inputs_exports, {
1340
1557
  runCiParse: () => runCiParse,
1341
1558
  writeOutputs: () => writeOutputs
1342
1559
  });
1343
- import * as fs11 from "fs";
1560
+ import * as fs12 from "fs";
1344
1561
  function generateTimestamp() {
1345
1562
  const now = /* @__PURE__ */ new Date();
1346
1563
  const pad = (n) => String(n).padStart(2, "0");
@@ -1506,12 +1723,12 @@ function writeOutputs(result) {
1506
1723
  function output(key, value) {
1507
1724
  if (outputFile) {
1508
1725
  if (value.includes("\n")) {
1509
- fs11.appendFileSync(outputFile, `${key}<<KODY_EOF
1726
+ fs12.appendFileSync(outputFile, `${key}<<KODY_EOF
1510
1727
  ${value}
1511
1728
  KODY_EOF
1512
1729
  `);
1513
1730
  } else {
1514
- fs11.appendFileSync(outputFile, `${key}=${value}
1731
+ fs12.appendFileSync(outputFile, `${key}=${value}
1515
1732
  `);
1516
1733
  }
1517
1734
  }
@@ -1636,7 +1853,7 @@ var init_definitions = __esm({
1636
1853
  });
1637
1854
 
1638
1855
  // src/git-utils.ts
1639
- import { execFileSync as execFileSync9 } from "child_process";
1856
+ import { execFileSync as execFileSync10 } from "child_process";
1640
1857
  function getHookSafeEnv() {
1641
1858
  if (!_hookSafeEnv) {
1642
1859
  _hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
@@ -1644,7 +1861,7 @@ function getHookSafeEnv() {
1644
1861
  return _hookSafeEnv;
1645
1862
  }
1646
1863
  function git(args2, options) {
1647
- return execFileSync9("git", args2, {
1864
+ return execFileSync10("git", args2, {
1648
1865
  encoding: "utf-8",
1649
1866
  timeout: options?.timeout ?? 3e4,
1650
1867
  cwd: options?.cwd,
@@ -1830,14 +2047,14 @@ var init_git_utils = __esm({
1830
2047
  });
1831
2048
 
1832
2049
  // src/pipeline/state.ts
1833
- import * as fs12 from "fs";
1834
- import * as path10 from "path";
2050
+ import * as fs13 from "fs";
2051
+ import * as path11 from "path";
1835
2052
  function loadState(taskId, taskDir) {
1836
- const p = path10.join(taskDir, "status.json");
1837
- if (!fs12.existsSync(p)) return null;
2053
+ const p = path11.join(taskDir, "status.json");
2054
+ if (!fs13.existsSync(p)) return null;
1838
2055
  try {
1839
2056
  const result = parseJsonSafe(
1840
- fs12.readFileSync(p, "utf-8"),
2057
+ fs13.readFileSync(p, "utf-8"),
1841
2058
  ["taskId", "state", "stages", "createdAt", "updatedAt"]
1842
2059
  );
1843
2060
  if (!result.ok) {
@@ -1855,10 +2072,10 @@ function writeState(state, taskDir) {
1855
2072
  ...state,
1856
2073
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1857
2074
  };
1858
- const target = path10.join(taskDir, "status.json");
2075
+ const target = path11.join(taskDir, "status.json");
1859
2076
  const tmp = target + ".tmp";
1860
- fs12.writeFileSync(tmp, JSON.stringify(updated, null, 2));
1861
- fs12.renameSync(tmp, target);
2077
+ fs13.writeFileSync(tmp, JSON.stringify(updated, null, 2));
2078
+ fs13.renameSync(tmp, target);
1862
2079
  return updated;
1863
2080
  }
1864
2081
  function initState(taskId) {
@@ -1899,16 +2116,16 @@ var init_complexity = __esm({
1899
2116
  });
1900
2117
 
1901
2118
  // src/memory.ts
1902
- import * as fs13 from "fs";
1903
- import * as path11 from "path";
2119
+ import * as fs14 from "fs";
2120
+ import * as path12 from "path";
1904
2121
  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();
2122
+ const memoryDir = path12.join(projectDir, ".kody", "memory");
2123
+ if (!fs14.existsSync(memoryDir)) return "";
2124
+ const files = fs14.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1908
2125
  if (files.length === 0) return "";
1909
2126
  const sections = [];
1910
2127
  for (const file of files) {
1911
- const content = fs13.readFileSync(path11.join(memoryDir, file), "utf-8").trim();
2128
+ const content = fs14.readFileSync(path12.join(memoryDir, file), "utf-8").trim();
1912
2129
  if (content) {
1913
2130
  sections.push(`## ${file.replace(".md", "")}
1914
2131
  ${content}`);
@@ -1927,8 +2144,8 @@ var init_memory = __esm({
1927
2144
  });
1928
2145
 
1929
2146
  // src/context-tiers.ts
1930
- import * as fs14 from "fs";
1931
- import * as path12 from "path";
2147
+ import * as fs15 from "fs";
2148
+ import * as path13 from "path";
1932
2149
  function estimateTokens(text) {
1933
2150
  return Math.ceil(text.length / 4);
1934
2151
  }
@@ -2019,7 +2236,7 @@ function generateL1Json(content) {
2019
2236
  }
2020
2237
  }
2021
2238
  function getTieredContent(filePath, content) {
2022
- const key = path12.basename(filePath);
2239
+ const key = path13.basename(filePath);
2023
2240
  return {
2024
2241
  source: filePath,
2025
2242
  L0: generateL0(content, key),
@@ -2031,15 +2248,15 @@ function selectTier(tiered, tier) {
2031
2248
  return tiered[tier];
2032
2249
  }
2033
2250
  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();
2251
+ const memoryDir = path13.join(projectDir, ".kody", "memory");
2252
+ if (!fs15.existsSync(memoryDir)) return "";
2253
+ const files = fs15.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
2037
2254
  if (files.length === 0) return "";
2038
2255
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
2039
2256
  const sections = [];
2040
2257
  for (const file of files) {
2041
- const filePath = path12.join(memoryDir, file);
2042
- const content = fs14.readFileSync(filePath, "utf-8").trim();
2258
+ const filePath = path13.join(memoryDir, file);
2259
+ const content = fs15.readFileSync(filePath, "utf-8").trim();
2043
2260
  if (!content) continue;
2044
2261
  const tiered = getTieredContent(filePath, content);
2045
2262
  const selected = selectTier(tiered, tier);
@@ -2062,9 +2279,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
2062
2279
  `;
2063
2280
  context += `Task Directory: ${taskDir}
2064
2281
  `;
2065
- const taskMdPath = path12.join(taskDir, "task.md");
2066
- if (fs14.existsSync(taskMdPath)) {
2067
- const content = fs14.readFileSync(taskMdPath, "utf-8");
2282
+ const taskMdPath = path13.join(taskDir, "task.md");
2283
+ if (fs15.existsSync(taskMdPath)) {
2284
+ const content = fs15.readFileSync(taskMdPath, "utf-8");
2068
2285
  const selected = selectContent(taskMdPath, content, policy.taskDescription);
2069
2286
  const label = tierLabel("Task Description", policy.taskDescription);
2070
2287
  context += `
@@ -2072,9 +2289,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
2072
2289
  ${selected}
2073
2290
  `;
2074
2291
  }
2075
- const taskJsonPath = path12.join(taskDir, "task.json");
2076
- if (fs14.existsSync(taskJsonPath)) {
2077
- const content = fs14.readFileSync(taskJsonPath, "utf-8");
2292
+ const taskJsonPath = path13.join(taskDir, "task.json");
2293
+ if (fs15.existsSync(taskJsonPath)) {
2294
+ const content = fs15.readFileSync(taskJsonPath, "utf-8");
2078
2295
  if (policy.taskClassification === "L2") {
2079
2296
  try {
2080
2297
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -2100,9 +2317,9 @@ ${selected}
2100
2317
  }
2101
2318
  }
2102
2319
  }
2103
- const specPath = path12.join(taskDir, "spec.md");
2104
- if (fs14.existsSync(specPath)) {
2105
- const content = fs14.readFileSync(specPath, "utf-8");
2320
+ const specPath = path13.join(taskDir, "spec.md");
2321
+ if (fs15.existsSync(specPath)) {
2322
+ const content = fs15.readFileSync(specPath, "utf-8");
2106
2323
  const selected = selectContent(specPath, content, policy.spec);
2107
2324
  const label = tierLabel("Spec", policy.spec);
2108
2325
  context += `
@@ -2110,9 +2327,9 @@ ${selected}
2110
2327
  ${selected}
2111
2328
  `;
2112
2329
  }
2113
- const planPath = path12.join(taskDir, "plan.md");
2114
- if (fs14.existsSync(planPath)) {
2115
- const content = fs14.readFileSync(planPath, "utf-8");
2330
+ const planPath = path13.join(taskDir, "plan.md");
2331
+ if (fs15.existsSync(planPath)) {
2332
+ const content = fs15.readFileSync(planPath, "utf-8");
2116
2333
  const selected = selectContent(planPath, content, policy.plan);
2117
2334
  const label = tierLabel("Plan", policy.plan);
2118
2335
  context += `
@@ -2120,9 +2337,9 @@ ${selected}
2120
2337
  ${selected}
2121
2338
  `;
2122
2339
  }
2123
- const contextMdPath = path12.join(taskDir, "context.md");
2124
- if (fs14.existsSync(contextMdPath)) {
2125
- const content = fs14.readFileSync(contextMdPath, "utf-8");
2340
+ const contextMdPath = path13.join(taskDir, "context.md");
2341
+ if (fs15.existsSync(contextMdPath)) {
2342
+ const content = fs15.readFileSync(contextMdPath, "utf-8");
2126
2343
  const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
2127
2344
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
2128
2345
  context += `
@@ -2208,24 +2425,24 @@ var init_context_tiers = __esm({
2208
2425
  });
2209
2426
 
2210
2427
  // src/context.ts
2211
- import * as fs15 from "fs";
2212
- import * as path13 from "path";
2428
+ import * as fs16 from "fs";
2429
+ import * as path14 from "path";
2213
2430
  function readPromptFile(stageName, projectDir) {
2214
2431
  if (projectDir) {
2215
- const stepFile = path13.join(projectDir, ".kody", "steps", `${stageName}.md`);
2216
- if (fs15.existsSync(stepFile)) {
2217
- return fs15.readFileSync(stepFile, "utf-8");
2432
+ const stepFile = path14.join(projectDir, ".kody", "steps", `${stageName}.md`);
2433
+ if (fs16.existsSync(stepFile)) {
2434
+ return fs16.readFileSync(stepFile, "utf-8");
2218
2435
  }
2219
2436
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
2220
2437
  }
2221
2438
  const scriptDir = new URL(".", import.meta.url).pathname;
2222
2439
  const candidates = [
2223
- path13.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
2224
- path13.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
2440
+ path14.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
2441
+ path14.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
2225
2442
  ];
2226
2443
  for (const candidate of candidates) {
2227
- if (fs15.existsSync(candidate)) {
2228
- return fs15.readFileSync(candidate, "utf-8");
2444
+ if (fs16.existsSync(candidate)) {
2445
+ return fs16.readFileSync(candidate, "utf-8");
2229
2446
  }
2230
2447
  }
2231
2448
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -2237,18 +2454,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
2237
2454
  `;
2238
2455
  context += `Task Directory: ${taskDir}
2239
2456
  `;
2240
- const taskMdPath = path13.join(taskDir, "task.md");
2241
- if (fs15.existsSync(taskMdPath)) {
2242
- const taskMd = fs15.readFileSync(taskMdPath, "utf-8");
2457
+ const taskMdPath = path14.join(taskDir, "task.md");
2458
+ if (fs16.existsSync(taskMdPath)) {
2459
+ const taskMd = fs16.readFileSync(taskMdPath, "utf-8");
2243
2460
  context += `
2244
2461
  ## Task Description
2245
2462
  ${taskMd}
2246
2463
  `;
2247
2464
  }
2248
- const taskJsonPath = path13.join(taskDir, "task.json");
2249
- if (fs15.existsSync(taskJsonPath)) {
2465
+ const taskJsonPath = path14.join(taskDir, "task.json");
2466
+ if (fs16.existsSync(taskJsonPath)) {
2250
2467
  try {
2251
- const taskDef = JSON.parse(fs15.readFileSync(taskJsonPath, "utf-8"));
2468
+ const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
2252
2469
  context += `
2253
2470
  ## Task Classification
2254
2471
  `;
@@ -2261,27 +2478,27 @@ ${taskMd}
2261
2478
  } catch {
2262
2479
  }
2263
2480
  }
2264
- const specPath = path13.join(taskDir, "spec.md");
2265
- if (fs15.existsSync(specPath)) {
2266
- const spec = fs15.readFileSync(specPath, "utf-8");
2481
+ const specPath = path14.join(taskDir, "spec.md");
2482
+ if (fs16.existsSync(specPath)) {
2483
+ const spec = fs16.readFileSync(specPath, "utf-8");
2267
2484
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
2268
2485
  context += `
2269
2486
  ## Spec Summary
2270
2487
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
2271
2488
  `;
2272
2489
  }
2273
- const planPath = path13.join(taskDir, "plan.md");
2274
- if (fs15.existsSync(planPath)) {
2275
- const plan = fs15.readFileSync(planPath, "utf-8");
2490
+ const planPath = path14.join(taskDir, "plan.md");
2491
+ if (fs16.existsSync(planPath)) {
2492
+ const plan = fs16.readFileSync(planPath, "utf-8");
2276
2493
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
2277
2494
  context += `
2278
2495
  ## Plan Summary
2279
2496
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
2280
2497
  `;
2281
2498
  }
2282
- const contextMdPath = path13.join(taskDir, "context.md");
2283
- if (fs15.existsSync(contextMdPath)) {
2284
- const accumulated = fs15.readFileSync(contextMdPath, "utf-8");
2499
+ const contextMdPath = path14.join(taskDir, "context.md");
2500
+ if (fs16.existsSync(contextMdPath)) {
2501
+ const accumulated = fs16.readFileSync(contextMdPath, "utf-8");
2285
2502
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
2286
2503
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
2287
2504
  context += `
@@ -2299,17 +2516,17 @@ ${feedback}
2299
2516
  }
2300
2517
  function inferHasUIFromScope(scope) {
2301
2518
  return scope.some((filePath) => {
2302
- const ext = path13.extname(filePath).toLowerCase();
2519
+ const ext = path14.extname(filePath).toLowerCase();
2303
2520
  if (UI_EXTENSIONS.has(ext)) return true;
2304
2521
  const normalized = filePath.replace(/\\/g, "/");
2305
2522
  return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
2306
2523
  });
2307
2524
  }
2308
2525
  function taskHasUI(taskDir) {
2309
- const taskJsonPath = path13.join(taskDir, "task.json");
2310
- if (!fs15.existsSync(taskJsonPath)) return true;
2526
+ const taskJsonPath = path14.join(taskDir, "task.json");
2527
+ if (!fs16.existsSync(taskJsonPath)) return true;
2311
2528
  try {
2312
- const taskDef = JSON.parse(fs15.readFileSync(taskJsonPath, "utf-8"));
2529
+ const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
2313
2530
  const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
2314
2531
  if (scope.length === 0) return true;
2315
2532
  return inferHasUIFromScope(scope);
@@ -2431,9 +2648,9 @@ ${prompt}` : prompt;
2431
2648
  }
2432
2649
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
2433
2650
  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();
2651
+ const qaGuidePath = path14.join(projectDir, ".kody", "qa-guide.md");
2652
+ if (fs16.existsSync(qaGuidePath)) {
2653
+ const qaGuide = fs16.readFileSync(qaGuidePath, "utf-8").trim();
2437
2654
  assembled = assembled + "\n\n" + qaGuide;
2438
2655
  }
2439
2656
  }
@@ -2528,8 +2745,8 @@ var init_runner_selection = __esm({
2528
2745
  });
2529
2746
 
2530
2747
  // src/stages/agent.ts
2531
- import * as fs16 from "fs";
2532
- import * as path14 from "path";
2748
+ import * as fs17 from "fs";
2749
+ import * as path15 from "path";
2533
2750
  function getSessionInfo(stageName, sessions) {
2534
2751
  const group = SESSION_GROUP[stageName];
2535
2752
  if (!group) return void 0;
@@ -2616,27 +2833,27 @@ async function executeAgentStage(ctx, def) {
2616
2833
  }
2617
2834
  const result = lastResult;
2618
2835
  if (def.outputFile && result.output) {
2619
- fs16.writeFileSync(path14.join(ctx.taskDir, def.outputFile), result.output);
2836
+ fs17.writeFileSync(path15.join(ctx.taskDir, def.outputFile), result.output);
2620
2837
  }
2621
2838
  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);
2839
+ const outputPath = path15.join(ctx.taskDir, def.outputFile);
2840
+ if (!fs17.existsSync(outputPath)) {
2841
+ const ext = path15.extname(def.outputFile);
2842
+ const base = path15.basename(def.outputFile, ext);
2843
+ const files = fs17.readdirSync(ctx.taskDir);
2627
2844
  const variant = files.find(
2628
2845
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
2629
2846
  );
2630
2847
  if (variant) {
2631
- fs16.renameSync(path14.join(ctx.taskDir, variant), outputPath);
2848
+ fs17.renameSync(path15.join(ctx.taskDir, variant), outputPath);
2632
2849
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
2633
2850
  }
2634
2851
  }
2635
2852
  }
2636
2853
  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");
2854
+ const outputPath = path15.join(ctx.taskDir, def.outputFile);
2855
+ if (fs17.existsSync(outputPath)) {
2856
+ const content = fs17.readFileSync(outputPath, "utf-8");
2640
2857
  const validation = validateStageOutput(def.name, content);
2641
2858
  if (!validation.valid) {
2642
2859
  if (def.name === "taskify") {
@@ -2650,7 +2867,7 @@ async function executeAgentStage(ctx, def) {
2650
2867
  const stripped = stripFences(retryResult.output);
2651
2868
  const retryValidation = validateTaskJson(stripped);
2652
2869
  if (retryValidation.valid) {
2653
- fs16.writeFileSync(outputPath, retryResult.output);
2870
+ fs17.writeFileSync(outputPath, retryResult.output);
2654
2871
  logger.info(` taskify retry produced valid JSON`);
2655
2872
  } else {
2656
2873
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -2663,7 +2880,7 @@ async function executeAgentStage(ctx, def) {
2663
2880
  risk_level: "low",
2664
2881
  questions: []
2665
2882
  }, null, 2);
2666
- fs16.writeFileSync(outputPath, fallback);
2883
+ fs17.writeFileSync(outputPath, fallback);
2667
2884
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
2668
2885
  }
2669
2886
  }
@@ -2677,7 +2894,7 @@ async function executeAgentStage(ctx, def) {
2677
2894
  return { outcome: "completed", outputFile: def.outputFile, retries };
2678
2895
  }
2679
2896
  function appendStageContext(taskDir, stageName, output) {
2680
- const contextPath = path14.join(taskDir, "context.md");
2897
+ const contextPath = path15.join(taskDir, "context.md");
2681
2898
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
2682
2899
  let summary;
2683
2900
  if (output && output.trim()) {
@@ -2690,7 +2907,7 @@ function appendStageContext(taskDir, stageName, output) {
2690
2907
  ### ${stageName} (${timestamp2})
2691
2908
  ${summary}
2692
2909
  `;
2693
- fs16.appendFileSync(contextPath, entry);
2910
+ fs17.appendFileSync(contextPath, entry);
2694
2911
  }
2695
2912
  var SESSION_GROUP;
2696
2913
  var init_agent = __esm({
@@ -2713,7 +2930,7 @@ var init_agent = __esm({
2713
2930
  });
2714
2931
 
2715
2932
  // src/verify-runner.ts
2716
- import { execFileSync as execFileSync10 } from "child_process";
2933
+ import { execFileSync as execFileSync11 } from "child_process";
2717
2934
  function isExecError(err) {
2718
2935
  return typeof err === "object" && err !== null;
2719
2936
  }
@@ -2749,7 +2966,7 @@ function runCommand(cmd, cwd, timeout) {
2749
2966
  return { success: true, output: "", timedOut: false };
2750
2967
  }
2751
2968
  try {
2752
- const output = execFileSync10(parts[0], parts.slice(1), {
2969
+ const output = execFileSync11(parts[0], parts.slice(1), {
2753
2970
  cwd,
2754
2971
  timeout,
2755
2972
  encoding: "utf-8",
@@ -2820,7 +3037,7 @@ var init_verify_runner = __esm({
2820
3037
  });
2821
3038
 
2822
3039
  // src/observer.ts
2823
- import { execFileSync as execFileSync11 } from "child_process";
3040
+ import { execFileSync as execFileSync12 } from "child_process";
2824
3041
  async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
2825
3042
  const context = [
2826
3043
  `Stage: ${stageName}`,
@@ -2880,13 +3097,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
2880
3097
  }
2881
3098
  function getModifiedFiles(projectDir) {
2882
3099
  try {
2883
- const staged = execFileSync11("git", ["diff", "--name-only", "--cached"], {
3100
+ const staged = execFileSync12("git", ["diff", "--name-only", "--cached"], {
2884
3101
  encoding: "utf-8",
2885
3102
  cwd: projectDir,
2886
3103
  timeout: 5e3,
2887
3104
  stdio: ["pipe", "pipe", "pipe"]
2888
3105
  }).trim();
2889
- const unstaged = execFileSync11("git", ["diff", "--name-only"], {
3106
+ const unstaged = execFileSync12("git", ["diff", "--name-only"], {
2890
3107
  encoding: "utf-8",
2891
3108
  cwd: projectDir,
2892
3109
  timeout: 5e3,
@@ -2929,8 +3146,8 @@ Error context:
2929
3146
  });
2930
3147
 
2931
3148
  // src/stages/gate.ts
2932
- import * as fs17 from "fs";
2933
- import * as path15 from "path";
3149
+ import * as fs18 from "fs";
3150
+ import * as path16 from "path";
2934
3151
  function executeGateStage(ctx, def) {
2935
3152
  if (ctx.input.dryRun) {
2936
3153
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -2973,7 +3190,7 @@ ${output}
2973
3190
  `);
2974
3191
  }
2975
3192
  }
2976
- fs17.writeFileSync(path15.join(ctx.taskDir, "verify.md"), lines.join(""));
3193
+ fs18.writeFileSync(path16.join(ctx.taskDir, "verify.md"), lines.join(""));
2977
3194
  return {
2978
3195
  outcome: verifyResult.pass ? "completed" : "failed",
2979
3196
  retries: 0
@@ -2988,9 +3205,9 @@ var init_gate = __esm({
2988
3205
  });
2989
3206
 
2990
3207
  // src/stages/verify.ts
2991
- import * as fs18 from "fs";
2992
- import * as path16 from "path";
2993
- import { execFileSync as execFileSync12 } from "child_process";
3208
+ import * as fs19 from "fs";
3209
+ import * as path17 from "path";
3210
+ import { execFileSync as execFileSync13 } from "child_process";
2994
3211
  async function executeVerifyWithAutofix(ctx, def) {
2995
3212
  const maxAttempts = def.maxRetries ?? 2;
2996
3213
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
@@ -3000,8 +3217,8 @@ async function executeVerifyWithAutofix(ctx, def) {
3000
3217
  return { ...gateResult, retries: attempt };
3001
3218
  }
3002
3219
  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";
3220
+ const verifyPath = path17.join(ctx.taskDir, "verify.md");
3221
+ const errorOutput = fs19.existsSync(verifyPath) ? fs19.readFileSync(verifyPath, "utf-8") : "Unknown error";
3005
3222
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
3006
3223
  const defaultRunner = getRunnerForStage(ctx, "taskify");
3007
3224
  const diagConfig = getProjectConfig();
@@ -3044,7 +3261,7 @@ ${diagnosis.resolution}`);
3044
3261
  const parts = parseCommand(cmd);
3045
3262
  if (parts.length === 0) return;
3046
3263
  try {
3047
- execFileSync12(parts[0], parts.slice(1), {
3264
+ execFileSync13(parts[0], parts.slice(1), {
3048
3265
  stdio: "pipe",
3049
3266
  timeout: FIX_COMMAND_TIMEOUT_MS
3050
3267
  });
@@ -3097,8 +3314,8 @@ var init_verify = __esm({
3097
3314
  });
3098
3315
 
3099
3316
  // src/review-standalone.ts
3100
- import * as fs19 from "fs";
3101
- import * as path17 from "path";
3317
+ import * as fs20 from "fs";
3318
+ import * as path18 from "path";
3102
3319
  function resolveReviewTarget(input) {
3103
3320
  if (input.prs.length === 0) {
3104
3321
  return {
@@ -3122,8 +3339,8 @@ Or comment on the specific PR: \`@kody review\``
3122
3339
  }
3123
3340
  async function runStandaloneReview(input) {
3124
3341
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
3125
- const taskDir = path17.join(input.projectDir, ".kody", "tasks", taskId);
3126
- fs19.mkdirSync(taskDir, { recursive: true });
3342
+ const taskDir = path18.join(input.projectDir, ".kody", "tasks", taskId);
3343
+ fs20.mkdirSync(taskDir, { recursive: true });
3127
3344
  let diffInstruction = "";
3128
3345
  let filesChangedSection = "";
3129
3346
  if (input.baseBranch) {
@@ -3150,7 +3367,7 @@ ${fileList}`;
3150
3367
  const taskContent = `# ${input.prTitle}
3151
3368
 
3152
3369
  ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
3153
- fs19.writeFileSync(path17.join(taskDir, "task.md"), taskContent);
3370
+ fs20.writeFileSync(path18.join(taskDir, "task.md"), taskContent);
3154
3371
  const reviewDef = STAGES.find((s) => s.name === "review");
3155
3372
  const ctx = {
3156
3373
  taskId,
@@ -3172,10 +3389,10 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
3172
3389
  error: result.error ?? "Review stage failed"
3173
3390
  };
3174
3391
  }
3175
- const reviewPath = path17.join(taskDir, "review.md");
3392
+ const reviewPath = path18.join(taskDir, "review.md");
3176
3393
  let reviewContent;
3177
- if (fs19.existsSync(reviewPath)) {
3178
- reviewContent = fs19.readFileSync(reviewPath, "utf-8");
3394
+ if (fs20.existsSync(reviewPath)) {
3395
+ reviewContent = fs20.readFileSync(reviewPath, "utf-8");
3179
3396
  }
3180
3397
  return {
3181
3398
  outcome: "completed",
@@ -3215,8 +3432,8 @@ var init_review_standalone = __esm({
3215
3432
  });
3216
3433
 
3217
3434
  // src/stages/review.ts
3218
- import * as fs20 from "fs";
3219
- import * as path18 from "path";
3435
+ import * as fs21 from "fs";
3436
+ import * as path19 from "path";
3220
3437
  async function executeReviewWithFix(ctx, def) {
3221
3438
  if (ctx.input.dryRun) {
3222
3439
  return { outcome: "completed", retries: 0 };
@@ -3230,11 +3447,11 @@ async function executeReviewWithFix(ctx, def) {
3230
3447
  if (reviewResult.outcome !== "completed") {
3231
3448
  return reviewResult;
3232
3449
  }
3233
- const reviewFile = path18.join(ctx.taskDir, "review.md");
3234
- if (!fs20.existsSync(reviewFile)) {
3450
+ const reviewFile = path19.join(ctx.taskDir, "review.md");
3451
+ if (!fs21.existsSync(reviewFile)) {
3235
3452
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
3236
3453
  }
3237
- const content = fs20.readFileSync(reviewFile, "utf-8");
3454
+ const content = fs21.readFileSync(reviewFile, "utf-8");
3238
3455
  if (detectReviewVerdict(content) !== "fail") {
3239
3456
  return { ...reviewResult, retries: iteration };
3240
3457
  }
@@ -3263,15 +3480,15 @@ var init_review = __esm({
3263
3480
  });
3264
3481
 
3265
3482
  // src/stages/ship.ts
3266
- import * as fs21 from "fs";
3267
- import * as path19 from "path";
3268
- import { execFileSync as execFileSync13 } from "child_process";
3483
+ import * as fs22 from "fs";
3484
+ import * as path20 from "path";
3485
+ import { execFileSync as execFileSync14 } from "child_process";
3269
3486
  function buildPrBody(ctx) {
3270
3487
  const sections = [];
3271
- const taskJsonPath = path19.join(ctx.taskDir, "task.json");
3272
- if (fs21.existsSync(taskJsonPath)) {
3488
+ const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3489
+ if (fs22.existsSync(taskJsonPath)) {
3273
3490
  try {
3274
- const raw = fs21.readFileSync(taskJsonPath, "utf-8");
3491
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
3275
3492
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3276
3493
  const task = JSON.parse(cleaned);
3277
3494
  if (task.description) {
@@ -3290,9 +3507,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
3290
3507
  } catch {
3291
3508
  }
3292
3509
  }
3293
- const reviewPath = path19.join(ctx.taskDir, "review.md");
3294
- if (fs21.existsSync(reviewPath)) {
3295
- const review = fs21.readFileSync(reviewPath, "utf-8");
3510
+ const reviewPath = path20.join(ctx.taskDir, "review.md");
3511
+ if (fs22.existsSync(reviewPath)) {
3512
+ const review = fs22.readFileSync(reviewPath, "utf-8");
3296
3513
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
3297
3514
  if (summaryMatch) {
3298
3515
  const summary = summaryMatch[1].trim();
@@ -3309,14 +3526,14 @@ ${summary}`);
3309
3526
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
3310
3527
  }
3311
3528
  }
3312
- const verifyPath = path19.join(ctx.taskDir, "verify.md");
3313
- if (fs21.existsSync(verifyPath)) {
3314
- const verify = fs21.readFileSync(verifyPath, "utf-8");
3529
+ const verifyPath = path20.join(ctx.taskDir, "verify.md");
3530
+ if (fs22.existsSync(verifyPath)) {
3531
+ const verify = fs22.readFileSync(verifyPath, "utf-8");
3315
3532
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
3316
3533
  }
3317
- const planPath = path19.join(ctx.taskDir, "plan.md");
3318
- if (fs21.existsSync(planPath)) {
3319
- const plan = fs21.readFileSync(planPath, "utf-8").trim();
3534
+ const planPath = path20.join(ctx.taskDir, "plan.md");
3535
+ if (fs22.existsSync(planPath)) {
3536
+ const plan = fs22.readFileSync(planPath, "utf-8").trim();
3320
3537
  if (plan) {
3321
3538
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
3322
3539
  sections.push(`
@@ -3336,25 +3553,25 @@ Closes #${ctx.input.issueNumber}`);
3336
3553
  return sections.join("\n");
3337
3554
  }
3338
3555
  function executeShipStage(ctx, _def) {
3339
- const shipPath = path19.join(ctx.taskDir, "ship.md");
3556
+ const shipPath = path20.join(ctx.taskDir, "ship.md");
3340
3557
  if (ctx.input.dryRun) {
3341
- fs21.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
3558
+ fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
3342
3559
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3343
3560
  }
3344
3561
  if (ctx.input.local && !ctx.input.issueNumber) {
3345
- fs21.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
3562
+ fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
3346
3563
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3347
3564
  }
3348
3565
  try {
3349
3566
  const head = getCurrentBranch(ctx.projectDir);
3350
3567
  const base = getDefaultBranch(ctx.projectDir);
3351
3568
  try {
3352
- execFileSync13("git", ["add", ctx.taskDir], {
3569
+ execFileSync14("git", ["add", ctx.taskDir], {
3353
3570
  cwd: ctx.projectDir,
3354
3571
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
3355
3572
  stdio: "pipe"
3356
3573
  });
3357
- execFileSync13("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
3574
+ execFileSync14("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
3358
3575
  cwd: ctx.projectDir,
3359
3576
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
3360
3577
  stdio: "pipe"
@@ -3368,7 +3585,7 @@ function executeShipStage(ctx, _def) {
3368
3585
  let repo = config.github?.repo;
3369
3586
  if (!owner || !repo) {
3370
3587
  try {
3371
- const remoteUrl = execFileSync13("git", ["remote", "get-url", "origin"], {
3588
+ const remoteUrl = execFileSync14("git", ["remote", "get-url", "origin"], {
3372
3589
  encoding: "utf-8",
3373
3590
  cwd: ctx.projectDir
3374
3591
  }).trim();
@@ -3389,28 +3606,28 @@ function executeShipStage(ctx, _def) {
3389
3606
  chore: "chore"
3390
3607
  };
3391
3608
  let prefix = "chore";
3392
- const taskJsonPath = path19.join(ctx.taskDir, "task.json");
3393
- if (fs21.existsSync(taskJsonPath)) {
3609
+ const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3610
+ if (fs22.existsSync(taskJsonPath)) {
3394
3611
  try {
3395
- const raw = fs21.readFileSync(taskJsonPath, "utf-8");
3612
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
3396
3613
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3397
3614
  const task = JSON.parse(cleaned);
3398
3615
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
3399
3616
  } catch {
3400
3617
  }
3401
3618
  }
3402
- const taskMdPath = path19.join(ctx.taskDir, "task.md");
3403
- if (fs21.existsSync(taskMdPath)) {
3404
- const content = fs21.readFileSync(taskMdPath, "utf-8");
3619
+ const taskMdPath = path20.join(ctx.taskDir, "task.md");
3620
+ if (fs22.existsSync(taskMdPath)) {
3621
+ const content = fs22.readFileSync(taskMdPath, "utf-8");
3405
3622
  const heading = content.split("\n").find((l) => l.startsWith("# "));
3406
3623
  if (heading) {
3407
3624
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
3408
3625
  }
3409
3626
  }
3410
3627
  if (title === "Update") {
3411
- if (fs21.existsSync(taskJsonPath)) {
3628
+ if (fs22.existsSync(taskJsonPath)) {
3412
3629
  try {
3413
- const raw = fs21.readFileSync(taskJsonPath, "utf-8");
3630
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
3414
3631
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3415
3632
  const task = JSON.parse(cleaned);
3416
3633
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -3433,7 +3650,7 @@ function executeShipStage(ctx, _def) {
3433
3650
  } catch {
3434
3651
  }
3435
3652
  }
3436
- fs21.writeFileSync(shipPath, `# Ship
3653
+ fs22.writeFileSync(shipPath, `# Ship
3437
3654
 
3438
3655
  Updated existing PR: ${existingPr.url}
3439
3656
  PR #${existingPr.number}
@@ -3454,20 +3671,20 @@ PR #${existingPr.number}
3454
3671
  } catch {
3455
3672
  }
3456
3673
  }
3457
- fs21.writeFileSync(shipPath, `# Ship
3674
+ fs22.writeFileSync(shipPath, `# Ship
3458
3675
 
3459
3676
  PR created: ${pr.url}
3460
3677
  PR #${pr.number}
3461
3678
  `);
3462
3679
  } else {
3463
- fs21.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3680
+ fs22.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3464
3681
  }
3465
3682
  }
3466
3683
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3467
3684
  } catch (err) {
3468
3685
  const msg = err instanceof Error ? err.message : String(err);
3469
3686
  try {
3470
- fs21.writeFileSync(shipPath, `# Ship
3687
+ fs22.writeFileSync(shipPath, `# Ship
3471
3688
 
3472
3689
  Failed: ${msg}
3473
3690
  `);
@@ -3516,15 +3733,15 @@ var init_executor_registry = __esm({
3516
3733
  });
3517
3734
 
3518
3735
  // src/pipeline/questions.ts
3519
- import * as fs22 from "fs";
3520
- import * as path20 from "path";
3736
+ import * as fs23 from "fs";
3737
+ import * as path21 from "path";
3521
3738
  function checkForQuestions(ctx, stageName) {
3522
3739
  if (ctx.input.local || !ctx.input.issueNumber) return false;
3523
3740
  try {
3524
3741
  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");
3742
+ const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3743
+ if (!fs23.existsSync(taskJsonPath)) return false;
3744
+ const raw = fs23.readFileSync(taskJsonPath, "utf-8");
3528
3745
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3529
3746
  const taskJson = JSON.parse(cleaned);
3530
3747
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -3539,9 +3756,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
3539
3756
  }
3540
3757
  }
3541
3758
  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");
3759
+ const planPath = path21.join(ctx.taskDir, "plan.md");
3760
+ if (!fs23.existsSync(planPath)) return false;
3761
+ const plan = fs23.readFileSync(planPath, "utf-8");
3545
3762
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
3546
3763
  if (questionsMatch) {
3547
3764
  const questionsText = questionsMatch[1].trim();
@@ -3570,8 +3787,8 @@ var init_questions = __esm({
3570
3787
  });
3571
3788
 
3572
3789
  // src/pipeline/hooks.ts
3573
- import * as fs23 from "fs";
3574
- import * as path21 from "path";
3790
+ import * as fs24 from "fs";
3791
+ import * as path22 from "path";
3575
3792
  function applyPreStageLabel(ctx, def) {
3576
3793
  if (!ctx.input.issueNumber || ctx.input.local) return;
3577
3794
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
@@ -3609,9 +3826,9 @@ function autoDetectComplexity(ctx, def) {
3609
3826
  return { complexity, activeStages };
3610
3827
  }
3611
3828
  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");
3829
+ const taskJsonPath = path22.join(ctx.taskDir, "task.json");
3830
+ if (!fs24.existsSync(taskJsonPath)) return null;
3831
+ const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3615
3832
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3616
3833
  const taskJson = JSON.parse(cleaned);
3617
3834
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -3641,8 +3858,8 @@ function checkRiskGate(ctx, def, state, complexity) {
3641
3858
  if (ctx.input.dryRun || ctx.input.local) return null;
3642
3859
  if (ctx.input.mode === "rerun") return null;
3643
3860
  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)";
3861
+ const planPath = path22.join(ctx.taskDir, "plan.md");
3862
+ const plan = fs24.existsSync(planPath) ? fs24.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
3646
3863
  try {
3647
3864
  postComment(
3648
3865
  ctx.input.issueNumber,
@@ -3709,22 +3926,22 @@ var init_hooks = __esm({
3709
3926
  });
3710
3927
 
3711
3928
  // src/learning/auto-learn.ts
3712
- import * as fs24 from "fs";
3713
- import * as path22 from "path";
3929
+ import * as fs25 from "fs";
3930
+ import * as path23 from "path";
3714
3931
  function stripAnsi(str) {
3715
3932
  return str.replace(/\x1b\[[0-9;]*m/g, "");
3716
3933
  }
3717
3934
  function autoLearn(ctx) {
3718
3935
  try {
3719
- const memoryDir = path22.join(ctx.projectDir, ".kody", "memory");
3720
- if (!fs24.existsSync(memoryDir)) {
3721
- fs24.mkdirSync(memoryDir, { recursive: true });
3936
+ const memoryDir = path23.join(ctx.projectDir, ".kody", "memory");
3937
+ if (!fs25.existsSync(memoryDir)) {
3938
+ fs25.mkdirSync(memoryDir, { recursive: true });
3722
3939
  }
3723
3940
  const learnings = [];
3724
3941
  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"));
3942
+ const verifyPath = path23.join(ctx.taskDir, "verify.md");
3943
+ if (fs25.existsSync(verifyPath)) {
3944
+ const verify = stripAnsi(fs25.readFileSync(verifyPath, "utf-8"));
3728
3945
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
3729
3946
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
3730
3947
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -3733,18 +3950,18 @@ function autoLearn(ctx) {
3733
3950
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
3734
3951
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
3735
3952
  }
3736
- const reviewPath = path22.join(ctx.taskDir, "review.md");
3737
- if (fs24.existsSync(reviewPath)) {
3738
- const review = fs24.readFileSync(reviewPath, "utf-8");
3953
+ const reviewPath = path23.join(ctx.taskDir, "review.md");
3954
+ if (fs25.existsSync(reviewPath)) {
3955
+ const review = fs25.readFileSync(reviewPath, "utf-8");
3739
3956
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
3740
3957
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
3741
3958
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
3742
3959
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
3743
3960
  }
3744
- const taskJsonPath = path22.join(ctx.taskDir, "task.json");
3745
- if (fs24.existsSync(taskJsonPath)) {
3961
+ const taskJsonPath = path23.join(ctx.taskDir, "task.json");
3962
+ if (fs25.existsSync(taskJsonPath)) {
3746
3963
  try {
3747
- const raw = stripAnsi(fs24.readFileSync(taskJsonPath, "utf-8"));
3964
+ const raw = stripAnsi(fs25.readFileSync(taskJsonPath, "utf-8"));
3748
3965
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3749
3966
  const task = JSON.parse(cleaned);
3750
3967
  if (task.scope && Array.isArray(task.scope)) {
@@ -3755,12 +3972,12 @@ function autoLearn(ctx) {
3755
3972
  }
3756
3973
  }
3757
3974
  if (learnings.length > 0) {
3758
- const conventionsPath = path22.join(memoryDir, "conventions.md");
3975
+ const conventionsPath = path23.join(memoryDir, "conventions.md");
3759
3976
  const entry = `
3760
3977
  ## Learned ${timestamp2} (task: ${ctx.taskId})
3761
3978
  ${learnings.join("\n")}
3762
3979
  `;
3763
- fs24.appendFileSync(conventionsPath, entry);
3980
+ fs25.appendFileSync(conventionsPath, entry);
3764
3981
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
3765
3982
  }
3766
3983
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
@@ -3768,8 +3985,8 @@ ${learnings.join("\n")}
3768
3985
  }
3769
3986
  }
3770
3987
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3771
- const archPath = path22.join(memoryDir, "architecture.md");
3772
- if (fs24.existsSync(archPath)) return;
3988
+ const archPath = path23.join(memoryDir, "architecture.md");
3989
+ if (fs25.existsSync(archPath)) return;
3773
3990
  const detected = detectArchitectureBasic(projectDir);
3774
3991
  if (detected.length > 0) {
3775
3992
  const content = `# Architecture (auto-detected ${timestamp2})
@@ -3777,7 +3994,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3777
3994
  ## Overview
3778
3995
  ${detected.join("\n")}
3779
3996
  `;
3780
- fs24.writeFileSync(archPath, content);
3997
+ fs25.writeFileSync(archPath, content);
3781
3998
  logger.info(`Auto-detected architecture (${detected.length} items)`);
3782
3999
  }
3783
4000
  }
@@ -3790,13 +4007,13 @@ var init_auto_learn = __esm({
3790
4007
  });
3791
4008
 
3792
4009
  // src/retrospective.ts
3793
- import * as fs25 from "fs";
3794
- import * as path23 from "path";
4010
+ import * as fs26 from "fs";
4011
+ import * as path24 from "path";
3795
4012
  function readArtifact(taskDir, filename, maxChars) {
3796
- const p = path23.join(taskDir, filename);
3797
- if (!fs25.existsSync(p)) return null;
4013
+ const p = path24.join(taskDir, filename);
4014
+ if (!fs26.existsSync(p)) return null;
3798
4015
  try {
3799
- const content = fs25.readFileSync(p, "utf-8");
4016
+ const content = fs26.readFileSync(p, "utf-8");
3800
4017
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
3801
4018
  } catch {
3802
4019
  return null;
@@ -3849,13 +4066,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
3849
4066
  return lines.join("\n");
3850
4067
  }
3851
4068
  function getLogPath(projectDir) {
3852
- return path23.join(projectDir, ".kody", "memory", "observer-log.jsonl");
4069
+ return path24.join(projectDir, ".kody", "memory", "observer-log.jsonl");
3853
4070
  }
3854
4071
  function readPreviousRetrospectives(projectDir, limit = 10) {
3855
4072
  const logPath = getLogPath(projectDir);
3856
- if (!fs25.existsSync(logPath)) return [];
4073
+ if (!fs26.existsSync(logPath)) return [];
3857
4074
  try {
3858
- const content = fs25.readFileSync(logPath, "utf-8");
4075
+ const content = fs26.readFileSync(logPath, "utf-8");
3859
4076
  const lines = content.split("\n").filter(Boolean);
3860
4077
  const entries = [];
3861
4078
  const start = Math.max(0, lines.length - limit);
@@ -3882,11 +4099,11 @@ function formatPreviousEntries(entries) {
3882
4099
  }
3883
4100
  function appendRetrospectiveEntry(projectDir, entry) {
3884
4101
  const logPath = getLogPath(projectDir);
3885
- const dir = path23.dirname(logPath);
3886
- if (!fs25.existsSync(dir)) {
3887
- fs25.mkdirSync(dir, { recursive: true });
4102
+ const dir = path24.dirname(logPath);
4103
+ if (!fs26.existsSync(dir)) {
4104
+ fs26.mkdirSync(dir, { recursive: true });
3888
4105
  }
3889
- fs25.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4106
+ fs26.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3890
4107
  }
3891
4108
  async function runRetrospective(ctx, state, pipelineStartTime) {
3892
4109
  if (ctx.input.dryRun) return;
@@ -4054,8 +4271,8 @@ var init_summary = __esm({
4054
4271
  });
4055
4272
 
4056
4273
  // src/pipeline.ts
4057
- import * as fs26 from "fs";
4058
- import * as path24 from "path";
4274
+ import * as fs27 from "fs";
4275
+ import * as path25 from "path";
4059
4276
  function ensureFeatureBranchIfNeeded(ctx) {
4060
4277
  if (ctx.input.dryRun) return;
4061
4278
  if (ctx.input.prNumber) {
@@ -4068,8 +4285,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
4068
4285
  }
4069
4286
  if (!ctx.input.issueNumber) return;
4070
4287
  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;
4288
+ const taskMdPath = path25.join(ctx.taskDir, "task.md");
4289
+ const title = fs27.existsSync(taskMdPath) ? fs27.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
4073
4290
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
4074
4291
  syncWithDefault(ctx.projectDir);
4075
4292
  } catch (err) {
@@ -4083,10 +4300,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
4083
4300
  }
4084
4301
  }
4085
4302
  function acquireLock(taskDir) {
4086
- const lockPath = path24.join(taskDir, ".lock");
4087
- if (fs26.existsSync(lockPath)) {
4303
+ const lockPath = path25.join(taskDir, ".lock");
4304
+ if (fs27.existsSync(lockPath)) {
4088
4305
  try {
4089
- const pid = parseInt(fs26.readFileSync(lockPath, "utf-8").trim(), 10);
4306
+ const pid = parseInt(fs27.readFileSync(lockPath, "utf-8").trim(), 10);
4090
4307
  if (!isNaN(pid)) {
4091
4308
  try {
4092
4309
  process.kill(pid, 0);
@@ -4103,14 +4320,14 @@ function acquireLock(taskDir) {
4103
4320
  logger.warn(` Corrupt lock file \u2014 overwriting`);
4104
4321
  }
4105
4322
  try {
4106
- fs26.unlinkSync(lockPath);
4323
+ fs27.unlinkSync(lockPath);
4107
4324
  } catch {
4108
4325
  }
4109
4326
  }
4110
4327
  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);
4328
+ const fd = fs27.openSync(lockPath, fs27.constants.O_WRONLY | fs27.constants.O_CREAT | fs27.constants.O_EXCL);
4329
+ fs27.writeSync(fd, String(process.pid));
4330
+ fs27.closeSync(fd);
4114
4331
  } catch (err) {
4115
4332
  if (err.code === "EEXIST") {
4116
4333
  throw new Error("Pipeline already running (lock acquired by another process)");
@@ -4120,7 +4337,7 @@ function acquireLock(taskDir) {
4120
4337
  }
4121
4338
  function releaseLock(taskDir) {
4122
4339
  try {
4123
- fs26.unlinkSync(path24.join(taskDir, ".lock"));
4340
+ fs27.unlinkSync(path25.join(taskDir, ".lock"));
4124
4341
  } catch {
4125
4342
  }
4126
4343
  }
@@ -4328,8 +4545,8 @@ var init_pipeline = __esm({
4328
4545
  });
4329
4546
 
4330
4547
  // src/preflight.ts
4331
- import { execFileSync as execFileSync14 } from "child_process";
4332
- import * as fs27 from "fs";
4548
+ import { execFileSync as execFileSync15 } from "child_process";
4549
+ import * as fs28 from "fs";
4333
4550
  function check(name, fn) {
4334
4551
  try {
4335
4552
  const detail = fn() ?? void 0;
@@ -4341,7 +4558,7 @@ function check(name, fn) {
4341
4558
  function runPreflight() {
4342
4559
  const checks = [
4343
4560
  check("claude CLI", () => {
4344
- const v = execFileSync14("claude", ["--version"], {
4561
+ const v = execFileSync15("claude", ["--version"], {
4345
4562
  encoding: "utf-8",
4346
4563
  timeout: 1e4,
4347
4564
  stdio: ["pipe", "pipe", "pipe"]
@@ -4349,14 +4566,14 @@ function runPreflight() {
4349
4566
  return v;
4350
4567
  }),
4351
4568
  check("git repo", () => {
4352
- execFileSync14("git", ["rev-parse", "--is-inside-work-tree"], {
4569
+ execFileSync15("git", ["rev-parse", "--is-inside-work-tree"], {
4353
4570
  encoding: "utf-8",
4354
4571
  timeout: 5e3,
4355
4572
  stdio: ["pipe", "pipe", "pipe"]
4356
4573
  });
4357
4574
  }),
4358
4575
  check("pnpm", () => {
4359
- const v = execFileSync14("pnpm", ["--version"], {
4576
+ const v = execFileSync15("pnpm", ["--version"], {
4360
4577
  encoding: "utf-8",
4361
4578
  timeout: 5e3,
4362
4579
  stdio: ["pipe", "pipe", "pipe"]
@@ -4364,7 +4581,7 @@ function runPreflight() {
4364
4581
  return v;
4365
4582
  }),
4366
4583
  check("node >= 18", () => {
4367
- const v = execFileSync14("node", ["--version"], {
4584
+ const v = execFileSync15("node", ["--version"], {
4368
4585
  encoding: "utf-8",
4369
4586
  timeout: 5e3,
4370
4587
  stdio: ["pipe", "pipe", "pipe"]
@@ -4374,7 +4591,7 @@ function runPreflight() {
4374
4591
  return v;
4375
4592
  }),
4376
4593
  check("gh CLI", () => {
4377
- const v = execFileSync14("gh", ["--version"], {
4594
+ const v = execFileSync15("gh", ["--version"], {
4378
4595
  encoding: "utf-8",
4379
4596
  timeout: 5e3,
4380
4597
  stdio: ["pipe", "pipe", "pipe"]
@@ -4382,7 +4599,7 @@ function runPreflight() {
4382
4599
  return v;
4383
4600
  }),
4384
4601
  check("package.json", () => {
4385
- if (!fs27.existsSync("package.json")) throw new Error("not found");
4602
+ if (!fs28.existsSync("package.json")) throw new Error("not found");
4386
4603
  })
4387
4604
  ];
4388
4605
  const failed = checks.filter((c) => !c.ok);
@@ -4458,180 +4675,6 @@ var init_args = __esm({
4458
4675
  }
4459
4676
  });
4460
4677
 
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
4678
  // src/cli/task-state.ts
4636
4679
  import * as fs29 from "fs";
4637
4680
  import * as path26 from "path";