@kody-ade/kody-engine-lite 0.1.117 → 0.1.118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.js CHANGED
@@ -180,8 +180,8 @@ var init_validators = __esm({
180
180
  });
181
181
 
182
182
  // src/config.ts
183
- import * as fs8 from "fs";
184
- import * as path7 from "path";
183
+ import * as fs7 from "fs";
184
+ import * as path6 from "path";
185
185
  function resolveStageConfig(config, stageName, modelTier) {
186
186
  const stageOverride = config.agent.stages?.[stageName];
187
187
  if (stageOverride) return stageOverride;
@@ -214,7 +214,9 @@ function getLitellmUrl() {
214
214
  return LITELLM_DEFAULT_URL;
215
215
  }
216
216
  function providerApiKeyEnvVar(provider) {
217
- if (provider === "anthropic") return "ANTHROPIC_API_KEY";
217
+ if (provider === "anthropic" || provider === "claude") return "ANTHROPIC_API_KEY";
218
+ const derived = `${provider.toUpperCase()}_API_KEY`;
219
+ if (process.env[derived]) return derived;
218
220
  return "ANTHROPIC_COMPATIBLE_API_KEY";
219
221
  }
220
222
  function setConfigDir(dir) {
@@ -223,16 +225,16 @@ function setConfigDir(dir) {
223
225
  }
224
226
  function getProjectConfig() {
225
227
  if (_config) return _config;
226
- const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
227
- if (fs8.existsSync(configPath)) {
228
+ const configPath = path6.join(_configDir ?? process.cwd(), "kody.config.json");
229
+ if (fs7.existsSync(configPath)) {
228
230
  try {
229
- const result = parseJsonSafe(fs8.readFileSync(configPath, "utf-8"));
230
- if (!result.ok) {
231
- logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
231
+ const result2 = parseJsonSafe(fs7.readFileSync(configPath, "utf-8"));
232
+ if (!result2.ok) {
233
+ logger.warn(`kody.config.json: ${result2.error} \u2014 using defaults`);
232
234
  _config = { ...DEFAULT_CONFIG };
233
235
  return _config;
234
236
  }
235
- const raw = result.data;
237
+ const raw = result2.data;
236
238
  _config = {
237
239
  quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
238
240
  git: { ...DEFAULT_CONFIG.git, ...raw.git },
@@ -283,7 +285,7 @@ var init_config = __esm({
283
285
  repo: ""
284
286
  },
285
287
  agent: {
286
- modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
288
+ modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
287
289
  },
288
290
  contextTiers: {
289
291
  enabled: true,
@@ -389,10 +391,11 @@ function createClaudeCodeRunner() {
389
391
  model,
390
392
  "--dangerously-skip-permissions"
391
393
  ];
394
+ const baseTools = "Bash,Edit,Read,Write,Glob,Grep";
392
395
  if (options?.mcpConfigJson) {
393
396
  args2.push("--mcp-config", options.mcpConfigJson);
394
397
  } else {
395
- args2.push("--allowedTools", "Bash,Edit,Read,Write,Glob,Grep");
398
+ args2.push("--allowedTools", baseTools);
396
399
  }
397
400
  if (options?.sessionId) {
398
401
  if (options.resumeSession) {
@@ -965,8 +968,8 @@ function findLatestTaskForIssue(issueNumber, projectDir) {
965
968
  }
966
969
  function generateTaskId() {
967
970
  const now = /* @__PURE__ */ new Date();
968
- const pad = (n) => String(n).padStart(2, "0");
969
- return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
971
+ const pad2 = (n) => String(n).padStart(2, "0");
972
+ return `${String(now.getFullYear()).slice(2)}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
970
973
  }
971
974
  function resolveTaskIdFromComments(issueNumber) {
972
975
  try {
@@ -1192,6 +1195,7 @@ var init_litellm = __esm({
1192
1195
  // src/cli/taskify-command.ts
1193
1196
  var taskify_command_exports = {};
1194
1197
  __export(taskify_command_exports, {
1198
+ TaskifyError: () => TaskifyError,
1195
1199
  isTaskifyRun: () => isTaskifyRun,
1196
1200
  readTaskifyMarker: () => readTaskifyMarker,
1197
1201
  runTaskifyCommand: () => runTaskifyCommand,
@@ -1253,8 +1257,8 @@ async function runTaskifyCommand() {
1253
1257
  const local = hasFlag(args2, "--local") || !process.env.CI;
1254
1258
  const taskIdArg = getArg(args2, "--task-id") ?? process.env.TASK_ID;
1255
1259
  const taskId = taskIdArg ?? (issueNumber ? `taskify-${issueNumber}-${generateTaskId()}` : `taskify-${generateTaskId()}`);
1256
- if (!ticketId && !prdFile) {
1257
- logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md>");
1260
+ if (!ticketId && !prdFile && !issueNumber) {
1261
+ logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md> OR kody taskify --issue-number <n>");
1258
1262
  process.exit(1);
1259
1263
  }
1260
1264
  if (prdFile && !fs11.existsSync(prdFile)) {
@@ -1283,23 +1287,31 @@ async function runTaskifyCommand() {
1283
1287
  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
1284
1288
  };
1285
1289
  }
1286
- await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1287
- litellmProcess?.kill();
1290
+ try {
1291
+ await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1292
+ } catch (err) {
1293
+ if (err instanceof TaskifyError) {
1294
+ logger.error(`[taskify] ${err.message}`);
1295
+ process.exit(1);
1296
+ }
1297
+ throw err;
1298
+ } finally {
1299
+ litellmProcess?.kill();
1300
+ }
1288
1301
  }
1289
1302
  async function taskifyCommand(opts) {
1290
1303
  const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
1291
1304
  const config = getProjectConfig();
1292
1305
  const taskDir = path10.join(projectDir, ".kody", "tasks", taskId);
1293
1306
  fs11.mkdirSync(taskDir, { recursive: true });
1294
- const mode = prdFile ? "file" : "ticket";
1295
- logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile} issue=${issueNumber ?? "none"} task=${taskId}`);
1307
+ const mode = prdFile ? "file" : ticketId ? "ticket" : "issue";
1308
+ logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile ?? `issue#${issueNumber}`} issue=${issueNumber ?? "none"} task=${taskId}`);
1296
1309
  let mcpConfigJson;
1297
1310
  if (mode === "ticket") {
1298
1311
  try {
1299
1312
  mcpConfigJson = buildTaskifyMcpConfigJson(config);
1300
1313
  } catch (err) {
1301
1314
  const msg = err instanceof Error ? err.message : String(err);
1302
- logger.error(`[taskify] MCP config error: ${msg}`);
1303
1315
  if (issueNumber && !local) {
1304
1316
  postComment(
1305
1317
  issueNumber,
@@ -1310,12 +1322,24 @@ async function taskifyCommand(opts) {
1310
1322
  Add the required MCP server config to \`kody.config.json\` and try again.`
1311
1323
  );
1312
1324
  }
1313
- process.exit(1);
1325
+ throw new TaskifyError(`MCP config error: ${msg}`);
1314
1326
  }
1315
1327
  }
1316
1328
  const sc = resolveStageConfig(config, "taskify", "strong");
1317
1329
  const model = sc.model;
1318
1330
  const fileContent = prdFile ? fs11.readFileSync(prdFile, "utf-8") : void 0;
1331
+ let issueBody;
1332
+ if (mode === "issue" && issueNumber) {
1333
+ const issue = getIssue(issueNumber);
1334
+ if (issue) {
1335
+ issueBody = `# ${issue.title}
1336
+
1337
+ ${issue.body}`;
1338
+ logger.info(` Fetched issue #${issueNumber} body (${issueBody.length} chars)`);
1339
+ } else {
1340
+ throw new TaskifyError(`Could not fetch issue #${issueNumber}`);
1341
+ }
1342
+ }
1319
1343
  let projectContext;
1320
1344
  {
1321
1345
  const parts = [];
@@ -1339,9 +1363,9 @@ ${lines.join("\n")}
1339
1363
  }
1340
1364
  if (parts.length > 0) projectContext = parts.join("\n\n");
1341
1365
  }
1342
- const prompt = buildPrompt({ ticketId, fileContent, taskDir, feedback, projectContext });
1366
+ const prompt = buildPrompt({ ticketId, fileContent, issueBody, taskDir, feedback, projectContext });
1343
1367
  if (issueNumber && !local) {
1344
- const src = mode === "file" ? `file \`${path10.basename(prdFile)}\`` : `ticket **${ticketId}**`;
1368
+ const src = mode === "file" ? `file \`${path10.basename(prdFile)}\`` : mode === "ticket" ? `ticket **${ticketId}**` : `issue #${issueNumber} description`;
1345
1369
  const runUrl = process.env.RUN_URL ? ` ([logs](${process.env.RUN_URL}))` : "";
1346
1370
  postComment(issueNumber, `\u{1F680} Kody pipeline started: \`${taskId}\`${runUrl}
1347
1371
 
@@ -1351,61 +1375,57 @@ Kody is decomposing ${src} into tasks...`);
1351
1375
  fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1352
1376
  const runner = opts.runner ?? createClaudeCodeRunner();
1353
1377
  logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
1354
- const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
1378
+ const result2 = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
1355
1379
  cwd: projectDir,
1356
1380
  mcpConfigJson,
1357
1381
  env: opts.runnerEnv
1358
1382
  });
1359
- if (result.outcome !== "completed") {
1360
- const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
1361
- logger.error(`[taskify] ${errMsg}`);
1383
+ if (result2.outcome !== "completed") {
1384
+ const errMsg = result2.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result2.error}`;
1362
1385
  if (issueNumber && !local) {
1363
1386
  postComment(issueNumber, `Kody taskify failed:
1364
1387
 
1365
1388
  > ${errMsg}`);
1366
1389
  setLifecycleLabel(issueNumber, "failed");
1367
1390
  }
1368
- process.exit(1);
1391
+ throw new TaskifyError(errMsg);
1369
1392
  }
1370
1393
  const resultPath = path10.join(taskDir, RESULT_FILE);
1371
1394
  if (!fs11.existsSync(resultPath)) {
1372
1395
  const errMsg = `Claude did not write ${RESULT_FILE}. Output:
1373
1396
 
1374
- ${result.output?.slice(0, 500) ?? "(none)"}`;
1375
- logger.error(`[taskify] ${errMsg}`);
1397
+ ${result2.output?.slice(0, 500) ?? "(none)"}`;
1376
1398
  if (issueNumber && !local) {
1377
1399
  postComment(issueNumber, `Kody taskify failed: result file not found.
1378
1400
 
1379
1401
  ${errMsg}`);
1380
1402
  setLifecycleLabel(issueNumber, "failed");
1381
1403
  }
1382
- process.exit(1);
1404
+ throw new TaskifyError(errMsg);
1383
1405
  }
1384
1406
  let parsed;
1385
1407
  try {
1386
1408
  parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
1387
1409
  } catch {
1388
1410
  const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
1389
- logger.error(`[taskify] ${errMsg}`);
1390
1411
  if (issueNumber && !local) {
1391
1412
  postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1392
1413
  setLifecycleLabel(issueNumber, "failed");
1393
1414
  }
1394
- process.exit(1);
1415
+ throw new TaskifyError(errMsg);
1395
1416
  }
1396
- const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
1417
+ const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : issueNumber ? `issue #${issueNumber}` : "spec");
1397
1418
  if (parsed.status === "questions") {
1398
1419
  handleQuestions(parsed, sourceLabel, issueNumber, local ?? false);
1399
1420
  } else if (parsed.status === "ready") {
1400
1421
  await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
1401
1422
  } else {
1402
1423
  const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
1403
- logger.error(`[taskify] ${errMsg}`);
1404
1424
  if (issueNumber && !local) {
1405
1425
  postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1406
1426
  setLifecycleLabel(issueNumber, "failed");
1407
1427
  }
1408
- process.exit(1);
1428
+ throw new TaskifyError(errMsg);
1409
1429
  }
1410
1430
  }
1411
1431
  function handleQuestions(parsed, ticketId, issueNumber, local) {
@@ -1491,7 +1511,7 @@ ${links}${triggerNote}`
1491
1511
  }
1492
1512
  }
1493
1513
  function buildPrompt(opts) {
1494
- const { ticketId, fileContent, taskDir, feedback, projectContext } = opts;
1514
+ const { ticketId, fileContent, issueBody, taskDir, feedback, projectContext } = opts;
1495
1515
  const scriptDir = new URL(".", import.meta.url).pathname;
1496
1516
  const candidates = [
1497
1517
  path10.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
@@ -1520,6 +1540,7 @@ function buildPrompt(opts) {
1520
1540
  resolveBlock("PROJECT_CONTEXT", projectContext);
1521
1541
  resolveBlock("TICKET_ID", ticketId);
1522
1542
  resolveBlock("FILE_CONTENT", fileContent);
1543
+ resolveBlock("ISSUE_BODY", issueBody);
1523
1544
  resolveBlock("FEEDBACK", feedback);
1524
1545
  template = template.replace(/\{\{TASK_DIR\}\}/g, taskDir);
1525
1546
  return template;
@@ -1536,7 +1557,7 @@ function readTaskifyMarker(taskDir) {
1536
1557
  return null;
1537
1558
  }
1538
1559
  }
1539
- var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
1560
+ var __dirname, TaskifyError, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
1540
1561
  var init_taskify_command = __esm({
1541
1562
  "src/cli/taskify-command.ts"() {
1542
1563
  "use strict";
@@ -1548,6 +1569,12 @@ var init_taskify_command = __esm({
1548
1569
  init_task_resolution();
1549
1570
  init_litellm();
1550
1571
  __dirname = path10.dirname(fileURLToPath(import.meta.url));
1572
+ TaskifyError = class extends Error {
1573
+ constructor(message) {
1574
+ super(message);
1575
+ this.name = "TaskifyError";
1576
+ }
1577
+ };
1551
1578
  AUTO_TRIGGER_THRESHOLD = 5;
1552
1579
  MAX_TASKS_GUARD = 20;
1553
1580
  TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -1556,6 +1583,1161 @@ var init_taskify_command = __esm({
1556
1583
  }
1557
1584
  });
1558
1585
 
1586
+ // src/cli/test-model-tests.ts
1587
+ import * as fs12 from "fs";
1588
+ import * as os2 from "os";
1589
+ import * as path11 from "path";
1590
+ import * as zlib from "zlib";
1591
+ import { spawnSync, execSync as execSync2 } from "child_process";
1592
+ function canRunApiTests(ctx) {
1593
+ return !!ctx.apiKey;
1594
+ }
1595
+ async function apiCall(ctx, body) {
1596
+ try {
1597
+ const res = await fetch(`${ctx.proxyUrl}/v1/messages`, {
1598
+ method: "POST",
1599
+ headers: {
1600
+ "Content-Type": "application/json",
1601
+ "x-api-key": ctx.apiKey,
1602
+ "anthropic-version": "2023-06-01"
1603
+ },
1604
+ body: JSON.stringify({ model: ctx.model, ...body }),
1605
+ signal: AbortSignal.timeout(6e4)
1606
+ });
1607
+ const data = await res.json();
1608
+ if (!res.ok) {
1609
+ return { ok: false, data, status: res.status, errorMsg: data?.error?.message ?? `HTTP ${res.status}` };
1610
+ }
1611
+ return { ok: true, data, status: res.status };
1612
+ } catch (err) {
1613
+ return { ok: false, data: null, status: 0, errorMsg: err instanceof Error ? err.message : String(err) };
1614
+ }
1615
+ }
1616
+ function extractText(data) {
1617
+ if (!data?.content) return "";
1618
+ return data.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
1619
+ }
1620
+ async function runToolConversation(ctx, tools, userPrompt, simulate, opts) {
1621
+ const messages = [{ role: "user", content: userPrompt }];
1622
+ const allCalls = [];
1623
+ for (let turn = 0; turn < (opts?.maxTurns ?? 5); turn++) {
1624
+ const body = {
1625
+ max_tokens: 1024,
1626
+ temperature: 0,
1627
+ messages,
1628
+ tools
1629
+ };
1630
+ if (opts?.system) body.system = opts.system;
1631
+ const res = await apiCall(ctx, body);
1632
+ if (!res.ok) return { finalText: "", toolCalls: allCalls, error: res.errorMsg };
1633
+ const content = res.data.content ?? [];
1634
+ const toolBlocks = content.filter((b) => b.type === "tool_use");
1635
+ const textBlocks = content.filter((b) => b.type === "text");
1636
+ if (toolBlocks.length === 0) {
1637
+ return { finalText: textBlocks.map((b) => b.text ?? "").join(""), toolCalls: allCalls };
1638
+ }
1639
+ for (const tc of toolBlocks) allCalls.push({ name: tc.name, input: tc.input });
1640
+ messages.push({ role: "assistant", content });
1641
+ messages.push({
1642
+ role: "user",
1643
+ content: toolBlocks.map((tc) => ({
1644
+ type: "tool_result",
1645
+ tool_use_id: tc.id,
1646
+ content: simulate(tc.name, tc.input)
1647
+ }))
1648
+ });
1649
+ }
1650
+ return { finalText: "", toolCalls: allCalls, error: "Max turns reached" };
1651
+ }
1652
+ function filterStderr(stderr) {
1653
+ return stderr.split("\n").filter((l) => !l.includes("CPU lacks AVX") && !l.includes("bun-darwin") && !l.includes("Warning: no stdin data") && l.trim().length > 0).join("\n").trim();
1654
+ }
1655
+ function runClaudeTest(ctx, prompt, extraFlags = [], timeout = 9e4) {
1656
+ try {
1657
+ const isDirectAnthropic = ctx.proxyUrl.includes("api.anthropic.com");
1658
+ const envOverrides = isDirectAnthropic ? {} : { ANTHROPIC_BASE_URL: ctx.proxyUrl, ANTHROPIC_API_KEY: ctx.apiKey };
1659
+ const result2 = spawnSync("claude", [
1660
+ "--print",
1661
+ "--model",
1662
+ ctx.model,
1663
+ "--dangerously-skip-permissions",
1664
+ ...extraFlags,
1665
+ "-p",
1666
+ prompt
1667
+ ], {
1668
+ env: { ...process.env, ...envOverrides },
1669
+ timeout,
1670
+ encoding: "utf-8",
1671
+ cwd: ctx.projectDir
1672
+ });
1673
+ return {
1674
+ stdout: result2.stdout ?? "",
1675
+ stderr: filterStderr(result2.stderr ?? ""),
1676
+ exitCode: result2.status ?? 1
1677
+ };
1678
+ } catch (err) {
1679
+ return { stdout: "", stderr: String(err), exitCode: 1 };
1680
+ }
1681
+ }
1682
+ function isGitClean(dir) {
1683
+ try {
1684
+ const out = execSync2("git diff --name-only", { cwd: dir, encoding: "utf-8", timeout: 5e3 });
1685
+ return out.trim().length === 0;
1686
+ } catch {
1687
+ return false;
1688
+ }
1689
+ }
1690
+ function revertChanges(dir) {
1691
+ try {
1692
+ execSync2("git checkout -- src/logger.ts", { cwd: dir, timeout: 5e3, stdio: "pipe" });
1693
+ } catch {
1694
+ }
1695
+ }
1696
+ function result(name, category, status, accuracy, durationMs, detail, metrics) {
1697
+ return { name, category, status, accuracy, durationMs, detail, metrics };
1698
+ }
1699
+ function crc32(buf) {
1700
+ let c = 4294967295;
1701
+ for (const b of buf) c = CRC_TABLE[(c ^ b) & 255] ^ c >>> 8;
1702
+ return (c ^ 4294967295) >>> 0;
1703
+ }
1704
+ function createRedPng() {
1705
+ const w = 4, h = 4;
1706
+ const scanlines = Buffer.alloc(h * (1 + w * 3));
1707
+ for (let y = 0; y < h; y++) {
1708
+ const off = y * (1 + w * 3);
1709
+ scanlines[off] = 0;
1710
+ for (let x = 0; x < w; x++) {
1711
+ scanlines[off + 1 + x * 3] = 255;
1712
+ scanlines[off + 1 + x * 3 + 1] = 0;
1713
+ scanlines[off + 1 + x * 3 + 2] = 0;
1714
+ }
1715
+ }
1716
+ function chunk(type, data) {
1717
+ const tb = Buffer.from(type, "ascii");
1718
+ const merged = Buffer.concat([tb, data]);
1719
+ const len = Buffer.alloc(4);
1720
+ len.writeUInt32BE(data.length);
1721
+ const crcBuf = Buffer.alloc(4);
1722
+ crcBuf.writeUInt32BE(crc32(merged));
1723
+ return Buffer.concat([len, tb, data, crcBuf]);
1724
+ }
1725
+ const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
1726
+ const ihdr = Buffer.alloc(13);
1727
+ ihdr.writeUInt32BE(w, 0);
1728
+ ihdr.writeUInt32BE(h, 4);
1729
+ ihdr[8] = 8;
1730
+ ihdr[9] = 2;
1731
+ return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(scanlines)), chunk("IEND", Buffer.alloc(0))]);
1732
+ }
1733
+ async function testSimplePrompt(ctx) {
1734
+ const t = Date.now();
1735
+ if (!canRunApiTests(ctx)) {
1736
+ const r = runClaudeTest(ctx, "Reply with exactly: KODY_TEST_OK");
1737
+ const ok2 = r.stdout.includes("KODY_TEST_OK");
1738
+ return result(
1739
+ "simple_prompt",
1740
+ "basic",
1741
+ ok2 ? "pass" : "fail",
1742
+ ok2 ? 100 : 0,
1743
+ Date.now() - t,
1744
+ ok2 ? "Model responded correctly (via CLI)" : `Got: ${r.stdout.slice(0, 80)}`
1745
+ );
1746
+ }
1747
+ const res = await apiCall(ctx, {
1748
+ max_tokens: 50,
1749
+ temperature: 0,
1750
+ messages: [{ role: "user", content: "Reply with exactly: KODY_TEST_OK" }]
1751
+ });
1752
+ if (!res.ok) return result("simple_prompt", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
1753
+ const text = extractText(res.data);
1754
+ const ok = text.includes("KODY_TEST_OK");
1755
+ return result(
1756
+ "simple_prompt",
1757
+ "basic",
1758
+ ok ? "pass" : "fail",
1759
+ ok ? 100 : 0,
1760
+ Date.now() - t,
1761
+ ok ? "Model responded correctly" : `Expected KODY_TEST_OK, got: ${text.slice(0, 80)}`
1762
+ );
1763
+ }
1764
+ async function testJsonOutput(ctx) {
1765
+ if (!canRunApiTests(ctx)) {
1766
+ const t2 = Date.now();
1767
+ const r = runClaudeTest(ctx, 'Respond with ONLY valid JSON, no markdown fences. Return: {"status":"ok","model":"your name"}');
1768
+ let text2 = r.stdout.trim().replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
1769
+ try {
1770
+ JSON.parse(text2);
1771
+ return result("json_output", "basic", "pass", 100, Date.now() - t2, "Valid JSON via CLI");
1772
+ } catch {
1773
+ return result("json_output", "basic", "fail", 0, Date.now() - t2, `Invalid JSON: ${text2.slice(0, 80)}`);
1774
+ }
1775
+ }
1776
+ const t = Date.now();
1777
+ const res = await apiCall(ctx, {
1778
+ max_tokens: 200,
1779
+ temperature: 0,
1780
+ system: "Respond with ONLY valid JSON. No markdown fences, no explanation. Just raw JSON.",
1781
+ messages: [{ role: "user", content: 'Return a JSON object with keys "status" (string "ok") and "model" (string, your model name).' }]
1782
+ });
1783
+ if (!res.ok) return result("json_output", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
1784
+ let text = extractText(res.data).trim();
1785
+ text = text.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
1786
+ try {
1787
+ const parsed = JSON.parse(text);
1788
+ const hasKeys = typeof parsed.status === "string" && typeof parsed.model === "string";
1789
+ return result(
1790
+ "json_output",
1791
+ "basic",
1792
+ "pass",
1793
+ hasKeys ? 100 : 70,
1794
+ Date.now() - t,
1795
+ hasKeys ? "Valid JSON with correct keys" : "Valid JSON but missing expected keys"
1796
+ );
1797
+ } catch {
1798
+ return result("json_output", "basic", "fail", 0, Date.now() - t, `Invalid JSON: ${text.slice(0, 80)}`);
1799
+ }
1800
+ }
1801
+ function scoreRules(text) {
1802
+ let score = 0;
1803
+ const checks = [];
1804
+ if (text.startsWith("KODY:") || text.startsWith("kody:")) {
1805
+ score += 20;
1806
+ checks.push("starts-with-kody");
1807
+ }
1808
+ if (!text.toLowerCase().split(/\s+/).includes("the")) {
1809
+ score += 20;
1810
+ checks.push("no-the");
1811
+ }
1812
+ if (text.split(/\s+/).length <= 55) {
1813
+ score += 20;
1814
+ checks.push("under-50-words");
1815
+ }
1816
+ if (text.endsWith("END") || text.endsWith("end")) {
1817
+ score += 20;
1818
+ checks.push("ends-with-end");
1819
+ }
1820
+ if (text === text.toLowerCase()) {
1821
+ score += 20;
1822
+ checks.push("all-lowercase");
1823
+ }
1824
+ return { score, checks };
1825
+ }
1826
+ async function testSystemPromptRules(ctx) {
1827
+ const rulesPrompt = [
1828
+ "STRICT RULES \u2014 violating ANY will crash the system:",
1829
+ "1) Start every response with 'KODY:'",
1830
+ "2) Never use the word 'the'",
1831
+ "3) Keep response under 50 words",
1832
+ "4) End your response with 'END'",
1833
+ "5) Use ONLY lowercase letters (no uppercase anywhere)"
1834
+ ].join("\n");
1835
+ if (!canRunApiTests(ctx)) {
1836
+ const t2 = Date.now();
1837
+ const r = runClaudeTest(ctx, [
1838
+ "Follow ALL these rules in your response:",
1839
+ "1) Your response must start with the word 'KODY:'",
1840
+ "2) Do not use the word 'the' anywhere",
1841
+ "3) Keep your response under 50 words total",
1842
+ "4) End your response with the word 'END'",
1843
+ "5) Use only lowercase letters throughout",
1844
+ "",
1845
+ "Now describe what a compiler does. Remember: follow ALL 5 rules above exactly."
1846
+ ].join("\n"));
1847
+ const { score: score2, checks: checks2 } = scoreRules(r.stdout.trim());
1848
+ const status2 = score2 >= 80 ? "pass" : score2 >= 40 ? "warn" : "fail";
1849
+ return result(
1850
+ "system_prompt_rules",
1851
+ "basic",
1852
+ status2,
1853
+ score2,
1854
+ Date.now() - t2,
1855
+ `${score2 / 20}/5 rules followed: ${checks2.join(", ")}`,
1856
+ { instructionCompliance: score2 }
1857
+ );
1858
+ }
1859
+ const t = Date.now();
1860
+ const res = await apiCall(ctx, {
1861
+ max_tokens: 200,
1862
+ temperature: 0,
1863
+ system: rulesPrompt,
1864
+ messages: [{ role: "user", content: "Describe what a compiler does." }]
1865
+ });
1866
+ if (!res.ok) return result("system_prompt_rules", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
1867
+ const text = extractText(res.data).trim();
1868
+ const { score, checks } = scoreRules(text);
1869
+ const status = score >= 80 ? "pass" : score >= 40 ? "warn" : "fail";
1870
+ return result(
1871
+ "system_prompt_rules",
1872
+ "basic",
1873
+ status,
1874
+ score,
1875
+ Date.now() - t,
1876
+ `${score / 20}/5 rules followed: ${checks.join(", ")}`,
1877
+ { instructionCompliance: score }
1878
+ );
1879
+ }
1880
+ async function testExtendedThinking(ctx) {
1881
+ if (!canRunApiTests(ctx)) {
1882
+ const t2 = Date.now();
1883
+ const r = runClaudeTest(ctx, "What is 15 * 23? Reply with just the number.");
1884
+ const ok = r.stdout.includes("345");
1885
+ return result(
1886
+ "extended_thinking",
1887
+ "infrastructure",
1888
+ ok ? "pass" : "warn",
1889
+ ok ? 100 : 50,
1890
+ Date.now() - t2,
1891
+ ok ? "Model responded correctly (thinking assumed via CLI)" : `Got: ${r.stdout.slice(0, 80)}`
1892
+ );
1893
+ }
1894
+ const t = Date.now();
1895
+ const res = await apiCall(ctx, {
1896
+ max_tokens: 200,
1897
+ thinking: { type: "enabled", budget_tokens: 2e3 },
1898
+ messages: [{ role: "user", content: "What is 15 * 23?" }]
1899
+ });
1900
+ if (!res.ok) return result(
1901
+ "extended_thinking",
1902
+ "infrastructure",
1903
+ "warn",
1904
+ 50,
1905
+ Date.now() - t,
1906
+ `Request failed (model may not support thinking): ${res.errorMsg?.slice(0, 80)}`
1907
+ );
1908
+ const hasThinking = Array.isArray(res.data.content) && res.data.content.some((b) => b.type === "thinking");
1909
+ const hasText = extractText(res.data).length > 0;
1910
+ if (hasThinking) return result("extended_thinking", "infrastructure", "pass", 100, Date.now() - t, "Thinking block present in response");
1911
+ if (hasText) return result("extended_thinking", "infrastructure", "warn", 70, Date.now() - t, "Response OK but no thinking block");
1912
+ return result("extended_thinking", "infrastructure", "fail", 0, Date.now() - t, "No content in response");
1913
+ }
1914
+ async function testToolRead(ctx) {
1915
+ if (!canRunApiTests(ctx)) {
1916
+ const t2 = Date.now();
1917
+ const testFile2 = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
1918
+ fs12.writeFileSync(testFile2, "KODY_SECRET_CONTENT_42");
1919
+ try {
1920
+ const r = runClaudeTest(ctx, `Read the file ${testFile2} and tell me its exact contents. Reply with ONLY the file contents.`);
1921
+ const ok = r.stdout.includes("KODY_SECRET_CONTENT_42");
1922
+ return result(
1923
+ "tool_read",
1924
+ "tool-use",
1925
+ ok ? "pass" : "fail",
1926
+ ok ? 100 : 0,
1927
+ Date.now() - t2,
1928
+ ok ? "Read tool works via CLI" : `Got: ${r.stdout.slice(0, 80)}`,
1929
+ { toolSelection: ok ? 100 : 0 }
1930
+ );
1931
+ } finally {
1932
+ fs12.rmSync(testFile2, { force: true });
1933
+ }
1934
+ }
1935
+ const t = Date.now();
1936
+ const testFile = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
1937
+ fs12.writeFileSync(testFile, "KODY_SECRET_CONTENT_42");
1938
+ try {
1939
+ const conv = await runToolConversation(
1940
+ ctx,
1941
+ [TOOL_READ],
1942
+ `Read the file ${testFile} and tell me what it contains.`,
1943
+ (name, input) => {
1944
+ if (name === "Read" && input.path === testFile) return "KODY_SECRET_CONTENT_42";
1945
+ return "Error: File not found";
1946
+ }
1947
+ );
1948
+ if (conv.error) return result("tool_read", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
1949
+ const calledRead = conv.toolCalls.some((tc) => tc.name === "Read");
1950
+ const correctPath = conv.toolCalls.some((tc) => tc.name === "Read" && tc.input.path === testFile);
1951
+ const mentionsContent = conv.finalText.includes("KODY_SECRET_CONTENT_42") || conv.finalText.includes("42");
1952
+ let acc = 0;
1953
+ if (calledRead) acc += 30;
1954
+ if (correctPath) acc += 30;
1955
+ if (mentionsContent) acc += 40;
1956
+ return result(
1957
+ "tool_read",
1958
+ "tool-use",
1959
+ acc >= 60 ? "pass" : "fail",
1960
+ acc,
1961
+ Date.now() - t,
1962
+ `Read called: ${calledRead}, correct path: ${correctPath}, content referenced: ${mentionsContent}`,
1963
+ { toolSelection: calledRead ? 100 : 0 }
1964
+ );
1965
+ } finally {
1966
+ fs12.rmSync(testFile, { force: true });
1967
+ }
1968
+ }
1969
+ async function testToolEdit(ctx) {
1970
+ if (!canRunApiTests(ctx)) {
1971
+ const t2 = Date.now();
1972
+ const testFile = path11.join(os2.tmpdir(), "kody-test-model-edit.txt");
1973
+ fs12.writeFileSync(testFile, "hello world");
1974
+ try {
1975
+ const r = runClaudeTest(ctx, `Use the Edit tool to replace "hello" with "goodbye" in ${testFile}. Do nothing else.`);
1976
+ const content = fs12.existsSync(testFile) ? fs12.readFileSync(testFile, "utf-8") : "";
1977
+ const ok = content.includes("goodbye");
1978
+ return result(
1979
+ "tool_edit",
1980
+ "tool-use",
1981
+ ok ? "pass" : "fail",
1982
+ ok ? 100 : 0,
1983
+ Date.now() - t2,
1984
+ ok ? "Edit tool works via CLI" : `File content: ${content.slice(0, 80)}`,
1985
+ { toolSelection: ok ? 100 : 0 }
1986
+ );
1987
+ } finally {
1988
+ fs12.rmSync(testFile, { force: true });
1989
+ }
1990
+ }
1991
+ const t = Date.now();
1992
+ const conv = await runToolConversation(
1993
+ ctx,
1994
+ [TOOL_READ, TOOL_EDIT],
1995
+ 'Read the file /tmp/kody-edit-test.txt, then use Edit to replace "hello" with "goodbye" in it.',
1996
+ (name, input) => {
1997
+ if (name === "Read") return "hello world";
1998
+ if (name === "Edit") return "File edited successfully";
1999
+ return "Unknown tool";
2000
+ }
2001
+ );
2002
+ if (conv.error) return result("tool_edit", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
2003
+ const editCall = conv.toolCalls.find((tc) => tc.name === "Edit");
2004
+ let acc = 0;
2005
+ if (editCall) {
2006
+ acc += 40;
2007
+ if (editCall.input.old_string === "hello") acc += 30;
2008
+ if (editCall.input.new_string === "goodbye") acc += 30;
2009
+ }
2010
+ return result(
2011
+ "tool_edit",
2012
+ "tool-use",
2013
+ acc >= 70 ? "pass" : acc > 0 ? "warn" : "fail",
2014
+ acc,
2015
+ Date.now() - t,
2016
+ editCall ? `Edit called with old="${editCall.input.old_string}" new="${editCall.input.new_string}"` : "Edit tool was not called",
2017
+ { toolSelection: editCall ? 100 : 0 }
2018
+ );
2019
+ }
2020
+ async function testToolBash(ctx) {
2021
+ if (!canRunApiTests(ctx)) {
2022
+ const t2 = Date.now();
2023
+ const r = runClaudeTest(ctx, "Run this bash command and tell me its output: echo KODY_BASH_OK");
2024
+ const ok = r.stdout.includes("KODY_BASH_OK");
2025
+ return result(
2026
+ "tool_bash",
2027
+ "tool-use",
2028
+ ok ? "pass" : "fail",
2029
+ ok ? 100 : 0,
2030
+ Date.now() - t2,
2031
+ ok ? "Bash tool works via CLI" : `Got: ${r.stdout.slice(0, 80)}`,
2032
+ { toolSelection: ok ? 100 : 0 }
2033
+ );
2034
+ }
2035
+ const t = Date.now();
2036
+ const conv = await runToolConversation(
2037
+ ctx,
2038
+ [TOOL_BASH],
2039
+ "Run this exact bash command: echo KODY_BASH_OK",
2040
+ (name, input) => {
2041
+ if (name === "Bash") return "KODY_BASH_OK\n";
2042
+ return "Error";
2043
+ }
2044
+ );
2045
+ if (conv.error) return result("tool_bash", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
2046
+ const bashCall = conv.toolCalls.find((tc) => tc.name === "Bash");
2047
+ const correctCmd = bashCall && String(bashCall.input.command).includes("echo KODY_BASH_OK");
2048
+ const acc = bashCall ? correctCmd ? 100 : 50 : 0;
2049
+ return result(
2050
+ "tool_bash",
2051
+ "tool-use",
2052
+ acc >= 50 ? "pass" : "fail",
2053
+ acc,
2054
+ Date.now() - t,
2055
+ bashCall ? `Bash called: ${bashCall.input.command}` : "Bash tool was not called",
2056
+ { toolSelection: bashCall ? 100 : 0 }
2057
+ );
2058
+ }
2059
+ async function testImageAttachment(ctx) {
2060
+ if (!canRunApiTests(ctx)) {
2061
+ const t2 = Date.now();
2062
+ const tmpPng = path11.join(os2.tmpdir(), "kody-test-image.png");
2063
+ fs12.writeFileSync(tmpPng, createRedPng());
2064
+ try {
2065
+ const r = runClaudeTest(ctx, `Read the image file at ${tmpPng} and tell me what color it is. Reply with just the color name.`);
2066
+ const text2 = r.stdout.toLowerCase();
2067
+ const ok = text2.includes("red");
2068
+ return result(
2069
+ "image_attachment",
2070
+ "tool-use",
2071
+ ok ? "pass" : "warn",
2072
+ ok ? 100 : 50,
2073
+ Date.now() - t2,
2074
+ ok ? "Image processed correctly via CLI" : `Got: ${text2.slice(0, 80)}`
2075
+ );
2076
+ } finally {
2077
+ fs12.rmSync(tmpPng, { force: true });
2078
+ }
2079
+ }
2080
+ const t = Date.now();
2081
+ const pngData = createRedPng().toString("base64");
2082
+ const res = await apiCall(ctx, {
2083
+ max_tokens: 100,
2084
+ temperature: 0,
2085
+ messages: [{
2086
+ role: "user",
2087
+ content: [
2088
+ { type: "image", source: { type: "base64", media_type: "image/png", data: pngData } },
2089
+ { type: "text", text: "What color is this image? Reply with just the color name." }
2090
+ ]
2091
+ }]
2092
+ });
2093
+ if (!res.ok) return result(
2094
+ "image_attachment",
2095
+ "tool-use",
2096
+ "fail",
2097
+ 0,
2098
+ Date.now() - t,
2099
+ `API error (model may not support vision): ${res.errorMsg?.slice(0, 80)}`
2100
+ );
2101
+ const text = extractText(res.data).toLowerCase();
2102
+ const mentionsRed = text.includes("red");
2103
+ const mentionsColor = mentionsRed || text.includes("color") || text.includes("image") || text.includes("pixel");
2104
+ const acc = mentionsRed ? 100 : mentionsColor ? 50 : 20;
2105
+ return result(
2106
+ "image_attachment",
2107
+ "tool-use",
2108
+ mentionsRed ? "pass" : mentionsColor ? "warn" : "fail",
2109
+ acc,
2110
+ Date.now() - t,
2111
+ `Response: ${text.slice(0, 80)}`
2112
+ );
2113
+ }
2114
+ async function testErrorRecovery(ctx) {
2115
+ if (!canRunApiTests(ctx)) {
2116
+ const t2 = Date.now();
2117
+ const r = runClaudeTest(ctx, "Read the file /tmp/kody-nonexistent-test-file-xyz.txt and tell me what's in it. If it doesn't exist, say 'FILE_NOT_FOUND'.");
2118
+ const ok = r.stdout.includes("FILE_NOT_FOUND") || r.stdout.toLowerCase().includes("not found") || r.stdout.toLowerCase().includes("does not exist") || r.stdout.toLowerCase().includes("doesn't exist");
2119
+ return result(
2120
+ "error_recovery",
2121
+ "advanced",
2122
+ ok ? "pass" : "warn",
2123
+ ok ? 100 : 50,
2124
+ Date.now() - t2,
2125
+ ok ? "Graceful error handling via CLI" : `Got: ${r.stdout.slice(0, 80)}`
2126
+ );
2127
+ }
2128
+ const t = Date.now();
2129
+ let errorGiven = false;
2130
+ const conv = await runToolConversation(
2131
+ ctx,
2132
+ [TOOL_READ, TOOL_BASH],
2133
+ "Read the file /tmp/nonexistent-kody-file.txt and tell me what's in it. If the file doesn't exist, say so.",
2134
+ (name, input) => {
2135
+ if (name === "Read" && !errorGiven) {
2136
+ errorGiven = true;
2137
+ return "Error: ENOENT: no such file or directory";
2138
+ }
2139
+ if (name === "Bash") return "ls: /tmp/nonexistent-kody-file.txt: No such file or directory";
2140
+ return "Error: File not found";
2141
+ }
2142
+ );
2143
+ if (conv.error) return result("error_recovery", "advanced", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
2144
+ const reported = conv.finalText.toLowerCase().includes("not found") || conv.finalText.toLowerCase().includes("doesn't exist") || conv.finalText.toLowerCase().includes("does not exist") || conv.finalText.toLowerCase().includes("no such file");
2145
+ const tried = conv.toolCalls.length >= 1;
2146
+ const acc = reported ? tried ? 100 : 70 : 20;
2147
+ return result(
2148
+ "error_recovery",
2149
+ "advanced",
2150
+ reported ? "pass" : "warn",
2151
+ acc,
2152
+ Date.now() - t,
2153
+ reported ? "Gracefully reported missing file" : `Response: ${conv.finalText.slice(0, 80)}`
2154
+ );
2155
+ }
2156
+ async function testToolMultiStep(ctx) {
2157
+ const t = Date.now();
2158
+ const r = runClaudeTest(
2159
+ ctx,
2160
+ "Do these steps in order: 1) Read kody.config.json 2) Tell me the value of git.defaultBranch. Reply with ONLY the branch name, nothing else."
2161
+ );
2162
+ if (!r.stdout.trim() && r.exitCode !== 0) return result(
2163
+ "tool_multi_step",
2164
+ "tool-use",
2165
+ "fail",
2166
+ 0,
2167
+ Date.now() - t,
2168
+ `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2169
+ );
2170
+ const out = r.stdout.trim().toLowerCase();
2171
+ const correct = out.includes("main");
2172
+ return result(
2173
+ "tool_multi_step",
2174
+ "tool-use",
2175
+ correct ? "pass" : "fail",
2176
+ correct ? 100 : 20,
2177
+ Date.now() - t,
2178
+ correct ? "Correct: main" : `Got: ${out.slice(0, 80)}`
2179
+ );
2180
+ }
2181
+ async function testPlanStage(ctx) {
2182
+ const t = Date.now();
2183
+ const wasClean = isGitClean(ctx.projectDir);
2184
+ const r = runClaudeTest(ctx, [
2185
+ "You are a planning agent. Your ONLY job is to output a markdown plan.",
2186
+ "CRITICAL: Do NOT use Edit, Write, or Bash tools. Do NOT modify any files. ONLY use Read, Glob, and Grep for research.",
2187
+ "If you modify any files, the system will crash.",
2188
+ "",
2189
+ "Task: Plan adding a /health endpoint to an Express app.",
2190
+ "Output a markdown plan with ## Step N sections. Each step must have File, Change, and Why fields.",
2191
+ "Keep it to 3 steps maximum."
2192
+ ].join("\n"), [], 12e4);
2193
+ const filesModified = wasClean && !isGitClean(ctx.projectDir);
2194
+ if (filesModified) revertChanges(ctx.projectDir);
2195
+ if (!r.stdout.trim() && r.exitCode !== 0) return result(
2196
+ "plan_stage",
2197
+ "stage-simulation",
2198
+ "fail",
2199
+ 0,
2200
+ Date.now() - t,
2201
+ `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2202
+ );
2203
+ const out = r.stdout;
2204
+ const hasStepFormat = /##\s*Step/i.test(out);
2205
+ const hasStructure = hasStepFormat || /\*\*File\*\*/i.test(out) && /\*\*Change\*\*/i.test(out);
2206
+ const boundary = filesModified ? 0 : 100;
2207
+ const format = hasStructure ? 100 : hasStepFormat ? 70 : out.length > 50 ? 30 : 0;
2208
+ const acc = Math.round(boundary * 0.6 + format * 0.4);
2209
+ const status = filesModified ? "fail" : hasStructure ? "pass" : "warn";
2210
+ return result(
2211
+ "plan_stage",
2212
+ "stage-simulation",
2213
+ status,
2214
+ acc,
2215
+ Date.now() - t,
2216
+ filesModified ? "FAIL: Model modified files during plan stage (instruction violation)" : hasStructure ? "Plan output with correct structure, no files modified" : "Output lacks expected ## Step structure",
2217
+ { boundaryRespect: boundary, outputFormat: format, instructionCompliance: boundary }
2218
+ );
2219
+ }
2220
+ async function testBuildStage(ctx) {
2221
+ const t = Date.now();
2222
+ const r = runClaudeTest(ctx, "Add a comment '// kody-build-test' as the very first line of src/logger.ts. That is your only task.");
2223
+ const diff = (() => {
2224
+ try {
2225
+ return execSync2("git diff src/logger.ts", { cwd: ctx.projectDir, encoding: "utf-8", timeout: 5e3 });
2226
+ } catch {
2227
+ return "";
2228
+ }
2229
+ })();
2230
+ const edited = diff.includes("kody-build-test");
2231
+ revertChanges(ctx.projectDir);
2232
+ if (!r.stdout.trim() && r.exitCode !== 0 && !edited) return result(
2233
+ "build_stage",
2234
+ "stage-simulation",
2235
+ "fail",
2236
+ 0,
2237
+ Date.now() - t,
2238
+ `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2239
+ );
2240
+ return result(
2241
+ "build_stage",
2242
+ "stage-simulation",
2243
+ edited ? "pass" : "fail",
2244
+ edited ? 100 : 0,
2245
+ Date.now() - t,
2246
+ edited ? "File correctly modified with expected comment" : "File was not modified as expected"
2247
+ );
2248
+ }
2249
+ async function testReviewStage(ctx) {
2250
+ const t = Date.now();
2251
+ const wasClean = isGitClean(ctx.projectDir);
2252
+ const r = runClaudeTest(ctx, [
2253
+ "You are a code review agent. Review the file src/logger.ts.",
2254
+ "CRITICAL: Do NOT modify any files. Only READ and analyze.",
2255
+ "Output your review as markdown with this exact format:",
2256
+ "## Summary",
2257
+ "<1-2 sentence summary>",
2258
+ "## Issues Found",
2259
+ "- <issues>",
2260
+ "## Verdict",
2261
+ "APPROVE or REQUEST_CHANGES"
2262
+ ].join("\n"));
2263
+ const filesModified = wasClean && !isGitClean(ctx.projectDir);
2264
+ if (filesModified) revertChanges(ctx.projectDir);
2265
+ if (!r.stdout.trim() && r.exitCode !== 0) return result(
2266
+ "review_stage",
2267
+ "stage-simulation",
2268
+ "fail",
2269
+ 0,
2270
+ Date.now() - t,
2271
+ `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2272
+ );
2273
+ const out = r.stdout;
2274
+ const hasVerdict = /verdict/i.test(out);
2275
+ const hasSummary = /summary/i.test(out);
2276
+ const boundary = filesModified ? 0 : 100;
2277
+ const format = (hasVerdict ? 50 : 0) + (hasSummary ? 50 : 0);
2278
+ const acc = Math.round(boundary * 0.5 + format * 0.5);
2279
+ const status = filesModified ? "fail" : hasVerdict && hasSummary ? "pass" : "warn";
2280
+ return result(
2281
+ "review_stage",
2282
+ "stage-simulation",
2283
+ status,
2284
+ acc,
2285
+ Date.now() - t,
2286
+ filesModified ? "FAIL: Model modified files during review (instruction violation)" : `Summary: ${hasSummary}, Verdict: ${hasVerdict}, no files modified`,
2287
+ { boundaryRespect: boundary, outputFormat: format }
2288
+ );
2289
+ }
2290
+ async function testMcpTools(ctx) {
2291
+ const t = Date.now();
2292
+ const mcpConfig = path11.join(os2.tmpdir(), `kody-test-mcp-${Date.now()}.json`);
2293
+ const testFile = path11.join(ctx.projectDir, "kody-mcp-compat-test.txt");
2294
+ try {
2295
+ fs12.writeFileSync(mcpConfig, JSON.stringify({
2296
+ mcpServers: {
2297
+ filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", ctx.projectDir] }
2298
+ }
2299
+ }));
2300
+ const r = runClaudeTest(
2301
+ ctx,
2302
+ `Use the MCP filesystem write_file tool to create a file at ${testFile} with the content 'mcp-ok'. Do not use the built-in Write tool.`,
2303
+ ["--mcp-config", mcpConfig],
2304
+ 12e4
2305
+ );
2306
+ const created = fs12.existsSync(testFile);
2307
+ const content = created ? fs12.readFileSync(testFile, "utf-8").trim() : "";
2308
+ const correct = content.includes("mcp-ok");
2309
+ return result(
2310
+ "mcp_tools",
2311
+ "advanced",
2312
+ created ? "pass" : "fail",
2313
+ correct ? 100 : created ? 70 : 0,
2314
+ Date.now() - t,
2315
+ created ? `File created, content: ${content.slice(0, 50)}` : `MCP test failed: ${r.stderr.slice(0, 80)}`
2316
+ );
2317
+ } catch (err) {
2318
+ return result("mcp_tools", "advanced", "warn", 0, Date.now() - t, `MCP test error: ${err instanceof Error ? err.message : String(err)}`);
2319
+ } finally {
2320
+ fs12.rmSync(mcpConfig, { force: true });
2321
+ fs12.rmSync(testFile, { force: true });
2322
+ revertChanges(ctx.projectDir);
2323
+ }
2324
+ }
2325
+ var TOOL_READ, TOOL_EDIT, TOOL_BASH, CRC_TABLE, ALL_TESTS;
2326
+ var init_test_model_tests = __esm({
2327
+ "src/cli/test-model-tests.ts"() {
2328
+ "use strict";
2329
+ TOOL_READ = {
2330
+ name: "Read",
2331
+ description: "Read a file from the filesystem",
2332
+ input_schema: {
2333
+ type: "object",
2334
+ properties: { path: { type: "string", description: "Absolute file path" } },
2335
+ required: ["path"]
2336
+ }
2337
+ };
2338
+ TOOL_EDIT = {
2339
+ name: "Edit",
2340
+ description: "Replace old_string with new_string in a file",
2341
+ input_schema: {
2342
+ type: "object",
2343
+ properties: {
2344
+ file_path: { type: "string" },
2345
+ old_string: { type: "string" },
2346
+ new_string: { type: "string" }
2347
+ },
2348
+ required: ["file_path", "old_string", "new_string"]
2349
+ }
2350
+ };
2351
+ TOOL_BASH = {
2352
+ name: "Bash",
2353
+ description: "Execute a bash command and return output",
2354
+ input_schema: {
2355
+ type: "object",
2356
+ properties: { command: { type: "string", description: "The command to run" } },
2357
+ required: ["command"]
2358
+ }
2359
+ };
2360
+ CRC_TABLE = new Uint32Array(256);
2361
+ for (let n = 0; n < 256; n++) {
2362
+ let c = n;
2363
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
2364
+ CRC_TABLE[n] = c >>> 0;
2365
+ }
2366
+ ALL_TESTS = [
2367
+ // Infrastructure
2368
+ { name: "extended_thinking", category: "infrastructure", description: "Extended thinking parameter support", run: testExtendedThinking },
2369
+ // Basic
2370
+ { name: "simple_prompt", category: "basic", description: "Basic text prompt and response", run: testSimplePrompt },
2371
+ { name: "json_output", category: "basic", description: "JSON-only output constraint", run: testJsonOutput },
2372
+ { name: "system_prompt_rules", category: "basic", description: "Multi-rule system prompt adherence", run: testSystemPromptRules },
2373
+ // Tool use
2374
+ { name: "tool_read", category: "tool-use", description: "Read tool: file reading", run: testToolRead },
2375
+ { name: "tool_edit", category: "tool-use", description: "Edit tool: old/new string replacement", run: testToolEdit },
2376
+ { name: "tool_bash", category: "tool-use", description: "Bash tool: command execution", run: testToolBash },
2377
+ { name: "tool_multi_step", category: "tool-use", description: "Multi-step tool chain via CLI", run: testToolMultiStep },
2378
+ { name: "image_attachment", category: "tool-use", description: "Vision: image content processing", run: testImageAttachment },
2379
+ // Stage simulation
2380
+ { name: "plan_stage", category: "stage-simulation", description: "Plan stage: read-only research + structured output", run: testPlanStage },
2381
+ { name: "build_stage", category: "stage-simulation", description: "Build stage: code editing", run: testBuildStage },
2382
+ { name: "review_stage", category: "stage-simulation", description: "Review stage: read-only + structured verdict", run: testReviewStage },
2383
+ // Advanced
2384
+ { name: "mcp_tools", category: "advanced", description: "MCP server tool integration", run: testMcpTools },
2385
+ { name: "error_recovery", category: "advanced", description: "Graceful error handling on tool failure", run: testErrorRecovery }
2386
+ ];
2387
+ }
2388
+ });
2389
+
2390
+ // src/cli/test-model-report.ts
2391
+ function pad(str, len) {
2392
+ return str.padEnd(len);
2393
+ }
2394
+ function fmtDuration(ms) {
2395
+ return `${(ms / 1e3).toFixed(1)}s`;
2396
+ }
2397
+ function formatReport(report) {
2398
+ const W = 74;
2399
+ const lines = [];
2400
+ lines.push("=".repeat(W));
2401
+ lines.push("");
2402
+ lines.push(" Model Compatibility Report");
2403
+ lines.push(` Provider: ${report.provider} | Model: ${report.model}`);
2404
+ lines.push(` Date: ${report.timestamp}`);
2405
+ lines.push(` Duration: ${fmtDuration(report.totalDurationMs)}`);
2406
+ lines.push("");
2407
+ lines.push("-".repeat(W));
2408
+ for (const cat of CATEGORY_ORDER) {
2409
+ const catResults = report.results.filter((r) => r.category === cat);
2410
+ if (catResults.length === 0) continue;
2411
+ lines.push("");
2412
+ lines.push(` ${CATEGORY_LABELS[cat]}`);
2413
+ lines.push("");
2414
+ for (const r of catResults) {
2415
+ const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
2416
+ const name = pad(r.name, 28);
2417
+ const status = pad(r.status.toUpperCase(), 6);
2418
+ const acc = pad(`${r.accuracy}%`, 5);
2419
+ const dur = fmtDuration(r.durationMs);
2420
+ lines.push(` [${icon}] ${name} ${status} ${acc} ${dur}`);
2421
+ if (r.status !== "pass" && r.detail) {
2422
+ lines.push(` ${r.detail.slice(0, W - 8)}`);
2423
+ }
2424
+ }
2425
+ }
2426
+ const passed = report.results.filter((r) => r.status === "pass").length;
2427
+ const failed = report.results.filter((r) => r.status === "fail").length;
2428
+ const skipped = report.results.filter((r) => r.status === "warn" && r.durationMs === 0 && r.detail.includes("Skipped")).length;
2429
+ const warned = report.results.filter((r) => r.status === "warn").length - skipped;
2430
+ const total = report.results.length;
2431
+ const scored = report.results.filter((r) => !(r.status === "warn" && r.durationMs === 0 && r.detail.includes("Skipped")));
2432
+ const avgAccuracy = scored.length > 0 ? Math.round(scored.reduce((s, r) => s + r.accuracy, 0) / scored.length) : 0;
2433
+ lines.push("");
2434
+ lines.push("-".repeat(W));
2435
+ lines.push("");
2436
+ lines.push(` RESULTS: ${passed}/${total - skipped} PASS | ${failed} FAIL | ${warned} WARN${skipped > 0 ? ` | ${skipped} SKIPPED` : ""}`);
2437
+ lines.push(` OVERALL ACCURACY: ${avgAccuracy}%`);
2438
+ lines.push(` drop_params required: ${report.dropParamsRequired ? "YES" : "NO"}`);
2439
+ lines.push("");
2440
+ lines.push(" ACCURACY BY CATEGORY:");
2441
+ for (const cat of CATEGORY_ORDER) {
2442
+ const cr = report.results.filter((r) => r.category === cat && !(r.status === "warn" && r.durationMs === 0 && r.detail.includes("Skipped")));
2443
+ if (cr.length === 0) continue;
2444
+ const avg = Math.round(cr.reduce((s, r) => s + r.accuracy, 0) / cr.length);
2445
+ lines.push(` ${pad(CATEGORY_LABELS[cat], 22)} ${avg}%`);
2446
+ }
2447
+ lines.push("");
2448
+ lines.push(" RECOMMENDATION:");
2449
+ for (const line of getRecommendation(report)) {
2450
+ lines.push(` ${line}`);
2451
+ }
2452
+ lines.push("");
2453
+ lines.push("=".repeat(W));
2454
+ return lines.join("\n");
2455
+ }
2456
+ function getRecommendation(report) {
2457
+ const lines = [];
2458
+ const failedTests = report.results.filter((r) => r.status === "fail");
2459
+ const avg = report.results.length > 0 ? Math.round(report.results.reduce((s, r) => s + r.accuracy, 0) / report.results.length) : 0;
2460
+ if (avg >= 90 && failedTests.length === 0) {
2461
+ lines.push("[+] Fully compatible -- suitable for all pipeline stages");
2462
+ return lines;
2463
+ }
2464
+ const stageResults = report.results.filter((r) => r.category === "stage-simulation");
2465
+ const workingStages = stageResults.filter((r) => r.status === "pass").map((r) => r.name.replace("_stage", ""));
2466
+ const failingStages = stageResults.filter((r) => r.status !== "pass").map((r) => r.name.replace("_stage", ""));
2467
+ if (workingStages.length > 0) {
2468
+ lines.push(`[+] Suitable for: ${workingStages.join(", ")} stages`);
2469
+ }
2470
+ if (failingStages.length > 0) {
2471
+ lines.push(`[x] Not recommended for: ${failingStages.join(", ")} stages`);
2472
+ }
2473
+ if (failedTests.length > 0) {
2474
+ lines.push("");
2475
+ lines.push("Failed tests:");
2476
+ for (const t of failedTests) {
2477
+ lines.push(` - ${t.name}: ${t.detail.slice(0, 60)}`);
2478
+ }
2479
+ }
2480
+ return lines;
2481
+ }
2482
+ var CATEGORY_ORDER, CATEGORY_LABELS;
2483
+ var init_test_model_report = __esm({
2484
+ "src/cli/test-model-report.ts"() {
2485
+ "use strict";
2486
+ CATEGORY_ORDER = ["infrastructure", "basic", "tool-use", "stage-simulation", "advanced"];
2487
+ CATEGORY_LABELS = {
2488
+ infrastructure: "INFRASTRUCTURE",
2489
+ basic: "BASIC CAPABILITIES",
2490
+ "tool-use": "TOOL USE",
2491
+ "stage-simulation": "STAGE SIMULATION",
2492
+ advanced: "ADVANCED"
2493
+ };
2494
+ }
2495
+ });
2496
+
2497
+ // src/cli/test-model-command.ts
2498
+ var test_model_command_exports = {};
2499
+ __export(test_model_command_exports, {
2500
+ runTestModelCommand: () => runTestModelCommand
2501
+ });
2502
+ import * as fs13 from "fs";
2503
+ import * as os3 from "os";
2504
+ import * as path12 from "path";
2505
+ import { execFileSync as execFileSync10 } from "child_process";
2506
+ function parseTestModelArgs() {
2507
+ const args2 = process.argv.slice(3);
2508
+ function getArg3(flag) {
2509
+ const idx = args2.indexOf(flag);
2510
+ if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) return args2[idx + 1];
2511
+ return void 0;
2512
+ }
2513
+ const hasFlag3 = (f) => args2.includes(f);
2514
+ if (hasFlag3("--help") || hasFlag3("-h")) {
2515
+ logger.info([
2516
+ "Usage: kody test-model --provider <provider> --model <model> --key <api-key> [options]",
2517
+ "",
2518
+ "Options:",
2519
+ " --provider LLM provider name (e.g. gemini, openai, claude)",
2520
+ " --model Model identifier (e.g. gemini-2.5-flash, claude-sonnet-4-6)",
2521
+ " --key API key (optional for claude/anthropic \u2014 uses CLI auth)",
2522
+ " --key-env Read API key from this environment variable",
2523
+ " --skip-proxy Use an already-running LiteLLM proxy (don't start one)",
2524
+ " --litellm-url LiteLLM proxy URL (default: http://localhost:4099)",
2525
+ " --filter Comma-separated test names to run (default: all)",
2526
+ " --list List all available tests and exit"
2527
+ ].join("\n"));
2528
+ process.exit(0);
2529
+ }
2530
+ if (hasFlag3("--list")) {
2531
+ for (const t of ALL_TESTS) {
2532
+ logger.info(` ${t.name.padEnd(24)} [${t.category}] ${t.description}`);
2533
+ }
2534
+ process.exit(0);
2535
+ }
2536
+ const provider = getArg3("--provider");
2537
+ const model = getArg3("--model");
2538
+ const key = getArg3("--key");
2539
+ const keyEnv = getArg3("--key-env");
2540
+ if (!provider || !model) {
2541
+ logger.error("Required: --provider <provider> --model <model> --key <key>");
2542
+ logger.error("Run with --help for usage.");
2543
+ process.exit(1);
2544
+ }
2545
+ const isDirectAnthropic = provider === "claude" || provider === "anthropic";
2546
+ let apiKey = key;
2547
+ if (!apiKey && keyEnv) apiKey = process.env[keyEnv];
2548
+ if (!apiKey && !isDirectAnthropic) {
2549
+ logger.error("API key required: use --key <value> or --key-env <ENV_VAR>");
2550
+ logger.error("(For claude/anthropic provider, --key is optional \u2014 uses Claude Code auth)");
2551
+ process.exit(1);
2552
+ }
2553
+ return {
2554
+ provider,
2555
+ model,
2556
+ apiKey: apiKey ?? "",
2557
+ proxyUrl: isDirectAnthropic ? "https://api.anthropic.com" : getArg3("--litellm-url") ?? TEST_URL,
2558
+ skipProxy: isDirectAnthropic || hasFlag3("--skip-proxy"),
2559
+ filter: getArg3("--filter")?.split(",")
2560
+ };
2561
+ }
2562
+ function generateConfig(provider, model, dropParams) {
2563
+ const lines = [];
2564
+ if (dropParams) {
2565
+ lines.push("litellm_settings:");
2566
+ lines.push(" drop_params: true");
2567
+ lines.push("");
2568
+ }
2569
+ lines.push("model_list:");
2570
+ lines.push(` - model_name: ${model}`);
2571
+ lines.push(" litellm_params:");
2572
+ lines.push(` model: ${provider}/${model}`);
2573
+ lines.push(" api_key: os.environ/ANTHROPIC_COMPATIBLE_API_KEY");
2574
+ return lines.join("\n") + "\n";
2575
+ }
2576
+ async function startProxy(config, url) {
2577
+ try {
2578
+ execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2579
+ } catch {
2580
+ try {
2581
+ execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
2582
+ } catch {
2583
+ logger.error("litellm not installed. Install: pip install 'litellm[proxy]'");
2584
+ return null;
2585
+ }
2586
+ }
2587
+ fs13.writeFileSync(CONFIG_PATH, config);
2588
+ const portMatch = url.match(/:(\d+)/);
2589
+ const port = portMatch ? portMatch[1] : "4099";
2590
+ const { spawn: spawn2 } = await import("child_process");
2591
+ const child = spawn2("litellm", ["--config", CONFIG_PATH, "--port", port], {
2592
+ stdio: ["ignore", "pipe", "pipe"],
2593
+ detached: true,
2594
+ env: process.env
2595
+ });
2596
+ for (let i = 0; i < 30; i++) {
2597
+ await delay(2e3);
2598
+ if (await checkLitellmHealth(url)) {
2599
+ logger.info(`LiteLLM proxy ready at ${url}`);
2600
+ return child;
2601
+ }
2602
+ }
2603
+ child.kill();
2604
+ return null;
2605
+ }
2606
+ async function quickApiTest(url, model, apiKey) {
2607
+ try {
2608
+ const res = await fetch(`${url}/v1/messages`, {
2609
+ method: "POST",
2610
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
2611
+ body: JSON.stringify({
2612
+ model,
2613
+ max_tokens: 32,
2614
+ messages: [{ role: "user", content: "Say ok" }],
2615
+ context_management: { policy: "smart" }
2616
+ }),
2617
+ signal: AbortSignal.timeout(3e4)
2618
+ });
2619
+ if (!res.ok) {
2620
+ const body = await res.text();
2621
+ return { ok: false, error: body.slice(0, 200) };
2622
+ }
2623
+ return { ok: true };
2624
+ } catch (err) {
2625
+ return { ok: false, error: String(err) };
2626
+ }
2627
+ }
2628
+ function delay(ms) {
2629
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
2630
+ }
2631
+ async function runTestModelCommand() {
2632
+ const opts = parseTestModelArgs();
2633
+ const startTime = Date.now();
2634
+ logger.info(`Testing model compatibility: ${opts.provider}/${opts.model}`);
2635
+ logger.info("");
2636
+ let proxyProcess = null;
2637
+ let dropParamsRequired = false;
2638
+ const cleanup = () => {
2639
+ if (proxyProcess) {
2640
+ proxyProcess.kill();
2641
+ proxyProcess = null;
2642
+ }
2643
+ fs13.rmSync(CONFIG_PATH, { force: true });
2644
+ };
2645
+ process.on("SIGINT", () => {
2646
+ cleanup();
2647
+ process.exit(1);
2648
+ });
2649
+ process.on("SIGTERM", () => {
2650
+ cleanup();
2651
+ process.exit(1);
2652
+ });
2653
+ try {
2654
+ if (!opts.skipProxy) {
2655
+ process.env.ANTHROPIC_COMPATIBLE_API_KEY = opts.apiKey;
2656
+ logger.info("Starting LiteLLM proxy (without drop_params)...");
2657
+ proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, false), opts.proxyUrl);
2658
+ if (!proxyProcess) {
2659
+ logger.error("Failed to start LiteLLM proxy");
2660
+ process.exit(1);
2661
+ }
2662
+ const quickRes = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
2663
+ if (!quickRes.ok) {
2664
+ logger.info("Model needs drop_params: true -- restarting proxy...");
2665
+ proxyProcess.kill();
2666
+ proxyProcess = null;
2667
+ await delay(2e3);
2668
+ proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, true), opts.proxyUrl);
2669
+ dropParamsRequired = true;
2670
+ if (!proxyProcess) {
2671
+ logger.error("Failed to start LiteLLM proxy with drop_params");
2672
+ process.exit(1);
2673
+ }
2674
+ const retry = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
2675
+ if (!retry.ok) {
2676
+ logger.error(`Model not accessible: ${retry.error}`);
2677
+ process.exit(1);
2678
+ }
2679
+ logger.info("Proxy restarted with drop_params: true");
2680
+ } else {
2681
+ logger.info("drop_params not required");
2682
+ }
2683
+ } else {
2684
+ logger.info(`Using existing proxy at ${opts.proxyUrl}`);
2685
+ }
2686
+ const tests = opts.filter ? ALL_TESTS.filter((t) => opts.filter.includes(t.name)) : ALL_TESTS;
2687
+ logger.info(`Running ${tests.length} compatibility tests...`);
2688
+ logger.info("");
2689
+ const ctx = { proxyUrl: opts.proxyUrl, model: opts.model, apiKey: opts.apiKey, projectDir: process.cwd() };
2690
+ const results = [];
2691
+ for (const test of tests) {
2692
+ process.stdout.write(` ${test.name.padEnd(28)} `);
2693
+ try {
2694
+ const r = await test.run(ctx);
2695
+ results.push(r);
2696
+ const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
2697
+ logger.info(`[${icon}] ${r.status.toUpperCase()} ${r.accuracy}% (${(r.durationMs / 1e3).toFixed(1)}s)`);
2698
+ } catch (err) {
2699
+ const r = {
2700
+ name: test.name,
2701
+ category: test.category,
2702
+ status: "fail",
2703
+ accuracy: 0,
2704
+ durationMs: 0,
2705
+ detail: `Crash: ${err instanceof Error ? err.message : String(err)}`
2706
+ };
2707
+ results.push(r);
2708
+ logger.info("[x] CRASH");
2709
+ }
2710
+ }
2711
+ const report = {
2712
+ provider: opts.provider,
2713
+ model: opts.model,
2714
+ results,
2715
+ totalDurationMs: Date.now() - startTime,
2716
+ dropParamsRequired,
2717
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
2718
+ };
2719
+ console.log("");
2720
+ console.log(formatReport(report));
2721
+ const failed = results.filter((r) => r.status === "fail").length;
2722
+ process.exit(failed > 0 ? 1 : 0);
2723
+ } finally {
2724
+ cleanup();
2725
+ }
2726
+ }
2727
+ var TEST_PORT, TEST_URL, CONFIG_PATH;
2728
+ var init_test_model_command = __esm({
2729
+ "src/cli/test-model-command.ts"() {
2730
+ "use strict";
2731
+ init_logger();
2732
+ init_litellm();
2733
+ init_test_model_tests();
2734
+ init_test_model_report();
2735
+ TEST_PORT = 4099;
2736
+ TEST_URL = `http://localhost:${TEST_PORT}`;
2737
+ CONFIG_PATH = path12.join(os3.tmpdir(), "kody-test-model-config.yaml");
2738
+ }
2739
+ });
2740
+
1559
2741
  // src/ci/parse-inputs.ts
1560
2742
  var parse_inputs_exports = {};
1561
2743
  __export(parse_inputs_exports, {
@@ -1563,16 +2745,16 @@ __export(parse_inputs_exports, {
1563
2745
  runCiParse: () => runCiParse,
1564
2746
  writeOutputs: () => writeOutputs
1565
2747
  });
1566
- import * as fs12 from "fs";
2748
+ import * as fs14 from "fs";
1567
2749
  function generateTimestamp() {
1568
2750
  const now = /* @__PURE__ */ new Date();
1569
- const pad = (n) => String(n).padStart(2, "0");
2751
+ const pad2 = (n) => String(n).padStart(2, "0");
1570
2752
  const y = String(now.getFullYear()).slice(2);
1571
- const m = pad(now.getMonth() + 1);
1572
- const d = pad(now.getDate());
1573
- const H = pad(now.getHours());
1574
- const M = pad(now.getMinutes());
1575
- const S = pad(now.getSeconds());
2753
+ const m = pad2(now.getMonth() + 1);
2754
+ const d = pad2(now.getDate());
2755
+ const H = pad2(now.getHours());
2756
+ const M = pad2(now.getMinutes());
2757
+ const S = pad2(now.getSeconds());
1576
2758
  return `${y}${m}${d}-${H}${M}${S}`;
1577
2759
  }
1578
2760
  function parseCommentInputs() {
@@ -1691,23 +2873,6 @@ function parseCommentInputs() {
1691
2873
  }
1692
2874
  const modesWithoutTaskId = ["fix", "fix-ci", "status", "review", "resolve", "rerun"];
1693
2875
  const valid = !!taskId || modesWithoutTaskId.includes(mode);
1694
- if (mode === "taskify" && !ticketId && !prdFile) {
1695
- return {
1696
- task_id: taskId,
1697
- mode,
1698
- from_stage: fromStage,
1699
- issue_number: issueNumber,
1700
- pr_number: "",
1701
- feedback,
1702
- complexity,
1703
- ci_run_id: ciRunId,
1704
- ticket_id: "",
1705
- prd_file: "",
1706
- dry_run: dryRun,
1707
- valid: false,
1708
- trigger_type: "comment"
1709
- };
1710
- }
1711
2876
  return {
1712
2877
  task_id: taskId,
1713
2878
  mode,
@@ -1724,40 +2889,40 @@ function parseCommentInputs() {
1724
2889
  trigger_type: "comment"
1725
2890
  };
1726
2891
  }
1727
- function writeOutputs(result) {
2892
+ function writeOutputs(result2) {
1728
2893
  const outputFile = process.env.GITHUB_OUTPUT;
1729
2894
  function output(key, value) {
1730
2895
  if (outputFile) {
1731
2896
  if (value.includes("\n")) {
1732
- fs12.appendFileSync(outputFile, `${key}<<KODY_EOF
2897
+ fs14.appendFileSync(outputFile, `${key}<<KODY_EOF
1733
2898
  ${value}
1734
2899
  KODY_EOF
1735
2900
  `);
1736
2901
  } else {
1737
- fs12.appendFileSync(outputFile, `${key}=${value}
2902
+ fs14.appendFileSync(outputFile, `${key}=${value}
1738
2903
  `);
1739
2904
  }
1740
2905
  }
1741
2906
  const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
1742
2907
  console.log(`${key}=${display}`);
1743
2908
  }
1744
- output("task_id", result.task_id);
1745
- output("mode", result.mode);
1746
- output("from_stage", result.from_stage);
1747
- output("issue_number", result.issue_number);
1748
- output("pr_number", result.pr_number);
1749
- output("feedback", result.feedback);
1750
- output("complexity", result.complexity);
1751
- output("ci_run_id", result.ci_run_id);
1752
- output("ticket_id", result.ticket_id);
1753
- output("prd_file", result.prd_file);
1754
- output("dry_run", result.dry_run ? "true" : "false");
1755
- output("valid", result.valid ? "true" : "false");
1756
- output("trigger_type", result.trigger_type);
2909
+ output("task_id", result2.task_id);
2910
+ output("mode", result2.mode);
2911
+ output("from_stage", result2.from_stage);
2912
+ output("issue_number", result2.issue_number);
2913
+ output("pr_number", result2.pr_number);
2914
+ output("feedback", result2.feedback);
2915
+ output("complexity", result2.complexity);
2916
+ output("ci_run_id", result2.ci_run_id);
2917
+ output("ticket_id", result2.ticket_id);
2918
+ output("prd_file", result2.prd_file);
2919
+ output("dry_run", result2.dry_run ? "true" : "false");
2920
+ output("valid", result2.valid ? "true" : "false");
2921
+ output("trigger_type", result2.trigger_type);
1757
2922
  }
1758
2923
  function runCiParse() {
1759
- const result = parseCommentInputs();
1760
- writeOutputs(result);
2924
+ const result2 = parseCommentInputs();
2925
+ writeOutputs(result2);
1761
2926
  }
1762
2927
  var VALID_MODES;
1763
2928
  var init_parse_inputs = __esm({
@@ -1859,7 +3024,7 @@ var init_definitions = __esm({
1859
3024
  });
1860
3025
 
1861
3026
  // src/git-utils.ts
1862
- import { execFileSync as execFileSync10 } from "child_process";
3027
+ import { execFileSync as execFileSync11 } from "child_process";
1863
3028
  function getHookSafeEnv() {
1864
3029
  if (!_hookSafeEnv) {
1865
3030
  _hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
@@ -1867,7 +3032,7 @@ function getHookSafeEnv() {
1867
3032
  return _hookSafeEnv;
1868
3033
  }
1869
3034
  function git(args2, options) {
1870
- return execFileSync10("git", args2, {
3035
+ return execFileSync11("git", args2, {
1871
3036
  encoding: "utf-8",
1872
3037
  timeout: options?.timeout ?? 3e4,
1873
3038
  cwd: options?.cwd,
@@ -2053,22 +3218,22 @@ var init_git_utils = __esm({
2053
3218
  });
2054
3219
 
2055
3220
  // src/pipeline/state.ts
2056
- import * as fs13 from "fs";
2057
- import * as path11 from "path";
3221
+ import * as fs15 from "fs";
3222
+ import * as path13 from "path";
2058
3223
  function loadState(taskId, taskDir) {
2059
- const p = path11.join(taskDir, "status.json");
2060
- if (!fs13.existsSync(p)) return null;
3224
+ const p = path13.join(taskDir, "status.json");
3225
+ if (!fs15.existsSync(p)) return null;
2061
3226
  try {
2062
- const result = parseJsonSafe(
2063
- fs13.readFileSync(p, "utf-8"),
3227
+ const result2 = parseJsonSafe(
3228
+ fs15.readFileSync(p, "utf-8"),
2064
3229
  ["taskId", "state", "stages", "createdAt", "updatedAt"]
2065
3230
  );
2066
- if (!result.ok) {
2067
- logger.warn(` Corrupt status.json: ${result.error}`);
3231
+ if (!result2.ok) {
3232
+ logger.warn(` Corrupt status.json: ${result2.error}`);
2068
3233
  return null;
2069
3234
  }
2070
- if (result.data.taskId !== taskId) return null;
2071
- return result.data;
3235
+ if (result2.data.taskId !== taskId) return null;
3236
+ return result2.data;
2072
3237
  } catch {
2073
3238
  return null;
2074
3239
  }
@@ -2078,10 +3243,10 @@ function writeState(state, taskDir) {
2078
3243
  ...state,
2079
3244
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2080
3245
  };
2081
- const target = path11.join(taskDir, "status.json");
3246
+ const target = path13.join(taskDir, "status.json");
2082
3247
  const tmp = target + ".tmp";
2083
- fs13.writeFileSync(tmp, JSON.stringify(updated, null, 2));
2084
- fs13.renameSync(tmp, target);
3248
+ fs15.writeFileSync(tmp, JSON.stringify(updated, null, 2));
3249
+ fs15.renameSync(tmp, target);
2085
3250
  return updated;
2086
3251
  }
2087
3252
  function initState(taskId) {
@@ -2122,16 +3287,16 @@ var init_complexity = __esm({
2122
3287
  });
2123
3288
 
2124
3289
  // src/memory.ts
2125
- import * as fs14 from "fs";
2126
- import * as path12 from "path";
3290
+ import * as fs16 from "fs";
3291
+ import * as path14 from "path";
2127
3292
  function readProjectMemory(projectDir) {
2128
- const memoryDir = path12.join(projectDir, ".kody", "memory");
2129
- if (!fs14.existsSync(memoryDir)) return "";
2130
- const files = fs14.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
3293
+ const memoryDir = path14.join(projectDir, ".kody", "memory");
3294
+ if (!fs16.existsSync(memoryDir)) return "";
3295
+ const files = fs16.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
2131
3296
  if (files.length === 0) return "";
2132
3297
  const sections = [];
2133
3298
  for (const file of files) {
2134
- const content = fs14.readFileSync(path12.join(memoryDir, file), "utf-8").trim();
3299
+ const content = fs16.readFileSync(path14.join(memoryDir, file), "utf-8").trim();
2135
3300
  if (content) {
2136
3301
  sections.push(`## ${file.replace(".md", "")}
2137
3302
  ${content}`);
@@ -2150,8 +3315,8 @@ var init_memory = __esm({
2150
3315
  });
2151
3316
 
2152
3317
  // src/context-tiers.ts
2153
- import * as fs15 from "fs";
2154
- import * as path13 from "path";
3318
+ import * as fs17 from "fs";
3319
+ import * as path15 from "path";
2155
3320
  function estimateTokens(text) {
2156
3321
  return Math.ceil(text.length / 4);
2157
3322
  }
@@ -2178,8 +3343,8 @@ function generateL0(content, filename) {
2178
3343
  break;
2179
3344
  }
2180
3345
  }
2181
- const result = parts.join("\n");
2182
- return result.slice(0, L0_MAX_CHARS);
3346
+ const result2 = parts.join("\n");
3347
+ return result2.slice(0, L0_MAX_CHARS);
2183
3348
  }
2184
3349
  function generateL0Json(content) {
2185
3350
  try {
@@ -2221,8 +3386,8 @@ function generateL1(content, filename) {
2221
3386
  inSection = false;
2222
3387
  }
2223
3388
  }
2224
- const result = parts.join("\n");
2225
- return result.slice(0, L1_MAX_CHARS);
3389
+ const result2 = parts.join("\n");
3390
+ return result2.slice(0, L1_MAX_CHARS);
2226
3391
  }
2227
3392
  function generateL1Json(content) {
2228
3393
  try {
@@ -2242,7 +3407,7 @@ function generateL1Json(content) {
2242
3407
  }
2243
3408
  }
2244
3409
  function getTieredContent(filePath, content) {
2245
- const key = path13.basename(filePath);
3410
+ const key = path15.basename(filePath);
2246
3411
  return {
2247
3412
  source: filePath,
2248
3413
  L0: generateL0(content, key),
@@ -2254,15 +3419,15 @@ function selectTier(tiered, tier) {
2254
3419
  return tiered[tier];
2255
3420
  }
2256
3421
  function readProjectMemoryTiered(projectDir, tier) {
2257
- const memoryDir = path13.join(projectDir, ".kody", "memory");
2258
- if (!fs15.existsSync(memoryDir)) return "";
2259
- const files = fs15.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
3422
+ const memoryDir = path15.join(projectDir, ".kody", "memory");
3423
+ if (!fs17.existsSync(memoryDir)) return "";
3424
+ const files = fs17.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
2260
3425
  if (files.length === 0) return "";
2261
3426
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
2262
3427
  const sections = [];
2263
3428
  for (const file of files) {
2264
- const filePath = path13.join(memoryDir, file);
2265
- const content = fs15.readFileSync(filePath, "utf-8").trim();
3429
+ const filePath = path15.join(memoryDir, file);
3430
+ const content = fs17.readFileSync(filePath, "utf-8").trim();
2266
3431
  if (!content) continue;
2267
3432
  const tiered = getTieredContent(filePath, content);
2268
3433
  const selected = selectTier(tiered, tier);
@@ -2285,9 +3450,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
2285
3450
  `;
2286
3451
  context += `Task Directory: ${taskDir}
2287
3452
  `;
2288
- const taskMdPath = path13.join(taskDir, "task.md");
2289
- if (fs15.existsSync(taskMdPath)) {
2290
- const content = fs15.readFileSync(taskMdPath, "utf-8");
3453
+ const taskMdPath = path15.join(taskDir, "task.md");
3454
+ if (fs17.existsSync(taskMdPath)) {
3455
+ const content = fs17.readFileSync(taskMdPath, "utf-8");
2291
3456
  const selected = selectContent(taskMdPath, content, policy.taskDescription);
2292
3457
  const label = tierLabel("Task Description", policy.taskDescription);
2293
3458
  context += `
@@ -2295,9 +3460,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
2295
3460
  ${selected}
2296
3461
  `;
2297
3462
  }
2298
- const taskJsonPath = path13.join(taskDir, "task.json");
2299
- if (fs15.existsSync(taskJsonPath)) {
2300
- const content = fs15.readFileSync(taskJsonPath, "utf-8");
3463
+ const taskJsonPath = path15.join(taskDir, "task.json");
3464
+ if (fs17.existsSync(taskJsonPath)) {
3465
+ const content = fs17.readFileSync(taskJsonPath, "utf-8");
2301
3466
  if (policy.taskClassification === "L2") {
2302
3467
  try {
2303
3468
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -2323,9 +3488,9 @@ ${selected}
2323
3488
  }
2324
3489
  }
2325
3490
  }
2326
- const specPath = path13.join(taskDir, "spec.md");
2327
- if (fs15.existsSync(specPath)) {
2328
- const content = fs15.readFileSync(specPath, "utf-8");
3491
+ const specPath = path15.join(taskDir, "spec.md");
3492
+ if (fs17.existsSync(specPath)) {
3493
+ const content = fs17.readFileSync(specPath, "utf-8");
2329
3494
  const selected = selectContent(specPath, content, policy.spec);
2330
3495
  const label = tierLabel("Spec", policy.spec);
2331
3496
  context += `
@@ -2333,9 +3498,9 @@ ${selected}
2333
3498
  ${selected}
2334
3499
  `;
2335
3500
  }
2336
- const planPath = path13.join(taskDir, "plan.md");
2337
- if (fs15.existsSync(planPath)) {
2338
- const content = fs15.readFileSync(planPath, "utf-8");
3501
+ const planPath = path15.join(taskDir, "plan.md");
3502
+ if (fs17.existsSync(planPath)) {
3503
+ const content = fs17.readFileSync(planPath, "utf-8");
2339
3504
  const selected = selectContent(planPath, content, policy.plan);
2340
3505
  const label = tierLabel("Plan", policy.plan);
2341
3506
  context += `
@@ -2343,9 +3508,9 @@ ${selected}
2343
3508
  ${selected}
2344
3509
  `;
2345
3510
  }
2346
- const contextMdPath = path13.join(taskDir, "context.md");
2347
- if (fs15.existsSync(contextMdPath)) {
2348
- const content = fs15.readFileSync(contextMdPath, "utf-8");
3511
+ const contextMdPath = path15.join(taskDir, "context.md");
3512
+ if (fs17.existsSync(contextMdPath)) {
3513
+ const content = fs17.readFileSync(contextMdPath, "utf-8");
2349
3514
  const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
2350
3515
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
2351
3516
  context += `
@@ -2431,24 +3596,24 @@ var init_context_tiers = __esm({
2431
3596
  });
2432
3597
 
2433
3598
  // src/context.ts
2434
- import * as fs16 from "fs";
2435
- import * as path14 from "path";
3599
+ import * as fs18 from "fs";
3600
+ import * as path16 from "path";
2436
3601
  function readPromptFile(stageName, projectDir) {
2437
3602
  if (projectDir) {
2438
- const stepFile = path14.join(projectDir, ".kody", "steps", `${stageName}.md`);
2439
- if (fs16.existsSync(stepFile)) {
2440
- return fs16.readFileSync(stepFile, "utf-8");
3603
+ const stepFile = path16.join(projectDir, ".kody", "steps", `${stageName}.md`);
3604
+ if (fs18.existsSync(stepFile)) {
3605
+ return fs18.readFileSync(stepFile, "utf-8");
2441
3606
  }
2442
3607
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
2443
3608
  }
2444
3609
  const scriptDir = new URL(".", import.meta.url).pathname;
2445
3610
  const candidates = [
2446
- path14.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
2447
- path14.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
3611
+ path16.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
3612
+ path16.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
2448
3613
  ];
2449
3614
  for (const candidate of candidates) {
2450
- if (fs16.existsSync(candidate)) {
2451
- return fs16.readFileSync(candidate, "utf-8");
3615
+ if (fs18.existsSync(candidate)) {
3616
+ return fs18.readFileSync(candidate, "utf-8");
2452
3617
  }
2453
3618
  }
2454
3619
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -2460,18 +3625,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
2460
3625
  `;
2461
3626
  context += `Task Directory: ${taskDir}
2462
3627
  `;
2463
- const taskMdPath = path14.join(taskDir, "task.md");
2464
- if (fs16.existsSync(taskMdPath)) {
2465
- const taskMd = fs16.readFileSync(taskMdPath, "utf-8");
3628
+ const taskMdPath = path16.join(taskDir, "task.md");
3629
+ if (fs18.existsSync(taskMdPath)) {
3630
+ const taskMd = fs18.readFileSync(taskMdPath, "utf-8");
2466
3631
  context += `
2467
3632
  ## Task Description
2468
3633
  ${taskMd}
2469
3634
  `;
2470
3635
  }
2471
- const taskJsonPath = path14.join(taskDir, "task.json");
2472
- if (fs16.existsSync(taskJsonPath)) {
3636
+ const taskJsonPath = path16.join(taskDir, "task.json");
3637
+ if (fs18.existsSync(taskJsonPath)) {
2473
3638
  try {
2474
- const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
3639
+ const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
2475
3640
  context += `
2476
3641
  ## Task Classification
2477
3642
  `;
@@ -2484,27 +3649,27 @@ ${taskMd}
2484
3649
  } catch {
2485
3650
  }
2486
3651
  }
2487
- const specPath = path14.join(taskDir, "spec.md");
2488
- if (fs16.existsSync(specPath)) {
2489
- const spec = fs16.readFileSync(specPath, "utf-8");
3652
+ const specPath = path16.join(taskDir, "spec.md");
3653
+ if (fs18.existsSync(specPath)) {
3654
+ const spec = fs18.readFileSync(specPath, "utf-8");
2490
3655
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
2491
3656
  context += `
2492
3657
  ## Spec Summary
2493
3658
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
2494
3659
  `;
2495
3660
  }
2496
- const planPath = path14.join(taskDir, "plan.md");
2497
- if (fs16.existsSync(planPath)) {
2498
- const plan = fs16.readFileSync(planPath, "utf-8");
3661
+ const planPath = path16.join(taskDir, "plan.md");
3662
+ if (fs18.existsSync(planPath)) {
3663
+ const plan = fs18.readFileSync(planPath, "utf-8");
2499
3664
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
2500
3665
  context += `
2501
3666
  ## Plan Summary
2502
3667
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
2503
3668
  `;
2504
3669
  }
2505
- const contextMdPath = path14.join(taskDir, "context.md");
2506
- if (fs16.existsSync(contextMdPath)) {
2507
- const accumulated = fs16.readFileSync(contextMdPath, "utf-8");
3670
+ const contextMdPath = path16.join(taskDir, "context.md");
3671
+ if (fs18.existsSync(contextMdPath)) {
3672
+ const accumulated = fs18.readFileSync(contextMdPath, "utf-8");
2508
3673
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
2509
3674
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
2510
3675
  context += `
@@ -2522,17 +3687,17 @@ ${feedback}
2522
3687
  }
2523
3688
  function inferHasUIFromScope(scope) {
2524
3689
  return scope.some((filePath) => {
2525
- const ext = path14.extname(filePath).toLowerCase();
3690
+ const ext = path16.extname(filePath).toLowerCase();
2526
3691
  if (UI_EXTENSIONS.has(ext)) return true;
2527
3692
  const normalized = filePath.replace(/\\/g, "/");
2528
3693
  return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
2529
3694
  });
2530
3695
  }
2531
3696
  function taskHasUI(taskDir) {
2532
- const taskJsonPath = path14.join(taskDir, "task.json");
2533
- if (!fs16.existsSync(taskJsonPath)) return true;
3697
+ const taskJsonPath = path16.join(taskDir, "task.json");
3698
+ if (!fs18.existsSync(taskJsonPath)) return true;
2534
3699
  try {
2535
- const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
3700
+ const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
2536
3701
  const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
2537
3702
  if (scope.length === 0) return true;
2538
3703
  return inferHasUIFromScope(scope);
@@ -2654,9 +3819,9 @@ ${prompt}` : prompt;
2654
3819
  }
2655
3820
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
2656
3821
  assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
2657
- const qaGuidePath = path14.join(projectDir, ".kody", "qa-guide.md");
2658
- if (fs16.existsSync(qaGuidePath)) {
2659
- const qaGuide = fs16.readFileSync(qaGuidePath, "utf-8").trim();
3822
+ const qaGuidePath = path16.join(projectDir, ".kody", "qa-guide.md");
3823
+ if (fs18.existsSync(qaGuidePath)) {
3824
+ const qaGuide = fs18.readFileSync(qaGuidePath, "utf-8").trim();
2660
3825
  assembled = assembled + "\n\n" + qaGuide;
2661
3826
  }
2662
3827
  }
@@ -2686,10 +3851,12 @@ function escalateModelTier(currentTier) {
2686
3851
  function resolveModel(modelTier, stageName) {
2687
3852
  const config = getProjectConfig();
2688
3853
  const mapped = config.agent.modelMap[modelTier];
2689
- if (mapped) return mapped;
2690
- return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
3854
+ if (!mapped) {
3855
+ throw new Error(`No model configured for tier '${modelTier}'. Set agent.modelMap.${modelTier} in kody.config.json`);
3856
+ }
3857
+ return mapped;
2691
3858
  }
2692
- var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION, DEFAULT_MODEL_MAP;
3859
+ var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
2693
3860
  var init_context = __esm({
2694
3861
  "src/context.ts"() {
2695
3862
  "use strict";
@@ -2723,11 +3890,6 @@ var init_context = __esm({
2723
3890
  mid: "strong",
2724
3891
  strong: "strong"
2725
3892
  };
2726
- DEFAULT_MODEL_MAP = {
2727
- cheap: "haiku",
2728
- mid: "sonnet",
2729
- strong: "opus"
2730
- };
2731
3893
  }
2732
3894
  });
2733
3895
 
@@ -2751,8 +3913,8 @@ var init_runner_selection = __esm({
2751
3913
  });
2752
3914
 
2753
3915
  // src/stages/agent.ts
2754
- import * as fs17 from "fs";
2755
- import * as path15 from "path";
3916
+ import * as fs19 from "fs";
3917
+ import * as path17 from "path";
2756
3918
  function getSessionInfo(stageName, sessions) {
2757
3919
  const group = SESSION_GROUP[stageName];
2758
3920
  if (!group) return void 0;
@@ -2837,29 +3999,29 @@ async function executeAgentStage(ctx, def) {
2837
3999
  if (lastResult.outcome !== "completed") {
2838
4000
  return { outcome: lastResult.outcome, error: lastResult.error, retries };
2839
4001
  }
2840
- const result = lastResult;
2841
- if (def.outputFile && result.output) {
2842
- fs17.writeFileSync(path15.join(ctx.taskDir, def.outputFile), result.output);
4002
+ const result2 = lastResult;
4003
+ if (def.outputFile && result2.output) {
4004
+ fs19.writeFileSync(path17.join(ctx.taskDir, def.outputFile), result2.output);
2843
4005
  }
2844
4006
  if (def.outputFile) {
2845
- const outputPath = path15.join(ctx.taskDir, def.outputFile);
2846
- if (!fs17.existsSync(outputPath)) {
2847
- const ext = path15.extname(def.outputFile);
2848
- const base = path15.basename(def.outputFile, ext);
2849
- const files = fs17.readdirSync(ctx.taskDir);
4007
+ const outputPath = path17.join(ctx.taskDir, def.outputFile);
4008
+ if (!fs19.existsSync(outputPath)) {
4009
+ const ext = path17.extname(def.outputFile);
4010
+ const base = path17.basename(def.outputFile, ext);
4011
+ const files = fs19.readdirSync(ctx.taskDir);
2850
4012
  const variant = files.find(
2851
4013
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
2852
4014
  );
2853
4015
  if (variant) {
2854
- fs17.renameSync(path15.join(ctx.taskDir, variant), outputPath);
4016
+ fs19.renameSync(path17.join(ctx.taskDir, variant), outputPath);
2855
4017
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
2856
4018
  }
2857
4019
  }
2858
4020
  }
2859
4021
  if (def.outputFile) {
2860
- const outputPath = path15.join(ctx.taskDir, def.outputFile);
2861
- if (fs17.existsSync(outputPath)) {
2862
- const content = fs17.readFileSync(outputPath, "utf-8");
4022
+ const outputPath = path17.join(ctx.taskDir, def.outputFile);
4023
+ if (fs19.existsSync(outputPath)) {
4024
+ const content = fs19.readFileSync(outputPath, "utf-8");
2863
4025
  const validation = validateStageOutput(def.name, content);
2864
4026
  if (!validation.valid) {
2865
4027
  if (def.name === "taskify") {
@@ -2873,7 +4035,7 @@ async function executeAgentStage(ctx, def) {
2873
4035
  const stripped = stripFences(retryResult.output);
2874
4036
  const retryValidation = validateTaskJson(stripped);
2875
4037
  if (retryValidation.valid) {
2876
- fs17.writeFileSync(outputPath, retryResult.output);
4038
+ fs19.writeFileSync(outputPath, retryResult.output);
2877
4039
  logger.info(` taskify retry produced valid JSON`);
2878
4040
  } else {
2879
4041
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -2886,7 +4048,7 @@ async function executeAgentStage(ctx, def) {
2886
4048
  risk_level: "low",
2887
4049
  questions: []
2888
4050
  }, null, 2);
2889
- fs17.writeFileSync(outputPath, fallback);
4051
+ fs19.writeFileSync(outputPath, fallback);
2890
4052
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
2891
4053
  }
2892
4054
  }
@@ -2896,11 +4058,11 @@ async function executeAgentStage(ctx, def) {
2896
4058
  }
2897
4059
  }
2898
4060
  }
2899
- appendStageContext(ctx.taskDir, def.name, result.output);
4061
+ appendStageContext(ctx.taskDir, def.name, result2.output);
2900
4062
  return { outcome: "completed", outputFile: def.outputFile, retries };
2901
4063
  }
2902
4064
  function appendStageContext(taskDir, stageName, output) {
2903
- const contextPath = path15.join(taskDir, "context.md");
4065
+ const contextPath = path17.join(taskDir, "context.md");
2904
4066
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
2905
4067
  let summary;
2906
4068
  if (output && output.trim()) {
@@ -2913,7 +4075,7 @@ function appendStageContext(taskDir, stageName, output) {
2913
4075
  ### ${stageName} (${timestamp2})
2914
4076
  ${summary}
2915
4077
  `;
2916
- fs17.appendFileSync(contextPath, entry);
4078
+ fs19.appendFileSync(contextPath, entry);
2917
4079
  }
2918
4080
  var SESSION_GROUP;
2919
4081
  var init_agent = __esm({
@@ -2936,7 +4098,7 @@ var init_agent = __esm({
2936
4098
  });
2937
4099
 
2938
4100
  // src/verify-runner.ts
2939
- import { execFileSync as execFileSync11 } from "child_process";
4101
+ import { execFileSync as execFileSync12 } from "child_process";
2940
4102
  function isExecError(err) {
2941
4103
  return typeof err === "object" && err !== null;
2942
4104
  }
@@ -2972,7 +4134,7 @@ function runCommand(cmd, cwd, timeout) {
2972
4134
  return { success: true, output: "", timedOut: false };
2973
4135
  }
2974
4136
  try {
2975
- const output = execFileSync11(parts[0], parts.slice(1), {
4137
+ const output = execFileSync12(parts[0], parts.slice(1), {
2976
4138
  cwd,
2977
4139
  timeout,
2978
4140
  encoding: "utf-8",
@@ -3018,19 +4180,19 @@ function runQualityGates(taskDir, projectRoot) {
3018
4180
  for (const { name, cmd } of commands) {
3019
4181
  if (!cmd) continue;
3020
4182
  logger.info(` Running ${name}: ${cmd}`);
3021
- const result = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
3022
- if (result.timedOut) {
4183
+ const result2 = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
4184
+ if (result2.timedOut) {
3023
4185
  allErrors.push(`${name}: timed out after ${VERIFY_COMMAND_TIMEOUT_MS / 1e3}s`);
3024
4186
  allPass = false;
3025
4187
  continue;
3026
4188
  }
3027
- if (!result.success) {
4189
+ if (!result2.success) {
3028
4190
  allPass = false;
3029
- const errors = parseErrors(result.output);
4191
+ const errors = parseErrors(result2.output);
3030
4192
  allErrors.push(...errors.map((e) => `[${name}] ${e}`));
3031
- rawOutputs.push({ name, output: result.output.slice(-3e3) });
4193
+ rawOutputs.push({ name, output: result2.output.slice(-3e3) });
3032
4194
  }
3033
- allSummary.push(...extractSummary(result.output, name));
4195
+ allSummary.push(...extractSummary(result2.output, name));
3034
4196
  }
3035
4197
  return { pass: allPass, errors: allErrors, summary: allSummary, rawOutputs };
3036
4198
  }
@@ -3043,7 +4205,7 @@ var init_verify_runner = __esm({
3043
4205
  });
3044
4206
 
3045
4207
  // src/observer.ts
3046
- import { execFileSync as execFileSync12 } from "child_process";
4208
+ import { execFileSync as execFileSync13 } from "child_process";
3047
4209
  async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
3048
4210
  const context = [
3049
4211
  `Stage: ${stageName}`,
@@ -3057,7 +4219,7 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
3057
4219
  ].join("\n");
3058
4220
  const prompt = DIAGNOSIS_PROMPT + context;
3059
4221
  try {
3060
- const result = await runner.run(
4222
+ const result2 = await runner.run(
3061
4223
  "diagnosis",
3062
4224
  prompt,
3063
4225
  model,
@@ -3066,8 +4228,8 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
3066
4228
  "",
3067
4229
  options
3068
4230
  );
3069
- if (result.outcome === "completed" && result.output) {
3070
- const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
4231
+ if (result2.outcome === "completed" && result2.output) {
4232
+ const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
3071
4233
  const parseResult = parseJsonSafe(cleaned, ["classification"]);
3072
4234
  if (parseResult.ok) {
3073
4235
  const { data } = parseResult;
@@ -3126,13 +4288,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
3126
4288
  }
3127
4289
  function getModifiedFiles(projectDir) {
3128
4290
  try {
3129
- const staged = execFileSync12("git", ["diff", "--name-only", "--cached"], {
4291
+ const staged = execFileSync13("git", ["diff", "--name-only", "--cached"], {
3130
4292
  encoding: "utf-8",
3131
4293
  cwd: projectDir,
3132
4294
  timeout: 5e3,
3133
4295
  stdio: ["pipe", "pipe", "pipe"]
3134
4296
  }).trim();
3135
- const unstaged = execFileSync12("git", ["diff", "--name-only"], {
4297
+ const unstaged = execFileSync13("git", ["diff", "--name-only"], {
3136
4298
  encoding: "utf-8",
3137
4299
  cwd: projectDir,
3138
4300
  timeout: 5e3,
@@ -3175,8 +4337,8 @@ Error context:
3175
4337
  });
3176
4338
 
3177
4339
  // src/stages/gate.ts
3178
- import * as fs18 from "fs";
3179
- import * as path16 from "path";
4340
+ import * as fs20 from "fs";
4341
+ import * as path18 from "path";
3180
4342
  function executeGateStage(ctx, def) {
3181
4343
  if (ctx.input.dryRun) {
3182
4344
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -3219,7 +4381,7 @@ ${output}
3219
4381
  `);
3220
4382
  }
3221
4383
  }
3222
- fs18.writeFileSync(path16.join(ctx.taskDir, "verify.md"), lines.join(""));
4384
+ fs20.writeFileSync(path18.join(ctx.taskDir, "verify.md"), lines.join(""));
3223
4385
  return {
3224
4386
  outcome: verifyResult.pass ? "completed" : "failed",
3225
4387
  retries: 0
@@ -3234,9 +4396,9 @@ var init_gate = __esm({
3234
4396
  });
3235
4397
 
3236
4398
  // src/stages/verify.ts
3237
- import * as fs19 from "fs";
3238
- import * as path17 from "path";
3239
- import { execFileSync as execFileSync13 } from "child_process";
4399
+ import * as fs21 from "fs";
4400
+ import * as path19 from "path";
4401
+ import { execFileSync as execFileSync14 } from "child_process";
3240
4402
  async function executeVerifyWithAutofix(ctx, def) {
3241
4403
  const maxAttempts = def.maxRetries ?? 2;
3242
4404
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
@@ -3246,8 +4408,8 @@ async function executeVerifyWithAutofix(ctx, def) {
3246
4408
  return { ...gateResult, retries: attempt };
3247
4409
  }
3248
4410
  if (attempt < maxAttempts) {
3249
- const verifyPath = path17.join(ctx.taskDir, "verify.md");
3250
- const errorOutput = fs19.existsSync(verifyPath) ? fs19.readFileSync(verifyPath, "utf-8") : "Unknown error";
4411
+ const verifyPath = path19.join(ctx.taskDir, "verify.md");
4412
+ const errorOutput = fs21.existsSync(verifyPath) ? fs21.readFileSync(verifyPath, "utf-8") : "Unknown error";
3251
4413
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
3252
4414
  const defaultRunner = getRunnerForStage(ctx, "taskify");
3253
4415
  const diagConfig = getProjectConfig();
@@ -3290,7 +4452,7 @@ ${diagnosis.resolution}`);
3290
4452
  const parts = parseCommand(cmd);
3291
4453
  if (parts.length === 0) return;
3292
4454
  try {
3293
- execFileSync13(parts[0], parts.slice(1), {
4455
+ execFileSync14(parts[0], parts.slice(1), {
3294
4456
  stdio: "pipe",
3295
4457
  timeout: FIX_COMMAND_TIMEOUT_MS
3296
4458
  });
@@ -3343,8 +4505,8 @@ var init_verify = __esm({
3343
4505
  });
3344
4506
 
3345
4507
  // src/review-standalone.ts
3346
- import * as fs20 from "fs";
3347
- import * as path18 from "path";
4508
+ import * as fs22 from "fs";
4509
+ import * as path20 from "path";
3348
4510
  function resolveReviewTarget(input) {
3349
4511
  if (input.prs.length === 0) {
3350
4512
  return {
@@ -3368,8 +4530,8 @@ Or comment on the specific PR: \`@kody review\``
3368
4530
  }
3369
4531
  async function runStandaloneReview(input) {
3370
4532
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
3371
- const taskDir = path18.join(input.projectDir, ".kody", "tasks", taskId);
3372
- fs20.mkdirSync(taskDir, { recursive: true });
4533
+ const taskDir = path20.join(input.projectDir, ".kody", "tasks", taskId);
4534
+ fs22.mkdirSync(taskDir, { recursive: true });
3373
4535
  let diffInstruction = "";
3374
4536
  let filesChangedSection = "";
3375
4537
  if (input.baseBranch) {
@@ -3396,7 +4558,7 @@ ${fileList}`;
3396
4558
  const taskContent = `# ${input.prTitle}
3397
4559
 
3398
4560
  ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
3399
- fs20.writeFileSync(path18.join(taskDir, "task.md"), taskContent);
4561
+ fs22.writeFileSync(path20.join(taskDir, "task.md"), taskContent);
3400
4562
  const reviewDef = STAGES.find((s) => s.name === "review");
3401
4563
  const ctx = {
3402
4564
  taskId,
@@ -3410,18 +4572,18 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
3410
4572
  }
3411
4573
  };
3412
4574
  logger.info(`[review] standalone review for: ${input.prTitle}`);
3413
- const result = await executeAgentStage(ctx, reviewDef);
3414
- if (result.outcome !== "completed") {
4575
+ const result2 = await executeAgentStage(ctx, reviewDef);
4576
+ if (result2.outcome !== "completed") {
3415
4577
  return {
3416
4578
  outcome: "failed",
3417
4579
  taskDir,
3418
- error: result.error ?? "Review stage failed"
4580
+ error: result2.error ?? "Review stage failed"
3419
4581
  };
3420
4582
  }
3421
- const reviewPath = path18.join(taskDir, "review.md");
4583
+ const reviewPath = path20.join(taskDir, "review.md");
3422
4584
  let reviewContent;
3423
- if (fs20.existsSync(reviewPath)) {
3424
- reviewContent = fs20.readFileSync(reviewPath, "utf-8");
4585
+ if (fs22.existsSync(reviewPath)) {
4586
+ reviewContent = fs22.readFileSync(reviewPath, "utf-8");
3425
4587
  }
3426
4588
  return {
3427
4589
  outcome: "completed",
@@ -3461,8 +4623,8 @@ var init_review_standalone = __esm({
3461
4623
  });
3462
4624
 
3463
4625
  // src/stages/review.ts
3464
- import * as fs21 from "fs";
3465
- import * as path19 from "path";
4626
+ import * as fs23 from "fs";
4627
+ import * as path21 from "path";
3466
4628
  async function executeReviewWithFix(ctx, def) {
3467
4629
  if (ctx.input.dryRun) {
3468
4630
  return { outcome: "completed", retries: 0 };
@@ -3476,11 +4638,11 @@ async function executeReviewWithFix(ctx, def) {
3476
4638
  if (reviewResult.outcome !== "completed") {
3477
4639
  return reviewResult;
3478
4640
  }
3479
- const reviewFile = path19.join(ctx.taskDir, "review.md");
3480
- if (!fs21.existsSync(reviewFile)) {
4641
+ const reviewFile = path21.join(ctx.taskDir, "review.md");
4642
+ if (!fs23.existsSync(reviewFile)) {
3481
4643
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
3482
4644
  }
3483
- const content = fs21.readFileSync(reviewFile, "utf-8");
4645
+ const content = fs23.readFileSync(reviewFile, "utf-8");
3484
4646
  if (detectReviewVerdict(content) !== "fail") {
3485
4647
  return { ...reviewResult, retries: iteration };
3486
4648
  }
@@ -3509,15 +4671,15 @@ var init_review = __esm({
3509
4671
  });
3510
4672
 
3511
4673
  // src/stages/ship.ts
3512
- import * as fs22 from "fs";
3513
- import * as path20 from "path";
3514
- import { execFileSync as execFileSync14 } from "child_process";
4674
+ import * as fs24 from "fs";
4675
+ import * as path22 from "path";
4676
+ import { execFileSync as execFileSync15 } from "child_process";
3515
4677
  function buildPrBody(ctx) {
3516
4678
  const sections = [];
3517
- const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3518
- if (fs22.existsSync(taskJsonPath)) {
4679
+ const taskJsonPath = path22.join(ctx.taskDir, "task.json");
4680
+ if (fs24.existsSync(taskJsonPath)) {
3519
4681
  try {
3520
- const raw = fs22.readFileSync(taskJsonPath, "utf-8");
4682
+ const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3521
4683
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3522
4684
  const task = JSON.parse(cleaned);
3523
4685
  if (task.description) {
@@ -3536,9 +4698,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
3536
4698
  } catch {
3537
4699
  }
3538
4700
  }
3539
- const reviewPath = path20.join(ctx.taskDir, "review.md");
3540
- if (fs22.existsSync(reviewPath)) {
3541
- const review = fs22.readFileSync(reviewPath, "utf-8");
4701
+ const reviewPath = path22.join(ctx.taskDir, "review.md");
4702
+ if (fs24.existsSync(reviewPath)) {
4703
+ const review = fs24.readFileSync(reviewPath, "utf-8");
3542
4704
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
3543
4705
  if (summaryMatch) {
3544
4706
  const summary = summaryMatch[1].trim();
@@ -3555,14 +4717,14 @@ ${summary}`);
3555
4717
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
3556
4718
  }
3557
4719
  }
3558
- const verifyPath = path20.join(ctx.taskDir, "verify.md");
3559
- if (fs22.existsSync(verifyPath)) {
3560
- const verify = fs22.readFileSync(verifyPath, "utf-8");
4720
+ const verifyPath = path22.join(ctx.taskDir, "verify.md");
4721
+ if (fs24.existsSync(verifyPath)) {
4722
+ const verify = fs24.readFileSync(verifyPath, "utf-8");
3561
4723
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
3562
4724
  }
3563
- const planPath = path20.join(ctx.taskDir, "plan.md");
3564
- if (fs22.existsSync(planPath)) {
3565
- const plan = fs22.readFileSync(planPath, "utf-8").trim();
4725
+ const planPath = path22.join(ctx.taskDir, "plan.md");
4726
+ if (fs24.existsSync(planPath)) {
4727
+ const plan = fs24.readFileSync(planPath, "utf-8").trim();
3566
4728
  if (plan) {
3567
4729
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
3568
4730
  sections.push(`
@@ -3582,25 +4744,25 @@ Closes #${ctx.input.issueNumber}`);
3582
4744
  return sections.join("\n");
3583
4745
  }
3584
4746
  function executeShipStage(ctx, _def) {
3585
- const shipPath = path20.join(ctx.taskDir, "ship.md");
4747
+ const shipPath = path22.join(ctx.taskDir, "ship.md");
3586
4748
  if (ctx.input.dryRun) {
3587
- fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
4749
+ fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
3588
4750
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3589
4751
  }
3590
4752
  if (ctx.input.local && !ctx.input.issueNumber) {
3591
- fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
4753
+ fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
3592
4754
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3593
4755
  }
3594
4756
  try {
3595
4757
  const head = getCurrentBranch(ctx.projectDir);
3596
4758
  const base = getDefaultBranch(ctx.projectDir);
3597
4759
  try {
3598
- execFileSync14("git", ["add", ctx.taskDir], {
4760
+ execFileSync15("git", ["add", ctx.taskDir], {
3599
4761
  cwd: ctx.projectDir,
3600
4762
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
3601
4763
  stdio: "pipe"
3602
4764
  });
3603
- execFileSync14("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
4765
+ execFileSync15("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
3604
4766
  cwd: ctx.projectDir,
3605
4767
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
3606
4768
  stdio: "pipe"
@@ -3614,7 +4776,7 @@ function executeShipStage(ctx, _def) {
3614
4776
  let repo = config.github?.repo;
3615
4777
  if (!owner || !repo) {
3616
4778
  try {
3617
- const remoteUrl = execFileSync14("git", ["remote", "get-url", "origin"], {
4779
+ const remoteUrl = execFileSync15("git", ["remote", "get-url", "origin"], {
3618
4780
  encoding: "utf-8",
3619
4781
  cwd: ctx.projectDir
3620
4782
  }).trim();
@@ -3635,28 +4797,28 @@ function executeShipStage(ctx, _def) {
3635
4797
  chore: "chore"
3636
4798
  };
3637
4799
  let prefix = "chore";
3638
- const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3639
- if (fs22.existsSync(taskJsonPath)) {
4800
+ const taskJsonPath = path22.join(ctx.taskDir, "task.json");
4801
+ if (fs24.existsSync(taskJsonPath)) {
3640
4802
  try {
3641
- const raw = fs22.readFileSync(taskJsonPath, "utf-8");
4803
+ const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3642
4804
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3643
4805
  const task = JSON.parse(cleaned);
3644
4806
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
3645
4807
  } catch {
3646
4808
  }
3647
4809
  }
3648
- const taskMdPath = path20.join(ctx.taskDir, "task.md");
3649
- if (fs22.existsSync(taskMdPath)) {
3650
- const content = fs22.readFileSync(taskMdPath, "utf-8");
4810
+ const taskMdPath = path22.join(ctx.taskDir, "task.md");
4811
+ if (fs24.existsSync(taskMdPath)) {
4812
+ const content = fs24.readFileSync(taskMdPath, "utf-8");
3651
4813
  const heading = content.split("\n").find((l) => l.startsWith("# "));
3652
4814
  if (heading) {
3653
4815
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
3654
4816
  }
3655
4817
  }
3656
4818
  if (title === "Update") {
3657
- if (fs22.existsSync(taskJsonPath)) {
4819
+ if (fs24.existsSync(taskJsonPath)) {
3658
4820
  try {
3659
- const raw = fs22.readFileSync(taskJsonPath, "utf-8");
4821
+ const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3660
4822
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3661
4823
  const task = JSON.parse(cleaned);
3662
4824
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -3679,7 +4841,7 @@ function executeShipStage(ctx, _def) {
3679
4841
  } catch {
3680
4842
  }
3681
4843
  }
3682
- fs22.writeFileSync(shipPath, `# Ship
4844
+ fs24.writeFileSync(shipPath, `# Ship
3683
4845
 
3684
4846
  Updated existing PR: ${existingPr.url}
3685
4847
  PR #${existingPr.number}
@@ -3700,20 +4862,20 @@ PR #${existingPr.number}
3700
4862
  } catch {
3701
4863
  }
3702
4864
  }
3703
- fs22.writeFileSync(shipPath, `# Ship
4865
+ fs24.writeFileSync(shipPath, `# Ship
3704
4866
 
3705
4867
  PR created: ${pr.url}
3706
4868
  PR #${pr.number}
3707
4869
  `);
3708
4870
  } else {
3709
- fs22.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
4871
+ fs24.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3710
4872
  }
3711
4873
  }
3712
4874
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3713
4875
  } catch (err) {
3714
4876
  const msg = err instanceof Error ? err.message : String(err);
3715
4877
  try {
3716
- fs22.writeFileSync(shipPath, `# Ship
4878
+ fs24.writeFileSync(shipPath, `# Ship
3717
4879
 
3718
4880
  Failed: ${msg}
3719
4881
  `);
@@ -3762,15 +4924,15 @@ var init_executor_registry = __esm({
3762
4924
  });
3763
4925
 
3764
4926
  // src/pipeline/questions.ts
3765
- import * as fs23 from "fs";
3766
- import * as path21 from "path";
4927
+ import * as fs25 from "fs";
4928
+ import * as path23 from "path";
3767
4929
  function checkForQuestions(ctx, stageName) {
3768
4930
  if (ctx.input.local || !ctx.input.issueNumber) return false;
3769
4931
  try {
3770
4932
  if (stageName === "taskify") {
3771
- const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3772
- if (!fs23.existsSync(taskJsonPath)) return false;
3773
- const raw = fs23.readFileSync(taskJsonPath, "utf-8");
4933
+ const taskJsonPath = path23.join(ctx.taskDir, "task.json");
4934
+ if (!fs25.existsSync(taskJsonPath)) return false;
4935
+ const raw = fs25.readFileSync(taskJsonPath, "utf-8");
3774
4936
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3775
4937
  const taskJson = JSON.parse(cleaned);
3776
4938
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -3785,9 +4947,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
3785
4947
  }
3786
4948
  }
3787
4949
  if (stageName === "plan") {
3788
- const planPath = path21.join(ctx.taskDir, "plan.md");
3789
- if (!fs23.existsSync(planPath)) return false;
3790
- const plan = fs23.readFileSync(planPath, "utf-8");
4950
+ const planPath = path23.join(ctx.taskDir, "plan.md");
4951
+ if (!fs25.existsSync(planPath)) return false;
4952
+ const plan = fs25.readFileSync(planPath, "utf-8");
3791
4953
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
3792
4954
  if (questionsMatch) {
3793
4955
  const questionsText = questionsMatch[1].trim();
@@ -3816,8 +4978,8 @@ var init_questions = __esm({
3816
4978
  });
3817
4979
 
3818
4980
  // src/pipeline/hooks.ts
3819
- import * as fs24 from "fs";
3820
- import * as path22 from "path";
4981
+ import * as fs26 from "fs";
4982
+ import * as path24 from "path";
3821
4983
  function applyPreStageLabel(ctx, def) {
3822
4984
  if (!ctx.input.issueNumber || ctx.input.local) return;
3823
4985
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
@@ -3855,9 +5017,9 @@ function autoDetectComplexity(ctx, def) {
3855
5017
  return { complexity, activeStages };
3856
5018
  }
3857
5019
  try {
3858
- const taskJsonPath = path22.join(ctx.taskDir, "task.json");
3859
- if (!fs24.existsSync(taskJsonPath)) return null;
3860
- const raw = fs24.readFileSync(taskJsonPath, "utf-8");
5020
+ const taskJsonPath = path24.join(ctx.taskDir, "task.json");
5021
+ if (!fs26.existsSync(taskJsonPath)) return null;
5022
+ const raw = fs26.readFileSync(taskJsonPath, "utf-8");
3861
5023
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3862
5024
  const taskJson = JSON.parse(cleaned);
3863
5025
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -3887,8 +5049,8 @@ function checkRiskGate(ctx, def, state, complexity) {
3887
5049
  if (ctx.input.dryRun || ctx.input.local) return null;
3888
5050
  if (ctx.input.mode === "rerun") return null;
3889
5051
  if (!ctx.input.issueNumber) return null;
3890
- const planPath = path22.join(ctx.taskDir, "plan.md");
3891
- const plan = fs24.existsSync(planPath) ? fs24.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
5052
+ const planPath = path24.join(ctx.taskDir, "plan.md");
5053
+ const plan = fs26.existsSync(planPath) ? fs26.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
3892
5054
  try {
3893
5055
  postComment(
3894
5056
  ctx.input.issueNumber,
@@ -3955,22 +5117,22 @@ var init_hooks = __esm({
3955
5117
  });
3956
5118
 
3957
5119
  // src/learning/auto-learn.ts
3958
- import * as fs25 from "fs";
3959
- import * as path23 from "path";
5120
+ import * as fs27 from "fs";
5121
+ import * as path25 from "path";
3960
5122
  function stripAnsi(str) {
3961
5123
  return str.replace(/\x1b\[[0-9;]*m/g, "");
3962
5124
  }
3963
5125
  function autoLearn(ctx) {
3964
5126
  try {
3965
- const memoryDir = path23.join(ctx.projectDir, ".kody", "memory");
3966
- if (!fs25.existsSync(memoryDir)) {
3967
- fs25.mkdirSync(memoryDir, { recursive: true });
5127
+ const memoryDir = path25.join(ctx.projectDir, ".kody", "memory");
5128
+ if (!fs27.existsSync(memoryDir)) {
5129
+ fs27.mkdirSync(memoryDir, { recursive: true });
3968
5130
  }
3969
5131
  const learnings = [];
3970
5132
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3971
- const verifyPath = path23.join(ctx.taskDir, "verify.md");
3972
- if (fs25.existsSync(verifyPath)) {
3973
- const verify = stripAnsi(fs25.readFileSync(verifyPath, "utf-8"));
5133
+ const verifyPath = path25.join(ctx.taskDir, "verify.md");
5134
+ if (fs27.existsSync(verifyPath)) {
5135
+ const verify = stripAnsi(fs27.readFileSync(verifyPath, "utf-8"));
3974
5136
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
3975
5137
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
3976
5138
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -3979,18 +5141,18 @@ function autoLearn(ctx) {
3979
5141
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
3980
5142
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
3981
5143
  }
3982
- const reviewPath = path23.join(ctx.taskDir, "review.md");
3983
- if (fs25.existsSync(reviewPath)) {
3984
- const review = fs25.readFileSync(reviewPath, "utf-8");
5144
+ const reviewPath = path25.join(ctx.taskDir, "review.md");
5145
+ if (fs27.existsSync(reviewPath)) {
5146
+ const review = fs27.readFileSync(reviewPath, "utf-8");
3985
5147
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
3986
5148
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
3987
5149
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
3988
5150
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
3989
5151
  }
3990
- const taskJsonPath = path23.join(ctx.taskDir, "task.json");
3991
- if (fs25.existsSync(taskJsonPath)) {
5152
+ const taskJsonPath = path25.join(ctx.taskDir, "task.json");
5153
+ if (fs27.existsSync(taskJsonPath)) {
3992
5154
  try {
3993
- const raw = stripAnsi(fs25.readFileSync(taskJsonPath, "utf-8"));
5155
+ const raw = stripAnsi(fs27.readFileSync(taskJsonPath, "utf-8"));
3994
5156
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3995
5157
  const task = JSON.parse(cleaned);
3996
5158
  if (task.scope && Array.isArray(task.scope)) {
@@ -4001,12 +5163,12 @@ function autoLearn(ctx) {
4001
5163
  }
4002
5164
  }
4003
5165
  if (learnings.length > 0) {
4004
- const conventionsPath = path23.join(memoryDir, "conventions.md");
5166
+ const conventionsPath = path25.join(memoryDir, "conventions.md");
4005
5167
  const entry = `
4006
5168
  ## Learned ${timestamp2} (task: ${ctx.taskId})
4007
5169
  ${learnings.join("\n")}
4008
5170
  `;
4009
- fs25.appendFileSync(conventionsPath, entry);
5171
+ fs27.appendFileSync(conventionsPath, entry);
4010
5172
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
4011
5173
  }
4012
5174
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
@@ -4014,8 +5176,8 @@ ${learnings.join("\n")}
4014
5176
  }
4015
5177
  }
4016
5178
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
4017
- const archPath = path23.join(memoryDir, "architecture.md");
4018
- if (fs25.existsSync(archPath)) return;
5179
+ const archPath = path25.join(memoryDir, "architecture.md");
5180
+ if (fs27.existsSync(archPath)) return;
4019
5181
  const detected = detectArchitectureBasic(projectDir);
4020
5182
  if (detected.length > 0) {
4021
5183
  const content = `# Architecture (auto-detected ${timestamp2})
@@ -4023,7 +5185,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
4023
5185
  ## Overview
4024
5186
  ${detected.join("\n")}
4025
5187
  `;
4026
- fs25.writeFileSync(archPath, content);
5188
+ fs27.writeFileSync(archPath, content);
4027
5189
  logger.info(`Auto-detected architecture (${detected.length} items)`);
4028
5190
  }
4029
5191
  }
@@ -4036,13 +5198,13 @@ var init_auto_learn = __esm({
4036
5198
  });
4037
5199
 
4038
5200
  // src/retrospective.ts
4039
- import * as fs26 from "fs";
4040
- import * as path24 from "path";
5201
+ import * as fs28 from "fs";
5202
+ import * as path26 from "path";
4041
5203
  function readArtifact(taskDir, filename, maxChars) {
4042
- const p = path24.join(taskDir, filename);
4043
- if (!fs26.existsSync(p)) return null;
5204
+ const p = path26.join(taskDir, filename);
5205
+ if (!fs28.existsSync(p)) return null;
4044
5206
  try {
4045
- const content = fs26.readFileSync(p, "utf-8");
5207
+ const content = fs28.readFileSync(p, "utf-8");
4046
5208
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
4047
5209
  } catch {
4048
5210
  return null;
@@ -4095,13 +5257,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
4095
5257
  return lines.join("\n");
4096
5258
  }
4097
5259
  function getLogPath(projectDir) {
4098
- return path24.join(projectDir, ".kody", "memory", "observer-log.jsonl");
5260
+ return path26.join(projectDir, ".kody", "memory", "observer-log.jsonl");
4099
5261
  }
4100
5262
  function readPreviousRetrospectives(projectDir, limit = 10) {
4101
5263
  const logPath = getLogPath(projectDir);
4102
- if (!fs26.existsSync(logPath)) return [];
5264
+ if (!fs28.existsSync(logPath)) return [];
4103
5265
  try {
4104
- const content = fs26.readFileSync(logPath, "utf-8");
5266
+ const content = fs28.readFileSync(logPath, "utf-8");
4105
5267
  const lines = content.split("\n").filter(Boolean);
4106
5268
  const entries = [];
4107
5269
  const start = Math.max(0, lines.length - limit);
@@ -4128,11 +5290,11 @@ function formatPreviousEntries(entries) {
4128
5290
  }
4129
5291
  function appendRetrospectiveEntry(projectDir, entry) {
4130
5292
  const logPath = getLogPath(projectDir);
4131
- const dir = path24.dirname(logPath);
4132
- if (!fs26.existsSync(dir)) {
4133
- fs26.mkdirSync(dir, { recursive: true });
5293
+ const dir = path26.dirname(logPath);
5294
+ if (!fs28.existsSync(dir)) {
5295
+ fs28.mkdirSync(dir, { recursive: true });
4134
5296
  }
4135
- fs26.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5297
+ fs28.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4136
5298
  }
4137
5299
  async function runRetrospective(ctx, state, pipelineStartTime) {
4138
5300
  if (ctx.input.dryRun) return;
@@ -4154,7 +5316,7 @@ ${previousText}
4154
5316
  if (needsLitellmProxy(config)) {
4155
5317
  extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
4156
5318
  }
4157
- const result = await runner.run("retrospective", prompt, model, 3e4, "", {
5319
+ const result2 = await runner.run("retrospective", prompt, model, 3e4, "", {
4158
5320
  cwd: ctx.projectDir,
4159
5321
  env: extraEnv
4160
5322
  });
@@ -4162,8 +5324,8 @@ ${previousText}
4162
5324
  let patternMatch = null;
4163
5325
  let suggestion = "No suggestion";
4164
5326
  let pipelineFlaw = null;
4165
- if (result.outcome === "completed" && result.output) {
4166
- const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
5327
+ if (result2.outcome === "completed" && result2.output) {
5328
+ const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
4167
5329
  try {
4168
5330
  const parsed = JSON.parse(cleaned);
4169
5331
  observation = parsed.observation ?? observation;
@@ -4300,8 +5462,8 @@ var init_summary = __esm({
4300
5462
  });
4301
5463
 
4302
5464
  // src/pipeline.ts
4303
- import * as fs27 from "fs";
4304
- import * as path25 from "path";
5465
+ import * as fs29 from "fs";
5466
+ import * as path27 from "path";
4305
5467
  function ensureFeatureBranchIfNeeded(ctx) {
4306
5468
  if (ctx.input.dryRun) return;
4307
5469
  if (ctx.input.prNumber) {
@@ -4314,8 +5476,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
4314
5476
  }
4315
5477
  if (!ctx.input.issueNumber) return;
4316
5478
  try {
4317
- const taskMdPath = path25.join(ctx.taskDir, "task.md");
4318
- const title = fs27.existsSync(taskMdPath) ? fs27.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
5479
+ const taskMdPath = path27.join(ctx.taskDir, "task.md");
5480
+ const title = fs29.existsSync(taskMdPath) ? fs29.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
4319
5481
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
4320
5482
  syncWithDefault(ctx.projectDir);
4321
5483
  } catch (err) {
@@ -4329,10 +5491,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
4329
5491
  }
4330
5492
  }
4331
5493
  function acquireLock(taskDir) {
4332
- const lockPath = path25.join(taskDir, ".lock");
4333
- if (fs27.existsSync(lockPath)) {
5494
+ const lockPath = path27.join(taskDir, ".lock");
5495
+ if (fs29.existsSync(lockPath)) {
4334
5496
  try {
4335
- const pid = parseInt(fs27.readFileSync(lockPath, "utf-8").trim(), 10);
5497
+ const pid = parseInt(fs29.readFileSync(lockPath, "utf-8").trim(), 10);
4336
5498
  if (!isNaN(pid)) {
4337
5499
  try {
4338
5500
  process.kill(pid, 0);
@@ -4349,14 +5511,14 @@ function acquireLock(taskDir) {
4349
5511
  logger.warn(` Corrupt lock file \u2014 overwriting`);
4350
5512
  }
4351
5513
  try {
4352
- fs27.unlinkSync(lockPath);
5514
+ fs29.unlinkSync(lockPath);
4353
5515
  } catch {
4354
5516
  }
4355
5517
  }
4356
5518
  try {
4357
- const fd = fs27.openSync(lockPath, fs27.constants.O_WRONLY | fs27.constants.O_CREAT | fs27.constants.O_EXCL);
4358
- fs27.writeSync(fd, String(process.pid));
4359
- fs27.closeSync(fd);
5519
+ const fd = fs29.openSync(lockPath, fs29.constants.O_WRONLY | fs29.constants.O_CREAT | fs29.constants.O_EXCL);
5520
+ fs29.writeSync(fd, String(process.pid));
5521
+ fs29.closeSync(fd);
4360
5522
  } catch (err) {
4361
5523
  if (err.code === "EEXIST") {
4362
5524
  throw new Error("Pipeline already running (lock acquired by another process)");
@@ -4366,7 +5528,7 @@ function acquireLock(taskDir) {
4366
5528
  }
4367
5529
  function releaseLock(taskDir) {
4368
5530
  try {
4369
- fs27.unlinkSync(path25.join(taskDir, ".lock"));
5531
+ fs29.unlinkSync(path27.join(taskDir, ".lock"));
4370
5532
  } catch {
4371
5533
  }
4372
5534
  }
@@ -4455,23 +5617,23 @@ async function runPipelineInner(ctx) {
4455
5617
  writeState(state, ctx.taskDir);
4456
5618
  logger.info(`[${def.name}] starting...`);
4457
5619
  applyPreStageLabel(ctx, def);
4458
- let result;
5620
+ let result2;
4459
5621
  try {
4460
- result = await getExecutor(def.name)(ctx, def);
5622
+ result2 = await getExecutor(def.name)(ctx, def);
4461
5623
  } catch (error) {
4462
- result = {
5624
+ result2 = {
4463
5625
  outcome: "failed",
4464
5626
  retries: 0,
4465
5627
  error: error instanceof Error ? error.message : String(error)
4466
5628
  };
4467
5629
  }
4468
5630
  ciGroupEnd();
4469
- if (result.outcome === "completed") {
5631
+ if (result2.outcome === "completed") {
4470
5632
  state.stages[def.name] = {
4471
5633
  state: "completed",
4472
5634
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
4473
- retries: result.retries,
4474
- outputFile: result.outputFile
5635
+ retries: result2.retries,
5636
+ outputFile: result2.outputFile
4475
5637
  };
4476
5638
  logger.info(`[${def.name}] \u2713 completed`);
4477
5639
  const detected = autoDetectComplexity(ctx, def);
@@ -4485,16 +5647,16 @@ async function runPipelineInner(ctx) {
4485
5647
  if (gated) return gated;
4486
5648
  commitAfterStage(ctx, def);
4487
5649
  } else {
4488
- const isTimeout = result.outcome === "timed_out";
5650
+ const isTimeout = result2.outcome === "timed_out";
4489
5651
  state.stages[def.name] = {
4490
5652
  state: isTimeout ? "timeout" : "failed",
4491
- retries: result.retries,
4492
- error: isTimeout ? "Stage timed out" : result.error ?? "Stage failed"
5653
+ retries: result2.retries,
5654
+ error: isTimeout ? "Stage timed out" : result2.error ?? "Stage failed"
4493
5655
  };
4494
5656
  state.state = "failed";
4495
5657
  state.sessions = ctx.sessions;
4496
5658
  writeState(state, ctx.taskDir);
4497
- logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result.error}`}`);
5659
+ logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result2.error}`}`);
4498
5660
  if (ctx.input.issueNumber && !ctx.input.local) {
4499
5661
  setLifecycleLabel(ctx.input.issueNumber, "failed");
4500
5662
  }
@@ -4574,8 +5736,8 @@ var init_pipeline = __esm({
4574
5736
  });
4575
5737
 
4576
5738
  // src/preflight.ts
4577
- import { execFileSync as execFileSync15 } from "child_process";
4578
- import * as fs28 from "fs";
5739
+ import { execFileSync as execFileSync16 } from "child_process";
5740
+ import * as fs30 from "fs";
4579
5741
  function check(name, fn) {
4580
5742
  try {
4581
5743
  const detail = fn() ?? void 0;
@@ -4587,7 +5749,7 @@ function check(name, fn) {
4587
5749
  function runPreflight() {
4588
5750
  const checks = [
4589
5751
  check("claude CLI", () => {
4590
- const v = execFileSync15("claude", ["--version"], {
5752
+ const v = execFileSync16("claude", ["--version"], {
4591
5753
  encoding: "utf-8",
4592
5754
  timeout: 1e4,
4593
5755
  stdio: ["pipe", "pipe", "pipe"]
@@ -4595,14 +5757,14 @@ function runPreflight() {
4595
5757
  return v;
4596
5758
  }),
4597
5759
  check("git repo", () => {
4598
- execFileSync15("git", ["rev-parse", "--is-inside-work-tree"], {
5760
+ execFileSync16("git", ["rev-parse", "--is-inside-work-tree"], {
4599
5761
  encoding: "utf-8",
4600
5762
  timeout: 5e3,
4601
5763
  stdio: ["pipe", "pipe", "pipe"]
4602
5764
  });
4603
5765
  }),
4604
5766
  check("pnpm", () => {
4605
- const v = execFileSync15("pnpm", ["--version"], {
5767
+ const v = execFileSync16("pnpm", ["--version"], {
4606
5768
  encoding: "utf-8",
4607
5769
  timeout: 5e3,
4608
5770
  stdio: ["pipe", "pipe", "pipe"]
@@ -4610,7 +5772,7 @@ function runPreflight() {
4610
5772
  return v;
4611
5773
  }),
4612
5774
  check("node >= 18", () => {
4613
- const v = execFileSync15("node", ["--version"], {
5775
+ const v = execFileSync16("node", ["--version"], {
4614
5776
  encoding: "utf-8",
4615
5777
  timeout: 5e3,
4616
5778
  stdio: ["pipe", "pipe", "pipe"]
@@ -4620,7 +5782,7 @@ function runPreflight() {
4620
5782
  return v;
4621
5783
  }),
4622
5784
  check("gh CLI", () => {
4623
- const v = execFileSync15("gh", ["--version"], {
5785
+ const v = execFileSync16("gh", ["--version"], {
4624
5786
  encoding: "utf-8",
4625
5787
  timeout: 5e3,
4626
5788
  stdio: ["pipe", "pipe", "pipe"]
@@ -4628,7 +5790,7 @@ function runPreflight() {
4628
5790
  return v;
4629
5791
  }),
4630
5792
  check("package.json", () => {
4631
- if (!fs28.existsSync("package.json")) throw new Error("not found");
5793
+ if (!fs30.existsSync("package.json")) throw new Error("not found");
4632
5794
  })
4633
5795
  ];
4634
5796
  const failed = checks.filter((c) => !c.ok);
@@ -4705,8 +5867,8 @@ var init_args = __esm({
4705
5867
  });
4706
5868
 
4707
5869
  // src/cli/task-state.ts
4708
- import * as fs29 from "fs";
4709
- import * as path26 from "path";
5870
+ import * as fs31 from "fs";
5871
+ import * as path28 from "path";
4710
5872
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
4711
5873
  if (!existingTaskId || !existingState) {
4712
5874
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -4738,11 +5900,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
4738
5900
  function resolveForIssue(issueNumber, projectDir) {
4739
5901
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
4740
5902
  if (existingTaskId) {
4741
- const statusPath = path26.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
5903
+ const statusPath = path28.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
4742
5904
  let existingState = null;
4743
- if (fs29.existsSync(statusPath)) {
5905
+ if (fs31.existsSync(statusPath)) {
4744
5906
  try {
4745
- existingState = JSON.parse(fs29.readFileSync(statusPath, "utf-8"));
5907
+ existingState = JSON.parse(fs31.readFileSync(statusPath, "utf-8"));
4746
5908
  } catch {
4747
5909
  }
4748
5910
  }
@@ -4775,12 +5937,12 @@ var resolve_exports = {};
4775
5937
  __export(resolve_exports, {
4776
5938
  runResolve: () => runResolve
4777
5939
  });
4778
- import { execFileSync as execFileSync16 } from "child_process";
5940
+ import { execFileSync as execFileSync17 } from "child_process";
4779
5941
  function getConflictContext(cwd, files) {
4780
5942
  const parts = [];
4781
5943
  for (const file of files.slice(0, 10)) {
4782
5944
  try {
4783
- const content = execFileSync16("git", ["diff", file], {
5945
+ const content = execFileSync17("git", ["diff", file], {
4784
5946
  cwd,
4785
5947
  encoding: "utf-8",
4786
5948
  stdio: ["pipe", "pipe", "pipe"]
@@ -4830,12 +5992,12 @@ async function runResolve(options) {
4830
5992
  extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
4831
5993
  }
4832
5994
  logger.info(` Running agent to resolve conflicts (model=${model})...`);
4833
- const result = await runner.run("resolve", prompt, model, 3e5, projectDir, {
5995
+ const result2 = await runner.run("resolve", prompt, model, 3e5, projectDir, {
4834
5996
  cwd: projectDir,
4835
5997
  env: extraEnv
4836
5998
  });
4837
- if (result.outcome !== "completed") {
4838
- return { outcome: "failed", error: `Agent failed: ${result.error}` };
5999
+ if (result2.outcome !== "completed") {
6000
+ return { outcome: "failed", error: `Agent failed: ${result2.error}` };
4839
6001
  }
4840
6002
  logger.info(" Verifying resolution...");
4841
6003
  const verify = runQualityGates(projectDir, projectDir);
@@ -4899,8 +6061,8 @@ var init_resolve = __esm({
4899
6061
 
4900
6062
  // src/entry.ts
4901
6063
  var entry_exports = {};
4902
- import * as fs30 from "fs";
4903
- import * as path27 from "path";
6064
+ import * as fs32 from "fs";
6065
+ import * as path29 from "path";
4904
6066
  async function ensureLitellmProxy(config, projectDir) {
4905
6067
  if (!anyStageNeedsProxy(config)) return null;
4906
6068
  const litellmUrl = getLitellmUrl();
@@ -4935,7 +6097,7 @@ async function ensureLitellmProxy(config, projectDir) {
4935
6097
  return litellmProcess;
4936
6098
  }
4937
6099
  async function runModelHealthCheck(config) {
4938
- const usesProxy = needsLitellmProxy(config);
6100
+ const usesProxy = anyStageNeedsProxy(config);
4939
6101
  const baseUrl = usesProxy ? getLitellmUrl() : "https://api.anthropic.com";
4940
6102
  const apiKey = usesProxy ? process.env.ANTHROPIC_COMPATIBLE_API_KEY : process.env.ANTHROPIC_API_KEY;
4941
6103
  if (!apiKey) {
@@ -4945,19 +6107,19 @@ async function runModelHealthCheck(config) {
4945
6107
  }
4946
6108
  const model = config.agent.modelMap.cheap;
4947
6109
  logger.info(`Model health check (${model} via ${usesProxy ? "LiteLLM" : "Anthropic"})...`);
4948
- const result = await checkModelHealth(baseUrl, apiKey, model);
4949
- if (result.ok) {
6110
+ const result2 = await checkModelHealth(baseUrl, apiKey, model);
6111
+ if (result2.ok) {
4950
6112
  logger.info(" \u2713 Model responded");
4951
6113
  } else {
4952
- logger.error(` \u2717 Model health check failed: ${result.error}`);
6114
+ logger.error(` \u2717 Model health check failed: ${result2.error}`);
4953
6115
  process.exit(1);
4954
6116
  }
4955
6117
  }
4956
6118
  async function main() {
4957
6119
  const input = parseArgs();
4958
- const projectDir = input.cwd ? path27.resolve(input.cwd) : process.cwd();
6120
+ const projectDir = input.cwd ? path29.resolve(input.cwd) : process.cwd();
4959
6121
  if (input.cwd) {
4960
- if (!fs30.existsSync(projectDir)) {
6122
+ if (!fs32.existsSync(projectDir)) {
4961
6123
  console.error(`--cwd path does not exist: ${projectDir}`);
4962
6124
  process.exit(1);
4963
6125
  }
@@ -4966,7 +6128,7 @@ async function main() {
4966
6128
  logger.info(`Working directory: ${projectDir}`);
4967
6129
  }
4968
6130
  const isPRFix = (input.command === "fix" || input.command === "fix-ci") && !!input.prNumber;
4969
- const skipStateCheck = input.command === "review" || input.command === "resolve" || input.command === "rerun";
6131
+ const skipStateCheck = input.command === "review" || input.command === "resolve" || input.command === "rerun" || input.command === "status";
4970
6132
  if (input.issueNumber && !skipStateCheck && !isPRFix) {
4971
6133
  const taskAction = resolveForIssue(input.issueNumber, projectDir);
4972
6134
  logger.info(`Task action: ${taskAction.action}`);
@@ -5023,8 +6185,8 @@ async function main() {
5023
6185
  process.exit(1);
5024
6186
  }
5025
6187
  }
5026
- const taskDir = path27.join(projectDir, ".kody", "tasks", taskId);
5027
- fs30.mkdirSync(taskDir, { recursive: true });
6188
+ const taskDir = path29.join(projectDir, ".kody", "tasks", taskId);
6189
+ fs32.mkdirSync(taskDir, { recursive: true });
5028
6190
  if (input.command === "rerun" && isTaskifyRun(taskDir)) {
5029
6191
  const marker = readTaskifyMarker(taskDir);
5030
6192
  if (marker) {
@@ -5089,7 +6251,7 @@ async function main() {
5089
6251
  console.error(`Runner "${defaultRunnerName2}" health check failed`);
5090
6252
  process.exit(1);
5091
6253
  }
5092
- const result = await runStandaloneReview({
6254
+ const result2 = await runStandaloneReview({
5093
6255
  projectDir,
5094
6256
  runners: runners2,
5095
6257
  prTitle,
@@ -5099,15 +6261,15 @@ async function main() {
5099
6261
  taskId
5100
6262
  });
5101
6263
  if (litellmProcess2) litellmProcess2.kill();
5102
- if (result.outcome === "failed") {
5103
- console.error(`Review failed: ${result.error}`);
6264
+ if (result2.outcome === "failed") {
6265
+ console.error(`Review failed: ${result2.error}`);
5104
6266
  process.exit(1);
5105
6267
  }
5106
- if (result.reviewContent) {
5107
- console.log(result.reviewContent);
6268
+ if (result2.reviewContent) {
6269
+ console.log(result2.reviewContent);
5108
6270
  if (!input.local && prNumber) {
5109
- const comment = formatReviewComment(result.reviewContent, taskId);
5110
- const verdict = detectReviewVerdict(result.reviewContent);
6271
+ const comment = formatReviewComment(result2.reviewContent, taskId);
6272
+ const verdict = detectReviewVerdict(result2.reviewContent);
5111
6273
  const event = verdict === "fail" ? "request-changes" : "approve";
5112
6274
  const posted = submitPRReview(prNumber, comment, event);
5113
6275
  if (!posted) {
@@ -5139,48 +6301,48 @@ async function main() {
5139
6301
  process.exit(1);
5140
6302
  }
5141
6303
  const { runResolve: runResolve2 } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
5142
- const result = await runResolve2({
6304
+ const result2 = await runResolve2({
5143
6305
  prNumber: input.prNumber,
5144
6306
  projectDir,
5145
6307
  runners: runners2,
5146
6308
  local: input.local ?? true
5147
6309
  });
5148
6310
  if (litellmProcess2) litellmProcess2.kill();
5149
- if (result.outcome === "failed") {
5150
- console.error(`Resolve failed: ${result.error}`);
6311
+ if (result2.outcome === "failed") {
6312
+ console.error(`Resolve failed: ${result2.error}`);
5151
6313
  process.exit(1);
5152
6314
  }
5153
- console.log(`Resolve: ${result.outcome}`);
6315
+ console.log(`Resolve: ${result2.outcome}`);
5154
6316
  process.exit(0);
5155
6317
  }
5156
6318
  logger.info("Preflight checks:");
5157
6319
  runPreflight();
5158
6320
  if (input.task) {
5159
- fs30.writeFileSync(path27.join(taskDir, "task.md"), input.task);
6321
+ fs32.writeFileSync(path29.join(taskDir, "task.md"), input.task);
5160
6322
  }
5161
- const taskMdPath = path27.join(taskDir, "task.md");
5162
- if (!fs30.existsSync(taskMdPath) && isPRFix && input.prNumber) {
6323
+ const taskMdPath = path29.join(taskDir, "task.md");
6324
+ if (!fs32.existsSync(taskMdPath) && isPRFix && input.prNumber) {
5163
6325
  logger.info(`Fetching PR #${input.prNumber} details as task context...`);
5164
6326
  const prDetails = getPRDetails(input.prNumber);
5165
6327
  if (prDetails) {
5166
6328
  const taskContent = `# ${prDetails.title}
5167
6329
 
5168
6330
  ${prDetails.body ?? ""}`;
5169
- fs30.writeFileSync(taskMdPath, taskContent);
6331
+ fs32.writeFileSync(taskMdPath, taskContent);
5170
6332
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
5171
6333
  }
5172
- } else if (!fs30.existsSync(taskMdPath) && input.issueNumber) {
6334
+ } else if (!fs32.existsSync(taskMdPath) && input.issueNumber) {
5173
6335
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
5174
6336
  const issue = getIssue(input.issueNumber);
5175
6337
  if (issue) {
5176
6338
  const taskContent = `# ${issue.title}
5177
6339
 
5178
6340
  ${issue.body ?? ""}`;
5179
- fs30.writeFileSync(taskMdPath, taskContent);
6341
+ fs32.writeFileSync(taskMdPath, taskContent);
5180
6342
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
5181
6343
  }
5182
6344
  }
5183
- if (!fs30.existsSync(taskMdPath)) {
6345
+ if (!fs32.existsSync(taskMdPath)) {
5184
6346
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
5185
6347
  process.exit(1);
5186
6348
  }
@@ -5318,7 +6480,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
5318
6480
  }
5319
6481
  }
5320
6482
  const state = await runPipeline(ctx);
5321
- const files = fs30.readdirSync(taskDir);
6483
+ const files = fs32.readdirSync(taskDir);
5322
6484
  console.log(`
5323
6485
  Artifacts in ${taskDir}:`);
5324
6486
  for (const f of files) {
@@ -5383,8 +6545,8 @@ var init_entry = __esm({
5383
6545
  });
5384
6546
 
5385
6547
  // src/bin/cli.ts
5386
- import * as fs31 from "fs";
5387
- import * as path28 from "path";
6548
+ import * as fs33 from "fs";
6549
+ import * as path30 from "path";
5388
6550
  import { fileURLToPath as fileURLToPath2 } from "url";
5389
6551
 
5390
6552
  // src/bin/commands/init.ts
@@ -5556,7 +6718,7 @@ function buildConfig(cwd, basic) {
5556
6718
  github: { owner: basic.owner, repo: basic.repo },
5557
6719
  agent: {
5558
6720
  provider: "anthropic",
5559
- modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
6721
+ modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
5560
6722
  }
5561
6723
  };
5562
6724
  const mcp = detectMcpConfig(cwd, basic.pm, pkg);
@@ -5756,15 +6918,15 @@ function initCommand(opts, pkgRoot) {
5756
6918
 
5757
6919
  // src/bin/commands/bootstrap.ts
5758
6920
  init_architecture_detection();
5759
- import * as fs7 from "fs";
5760
- import * as path6 from "path";
6921
+ import * as fs8 from "fs";
6922
+ import * as path7 from "path";
5761
6923
  import { execFileSync as execFileSync5 } from "child_process";
5762
6924
 
5763
6925
  // src/bin/qa-guide.ts
5764
6926
  import * as fs5 from "fs";
5765
6927
  import * as path4 from "path";
5766
6928
  function discoverQaContext(cwd) {
5767
- const result = {
6929
+ const result2 = {
5768
6930
  routes: [],
5769
6931
  authFiles: [],
5770
6932
  loginPage: null,
@@ -5777,21 +6939,21 @@ function discoverQaContext(cwd) {
5777
6939
  const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
5778
6940
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
5779
6941
  const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
5780
- if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
5781
- if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
5782
- else if (allDeps.vite) result.devPort = 5173;
6942
+ if (pkg.scripts?.dev) result2.devCommand = `${pm} dev`;
6943
+ if (allDeps.next || allDeps.nuxt) result2.devPort = 3e3;
6944
+ else if (allDeps.vite) result2.devPort = 5173;
5783
6945
  } catch {
5784
6946
  }
5785
6947
  const appDirs = ["src/app", "app"];
5786
6948
  for (const appDir of appDirs) {
5787
6949
  const fullAppDir = path4.join(cwd, appDir);
5788
6950
  if (!fs5.existsSync(fullAppDir)) continue;
5789
- scanRoutes(fullAppDir, appDir, "", result);
6951
+ scanRoutes(fullAppDir, appDir, "", result2);
5790
6952
  break;
5791
6953
  }
5792
6954
  const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
5793
6955
  for (const p of authPatterns) {
5794
- if (fs5.existsSync(path4.join(cwd, p))) result.authFiles.push(p);
6956
+ if (fs5.existsSync(path4.join(cwd, p))) result2.authFiles.push(p);
5795
6957
  }
5796
6958
  const authConfigGlobs = [
5797
6959
  "src/app/api/auth",
@@ -5802,7 +6964,7 @@ function discoverQaContext(cwd) {
5802
6964
  "src/app/api/oauth"
5803
6965
  ];
5804
6966
  for (const g of authConfigGlobs) {
5805
- if (fs5.existsSync(path4.join(cwd, g))) result.authFiles.push(g);
6967
+ if (fs5.existsSync(path4.join(cwd, g))) result2.authFiles.push(g);
5806
6968
  }
5807
6969
  try {
5808
6970
  const rolePaths = [
@@ -5824,7 +6986,7 @@ function discoverQaContext(cwd) {
5824
6986
  if (roleMatches) {
5825
6987
  for (const m of roleMatches) {
5826
6988
  const val = m.match(/['"](\w+)['"]/);
5827
- if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
6989
+ if (val && !result2.roles.includes(val[1])) result2.roles.push(val[1]);
5828
6990
  }
5829
6991
  }
5830
6992
  const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
@@ -5833,7 +6995,7 @@ function discoverQaContext(cwd) {
5833
6995
  if (vals) {
5834
6996
  for (const v of vals) {
5835
6997
  const clean = v.replace(/['"]/g, "");
5836
- if (!result.roles.includes(clean)) result.roles.push(clean);
6998
+ if (!result2.roles.includes(clean)) result2.roles.push(clean);
5837
6999
  }
5838
7000
  }
5839
7001
  }
@@ -5843,9 +7005,9 @@ function discoverQaContext(cwd) {
5843
7005
  }
5844
7006
  } catch {
5845
7007
  }
5846
- return result;
7008
+ return result2;
5847
7009
  }
5848
- function scanRoutes(dir, baseDir, prefix, result) {
7010
+ function scanRoutes(dir, baseDir, prefix, result2) {
5849
7011
  let entries;
5850
7012
  try {
5851
7013
  entries = fs5.readdirSync(dir, { withFileTypes: true });
@@ -5856,16 +7018,16 @@ function scanRoutes(dir, baseDir, prefix, result) {
5856
7018
  if (hasPage) {
5857
7019
  const routePath = prefix || "/";
5858
7020
  const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
5859
- result.routes.push({ path: routePath, group });
5860
- if (prefix.includes("/login")) result.loginPage = routePath;
5861
- if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
7021
+ result2.routes.push({ path: routePath, group });
7022
+ if (prefix.includes("/login")) result2.loginPage = routePath;
7023
+ if (prefix.startsWith("/admin") && !result2.adminPath) result2.adminPath = prefix;
5862
7024
  }
5863
7025
  for (const entry of entries) {
5864
7026
  if (!entry.isDirectory()) continue;
5865
7027
  if (entry.name === "node_modules" || entry.name === ".next") continue;
5866
7028
  let segment = entry.name;
5867
7029
  if (segment.startsWith("(") && segment.endsWith(")")) {
5868
- scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result);
7030
+ scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result2);
5869
7031
  continue;
5870
7032
  }
5871
7033
  if (segment.startsWith("[") && segment.endsWith("]")) {
@@ -5874,7 +7036,7 @@ function scanRoutes(dir, baseDir, prefix, result) {
5874
7036
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
5875
7037
  segment = `:${segment.slice(2, -2)}?`;
5876
7038
  }
5877
- scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
7039
+ scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result2);
5878
7040
  }
5879
7041
  }
5880
7042
  function generateQaGuide(discovery) {
@@ -6031,22 +7193,23 @@ function installSkillsForProject(cwd) {
6031
7193
  }
6032
7194
 
6033
7195
  // src/bin/commands/bootstrap.ts
7196
+ init_config();
6034
7197
  var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
6035
7198
  function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
6036
- const srcDir = path6.join(cwd, "src");
6037
- const baseDir = fs7.existsSync(srcDir) ? srcDir : cwd;
7199
+ const srcDir = path7.join(cwd, "src");
7200
+ const baseDir = fs8.existsSync(srcDir) ? srcDir : cwd;
6038
7201
  const results = [];
6039
7202
  function walk(dir) {
6040
7203
  const entries = [];
6041
7204
  try {
6042
- for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
7205
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
6043
7206
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
6044
- const full = path6.join(dir, entry.name);
7207
+ const full = path7.join(dir, entry.name);
6045
7208
  if (entry.isDirectory()) {
6046
7209
  entries.push(...walk(full));
6047
7210
  } else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
6048
7211
  try {
6049
- const stat = fs7.statSync(full);
7212
+ const stat = fs8.statSync(full);
6050
7213
  if (stat.size >= 200 && stat.size <= 5e3) {
6051
7214
  entries.push({ filePath: full, size: stat.size });
6052
7215
  }
@@ -6060,8 +7223,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
6060
7223
  }
6061
7224
  const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
6062
7225
  for (const { filePath } of files) {
6063
- const rel = path6.relative(cwd, filePath);
6064
- const content = fs7.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
7226
+ const rel = path7.relative(cwd, filePath);
7227
+ const content = fs8.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
6065
7228
  results.push(`### File: ${rel}
6066
7229
  \`\`\`typescript
6067
7230
  ${content}
@@ -6073,9 +7236,9 @@ function ghComment(issueNumber, body, cwd) {
6073
7236
  try {
6074
7237
  let repoSlug = "";
6075
7238
  try {
6076
- const configPath = path6.join(cwd, "kody.config.json");
6077
- if (fs7.existsSync(configPath)) {
6078
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
7239
+ const configPath = path7.join(cwd, "kody.config.json");
7240
+ if (fs8.existsSync(configPath)) {
7241
+ const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6079
7242
  if (config.github?.owner && config.github?.repo) {
6080
7243
  repoSlug = `${config.github.owner}/${config.github.repo}`;
6081
7244
  }
@@ -6102,7 +7265,9 @@ function ghComment(issueNumber, body, cwd) {
6102
7265
  }
6103
7266
  function bootstrapCommand(opts, pkgRoot) {
6104
7267
  const cwd = process.cwd();
7268
+ setConfigDir(cwd);
6105
7269
  const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
7270
+ const bootstrapModel = resolveStageConfig(getProjectConfig(), "bootstrap", "cheap").model;
6106
7271
  console.log(`
6107
7272
  \u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
6108
7273
  `);
@@ -6110,8 +7275,8 @@ function bootstrapCommand(opts, pkgRoot) {
6110
7275
  ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
6111
7276
  }
6112
7277
  const readIfExists = (rel, maxChars = 3e3) => {
6113
- const p = path6.join(cwd, rel);
6114
- if (fs7.existsSync(p)) return fs7.readFileSync(p, "utf-8").slice(0, maxChars);
7278
+ const p = path7.join(cwd, rel);
7279
+ if (fs8.existsSync(p)) return fs8.readFileSync(p, "utf-8").slice(0, maxChars);
6115
7280
  return null;
6116
7281
  };
6117
7282
  let repoContext = "";
@@ -6146,14 +7311,14 @@ ${sampleFiles}
6146
7311
 
6147
7312
  `;
6148
7313
  try {
6149
- const topDirs = fs7.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
7314
+ const topDirs = fs8.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
6150
7315
  repoContext += `## Top-level directories
6151
7316
  ${topDirs.join(", ")}
6152
7317
 
6153
7318
  `;
6154
- const srcDir = path6.join(cwd, "src");
6155
- if (fs7.existsSync(srcDir)) {
6156
- const srcDirs = fs7.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7319
+ const srcDir = path7.join(cwd, "src");
7320
+ if (fs8.existsSync(srcDir)) {
7321
+ const srcDirs = fs8.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6157
7322
  if (srcDirs.length > 0) repoContext += `## src/ subdirectories
6158
7323
  ${srcDirs.join(", ")}
6159
7324
 
@@ -6163,19 +7328,19 @@ ${srcDirs.join(", ")}
6163
7328
  }
6164
7329
  const existingFiles = [];
6165
7330
  for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "vitest.config.ts", "vitest.config.mts", "jest.config.ts", "playwright.config.ts", ".eslintrc.js", "eslint.config.mjs", ".prettierrc"]) {
6166
- if (fs7.existsSync(path6.join(cwd, f))) existingFiles.push(f);
7331
+ if (fs8.existsSync(path7.join(cwd, f))) existingFiles.push(f);
6167
7332
  }
6168
7333
  if (existingFiles.length) repoContext += `## Config files present
6169
7334
  ${existingFiles.join(", ")}
6170
7335
 
6171
7336
  `;
6172
7337
  console.log("\u2500\u2500 Project Memory \u2500\u2500");
6173
- const memoryDir = path6.join(cwd, ".kody", "memory");
6174
- fs7.mkdirSync(memoryDir, { recursive: true });
6175
- const archPath = path6.join(memoryDir, "architecture.md");
6176
- const conventionsPath = path6.join(memoryDir, "conventions.md");
6177
- const existingArch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
6178
- const existingConv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
7338
+ const memoryDir = path7.join(cwd, ".kody", "memory");
7339
+ fs8.mkdirSync(memoryDir, { recursive: true });
7340
+ const archPath = path7.join(memoryDir, "architecture.md");
7341
+ const conventionsPath = path7.join(memoryDir, "conventions.md");
7342
+ const existingArch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
7343
+ const existingConv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
6179
7344
  const hasExisting = !!(existingArch || existingConv);
6180
7345
  const extendInstruction = hasExisting && !opts.force ? `
6181
7346
  ## Existing Documentation (EXTEND, do not replace)
@@ -6219,7 +7384,7 @@ ${repoContext}`;
6219
7384
  const output = execFileSync5("claude", [
6220
7385
  "--print",
6221
7386
  "--model",
6222
- "claude-haiku-4-5-20251001",
7387
+ bootstrapModel,
6223
7388
  "--dangerously-skip-permissions",
6224
7389
  memoryPrompt
6225
7390
  ], {
@@ -6231,12 +7396,12 @@ ${repoContext}`;
6231
7396
  const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
6232
7397
  const parsed = JSON.parse(cleaned);
6233
7398
  if (parsed.architecture) {
6234
- fs7.writeFileSync(archPath, parsed.architecture);
7399
+ fs8.writeFileSync(archPath, parsed.architecture);
6235
7400
  const lineCount = parsed.architecture.split("\n").length;
6236
7401
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
6237
7402
  }
6238
7403
  if (parsed.conventions) {
6239
- fs7.writeFileSync(conventionsPath, parsed.conventions);
7404
+ fs8.writeFileSync(conventionsPath, parsed.conventions);
6240
7405
  const lineCount = parsed.conventions.split("\n").length;
6241
7406
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
6242
7407
  }
@@ -6245,39 +7410,39 @@ ${repoContext}`;
6245
7410
  const detected = detectArchitectureBasic(cwd);
6246
7411
  if (detected.length > 0) {
6247
7412
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6248
- fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
7413
+ fs8.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
6249
7414
 
6250
7415
  ## Overview
6251
7416
  ${detected.join("\n")}
6252
7417
  `);
6253
7418
  console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
6254
7419
  }
6255
- fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
7420
+ fs8.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
6256
7421
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
6257
7422
  }
6258
7423
  console.log("\n\u2500\u2500 Step Files \u2500\u2500");
6259
- const stepsDir = path6.join(cwd, ".kody", "steps");
6260
- fs7.mkdirSync(stepsDir, { recursive: true });
6261
- const arch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
6262
- const conv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
7424
+ const stepsDir = path7.join(cwd, ".kody", "steps");
7425
+ fs8.mkdirSync(stepsDir, { recursive: true });
7426
+ const arch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
7427
+ const conv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
6263
7428
  console.log(" \u23F3 Customizing step files...");
6264
7429
  let stepCount = 0;
6265
7430
  for (const stage of STEP_STAGES) {
6266
- const templatePath = path6.join(pkgRoot, "prompts", `${stage}.md`);
6267
- if (!fs7.existsSync(templatePath)) {
7431
+ const templatePath = path7.join(pkgRoot, "prompts", `${stage}.md`);
7432
+ if (!fs8.existsSync(templatePath)) {
6268
7433
  console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
6269
7434
  continue;
6270
7435
  }
6271
- const stepOutputPath = path6.join(stepsDir, `${stage}.md`);
6272
- if (fs7.existsSync(stepOutputPath) && !opts.force) {
7436
+ const stepOutputPath = path7.join(stepsDir, `${stage}.md`);
7437
+ if (fs8.existsSync(stepOutputPath) && !opts.force) {
6273
7438
  console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
6274
7439
  continue;
6275
7440
  }
6276
- const defaultPrompt = fs7.readFileSync(templatePath, "utf-8");
7441
+ const defaultPrompt = fs8.readFileSync(templatePath, "utf-8");
6277
7442
  const contextPlaceholder = "{{TASK_CONTEXT}}";
6278
7443
  const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
6279
7444
  if (placeholderIdx === -1) {
6280
- fs7.copyFileSync(templatePath, stepOutputPath);
7445
+ fs8.copyFileSync(templatePath, stepOutputPath);
6281
7446
  stepCount++;
6282
7447
  console.log(` \u2713 ${stage}.md`);
6283
7448
  continue;
@@ -6322,7 +7487,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6322
7487
  const output = execFileSync5("claude", [
6323
7488
  "--print",
6324
7489
  "--model",
6325
- "claude-haiku-4-5-20251001",
7490
+ bootstrapModel,
6326
7491
  "--dangerously-skip-permissions",
6327
7492
  customizationPrompt
6328
7493
  ], {
@@ -6334,23 +7499,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6334
7499
  let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
6335
7500
  cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
6336
7501
  const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
6337
- fs7.writeFileSync(stepOutputPath, finalPrompt);
7502
+ fs8.writeFileSync(stepOutputPath, finalPrompt);
6338
7503
  stepCount++;
6339
7504
  console.log(` \u2713 ${stage}.md`);
6340
7505
  } catch {
6341
7506
  console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
6342
- fs7.copyFileSync(templatePath, stepOutputPath);
7507
+ fs8.copyFileSync(templatePath, stepOutputPath);
6343
7508
  stepCount++;
6344
7509
  }
6345
7510
  }
6346
7511
  console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
6347
7512
  console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
6348
- const qaGuidePath = path6.join(cwd, ".kody", "qa-guide.md");
6349
- if (!fs7.existsSync(qaGuidePath) || opts.force) {
7513
+ const qaGuidePath = path7.join(cwd, ".kody", "qa-guide.md");
7514
+ if (!fs8.existsSync(qaGuidePath) || opts.force) {
6350
7515
  const discovery = discoverQaContext(cwd);
6351
7516
  if (discovery.routes.length > 0) {
6352
7517
  const qaGuide = generateQaGuide(discovery);
6353
- fs7.writeFileSync(qaGuidePath, qaGuide);
7518
+ fs8.writeFileSync(qaGuidePath, qaGuide);
6354
7519
  console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
6355
7520
  if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
6356
7521
  if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
@@ -6365,9 +7530,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6365
7530
  try {
6366
7531
  let repoSlug = "";
6367
7532
  try {
6368
- const configPath = path6.join(cwd, "kody.config.json");
6369
- if (fs7.existsSync(configPath)) {
6370
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
7533
+ const configPath = path7.join(cwd, "kody.config.json");
7534
+ if (fs8.existsSync(configPath)) {
7535
+ const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6371
7536
  if (config.github?.owner && config.github?.repo) {
6372
7537
  repoSlug = `${config.github.owner}/${config.github.repo}`;
6373
7538
  }
@@ -6440,19 +7605,19 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6440
7605
  ".kody/memory/conventions.md",
6441
7606
  ".kody/qa-guide.md",
6442
7607
  ...installedSkillPaths
6443
- ].filter((f) => fs7.existsSync(path6.join(cwd, f)));
6444
- if (fs7.existsSync(path6.join(cwd, "skills-lock.json"))) {
7608
+ ].filter((f) => fs8.existsSync(path7.join(cwd, f)));
7609
+ if (fs8.existsSync(path7.join(cwd, "skills-lock.json"))) {
6445
7610
  filesToCommit.push("skills-lock.json");
6446
7611
  }
6447
7612
  for (const stage of STEP_STAGES) {
6448
7613
  const stepFile = `.kody/steps/${stage}.md`;
6449
- if (fs7.existsSync(path6.join(cwd, stepFile))) {
7614
+ if (fs8.existsSync(path7.join(cwd, stepFile))) {
6450
7615
  filesToCommit.push(stepFile);
6451
7616
  }
6452
7617
  }
6453
7618
  if (filesToCommit.length > 0) {
6454
7619
  try {
6455
- const fullPaths = filesToCommit.map((f) => path6.join(cwd, f));
7620
+ const fullPaths = filesToCommit.map((f) => path7.join(cwd, f));
6456
7621
  for (let pass = 0; pass < 2; pass++) {
6457
7622
  execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
6458
7623
  cwd,
@@ -6479,9 +7644,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6479
7644
  console.log(` \u2713 Pushed branch: ${branchName}`);
6480
7645
  let baseBranch = "main";
6481
7646
  try {
6482
- const configPath = path6.join(cwd, "kody.config.json");
6483
- if (fs7.existsSync(configPath)) {
6484
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
7647
+ const configPath = path7.join(cwd, "kody.config.json");
7648
+ if (fs8.existsSync(configPath)) {
7649
+ const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6485
7650
  baseBranch = config.git?.defaultBranch ?? "main";
6486
7651
  }
6487
7652
  } catch {
@@ -6555,11 +7720,11 @@ Create it manually.`, cwd);
6555
7720
 
6556
7721
  // src/bin/cli.ts
6557
7722
  init_architecture_detection();
6558
- var __dirname2 = path28.dirname(fileURLToPath2(import.meta.url));
6559
- var PKG_ROOT = path28.resolve(__dirname2, "..", "..");
7723
+ var __dirname2 = path30.dirname(fileURLToPath2(import.meta.url));
7724
+ var PKG_ROOT = path30.resolve(__dirname2, "..", "..");
6560
7725
  function getVersion() {
6561
- const pkgPath = path28.join(PKG_ROOT, "package.json");
6562
- const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
7726
+ const pkgPath = path30.join(PKG_ROOT, "package.json");
7727
+ const pkg = JSON.parse(fs33.readFileSync(pkgPath, "utf-8"));
6563
7728
  return pkg.version;
6564
7729
  }
6565
7730
  var args = process.argv.slice(2);
@@ -6570,6 +7735,8 @@ if (command === "init") {
6570
7735
  bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
6571
7736
  } else if (command === "taskify") {
6572
7737
  Promise.resolve().then(() => (init_taskify_command(), taskify_command_exports)).then(({ runTaskifyCommand: runTaskifyCommand2 }) => runTaskifyCommand2());
7738
+ } else if (command === "test-model") {
7739
+ Promise.resolve().then(() => (init_test_model_command(), test_model_command_exports)).then(({ runTestModelCommand: runTestModelCommand2 }) => runTestModelCommand2());
6573
7740
  } else if (command === "ci-parse") {
6574
7741
  Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
6575
7742
  } else if (command === "version" || command === "--version" || command === "-v") {