@kody-ade/kody-engine-lite 0.1.112 → 0.1.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/agent-runner.d.ts +4 -0
  2. package/dist/agent-runner.js +122 -0
  3. package/dist/bin/cli.js +1436 -429
  4. package/dist/ci/parse-inputs.d.ts +6 -0
  5. package/dist/ci/parse-inputs.js +76 -0
  6. package/dist/ci/parse-safety.d.ts +6 -0
  7. package/dist/ci/parse-safety.js +22 -0
  8. package/dist/cli/args.d.ts +13 -0
  9. package/dist/cli/args.js +42 -0
  10. package/dist/cli/litellm.d.ts +2 -0
  11. package/dist/cli/litellm.js +85 -0
  12. package/dist/cli/task-resolution.d.ts +2 -0
  13. package/dist/cli/task-resolution.js +41 -0
  14. package/dist/config.d.ts +49 -0
  15. package/dist/config.js +72 -0
  16. package/dist/context.d.ts +4 -0
  17. package/dist/context.js +83 -0
  18. package/dist/definitions.d.ts +3 -0
  19. package/dist/definitions.js +59 -0
  20. package/dist/entry.d.ts +1 -0
  21. package/dist/entry.js +236 -0
  22. package/dist/git-utils.d.ts +13 -0
  23. package/dist/git-utils.js +174 -0
  24. package/dist/github-api.d.ts +14 -0
  25. package/dist/github-api.js +114 -0
  26. package/dist/kody-utils.d.ts +1 -0
  27. package/dist/kody-utils.js +9 -0
  28. package/dist/learning/auto-learn.d.ts +2 -0
  29. package/dist/learning/auto-learn.js +169 -0
  30. package/dist/logger.d.ts +14 -0
  31. package/dist/logger.js +51 -0
  32. package/dist/memory.d.ts +1 -0
  33. package/dist/memory.js +20 -0
  34. package/dist/observer.d.ts +9 -0
  35. package/dist/observer.js +80 -0
  36. package/dist/pipeline/complexity.d.ts +3 -0
  37. package/dist/pipeline/complexity.js +12 -0
  38. package/dist/pipeline/executor-registry.d.ts +3 -0
  39. package/dist/pipeline/executor-registry.js +20 -0
  40. package/dist/pipeline/hooks.d.ts +17 -0
  41. package/dist/pipeline/hooks.js +110 -0
  42. package/dist/pipeline/questions.d.ts +2 -0
  43. package/dist/pipeline/questions.js +44 -0
  44. package/dist/pipeline/runner-selection.d.ts +2 -0
  45. package/dist/pipeline/runner-selection.js +13 -0
  46. package/dist/pipeline/state.d.ts +4 -0
  47. package/dist/pipeline/state.js +37 -0
  48. package/dist/pipeline.d.ts +3 -0
  49. package/dist/pipeline.js +213 -0
  50. package/dist/preflight.d.ts +1 -0
  51. package/dist/preflight.js +69 -0
  52. package/dist/retrospective.d.ts +26 -0
  53. package/dist/retrospective.js +211 -0
  54. package/dist/stages/agent.d.ts +2 -0
  55. package/dist/stages/agent.js +94 -0
  56. package/dist/stages/gate.d.ts +2 -0
  57. package/dist/stages/gate.js +32 -0
  58. package/dist/stages/review.d.ts +2 -0
  59. package/dist/stages/review.js +32 -0
  60. package/dist/stages/ship.d.ts +3 -0
  61. package/dist/stages/ship.js +154 -0
  62. package/dist/stages/verify.d.ts +2 -0
  63. package/dist/stages/verify.js +94 -0
  64. package/dist/types.d.ts +61 -0
  65. package/dist/types.js +1 -0
  66. package/dist/validators.d.ts +8 -0
  67. package/dist/validators.js +42 -0
  68. package/dist/verify-runner.d.ts +11 -0
  69. package/dist/verify-runner.js +110 -0
  70. package/kody.config.schema.json +31 -0
  71. package/package.json +1 -1
  72. package/templates/kody.yml +1 -0
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;
@@ -223,16 +223,16 @@ function setConfigDir(dir) {
223
223
  }
224
224
  function getProjectConfig() {
225
225
  if (_config) return _config;
226
- const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
227
- if (fs8.existsSync(configPath)) {
226
+ const configPath = path6.join(_configDir ?? process.cwd(), "kody.config.json");
227
+ if (fs7.existsSync(configPath)) {
228
228
  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`);
229
+ const result2 = parseJsonSafe(fs7.readFileSync(configPath, "utf-8"));
230
+ if (!result2.ok) {
231
+ logger.warn(`kody.config.json: ${result2.error} \u2014 using defaults`);
232
232
  _config = { ...DEFAULT_CONFIG };
233
233
  return _config;
234
234
  }
235
- const raw = result.data;
235
+ const raw = result2.data;
236
236
  _config = {
237
237
  quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
238
238
  git: { ...DEFAULT_CONFIG.git, ...raw.git },
@@ -283,7 +283,7 @@ var init_config = __esm({
283
283
  repo: ""
284
284
  },
285
285
  agent: {
286
- modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
286
+ modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
287
287
  },
288
288
  contextTiers: {
289
289
  enabled: true,
@@ -389,10 +389,11 @@ function createClaudeCodeRunner() {
389
389
  model,
390
390
  "--dangerously-skip-permissions"
391
391
  ];
392
+ const baseTools = "Bash,Edit,Read,Write,Glob,Grep";
392
393
  if (options?.mcpConfigJson) {
393
394
  args2.push("--mcp-config", options.mcpConfigJson);
394
395
  } else {
395
- args2.push("--allowedTools", "Bash,Edit,Read,Write,Glob,Grep");
396
+ args2.push("--allowedTools", baseTools);
396
397
  }
397
398
  if (options?.sessionId) {
398
399
  if (options.resumeSession) {
@@ -965,8 +966,8 @@ function findLatestTaskForIssue(issueNumber, projectDir) {
965
966
  }
966
967
  function generateTaskId() {
967
968
  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())}`;
969
+ const pad2 = (n) => String(n).padStart(2, "0");
970
+ return `${String(now.getFullYear()).slice(2)}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
970
971
  }
971
972
  function resolveTaskIdFromComments(issueNumber) {
972
973
  try {
@@ -1192,6 +1193,7 @@ var init_litellm = __esm({
1192
1193
  // src/cli/taskify-command.ts
1193
1194
  var taskify_command_exports = {};
1194
1195
  __export(taskify_command_exports, {
1196
+ TaskifyError: () => TaskifyError,
1195
1197
  isTaskifyRun: () => isTaskifyRun,
1196
1198
  readTaskifyMarker: () => readTaskifyMarker,
1197
1199
  runTaskifyCommand: () => runTaskifyCommand,
@@ -1283,8 +1285,17 @@ async function runTaskifyCommand() {
1283
1285
  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
1284
1286
  };
1285
1287
  }
1286
- await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1287
- litellmProcess?.kill();
1288
+ try {
1289
+ await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1290
+ } catch (err) {
1291
+ if (err instanceof TaskifyError) {
1292
+ logger.error(`[taskify] ${err.message}`);
1293
+ process.exit(1);
1294
+ }
1295
+ throw err;
1296
+ } finally {
1297
+ litellmProcess?.kill();
1298
+ }
1288
1299
  }
1289
1300
  async function taskifyCommand(opts) {
1290
1301
  const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
@@ -1299,7 +1310,6 @@ async function taskifyCommand(opts) {
1299
1310
  mcpConfigJson = buildTaskifyMcpConfigJson(config);
1300
1311
  } catch (err) {
1301
1312
  const msg = err instanceof Error ? err.message : String(err);
1302
- logger.error(`[taskify] MCP config error: ${msg}`);
1303
1313
  if (issueNumber && !local) {
1304
1314
  postComment(
1305
1315
  issueNumber,
@@ -1310,7 +1320,7 @@ async function taskifyCommand(opts) {
1310
1320
  Add the required MCP server config to \`kody.config.json\` and try again.`
1311
1321
  );
1312
1322
  }
1313
- process.exit(1);
1323
+ throw new TaskifyError(`MCP config error: ${msg}`);
1314
1324
  }
1315
1325
  }
1316
1326
  const sc = resolveStageConfig(config, "taskify", "strong");
@@ -1351,47 +1361,44 @@ Kody is decomposing ${src} into tasks...`);
1351
1361
  fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1352
1362
  const runner = opts.runner ?? createClaudeCodeRunner();
1353
1363
  logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
1354
- const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
1364
+ const result2 = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
1355
1365
  cwd: projectDir,
1356
1366
  mcpConfigJson,
1357
1367
  env: opts.runnerEnv
1358
1368
  });
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}`);
1369
+ if (result2.outcome !== "completed") {
1370
+ const errMsg = result2.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result2.error}`;
1362
1371
  if (issueNumber && !local) {
1363
1372
  postComment(issueNumber, `Kody taskify failed:
1364
1373
 
1365
1374
  > ${errMsg}`);
1366
1375
  setLifecycleLabel(issueNumber, "failed");
1367
1376
  }
1368
- process.exit(1);
1377
+ throw new TaskifyError(errMsg);
1369
1378
  }
1370
1379
  const resultPath = path10.join(taskDir, RESULT_FILE);
1371
1380
  if (!fs11.existsSync(resultPath)) {
1372
1381
  const errMsg = `Claude did not write ${RESULT_FILE}. Output:
1373
1382
 
1374
- ${result.output?.slice(0, 500) ?? "(none)"}`;
1375
- logger.error(`[taskify] ${errMsg}`);
1383
+ ${result2.output?.slice(0, 500) ?? "(none)"}`;
1376
1384
  if (issueNumber && !local) {
1377
1385
  postComment(issueNumber, `Kody taskify failed: result file not found.
1378
1386
 
1379
1387
  ${errMsg}`);
1380
1388
  setLifecycleLabel(issueNumber, "failed");
1381
1389
  }
1382
- process.exit(1);
1390
+ throw new TaskifyError(errMsg);
1383
1391
  }
1384
1392
  let parsed;
1385
1393
  try {
1386
1394
  parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
1387
1395
  } catch {
1388
1396
  const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
1389
- logger.error(`[taskify] ${errMsg}`);
1390
1397
  if (issueNumber && !local) {
1391
1398
  postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1392
1399
  setLifecycleLabel(issueNumber, "failed");
1393
1400
  }
1394
- process.exit(1);
1401
+ throw new TaskifyError(errMsg);
1395
1402
  }
1396
1403
  const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
1397
1404
  if (parsed.status === "questions") {
@@ -1400,12 +1407,11 @@ ${errMsg}`);
1400
1407
  await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
1401
1408
  } else {
1402
1409
  const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
1403
- logger.error(`[taskify] ${errMsg}`);
1404
1410
  if (issueNumber && !local) {
1405
1411
  postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1406
1412
  setLifecycleLabel(issueNumber, "failed");
1407
1413
  }
1408
- process.exit(1);
1414
+ throw new TaskifyError(errMsg);
1409
1415
  }
1410
1416
  }
1411
1417
  function handleQuestions(parsed, ticketId, issueNumber, local) {
@@ -1536,7 +1542,7 @@ function readTaskifyMarker(taskDir) {
1536
1542
  return null;
1537
1543
  }
1538
1544
  }
1539
- var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
1545
+ var __dirname, TaskifyError, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
1540
1546
  var init_taskify_command = __esm({
1541
1547
  "src/cli/taskify-command.ts"() {
1542
1548
  "use strict";
@@ -1548,6 +1554,12 @@ var init_taskify_command = __esm({
1548
1554
  init_task_resolution();
1549
1555
  init_litellm();
1550
1556
  __dirname = path10.dirname(fileURLToPath(import.meta.url));
1557
+ TaskifyError = class extends Error {
1558
+ constructor(message) {
1559
+ super(message);
1560
+ this.name = "TaskifyError";
1561
+ }
1562
+ };
1551
1563
  AUTO_TRIGGER_THRESHOLD = 5;
1552
1564
  MAX_TASKS_GUARD = 20;
1553
1565
  TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -1556,6 +1568,999 @@ var init_taskify_command = __esm({
1556
1568
  }
1557
1569
  });
1558
1570
 
1571
+ // src/cli/test-model-tests.ts
1572
+ import * as fs12 from "fs";
1573
+ import * as os2 from "os";
1574
+ import * as path11 from "path";
1575
+ import * as zlib from "zlib";
1576
+ import { spawnSync, execSync as execSync2 } from "child_process";
1577
+ async function apiCall(ctx, body) {
1578
+ try {
1579
+ const res = await fetch(`${ctx.proxyUrl}/v1/messages`, {
1580
+ method: "POST",
1581
+ headers: {
1582
+ "Content-Type": "application/json",
1583
+ "x-api-key": ctx.apiKey,
1584
+ "anthropic-version": "2023-06-01"
1585
+ },
1586
+ body: JSON.stringify({ model: ctx.model, ...body }),
1587
+ signal: AbortSignal.timeout(6e4)
1588
+ });
1589
+ const data = await res.json();
1590
+ if (!res.ok) {
1591
+ return { ok: false, data, status: res.status, errorMsg: data?.error?.message ?? `HTTP ${res.status}` };
1592
+ }
1593
+ return { ok: true, data, status: res.status };
1594
+ } catch (err) {
1595
+ return { ok: false, data: null, status: 0, errorMsg: err instanceof Error ? err.message : String(err) };
1596
+ }
1597
+ }
1598
+ function extractText(data) {
1599
+ if (!data?.content) return "";
1600
+ return data.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
1601
+ }
1602
+ async function runToolConversation(ctx, tools, userPrompt, simulate, opts) {
1603
+ const messages = [{ role: "user", content: userPrompt }];
1604
+ const allCalls = [];
1605
+ for (let turn = 0; turn < (opts?.maxTurns ?? 5); turn++) {
1606
+ const body = {
1607
+ max_tokens: 1024,
1608
+ temperature: 0,
1609
+ messages,
1610
+ tools
1611
+ };
1612
+ if (opts?.system) body.system = opts.system;
1613
+ const res = await apiCall(ctx, body);
1614
+ if (!res.ok) return { finalText: "", toolCalls: allCalls, error: res.errorMsg };
1615
+ const content = res.data.content ?? [];
1616
+ const toolBlocks = content.filter((b) => b.type === "tool_use");
1617
+ const textBlocks = content.filter((b) => b.type === "text");
1618
+ if (toolBlocks.length === 0) {
1619
+ return { finalText: textBlocks.map((b) => b.text ?? "").join(""), toolCalls: allCalls };
1620
+ }
1621
+ for (const tc of toolBlocks) allCalls.push({ name: tc.name, input: tc.input });
1622
+ messages.push({ role: "assistant", content });
1623
+ messages.push({
1624
+ role: "user",
1625
+ content: toolBlocks.map((tc) => ({
1626
+ type: "tool_result",
1627
+ tool_use_id: tc.id,
1628
+ content: simulate(tc.name, tc.input)
1629
+ }))
1630
+ });
1631
+ }
1632
+ return { finalText: "", toolCalls: allCalls, error: "Max turns reached" };
1633
+ }
1634
+ function filterStderr(stderr) {
1635
+ 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();
1636
+ }
1637
+ function runClaudeTest(ctx, prompt, extraFlags = [], timeout = 9e4) {
1638
+ try {
1639
+ const result2 = spawnSync("claude", [
1640
+ "--print",
1641
+ "--model",
1642
+ ctx.model,
1643
+ "--dangerously-skip-permissions",
1644
+ ...extraFlags,
1645
+ "-p",
1646
+ prompt
1647
+ ], {
1648
+ env: { ...process.env, ANTHROPIC_BASE_URL: ctx.proxyUrl, ANTHROPIC_API_KEY: ctx.apiKey },
1649
+ timeout,
1650
+ encoding: "utf-8",
1651
+ cwd: ctx.projectDir
1652
+ });
1653
+ return {
1654
+ stdout: result2.stdout ?? "",
1655
+ stderr: filterStderr(result2.stderr ?? ""),
1656
+ exitCode: result2.status ?? 1
1657
+ };
1658
+ } catch (err) {
1659
+ return { stdout: "", stderr: String(err), exitCode: 1 };
1660
+ }
1661
+ }
1662
+ function isGitClean(dir) {
1663
+ try {
1664
+ const out = execSync2("git diff --name-only", { cwd: dir, encoding: "utf-8", timeout: 5e3 });
1665
+ return out.trim().length === 0;
1666
+ } catch {
1667
+ return false;
1668
+ }
1669
+ }
1670
+ function revertChanges(dir) {
1671
+ try {
1672
+ execSync2("git checkout -- src/logger.ts", { cwd: dir, timeout: 5e3, stdio: "pipe" });
1673
+ } catch {
1674
+ }
1675
+ }
1676
+ function result(name, category, status, accuracy, durationMs, detail, metrics) {
1677
+ return { name, category, status, accuracy, durationMs, detail, metrics };
1678
+ }
1679
+ function crc32(buf) {
1680
+ let c = 4294967295;
1681
+ for (const b of buf) c = CRC_TABLE[(c ^ b) & 255] ^ c >>> 8;
1682
+ return (c ^ 4294967295) >>> 0;
1683
+ }
1684
+ function createRedPng() {
1685
+ const w = 4, h = 4;
1686
+ const scanlines = Buffer.alloc(h * (1 + w * 3));
1687
+ for (let y = 0; y < h; y++) {
1688
+ const off = y * (1 + w * 3);
1689
+ scanlines[off] = 0;
1690
+ for (let x = 0; x < w; x++) {
1691
+ scanlines[off + 1 + x * 3] = 255;
1692
+ scanlines[off + 1 + x * 3 + 1] = 0;
1693
+ scanlines[off + 1 + x * 3 + 2] = 0;
1694
+ }
1695
+ }
1696
+ function chunk(type, data) {
1697
+ const tb = Buffer.from(type, "ascii");
1698
+ const merged = Buffer.concat([tb, data]);
1699
+ const len = Buffer.alloc(4);
1700
+ len.writeUInt32BE(data.length);
1701
+ const crcBuf = Buffer.alloc(4);
1702
+ crcBuf.writeUInt32BE(crc32(merged));
1703
+ return Buffer.concat([len, tb, data, crcBuf]);
1704
+ }
1705
+ const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
1706
+ const ihdr = Buffer.alloc(13);
1707
+ ihdr.writeUInt32BE(w, 0);
1708
+ ihdr.writeUInt32BE(h, 4);
1709
+ ihdr[8] = 8;
1710
+ ihdr[9] = 2;
1711
+ return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(scanlines)), chunk("IEND", Buffer.alloc(0))]);
1712
+ }
1713
+ async function testSimplePrompt(ctx) {
1714
+ const t = Date.now();
1715
+ const res = await apiCall(ctx, {
1716
+ max_tokens: 50,
1717
+ temperature: 0,
1718
+ messages: [{ role: "user", content: "Reply with exactly: KODY_TEST_OK" }]
1719
+ });
1720
+ if (!res.ok) return result("simple_prompt", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
1721
+ const text = extractText(res.data);
1722
+ const ok = text.includes("KODY_TEST_OK");
1723
+ return result(
1724
+ "simple_prompt",
1725
+ "basic",
1726
+ ok ? "pass" : "fail",
1727
+ ok ? 100 : 0,
1728
+ Date.now() - t,
1729
+ ok ? "Model responded correctly" : `Expected KODY_TEST_OK, got: ${text.slice(0, 80)}`
1730
+ );
1731
+ }
1732
+ async function testJsonOutput(ctx) {
1733
+ const t = Date.now();
1734
+ const res = await apiCall(ctx, {
1735
+ max_tokens: 200,
1736
+ temperature: 0,
1737
+ system: "Respond with ONLY valid JSON. No markdown fences, no explanation. Just raw JSON.",
1738
+ messages: [{ role: "user", content: 'Return a JSON object with keys "status" (string "ok") and "model" (string, your model name).' }]
1739
+ });
1740
+ if (!res.ok) return result("json_output", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
1741
+ let text = extractText(res.data).trim();
1742
+ text = text.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
1743
+ try {
1744
+ const parsed = JSON.parse(text);
1745
+ const hasKeys = typeof parsed.status === "string" && typeof parsed.model === "string";
1746
+ return result(
1747
+ "json_output",
1748
+ "basic",
1749
+ "pass",
1750
+ hasKeys ? 100 : 70,
1751
+ Date.now() - t,
1752
+ hasKeys ? "Valid JSON with correct keys" : "Valid JSON but missing expected keys"
1753
+ );
1754
+ } catch {
1755
+ return result("json_output", "basic", "fail", 0, Date.now() - t, `Invalid JSON: ${text.slice(0, 80)}`);
1756
+ }
1757
+ }
1758
+ async function testSystemPromptRules(ctx) {
1759
+ const t = Date.now();
1760
+ const res = await apiCall(ctx, {
1761
+ max_tokens: 200,
1762
+ temperature: 0,
1763
+ system: [
1764
+ "STRICT RULES \u2014 violating ANY will crash the system:",
1765
+ "1) Start every response with 'KODY:'",
1766
+ "2) Never use the word 'the'",
1767
+ "3) Keep response under 50 words",
1768
+ "4) End your response with 'END'",
1769
+ "5) Use ONLY lowercase letters (no uppercase anywhere)"
1770
+ ].join("\n"),
1771
+ messages: [{ role: "user", content: "Describe what a compiler does." }]
1772
+ });
1773
+ if (!res.ok) return result("system_prompt_rules", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
1774
+ const text = extractText(res.data).trim();
1775
+ let score = 0;
1776
+ const checks = [];
1777
+ if (text.startsWith("KODY:") || text.startsWith("kody:")) {
1778
+ score += 20;
1779
+ checks.push("starts-with-kody");
1780
+ }
1781
+ if (!text.toLowerCase().split(/\s+/).includes("the")) {
1782
+ score += 20;
1783
+ checks.push("no-the");
1784
+ }
1785
+ if (text.split(/\s+/).length <= 55) {
1786
+ score += 20;
1787
+ checks.push("under-50-words");
1788
+ }
1789
+ if (text.endsWith("END") || text.endsWith("end")) {
1790
+ score += 20;
1791
+ checks.push("ends-with-end");
1792
+ }
1793
+ if (text === text.toLowerCase()) {
1794
+ score += 20;
1795
+ checks.push("all-lowercase");
1796
+ }
1797
+ const status = score >= 80 ? "pass" : score >= 40 ? "warn" : "fail";
1798
+ return result(
1799
+ "system_prompt_rules",
1800
+ "basic",
1801
+ status,
1802
+ score,
1803
+ Date.now() - t,
1804
+ `${score / 20}/5 rules followed: ${checks.join(", ")}`,
1805
+ { instructionCompliance: score }
1806
+ );
1807
+ }
1808
+ async function testExtendedThinking(ctx) {
1809
+ const t = Date.now();
1810
+ const res = await apiCall(ctx, {
1811
+ max_tokens: 200,
1812
+ thinking: { type: "enabled", budget_tokens: 2e3 },
1813
+ messages: [{ role: "user", content: "What is 15 * 23?" }]
1814
+ });
1815
+ if (!res.ok) return result(
1816
+ "extended_thinking",
1817
+ "infrastructure",
1818
+ "warn",
1819
+ 50,
1820
+ Date.now() - t,
1821
+ `Request failed (model may not support thinking): ${res.errorMsg?.slice(0, 80)}`
1822
+ );
1823
+ const hasThinking = Array.isArray(res.data.content) && res.data.content.some((b) => b.type === "thinking");
1824
+ const hasText = extractText(res.data).length > 0;
1825
+ if (hasThinking) return result("extended_thinking", "infrastructure", "pass", 100, Date.now() - t, "Thinking block present in response");
1826
+ if (hasText) return result("extended_thinking", "infrastructure", "warn", 70, Date.now() - t, "Response OK but no thinking block");
1827
+ return result("extended_thinking", "infrastructure", "fail", 0, Date.now() - t, "No content in response");
1828
+ }
1829
+ async function testToolRead(ctx) {
1830
+ const t = Date.now();
1831
+ const testFile = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
1832
+ fs12.writeFileSync(testFile, "KODY_SECRET_CONTENT_42");
1833
+ try {
1834
+ const conv = await runToolConversation(
1835
+ ctx,
1836
+ [TOOL_READ],
1837
+ `Read the file ${testFile} and tell me what it contains.`,
1838
+ (name, input) => {
1839
+ if (name === "Read" && input.path === testFile) return "KODY_SECRET_CONTENT_42";
1840
+ return "Error: File not found";
1841
+ }
1842
+ );
1843
+ if (conv.error) return result("tool_read", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
1844
+ const calledRead = conv.toolCalls.some((tc) => tc.name === "Read");
1845
+ const correctPath = conv.toolCalls.some((tc) => tc.name === "Read" && tc.input.path === testFile);
1846
+ const mentionsContent = conv.finalText.includes("KODY_SECRET_CONTENT_42") || conv.finalText.includes("42");
1847
+ let acc = 0;
1848
+ if (calledRead) acc += 30;
1849
+ if (correctPath) acc += 30;
1850
+ if (mentionsContent) acc += 40;
1851
+ return result(
1852
+ "tool_read",
1853
+ "tool-use",
1854
+ acc >= 60 ? "pass" : "fail",
1855
+ acc,
1856
+ Date.now() - t,
1857
+ `Read called: ${calledRead}, correct path: ${correctPath}, content referenced: ${mentionsContent}`,
1858
+ { toolSelection: calledRead ? 100 : 0 }
1859
+ );
1860
+ } finally {
1861
+ fs12.rmSync(testFile, { force: true });
1862
+ }
1863
+ }
1864
+ async function testToolEdit(ctx) {
1865
+ const t = Date.now();
1866
+ const conv = await runToolConversation(
1867
+ ctx,
1868
+ [TOOL_READ, TOOL_EDIT],
1869
+ 'Read the file /tmp/kody-edit-test.txt, then use Edit to replace "hello" with "goodbye" in it.',
1870
+ (name, input) => {
1871
+ if (name === "Read") return "hello world";
1872
+ if (name === "Edit") return "File edited successfully";
1873
+ return "Unknown tool";
1874
+ }
1875
+ );
1876
+ if (conv.error) return result("tool_edit", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
1877
+ const editCall = conv.toolCalls.find((tc) => tc.name === "Edit");
1878
+ let acc = 0;
1879
+ if (editCall) {
1880
+ acc += 40;
1881
+ if (editCall.input.old_string === "hello") acc += 30;
1882
+ if (editCall.input.new_string === "goodbye") acc += 30;
1883
+ }
1884
+ return result(
1885
+ "tool_edit",
1886
+ "tool-use",
1887
+ acc >= 70 ? "pass" : acc > 0 ? "warn" : "fail",
1888
+ acc,
1889
+ Date.now() - t,
1890
+ editCall ? `Edit called with old="${editCall.input.old_string}" new="${editCall.input.new_string}"` : "Edit tool was not called",
1891
+ { toolSelection: editCall ? 100 : 0 }
1892
+ );
1893
+ }
1894
+ async function testToolBash(ctx) {
1895
+ const t = Date.now();
1896
+ const conv = await runToolConversation(
1897
+ ctx,
1898
+ [TOOL_BASH],
1899
+ "Run this exact bash command: echo KODY_BASH_OK",
1900
+ (name, input) => {
1901
+ if (name === "Bash") return "KODY_BASH_OK\n";
1902
+ return "Error";
1903
+ }
1904
+ );
1905
+ if (conv.error) return result("tool_bash", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
1906
+ const bashCall = conv.toolCalls.find((tc) => tc.name === "Bash");
1907
+ const correctCmd = bashCall && String(bashCall.input.command).includes("echo KODY_BASH_OK");
1908
+ const acc = bashCall ? correctCmd ? 100 : 50 : 0;
1909
+ return result(
1910
+ "tool_bash",
1911
+ "tool-use",
1912
+ acc >= 50 ? "pass" : "fail",
1913
+ acc,
1914
+ Date.now() - t,
1915
+ bashCall ? `Bash called: ${bashCall.input.command}` : "Bash tool was not called",
1916
+ { toolSelection: bashCall ? 100 : 0 }
1917
+ );
1918
+ }
1919
+ async function testImageAttachment(ctx) {
1920
+ const t = Date.now();
1921
+ const pngData = createRedPng().toString("base64");
1922
+ const res = await apiCall(ctx, {
1923
+ max_tokens: 100,
1924
+ temperature: 0,
1925
+ messages: [{
1926
+ role: "user",
1927
+ content: [
1928
+ { type: "image", source: { type: "base64", media_type: "image/png", data: pngData } },
1929
+ { type: "text", text: "What color is this image? Reply with just the color name." }
1930
+ ]
1931
+ }]
1932
+ });
1933
+ if (!res.ok) return result(
1934
+ "image_attachment",
1935
+ "tool-use",
1936
+ "fail",
1937
+ 0,
1938
+ Date.now() - t,
1939
+ `API error (model may not support vision): ${res.errorMsg?.slice(0, 80)}`
1940
+ );
1941
+ const text = extractText(res.data).toLowerCase();
1942
+ const mentionsRed = text.includes("red");
1943
+ const mentionsColor = mentionsRed || text.includes("color") || text.includes("image") || text.includes("pixel");
1944
+ const acc = mentionsRed ? 100 : mentionsColor ? 50 : 20;
1945
+ return result(
1946
+ "image_attachment",
1947
+ "tool-use",
1948
+ mentionsRed ? "pass" : mentionsColor ? "warn" : "fail",
1949
+ acc,
1950
+ Date.now() - t,
1951
+ `Response: ${text.slice(0, 80)}`
1952
+ );
1953
+ }
1954
+ async function testErrorRecovery(ctx) {
1955
+ const t = Date.now();
1956
+ let errorGiven = false;
1957
+ const conv = await runToolConversation(
1958
+ ctx,
1959
+ [TOOL_READ, TOOL_BASH],
1960
+ "Read the file /tmp/nonexistent-kody-file.txt and tell me what's in it. If the file doesn't exist, say so.",
1961
+ (name, input) => {
1962
+ if (name === "Read" && !errorGiven) {
1963
+ errorGiven = true;
1964
+ return "Error: ENOENT: no such file or directory";
1965
+ }
1966
+ if (name === "Bash") return "ls: /tmp/nonexistent-kody-file.txt: No such file or directory";
1967
+ return "Error: File not found";
1968
+ }
1969
+ );
1970
+ if (conv.error) return result("error_recovery", "advanced", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
1971
+ 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");
1972
+ const tried = conv.toolCalls.length >= 1;
1973
+ const acc = reported ? tried ? 100 : 70 : 20;
1974
+ return result(
1975
+ "error_recovery",
1976
+ "advanced",
1977
+ reported ? "pass" : "warn",
1978
+ acc,
1979
+ Date.now() - t,
1980
+ reported ? "Gracefully reported missing file" : `Response: ${conv.finalText.slice(0, 80)}`
1981
+ );
1982
+ }
1983
+ async function testToolMultiStep(ctx) {
1984
+ const t = Date.now();
1985
+ const r = runClaudeTest(
1986
+ ctx,
1987
+ "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."
1988
+ );
1989
+ if (!r.stdout.trim() && r.exitCode !== 0) return result(
1990
+ "tool_multi_step",
1991
+ "tool-use",
1992
+ "fail",
1993
+ 0,
1994
+ Date.now() - t,
1995
+ `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
1996
+ );
1997
+ const out = r.stdout.trim().toLowerCase();
1998
+ const correct = out.includes("main");
1999
+ return result(
2000
+ "tool_multi_step",
2001
+ "tool-use",
2002
+ correct ? "pass" : "fail",
2003
+ correct ? 100 : 20,
2004
+ Date.now() - t,
2005
+ correct ? "Correct: main" : `Got: ${out.slice(0, 80)}`
2006
+ );
2007
+ }
2008
+ async function testPlanStage(ctx) {
2009
+ const t = Date.now();
2010
+ const wasClean = isGitClean(ctx.projectDir);
2011
+ const r = runClaudeTest(ctx, [
2012
+ "You are a planning agent. Your ONLY job is to output a markdown plan.",
2013
+ "CRITICAL: Do NOT use Edit, Write, or Bash tools. Do NOT modify any files. ONLY use Read, Glob, and Grep for research.",
2014
+ "If you modify any files, the system will crash.",
2015
+ "",
2016
+ "Task: Plan adding a /health endpoint to an Express app.",
2017
+ "Output a markdown plan with ## Step N sections. Each step must have File, Change, and Why fields.",
2018
+ "Keep it to 3 steps maximum."
2019
+ ].join("\n"), [], 12e4);
2020
+ const filesModified = wasClean && !isGitClean(ctx.projectDir);
2021
+ if (filesModified) revertChanges(ctx.projectDir);
2022
+ if (!r.stdout.trim() && r.exitCode !== 0) return result(
2023
+ "plan_stage",
2024
+ "stage-simulation",
2025
+ "fail",
2026
+ 0,
2027
+ Date.now() - t,
2028
+ `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2029
+ );
2030
+ const out = r.stdout;
2031
+ const hasStepFormat = /##\s*Step/i.test(out);
2032
+ const hasStructure = hasStepFormat || /\*\*File\*\*/i.test(out) && /\*\*Change\*\*/i.test(out);
2033
+ const boundary = filesModified ? 0 : 100;
2034
+ const format = hasStructure ? 100 : hasStepFormat ? 70 : out.length > 50 ? 30 : 0;
2035
+ const acc = Math.round(boundary * 0.6 + format * 0.4);
2036
+ const status = filesModified ? "fail" : hasStructure ? "pass" : "warn";
2037
+ return result(
2038
+ "plan_stage",
2039
+ "stage-simulation",
2040
+ status,
2041
+ acc,
2042
+ Date.now() - t,
2043
+ filesModified ? "FAIL: Model modified files during plan stage (instruction violation)" : hasStructure ? "Plan output with correct structure, no files modified" : "Output lacks expected ## Step structure",
2044
+ { boundaryRespect: boundary, outputFormat: format, instructionCompliance: boundary }
2045
+ );
2046
+ }
2047
+ async function testBuildStage(ctx) {
2048
+ const t = Date.now();
2049
+ const r = runClaudeTest(ctx, "Add a comment '// kody-build-test' as the very first line of src/logger.ts. That is your only task.");
2050
+ const diff = (() => {
2051
+ try {
2052
+ return execSync2("git diff src/logger.ts", { cwd: ctx.projectDir, encoding: "utf-8", timeout: 5e3 });
2053
+ } catch {
2054
+ return "";
2055
+ }
2056
+ })();
2057
+ const edited = diff.includes("kody-build-test");
2058
+ revertChanges(ctx.projectDir);
2059
+ if (!r.stdout.trim() && r.exitCode !== 0 && !edited) return result(
2060
+ "build_stage",
2061
+ "stage-simulation",
2062
+ "fail",
2063
+ 0,
2064
+ Date.now() - t,
2065
+ `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2066
+ );
2067
+ return result(
2068
+ "build_stage",
2069
+ "stage-simulation",
2070
+ edited ? "pass" : "fail",
2071
+ edited ? 100 : 0,
2072
+ Date.now() - t,
2073
+ edited ? "File correctly modified with expected comment" : "File was not modified as expected"
2074
+ );
2075
+ }
2076
+ async function testReviewStage(ctx) {
2077
+ const t = Date.now();
2078
+ const wasClean = isGitClean(ctx.projectDir);
2079
+ const r = runClaudeTest(ctx, [
2080
+ "You are a code review agent. Review the file src/logger.ts.",
2081
+ "CRITICAL: Do NOT modify any files. Only READ and analyze.",
2082
+ "Output your review as markdown with this exact format:",
2083
+ "## Summary",
2084
+ "<1-2 sentence summary>",
2085
+ "## Issues Found",
2086
+ "- <issues>",
2087
+ "## Verdict",
2088
+ "APPROVE or REQUEST_CHANGES"
2089
+ ].join("\n"));
2090
+ const filesModified = wasClean && !isGitClean(ctx.projectDir);
2091
+ if (filesModified) revertChanges(ctx.projectDir);
2092
+ if (!r.stdout.trim() && r.exitCode !== 0) return result(
2093
+ "review_stage",
2094
+ "stage-simulation",
2095
+ "fail",
2096
+ 0,
2097
+ Date.now() - t,
2098
+ `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2099
+ );
2100
+ const out = r.stdout;
2101
+ const hasVerdict = /verdict/i.test(out);
2102
+ const hasSummary = /summary/i.test(out);
2103
+ const boundary = filesModified ? 0 : 100;
2104
+ const format = (hasVerdict ? 50 : 0) + (hasSummary ? 50 : 0);
2105
+ const acc = Math.round(boundary * 0.5 + format * 0.5);
2106
+ const status = filesModified ? "fail" : hasVerdict && hasSummary ? "pass" : "warn";
2107
+ return result(
2108
+ "review_stage",
2109
+ "stage-simulation",
2110
+ status,
2111
+ acc,
2112
+ Date.now() - t,
2113
+ filesModified ? "FAIL: Model modified files during review (instruction violation)" : `Summary: ${hasSummary}, Verdict: ${hasVerdict}, no files modified`,
2114
+ { boundaryRespect: boundary, outputFormat: format }
2115
+ );
2116
+ }
2117
+ async function testMcpTools(ctx) {
2118
+ const t = Date.now();
2119
+ const mcpConfig = path11.join(os2.tmpdir(), `kody-test-mcp-${Date.now()}.json`);
2120
+ const testFile = path11.join(ctx.projectDir, "kody-mcp-compat-test.txt");
2121
+ try {
2122
+ fs12.writeFileSync(mcpConfig, JSON.stringify({
2123
+ mcpServers: {
2124
+ filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", ctx.projectDir] }
2125
+ }
2126
+ }));
2127
+ const r = runClaudeTest(
2128
+ ctx,
2129
+ `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.`,
2130
+ ["--mcp-config", mcpConfig],
2131
+ 12e4
2132
+ );
2133
+ const created = fs12.existsSync(testFile);
2134
+ const content = created ? fs12.readFileSync(testFile, "utf-8").trim() : "";
2135
+ const correct = content.includes("mcp-ok");
2136
+ return result(
2137
+ "mcp_tools",
2138
+ "advanced",
2139
+ created ? "pass" : "fail",
2140
+ correct ? 100 : created ? 70 : 0,
2141
+ Date.now() - t,
2142
+ created ? `File created, content: ${content.slice(0, 50)}` : `MCP test failed: ${r.stderr.slice(0, 80)}`
2143
+ );
2144
+ } catch (err) {
2145
+ return result("mcp_tools", "advanced", "warn", 0, Date.now() - t, `MCP test error: ${err instanceof Error ? err.message : String(err)}`);
2146
+ } finally {
2147
+ fs12.rmSync(mcpConfig, { force: true });
2148
+ fs12.rmSync(testFile, { force: true });
2149
+ revertChanges(ctx.projectDir);
2150
+ }
2151
+ }
2152
+ var TOOL_READ, TOOL_EDIT, TOOL_BASH, CRC_TABLE, ALL_TESTS;
2153
+ var init_test_model_tests = __esm({
2154
+ "src/cli/test-model-tests.ts"() {
2155
+ "use strict";
2156
+ TOOL_READ = {
2157
+ name: "Read",
2158
+ description: "Read a file from the filesystem",
2159
+ input_schema: {
2160
+ type: "object",
2161
+ properties: { path: { type: "string", description: "Absolute file path" } },
2162
+ required: ["path"]
2163
+ }
2164
+ };
2165
+ TOOL_EDIT = {
2166
+ name: "Edit",
2167
+ description: "Replace old_string with new_string in a file",
2168
+ input_schema: {
2169
+ type: "object",
2170
+ properties: {
2171
+ file_path: { type: "string" },
2172
+ old_string: { type: "string" },
2173
+ new_string: { type: "string" }
2174
+ },
2175
+ required: ["file_path", "old_string", "new_string"]
2176
+ }
2177
+ };
2178
+ TOOL_BASH = {
2179
+ name: "Bash",
2180
+ description: "Execute a bash command and return output",
2181
+ input_schema: {
2182
+ type: "object",
2183
+ properties: { command: { type: "string", description: "The command to run" } },
2184
+ required: ["command"]
2185
+ }
2186
+ };
2187
+ CRC_TABLE = new Uint32Array(256);
2188
+ for (let n = 0; n < 256; n++) {
2189
+ let c = n;
2190
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
2191
+ CRC_TABLE[n] = c >>> 0;
2192
+ }
2193
+ ALL_TESTS = [
2194
+ // Infrastructure
2195
+ { name: "extended_thinking", category: "infrastructure", description: "Extended thinking parameter support", run: testExtendedThinking },
2196
+ // Basic
2197
+ { name: "simple_prompt", category: "basic", description: "Basic text prompt and response", run: testSimplePrompt },
2198
+ { name: "json_output", category: "basic", description: "JSON-only output constraint", run: testJsonOutput },
2199
+ { name: "system_prompt_rules", category: "basic", description: "Multi-rule system prompt adherence", run: testSystemPromptRules },
2200
+ // Tool use
2201
+ { name: "tool_read", category: "tool-use", description: "Read tool: file reading", run: testToolRead },
2202
+ { name: "tool_edit", category: "tool-use", description: "Edit tool: old/new string replacement", run: testToolEdit },
2203
+ { name: "tool_bash", category: "tool-use", description: "Bash tool: command execution", run: testToolBash },
2204
+ { name: "tool_multi_step", category: "tool-use", description: "Multi-step tool chain via CLI", run: testToolMultiStep },
2205
+ { name: "image_attachment", category: "tool-use", description: "Vision: image content processing", run: testImageAttachment },
2206
+ // Stage simulation
2207
+ { name: "plan_stage", category: "stage-simulation", description: "Plan stage: read-only research + structured output", run: testPlanStage },
2208
+ { name: "build_stage", category: "stage-simulation", description: "Build stage: code editing", run: testBuildStage },
2209
+ { name: "review_stage", category: "stage-simulation", description: "Review stage: read-only + structured verdict", run: testReviewStage },
2210
+ // Advanced
2211
+ { name: "mcp_tools", category: "advanced", description: "MCP server tool integration", run: testMcpTools },
2212
+ { name: "error_recovery", category: "advanced", description: "Graceful error handling on tool failure", run: testErrorRecovery }
2213
+ ];
2214
+ }
2215
+ });
2216
+
2217
+ // src/cli/test-model-report.ts
2218
+ function pad(str, len) {
2219
+ return str.padEnd(len);
2220
+ }
2221
+ function fmtDuration(ms) {
2222
+ return `${(ms / 1e3).toFixed(1)}s`;
2223
+ }
2224
+ function formatReport(report) {
2225
+ const W = 74;
2226
+ const lines = [];
2227
+ lines.push("=".repeat(W));
2228
+ lines.push("");
2229
+ lines.push(" Model Compatibility Report");
2230
+ lines.push(` Provider: ${report.provider} | Model: ${report.model}`);
2231
+ lines.push(` Date: ${report.timestamp}`);
2232
+ lines.push(` Duration: ${fmtDuration(report.totalDurationMs)}`);
2233
+ lines.push("");
2234
+ lines.push("-".repeat(W));
2235
+ for (const cat of CATEGORY_ORDER) {
2236
+ const catResults = report.results.filter((r) => r.category === cat);
2237
+ if (catResults.length === 0) continue;
2238
+ lines.push("");
2239
+ lines.push(` ${CATEGORY_LABELS[cat]}`);
2240
+ lines.push("");
2241
+ for (const r of catResults) {
2242
+ const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
2243
+ const name = pad(r.name, 28);
2244
+ const status = pad(r.status.toUpperCase(), 6);
2245
+ const acc = pad(`${r.accuracy}%`, 5);
2246
+ const dur = fmtDuration(r.durationMs);
2247
+ lines.push(` [${icon}] ${name} ${status} ${acc} ${dur}`);
2248
+ if (r.status !== "pass" && r.detail) {
2249
+ lines.push(` ${r.detail.slice(0, W - 8)}`);
2250
+ }
2251
+ }
2252
+ }
2253
+ const passed = report.results.filter((r) => r.status === "pass").length;
2254
+ const failed = report.results.filter((r) => r.status === "fail").length;
2255
+ const warned = report.results.filter((r) => r.status === "warn").length;
2256
+ const total = report.results.length;
2257
+ const avgAccuracy = total > 0 ? Math.round(report.results.reduce((s, r) => s + r.accuracy, 0) / total) : 0;
2258
+ lines.push("");
2259
+ lines.push("-".repeat(W));
2260
+ lines.push("");
2261
+ lines.push(` RESULTS: ${passed}/${total} PASS | ${failed} FAIL | ${warned} WARN`);
2262
+ lines.push(` OVERALL ACCURACY: ${avgAccuracy}%`);
2263
+ lines.push(` drop_params required: ${report.dropParamsRequired ? "YES" : "NO"}`);
2264
+ lines.push("");
2265
+ lines.push(" ACCURACY BY CATEGORY:");
2266
+ for (const cat of CATEGORY_ORDER) {
2267
+ const cr = report.results.filter((r) => r.category === cat);
2268
+ if (cr.length === 0) continue;
2269
+ const avg = Math.round(cr.reduce((s, r) => s + r.accuracy, 0) / cr.length);
2270
+ lines.push(` ${pad(CATEGORY_LABELS[cat], 22)} ${avg}%`);
2271
+ }
2272
+ lines.push("");
2273
+ lines.push(" RECOMMENDATION:");
2274
+ for (const line of getRecommendation(report)) {
2275
+ lines.push(` ${line}`);
2276
+ }
2277
+ lines.push("");
2278
+ lines.push("=".repeat(W));
2279
+ return lines.join("\n");
2280
+ }
2281
+ function getRecommendation(report) {
2282
+ const lines = [];
2283
+ const failedTests = report.results.filter((r) => r.status === "fail");
2284
+ const avg = report.results.length > 0 ? Math.round(report.results.reduce((s, r) => s + r.accuracy, 0) / report.results.length) : 0;
2285
+ if (avg >= 90 && failedTests.length === 0) {
2286
+ lines.push("[+] Fully compatible -- suitable for all pipeline stages");
2287
+ return lines;
2288
+ }
2289
+ const stageResults = report.results.filter((r) => r.category === "stage-simulation");
2290
+ const workingStages = stageResults.filter((r) => r.status === "pass").map((r) => r.name.replace("_stage", ""));
2291
+ const failingStages = stageResults.filter((r) => r.status !== "pass").map((r) => r.name.replace("_stage", ""));
2292
+ if (workingStages.length > 0) {
2293
+ lines.push(`[+] Suitable for: ${workingStages.join(", ")} stages`);
2294
+ }
2295
+ if (failingStages.length > 0) {
2296
+ lines.push(`[x] Not recommended for: ${failingStages.join(", ")} stages`);
2297
+ }
2298
+ if (failedTests.length > 0) {
2299
+ lines.push("");
2300
+ lines.push("Failed tests:");
2301
+ for (const t of failedTests) {
2302
+ lines.push(` - ${t.name}: ${t.detail.slice(0, 60)}`);
2303
+ }
2304
+ }
2305
+ return lines;
2306
+ }
2307
+ var CATEGORY_ORDER, CATEGORY_LABELS;
2308
+ var init_test_model_report = __esm({
2309
+ "src/cli/test-model-report.ts"() {
2310
+ "use strict";
2311
+ CATEGORY_ORDER = ["infrastructure", "basic", "tool-use", "stage-simulation", "advanced"];
2312
+ CATEGORY_LABELS = {
2313
+ infrastructure: "INFRASTRUCTURE",
2314
+ basic: "BASIC CAPABILITIES",
2315
+ "tool-use": "TOOL USE",
2316
+ "stage-simulation": "STAGE SIMULATION",
2317
+ advanced: "ADVANCED"
2318
+ };
2319
+ }
2320
+ });
2321
+
2322
+ // src/cli/test-model-command.ts
2323
+ var test_model_command_exports = {};
2324
+ __export(test_model_command_exports, {
2325
+ runTestModelCommand: () => runTestModelCommand
2326
+ });
2327
+ import * as fs13 from "fs";
2328
+ import * as os3 from "os";
2329
+ import * as path12 from "path";
2330
+ import { execFileSync as execFileSync10 } from "child_process";
2331
+ function parseTestModelArgs() {
2332
+ const args2 = process.argv.slice(3);
2333
+ function getArg3(flag) {
2334
+ const idx = args2.indexOf(flag);
2335
+ if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) return args2[idx + 1];
2336
+ return void 0;
2337
+ }
2338
+ const hasFlag3 = (f) => args2.includes(f);
2339
+ if (hasFlag3("--help") || hasFlag3("-h")) {
2340
+ logger.info([
2341
+ "Usage: kody test-model --provider <provider> --model <model> --key <api-key> [options]",
2342
+ "",
2343
+ "Options:",
2344
+ " --provider LLM provider name (e.g. gemini, openai, mistral)",
2345
+ " --model Model identifier (e.g. gemini-2.5-flash)",
2346
+ " --key API key for the provider",
2347
+ " --key-env Read API key from this environment variable",
2348
+ " --skip-proxy Use an already-running LiteLLM proxy (don't start one)",
2349
+ " --litellm-url LiteLLM proxy URL (default: http://localhost:4099)",
2350
+ " --filter Comma-separated test names to run (default: all)",
2351
+ " --list List all available tests and exit"
2352
+ ].join("\n"));
2353
+ process.exit(0);
2354
+ }
2355
+ if (hasFlag3("--list")) {
2356
+ for (const t of ALL_TESTS) {
2357
+ logger.info(` ${t.name.padEnd(24)} [${t.category}] ${t.description}`);
2358
+ }
2359
+ process.exit(0);
2360
+ }
2361
+ const provider = getArg3("--provider");
2362
+ const model = getArg3("--model");
2363
+ const key = getArg3("--key");
2364
+ const keyEnv = getArg3("--key-env");
2365
+ if (!provider || !model) {
2366
+ logger.error("Required: --provider <provider> --model <model> --key <key>");
2367
+ logger.error("Run with --help for usage.");
2368
+ process.exit(1);
2369
+ }
2370
+ let apiKey = key;
2371
+ if (!apiKey && keyEnv) apiKey = process.env[keyEnv];
2372
+ if (!apiKey) {
2373
+ logger.error("API key required: use --key <value> or --key-env <ENV_VAR>");
2374
+ process.exit(1);
2375
+ }
2376
+ return {
2377
+ provider,
2378
+ model,
2379
+ apiKey,
2380
+ proxyUrl: getArg3("--litellm-url") ?? TEST_URL,
2381
+ skipProxy: hasFlag3("--skip-proxy"),
2382
+ filter: getArg3("--filter")?.split(",")
2383
+ };
2384
+ }
2385
+ function generateConfig(provider, model, dropParams) {
2386
+ const lines = [];
2387
+ if (dropParams) {
2388
+ lines.push("litellm_settings:");
2389
+ lines.push(" drop_params: true");
2390
+ lines.push("");
2391
+ }
2392
+ lines.push("model_list:");
2393
+ lines.push(` - model_name: ${model}`);
2394
+ lines.push(" litellm_params:");
2395
+ lines.push(` model: ${provider}/${model}`);
2396
+ lines.push(" api_key: os.environ/ANTHROPIC_COMPATIBLE_API_KEY");
2397
+ return lines.join("\n") + "\n";
2398
+ }
2399
+ async function startProxy(config, url) {
2400
+ try {
2401
+ execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2402
+ } catch {
2403
+ try {
2404
+ execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
2405
+ } catch {
2406
+ logger.error("litellm not installed. Install: pip install 'litellm[proxy]'");
2407
+ return null;
2408
+ }
2409
+ }
2410
+ fs13.writeFileSync(CONFIG_PATH, config);
2411
+ const portMatch = url.match(/:(\d+)/);
2412
+ const port = portMatch ? portMatch[1] : "4099";
2413
+ const { spawn: spawn2 } = await import("child_process");
2414
+ const child = spawn2("litellm", ["--config", CONFIG_PATH, "--port", port], {
2415
+ stdio: ["ignore", "pipe", "pipe"],
2416
+ detached: true,
2417
+ env: process.env
2418
+ });
2419
+ for (let i = 0; i < 30; i++) {
2420
+ await delay(2e3);
2421
+ if (await checkLitellmHealth(url)) {
2422
+ logger.info(`LiteLLM proxy ready at ${url}`);
2423
+ return child;
2424
+ }
2425
+ }
2426
+ child.kill();
2427
+ return null;
2428
+ }
2429
+ async function quickApiTest(url, model, apiKey) {
2430
+ try {
2431
+ const res = await fetch(`${url}/v1/messages`, {
2432
+ method: "POST",
2433
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
2434
+ body: JSON.stringify({
2435
+ model,
2436
+ max_tokens: 10,
2437
+ messages: [{ role: "user", content: "Say ok" }],
2438
+ context_management: { policy: "smart" }
2439
+ }),
2440
+ signal: AbortSignal.timeout(3e4)
2441
+ });
2442
+ if (!res.ok) {
2443
+ const body = await res.text();
2444
+ return { ok: false, error: body.slice(0, 200) };
2445
+ }
2446
+ return { ok: true };
2447
+ } catch (err) {
2448
+ return { ok: false, error: String(err) };
2449
+ }
2450
+ }
2451
+ function delay(ms) {
2452
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
2453
+ }
2454
+ async function runTestModelCommand() {
2455
+ const opts = parseTestModelArgs();
2456
+ const startTime = Date.now();
2457
+ logger.info(`Testing model compatibility: ${opts.provider}/${opts.model}`);
2458
+ logger.info("");
2459
+ let proxyProcess = null;
2460
+ let dropParamsRequired = false;
2461
+ const cleanup = () => {
2462
+ if (proxyProcess) {
2463
+ proxyProcess.kill();
2464
+ proxyProcess = null;
2465
+ }
2466
+ fs13.rmSync(CONFIG_PATH, { force: true });
2467
+ };
2468
+ process.on("SIGINT", () => {
2469
+ cleanup();
2470
+ process.exit(1);
2471
+ });
2472
+ process.on("SIGTERM", () => {
2473
+ cleanup();
2474
+ process.exit(1);
2475
+ });
2476
+ try {
2477
+ if (!opts.skipProxy) {
2478
+ process.env.ANTHROPIC_COMPATIBLE_API_KEY = opts.apiKey;
2479
+ logger.info("Starting LiteLLM proxy (without drop_params)...");
2480
+ proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, false), opts.proxyUrl);
2481
+ if (!proxyProcess) {
2482
+ logger.error("Failed to start LiteLLM proxy");
2483
+ process.exit(1);
2484
+ }
2485
+ const quickRes = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
2486
+ if (!quickRes.ok) {
2487
+ logger.info("Model needs drop_params: true -- restarting proxy...");
2488
+ proxyProcess.kill();
2489
+ proxyProcess = null;
2490
+ await delay(2e3);
2491
+ proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, true), opts.proxyUrl);
2492
+ dropParamsRequired = true;
2493
+ if (!proxyProcess) {
2494
+ logger.error("Failed to start LiteLLM proxy with drop_params");
2495
+ process.exit(1);
2496
+ }
2497
+ const retry = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
2498
+ if (!retry.ok) {
2499
+ logger.error(`Model not accessible: ${retry.error}`);
2500
+ process.exit(1);
2501
+ }
2502
+ logger.info("Proxy restarted with drop_params: true");
2503
+ } else {
2504
+ logger.info("drop_params not required");
2505
+ }
2506
+ } else {
2507
+ logger.info(`Using existing proxy at ${opts.proxyUrl}`);
2508
+ }
2509
+ const tests = opts.filter ? ALL_TESTS.filter((t) => opts.filter.includes(t.name)) : ALL_TESTS;
2510
+ logger.info(`Running ${tests.length} compatibility tests...`);
2511
+ logger.info("");
2512
+ const ctx = { proxyUrl: opts.proxyUrl, model: opts.model, apiKey: opts.apiKey, projectDir: process.cwd() };
2513
+ const results = [];
2514
+ for (const test of tests) {
2515
+ process.stdout.write(` ${test.name.padEnd(28)} `);
2516
+ try {
2517
+ const r = await test.run(ctx);
2518
+ results.push(r);
2519
+ const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
2520
+ logger.info(`[${icon}] ${r.status.toUpperCase()} ${r.accuracy}% (${(r.durationMs / 1e3).toFixed(1)}s)`);
2521
+ } catch (err) {
2522
+ const r = {
2523
+ name: test.name,
2524
+ category: test.category,
2525
+ status: "fail",
2526
+ accuracy: 0,
2527
+ durationMs: 0,
2528
+ detail: `Crash: ${err instanceof Error ? err.message : String(err)}`
2529
+ };
2530
+ results.push(r);
2531
+ logger.info("[x] CRASH");
2532
+ }
2533
+ }
2534
+ const report = {
2535
+ provider: opts.provider,
2536
+ model: opts.model,
2537
+ results,
2538
+ totalDurationMs: Date.now() - startTime,
2539
+ dropParamsRequired,
2540
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
2541
+ };
2542
+ console.log("");
2543
+ console.log(formatReport(report));
2544
+ const failed = results.filter((r) => r.status === "fail").length;
2545
+ process.exit(failed > 0 ? 1 : 0);
2546
+ } finally {
2547
+ cleanup();
2548
+ }
2549
+ }
2550
+ var TEST_PORT, TEST_URL, CONFIG_PATH;
2551
+ var init_test_model_command = __esm({
2552
+ "src/cli/test-model-command.ts"() {
2553
+ "use strict";
2554
+ init_logger();
2555
+ init_litellm();
2556
+ init_test_model_tests();
2557
+ init_test_model_report();
2558
+ TEST_PORT = 4099;
2559
+ TEST_URL = `http://localhost:${TEST_PORT}`;
2560
+ CONFIG_PATH = path12.join(os3.tmpdir(), "kody-test-model-config.yaml");
2561
+ }
2562
+ });
2563
+
1559
2564
  // src/ci/parse-inputs.ts
1560
2565
  var parse_inputs_exports = {};
1561
2566
  __export(parse_inputs_exports, {
@@ -1563,16 +2568,16 @@ __export(parse_inputs_exports, {
1563
2568
  runCiParse: () => runCiParse,
1564
2569
  writeOutputs: () => writeOutputs
1565
2570
  });
1566
- import * as fs12 from "fs";
2571
+ import * as fs14 from "fs";
1567
2572
  function generateTimestamp() {
1568
2573
  const now = /* @__PURE__ */ new Date();
1569
- const pad = (n) => String(n).padStart(2, "0");
2574
+ const pad2 = (n) => String(n).padStart(2, "0");
1570
2575
  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());
2576
+ const m = pad2(now.getMonth() + 1);
2577
+ const d = pad2(now.getDate());
2578
+ const H = pad2(now.getHours());
2579
+ const M = pad2(now.getMinutes());
2580
+ const S = pad2(now.getSeconds());
1576
2581
  return `${y}${m}${d}-${H}${M}${S}`;
1577
2582
  }
1578
2583
  function parseCommentInputs() {
@@ -1724,40 +2729,40 @@ function parseCommentInputs() {
1724
2729
  trigger_type: "comment"
1725
2730
  };
1726
2731
  }
1727
- function writeOutputs(result) {
2732
+ function writeOutputs(result2) {
1728
2733
  const outputFile = process.env.GITHUB_OUTPUT;
1729
2734
  function output(key, value) {
1730
2735
  if (outputFile) {
1731
2736
  if (value.includes("\n")) {
1732
- fs12.appendFileSync(outputFile, `${key}<<KODY_EOF
2737
+ fs14.appendFileSync(outputFile, `${key}<<KODY_EOF
1733
2738
  ${value}
1734
2739
  KODY_EOF
1735
2740
  `);
1736
2741
  } else {
1737
- fs12.appendFileSync(outputFile, `${key}=${value}
2742
+ fs14.appendFileSync(outputFile, `${key}=${value}
1738
2743
  `);
1739
2744
  }
1740
2745
  }
1741
2746
  const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
1742
2747
  console.log(`${key}=${display}`);
1743
2748
  }
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);
2749
+ output("task_id", result2.task_id);
2750
+ output("mode", result2.mode);
2751
+ output("from_stage", result2.from_stage);
2752
+ output("issue_number", result2.issue_number);
2753
+ output("pr_number", result2.pr_number);
2754
+ output("feedback", result2.feedback);
2755
+ output("complexity", result2.complexity);
2756
+ output("ci_run_id", result2.ci_run_id);
2757
+ output("ticket_id", result2.ticket_id);
2758
+ output("prd_file", result2.prd_file);
2759
+ output("dry_run", result2.dry_run ? "true" : "false");
2760
+ output("valid", result2.valid ? "true" : "false");
2761
+ output("trigger_type", result2.trigger_type);
1757
2762
  }
1758
2763
  function runCiParse() {
1759
- const result = parseCommentInputs();
1760
- writeOutputs(result);
2764
+ const result2 = parseCommentInputs();
2765
+ writeOutputs(result2);
1761
2766
  }
1762
2767
  var VALID_MODES;
1763
2768
  var init_parse_inputs = __esm({
@@ -1859,7 +2864,7 @@ var init_definitions = __esm({
1859
2864
  });
1860
2865
 
1861
2866
  // src/git-utils.ts
1862
- import { execFileSync as execFileSync10 } from "child_process";
2867
+ import { execFileSync as execFileSync11 } from "child_process";
1863
2868
  function getHookSafeEnv() {
1864
2869
  if (!_hookSafeEnv) {
1865
2870
  _hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
@@ -1867,7 +2872,7 @@ function getHookSafeEnv() {
1867
2872
  return _hookSafeEnv;
1868
2873
  }
1869
2874
  function git(args2, options) {
1870
- return execFileSync10("git", args2, {
2875
+ return execFileSync11("git", args2, {
1871
2876
  encoding: "utf-8",
1872
2877
  timeout: options?.timeout ?? 3e4,
1873
2878
  cwd: options?.cwd,
@@ -2053,22 +3058,22 @@ var init_git_utils = __esm({
2053
3058
  });
2054
3059
 
2055
3060
  // src/pipeline/state.ts
2056
- import * as fs13 from "fs";
2057
- import * as path11 from "path";
3061
+ import * as fs15 from "fs";
3062
+ import * as path13 from "path";
2058
3063
  function loadState(taskId, taskDir) {
2059
- const p = path11.join(taskDir, "status.json");
2060
- if (!fs13.existsSync(p)) return null;
3064
+ const p = path13.join(taskDir, "status.json");
3065
+ if (!fs15.existsSync(p)) return null;
2061
3066
  try {
2062
- const result = parseJsonSafe(
2063
- fs13.readFileSync(p, "utf-8"),
3067
+ const result2 = parseJsonSafe(
3068
+ fs15.readFileSync(p, "utf-8"),
2064
3069
  ["taskId", "state", "stages", "createdAt", "updatedAt"]
2065
3070
  );
2066
- if (!result.ok) {
2067
- logger.warn(` Corrupt status.json: ${result.error}`);
3071
+ if (!result2.ok) {
3072
+ logger.warn(` Corrupt status.json: ${result2.error}`);
2068
3073
  return null;
2069
3074
  }
2070
- if (result.data.taskId !== taskId) return null;
2071
- return result.data;
3075
+ if (result2.data.taskId !== taskId) return null;
3076
+ return result2.data;
2072
3077
  } catch {
2073
3078
  return null;
2074
3079
  }
@@ -2078,10 +3083,10 @@ function writeState(state, taskDir) {
2078
3083
  ...state,
2079
3084
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2080
3085
  };
2081
- const target = path11.join(taskDir, "status.json");
3086
+ const target = path13.join(taskDir, "status.json");
2082
3087
  const tmp = target + ".tmp";
2083
- fs13.writeFileSync(tmp, JSON.stringify(updated, null, 2));
2084
- fs13.renameSync(tmp, target);
3088
+ fs15.writeFileSync(tmp, JSON.stringify(updated, null, 2));
3089
+ fs15.renameSync(tmp, target);
2085
3090
  return updated;
2086
3091
  }
2087
3092
  function initState(taskId) {
@@ -2122,16 +3127,16 @@ var init_complexity = __esm({
2122
3127
  });
2123
3128
 
2124
3129
  // src/memory.ts
2125
- import * as fs14 from "fs";
2126
- import * as path12 from "path";
3130
+ import * as fs16 from "fs";
3131
+ import * as path14 from "path";
2127
3132
  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();
3133
+ const memoryDir = path14.join(projectDir, ".kody", "memory");
3134
+ if (!fs16.existsSync(memoryDir)) return "";
3135
+ const files = fs16.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
2131
3136
  if (files.length === 0) return "";
2132
3137
  const sections = [];
2133
3138
  for (const file of files) {
2134
- const content = fs14.readFileSync(path12.join(memoryDir, file), "utf-8").trim();
3139
+ const content = fs16.readFileSync(path14.join(memoryDir, file), "utf-8").trim();
2135
3140
  if (content) {
2136
3141
  sections.push(`## ${file.replace(".md", "")}
2137
3142
  ${content}`);
@@ -2150,8 +3155,8 @@ var init_memory = __esm({
2150
3155
  });
2151
3156
 
2152
3157
  // src/context-tiers.ts
2153
- import * as fs15 from "fs";
2154
- import * as path13 from "path";
3158
+ import * as fs17 from "fs";
3159
+ import * as path15 from "path";
2155
3160
  function estimateTokens(text) {
2156
3161
  return Math.ceil(text.length / 4);
2157
3162
  }
@@ -2178,8 +3183,8 @@ function generateL0(content, filename) {
2178
3183
  break;
2179
3184
  }
2180
3185
  }
2181
- const result = parts.join("\n");
2182
- return result.slice(0, L0_MAX_CHARS);
3186
+ const result2 = parts.join("\n");
3187
+ return result2.slice(0, L0_MAX_CHARS);
2183
3188
  }
2184
3189
  function generateL0Json(content) {
2185
3190
  try {
@@ -2221,8 +3226,8 @@ function generateL1(content, filename) {
2221
3226
  inSection = false;
2222
3227
  }
2223
3228
  }
2224
- const result = parts.join("\n");
2225
- return result.slice(0, L1_MAX_CHARS);
3229
+ const result2 = parts.join("\n");
3230
+ return result2.slice(0, L1_MAX_CHARS);
2226
3231
  }
2227
3232
  function generateL1Json(content) {
2228
3233
  try {
@@ -2242,7 +3247,7 @@ function generateL1Json(content) {
2242
3247
  }
2243
3248
  }
2244
3249
  function getTieredContent(filePath, content) {
2245
- const key = path13.basename(filePath);
3250
+ const key = path15.basename(filePath);
2246
3251
  return {
2247
3252
  source: filePath,
2248
3253
  L0: generateL0(content, key),
@@ -2254,15 +3259,15 @@ function selectTier(tiered, tier) {
2254
3259
  return tiered[tier];
2255
3260
  }
2256
3261
  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();
3262
+ const memoryDir = path15.join(projectDir, ".kody", "memory");
3263
+ if (!fs17.existsSync(memoryDir)) return "";
3264
+ const files = fs17.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
2260
3265
  if (files.length === 0) return "";
2261
3266
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
2262
3267
  const sections = [];
2263
3268
  for (const file of files) {
2264
- const filePath = path13.join(memoryDir, file);
2265
- const content = fs15.readFileSync(filePath, "utf-8").trim();
3269
+ const filePath = path15.join(memoryDir, file);
3270
+ const content = fs17.readFileSync(filePath, "utf-8").trim();
2266
3271
  if (!content) continue;
2267
3272
  const tiered = getTieredContent(filePath, content);
2268
3273
  const selected = selectTier(tiered, tier);
@@ -2285,9 +3290,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
2285
3290
  `;
2286
3291
  context += `Task Directory: ${taskDir}
2287
3292
  `;
2288
- const taskMdPath = path13.join(taskDir, "task.md");
2289
- if (fs15.existsSync(taskMdPath)) {
2290
- const content = fs15.readFileSync(taskMdPath, "utf-8");
3293
+ const taskMdPath = path15.join(taskDir, "task.md");
3294
+ if (fs17.existsSync(taskMdPath)) {
3295
+ const content = fs17.readFileSync(taskMdPath, "utf-8");
2291
3296
  const selected = selectContent(taskMdPath, content, policy.taskDescription);
2292
3297
  const label = tierLabel("Task Description", policy.taskDescription);
2293
3298
  context += `
@@ -2295,9 +3300,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
2295
3300
  ${selected}
2296
3301
  `;
2297
3302
  }
2298
- const taskJsonPath = path13.join(taskDir, "task.json");
2299
- if (fs15.existsSync(taskJsonPath)) {
2300
- const content = fs15.readFileSync(taskJsonPath, "utf-8");
3303
+ const taskJsonPath = path15.join(taskDir, "task.json");
3304
+ if (fs17.existsSync(taskJsonPath)) {
3305
+ const content = fs17.readFileSync(taskJsonPath, "utf-8");
2301
3306
  if (policy.taskClassification === "L2") {
2302
3307
  try {
2303
3308
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -2323,9 +3328,9 @@ ${selected}
2323
3328
  }
2324
3329
  }
2325
3330
  }
2326
- const specPath = path13.join(taskDir, "spec.md");
2327
- if (fs15.existsSync(specPath)) {
2328
- const content = fs15.readFileSync(specPath, "utf-8");
3331
+ const specPath = path15.join(taskDir, "spec.md");
3332
+ if (fs17.existsSync(specPath)) {
3333
+ const content = fs17.readFileSync(specPath, "utf-8");
2329
3334
  const selected = selectContent(specPath, content, policy.spec);
2330
3335
  const label = tierLabel("Spec", policy.spec);
2331
3336
  context += `
@@ -2333,9 +3338,9 @@ ${selected}
2333
3338
  ${selected}
2334
3339
  `;
2335
3340
  }
2336
- const planPath = path13.join(taskDir, "plan.md");
2337
- if (fs15.existsSync(planPath)) {
2338
- const content = fs15.readFileSync(planPath, "utf-8");
3341
+ const planPath = path15.join(taskDir, "plan.md");
3342
+ if (fs17.existsSync(planPath)) {
3343
+ const content = fs17.readFileSync(planPath, "utf-8");
2339
3344
  const selected = selectContent(planPath, content, policy.plan);
2340
3345
  const label = tierLabel("Plan", policy.plan);
2341
3346
  context += `
@@ -2343,9 +3348,9 @@ ${selected}
2343
3348
  ${selected}
2344
3349
  `;
2345
3350
  }
2346
- const contextMdPath = path13.join(taskDir, "context.md");
2347
- if (fs15.existsSync(contextMdPath)) {
2348
- const content = fs15.readFileSync(contextMdPath, "utf-8");
3351
+ const contextMdPath = path15.join(taskDir, "context.md");
3352
+ if (fs17.existsSync(contextMdPath)) {
3353
+ const content = fs17.readFileSync(contextMdPath, "utf-8");
2349
3354
  const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
2350
3355
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
2351
3356
  context += `
@@ -2431,24 +3436,24 @@ var init_context_tiers = __esm({
2431
3436
  });
2432
3437
 
2433
3438
  // src/context.ts
2434
- import * as fs16 from "fs";
2435
- import * as path14 from "path";
3439
+ import * as fs18 from "fs";
3440
+ import * as path16 from "path";
2436
3441
  function readPromptFile(stageName, projectDir) {
2437
3442
  if (projectDir) {
2438
- const stepFile = path14.join(projectDir, ".kody", "steps", `${stageName}.md`);
2439
- if (fs16.existsSync(stepFile)) {
2440
- return fs16.readFileSync(stepFile, "utf-8");
3443
+ const stepFile = path16.join(projectDir, ".kody", "steps", `${stageName}.md`);
3444
+ if (fs18.existsSync(stepFile)) {
3445
+ return fs18.readFileSync(stepFile, "utf-8");
2441
3446
  }
2442
3447
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
2443
3448
  }
2444
3449
  const scriptDir = new URL(".", import.meta.url).pathname;
2445
3450
  const candidates = [
2446
- path14.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
2447
- path14.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
3451
+ path16.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
3452
+ path16.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
2448
3453
  ];
2449
3454
  for (const candidate of candidates) {
2450
- if (fs16.existsSync(candidate)) {
2451
- return fs16.readFileSync(candidate, "utf-8");
3455
+ if (fs18.existsSync(candidate)) {
3456
+ return fs18.readFileSync(candidate, "utf-8");
2452
3457
  }
2453
3458
  }
2454
3459
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -2460,18 +3465,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
2460
3465
  `;
2461
3466
  context += `Task Directory: ${taskDir}
2462
3467
  `;
2463
- const taskMdPath = path14.join(taskDir, "task.md");
2464
- if (fs16.existsSync(taskMdPath)) {
2465
- const taskMd = fs16.readFileSync(taskMdPath, "utf-8");
3468
+ const taskMdPath = path16.join(taskDir, "task.md");
3469
+ if (fs18.existsSync(taskMdPath)) {
3470
+ const taskMd = fs18.readFileSync(taskMdPath, "utf-8");
2466
3471
  context += `
2467
3472
  ## Task Description
2468
3473
  ${taskMd}
2469
3474
  `;
2470
3475
  }
2471
- const taskJsonPath = path14.join(taskDir, "task.json");
2472
- if (fs16.existsSync(taskJsonPath)) {
3476
+ const taskJsonPath = path16.join(taskDir, "task.json");
3477
+ if (fs18.existsSync(taskJsonPath)) {
2473
3478
  try {
2474
- const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
3479
+ const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
2475
3480
  context += `
2476
3481
  ## Task Classification
2477
3482
  `;
@@ -2484,27 +3489,27 @@ ${taskMd}
2484
3489
  } catch {
2485
3490
  }
2486
3491
  }
2487
- const specPath = path14.join(taskDir, "spec.md");
2488
- if (fs16.existsSync(specPath)) {
2489
- const spec = fs16.readFileSync(specPath, "utf-8");
3492
+ const specPath = path16.join(taskDir, "spec.md");
3493
+ if (fs18.existsSync(specPath)) {
3494
+ const spec = fs18.readFileSync(specPath, "utf-8");
2490
3495
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
2491
3496
  context += `
2492
3497
  ## Spec Summary
2493
3498
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
2494
3499
  `;
2495
3500
  }
2496
- const planPath = path14.join(taskDir, "plan.md");
2497
- if (fs16.existsSync(planPath)) {
2498
- const plan = fs16.readFileSync(planPath, "utf-8");
3501
+ const planPath = path16.join(taskDir, "plan.md");
3502
+ if (fs18.existsSync(planPath)) {
3503
+ const plan = fs18.readFileSync(planPath, "utf-8");
2499
3504
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
2500
3505
  context += `
2501
3506
  ## Plan Summary
2502
3507
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
2503
3508
  `;
2504
3509
  }
2505
- const contextMdPath = path14.join(taskDir, "context.md");
2506
- if (fs16.existsSync(contextMdPath)) {
2507
- const accumulated = fs16.readFileSync(contextMdPath, "utf-8");
3510
+ const contextMdPath = path16.join(taskDir, "context.md");
3511
+ if (fs18.existsSync(contextMdPath)) {
3512
+ const accumulated = fs18.readFileSync(contextMdPath, "utf-8");
2508
3513
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
2509
3514
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
2510
3515
  context += `
@@ -2522,17 +3527,17 @@ ${feedback}
2522
3527
  }
2523
3528
  function inferHasUIFromScope(scope) {
2524
3529
  return scope.some((filePath) => {
2525
- const ext = path14.extname(filePath).toLowerCase();
3530
+ const ext = path16.extname(filePath).toLowerCase();
2526
3531
  if (UI_EXTENSIONS.has(ext)) return true;
2527
3532
  const normalized = filePath.replace(/\\/g, "/");
2528
3533
  return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
2529
3534
  });
2530
3535
  }
2531
3536
  function taskHasUI(taskDir) {
2532
- const taskJsonPath = path14.join(taskDir, "task.json");
2533
- if (!fs16.existsSync(taskJsonPath)) return true;
3537
+ const taskJsonPath = path16.join(taskDir, "task.json");
3538
+ if (!fs18.existsSync(taskJsonPath)) return true;
2534
3539
  try {
2535
- const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
3540
+ const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
2536
3541
  const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
2537
3542
  if (scope.length === 0) return true;
2538
3543
  return inferHasUIFromScope(scope);
@@ -2654,9 +3659,9 @@ ${prompt}` : prompt;
2654
3659
  }
2655
3660
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
2656
3661
  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();
3662
+ const qaGuidePath = path16.join(projectDir, ".kody", "qa-guide.md");
3663
+ if (fs18.existsSync(qaGuidePath)) {
3664
+ const qaGuide = fs18.readFileSync(qaGuidePath, "utf-8").trim();
2660
3665
  assembled = assembled + "\n\n" + qaGuide;
2661
3666
  }
2662
3667
  }
@@ -2686,10 +3691,12 @@ function escalateModelTier(currentTier) {
2686
3691
  function resolveModel(modelTier, stageName) {
2687
3692
  const config = getProjectConfig();
2688
3693
  const mapped = config.agent.modelMap[modelTier];
2689
- if (mapped) return mapped;
2690
- return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
3694
+ if (!mapped) {
3695
+ throw new Error(`No model configured for tier '${modelTier}'. Set agent.modelMap.${modelTier} in kody.config.json`);
3696
+ }
3697
+ return mapped;
2691
3698
  }
2692
- var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION, DEFAULT_MODEL_MAP;
3699
+ var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
2693
3700
  var init_context = __esm({
2694
3701
  "src/context.ts"() {
2695
3702
  "use strict";
@@ -2723,11 +3730,6 @@ var init_context = __esm({
2723
3730
  mid: "strong",
2724
3731
  strong: "strong"
2725
3732
  };
2726
- DEFAULT_MODEL_MAP = {
2727
- cheap: "haiku",
2728
- mid: "sonnet",
2729
- strong: "opus"
2730
- };
2731
3733
  }
2732
3734
  });
2733
3735
 
@@ -2751,8 +3753,8 @@ var init_runner_selection = __esm({
2751
3753
  });
2752
3754
 
2753
3755
  // src/stages/agent.ts
2754
- import * as fs17 from "fs";
2755
- import * as path15 from "path";
3756
+ import * as fs19 from "fs";
3757
+ import * as path17 from "path";
2756
3758
  function getSessionInfo(stageName, sessions) {
2757
3759
  const group = SESSION_GROUP[stageName];
2758
3760
  if (!group) return void 0;
@@ -2837,29 +3839,29 @@ async function executeAgentStage(ctx, def) {
2837
3839
  if (lastResult.outcome !== "completed") {
2838
3840
  return { outcome: lastResult.outcome, error: lastResult.error, retries };
2839
3841
  }
2840
- const result = lastResult;
2841
- if (def.outputFile && result.output) {
2842
- fs17.writeFileSync(path15.join(ctx.taskDir, def.outputFile), result.output);
3842
+ const result2 = lastResult;
3843
+ if (def.outputFile && result2.output) {
3844
+ fs19.writeFileSync(path17.join(ctx.taskDir, def.outputFile), result2.output);
2843
3845
  }
2844
3846
  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);
3847
+ const outputPath = path17.join(ctx.taskDir, def.outputFile);
3848
+ if (!fs19.existsSync(outputPath)) {
3849
+ const ext = path17.extname(def.outputFile);
3850
+ const base = path17.basename(def.outputFile, ext);
3851
+ const files = fs19.readdirSync(ctx.taskDir);
2850
3852
  const variant = files.find(
2851
3853
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
2852
3854
  );
2853
3855
  if (variant) {
2854
- fs17.renameSync(path15.join(ctx.taskDir, variant), outputPath);
3856
+ fs19.renameSync(path17.join(ctx.taskDir, variant), outputPath);
2855
3857
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
2856
3858
  }
2857
3859
  }
2858
3860
  }
2859
3861
  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");
3862
+ const outputPath = path17.join(ctx.taskDir, def.outputFile);
3863
+ if (fs19.existsSync(outputPath)) {
3864
+ const content = fs19.readFileSync(outputPath, "utf-8");
2863
3865
  const validation = validateStageOutput(def.name, content);
2864
3866
  if (!validation.valid) {
2865
3867
  if (def.name === "taskify") {
@@ -2873,7 +3875,7 @@ async function executeAgentStage(ctx, def) {
2873
3875
  const stripped = stripFences(retryResult.output);
2874
3876
  const retryValidation = validateTaskJson(stripped);
2875
3877
  if (retryValidation.valid) {
2876
- fs17.writeFileSync(outputPath, retryResult.output);
3878
+ fs19.writeFileSync(outputPath, retryResult.output);
2877
3879
  logger.info(` taskify retry produced valid JSON`);
2878
3880
  } else {
2879
3881
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -2886,7 +3888,7 @@ async function executeAgentStage(ctx, def) {
2886
3888
  risk_level: "low",
2887
3889
  questions: []
2888
3890
  }, null, 2);
2889
- fs17.writeFileSync(outputPath, fallback);
3891
+ fs19.writeFileSync(outputPath, fallback);
2890
3892
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
2891
3893
  }
2892
3894
  }
@@ -2896,11 +3898,11 @@ async function executeAgentStage(ctx, def) {
2896
3898
  }
2897
3899
  }
2898
3900
  }
2899
- appendStageContext(ctx.taskDir, def.name, result.output);
3901
+ appendStageContext(ctx.taskDir, def.name, result2.output);
2900
3902
  return { outcome: "completed", outputFile: def.outputFile, retries };
2901
3903
  }
2902
3904
  function appendStageContext(taskDir, stageName, output) {
2903
- const contextPath = path15.join(taskDir, "context.md");
3905
+ const contextPath = path17.join(taskDir, "context.md");
2904
3906
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
2905
3907
  let summary;
2906
3908
  if (output && output.trim()) {
@@ -2913,7 +3915,7 @@ function appendStageContext(taskDir, stageName, output) {
2913
3915
  ### ${stageName} (${timestamp2})
2914
3916
  ${summary}
2915
3917
  `;
2916
- fs17.appendFileSync(contextPath, entry);
3918
+ fs19.appendFileSync(contextPath, entry);
2917
3919
  }
2918
3920
  var SESSION_GROUP;
2919
3921
  var init_agent = __esm({
@@ -2936,7 +3938,7 @@ var init_agent = __esm({
2936
3938
  });
2937
3939
 
2938
3940
  // src/verify-runner.ts
2939
- import { execFileSync as execFileSync11 } from "child_process";
3941
+ import { execFileSync as execFileSync12 } from "child_process";
2940
3942
  function isExecError(err) {
2941
3943
  return typeof err === "object" && err !== null;
2942
3944
  }
@@ -2972,7 +3974,7 @@ function runCommand(cmd, cwd, timeout) {
2972
3974
  return { success: true, output: "", timedOut: false };
2973
3975
  }
2974
3976
  try {
2975
- const output = execFileSync11(parts[0], parts.slice(1), {
3977
+ const output = execFileSync12(parts[0], parts.slice(1), {
2976
3978
  cwd,
2977
3979
  timeout,
2978
3980
  encoding: "utf-8",
@@ -3018,19 +4020,19 @@ function runQualityGates(taskDir, projectRoot) {
3018
4020
  for (const { name, cmd } of commands) {
3019
4021
  if (!cmd) continue;
3020
4022
  logger.info(` Running ${name}: ${cmd}`);
3021
- const result = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
3022
- if (result.timedOut) {
4023
+ const result2 = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
4024
+ if (result2.timedOut) {
3023
4025
  allErrors.push(`${name}: timed out after ${VERIFY_COMMAND_TIMEOUT_MS / 1e3}s`);
3024
4026
  allPass = false;
3025
4027
  continue;
3026
4028
  }
3027
- if (!result.success) {
4029
+ if (!result2.success) {
3028
4030
  allPass = false;
3029
- const errors = parseErrors(result.output);
4031
+ const errors = parseErrors(result2.output);
3030
4032
  allErrors.push(...errors.map((e) => `[${name}] ${e}`));
3031
- rawOutputs.push({ name, output: result.output.slice(-3e3) });
4033
+ rawOutputs.push({ name, output: result2.output.slice(-3e3) });
3032
4034
  }
3033
- allSummary.push(...extractSummary(result.output, name));
4035
+ allSummary.push(...extractSummary(result2.output, name));
3034
4036
  }
3035
4037
  return { pass: allPass, errors: allErrors, summary: allSummary, rawOutputs };
3036
4038
  }
@@ -3043,7 +4045,7 @@ var init_verify_runner = __esm({
3043
4045
  });
3044
4046
 
3045
4047
  // src/observer.ts
3046
- import { execFileSync as execFileSync12 } from "child_process";
4048
+ import { execFileSync as execFileSync13 } from "child_process";
3047
4049
  async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
3048
4050
  const context = [
3049
4051
  `Stage: ${stageName}`,
@@ -3057,7 +4059,7 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
3057
4059
  ].join("\n");
3058
4060
  const prompt = DIAGNOSIS_PROMPT + context;
3059
4061
  try {
3060
- const result = await runner.run(
4062
+ const result2 = await runner.run(
3061
4063
  "diagnosis",
3062
4064
  prompt,
3063
4065
  model,
@@ -3066,8 +4068,8 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
3066
4068
  "",
3067
4069
  options
3068
4070
  );
3069
- if (result.outcome === "completed" && result.output) {
3070
- const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
4071
+ if (result2.outcome === "completed" && result2.output) {
4072
+ const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
3071
4073
  const parseResult = parseJsonSafe(cleaned, ["classification"]);
3072
4074
  if (parseResult.ok) {
3073
4075
  const { data } = parseResult;
@@ -3103,13 +4105,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
3103
4105
  }
3104
4106
  function getModifiedFiles(projectDir) {
3105
4107
  try {
3106
- const staged = execFileSync12("git", ["diff", "--name-only", "--cached"], {
4108
+ const staged = execFileSync13("git", ["diff", "--name-only", "--cached"], {
3107
4109
  encoding: "utf-8",
3108
4110
  cwd: projectDir,
3109
4111
  timeout: 5e3,
3110
4112
  stdio: ["pipe", "pipe", "pipe"]
3111
4113
  }).trim();
3112
- const unstaged = execFileSync12("git", ["diff", "--name-only"], {
4114
+ const unstaged = execFileSync13("git", ["diff", "--name-only"], {
3113
4115
  encoding: "utf-8",
3114
4116
  cwd: projectDir,
3115
4117
  timeout: 5e3,
@@ -3152,8 +4154,8 @@ Error context:
3152
4154
  });
3153
4155
 
3154
4156
  // src/stages/gate.ts
3155
- import * as fs18 from "fs";
3156
- import * as path16 from "path";
4157
+ import * as fs20 from "fs";
4158
+ import * as path18 from "path";
3157
4159
  function executeGateStage(ctx, def) {
3158
4160
  if (ctx.input.dryRun) {
3159
4161
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -3196,7 +4198,7 @@ ${output}
3196
4198
  `);
3197
4199
  }
3198
4200
  }
3199
- fs18.writeFileSync(path16.join(ctx.taskDir, "verify.md"), lines.join(""));
4201
+ fs20.writeFileSync(path18.join(ctx.taskDir, "verify.md"), lines.join(""));
3200
4202
  return {
3201
4203
  outcome: verifyResult.pass ? "completed" : "failed",
3202
4204
  retries: 0
@@ -3211,9 +4213,9 @@ var init_gate = __esm({
3211
4213
  });
3212
4214
 
3213
4215
  // src/stages/verify.ts
3214
- import * as fs19 from "fs";
3215
- import * as path17 from "path";
3216
- import { execFileSync as execFileSync13 } from "child_process";
4216
+ import * as fs21 from "fs";
4217
+ import * as path19 from "path";
4218
+ import { execFileSync as execFileSync14 } from "child_process";
3217
4219
  async function executeVerifyWithAutofix(ctx, def) {
3218
4220
  const maxAttempts = def.maxRetries ?? 2;
3219
4221
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
@@ -3223,8 +4225,8 @@ async function executeVerifyWithAutofix(ctx, def) {
3223
4225
  return { ...gateResult, retries: attempt };
3224
4226
  }
3225
4227
  if (attempt < maxAttempts) {
3226
- const verifyPath = path17.join(ctx.taskDir, "verify.md");
3227
- const errorOutput = fs19.existsSync(verifyPath) ? fs19.readFileSync(verifyPath, "utf-8") : "Unknown error";
4228
+ const verifyPath = path19.join(ctx.taskDir, "verify.md");
4229
+ const errorOutput = fs21.existsSync(verifyPath) ? fs21.readFileSync(verifyPath, "utf-8") : "Unknown error";
3228
4230
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
3229
4231
  const defaultRunner = getRunnerForStage(ctx, "taskify");
3230
4232
  const diagConfig = getProjectConfig();
@@ -3267,7 +4269,7 @@ ${diagnosis.resolution}`);
3267
4269
  const parts = parseCommand(cmd);
3268
4270
  if (parts.length === 0) return;
3269
4271
  try {
3270
- execFileSync13(parts[0], parts.slice(1), {
4272
+ execFileSync14(parts[0], parts.slice(1), {
3271
4273
  stdio: "pipe",
3272
4274
  timeout: FIX_COMMAND_TIMEOUT_MS
3273
4275
  });
@@ -3320,8 +4322,8 @@ var init_verify = __esm({
3320
4322
  });
3321
4323
 
3322
4324
  // src/review-standalone.ts
3323
- import * as fs20 from "fs";
3324
- import * as path18 from "path";
4325
+ import * as fs22 from "fs";
4326
+ import * as path20 from "path";
3325
4327
  function resolveReviewTarget(input) {
3326
4328
  if (input.prs.length === 0) {
3327
4329
  return {
@@ -3345,8 +4347,8 @@ Or comment on the specific PR: \`@kody review\``
3345
4347
  }
3346
4348
  async function runStandaloneReview(input) {
3347
4349
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
3348
- const taskDir = path18.join(input.projectDir, ".kody", "tasks", taskId);
3349
- fs20.mkdirSync(taskDir, { recursive: true });
4350
+ const taskDir = path20.join(input.projectDir, ".kody", "tasks", taskId);
4351
+ fs22.mkdirSync(taskDir, { recursive: true });
3350
4352
  let diffInstruction = "";
3351
4353
  let filesChangedSection = "";
3352
4354
  if (input.baseBranch) {
@@ -3373,7 +4375,7 @@ ${fileList}`;
3373
4375
  const taskContent = `# ${input.prTitle}
3374
4376
 
3375
4377
  ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
3376
- fs20.writeFileSync(path18.join(taskDir, "task.md"), taskContent);
4378
+ fs22.writeFileSync(path20.join(taskDir, "task.md"), taskContent);
3377
4379
  const reviewDef = STAGES.find((s) => s.name === "review");
3378
4380
  const ctx = {
3379
4381
  taskId,
@@ -3387,18 +4389,18 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
3387
4389
  }
3388
4390
  };
3389
4391
  logger.info(`[review] standalone review for: ${input.prTitle}`);
3390
- const result = await executeAgentStage(ctx, reviewDef);
3391
- if (result.outcome !== "completed") {
4392
+ const result2 = await executeAgentStage(ctx, reviewDef);
4393
+ if (result2.outcome !== "completed") {
3392
4394
  return {
3393
4395
  outcome: "failed",
3394
4396
  taskDir,
3395
- error: result.error ?? "Review stage failed"
4397
+ error: result2.error ?? "Review stage failed"
3396
4398
  };
3397
4399
  }
3398
- const reviewPath = path18.join(taskDir, "review.md");
4400
+ const reviewPath = path20.join(taskDir, "review.md");
3399
4401
  let reviewContent;
3400
- if (fs20.existsSync(reviewPath)) {
3401
- reviewContent = fs20.readFileSync(reviewPath, "utf-8");
4402
+ if (fs22.existsSync(reviewPath)) {
4403
+ reviewContent = fs22.readFileSync(reviewPath, "utf-8");
3402
4404
  }
3403
4405
  return {
3404
4406
  outcome: "completed",
@@ -3438,8 +4440,8 @@ var init_review_standalone = __esm({
3438
4440
  });
3439
4441
 
3440
4442
  // src/stages/review.ts
3441
- import * as fs21 from "fs";
3442
- import * as path19 from "path";
4443
+ import * as fs23 from "fs";
4444
+ import * as path21 from "path";
3443
4445
  async function executeReviewWithFix(ctx, def) {
3444
4446
  if (ctx.input.dryRun) {
3445
4447
  return { outcome: "completed", retries: 0 };
@@ -3453,11 +4455,11 @@ async function executeReviewWithFix(ctx, def) {
3453
4455
  if (reviewResult.outcome !== "completed") {
3454
4456
  return reviewResult;
3455
4457
  }
3456
- const reviewFile = path19.join(ctx.taskDir, "review.md");
3457
- if (!fs21.existsSync(reviewFile)) {
4458
+ const reviewFile = path21.join(ctx.taskDir, "review.md");
4459
+ if (!fs23.existsSync(reviewFile)) {
3458
4460
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
3459
4461
  }
3460
- const content = fs21.readFileSync(reviewFile, "utf-8");
4462
+ const content = fs23.readFileSync(reviewFile, "utf-8");
3461
4463
  if (detectReviewVerdict(content) !== "fail") {
3462
4464
  return { ...reviewResult, retries: iteration };
3463
4465
  }
@@ -3486,15 +4488,15 @@ var init_review = __esm({
3486
4488
  });
3487
4489
 
3488
4490
  // src/stages/ship.ts
3489
- import * as fs22 from "fs";
3490
- import * as path20 from "path";
3491
- import { execFileSync as execFileSync14 } from "child_process";
4491
+ import * as fs24 from "fs";
4492
+ import * as path22 from "path";
4493
+ import { execFileSync as execFileSync15 } from "child_process";
3492
4494
  function buildPrBody(ctx) {
3493
4495
  const sections = [];
3494
- const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3495
- if (fs22.existsSync(taskJsonPath)) {
4496
+ const taskJsonPath = path22.join(ctx.taskDir, "task.json");
4497
+ if (fs24.existsSync(taskJsonPath)) {
3496
4498
  try {
3497
- const raw = fs22.readFileSync(taskJsonPath, "utf-8");
4499
+ const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3498
4500
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3499
4501
  const task = JSON.parse(cleaned);
3500
4502
  if (task.description) {
@@ -3513,9 +4515,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
3513
4515
  } catch {
3514
4516
  }
3515
4517
  }
3516
- const reviewPath = path20.join(ctx.taskDir, "review.md");
3517
- if (fs22.existsSync(reviewPath)) {
3518
- const review = fs22.readFileSync(reviewPath, "utf-8");
4518
+ const reviewPath = path22.join(ctx.taskDir, "review.md");
4519
+ if (fs24.existsSync(reviewPath)) {
4520
+ const review = fs24.readFileSync(reviewPath, "utf-8");
3519
4521
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
3520
4522
  if (summaryMatch) {
3521
4523
  const summary = summaryMatch[1].trim();
@@ -3532,14 +4534,14 @@ ${summary}`);
3532
4534
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
3533
4535
  }
3534
4536
  }
3535
- const verifyPath = path20.join(ctx.taskDir, "verify.md");
3536
- if (fs22.existsSync(verifyPath)) {
3537
- const verify = fs22.readFileSync(verifyPath, "utf-8");
4537
+ const verifyPath = path22.join(ctx.taskDir, "verify.md");
4538
+ if (fs24.existsSync(verifyPath)) {
4539
+ const verify = fs24.readFileSync(verifyPath, "utf-8");
3538
4540
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
3539
4541
  }
3540
- const planPath = path20.join(ctx.taskDir, "plan.md");
3541
- if (fs22.existsSync(planPath)) {
3542
- const plan = fs22.readFileSync(planPath, "utf-8").trim();
4542
+ const planPath = path22.join(ctx.taskDir, "plan.md");
4543
+ if (fs24.existsSync(planPath)) {
4544
+ const plan = fs24.readFileSync(planPath, "utf-8").trim();
3543
4545
  if (plan) {
3544
4546
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
3545
4547
  sections.push(`
@@ -3559,25 +4561,25 @@ Closes #${ctx.input.issueNumber}`);
3559
4561
  return sections.join("\n");
3560
4562
  }
3561
4563
  function executeShipStage(ctx, _def) {
3562
- const shipPath = path20.join(ctx.taskDir, "ship.md");
4564
+ const shipPath = path22.join(ctx.taskDir, "ship.md");
3563
4565
  if (ctx.input.dryRun) {
3564
- fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
4566
+ fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
3565
4567
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3566
4568
  }
3567
4569
  if (ctx.input.local && !ctx.input.issueNumber) {
3568
- fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
4570
+ fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
3569
4571
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3570
4572
  }
3571
4573
  try {
3572
4574
  const head = getCurrentBranch(ctx.projectDir);
3573
4575
  const base = getDefaultBranch(ctx.projectDir);
3574
4576
  try {
3575
- execFileSync14("git", ["add", ctx.taskDir], {
4577
+ execFileSync15("git", ["add", ctx.taskDir], {
3576
4578
  cwd: ctx.projectDir,
3577
4579
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
3578
4580
  stdio: "pipe"
3579
4581
  });
3580
- execFileSync14("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
4582
+ execFileSync15("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
3581
4583
  cwd: ctx.projectDir,
3582
4584
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
3583
4585
  stdio: "pipe"
@@ -3591,7 +4593,7 @@ function executeShipStage(ctx, _def) {
3591
4593
  let repo = config.github?.repo;
3592
4594
  if (!owner || !repo) {
3593
4595
  try {
3594
- const remoteUrl = execFileSync14("git", ["remote", "get-url", "origin"], {
4596
+ const remoteUrl = execFileSync15("git", ["remote", "get-url", "origin"], {
3595
4597
  encoding: "utf-8",
3596
4598
  cwd: ctx.projectDir
3597
4599
  }).trim();
@@ -3612,28 +4614,28 @@ function executeShipStage(ctx, _def) {
3612
4614
  chore: "chore"
3613
4615
  };
3614
4616
  let prefix = "chore";
3615
- const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3616
- if (fs22.existsSync(taskJsonPath)) {
4617
+ const taskJsonPath = path22.join(ctx.taskDir, "task.json");
4618
+ if (fs24.existsSync(taskJsonPath)) {
3617
4619
  try {
3618
- const raw = fs22.readFileSync(taskJsonPath, "utf-8");
4620
+ const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3619
4621
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3620
4622
  const task = JSON.parse(cleaned);
3621
4623
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
3622
4624
  } catch {
3623
4625
  }
3624
4626
  }
3625
- const taskMdPath = path20.join(ctx.taskDir, "task.md");
3626
- if (fs22.existsSync(taskMdPath)) {
3627
- const content = fs22.readFileSync(taskMdPath, "utf-8");
4627
+ const taskMdPath = path22.join(ctx.taskDir, "task.md");
4628
+ if (fs24.existsSync(taskMdPath)) {
4629
+ const content = fs24.readFileSync(taskMdPath, "utf-8");
3628
4630
  const heading = content.split("\n").find((l) => l.startsWith("# "));
3629
4631
  if (heading) {
3630
4632
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
3631
4633
  }
3632
4634
  }
3633
4635
  if (title === "Update") {
3634
- if (fs22.existsSync(taskJsonPath)) {
4636
+ if (fs24.existsSync(taskJsonPath)) {
3635
4637
  try {
3636
- const raw = fs22.readFileSync(taskJsonPath, "utf-8");
4638
+ const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3637
4639
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3638
4640
  const task = JSON.parse(cleaned);
3639
4641
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -3656,7 +4658,7 @@ function executeShipStage(ctx, _def) {
3656
4658
  } catch {
3657
4659
  }
3658
4660
  }
3659
- fs22.writeFileSync(shipPath, `# Ship
4661
+ fs24.writeFileSync(shipPath, `# Ship
3660
4662
 
3661
4663
  Updated existing PR: ${existingPr.url}
3662
4664
  PR #${existingPr.number}
@@ -3677,20 +4679,20 @@ PR #${existingPr.number}
3677
4679
  } catch {
3678
4680
  }
3679
4681
  }
3680
- fs22.writeFileSync(shipPath, `# Ship
4682
+ fs24.writeFileSync(shipPath, `# Ship
3681
4683
 
3682
4684
  PR created: ${pr.url}
3683
4685
  PR #${pr.number}
3684
4686
  `);
3685
4687
  } else {
3686
- fs22.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
4688
+ fs24.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3687
4689
  }
3688
4690
  }
3689
4691
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
3690
4692
  } catch (err) {
3691
4693
  const msg = err instanceof Error ? err.message : String(err);
3692
4694
  try {
3693
- fs22.writeFileSync(shipPath, `# Ship
4695
+ fs24.writeFileSync(shipPath, `# Ship
3694
4696
 
3695
4697
  Failed: ${msg}
3696
4698
  `);
@@ -3739,15 +4741,15 @@ var init_executor_registry = __esm({
3739
4741
  });
3740
4742
 
3741
4743
  // src/pipeline/questions.ts
3742
- import * as fs23 from "fs";
3743
- import * as path21 from "path";
4744
+ import * as fs25 from "fs";
4745
+ import * as path23 from "path";
3744
4746
  function checkForQuestions(ctx, stageName) {
3745
4747
  if (ctx.input.local || !ctx.input.issueNumber) return false;
3746
4748
  try {
3747
4749
  if (stageName === "taskify") {
3748
- const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3749
- if (!fs23.existsSync(taskJsonPath)) return false;
3750
- const raw = fs23.readFileSync(taskJsonPath, "utf-8");
4750
+ const taskJsonPath = path23.join(ctx.taskDir, "task.json");
4751
+ if (!fs25.existsSync(taskJsonPath)) return false;
4752
+ const raw = fs25.readFileSync(taskJsonPath, "utf-8");
3751
4753
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3752
4754
  const taskJson = JSON.parse(cleaned);
3753
4755
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -3762,9 +4764,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
3762
4764
  }
3763
4765
  }
3764
4766
  if (stageName === "plan") {
3765
- const planPath = path21.join(ctx.taskDir, "plan.md");
3766
- if (!fs23.existsSync(planPath)) return false;
3767
- const plan = fs23.readFileSync(planPath, "utf-8");
4767
+ const planPath = path23.join(ctx.taskDir, "plan.md");
4768
+ if (!fs25.existsSync(planPath)) return false;
4769
+ const plan = fs25.readFileSync(planPath, "utf-8");
3768
4770
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
3769
4771
  if (questionsMatch) {
3770
4772
  const questionsText = questionsMatch[1].trim();
@@ -3793,8 +4795,8 @@ var init_questions = __esm({
3793
4795
  });
3794
4796
 
3795
4797
  // src/pipeline/hooks.ts
3796
- import * as fs24 from "fs";
3797
- import * as path22 from "path";
4798
+ import * as fs26 from "fs";
4799
+ import * as path24 from "path";
3798
4800
  function applyPreStageLabel(ctx, def) {
3799
4801
  if (!ctx.input.issueNumber || ctx.input.local) return;
3800
4802
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
@@ -3832,9 +4834,9 @@ function autoDetectComplexity(ctx, def) {
3832
4834
  return { complexity, activeStages };
3833
4835
  }
3834
4836
  try {
3835
- const taskJsonPath = path22.join(ctx.taskDir, "task.json");
3836
- if (!fs24.existsSync(taskJsonPath)) return null;
3837
- const raw = fs24.readFileSync(taskJsonPath, "utf-8");
4837
+ const taskJsonPath = path24.join(ctx.taskDir, "task.json");
4838
+ if (!fs26.existsSync(taskJsonPath)) return null;
4839
+ const raw = fs26.readFileSync(taskJsonPath, "utf-8");
3838
4840
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3839
4841
  const taskJson = JSON.parse(cleaned);
3840
4842
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -3864,8 +4866,8 @@ function checkRiskGate(ctx, def, state, complexity) {
3864
4866
  if (ctx.input.dryRun || ctx.input.local) return null;
3865
4867
  if (ctx.input.mode === "rerun") return null;
3866
4868
  if (!ctx.input.issueNumber) return null;
3867
- const planPath = path22.join(ctx.taskDir, "plan.md");
3868
- const plan = fs24.existsSync(planPath) ? fs24.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
4869
+ const planPath = path24.join(ctx.taskDir, "plan.md");
4870
+ const plan = fs26.existsSync(planPath) ? fs26.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
3869
4871
  try {
3870
4872
  postComment(
3871
4873
  ctx.input.issueNumber,
@@ -3932,22 +4934,22 @@ var init_hooks = __esm({
3932
4934
  });
3933
4935
 
3934
4936
  // src/learning/auto-learn.ts
3935
- import * as fs25 from "fs";
3936
- import * as path23 from "path";
4937
+ import * as fs27 from "fs";
4938
+ import * as path25 from "path";
3937
4939
  function stripAnsi(str) {
3938
4940
  return str.replace(/\x1b\[[0-9;]*m/g, "");
3939
4941
  }
3940
4942
  function autoLearn(ctx) {
3941
4943
  try {
3942
- const memoryDir = path23.join(ctx.projectDir, ".kody", "memory");
3943
- if (!fs25.existsSync(memoryDir)) {
3944
- fs25.mkdirSync(memoryDir, { recursive: true });
4944
+ const memoryDir = path25.join(ctx.projectDir, ".kody", "memory");
4945
+ if (!fs27.existsSync(memoryDir)) {
4946
+ fs27.mkdirSync(memoryDir, { recursive: true });
3945
4947
  }
3946
4948
  const learnings = [];
3947
4949
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3948
- const verifyPath = path23.join(ctx.taskDir, "verify.md");
3949
- if (fs25.existsSync(verifyPath)) {
3950
- const verify = stripAnsi(fs25.readFileSync(verifyPath, "utf-8"));
4950
+ const verifyPath = path25.join(ctx.taskDir, "verify.md");
4951
+ if (fs27.existsSync(verifyPath)) {
4952
+ const verify = stripAnsi(fs27.readFileSync(verifyPath, "utf-8"));
3951
4953
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
3952
4954
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
3953
4955
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -3956,18 +4958,18 @@ function autoLearn(ctx) {
3956
4958
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
3957
4959
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
3958
4960
  }
3959
- const reviewPath = path23.join(ctx.taskDir, "review.md");
3960
- if (fs25.existsSync(reviewPath)) {
3961
- const review = fs25.readFileSync(reviewPath, "utf-8");
4961
+ const reviewPath = path25.join(ctx.taskDir, "review.md");
4962
+ if (fs27.existsSync(reviewPath)) {
4963
+ const review = fs27.readFileSync(reviewPath, "utf-8");
3962
4964
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
3963
4965
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
3964
4966
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
3965
4967
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
3966
4968
  }
3967
- const taskJsonPath = path23.join(ctx.taskDir, "task.json");
3968
- if (fs25.existsSync(taskJsonPath)) {
4969
+ const taskJsonPath = path25.join(ctx.taskDir, "task.json");
4970
+ if (fs27.existsSync(taskJsonPath)) {
3969
4971
  try {
3970
- const raw = stripAnsi(fs25.readFileSync(taskJsonPath, "utf-8"));
4972
+ const raw = stripAnsi(fs27.readFileSync(taskJsonPath, "utf-8"));
3971
4973
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
3972
4974
  const task = JSON.parse(cleaned);
3973
4975
  if (task.scope && Array.isArray(task.scope)) {
@@ -3978,12 +4980,12 @@ function autoLearn(ctx) {
3978
4980
  }
3979
4981
  }
3980
4982
  if (learnings.length > 0) {
3981
- const conventionsPath = path23.join(memoryDir, "conventions.md");
4983
+ const conventionsPath = path25.join(memoryDir, "conventions.md");
3982
4984
  const entry = `
3983
4985
  ## Learned ${timestamp2} (task: ${ctx.taskId})
3984
4986
  ${learnings.join("\n")}
3985
4987
  `;
3986
- fs25.appendFileSync(conventionsPath, entry);
4988
+ fs27.appendFileSync(conventionsPath, entry);
3987
4989
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
3988
4990
  }
3989
4991
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
@@ -3991,8 +4993,8 @@ ${learnings.join("\n")}
3991
4993
  }
3992
4994
  }
3993
4995
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3994
- const archPath = path23.join(memoryDir, "architecture.md");
3995
- if (fs25.existsSync(archPath)) return;
4996
+ const archPath = path25.join(memoryDir, "architecture.md");
4997
+ if (fs27.existsSync(archPath)) return;
3996
4998
  const detected = detectArchitectureBasic(projectDir);
3997
4999
  if (detected.length > 0) {
3998
5000
  const content = `# Architecture (auto-detected ${timestamp2})
@@ -4000,7 +5002,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
4000
5002
  ## Overview
4001
5003
  ${detected.join("\n")}
4002
5004
  `;
4003
- fs25.writeFileSync(archPath, content);
5005
+ fs27.writeFileSync(archPath, content);
4004
5006
  logger.info(`Auto-detected architecture (${detected.length} items)`);
4005
5007
  }
4006
5008
  }
@@ -4013,13 +5015,13 @@ var init_auto_learn = __esm({
4013
5015
  });
4014
5016
 
4015
5017
  // src/retrospective.ts
4016
- import * as fs26 from "fs";
4017
- import * as path24 from "path";
5018
+ import * as fs28 from "fs";
5019
+ import * as path26 from "path";
4018
5020
  function readArtifact(taskDir, filename, maxChars) {
4019
- const p = path24.join(taskDir, filename);
4020
- if (!fs26.existsSync(p)) return null;
5021
+ const p = path26.join(taskDir, filename);
5022
+ if (!fs28.existsSync(p)) return null;
4021
5023
  try {
4022
- const content = fs26.readFileSync(p, "utf-8");
5024
+ const content = fs28.readFileSync(p, "utf-8");
4023
5025
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
4024
5026
  } catch {
4025
5027
  return null;
@@ -4072,13 +5074,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
4072
5074
  return lines.join("\n");
4073
5075
  }
4074
5076
  function getLogPath(projectDir) {
4075
- return path24.join(projectDir, ".kody", "memory", "observer-log.jsonl");
5077
+ return path26.join(projectDir, ".kody", "memory", "observer-log.jsonl");
4076
5078
  }
4077
5079
  function readPreviousRetrospectives(projectDir, limit = 10) {
4078
5080
  const logPath = getLogPath(projectDir);
4079
- if (!fs26.existsSync(logPath)) return [];
5081
+ if (!fs28.existsSync(logPath)) return [];
4080
5082
  try {
4081
- const content = fs26.readFileSync(logPath, "utf-8");
5083
+ const content = fs28.readFileSync(logPath, "utf-8");
4082
5084
  const lines = content.split("\n").filter(Boolean);
4083
5085
  const entries = [];
4084
5086
  const start = Math.max(0, lines.length - limit);
@@ -4105,11 +5107,11 @@ function formatPreviousEntries(entries) {
4105
5107
  }
4106
5108
  function appendRetrospectiveEntry(projectDir, entry) {
4107
5109
  const logPath = getLogPath(projectDir);
4108
- const dir = path24.dirname(logPath);
4109
- if (!fs26.existsSync(dir)) {
4110
- fs26.mkdirSync(dir, { recursive: true });
5110
+ const dir = path26.dirname(logPath);
5111
+ if (!fs28.existsSync(dir)) {
5112
+ fs28.mkdirSync(dir, { recursive: true });
4111
5113
  }
4112
- fs26.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5114
+ fs28.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4113
5115
  }
4114
5116
  async function runRetrospective(ctx, state, pipelineStartTime) {
4115
5117
  if (ctx.input.dryRun) return;
@@ -4131,7 +5133,7 @@ ${previousText}
4131
5133
  if (needsLitellmProxy(config)) {
4132
5134
  extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
4133
5135
  }
4134
- const result = await runner.run("retrospective", prompt, model, 3e4, "", {
5136
+ const result2 = await runner.run("retrospective", prompt, model, 3e4, "", {
4135
5137
  cwd: ctx.projectDir,
4136
5138
  env: extraEnv
4137
5139
  });
@@ -4139,8 +5141,8 @@ ${previousText}
4139
5141
  let patternMatch = null;
4140
5142
  let suggestion = "No suggestion";
4141
5143
  let pipelineFlaw = null;
4142
- if (result.outcome === "completed" && result.output) {
4143
- const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
5144
+ if (result2.outcome === "completed" && result2.output) {
5145
+ const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
4144
5146
  try {
4145
5147
  const parsed = JSON.parse(cleaned);
4146
5148
  observation = parsed.observation ?? observation;
@@ -4277,8 +5279,8 @@ var init_summary = __esm({
4277
5279
  });
4278
5280
 
4279
5281
  // src/pipeline.ts
4280
- import * as fs27 from "fs";
4281
- import * as path25 from "path";
5282
+ import * as fs29 from "fs";
5283
+ import * as path27 from "path";
4282
5284
  function ensureFeatureBranchIfNeeded(ctx) {
4283
5285
  if (ctx.input.dryRun) return;
4284
5286
  if (ctx.input.prNumber) {
@@ -4291,8 +5293,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
4291
5293
  }
4292
5294
  if (!ctx.input.issueNumber) return;
4293
5295
  try {
4294
- const taskMdPath = path25.join(ctx.taskDir, "task.md");
4295
- const title = fs27.existsSync(taskMdPath) ? fs27.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
5296
+ const taskMdPath = path27.join(ctx.taskDir, "task.md");
5297
+ const title = fs29.existsSync(taskMdPath) ? fs29.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
4296
5298
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
4297
5299
  syncWithDefault(ctx.projectDir);
4298
5300
  } catch (err) {
@@ -4306,10 +5308,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
4306
5308
  }
4307
5309
  }
4308
5310
  function acquireLock(taskDir) {
4309
- const lockPath = path25.join(taskDir, ".lock");
4310
- if (fs27.existsSync(lockPath)) {
5311
+ const lockPath = path27.join(taskDir, ".lock");
5312
+ if (fs29.existsSync(lockPath)) {
4311
5313
  try {
4312
- const pid = parseInt(fs27.readFileSync(lockPath, "utf-8").trim(), 10);
5314
+ const pid = parseInt(fs29.readFileSync(lockPath, "utf-8").trim(), 10);
4313
5315
  if (!isNaN(pid)) {
4314
5316
  try {
4315
5317
  process.kill(pid, 0);
@@ -4326,14 +5328,14 @@ function acquireLock(taskDir) {
4326
5328
  logger.warn(` Corrupt lock file \u2014 overwriting`);
4327
5329
  }
4328
5330
  try {
4329
- fs27.unlinkSync(lockPath);
5331
+ fs29.unlinkSync(lockPath);
4330
5332
  } catch {
4331
5333
  }
4332
5334
  }
4333
5335
  try {
4334
- const fd = fs27.openSync(lockPath, fs27.constants.O_WRONLY | fs27.constants.O_CREAT | fs27.constants.O_EXCL);
4335
- fs27.writeSync(fd, String(process.pid));
4336
- fs27.closeSync(fd);
5336
+ const fd = fs29.openSync(lockPath, fs29.constants.O_WRONLY | fs29.constants.O_CREAT | fs29.constants.O_EXCL);
5337
+ fs29.writeSync(fd, String(process.pid));
5338
+ fs29.closeSync(fd);
4337
5339
  } catch (err) {
4338
5340
  if (err.code === "EEXIST") {
4339
5341
  throw new Error("Pipeline already running (lock acquired by another process)");
@@ -4343,7 +5345,7 @@ function acquireLock(taskDir) {
4343
5345
  }
4344
5346
  function releaseLock(taskDir) {
4345
5347
  try {
4346
- fs27.unlinkSync(path25.join(taskDir, ".lock"));
5348
+ fs29.unlinkSync(path27.join(taskDir, ".lock"));
4347
5349
  } catch {
4348
5350
  }
4349
5351
  }
@@ -4432,23 +5434,23 @@ async function runPipelineInner(ctx) {
4432
5434
  writeState(state, ctx.taskDir);
4433
5435
  logger.info(`[${def.name}] starting...`);
4434
5436
  applyPreStageLabel(ctx, def);
4435
- let result;
5437
+ let result2;
4436
5438
  try {
4437
- result = await getExecutor(def.name)(ctx, def);
5439
+ result2 = await getExecutor(def.name)(ctx, def);
4438
5440
  } catch (error) {
4439
- result = {
5441
+ result2 = {
4440
5442
  outcome: "failed",
4441
5443
  retries: 0,
4442
5444
  error: error instanceof Error ? error.message : String(error)
4443
5445
  };
4444
5446
  }
4445
5447
  ciGroupEnd();
4446
- if (result.outcome === "completed") {
5448
+ if (result2.outcome === "completed") {
4447
5449
  state.stages[def.name] = {
4448
5450
  state: "completed",
4449
5451
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
4450
- retries: result.retries,
4451
- outputFile: result.outputFile
5452
+ retries: result2.retries,
5453
+ outputFile: result2.outputFile
4452
5454
  };
4453
5455
  logger.info(`[${def.name}] \u2713 completed`);
4454
5456
  const detected = autoDetectComplexity(ctx, def);
@@ -4462,16 +5464,16 @@ async function runPipelineInner(ctx) {
4462
5464
  if (gated) return gated;
4463
5465
  commitAfterStage(ctx, def);
4464
5466
  } else {
4465
- const isTimeout = result.outcome === "timed_out";
5467
+ const isTimeout = result2.outcome === "timed_out";
4466
5468
  state.stages[def.name] = {
4467
5469
  state: isTimeout ? "timeout" : "failed",
4468
- retries: result.retries,
4469
- error: isTimeout ? "Stage timed out" : result.error ?? "Stage failed"
5470
+ retries: result2.retries,
5471
+ error: isTimeout ? "Stage timed out" : result2.error ?? "Stage failed"
4470
5472
  };
4471
5473
  state.state = "failed";
4472
5474
  state.sessions = ctx.sessions;
4473
5475
  writeState(state, ctx.taskDir);
4474
- logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result.error}`}`);
5476
+ logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result2.error}`}`);
4475
5477
  if (ctx.input.issueNumber && !ctx.input.local) {
4476
5478
  setLifecycleLabel(ctx.input.issueNumber, "failed");
4477
5479
  }
@@ -4551,8 +5553,8 @@ var init_pipeline = __esm({
4551
5553
  });
4552
5554
 
4553
5555
  // src/preflight.ts
4554
- import { execFileSync as execFileSync15 } from "child_process";
4555
- import * as fs28 from "fs";
5556
+ import { execFileSync as execFileSync16 } from "child_process";
5557
+ import * as fs30 from "fs";
4556
5558
  function check(name, fn) {
4557
5559
  try {
4558
5560
  const detail = fn() ?? void 0;
@@ -4564,7 +5566,7 @@ function check(name, fn) {
4564
5566
  function runPreflight() {
4565
5567
  const checks = [
4566
5568
  check("claude CLI", () => {
4567
- const v = execFileSync15("claude", ["--version"], {
5569
+ const v = execFileSync16("claude", ["--version"], {
4568
5570
  encoding: "utf-8",
4569
5571
  timeout: 1e4,
4570
5572
  stdio: ["pipe", "pipe", "pipe"]
@@ -4572,14 +5574,14 @@ function runPreflight() {
4572
5574
  return v;
4573
5575
  }),
4574
5576
  check("git repo", () => {
4575
- execFileSync15("git", ["rev-parse", "--is-inside-work-tree"], {
5577
+ execFileSync16("git", ["rev-parse", "--is-inside-work-tree"], {
4576
5578
  encoding: "utf-8",
4577
5579
  timeout: 5e3,
4578
5580
  stdio: ["pipe", "pipe", "pipe"]
4579
5581
  });
4580
5582
  }),
4581
5583
  check("pnpm", () => {
4582
- const v = execFileSync15("pnpm", ["--version"], {
5584
+ const v = execFileSync16("pnpm", ["--version"], {
4583
5585
  encoding: "utf-8",
4584
5586
  timeout: 5e3,
4585
5587
  stdio: ["pipe", "pipe", "pipe"]
@@ -4587,7 +5589,7 @@ function runPreflight() {
4587
5589
  return v;
4588
5590
  }),
4589
5591
  check("node >= 18", () => {
4590
- const v = execFileSync15("node", ["--version"], {
5592
+ const v = execFileSync16("node", ["--version"], {
4591
5593
  encoding: "utf-8",
4592
5594
  timeout: 5e3,
4593
5595
  stdio: ["pipe", "pipe", "pipe"]
@@ -4597,7 +5599,7 @@ function runPreflight() {
4597
5599
  return v;
4598
5600
  }),
4599
5601
  check("gh CLI", () => {
4600
- const v = execFileSync15("gh", ["--version"], {
5602
+ const v = execFileSync16("gh", ["--version"], {
4601
5603
  encoding: "utf-8",
4602
5604
  timeout: 5e3,
4603
5605
  stdio: ["pipe", "pipe", "pipe"]
@@ -4605,7 +5607,7 @@ function runPreflight() {
4605
5607
  return v;
4606
5608
  }),
4607
5609
  check("package.json", () => {
4608
- if (!fs28.existsSync("package.json")) throw new Error("not found");
5610
+ if (!fs30.existsSync("package.json")) throw new Error("not found");
4609
5611
  })
4610
5612
  ];
4611
5613
  const failed = checks.filter((c) => !c.ok);
@@ -4682,8 +5684,8 @@ var init_args = __esm({
4682
5684
  });
4683
5685
 
4684
5686
  // src/cli/task-state.ts
4685
- import * as fs29 from "fs";
4686
- import * as path26 from "path";
5687
+ import * as fs31 from "fs";
5688
+ import * as path28 from "path";
4687
5689
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
4688
5690
  if (!existingTaskId || !existingState) {
4689
5691
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -4715,11 +5717,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
4715
5717
  function resolveForIssue(issueNumber, projectDir) {
4716
5718
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
4717
5719
  if (existingTaskId) {
4718
- const statusPath = path26.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
5720
+ const statusPath = path28.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
4719
5721
  let existingState = null;
4720
- if (fs29.existsSync(statusPath)) {
5722
+ if (fs31.existsSync(statusPath)) {
4721
5723
  try {
4722
- existingState = JSON.parse(fs29.readFileSync(statusPath, "utf-8"));
5724
+ existingState = JSON.parse(fs31.readFileSync(statusPath, "utf-8"));
4723
5725
  } catch {
4724
5726
  }
4725
5727
  }
@@ -4752,12 +5754,12 @@ var resolve_exports = {};
4752
5754
  __export(resolve_exports, {
4753
5755
  runResolve: () => runResolve
4754
5756
  });
4755
- import { execFileSync as execFileSync16 } from "child_process";
5757
+ import { execFileSync as execFileSync17 } from "child_process";
4756
5758
  function getConflictContext(cwd, files) {
4757
5759
  const parts = [];
4758
5760
  for (const file of files.slice(0, 10)) {
4759
5761
  try {
4760
- const content = execFileSync16("git", ["diff", file], {
5762
+ const content = execFileSync17("git", ["diff", file], {
4761
5763
  cwd,
4762
5764
  encoding: "utf-8",
4763
5765
  stdio: ["pipe", "pipe", "pipe"]
@@ -4807,12 +5809,12 @@ async function runResolve(options) {
4807
5809
  extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
4808
5810
  }
4809
5811
  logger.info(` Running agent to resolve conflicts (model=${model})...`);
4810
- const result = await runner.run("resolve", prompt, model, 3e5, projectDir, {
5812
+ const result2 = await runner.run("resolve", prompt, model, 3e5, projectDir, {
4811
5813
  cwd: projectDir,
4812
5814
  env: extraEnv
4813
5815
  });
4814
- if (result.outcome !== "completed") {
4815
- return { outcome: "failed", error: `Agent failed: ${result.error}` };
5816
+ if (result2.outcome !== "completed") {
5817
+ return { outcome: "failed", error: `Agent failed: ${result2.error}` };
4816
5818
  }
4817
5819
  logger.info(" Verifying resolution...");
4818
5820
  const verify = runQualityGates(projectDir, projectDir);
@@ -4876,8 +5878,8 @@ var init_resolve = __esm({
4876
5878
 
4877
5879
  // src/entry.ts
4878
5880
  var entry_exports = {};
4879
- import * as fs30 from "fs";
4880
- import * as path27 from "path";
5881
+ import * as fs32 from "fs";
5882
+ import * as path29 from "path";
4881
5883
  async function ensureLitellmProxy(config, projectDir) {
4882
5884
  if (!anyStageNeedsProxy(config)) return null;
4883
5885
  const litellmUrl = getLitellmUrl();
@@ -4912,7 +5914,7 @@ async function ensureLitellmProxy(config, projectDir) {
4912
5914
  return litellmProcess;
4913
5915
  }
4914
5916
  async function runModelHealthCheck(config) {
4915
- const usesProxy = needsLitellmProxy(config);
5917
+ const usesProxy = anyStageNeedsProxy(config);
4916
5918
  const baseUrl = usesProxy ? getLitellmUrl() : "https://api.anthropic.com";
4917
5919
  const apiKey = usesProxy ? process.env.ANTHROPIC_COMPATIBLE_API_KEY : process.env.ANTHROPIC_API_KEY;
4918
5920
  if (!apiKey) {
@@ -4922,19 +5924,19 @@ async function runModelHealthCheck(config) {
4922
5924
  }
4923
5925
  const model = config.agent.modelMap.cheap;
4924
5926
  logger.info(`Model health check (${model} via ${usesProxy ? "LiteLLM" : "Anthropic"})...`);
4925
- const result = await checkModelHealth(baseUrl, apiKey, model);
4926
- if (result.ok) {
5927
+ const result2 = await checkModelHealth(baseUrl, apiKey, model);
5928
+ if (result2.ok) {
4927
5929
  logger.info(" \u2713 Model responded");
4928
5930
  } else {
4929
- logger.error(` \u2717 Model health check failed: ${result.error}`);
5931
+ logger.error(` \u2717 Model health check failed: ${result2.error}`);
4930
5932
  process.exit(1);
4931
5933
  }
4932
5934
  }
4933
5935
  async function main() {
4934
5936
  const input = parseArgs();
4935
- const projectDir = input.cwd ? path27.resolve(input.cwd) : process.cwd();
5937
+ const projectDir = input.cwd ? path29.resolve(input.cwd) : process.cwd();
4936
5938
  if (input.cwd) {
4937
- if (!fs30.existsSync(projectDir)) {
5939
+ if (!fs32.existsSync(projectDir)) {
4938
5940
  console.error(`--cwd path does not exist: ${projectDir}`);
4939
5941
  process.exit(1);
4940
5942
  }
@@ -5000,8 +6002,8 @@ async function main() {
5000
6002
  process.exit(1);
5001
6003
  }
5002
6004
  }
5003
- const taskDir = path27.join(projectDir, ".kody", "tasks", taskId);
5004
- fs30.mkdirSync(taskDir, { recursive: true });
6005
+ const taskDir = path29.join(projectDir, ".kody", "tasks", taskId);
6006
+ fs32.mkdirSync(taskDir, { recursive: true });
5005
6007
  if (input.command === "rerun" && isTaskifyRun(taskDir)) {
5006
6008
  const marker = readTaskifyMarker(taskDir);
5007
6009
  if (marker) {
@@ -5066,7 +6068,7 @@ async function main() {
5066
6068
  console.error(`Runner "${defaultRunnerName2}" health check failed`);
5067
6069
  process.exit(1);
5068
6070
  }
5069
- const result = await runStandaloneReview({
6071
+ const result2 = await runStandaloneReview({
5070
6072
  projectDir,
5071
6073
  runners: runners2,
5072
6074
  prTitle,
@@ -5076,15 +6078,15 @@ async function main() {
5076
6078
  taskId
5077
6079
  });
5078
6080
  if (litellmProcess2) litellmProcess2.kill();
5079
- if (result.outcome === "failed") {
5080
- console.error(`Review failed: ${result.error}`);
6081
+ if (result2.outcome === "failed") {
6082
+ console.error(`Review failed: ${result2.error}`);
5081
6083
  process.exit(1);
5082
6084
  }
5083
- if (result.reviewContent) {
5084
- console.log(result.reviewContent);
6085
+ if (result2.reviewContent) {
6086
+ console.log(result2.reviewContent);
5085
6087
  if (!input.local && prNumber) {
5086
- const comment = formatReviewComment(result.reviewContent, taskId);
5087
- const verdict = detectReviewVerdict(result.reviewContent);
6088
+ const comment = formatReviewComment(result2.reviewContent, taskId);
6089
+ const verdict = detectReviewVerdict(result2.reviewContent);
5088
6090
  const event = verdict === "fail" ? "request-changes" : "approve";
5089
6091
  const posted = submitPRReview(prNumber, comment, event);
5090
6092
  if (!posted) {
@@ -5116,48 +6118,48 @@ async function main() {
5116
6118
  process.exit(1);
5117
6119
  }
5118
6120
  const { runResolve: runResolve2 } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
5119
- const result = await runResolve2({
6121
+ const result2 = await runResolve2({
5120
6122
  prNumber: input.prNumber,
5121
6123
  projectDir,
5122
6124
  runners: runners2,
5123
6125
  local: input.local ?? true
5124
6126
  });
5125
6127
  if (litellmProcess2) litellmProcess2.kill();
5126
- if (result.outcome === "failed") {
5127
- console.error(`Resolve failed: ${result.error}`);
6128
+ if (result2.outcome === "failed") {
6129
+ console.error(`Resolve failed: ${result2.error}`);
5128
6130
  process.exit(1);
5129
6131
  }
5130
- console.log(`Resolve: ${result.outcome}`);
6132
+ console.log(`Resolve: ${result2.outcome}`);
5131
6133
  process.exit(0);
5132
6134
  }
5133
6135
  logger.info("Preflight checks:");
5134
6136
  runPreflight();
5135
6137
  if (input.task) {
5136
- fs30.writeFileSync(path27.join(taskDir, "task.md"), input.task);
6138
+ fs32.writeFileSync(path29.join(taskDir, "task.md"), input.task);
5137
6139
  }
5138
- const taskMdPath = path27.join(taskDir, "task.md");
5139
- if (!fs30.existsSync(taskMdPath) && isPRFix && input.prNumber) {
6140
+ const taskMdPath = path29.join(taskDir, "task.md");
6141
+ if (!fs32.existsSync(taskMdPath) && isPRFix && input.prNumber) {
5140
6142
  logger.info(`Fetching PR #${input.prNumber} details as task context...`);
5141
6143
  const prDetails = getPRDetails(input.prNumber);
5142
6144
  if (prDetails) {
5143
6145
  const taskContent = `# ${prDetails.title}
5144
6146
 
5145
6147
  ${prDetails.body ?? ""}`;
5146
- fs30.writeFileSync(taskMdPath, taskContent);
6148
+ fs32.writeFileSync(taskMdPath, taskContent);
5147
6149
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
5148
6150
  }
5149
- } else if (!fs30.existsSync(taskMdPath) && input.issueNumber) {
6151
+ } else if (!fs32.existsSync(taskMdPath) && input.issueNumber) {
5150
6152
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
5151
6153
  const issue = getIssue(input.issueNumber);
5152
6154
  if (issue) {
5153
6155
  const taskContent = `# ${issue.title}
5154
6156
 
5155
6157
  ${issue.body ?? ""}`;
5156
- fs30.writeFileSync(taskMdPath, taskContent);
6158
+ fs32.writeFileSync(taskMdPath, taskContent);
5157
6159
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
5158
6160
  }
5159
6161
  }
5160
- if (!fs30.existsSync(taskMdPath)) {
6162
+ if (!fs32.existsSync(taskMdPath)) {
5161
6163
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
5162
6164
  process.exit(1);
5163
6165
  }
@@ -5295,7 +6297,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
5295
6297
  }
5296
6298
  }
5297
6299
  const state = await runPipeline(ctx);
5298
- const files = fs30.readdirSync(taskDir);
6300
+ const files = fs32.readdirSync(taskDir);
5299
6301
  console.log(`
5300
6302
  Artifacts in ${taskDir}:`);
5301
6303
  for (const f of files) {
@@ -5360,8 +6362,8 @@ var init_entry = __esm({
5360
6362
  });
5361
6363
 
5362
6364
  // src/bin/cli.ts
5363
- import * as fs31 from "fs";
5364
- import * as path28 from "path";
6365
+ import * as fs33 from "fs";
6366
+ import * as path30 from "path";
5365
6367
  import { fileURLToPath as fileURLToPath2 } from "url";
5366
6368
 
5367
6369
  // src/bin/commands/init.ts
@@ -5533,7 +6535,7 @@ function buildConfig(cwd, basic) {
5533
6535
  github: { owner: basic.owner, repo: basic.repo },
5534
6536
  agent: {
5535
6537
  provider: "anthropic",
5536
- modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
6538
+ modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
5537
6539
  }
5538
6540
  };
5539
6541
  const mcp = detectMcpConfig(cwd, basic.pm, pkg);
@@ -5733,15 +6735,15 @@ function initCommand(opts, pkgRoot) {
5733
6735
 
5734
6736
  // src/bin/commands/bootstrap.ts
5735
6737
  init_architecture_detection();
5736
- import * as fs7 from "fs";
5737
- import * as path6 from "path";
6738
+ import * as fs8 from "fs";
6739
+ import * as path7 from "path";
5738
6740
  import { execFileSync as execFileSync5 } from "child_process";
5739
6741
 
5740
6742
  // src/bin/qa-guide.ts
5741
6743
  import * as fs5 from "fs";
5742
6744
  import * as path4 from "path";
5743
6745
  function discoverQaContext(cwd) {
5744
- const result = {
6746
+ const result2 = {
5745
6747
  routes: [],
5746
6748
  authFiles: [],
5747
6749
  loginPage: null,
@@ -5754,21 +6756,21 @@ function discoverQaContext(cwd) {
5754
6756
  const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
5755
6757
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
5756
6758
  const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
5757
- if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
5758
- if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
5759
- else if (allDeps.vite) result.devPort = 5173;
6759
+ if (pkg.scripts?.dev) result2.devCommand = `${pm} dev`;
6760
+ if (allDeps.next || allDeps.nuxt) result2.devPort = 3e3;
6761
+ else if (allDeps.vite) result2.devPort = 5173;
5760
6762
  } catch {
5761
6763
  }
5762
6764
  const appDirs = ["src/app", "app"];
5763
6765
  for (const appDir of appDirs) {
5764
6766
  const fullAppDir = path4.join(cwd, appDir);
5765
6767
  if (!fs5.existsSync(fullAppDir)) continue;
5766
- scanRoutes(fullAppDir, appDir, "", result);
6768
+ scanRoutes(fullAppDir, appDir, "", result2);
5767
6769
  break;
5768
6770
  }
5769
6771
  const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
5770
6772
  for (const p of authPatterns) {
5771
- if (fs5.existsSync(path4.join(cwd, p))) result.authFiles.push(p);
6773
+ if (fs5.existsSync(path4.join(cwd, p))) result2.authFiles.push(p);
5772
6774
  }
5773
6775
  const authConfigGlobs = [
5774
6776
  "src/app/api/auth",
@@ -5779,7 +6781,7 @@ function discoverQaContext(cwd) {
5779
6781
  "src/app/api/oauth"
5780
6782
  ];
5781
6783
  for (const g of authConfigGlobs) {
5782
- if (fs5.existsSync(path4.join(cwd, g))) result.authFiles.push(g);
6784
+ if (fs5.existsSync(path4.join(cwd, g))) result2.authFiles.push(g);
5783
6785
  }
5784
6786
  try {
5785
6787
  const rolePaths = [
@@ -5801,7 +6803,7 @@ function discoverQaContext(cwd) {
5801
6803
  if (roleMatches) {
5802
6804
  for (const m of roleMatches) {
5803
6805
  const val = m.match(/['"](\w+)['"]/);
5804
- if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
6806
+ if (val && !result2.roles.includes(val[1])) result2.roles.push(val[1]);
5805
6807
  }
5806
6808
  }
5807
6809
  const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
@@ -5810,7 +6812,7 @@ function discoverQaContext(cwd) {
5810
6812
  if (vals) {
5811
6813
  for (const v of vals) {
5812
6814
  const clean = v.replace(/['"]/g, "");
5813
- if (!result.roles.includes(clean)) result.roles.push(clean);
6815
+ if (!result2.roles.includes(clean)) result2.roles.push(clean);
5814
6816
  }
5815
6817
  }
5816
6818
  }
@@ -5820,9 +6822,9 @@ function discoverQaContext(cwd) {
5820
6822
  }
5821
6823
  } catch {
5822
6824
  }
5823
- return result;
6825
+ return result2;
5824
6826
  }
5825
- function scanRoutes(dir, baseDir, prefix, result) {
6827
+ function scanRoutes(dir, baseDir, prefix, result2) {
5826
6828
  let entries;
5827
6829
  try {
5828
6830
  entries = fs5.readdirSync(dir, { withFileTypes: true });
@@ -5833,16 +6835,16 @@ function scanRoutes(dir, baseDir, prefix, result) {
5833
6835
  if (hasPage) {
5834
6836
  const routePath = prefix || "/";
5835
6837
  const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
5836
- result.routes.push({ path: routePath, group });
5837
- if (prefix.includes("/login")) result.loginPage = routePath;
5838
- if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
6838
+ result2.routes.push({ path: routePath, group });
6839
+ if (prefix.includes("/login")) result2.loginPage = routePath;
6840
+ if (prefix.startsWith("/admin") && !result2.adminPath) result2.adminPath = prefix;
5839
6841
  }
5840
6842
  for (const entry of entries) {
5841
6843
  if (!entry.isDirectory()) continue;
5842
6844
  if (entry.name === "node_modules" || entry.name === ".next") continue;
5843
6845
  let segment = entry.name;
5844
6846
  if (segment.startsWith("(") && segment.endsWith(")")) {
5845
- scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result);
6847
+ scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result2);
5846
6848
  continue;
5847
6849
  }
5848
6850
  if (segment.startsWith("[") && segment.endsWith("]")) {
@@ -5851,7 +6853,7 @@ function scanRoutes(dir, baseDir, prefix, result) {
5851
6853
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
5852
6854
  segment = `:${segment.slice(2, -2)}?`;
5853
6855
  }
5854
- scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
6856
+ scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result2);
5855
6857
  }
5856
6858
  }
5857
6859
  function generateQaGuide(discovery) {
@@ -6008,22 +7010,23 @@ function installSkillsForProject(cwd) {
6008
7010
  }
6009
7011
 
6010
7012
  // src/bin/commands/bootstrap.ts
7013
+ init_config();
6011
7014
  var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
6012
7015
  function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
6013
- const srcDir = path6.join(cwd, "src");
6014
- const baseDir = fs7.existsSync(srcDir) ? srcDir : cwd;
7016
+ const srcDir = path7.join(cwd, "src");
7017
+ const baseDir = fs8.existsSync(srcDir) ? srcDir : cwd;
6015
7018
  const results = [];
6016
7019
  function walk(dir) {
6017
7020
  const entries = [];
6018
7021
  try {
6019
- for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
7022
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
6020
7023
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
6021
- const full = path6.join(dir, entry.name);
7024
+ const full = path7.join(dir, entry.name);
6022
7025
  if (entry.isDirectory()) {
6023
7026
  entries.push(...walk(full));
6024
7027
  } else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
6025
7028
  try {
6026
- const stat = fs7.statSync(full);
7029
+ const stat = fs8.statSync(full);
6027
7030
  if (stat.size >= 200 && stat.size <= 5e3) {
6028
7031
  entries.push({ filePath: full, size: stat.size });
6029
7032
  }
@@ -6037,8 +7040,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
6037
7040
  }
6038
7041
  const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
6039
7042
  for (const { filePath } of files) {
6040
- const rel = path6.relative(cwd, filePath);
6041
- const content = fs7.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
7043
+ const rel = path7.relative(cwd, filePath);
7044
+ const content = fs8.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
6042
7045
  results.push(`### File: ${rel}
6043
7046
  \`\`\`typescript
6044
7047
  ${content}
@@ -6050,9 +7053,9 @@ function ghComment(issueNumber, body, cwd) {
6050
7053
  try {
6051
7054
  let repoSlug = "";
6052
7055
  try {
6053
- const configPath = path6.join(cwd, "kody.config.json");
6054
- if (fs7.existsSync(configPath)) {
6055
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
7056
+ const configPath = path7.join(cwd, "kody.config.json");
7057
+ if (fs8.existsSync(configPath)) {
7058
+ const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6056
7059
  if (config.github?.owner && config.github?.repo) {
6057
7060
  repoSlug = `${config.github.owner}/${config.github.repo}`;
6058
7061
  }
@@ -6079,7 +7082,9 @@ function ghComment(issueNumber, body, cwd) {
6079
7082
  }
6080
7083
  function bootstrapCommand(opts, pkgRoot) {
6081
7084
  const cwd = process.cwd();
7085
+ setConfigDir(cwd);
6082
7086
  const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
7087
+ const bootstrapModel = resolveStageConfig(getProjectConfig(), "bootstrap", "cheap").model;
6083
7088
  console.log(`
6084
7089
  \u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
6085
7090
  `);
@@ -6087,8 +7092,8 @@ function bootstrapCommand(opts, pkgRoot) {
6087
7092
  ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
6088
7093
  }
6089
7094
  const readIfExists = (rel, maxChars = 3e3) => {
6090
- const p = path6.join(cwd, rel);
6091
- if (fs7.existsSync(p)) return fs7.readFileSync(p, "utf-8").slice(0, maxChars);
7095
+ const p = path7.join(cwd, rel);
7096
+ if (fs8.existsSync(p)) return fs8.readFileSync(p, "utf-8").slice(0, maxChars);
6092
7097
  return null;
6093
7098
  };
6094
7099
  let repoContext = "";
@@ -6123,14 +7128,14 @@ ${sampleFiles}
6123
7128
 
6124
7129
  `;
6125
7130
  try {
6126
- const topDirs = fs7.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
7131
+ const topDirs = fs8.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
6127
7132
  repoContext += `## Top-level directories
6128
7133
  ${topDirs.join(", ")}
6129
7134
 
6130
7135
  `;
6131
- const srcDir = path6.join(cwd, "src");
6132
- if (fs7.existsSync(srcDir)) {
6133
- const srcDirs = fs7.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7136
+ const srcDir = path7.join(cwd, "src");
7137
+ if (fs8.existsSync(srcDir)) {
7138
+ const srcDirs = fs8.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6134
7139
  if (srcDirs.length > 0) repoContext += `## src/ subdirectories
6135
7140
  ${srcDirs.join(", ")}
6136
7141
 
@@ -6140,19 +7145,19 @@ ${srcDirs.join(", ")}
6140
7145
  }
6141
7146
  const existingFiles = [];
6142
7147
  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"]) {
6143
- if (fs7.existsSync(path6.join(cwd, f))) existingFiles.push(f);
7148
+ if (fs8.existsSync(path7.join(cwd, f))) existingFiles.push(f);
6144
7149
  }
6145
7150
  if (existingFiles.length) repoContext += `## Config files present
6146
7151
  ${existingFiles.join(", ")}
6147
7152
 
6148
7153
  `;
6149
7154
  console.log("\u2500\u2500 Project Memory \u2500\u2500");
6150
- const memoryDir = path6.join(cwd, ".kody", "memory");
6151
- fs7.mkdirSync(memoryDir, { recursive: true });
6152
- const archPath = path6.join(memoryDir, "architecture.md");
6153
- const conventionsPath = path6.join(memoryDir, "conventions.md");
6154
- const existingArch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
6155
- const existingConv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
7155
+ const memoryDir = path7.join(cwd, ".kody", "memory");
7156
+ fs8.mkdirSync(memoryDir, { recursive: true });
7157
+ const archPath = path7.join(memoryDir, "architecture.md");
7158
+ const conventionsPath = path7.join(memoryDir, "conventions.md");
7159
+ const existingArch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
7160
+ const existingConv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
6156
7161
  const hasExisting = !!(existingArch || existingConv);
6157
7162
  const extendInstruction = hasExisting && !opts.force ? `
6158
7163
  ## Existing Documentation (EXTEND, do not replace)
@@ -6196,7 +7201,7 @@ ${repoContext}`;
6196
7201
  const output = execFileSync5("claude", [
6197
7202
  "--print",
6198
7203
  "--model",
6199
- "claude-haiku-4-5-20251001",
7204
+ bootstrapModel,
6200
7205
  "--dangerously-skip-permissions",
6201
7206
  memoryPrompt
6202
7207
  ], {
@@ -6208,12 +7213,12 @@ ${repoContext}`;
6208
7213
  const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
6209
7214
  const parsed = JSON.parse(cleaned);
6210
7215
  if (parsed.architecture) {
6211
- fs7.writeFileSync(archPath, parsed.architecture);
7216
+ fs8.writeFileSync(archPath, parsed.architecture);
6212
7217
  const lineCount = parsed.architecture.split("\n").length;
6213
7218
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
6214
7219
  }
6215
7220
  if (parsed.conventions) {
6216
- fs7.writeFileSync(conventionsPath, parsed.conventions);
7221
+ fs8.writeFileSync(conventionsPath, parsed.conventions);
6217
7222
  const lineCount = parsed.conventions.split("\n").length;
6218
7223
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
6219
7224
  }
@@ -6222,39 +7227,39 @@ ${repoContext}`;
6222
7227
  const detected = detectArchitectureBasic(cwd);
6223
7228
  if (detected.length > 0) {
6224
7229
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6225
- fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
7230
+ fs8.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
6226
7231
 
6227
7232
  ## Overview
6228
7233
  ${detected.join("\n")}
6229
7234
  `);
6230
7235
  console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
6231
7236
  }
6232
- fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
7237
+ fs8.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
6233
7238
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
6234
7239
  }
6235
7240
  console.log("\n\u2500\u2500 Step Files \u2500\u2500");
6236
- const stepsDir = path6.join(cwd, ".kody", "steps");
6237
- fs7.mkdirSync(stepsDir, { recursive: true });
6238
- const arch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
6239
- const conv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
7241
+ const stepsDir = path7.join(cwd, ".kody", "steps");
7242
+ fs8.mkdirSync(stepsDir, { recursive: true });
7243
+ const arch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
7244
+ const conv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
6240
7245
  console.log(" \u23F3 Customizing step files...");
6241
7246
  let stepCount = 0;
6242
7247
  for (const stage of STEP_STAGES) {
6243
- const templatePath = path6.join(pkgRoot, "prompts", `${stage}.md`);
6244
- if (!fs7.existsSync(templatePath)) {
7248
+ const templatePath = path7.join(pkgRoot, "prompts", `${stage}.md`);
7249
+ if (!fs8.existsSync(templatePath)) {
6245
7250
  console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
6246
7251
  continue;
6247
7252
  }
6248
- const stepOutputPath = path6.join(stepsDir, `${stage}.md`);
6249
- if (fs7.existsSync(stepOutputPath) && !opts.force) {
7253
+ const stepOutputPath = path7.join(stepsDir, `${stage}.md`);
7254
+ if (fs8.existsSync(stepOutputPath) && !opts.force) {
6250
7255
  console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
6251
7256
  continue;
6252
7257
  }
6253
- const defaultPrompt = fs7.readFileSync(templatePath, "utf-8");
7258
+ const defaultPrompt = fs8.readFileSync(templatePath, "utf-8");
6254
7259
  const contextPlaceholder = "{{TASK_CONTEXT}}";
6255
7260
  const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
6256
7261
  if (placeholderIdx === -1) {
6257
- fs7.copyFileSync(templatePath, stepOutputPath);
7262
+ fs8.copyFileSync(templatePath, stepOutputPath);
6258
7263
  stepCount++;
6259
7264
  console.log(` \u2713 ${stage}.md`);
6260
7265
  continue;
@@ -6299,7 +7304,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6299
7304
  const output = execFileSync5("claude", [
6300
7305
  "--print",
6301
7306
  "--model",
6302
- "claude-haiku-4-5-20251001",
7307
+ bootstrapModel,
6303
7308
  "--dangerously-skip-permissions",
6304
7309
  customizationPrompt
6305
7310
  ], {
@@ -6311,23 +7316,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6311
7316
  let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
6312
7317
  cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
6313
7318
  const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
6314
- fs7.writeFileSync(stepOutputPath, finalPrompt);
7319
+ fs8.writeFileSync(stepOutputPath, finalPrompt);
6315
7320
  stepCount++;
6316
7321
  console.log(` \u2713 ${stage}.md`);
6317
7322
  } catch {
6318
7323
  console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
6319
- fs7.copyFileSync(templatePath, stepOutputPath);
7324
+ fs8.copyFileSync(templatePath, stepOutputPath);
6320
7325
  stepCount++;
6321
7326
  }
6322
7327
  }
6323
7328
  console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
6324
7329
  console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
6325
- const qaGuidePath = path6.join(cwd, ".kody", "qa-guide.md");
6326
- if (!fs7.existsSync(qaGuidePath) || opts.force) {
7330
+ const qaGuidePath = path7.join(cwd, ".kody", "qa-guide.md");
7331
+ if (!fs8.existsSync(qaGuidePath) || opts.force) {
6327
7332
  const discovery = discoverQaContext(cwd);
6328
7333
  if (discovery.routes.length > 0) {
6329
7334
  const qaGuide = generateQaGuide(discovery);
6330
- fs7.writeFileSync(qaGuidePath, qaGuide);
7335
+ fs8.writeFileSync(qaGuidePath, qaGuide);
6331
7336
  console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
6332
7337
  if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
6333
7338
  if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
@@ -6342,9 +7347,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6342
7347
  try {
6343
7348
  let repoSlug = "";
6344
7349
  try {
6345
- const configPath = path6.join(cwd, "kody.config.json");
6346
- if (fs7.existsSync(configPath)) {
6347
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
7350
+ const configPath = path7.join(cwd, "kody.config.json");
7351
+ if (fs8.existsSync(configPath)) {
7352
+ const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6348
7353
  if (config.github?.owner && config.github?.repo) {
6349
7354
  repoSlug = `${config.github.owner}/${config.github.repo}`;
6350
7355
  }
@@ -6417,19 +7422,19 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6417
7422
  ".kody/memory/conventions.md",
6418
7423
  ".kody/qa-guide.md",
6419
7424
  ...installedSkillPaths
6420
- ].filter((f) => fs7.existsSync(path6.join(cwd, f)));
6421
- if (fs7.existsSync(path6.join(cwd, "skills-lock.json"))) {
7425
+ ].filter((f) => fs8.existsSync(path7.join(cwd, f)));
7426
+ if (fs8.existsSync(path7.join(cwd, "skills-lock.json"))) {
6422
7427
  filesToCommit.push("skills-lock.json");
6423
7428
  }
6424
7429
  for (const stage of STEP_STAGES) {
6425
7430
  const stepFile = `.kody/steps/${stage}.md`;
6426
- if (fs7.existsSync(path6.join(cwd, stepFile))) {
7431
+ if (fs8.existsSync(path7.join(cwd, stepFile))) {
6427
7432
  filesToCommit.push(stepFile);
6428
7433
  }
6429
7434
  }
6430
7435
  if (filesToCommit.length > 0) {
6431
7436
  try {
6432
- const fullPaths = filesToCommit.map((f) => path6.join(cwd, f));
7437
+ const fullPaths = filesToCommit.map((f) => path7.join(cwd, f));
6433
7438
  for (let pass = 0; pass < 2; pass++) {
6434
7439
  execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
6435
7440
  cwd,
@@ -6456,9 +7461,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6456
7461
  console.log(` \u2713 Pushed branch: ${branchName}`);
6457
7462
  let baseBranch = "main";
6458
7463
  try {
6459
- const configPath = path6.join(cwd, "kody.config.json");
6460
- if (fs7.existsSync(configPath)) {
6461
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
7464
+ const configPath = path7.join(cwd, "kody.config.json");
7465
+ if (fs8.existsSync(configPath)) {
7466
+ const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6462
7467
  baseBranch = config.git?.defaultBranch ?? "main";
6463
7468
  }
6464
7469
  } catch {
@@ -6532,11 +7537,11 @@ Create it manually.`, cwd);
6532
7537
 
6533
7538
  // src/bin/cli.ts
6534
7539
  init_architecture_detection();
6535
- var __dirname2 = path28.dirname(fileURLToPath2(import.meta.url));
6536
- var PKG_ROOT = path28.resolve(__dirname2, "..", "..");
7540
+ var __dirname2 = path30.dirname(fileURLToPath2(import.meta.url));
7541
+ var PKG_ROOT = path30.resolve(__dirname2, "..", "..");
6537
7542
  function getVersion() {
6538
- const pkgPath = path28.join(PKG_ROOT, "package.json");
6539
- const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
7543
+ const pkgPath = path30.join(PKG_ROOT, "package.json");
7544
+ const pkg = JSON.parse(fs33.readFileSync(pkgPath, "utf-8"));
6540
7545
  return pkg.version;
6541
7546
  }
6542
7547
  var args = process.argv.slice(2);
@@ -6547,6 +7552,8 @@ if (command === "init") {
6547
7552
  bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
6548
7553
  } else if (command === "taskify") {
6549
7554
  Promise.resolve().then(() => (init_taskify_command(), taskify_command_exports)).then(({ runTaskifyCommand: runTaskifyCommand2 }) => runTaskifyCommand2());
7555
+ } else if (command === "test-model") {
7556
+ Promise.resolve().then(() => (init_test_model_command(), test_model_command_exports)).then(({ runTestModelCommand: runTestModelCommand2 }) => runTestModelCommand2());
6550
7557
  } else if (command === "ci-parse") {
6551
7558
  Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
6552
7559
  } else if (command === "version" || command === "--version" || command === "-v") {