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

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/bin/cli.js +456 -1604
  2. package/kody.config.schema.json +0 -31
  3. package/package.json +1 -1
  4. package/templates/kody.yml +0 -1
  5. package/dist/agent-runner.d.ts +0 -4
  6. package/dist/agent-runner.js +0 -122
  7. package/dist/ci/parse-inputs.d.ts +0 -6
  8. package/dist/ci/parse-inputs.js +0 -76
  9. package/dist/ci/parse-safety.d.ts +0 -6
  10. package/dist/ci/parse-safety.js +0 -22
  11. package/dist/cli/args.d.ts +0 -13
  12. package/dist/cli/args.js +0 -42
  13. package/dist/cli/litellm.d.ts +0 -2
  14. package/dist/cli/litellm.js +0 -85
  15. package/dist/cli/task-resolution.d.ts +0 -2
  16. package/dist/cli/task-resolution.js +0 -41
  17. package/dist/config.d.ts +0 -49
  18. package/dist/config.js +0 -72
  19. package/dist/context.d.ts +0 -4
  20. package/dist/context.js +0 -83
  21. package/dist/definitions.d.ts +0 -3
  22. package/dist/definitions.js +0 -59
  23. package/dist/entry.d.ts +0 -1
  24. package/dist/entry.js +0 -236
  25. package/dist/git-utils.d.ts +0 -13
  26. package/dist/git-utils.js +0 -174
  27. package/dist/github-api.d.ts +0 -14
  28. package/dist/github-api.js +0 -114
  29. package/dist/kody-utils.d.ts +0 -1
  30. package/dist/kody-utils.js +0 -9
  31. package/dist/learning/auto-learn.d.ts +0 -2
  32. package/dist/learning/auto-learn.js +0 -169
  33. package/dist/logger.d.ts +0 -14
  34. package/dist/logger.js +0 -51
  35. package/dist/memory.d.ts +0 -1
  36. package/dist/memory.js +0 -20
  37. package/dist/observer.d.ts +0 -9
  38. package/dist/observer.js +0 -80
  39. package/dist/pipeline/complexity.d.ts +0 -3
  40. package/dist/pipeline/complexity.js +0 -12
  41. package/dist/pipeline/executor-registry.d.ts +0 -3
  42. package/dist/pipeline/executor-registry.js +0 -20
  43. package/dist/pipeline/hooks.d.ts +0 -17
  44. package/dist/pipeline/hooks.js +0 -110
  45. package/dist/pipeline/questions.d.ts +0 -2
  46. package/dist/pipeline/questions.js +0 -44
  47. package/dist/pipeline/runner-selection.d.ts +0 -2
  48. package/dist/pipeline/runner-selection.js +0 -13
  49. package/dist/pipeline/state.d.ts +0 -4
  50. package/dist/pipeline/state.js +0 -37
  51. package/dist/pipeline.d.ts +0 -3
  52. package/dist/pipeline.js +0 -213
  53. package/dist/preflight.d.ts +0 -1
  54. package/dist/preflight.js +0 -69
  55. package/dist/retrospective.d.ts +0 -26
  56. package/dist/retrospective.js +0 -211
  57. package/dist/stages/agent.d.ts +0 -2
  58. package/dist/stages/agent.js +0 -94
  59. package/dist/stages/gate.d.ts +0 -2
  60. package/dist/stages/gate.js +0 -32
  61. package/dist/stages/review.d.ts +0 -2
  62. package/dist/stages/review.js +0 -32
  63. package/dist/stages/ship.d.ts +0 -3
  64. package/dist/stages/ship.js +0 -154
  65. package/dist/stages/verify.d.ts +0 -2
  66. package/dist/stages/verify.js +0 -94
  67. package/dist/types.d.ts +0 -61
  68. package/dist/types.js +0 -1
  69. package/dist/validators.d.ts +0 -8
  70. package/dist/validators.js +0 -42
  71. package/dist/verify-runner.d.ts +0 -11
  72. package/dist/verify-runner.js +0 -110
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 fs7 from "fs";
184
- import * as path6 from "path";
183
+ import * as fs8 from "fs";
184
+ import * as path7 from "path";
185
185
  function resolveStageConfig(config, stageName, modelTier) {
186
186
  const stageOverride = config.agent.stages?.[stageName];
187
187
  if (stageOverride) return stageOverride;
@@ -214,9 +214,7 @@ function getLitellmUrl() {
214
214
  return LITELLM_DEFAULT_URL;
215
215
  }
216
216
  function providerApiKeyEnvVar(provider) {
217
- if (provider === "anthropic" || provider === "claude") return "ANTHROPIC_API_KEY";
218
- const derived = `${provider.toUpperCase()}_API_KEY`;
219
- if (process.env[derived]) return derived;
217
+ if (provider === "anthropic") return "ANTHROPIC_API_KEY";
220
218
  return "ANTHROPIC_COMPATIBLE_API_KEY";
221
219
  }
222
220
  function setConfigDir(dir) {
@@ -225,16 +223,16 @@ function setConfigDir(dir) {
225
223
  }
226
224
  function getProjectConfig() {
227
225
  if (_config) return _config;
228
- const configPath = path6.join(_configDir ?? process.cwd(), "kody.config.json");
229
- if (fs7.existsSync(configPath)) {
226
+ const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
227
+ if (fs8.existsSync(configPath)) {
230
228
  try {
231
- const result2 = parseJsonSafe(fs7.readFileSync(configPath, "utf-8"));
232
- if (!result2.ok) {
233
- logger.warn(`kody.config.json: ${result2.error} \u2014 using defaults`);
229
+ const result = parseJsonSafe(fs8.readFileSync(configPath, "utf-8"));
230
+ if (!result.ok) {
231
+ logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
234
232
  _config = { ...DEFAULT_CONFIG };
235
233
  return _config;
236
234
  }
237
- const raw = result2.data;
235
+ const raw = result.data;
238
236
  _config = {
239
237
  quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
240
238
  git: { ...DEFAULT_CONFIG.git, ...raw.git },
@@ -285,7 +283,7 @@ var init_config = __esm({
285
283
  repo: ""
286
284
  },
287
285
  agent: {
288
- modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
286
+ modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
289
287
  },
290
288
  contextTiers: {
291
289
  enabled: true,
@@ -391,11 +389,10 @@ function createClaudeCodeRunner() {
391
389
  model,
392
390
  "--dangerously-skip-permissions"
393
391
  ];
394
- const baseTools = "Bash,Edit,Read,Write,Glob,Grep";
395
392
  if (options?.mcpConfigJson) {
396
393
  args2.push("--mcp-config", options.mcpConfigJson);
397
394
  } else {
398
- args2.push("--allowedTools", baseTools);
395
+ args2.push("--allowedTools", "Bash,Edit,Read,Write,Glob,Grep");
399
396
  }
400
397
  if (options?.sessionId) {
401
398
  if (options.resumeSession) {
@@ -968,8 +965,8 @@ function findLatestTaskForIssue(issueNumber, projectDir) {
968
965
  }
969
966
  function generateTaskId() {
970
967
  const now = /* @__PURE__ */ new Date();
971
- const pad2 = (n) => String(n).padStart(2, "0");
972
- return `${String(now.getFullYear()).slice(2)}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
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())}`;
973
970
  }
974
971
  function resolveTaskIdFromComments(issueNumber) {
975
972
  try {
@@ -1195,7 +1192,6 @@ var init_litellm = __esm({
1195
1192
  // src/cli/taskify-command.ts
1196
1193
  var taskify_command_exports = {};
1197
1194
  __export(taskify_command_exports, {
1198
- TaskifyError: () => TaskifyError,
1199
1195
  isTaskifyRun: () => isTaskifyRun,
1200
1196
  readTaskifyMarker: () => readTaskifyMarker,
1201
1197
  runTaskifyCommand: () => runTaskifyCommand,
@@ -1287,17 +1283,8 @@ async function runTaskifyCommand() {
1287
1283
  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
1288
1284
  };
1289
1285
  }
1290
- try {
1291
- await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1292
- } catch (err) {
1293
- if (err instanceof TaskifyError) {
1294
- logger.error(`[taskify] ${err.message}`);
1295
- process.exit(1);
1296
- }
1297
- throw err;
1298
- } finally {
1299
- litellmProcess?.kill();
1300
- }
1286
+ await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1287
+ litellmProcess?.kill();
1301
1288
  }
1302
1289
  async function taskifyCommand(opts) {
1303
1290
  const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
@@ -1312,6 +1299,7 @@ async function taskifyCommand(opts) {
1312
1299
  mcpConfigJson = buildTaskifyMcpConfigJson(config);
1313
1300
  } catch (err) {
1314
1301
  const msg = err instanceof Error ? err.message : String(err);
1302
+ logger.error(`[taskify] MCP config error: ${msg}`);
1315
1303
  if (issueNumber && !local) {
1316
1304
  postComment(
1317
1305
  issueNumber,
@@ -1322,7 +1310,7 @@ async function taskifyCommand(opts) {
1322
1310
  Add the required MCP server config to \`kody.config.json\` and try again.`
1323
1311
  );
1324
1312
  }
1325
- throw new TaskifyError(`MCP config error: ${msg}`);
1313
+ process.exit(1);
1326
1314
  }
1327
1315
  }
1328
1316
  const sc = resolveStageConfig(config, "taskify", "strong");
@@ -1363,44 +1351,47 @@ Kody is decomposing ${src} into tasks...`);
1363
1351
  fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1364
1352
  const runner = opts.runner ?? createClaudeCodeRunner();
1365
1353
  logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
1366
- const result2 = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
1354
+ const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
1367
1355
  cwd: projectDir,
1368
1356
  mcpConfigJson,
1369
1357
  env: opts.runnerEnv
1370
1358
  });
1371
- if (result2.outcome !== "completed") {
1372
- const errMsg = result2.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result2.error}`;
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}`);
1373
1362
  if (issueNumber && !local) {
1374
1363
  postComment(issueNumber, `Kody taskify failed:
1375
1364
 
1376
1365
  > ${errMsg}`);
1377
1366
  setLifecycleLabel(issueNumber, "failed");
1378
1367
  }
1379
- throw new TaskifyError(errMsg);
1368
+ process.exit(1);
1380
1369
  }
1381
1370
  const resultPath = path10.join(taskDir, RESULT_FILE);
1382
1371
  if (!fs11.existsSync(resultPath)) {
1383
1372
  const errMsg = `Claude did not write ${RESULT_FILE}. Output:
1384
1373
 
1385
- ${result2.output?.slice(0, 500) ?? "(none)"}`;
1374
+ ${result.output?.slice(0, 500) ?? "(none)"}`;
1375
+ logger.error(`[taskify] ${errMsg}`);
1386
1376
  if (issueNumber && !local) {
1387
1377
  postComment(issueNumber, `Kody taskify failed: result file not found.
1388
1378
 
1389
1379
  ${errMsg}`);
1390
1380
  setLifecycleLabel(issueNumber, "failed");
1391
1381
  }
1392
- throw new TaskifyError(errMsg);
1382
+ process.exit(1);
1393
1383
  }
1394
1384
  let parsed;
1395
1385
  try {
1396
1386
  parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
1397
1387
  } catch {
1398
1388
  const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
1389
+ logger.error(`[taskify] ${errMsg}`);
1399
1390
  if (issueNumber && !local) {
1400
1391
  postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1401
1392
  setLifecycleLabel(issueNumber, "failed");
1402
1393
  }
1403
- throw new TaskifyError(errMsg);
1394
+ process.exit(1);
1404
1395
  }
1405
1396
  const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
1406
1397
  if (parsed.status === "questions") {
@@ -1409,11 +1400,12 @@ ${errMsg}`);
1409
1400
  await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
1410
1401
  } else {
1411
1402
  const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
1403
+ logger.error(`[taskify] ${errMsg}`);
1412
1404
  if (issueNumber && !local) {
1413
1405
  postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1414
1406
  setLifecycleLabel(issueNumber, "failed");
1415
1407
  }
1416
- throw new TaskifyError(errMsg);
1408
+ process.exit(1);
1417
1409
  }
1418
1410
  }
1419
1411
  function handleQuestions(parsed, ticketId, issueNumber, local) {
@@ -1544,7 +1536,7 @@ function readTaskifyMarker(taskDir) {
1544
1536
  return null;
1545
1537
  }
1546
1538
  }
1547
- var __dirname, TaskifyError, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
1539
+ var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
1548
1540
  var init_taskify_command = __esm({
1549
1541
  "src/cli/taskify-command.ts"() {
1550
1542
  "use strict";
@@ -1556,12 +1548,6 @@ var init_taskify_command = __esm({
1556
1548
  init_task_resolution();
1557
1549
  init_litellm();
1558
1550
  __dirname = path10.dirname(fileURLToPath(import.meta.url));
1559
- TaskifyError = class extends Error {
1560
- constructor(message) {
1561
- super(message);
1562
- this.name = "TaskifyError";
1563
- }
1564
- };
1565
1551
  AUTO_TRIGGER_THRESHOLD = 5;
1566
1552
  MAX_TASKS_GUARD = 20;
1567
1553
  TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -1570,1161 +1556,6 @@ var init_taskify_command = __esm({
1570
1556
  }
1571
1557
  });
1572
1558
 
1573
- // src/cli/test-model-tests.ts
1574
- import * as fs12 from "fs";
1575
- import * as os2 from "os";
1576
- import * as path11 from "path";
1577
- import * as zlib from "zlib";
1578
- import { spawnSync, execSync as execSync2 } from "child_process";
1579
- function canRunApiTests(ctx) {
1580
- return !!ctx.apiKey;
1581
- }
1582
- async function apiCall(ctx, body) {
1583
- try {
1584
- const res = await fetch(`${ctx.proxyUrl}/v1/messages`, {
1585
- method: "POST",
1586
- headers: {
1587
- "Content-Type": "application/json",
1588
- "x-api-key": ctx.apiKey,
1589
- "anthropic-version": "2023-06-01"
1590
- },
1591
- body: JSON.stringify({ model: ctx.model, ...body }),
1592
- signal: AbortSignal.timeout(6e4)
1593
- });
1594
- const data = await res.json();
1595
- if (!res.ok) {
1596
- return { ok: false, data, status: res.status, errorMsg: data?.error?.message ?? `HTTP ${res.status}` };
1597
- }
1598
- return { ok: true, data, status: res.status };
1599
- } catch (err) {
1600
- return { ok: false, data: null, status: 0, errorMsg: err instanceof Error ? err.message : String(err) };
1601
- }
1602
- }
1603
- function extractText(data) {
1604
- if (!data?.content) return "";
1605
- return data.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
1606
- }
1607
- async function runToolConversation(ctx, tools, userPrompt, simulate, opts) {
1608
- const messages = [{ role: "user", content: userPrompt }];
1609
- const allCalls = [];
1610
- for (let turn = 0; turn < (opts?.maxTurns ?? 5); turn++) {
1611
- const body = {
1612
- max_tokens: 1024,
1613
- temperature: 0,
1614
- messages,
1615
- tools
1616
- };
1617
- if (opts?.system) body.system = opts.system;
1618
- const res = await apiCall(ctx, body);
1619
- if (!res.ok) return { finalText: "", toolCalls: allCalls, error: res.errorMsg };
1620
- const content = res.data.content ?? [];
1621
- const toolBlocks = content.filter((b) => b.type === "tool_use");
1622
- const textBlocks = content.filter((b) => b.type === "text");
1623
- if (toolBlocks.length === 0) {
1624
- return { finalText: textBlocks.map((b) => b.text ?? "").join(""), toolCalls: allCalls };
1625
- }
1626
- for (const tc of toolBlocks) allCalls.push({ name: tc.name, input: tc.input });
1627
- messages.push({ role: "assistant", content });
1628
- messages.push({
1629
- role: "user",
1630
- content: toolBlocks.map((tc) => ({
1631
- type: "tool_result",
1632
- tool_use_id: tc.id,
1633
- content: simulate(tc.name, tc.input)
1634
- }))
1635
- });
1636
- }
1637
- return { finalText: "", toolCalls: allCalls, error: "Max turns reached" };
1638
- }
1639
- function filterStderr(stderr) {
1640
- 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();
1641
- }
1642
- function runClaudeTest(ctx, prompt, extraFlags = [], timeout = 9e4) {
1643
- try {
1644
- const isDirectAnthropic = ctx.proxyUrl.includes("api.anthropic.com");
1645
- const envOverrides = isDirectAnthropic ? {} : { ANTHROPIC_BASE_URL: ctx.proxyUrl, ANTHROPIC_API_KEY: ctx.apiKey };
1646
- const result2 = spawnSync("claude", [
1647
- "--print",
1648
- "--model",
1649
- ctx.model,
1650
- "--dangerously-skip-permissions",
1651
- ...extraFlags,
1652
- "-p",
1653
- prompt
1654
- ], {
1655
- env: { ...process.env, ...envOverrides },
1656
- timeout,
1657
- encoding: "utf-8",
1658
- cwd: ctx.projectDir
1659
- });
1660
- return {
1661
- stdout: result2.stdout ?? "",
1662
- stderr: filterStderr(result2.stderr ?? ""),
1663
- exitCode: result2.status ?? 1
1664
- };
1665
- } catch (err) {
1666
- return { stdout: "", stderr: String(err), exitCode: 1 };
1667
- }
1668
- }
1669
- function isGitClean(dir) {
1670
- try {
1671
- const out = execSync2("git diff --name-only", { cwd: dir, encoding: "utf-8", timeout: 5e3 });
1672
- return out.trim().length === 0;
1673
- } catch {
1674
- return false;
1675
- }
1676
- }
1677
- function revertChanges(dir) {
1678
- try {
1679
- execSync2("git checkout -- src/logger.ts", { cwd: dir, timeout: 5e3, stdio: "pipe" });
1680
- } catch {
1681
- }
1682
- }
1683
- function result(name, category, status, accuracy, durationMs, detail, metrics) {
1684
- return { name, category, status, accuracy, durationMs, detail, metrics };
1685
- }
1686
- function crc32(buf) {
1687
- let c = 4294967295;
1688
- for (const b of buf) c = CRC_TABLE[(c ^ b) & 255] ^ c >>> 8;
1689
- return (c ^ 4294967295) >>> 0;
1690
- }
1691
- function createRedPng() {
1692
- const w = 4, h = 4;
1693
- const scanlines = Buffer.alloc(h * (1 + w * 3));
1694
- for (let y = 0; y < h; y++) {
1695
- const off = y * (1 + w * 3);
1696
- scanlines[off] = 0;
1697
- for (let x = 0; x < w; x++) {
1698
- scanlines[off + 1 + x * 3] = 255;
1699
- scanlines[off + 1 + x * 3 + 1] = 0;
1700
- scanlines[off + 1 + x * 3 + 2] = 0;
1701
- }
1702
- }
1703
- function chunk(type, data) {
1704
- const tb = Buffer.from(type, "ascii");
1705
- const merged = Buffer.concat([tb, data]);
1706
- const len = Buffer.alloc(4);
1707
- len.writeUInt32BE(data.length);
1708
- const crcBuf = Buffer.alloc(4);
1709
- crcBuf.writeUInt32BE(crc32(merged));
1710
- return Buffer.concat([len, tb, data, crcBuf]);
1711
- }
1712
- const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
1713
- const ihdr = Buffer.alloc(13);
1714
- ihdr.writeUInt32BE(w, 0);
1715
- ihdr.writeUInt32BE(h, 4);
1716
- ihdr[8] = 8;
1717
- ihdr[9] = 2;
1718
- return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(scanlines)), chunk("IEND", Buffer.alloc(0))]);
1719
- }
1720
- async function testSimplePrompt(ctx) {
1721
- const t = Date.now();
1722
- if (!canRunApiTests(ctx)) {
1723
- const r = runClaudeTest(ctx, "Reply with exactly: KODY_TEST_OK");
1724
- const ok2 = r.stdout.includes("KODY_TEST_OK");
1725
- return result(
1726
- "simple_prompt",
1727
- "basic",
1728
- ok2 ? "pass" : "fail",
1729
- ok2 ? 100 : 0,
1730
- Date.now() - t,
1731
- ok2 ? "Model responded correctly (via CLI)" : `Got: ${r.stdout.slice(0, 80)}`
1732
- );
1733
- }
1734
- const res = await apiCall(ctx, {
1735
- max_tokens: 50,
1736
- temperature: 0,
1737
- messages: [{ role: "user", content: "Reply with exactly: KODY_TEST_OK" }]
1738
- });
1739
- if (!res.ok) return result("simple_prompt", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
1740
- const text = extractText(res.data);
1741
- const ok = text.includes("KODY_TEST_OK");
1742
- return result(
1743
- "simple_prompt",
1744
- "basic",
1745
- ok ? "pass" : "fail",
1746
- ok ? 100 : 0,
1747
- Date.now() - t,
1748
- ok ? "Model responded correctly" : `Expected KODY_TEST_OK, got: ${text.slice(0, 80)}`
1749
- );
1750
- }
1751
- async function testJsonOutput(ctx) {
1752
- if (!canRunApiTests(ctx)) {
1753
- const t2 = Date.now();
1754
- const r = runClaudeTest(ctx, 'Respond with ONLY valid JSON, no markdown fences. Return: {"status":"ok","model":"your name"}');
1755
- let text2 = r.stdout.trim().replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
1756
- try {
1757
- JSON.parse(text2);
1758
- return result("json_output", "basic", "pass", 100, Date.now() - t2, "Valid JSON via CLI");
1759
- } catch {
1760
- return result("json_output", "basic", "fail", 0, Date.now() - t2, `Invalid JSON: ${text2.slice(0, 80)}`);
1761
- }
1762
- }
1763
- const t = Date.now();
1764
- const res = await apiCall(ctx, {
1765
- max_tokens: 200,
1766
- temperature: 0,
1767
- system: "Respond with ONLY valid JSON. No markdown fences, no explanation. Just raw JSON.",
1768
- messages: [{ role: "user", content: 'Return a JSON object with keys "status" (string "ok") and "model" (string, your model name).' }]
1769
- });
1770
- if (!res.ok) return result("json_output", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
1771
- let text = extractText(res.data).trim();
1772
- text = text.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
1773
- try {
1774
- const parsed = JSON.parse(text);
1775
- const hasKeys = typeof parsed.status === "string" && typeof parsed.model === "string";
1776
- return result(
1777
- "json_output",
1778
- "basic",
1779
- "pass",
1780
- hasKeys ? 100 : 70,
1781
- Date.now() - t,
1782
- hasKeys ? "Valid JSON with correct keys" : "Valid JSON but missing expected keys"
1783
- );
1784
- } catch {
1785
- return result("json_output", "basic", "fail", 0, Date.now() - t, `Invalid JSON: ${text.slice(0, 80)}`);
1786
- }
1787
- }
1788
- function scoreRules(text) {
1789
- let score = 0;
1790
- const checks = [];
1791
- if (text.startsWith("KODY:") || text.startsWith("kody:")) {
1792
- score += 20;
1793
- checks.push("starts-with-kody");
1794
- }
1795
- if (!text.toLowerCase().split(/\s+/).includes("the")) {
1796
- score += 20;
1797
- checks.push("no-the");
1798
- }
1799
- if (text.split(/\s+/).length <= 55) {
1800
- score += 20;
1801
- checks.push("under-50-words");
1802
- }
1803
- if (text.endsWith("END") || text.endsWith("end")) {
1804
- score += 20;
1805
- checks.push("ends-with-end");
1806
- }
1807
- if (text === text.toLowerCase()) {
1808
- score += 20;
1809
- checks.push("all-lowercase");
1810
- }
1811
- return { score, checks };
1812
- }
1813
- async function testSystemPromptRules(ctx) {
1814
- const rulesPrompt = [
1815
- "STRICT RULES \u2014 violating ANY will crash the system:",
1816
- "1) Start every response with 'KODY:'",
1817
- "2) Never use the word 'the'",
1818
- "3) Keep response under 50 words",
1819
- "4) End your response with 'END'",
1820
- "5) Use ONLY lowercase letters (no uppercase anywhere)"
1821
- ].join("\n");
1822
- if (!canRunApiTests(ctx)) {
1823
- const t2 = Date.now();
1824
- const r = runClaudeTest(ctx, [
1825
- "Follow ALL these rules in your response:",
1826
- "1) Your response must start with the word 'KODY:'",
1827
- "2) Do not use the word 'the' anywhere",
1828
- "3) Keep your response under 50 words total",
1829
- "4) End your response with the word 'END'",
1830
- "5) Use only lowercase letters throughout",
1831
- "",
1832
- "Now describe what a compiler does. Remember: follow ALL 5 rules above exactly."
1833
- ].join("\n"));
1834
- const { score: score2, checks: checks2 } = scoreRules(r.stdout.trim());
1835
- const status2 = score2 >= 80 ? "pass" : score2 >= 40 ? "warn" : "fail";
1836
- return result(
1837
- "system_prompt_rules",
1838
- "basic",
1839
- status2,
1840
- score2,
1841
- Date.now() - t2,
1842
- `${score2 / 20}/5 rules followed: ${checks2.join(", ")}`,
1843
- { instructionCompliance: score2 }
1844
- );
1845
- }
1846
- const t = Date.now();
1847
- const res = await apiCall(ctx, {
1848
- max_tokens: 200,
1849
- temperature: 0,
1850
- system: rulesPrompt,
1851
- messages: [{ role: "user", content: "Describe what a compiler does." }]
1852
- });
1853
- if (!res.ok) return result("system_prompt_rules", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
1854
- const text = extractText(res.data).trim();
1855
- const { score, checks } = scoreRules(text);
1856
- const status = score >= 80 ? "pass" : score >= 40 ? "warn" : "fail";
1857
- return result(
1858
- "system_prompt_rules",
1859
- "basic",
1860
- status,
1861
- score,
1862
- Date.now() - t,
1863
- `${score / 20}/5 rules followed: ${checks.join(", ")}`,
1864
- { instructionCompliance: score }
1865
- );
1866
- }
1867
- async function testExtendedThinking(ctx) {
1868
- if (!canRunApiTests(ctx)) {
1869
- const t2 = Date.now();
1870
- const r = runClaudeTest(ctx, "What is 15 * 23? Reply with just the number.");
1871
- const ok = r.stdout.includes("345");
1872
- return result(
1873
- "extended_thinking",
1874
- "infrastructure",
1875
- ok ? "pass" : "warn",
1876
- ok ? 100 : 50,
1877
- Date.now() - t2,
1878
- ok ? "Model responded correctly (thinking assumed via CLI)" : `Got: ${r.stdout.slice(0, 80)}`
1879
- );
1880
- }
1881
- const t = Date.now();
1882
- const res = await apiCall(ctx, {
1883
- max_tokens: 200,
1884
- thinking: { type: "enabled", budget_tokens: 2e3 },
1885
- messages: [{ role: "user", content: "What is 15 * 23?" }]
1886
- });
1887
- if (!res.ok) return result(
1888
- "extended_thinking",
1889
- "infrastructure",
1890
- "warn",
1891
- 50,
1892
- Date.now() - t,
1893
- `Request failed (model may not support thinking): ${res.errorMsg?.slice(0, 80)}`
1894
- );
1895
- const hasThinking = Array.isArray(res.data.content) && res.data.content.some((b) => b.type === "thinking");
1896
- const hasText = extractText(res.data).length > 0;
1897
- if (hasThinking) return result("extended_thinking", "infrastructure", "pass", 100, Date.now() - t, "Thinking block present in response");
1898
- if (hasText) return result("extended_thinking", "infrastructure", "warn", 70, Date.now() - t, "Response OK but no thinking block");
1899
- return result("extended_thinking", "infrastructure", "fail", 0, Date.now() - t, "No content in response");
1900
- }
1901
- async function testToolRead(ctx) {
1902
- if (!canRunApiTests(ctx)) {
1903
- const t2 = Date.now();
1904
- const testFile2 = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
1905
- fs12.writeFileSync(testFile2, "KODY_SECRET_CONTENT_42");
1906
- try {
1907
- const r = runClaudeTest(ctx, `Read the file ${testFile2} and tell me its exact contents. Reply with ONLY the file contents.`);
1908
- const ok = r.stdout.includes("KODY_SECRET_CONTENT_42");
1909
- return result(
1910
- "tool_read",
1911
- "tool-use",
1912
- ok ? "pass" : "fail",
1913
- ok ? 100 : 0,
1914
- Date.now() - t2,
1915
- ok ? "Read tool works via CLI" : `Got: ${r.stdout.slice(0, 80)}`,
1916
- { toolSelection: ok ? 100 : 0 }
1917
- );
1918
- } finally {
1919
- fs12.rmSync(testFile2, { force: true });
1920
- }
1921
- }
1922
- const t = Date.now();
1923
- const testFile = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
1924
- fs12.writeFileSync(testFile, "KODY_SECRET_CONTENT_42");
1925
- try {
1926
- const conv = await runToolConversation(
1927
- ctx,
1928
- [TOOL_READ],
1929
- `Read the file ${testFile} and tell me what it contains.`,
1930
- (name, input) => {
1931
- if (name === "Read" && input.path === testFile) return "KODY_SECRET_CONTENT_42";
1932
- return "Error: File not found";
1933
- }
1934
- );
1935
- if (conv.error) return result("tool_read", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
1936
- const calledRead = conv.toolCalls.some((tc) => tc.name === "Read");
1937
- const correctPath = conv.toolCalls.some((tc) => tc.name === "Read" && tc.input.path === testFile);
1938
- const mentionsContent = conv.finalText.includes("KODY_SECRET_CONTENT_42") || conv.finalText.includes("42");
1939
- let acc = 0;
1940
- if (calledRead) acc += 30;
1941
- if (correctPath) acc += 30;
1942
- if (mentionsContent) acc += 40;
1943
- return result(
1944
- "tool_read",
1945
- "tool-use",
1946
- acc >= 60 ? "pass" : "fail",
1947
- acc,
1948
- Date.now() - t,
1949
- `Read called: ${calledRead}, correct path: ${correctPath}, content referenced: ${mentionsContent}`,
1950
- { toolSelection: calledRead ? 100 : 0 }
1951
- );
1952
- } finally {
1953
- fs12.rmSync(testFile, { force: true });
1954
- }
1955
- }
1956
- async function testToolEdit(ctx) {
1957
- if (!canRunApiTests(ctx)) {
1958
- const t2 = Date.now();
1959
- const testFile = path11.join(os2.tmpdir(), "kody-test-model-edit.txt");
1960
- fs12.writeFileSync(testFile, "hello world");
1961
- try {
1962
- const r = runClaudeTest(ctx, `Use the Edit tool to replace "hello" with "goodbye" in ${testFile}. Do nothing else.`);
1963
- const content = fs12.existsSync(testFile) ? fs12.readFileSync(testFile, "utf-8") : "";
1964
- const ok = content.includes("goodbye");
1965
- return result(
1966
- "tool_edit",
1967
- "tool-use",
1968
- ok ? "pass" : "fail",
1969
- ok ? 100 : 0,
1970
- Date.now() - t2,
1971
- ok ? "Edit tool works via CLI" : `File content: ${content.slice(0, 80)}`,
1972
- { toolSelection: ok ? 100 : 0 }
1973
- );
1974
- } finally {
1975
- fs12.rmSync(testFile, { force: true });
1976
- }
1977
- }
1978
- const t = Date.now();
1979
- const conv = await runToolConversation(
1980
- ctx,
1981
- [TOOL_READ, TOOL_EDIT],
1982
- 'Read the file /tmp/kody-edit-test.txt, then use Edit to replace "hello" with "goodbye" in it.',
1983
- (name, input) => {
1984
- if (name === "Read") return "hello world";
1985
- if (name === "Edit") return "File edited successfully";
1986
- return "Unknown tool";
1987
- }
1988
- );
1989
- if (conv.error) return result("tool_edit", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
1990
- const editCall = conv.toolCalls.find((tc) => tc.name === "Edit");
1991
- let acc = 0;
1992
- if (editCall) {
1993
- acc += 40;
1994
- if (editCall.input.old_string === "hello") acc += 30;
1995
- if (editCall.input.new_string === "goodbye") acc += 30;
1996
- }
1997
- return result(
1998
- "tool_edit",
1999
- "tool-use",
2000
- acc >= 70 ? "pass" : acc > 0 ? "warn" : "fail",
2001
- acc,
2002
- Date.now() - t,
2003
- editCall ? `Edit called with old="${editCall.input.old_string}" new="${editCall.input.new_string}"` : "Edit tool was not called",
2004
- { toolSelection: editCall ? 100 : 0 }
2005
- );
2006
- }
2007
- async function testToolBash(ctx) {
2008
- if (!canRunApiTests(ctx)) {
2009
- const t2 = Date.now();
2010
- const r = runClaudeTest(ctx, "Run this bash command and tell me its output: echo KODY_BASH_OK");
2011
- const ok = r.stdout.includes("KODY_BASH_OK");
2012
- return result(
2013
- "tool_bash",
2014
- "tool-use",
2015
- ok ? "pass" : "fail",
2016
- ok ? 100 : 0,
2017
- Date.now() - t2,
2018
- ok ? "Bash tool works via CLI" : `Got: ${r.stdout.slice(0, 80)}`,
2019
- { toolSelection: ok ? 100 : 0 }
2020
- );
2021
- }
2022
- const t = Date.now();
2023
- const conv = await runToolConversation(
2024
- ctx,
2025
- [TOOL_BASH],
2026
- "Run this exact bash command: echo KODY_BASH_OK",
2027
- (name, input) => {
2028
- if (name === "Bash") return "KODY_BASH_OK\n";
2029
- return "Error";
2030
- }
2031
- );
2032
- if (conv.error) return result("tool_bash", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
2033
- const bashCall = conv.toolCalls.find((tc) => tc.name === "Bash");
2034
- const correctCmd = bashCall && String(bashCall.input.command).includes("echo KODY_BASH_OK");
2035
- const acc = bashCall ? correctCmd ? 100 : 50 : 0;
2036
- return result(
2037
- "tool_bash",
2038
- "tool-use",
2039
- acc >= 50 ? "pass" : "fail",
2040
- acc,
2041
- Date.now() - t,
2042
- bashCall ? `Bash called: ${bashCall.input.command}` : "Bash tool was not called",
2043
- { toolSelection: bashCall ? 100 : 0 }
2044
- );
2045
- }
2046
- async function testImageAttachment(ctx) {
2047
- if (!canRunApiTests(ctx)) {
2048
- const t2 = Date.now();
2049
- const tmpPng = path11.join(os2.tmpdir(), "kody-test-image.png");
2050
- fs12.writeFileSync(tmpPng, createRedPng());
2051
- try {
2052
- const r = runClaudeTest(ctx, `Read the image file at ${tmpPng} and tell me what color it is. Reply with just the color name.`);
2053
- const text2 = r.stdout.toLowerCase();
2054
- const ok = text2.includes("red");
2055
- return result(
2056
- "image_attachment",
2057
- "tool-use",
2058
- ok ? "pass" : "warn",
2059
- ok ? 100 : 50,
2060
- Date.now() - t2,
2061
- ok ? "Image processed correctly via CLI" : `Got: ${text2.slice(0, 80)}`
2062
- );
2063
- } finally {
2064
- fs12.rmSync(tmpPng, { force: true });
2065
- }
2066
- }
2067
- const t = Date.now();
2068
- const pngData = createRedPng().toString("base64");
2069
- const res = await apiCall(ctx, {
2070
- max_tokens: 100,
2071
- temperature: 0,
2072
- messages: [{
2073
- role: "user",
2074
- content: [
2075
- { type: "image", source: { type: "base64", media_type: "image/png", data: pngData } },
2076
- { type: "text", text: "What color is this image? Reply with just the color name." }
2077
- ]
2078
- }]
2079
- });
2080
- if (!res.ok) return result(
2081
- "image_attachment",
2082
- "tool-use",
2083
- "fail",
2084
- 0,
2085
- Date.now() - t,
2086
- `API error (model may not support vision): ${res.errorMsg?.slice(0, 80)}`
2087
- );
2088
- const text = extractText(res.data).toLowerCase();
2089
- const mentionsRed = text.includes("red");
2090
- const mentionsColor = mentionsRed || text.includes("color") || text.includes("image") || text.includes("pixel");
2091
- const acc = mentionsRed ? 100 : mentionsColor ? 50 : 20;
2092
- return result(
2093
- "image_attachment",
2094
- "tool-use",
2095
- mentionsRed ? "pass" : mentionsColor ? "warn" : "fail",
2096
- acc,
2097
- Date.now() - t,
2098
- `Response: ${text.slice(0, 80)}`
2099
- );
2100
- }
2101
- async function testErrorRecovery(ctx) {
2102
- if (!canRunApiTests(ctx)) {
2103
- const t2 = Date.now();
2104
- const r = runClaudeTest(ctx, "Read the file /tmp/kody-nonexistent-test-file-xyz.txt and tell me what's in it. If it doesn't exist, say 'FILE_NOT_FOUND'.");
2105
- const ok = r.stdout.includes("FILE_NOT_FOUND") || r.stdout.toLowerCase().includes("not found") || r.stdout.toLowerCase().includes("does not exist") || r.stdout.toLowerCase().includes("doesn't exist");
2106
- return result(
2107
- "error_recovery",
2108
- "advanced",
2109
- ok ? "pass" : "warn",
2110
- ok ? 100 : 50,
2111
- Date.now() - t2,
2112
- ok ? "Graceful error handling via CLI" : `Got: ${r.stdout.slice(0, 80)}`
2113
- );
2114
- }
2115
- const t = Date.now();
2116
- let errorGiven = false;
2117
- const conv = await runToolConversation(
2118
- ctx,
2119
- [TOOL_READ, TOOL_BASH],
2120
- "Read the file /tmp/nonexistent-kody-file.txt and tell me what's in it. If the file doesn't exist, say so.",
2121
- (name, input) => {
2122
- if (name === "Read" && !errorGiven) {
2123
- errorGiven = true;
2124
- return "Error: ENOENT: no such file or directory";
2125
- }
2126
- if (name === "Bash") return "ls: /tmp/nonexistent-kody-file.txt: No such file or directory";
2127
- return "Error: File not found";
2128
- }
2129
- );
2130
- if (conv.error) return result("error_recovery", "advanced", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
2131
- 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");
2132
- const tried = conv.toolCalls.length >= 1;
2133
- const acc = reported ? tried ? 100 : 70 : 20;
2134
- return result(
2135
- "error_recovery",
2136
- "advanced",
2137
- reported ? "pass" : "warn",
2138
- acc,
2139
- Date.now() - t,
2140
- reported ? "Gracefully reported missing file" : `Response: ${conv.finalText.slice(0, 80)}`
2141
- );
2142
- }
2143
- async function testToolMultiStep(ctx) {
2144
- const t = Date.now();
2145
- const r = runClaudeTest(
2146
- ctx,
2147
- "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."
2148
- );
2149
- if (!r.stdout.trim() && r.exitCode !== 0) return result(
2150
- "tool_multi_step",
2151
- "tool-use",
2152
- "fail",
2153
- 0,
2154
- Date.now() - t,
2155
- `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2156
- );
2157
- const out = r.stdout.trim().toLowerCase();
2158
- const correct = out.includes("main");
2159
- return result(
2160
- "tool_multi_step",
2161
- "tool-use",
2162
- correct ? "pass" : "fail",
2163
- correct ? 100 : 20,
2164
- Date.now() - t,
2165
- correct ? "Correct: main" : `Got: ${out.slice(0, 80)}`
2166
- );
2167
- }
2168
- async function testPlanStage(ctx) {
2169
- const t = Date.now();
2170
- const wasClean = isGitClean(ctx.projectDir);
2171
- const r = runClaudeTest(ctx, [
2172
- "You are a planning agent. Your ONLY job is to output a markdown plan.",
2173
- "CRITICAL: Do NOT use Edit, Write, or Bash tools. Do NOT modify any files. ONLY use Read, Glob, and Grep for research.",
2174
- "If you modify any files, the system will crash.",
2175
- "",
2176
- "Task: Plan adding a /health endpoint to an Express app.",
2177
- "Output a markdown plan with ## Step N sections. Each step must have File, Change, and Why fields.",
2178
- "Keep it to 3 steps maximum."
2179
- ].join("\n"), [], 12e4);
2180
- const filesModified = wasClean && !isGitClean(ctx.projectDir);
2181
- if (filesModified) revertChanges(ctx.projectDir);
2182
- if (!r.stdout.trim() && r.exitCode !== 0) return result(
2183
- "plan_stage",
2184
- "stage-simulation",
2185
- "fail",
2186
- 0,
2187
- Date.now() - t,
2188
- `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2189
- );
2190
- const out = r.stdout;
2191
- const hasStepFormat = /##\s*Step/i.test(out);
2192
- const hasStructure = hasStepFormat || /\*\*File\*\*/i.test(out) && /\*\*Change\*\*/i.test(out);
2193
- const boundary = filesModified ? 0 : 100;
2194
- const format = hasStructure ? 100 : hasStepFormat ? 70 : out.length > 50 ? 30 : 0;
2195
- const acc = Math.round(boundary * 0.6 + format * 0.4);
2196
- const status = filesModified ? "fail" : hasStructure ? "pass" : "warn";
2197
- return result(
2198
- "plan_stage",
2199
- "stage-simulation",
2200
- status,
2201
- acc,
2202
- Date.now() - t,
2203
- filesModified ? "FAIL: Model modified files during plan stage (instruction violation)" : hasStructure ? "Plan output with correct structure, no files modified" : "Output lacks expected ## Step structure",
2204
- { boundaryRespect: boundary, outputFormat: format, instructionCompliance: boundary }
2205
- );
2206
- }
2207
- async function testBuildStage(ctx) {
2208
- const t = Date.now();
2209
- const r = runClaudeTest(ctx, "Add a comment '// kody-build-test' as the very first line of src/logger.ts. That is your only task.");
2210
- const diff = (() => {
2211
- try {
2212
- return execSync2("git diff src/logger.ts", { cwd: ctx.projectDir, encoding: "utf-8", timeout: 5e3 });
2213
- } catch {
2214
- return "";
2215
- }
2216
- })();
2217
- const edited = diff.includes("kody-build-test");
2218
- revertChanges(ctx.projectDir);
2219
- if (!r.stdout.trim() && r.exitCode !== 0 && !edited) return result(
2220
- "build_stage",
2221
- "stage-simulation",
2222
- "fail",
2223
- 0,
2224
- Date.now() - t,
2225
- `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2226
- );
2227
- return result(
2228
- "build_stage",
2229
- "stage-simulation",
2230
- edited ? "pass" : "fail",
2231
- edited ? 100 : 0,
2232
- Date.now() - t,
2233
- edited ? "File correctly modified with expected comment" : "File was not modified as expected"
2234
- );
2235
- }
2236
- async function testReviewStage(ctx) {
2237
- const t = Date.now();
2238
- const wasClean = isGitClean(ctx.projectDir);
2239
- const r = runClaudeTest(ctx, [
2240
- "You are a code review agent. Review the file src/logger.ts.",
2241
- "CRITICAL: Do NOT modify any files. Only READ and analyze.",
2242
- "Output your review as markdown with this exact format:",
2243
- "## Summary",
2244
- "<1-2 sentence summary>",
2245
- "## Issues Found",
2246
- "- <issues>",
2247
- "## Verdict",
2248
- "APPROVE or REQUEST_CHANGES"
2249
- ].join("\n"));
2250
- const filesModified = wasClean && !isGitClean(ctx.projectDir);
2251
- if (filesModified) revertChanges(ctx.projectDir);
2252
- if (!r.stdout.trim() && r.exitCode !== 0) return result(
2253
- "review_stage",
2254
- "stage-simulation",
2255
- "fail",
2256
- 0,
2257
- Date.now() - t,
2258
- `CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
2259
- );
2260
- const out = r.stdout;
2261
- const hasVerdict = /verdict/i.test(out);
2262
- const hasSummary = /summary/i.test(out);
2263
- const boundary = filesModified ? 0 : 100;
2264
- const format = (hasVerdict ? 50 : 0) + (hasSummary ? 50 : 0);
2265
- const acc = Math.round(boundary * 0.5 + format * 0.5);
2266
- const status = filesModified ? "fail" : hasVerdict && hasSummary ? "pass" : "warn";
2267
- return result(
2268
- "review_stage",
2269
- "stage-simulation",
2270
- status,
2271
- acc,
2272
- Date.now() - t,
2273
- filesModified ? "FAIL: Model modified files during review (instruction violation)" : `Summary: ${hasSummary}, Verdict: ${hasVerdict}, no files modified`,
2274
- { boundaryRespect: boundary, outputFormat: format }
2275
- );
2276
- }
2277
- async function testMcpTools(ctx) {
2278
- const t = Date.now();
2279
- const mcpConfig = path11.join(os2.tmpdir(), `kody-test-mcp-${Date.now()}.json`);
2280
- const testFile = path11.join(ctx.projectDir, "kody-mcp-compat-test.txt");
2281
- try {
2282
- fs12.writeFileSync(mcpConfig, JSON.stringify({
2283
- mcpServers: {
2284
- filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", ctx.projectDir] }
2285
- }
2286
- }));
2287
- const r = runClaudeTest(
2288
- ctx,
2289
- `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.`,
2290
- ["--mcp-config", mcpConfig],
2291
- 12e4
2292
- );
2293
- const created = fs12.existsSync(testFile);
2294
- const content = created ? fs12.readFileSync(testFile, "utf-8").trim() : "";
2295
- const correct = content.includes("mcp-ok");
2296
- return result(
2297
- "mcp_tools",
2298
- "advanced",
2299
- created ? "pass" : "fail",
2300
- correct ? 100 : created ? 70 : 0,
2301
- Date.now() - t,
2302
- created ? `File created, content: ${content.slice(0, 50)}` : `MCP test failed: ${r.stderr.slice(0, 80)}`
2303
- );
2304
- } catch (err) {
2305
- return result("mcp_tools", "advanced", "warn", 0, Date.now() - t, `MCP test error: ${err instanceof Error ? err.message : String(err)}`);
2306
- } finally {
2307
- fs12.rmSync(mcpConfig, { force: true });
2308
- fs12.rmSync(testFile, { force: true });
2309
- revertChanges(ctx.projectDir);
2310
- }
2311
- }
2312
- var TOOL_READ, TOOL_EDIT, TOOL_BASH, CRC_TABLE, ALL_TESTS;
2313
- var init_test_model_tests = __esm({
2314
- "src/cli/test-model-tests.ts"() {
2315
- "use strict";
2316
- TOOL_READ = {
2317
- name: "Read",
2318
- description: "Read a file from the filesystem",
2319
- input_schema: {
2320
- type: "object",
2321
- properties: { path: { type: "string", description: "Absolute file path" } },
2322
- required: ["path"]
2323
- }
2324
- };
2325
- TOOL_EDIT = {
2326
- name: "Edit",
2327
- description: "Replace old_string with new_string in a file",
2328
- input_schema: {
2329
- type: "object",
2330
- properties: {
2331
- file_path: { type: "string" },
2332
- old_string: { type: "string" },
2333
- new_string: { type: "string" }
2334
- },
2335
- required: ["file_path", "old_string", "new_string"]
2336
- }
2337
- };
2338
- TOOL_BASH = {
2339
- name: "Bash",
2340
- description: "Execute a bash command and return output",
2341
- input_schema: {
2342
- type: "object",
2343
- properties: { command: { type: "string", description: "The command to run" } },
2344
- required: ["command"]
2345
- }
2346
- };
2347
- CRC_TABLE = new Uint32Array(256);
2348
- for (let n = 0; n < 256; n++) {
2349
- let c = n;
2350
- for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
2351
- CRC_TABLE[n] = c >>> 0;
2352
- }
2353
- ALL_TESTS = [
2354
- // Infrastructure
2355
- { name: "extended_thinking", category: "infrastructure", description: "Extended thinking parameter support", run: testExtendedThinking },
2356
- // Basic
2357
- { name: "simple_prompt", category: "basic", description: "Basic text prompt and response", run: testSimplePrompt },
2358
- { name: "json_output", category: "basic", description: "JSON-only output constraint", run: testJsonOutput },
2359
- { name: "system_prompt_rules", category: "basic", description: "Multi-rule system prompt adherence", run: testSystemPromptRules },
2360
- // Tool use
2361
- { name: "tool_read", category: "tool-use", description: "Read tool: file reading", run: testToolRead },
2362
- { name: "tool_edit", category: "tool-use", description: "Edit tool: old/new string replacement", run: testToolEdit },
2363
- { name: "tool_bash", category: "tool-use", description: "Bash tool: command execution", run: testToolBash },
2364
- { name: "tool_multi_step", category: "tool-use", description: "Multi-step tool chain via CLI", run: testToolMultiStep },
2365
- { name: "image_attachment", category: "tool-use", description: "Vision: image content processing", run: testImageAttachment },
2366
- // Stage simulation
2367
- { name: "plan_stage", category: "stage-simulation", description: "Plan stage: read-only research + structured output", run: testPlanStage },
2368
- { name: "build_stage", category: "stage-simulation", description: "Build stage: code editing", run: testBuildStage },
2369
- { name: "review_stage", category: "stage-simulation", description: "Review stage: read-only + structured verdict", run: testReviewStage },
2370
- // Advanced
2371
- { name: "mcp_tools", category: "advanced", description: "MCP server tool integration", run: testMcpTools },
2372
- { name: "error_recovery", category: "advanced", description: "Graceful error handling on tool failure", run: testErrorRecovery }
2373
- ];
2374
- }
2375
- });
2376
-
2377
- // src/cli/test-model-report.ts
2378
- function pad(str, len) {
2379
- return str.padEnd(len);
2380
- }
2381
- function fmtDuration(ms) {
2382
- return `${(ms / 1e3).toFixed(1)}s`;
2383
- }
2384
- function formatReport(report) {
2385
- const W = 74;
2386
- const lines = [];
2387
- lines.push("=".repeat(W));
2388
- lines.push("");
2389
- lines.push(" Model Compatibility Report");
2390
- lines.push(` Provider: ${report.provider} | Model: ${report.model}`);
2391
- lines.push(` Date: ${report.timestamp}`);
2392
- lines.push(` Duration: ${fmtDuration(report.totalDurationMs)}`);
2393
- lines.push("");
2394
- lines.push("-".repeat(W));
2395
- for (const cat of CATEGORY_ORDER) {
2396
- const catResults = report.results.filter((r) => r.category === cat);
2397
- if (catResults.length === 0) continue;
2398
- lines.push("");
2399
- lines.push(` ${CATEGORY_LABELS[cat]}`);
2400
- lines.push("");
2401
- for (const r of catResults) {
2402
- const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
2403
- const name = pad(r.name, 28);
2404
- const status = pad(r.status.toUpperCase(), 6);
2405
- const acc = pad(`${r.accuracy}%`, 5);
2406
- const dur = fmtDuration(r.durationMs);
2407
- lines.push(` [${icon}] ${name} ${status} ${acc} ${dur}`);
2408
- if (r.status !== "pass" && r.detail) {
2409
- lines.push(` ${r.detail.slice(0, W - 8)}`);
2410
- }
2411
- }
2412
- }
2413
- const passed = report.results.filter((r) => r.status === "pass").length;
2414
- const failed = report.results.filter((r) => r.status === "fail").length;
2415
- const skipped = report.results.filter((r) => r.status === "warn" && r.durationMs === 0 && r.detail.includes("Skipped")).length;
2416
- const warned = report.results.filter((r) => r.status === "warn").length - skipped;
2417
- const total = report.results.length;
2418
- const scored = report.results.filter((r) => !(r.status === "warn" && r.durationMs === 0 && r.detail.includes("Skipped")));
2419
- const avgAccuracy = scored.length > 0 ? Math.round(scored.reduce((s, r) => s + r.accuracy, 0) / scored.length) : 0;
2420
- lines.push("");
2421
- lines.push("-".repeat(W));
2422
- lines.push("");
2423
- lines.push(` RESULTS: ${passed}/${total - skipped} PASS | ${failed} FAIL | ${warned} WARN${skipped > 0 ? ` | ${skipped} SKIPPED` : ""}`);
2424
- lines.push(` OVERALL ACCURACY: ${avgAccuracy}%`);
2425
- lines.push(` drop_params required: ${report.dropParamsRequired ? "YES" : "NO"}`);
2426
- lines.push("");
2427
- lines.push(" ACCURACY BY CATEGORY:");
2428
- for (const cat of CATEGORY_ORDER) {
2429
- const cr = report.results.filter((r) => r.category === cat && !(r.status === "warn" && r.durationMs === 0 && r.detail.includes("Skipped")));
2430
- if (cr.length === 0) continue;
2431
- const avg = Math.round(cr.reduce((s, r) => s + r.accuracy, 0) / cr.length);
2432
- lines.push(` ${pad(CATEGORY_LABELS[cat], 22)} ${avg}%`);
2433
- }
2434
- lines.push("");
2435
- lines.push(" RECOMMENDATION:");
2436
- for (const line of getRecommendation(report)) {
2437
- lines.push(` ${line}`);
2438
- }
2439
- lines.push("");
2440
- lines.push("=".repeat(W));
2441
- return lines.join("\n");
2442
- }
2443
- function getRecommendation(report) {
2444
- const lines = [];
2445
- const failedTests = report.results.filter((r) => r.status === "fail");
2446
- const avg = report.results.length > 0 ? Math.round(report.results.reduce((s, r) => s + r.accuracy, 0) / report.results.length) : 0;
2447
- if (avg >= 90 && failedTests.length === 0) {
2448
- lines.push("[+] Fully compatible -- suitable for all pipeline stages");
2449
- return lines;
2450
- }
2451
- const stageResults = report.results.filter((r) => r.category === "stage-simulation");
2452
- const workingStages = stageResults.filter((r) => r.status === "pass").map((r) => r.name.replace("_stage", ""));
2453
- const failingStages = stageResults.filter((r) => r.status !== "pass").map((r) => r.name.replace("_stage", ""));
2454
- if (workingStages.length > 0) {
2455
- lines.push(`[+] Suitable for: ${workingStages.join(", ")} stages`);
2456
- }
2457
- if (failingStages.length > 0) {
2458
- lines.push(`[x] Not recommended for: ${failingStages.join(", ")} stages`);
2459
- }
2460
- if (failedTests.length > 0) {
2461
- lines.push("");
2462
- lines.push("Failed tests:");
2463
- for (const t of failedTests) {
2464
- lines.push(` - ${t.name}: ${t.detail.slice(0, 60)}`);
2465
- }
2466
- }
2467
- return lines;
2468
- }
2469
- var CATEGORY_ORDER, CATEGORY_LABELS;
2470
- var init_test_model_report = __esm({
2471
- "src/cli/test-model-report.ts"() {
2472
- "use strict";
2473
- CATEGORY_ORDER = ["infrastructure", "basic", "tool-use", "stage-simulation", "advanced"];
2474
- CATEGORY_LABELS = {
2475
- infrastructure: "INFRASTRUCTURE",
2476
- basic: "BASIC CAPABILITIES",
2477
- "tool-use": "TOOL USE",
2478
- "stage-simulation": "STAGE SIMULATION",
2479
- advanced: "ADVANCED"
2480
- };
2481
- }
2482
- });
2483
-
2484
- // src/cli/test-model-command.ts
2485
- var test_model_command_exports = {};
2486
- __export(test_model_command_exports, {
2487
- runTestModelCommand: () => runTestModelCommand
2488
- });
2489
- import * as fs13 from "fs";
2490
- import * as os3 from "os";
2491
- import * as path12 from "path";
2492
- import { execFileSync as execFileSync10 } from "child_process";
2493
- function parseTestModelArgs() {
2494
- const args2 = process.argv.slice(3);
2495
- function getArg3(flag) {
2496
- const idx = args2.indexOf(flag);
2497
- if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) return args2[idx + 1];
2498
- return void 0;
2499
- }
2500
- const hasFlag3 = (f) => args2.includes(f);
2501
- if (hasFlag3("--help") || hasFlag3("-h")) {
2502
- logger.info([
2503
- "Usage: kody test-model --provider <provider> --model <model> --key <api-key> [options]",
2504
- "",
2505
- "Options:",
2506
- " --provider LLM provider name (e.g. gemini, openai, claude)",
2507
- " --model Model identifier (e.g. gemini-2.5-flash, claude-sonnet-4-6)",
2508
- " --key API key (optional for claude/anthropic \u2014 uses CLI auth)",
2509
- " --key-env Read API key from this environment variable",
2510
- " --skip-proxy Use an already-running LiteLLM proxy (don't start one)",
2511
- " --litellm-url LiteLLM proxy URL (default: http://localhost:4099)",
2512
- " --filter Comma-separated test names to run (default: all)",
2513
- " --list List all available tests and exit"
2514
- ].join("\n"));
2515
- process.exit(0);
2516
- }
2517
- if (hasFlag3("--list")) {
2518
- for (const t of ALL_TESTS) {
2519
- logger.info(` ${t.name.padEnd(24)} [${t.category}] ${t.description}`);
2520
- }
2521
- process.exit(0);
2522
- }
2523
- const provider = getArg3("--provider");
2524
- const model = getArg3("--model");
2525
- const key = getArg3("--key");
2526
- const keyEnv = getArg3("--key-env");
2527
- if (!provider || !model) {
2528
- logger.error("Required: --provider <provider> --model <model> --key <key>");
2529
- logger.error("Run with --help for usage.");
2530
- process.exit(1);
2531
- }
2532
- const isDirectAnthropic = provider === "claude" || provider === "anthropic";
2533
- let apiKey = key;
2534
- if (!apiKey && keyEnv) apiKey = process.env[keyEnv];
2535
- if (!apiKey && !isDirectAnthropic) {
2536
- logger.error("API key required: use --key <value> or --key-env <ENV_VAR>");
2537
- logger.error("(For claude/anthropic provider, --key is optional \u2014 uses Claude Code auth)");
2538
- process.exit(1);
2539
- }
2540
- return {
2541
- provider,
2542
- model,
2543
- apiKey: apiKey ?? "",
2544
- proxyUrl: isDirectAnthropic ? "https://api.anthropic.com" : getArg3("--litellm-url") ?? TEST_URL,
2545
- skipProxy: isDirectAnthropic || hasFlag3("--skip-proxy"),
2546
- filter: getArg3("--filter")?.split(",")
2547
- };
2548
- }
2549
- function generateConfig(provider, model, dropParams) {
2550
- const lines = [];
2551
- if (dropParams) {
2552
- lines.push("litellm_settings:");
2553
- lines.push(" drop_params: true");
2554
- lines.push("");
2555
- }
2556
- lines.push("model_list:");
2557
- lines.push(` - model_name: ${model}`);
2558
- lines.push(" litellm_params:");
2559
- lines.push(` model: ${provider}/${model}`);
2560
- lines.push(" api_key: os.environ/ANTHROPIC_COMPATIBLE_API_KEY");
2561
- return lines.join("\n") + "\n";
2562
- }
2563
- async function startProxy(config, url) {
2564
- try {
2565
- execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
2566
- } catch {
2567
- try {
2568
- execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
2569
- } catch {
2570
- logger.error("litellm not installed. Install: pip install 'litellm[proxy]'");
2571
- return null;
2572
- }
2573
- }
2574
- fs13.writeFileSync(CONFIG_PATH, config);
2575
- const portMatch = url.match(/:(\d+)/);
2576
- const port = portMatch ? portMatch[1] : "4099";
2577
- const { spawn: spawn2 } = await import("child_process");
2578
- const child = spawn2("litellm", ["--config", CONFIG_PATH, "--port", port], {
2579
- stdio: ["ignore", "pipe", "pipe"],
2580
- detached: true,
2581
- env: process.env
2582
- });
2583
- for (let i = 0; i < 30; i++) {
2584
- await delay(2e3);
2585
- if (await checkLitellmHealth(url)) {
2586
- logger.info(`LiteLLM proxy ready at ${url}`);
2587
- return child;
2588
- }
2589
- }
2590
- child.kill();
2591
- return null;
2592
- }
2593
- async function quickApiTest(url, model, apiKey) {
2594
- try {
2595
- const res = await fetch(`${url}/v1/messages`, {
2596
- method: "POST",
2597
- headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
2598
- body: JSON.stringify({
2599
- model,
2600
- max_tokens: 32,
2601
- messages: [{ role: "user", content: "Say ok" }],
2602
- context_management: { policy: "smart" }
2603
- }),
2604
- signal: AbortSignal.timeout(3e4)
2605
- });
2606
- if (!res.ok) {
2607
- const body = await res.text();
2608
- return { ok: false, error: body.slice(0, 200) };
2609
- }
2610
- return { ok: true };
2611
- } catch (err) {
2612
- return { ok: false, error: String(err) };
2613
- }
2614
- }
2615
- function delay(ms) {
2616
- return new Promise((resolve5) => setTimeout(resolve5, ms));
2617
- }
2618
- async function runTestModelCommand() {
2619
- const opts = parseTestModelArgs();
2620
- const startTime = Date.now();
2621
- logger.info(`Testing model compatibility: ${opts.provider}/${opts.model}`);
2622
- logger.info("");
2623
- let proxyProcess = null;
2624
- let dropParamsRequired = false;
2625
- const cleanup = () => {
2626
- if (proxyProcess) {
2627
- proxyProcess.kill();
2628
- proxyProcess = null;
2629
- }
2630
- fs13.rmSync(CONFIG_PATH, { force: true });
2631
- };
2632
- process.on("SIGINT", () => {
2633
- cleanup();
2634
- process.exit(1);
2635
- });
2636
- process.on("SIGTERM", () => {
2637
- cleanup();
2638
- process.exit(1);
2639
- });
2640
- try {
2641
- if (!opts.skipProxy) {
2642
- process.env.ANTHROPIC_COMPATIBLE_API_KEY = opts.apiKey;
2643
- logger.info("Starting LiteLLM proxy (without drop_params)...");
2644
- proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, false), opts.proxyUrl);
2645
- if (!proxyProcess) {
2646
- logger.error("Failed to start LiteLLM proxy");
2647
- process.exit(1);
2648
- }
2649
- const quickRes = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
2650
- if (!quickRes.ok) {
2651
- logger.info("Model needs drop_params: true -- restarting proxy...");
2652
- proxyProcess.kill();
2653
- proxyProcess = null;
2654
- await delay(2e3);
2655
- proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, true), opts.proxyUrl);
2656
- dropParamsRequired = true;
2657
- if (!proxyProcess) {
2658
- logger.error("Failed to start LiteLLM proxy with drop_params");
2659
- process.exit(1);
2660
- }
2661
- const retry = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
2662
- if (!retry.ok) {
2663
- logger.error(`Model not accessible: ${retry.error}`);
2664
- process.exit(1);
2665
- }
2666
- logger.info("Proxy restarted with drop_params: true");
2667
- } else {
2668
- logger.info("drop_params not required");
2669
- }
2670
- } else {
2671
- logger.info(`Using existing proxy at ${opts.proxyUrl}`);
2672
- }
2673
- const tests = opts.filter ? ALL_TESTS.filter((t) => opts.filter.includes(t.name)) : ALL_TESTS;
2674
- logger.info(`Running ${tests.length} compatibility tests...`);
2675
- logger.info("");
2676
- const ctx = { proxyUrl: opts.proxyUrl, model: opts.model, apiKey: opts.apiKey, projectDir: process.cwd() };
2677
- const results = [];
2678
- for (const test of tests) {
2679
- process.stdout.write(` ${test.name.padEnd(28)} `);
2680
- try {
2681
- const r = await test.run(ctx);
2682
- results.push(r);
2683
- const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
2684
- logger.info(`[${icon}] ${r.status.toUpperCase()} ${r.accuracy}% (${(r.durationMs / 1e3).toFixed(1)}s)`);
2685
- } catch (err) {
2686
- const r = {
2687
- name: test.name,
2688
- category: test.category,
2689
- status: "fail",
2690
- accuracy: 0,
2691
- durationMs: 0,
2692
- detail: `Crash: ${err instanceof Error ? err.message : String(err)}`
2693
- };
2694
- results.push(r);
2695
- logger.info("[x] CRASH");
2696
- }
2697
- }
2698
- const report = {
2699
- provider: opts.provider,
2700
- model: opts.model,
2701
- results,
2702
- totalDurationMs: Date.now() - startTime,
2703
- dropParamsRequired,
2704
- timestamp: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
2705
- };
2706
- console.log("");
2707
- console.log(formatReport(report));
2708
- const failed = results.filter((r) => r.status === "fail").length;
2709
- process.exit(failed > 0 ? 1 : 0);
2710
- } finally {
2711
- cleanup();
2712
- }
2713
- }
2714
- var TEST_PORT, TEST_URL, CONFIG_PATH;
2715
- var init_test_model_command = __esm({
2716
- "src/cli/test-model-command.ts"() {
2717
- "use strict";
2718
- init_logger();
2719
- init_litellm();
2720
- init_test_model_tests();
2721
- init_test_model_report();
2722
- TEST_PORT = 4099;
2723
- TEST_URL = `http://localhost:${TEST_PORT}`;
2724
- CONFIG_PATH = path12.join(os3.tmpdir(), "kody-test-model-config.yaml");
2725
- }
2726
- });
2727
-
2728
1559
  // src/ci/parse-inputs.ts
2729
1560
  var parse_inputs_exports = {};
2730
1561
  __export(parse_inputs_exports, {
@@ -2732,16 +1563,16 @@ __export(parse_inputs_exports, {
2732
1563
  runCiParse: () => runCiParse,
2733
1564
  writeOutputs: () => writeOutputs
2734
1565
  });
2735
- import * as fs14 from "fs";
1566
+ import * as fs12 from "fs";
2736
1567
  function generateTimestamp() {
2737
1568
  const now = /* @__PURE__ */ new Date();
2738
- const pad2 = (n) => String(n).padStart(2, "0");
1569
+ const pad = (n) => String(n).padStart(2, "0");
2739
1570
  const y = String(now.getFullYear()).slice(2);
2740
- const m = pad2(now.getMonth() + 1);
2741
- const d = pad2(now.getDate());
2742
- const H = pad2(now.getHours());
2743
- const M = pad2(now.getMinutes());
2744
- const S = pad2(now.getSeconds());
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());
2745
1576
  return `${y}${m}${d}-${H}${M}${S}`;
2746
1577
  }
2747
1578
  function parseCommentInputs() {
@@ -2893,40 +1724,40 @@ function parseCommentInputs() {
2893
1724
  trigger_type: "comment"
2894
1725
  };
2895
1726
  }
2896
- function writeOutputs(result2) {
1727
+ function writeOutputs(result) {
2897
1728
  const outputFile = process.env.GITHUB_OUTPUT;
2898
1729
  function output(key, value) {
2899
1730
  if (outputFile) {
2900
1731
  if (value.includes("\n")) {
2901
- fs14.appendFileSync(outputFile, `${key}<<KODY_EOF
1732
+ fs12.appendFileSync(outputFile, `${key}<<KODY_EOF
2902
1733
  ${value}
2903
1734
  KODY_EOF
2904
1735
  `);
2905
1736
  } else {
2906
- fs14.appendFileSync(outputFile, `${key}=${value}
1737
+ fs12.appendFileSync(outputFile, `${key}=${value}
2907
1738
  `);
2908
1739
  }
2909
1740
  }
2910
1741
  const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
2911
1742
  console.log(`${key}=${display}`);
2912
1743
  }
2913
- output("task_id", result2.task_id);
2914
- output("mode", result2.mode);
2915
- output("from_stage", result2.from_stage);
2916
- output("issue_number", result2.issue_number);
2917
- output("pr_number", result2.pr_number);
2918
- output("feedback", result2.feedback);
2919
- output("complexity", result2.complexity);
2920
- output("ci_run_id", result2.ci_run_id);
2921
- output("ticket_id", result2.ticket_id);
2922
- output("prd_file", result2.prd_file);
2923
- output("dry_run", result2.dry_run ? "true" : "false");
2924
- output("valid", result2.valid ? "true" : "false");
2925
- output("trigger_type", result2.trigger_type);
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);
2926
1757
  }
2927
1758
  function runCiParse() {
2928
- const result2 = parseCommentInputs();
2929
- writeOutputs(result2);
1759
+ const result = parseCommentInputs();
1760
+ writeOutputs(result);
2930
1761
  }
2931
1762
  var VALID_MODES;
2932
1763
  var init_parse_inputs = __esm({
@@ -3028,7 +1859,7 @@ var init_definitions = __esm({
3028
1859
  });
3029
1860
 
3030
1861
  // src/git-utils.ts
3031
- import { execFileSync as execFileSync11 } from "child_process";
1862
+ import { execFileSync as execFileSync10 } from "child_process";
3032
1863
  function getHookSafeEnv() {
3033
1864
  if (!_hookSafeEnv) {
3034
1865
  _hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
@@ -3036,7 +1867,7 @@ function getHookSafeEnv() {
3036
1867
  return _hookSafeEnv;
3037
1868
  }
3038
1869
  function git(args2, options) {
3039
- return execFileSync11("git", args2, {
1870
+ return execFileSync10("git", args2, {
3040
1871
  encoding: "utf-8",
3041
1872
  timeout: options?.timeout ?? 3e4,
3042
1873
  cwd: options?.cwd,
@@ -3222,22 +2053,22 @@ var init_git_utils = __esm({
3222
2053
  });
3223
2054
 
3224
2055
  // src/pipeline/state.ts
3225
- import * as fs15 from "fs";
3226
- import * as path13 from "path";
2056
+ import * as fs13 from "fs";
2057
+ import * as path11 from "path";
3227
2058
  function loadState(taskId, taskDir) {
3228
- const p = path13.join(taskDir, "status.json");
3229
- if (!fs15.existsSync(p)) return null;
2059
+ const p = path11.join(taskDir, "status.json");
2060
+ if (!fs13.existsSync(p)) return null;
3230
2061
  try {
3231
- const result2 = parseJsonSafe(
3232
- fs15.readFileSync(p, "utf-8"),
2062
+ const result = parseJsonSafe(
2063
+ fs13.readFileSync(p, "utf-8"),
3233
2064
  ["taskId", "state", "stages", "createdAt", "updatedAt"]
3234
2065
  );
3235
- if (!result2.ok) {
3236
- logger.warn(` Corrupt status.json: ${result2.error}`);
2066
+ if (!result.ok) {
2067
+ logger.warn(` Corrupt status.json: ${result.error}`);
3237
2068
  return null;
3238
2069
  }
3239
- if (result2.data.taskId !== taskId) return null;
3240
- return result2.data;
2070
+ if (result.data.taskId !== taskId) return null;
2071
+ return result.data;
3241
2072
  } catch {
3242
2073
  return null;
3243
2074
  }
@@ -3247,10 +2078,10 @@ function writeState(state, taskDir) {
3247
2078
  ...state,
3248
2079
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3249
2080
  };
3250
- const target = path13.join(taskDir, "status.json");
2081
+ const target = path11.join(taskDir, "status.json");
3251
2082
  const tmp = target + ".tmp";
3252
- fs15.writeFileSync(tmp, JSON.stringify(updated, null, 2));
3253
- fs15.renameSync(tmp, target);
2083
+ fs13.writeFileSync(tmp, JSON.stringify(updated, null, 2));
2084
+ fs13.renameSync(tmp, target);
3254
2085
  return updated;
3255
2086
  }
3256
2087
  function initState(taskId) {
@@ -3291,16 +2122,16 @@ var init_complexity = __esm({
3291
2122
  });
3292
2123
 
3293
2124
  // src/memory.ts
3294
- import * as fs16 from "fs";
3295
- import * as path14 from "path";
2125
+ import * as fs14 from "fs";
2126
+ import * as path12 from "path";
3296
2127
  function readProjectMemory(projectDir) {
3297
- const memoryDir = path14.join(projectDir, ".kody", "memory");
3298
- if (!fs16.existsSync(memoryDir)) return "";
3299
- const files = fs16.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
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();
3300
2131
  if (files.length === 0) return "";
3301
2132
  const sections = [];
3302
2133
  for (const file of files) {
3303
- const content = fs16.readFileSync(path14.join(memoryDir, file), "utf-8").trim();
2134
+ const content = fs14.readFileSync(path12.join(memoryDir, file), "utf-8").trim();
3304
2135
  if (content) {
3305
2136
  sections.push(`## ${file.replace(".md", "")}
3306
2137
  ${content}`);
@@ -3319,8 +2150,8 @@ var init_memory = __esm({
3319
2150
  });
3320
2151
 
3321
2152
  // src/context-tiers.ts
3322
- import * as fs17 from "fs";
3323
- import * as path15 from "path";
2153
+ import * as fs15 from "fs";
2154
+ import * as path13 from "path";
3324
2155
  function estimateTokens(text) {
3325
2156
  return Math.ceil(text.length / 4);
3326
2157
  }
@@ -3347,8 +2178,8 @@ function generateL0(content, filename) {
3347
2178
  break;
3348
2179
  }
3349
2180
  }
3350
- const result2 = parts.join("\n");
3351
- return result2.slice(0, L0_MAX_CHARS);
2181
+ const result = parts.join("\n");
2182
+ return result.slice(0, L0_MAX_CHARS);
3352
2183
  }
3353
2184
  function generateL0Json(content) {
3354
2185
  try {
@@ -3390,8 +2221,8 @@ function generateL1(content, filename) {
3390
2221
  inSection = false;
3391
2222
  }
3392
2223
  }
3393
- const result2 = parts.join("\n");
3394
- return result2.slice(0, L1_MAX_CHARS);
2224
+ const result = parts.join("\n");
2225
+ return result.slice(0, L1_MAX_CHARS);
3395
2226
  }
3396
2227
  function generateL1Json(content) {
3397
2228
  try {
@@ -3411,7 +2242,7 @@ function generateL1Json(content) {
3411
2242
  }
3412
2243
  }
3413
2244
  function getTieredContent(filePath, content) {
3414
- const key = path15.basename(filePath);
2245
+ const key = path13.basename(filePath);
3415
2246
  return {
3416
2247
  source: filePath,
3417
2248
  L0: generateL0(content, key),
@@ -3423,15 +2254,15 @@ function selectTier(tiered, tier) {
3423
2254
  return tiered[tier];
3424
2255
  }
3425
2256
  function readProjectMemoryTiered(projectDir, tier) {
3426
- const memoryDir = path15.join(projectDir, ".kody", "memory");
3427
- if (!fs17.existsSync(memoryDir)) return "";
3428
- const files = fs17.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
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();
3429
2260
  if (files.length === 0) return "";
3430
2261
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
3431
2262
  const sections = [];
3432
2263
  for (const file of files) {
3433
- const filePath = path15.join(memoryDir, file);
3434
- const content = fs17.readFileSync(filePath, "utf-8").trim();
2264
+ const filePath = path13.join(memoryDir, file);
2265
+ const content = fs15.readFileSync(filePath, "utf-8").trim();
3435
2266
  if (!content) continue;
3436
2267
  const tiered = getTieredContent(filePath, content);
3437
2268
  const selected = selectTier(tiered, tier);
@@ -3454,9 +2285,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
3454
2285
  `;
3455
2286
  context += `Task Directory: ${taskDir}
3456
2287
  `;
3457
- const taskMdPath = path15.join(taskDir, "task.md");
3458
- if (fs17.existsSync(taskMdPath)) {
3459
- const content = fs17.readFileSync(taskMdPath, "utf-8");
2288
+ const taskMdPath = path13.join(taskDir, "task.md");
2289
+ if (fs15.existsSync(taskMdPath)) {
2290
+ const content = fs15.readFileSync(taskMdPath, "utf-8");
3460
2291
  const selected = selectContent(taskMdPath, content, policy.taskDescription);
3461
2292
  const label = tierLabel("Task Description", policy.taskDescription);
3462
2293
  context += `
@@ -3464,9 +2295,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
3464
2295
  ${selected}
3465
2296
  `;
3466
2297
  }
3467
- const taskJsonPath = path15.join(taskDir, "task.json");
3468
- if (fs17.existsSync(taskJsonPath)) {
3469
- const content = fs17.readFileSync(taskJsonPath, "utf-8");
2298
+ const taskJsonPath = path13.join(taskDir, "task.json");
2299
+ if (fs15.existsSync(taskJsonPath)) {
2300
+ const content = fs15.readFileSync(taskJsonPath, "utf-8");
3470
2301
  if (policy.taskClassification === "L2") {
3471
2302
  try {
3472
2303
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -3492,9 +2323,9 @@ ${selected}
3492
2323
  }
3493
2324
  }
3494
2325
  }
3495
- const specPath = path15.join(taskDir, "spec.md");
3496
- if (fs17.existsSync(specPath)) {
3497
- const content = fs17.readFileSync(specPath, "utf-8");
2326
+ const specPath = path13.join(taskDir, "spec.md");
2327
+ if (fs15.existsSync(specPath)) {
2328
+ const content = fs15.readFileSync(specPath, "utf-8");
3498
2329
  const selected = selectContent(specPath, content, policy.spec);
3499
2330
  const label = tierLabel("Spec", policy.spec);
3500
2331
  context += `
@@ -3502,9 +2333,9 @@ ${selected}
3502
2333
  ${selected}
3503
2334
  `;
3504
2335
  }
3505
- const planPath = path15.join(taskDir, "plan.md");
3506
- if (fs17.existsSync(planPath)) {
3507
- const content = fs17.readFileSync(planPath, "utf-8");
2336
+ const planPath = path13.join(taskDir, "plan.md");
2337
+ if (fs15.existsSync(planPath)) {
2338
+ const content = fs15.readFileSync(planPath, "utf-8");
3508
2339
  const selected = selectContent(planPath, content, policy.plan);
3509
2340
  const label = tierLabel("Plan", policy.plan);
3510
2341
  context += `
@@ -3512,9 +2343,9 @@ ${selected}
3512
2343
  ${selected}
3513
2344
  `;
3514
2345
  }
3515
- const contextMdPath = path15.join(taskDir, "context.md");
3516
- if (fs17.existsSync(contextMdPath)) {
3517
- const content = fs17.readFileSync(contextMdPath, "utf-8");
2346
+ const contextMdPath = path13.join(taskDir, "context.md");
2347
+ if (fs15.existsSync(contextMdPath)) {
2348
+ const content = fs15.readFileSync(contextMdPath, "utf-8");
3518
2349
  const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
3519
2350
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
3520
2351
  context += `
@@ -3600,24 +2431,24 @@ var init_context_tiers = __esm({
3600
2431
  });
3601
2432
 
3602
2433
  // src/context.ts
3603
- import * as fs18 from "fs";
3604
- import * as path16 from "path";
2434
+ import * as fs16 from "fs";
2435
+ import * as path14 from "path";
3605
2436
  function readPromptFile(stageName, projectDir) {
3606
2437
  if (projectDir) {
3607
- const stepFile = path16.join(projectDir, ".kody", "steps", `${stageName}.md`);
3608
- if (fs18.existsSync(stepFile)) {
3609
- return fs18.readFileSync(stepFile, "utf-8");
2438
+ const stepFile = path14.join(projectDir, ".kody", "steps", `${stageName}.md`);
2439
+ if (fs16.existsSync(stepFile)) {
2440
+ return fs16.readFileSync(stepFile, "utf-8");
3610
2441
  }
3611
2442
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
3612
2443
  }
3613
2444
  const scriptDir = new URL(".", import.meta.url).pathname;
3614
2445
  const candidates = [
3615
- path16.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
3616
- path16.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
2446
+ path14.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
2447
+ path14.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
3617
2448
  ];
3618
2449
  for (const candidate of candidates) {
3619
- if (fs18.existsSync(candidate)) {
3620
- return fs18.readFileSync(candidate, "utf-8");
2450
+ if (fs16.existsSync(candidate)) {
2451
+ return fs16.readFileSync(candidate, "utf-8");
3621
2452
  }
3622
2453
  }
3623
2454
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -3629,18 +2460,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
3629
2460
  `;
3630
2461
  context += `Task Directory: ${taskDir}
3631
2462
  `;
3632
- const taskMdPath = path16.join(taskDir, "task.md");
3633
- if (fs18.existsSync(taskMdPath)) {
3634
- const taskMd = fs18.readFileSync(taskMdPath, "utf-8");
2463
+ const taskMdPath = path14.join(taskDir, "task.md");
2464
+ if (fs16.existsSync(taskMdPath)) {
2465
+ const taskMd = fs16.readFileSync(taskMdPath, "utf-8");
3635
2466
  context += `
3636
2467
  ## Task Description
3637
2468
  ${taskMd}
3638
2469
  `;
3639
2470
  }
3640
- const taskJsonPath = path16.join(taskDir, "task.json");
3641
- if (fs18.existsSync(taskJsonPath)) {
2471
+ const taskJsonPath = path14.join(taskDir, "task.json");
2472
+ if (fs16.existsSync(taskJsonPath)) {
3642
2473
  try {
3643
- const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
2474
+ const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
3644
2475
  context += `
3645
2476
  ## Task Classification
3646
2477
  `;
@@ -3653,27 +2484,27 @@ ${taskMd}
3653
2484
  } catch {
3654
2485
  }
3655
2486
  }
3656
- const specPath = path16.join(taskDir, "spec.md");
3657
- if (fs18.existsSync(specPath)) {
3658
- const spec = fs18.readFileSync(specPath, "utf-8");
2487
+ const specPath = path14.join(taskDir, "spec.md");
2488
+ if (fs16.existsSync(specPath)) {
2489
+ const spec = fs16.readFileSync(specPath, "utf-8");
3659
2490
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
3660
2491
  context += `
3661
2492
  ## Spec Summary
3662
2493
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
3663
2494
  `;
3664
2495
  }
3665
- const planPath = path16.join(taskDir, "plan.md");
3666
- if (fs18.existsSync(planPath)) {
3667
- const plan = fs18.readFileSync(planPath, "utf-8");
2496
+ const planPath = path14.join(taskDir, "plan.md");
2497
+ if (fs16.existsSync(planPath)) {
2498
+ const plan = fs16.readFileSync(planPath, "utf-8");
3668
2499
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
3669
2500
  context += `
3670
2501
  ## Plan Summary
3671
2502
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
3672
2503
  `;
3673
2504
  }
3674
- const contextMdPath = path16.join(taskDir, "context.md");
3675
- if (fs18.existsSync(contextMdPath)) {
3676
- const accumulated = fs18.readFileSync(contextMdPath, "utf-8");
2505
+ const contextMdPath = path14.join(taskDir, "context.md");
2506
+ if (fs16.existsSync(contextMdPath)) {
2507
+ const accumulated = fs16.readFileSync(contextMdPath, "utf-8");
3677
2508
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
3678
2509
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
3679
2510
  context += `
@@ -3691,17 +2522,17 @@ ${feedback}
3691
2522
  }
3692
2523
  function inferHasUIFromScope(scope) {
3693
2524
  return scope.some((filePath) => {
3694
- const ext = path16.extname(filePath).toLowerCase();
2525
+ const ext = path14.extname(filePath).toLowerCase();
3695
2526
  if (UI_EXTENSIONS.has(ext)) return true;
3696
2527
  const normalized = filePath.replace(/\\/g, "/");
3697
2528
  return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
3698
2529
  });
3699
2530
  }
3700
2531
  function taskHasUI(taskDir) {
3701
- const taskJsonPath = path16.join(taskDir, "task.json");
3702
- if (!fs18.existsSync(taskJsonPath)) return true;
2532
+ const taskJsonPath = path14.join(taskDir, "task.json");
2533
+ if (!fs16.existsSync(taskJsonPath)) return true;
3703
2534
  try {
3704
- const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
2535
+ const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
3705
2536
  const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
3706
2537
  if (scope.length === 0) return true;
3707
2538
  return inferHasUIFromScope(scope);
@@ -3823,9 +2654,9 @@ ${prompt}` : prompt;
3823
2654
  }
3824
2655
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
3825
2656
  assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
3826
- const qaGuidePath = path16.join(projectDir, ".kody", "qa-guide.md");
3827
- if (fs18.existsSync(qaGuidePath)) {
3828
- const qaGuide = fs18.readFileSync(qaGuidePath, "utf-8").trim();
2657
+ const qaGuidePath = path14.join(projectDir, ".kody", "qa-guide.md");
2658
+ if (fs16.existsSync(qaGuidePath)) {
2659
+ const qaGuide = fs16.readFileSync(qaGuidePath, "utf-8").trim();
3829
2660
  assembled = assembled + "\n\n" + qaGuide;
3830
2661
  }
3831
2662
  }
@@ -3855,12 +2686,10 @@ function escalateModelTier(currentTier) {
3855
2686
  function resolveModel(modelTier, stageName) {
3856
2687
  const config = getProjectConfig();
3857
2688
  const mapped = config.agent.modelMap[modelTier];
3858
- if (!mapped) {
3859
- throw new Error(`No model configured for tier '${modelTier}'. Set agent.modelMap.${modelTier} in kody.config.json`);
3860
- }
3861
- return mapped;
2689
+ if (mapped) return mapped;
2690
+ return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
3862
2691
  }
3863
- var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
2692
+ var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION, DEFAULT_MODEL_MAP;
3864
2693
  var init_context = __esm({
3865
2694
  "src/context.ts"() {
3866
2695
  "use strict";
@@ -3894,6 +2723,11 @@ var init_context = __esm({
3894
2723
  mid: "strong",
3895
2724
  strong: "strong"
3896
2725
  };
2726
+ DEFAULT_MODEL_MAP = {
2727
+ cheap: "haiku",
2728
+ mid: "sonnet",
2729
+ strong: "opus"
2730
+ };
3897
2731
  }
3898
2732
  });
3899
2733
 
@@ -3917,8 +2751,8 @@ var init_runner_selection = __esm({
3917
2751
  });
3918
2752
 
3919
2753
  // src/stages/agent.ts
3920
- import * as fs19 from "fs";
3921
- import * as path17 from "path";
2754
+ import * as fs17 from "fs";
2755
+ import * as path15 from "path";
3922
2756
  function getSessionInfo(stageName, sessions) {
3923
2757
  const group = SESSION_GROUP[stageName];
3924
2758
  if (!group) return void 0;
@@ -4003,29 +2837,29 @@ async function executeAgentStage(ctx, def) {
4003
2837
  if (lastResult.outcome !== "completed") {
4004
2838
  return { outcome: lastResult.outcome, error: lastResult.error, retries };
4005
2839
  }
4006
- const result2 = lastResult;
4007
- if (def.outputFile && result2.output) {
4008
- fs19.writeFileSync(path17.join(ctx.taskDir, def.outputFile), result2.output);
2840
+ const result = lastResult;
2841
+ if (def.outputFile && result.output) {
2842
+ fs17.writeFileSync(path15.join(ctx.taskDir, def.outputFile), result.output);
4009
2843
  }
4010
2844
  if (def.outputFile) {
4011
- const outputPath = path17.join(ctx.taskDir, def.outputFile);
4012
- if (!fs19.existsSync(outputPath)) {
4013
- const ext = path17.extname(def.outputFile);
4014
- const base = path17.basename(def.outputFile, ext);
4015
- const files = fs19.readdirSync(ctx.taskDir);
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);
4016
2850
  const variant = files.find(
4017
2851
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
4018
2852
  );
4019
2853
  if (variant) {
4020
- fs19.renameSync(path17.join(ctx.taskDir, variant), outputPath);
2854
+ fs17.renameSync(path15.join(ctx.taskDir, variant), outputPath);
4021
2855
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
4022
2856
  }
4023
2857
  }
4024
2858
  }
4025
2859
  if (def.outputFile) {
4026
- const outputPath = path17.join(ctx.taskDir, def.outputFile);
4027
- if (fs19.existsSync(outputPath)) {
4028
- const content = fs19.readFileSync(outputPath, "utf-8");
2860
+ const outputPath = path15.join(ctx.taskDir, def.outputFile);
2861
+ if (fs17.existsSync(outputPath)) {
2862
+ const content = fs17.readFileSync(outputPath, "utf-8");
4029
2863
  const validation = validateStageOutput(def.name, content);
4030
2864
  if (!validation.valid) {
4031
2865
  if (def.name === "taskify") {
@@ -4039,7 +2873,7 @@ async function executeAgentStage(ctx, def) {
4039
2873
  const stripped = stripFences(retryResult.output);
4040
2874
  const retryValidation = validateTaskJson(stripped);
4041
2875
  if (retryValidation.valid) {
4042
- fs19.writeFileSync(outputPath, retryResult.output);
2876
+ fs17.writeFileSync(outputPath, retryResult.output);
4043
2877
  logger.info(` taskify retry produced valid JSON`);
4044
2878
  } else {
4045
2879
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -4052,7 +2886,7 @@ async function executeAgentStage(ctx, def) {
4052
2886
  risk_level: "low",
4053
2887
  questions: []
4054
2888
  }, null, 2);
4055
- fs19.writeFileSync(outputPath, fallback);
2889
+ fs17.writeFileSync(outputPath, fallback);
4056
2890
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
4057
2891
  }
4058
2892
  }
@@ -4062,11 +2896,11 @@ async function executeAgentStage(ctx, def) {
4062
2896
  }
4063
2897
  }
4064
2898
  }
4065
- appendStageContext(ctx.taskDir, def.name, result2.output);
2899
+ appendStageContext(ctx.taskDir, def.name, result.output);
4066
2900
  return { outcome: "completed", outputFile: def.outputFile, retries };
4067
2901
  }
4068
2902
  function appendStageContext(taskDir, stageName, output) {
4069
- const contextPath = path17.join(taskDir, "context.md");
2903
+ const contextPath = path15.join(taskDir, "context.md");
4070
2904
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
4071
2905
  let summary;
4072
2906
  if (output && output.trim()) {
@@ -4079,7 +2913,7 @@ function appendStageContext(taskDir, stageName, output) {
4079
2913
  ### ${stageName} (${timestamp2})
4080
2914
  ${summary}
4081
2915
  `;
4082
- fs19.appendFileSync(contextPath, entry);
2916
+ fs17.appendFileSync(contextPath, entry);
4083
2917
  }
4084
2918
  var SESSION_GROUP;
4085
2919
  var init_agent = __esm({
@@ -4102,7 +2936,7 @@ var init_agent = __esm({
4102
2936
  });
4103
2937
 
4104
2938
  // src/verify-runner.ts
4105
- import { execFileSync as execFileSync12 } from "child_process";
2939
+ import { execFileSync as execFileSync11 } from "child_process";
4106
2940
  function isExecError(err) {
4107
2941
  return typeof err === "object" && err !== null;
4108
2942
  }
@@ -4138,7 +2972,7 @@ function runCommand(cmd, cwd, timeout) {
4138
2972
  return { success: true, output: "", timedOut: false };
4139
2973
  }
4140
2974
  try {
4141
- const output = execFileSync12(parts[0], parts.slice(1), {
2975
+ const output = execFileSync11(parts[0], parts.slice(1), {
4142
2976
  cwd,
4143
2977
  timeout,
4144
2978
  encoding: "utf-8",
@@ -4184,19 +3018,19 @@ function runQualityGates(taskDir, projectRoot) {
4184
3018
  for (const { name, cmd } of commands) {
4185
3019
  if (!cmd) continue;
4186
3020
  logger.info(` Running ${name}: ${cmd}`);
4187
- const result2 = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
4188
- if (result2.timedOut) {
3021
+ const result = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
3022
+ if (result.timedOut) {
4189
3023
  allErrors.push(`${name}: timed out after ${VERIFY_COMMAND_TIMEOUT_MS / 1e3}s`);
4190
3024
  allPass = false;
4191
3025
  continue;
4192
3026
  }
4193
- if (!result2.success) {
3027
+ if (!result.success) {
4194
3028
  allPass = false;
4195
- const errors = parseErrors(result2.output);
3029
+ const errors = parseErrors(result.output);
4196
3030
  allErrors.push(...errors.map((e) => `[${name}] ${e}`));
4197
- rawOutputs.push({ name, output: result2.output.slice(-3e3) });
3031
+ rawOutputs.push({ name, output: result.output.slice(-3e3) });
4198
3032
  }
4199
- allSummary.push(...extractSummary(result2.output, name));
3033
+ allSummary.push(...extractSummary(result.output, name));
4200
3034
  }
4201
3035
  return { pass: allPass, errors: allErrors, summary: allSummary, rawOutputs };
4202
3036
  }
@@ -4209,7 +3043,7 @@ var init_verify_runner = __esm({
4209
3043
  });
4210
3044
 
4211
3045
  // src/observer.ts
4212
- import { execFileSync as execFileSync13 } from "child_process";
3046
+ import { execFileSync as execFileSync12 } from "child_process";
4213
3047
  async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
4214
3048
  const context = [
4215
3049
  `Stage: ${stageName}`,
@@ -4223,17 +3057,17 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
4223
3057
  ].join("\n");
4224
3058
  const prompt = DIAGNOSIS_PROMPT + context;
4225
3059
  try {
4226
- const result2 = await runner.run(
3060
+ const result = await runner.run(
4227
3061
  "diagnosis",
4228
3062
  prompt,
4229
3063
  model,
4230
- 3e4,
4231
- // 30s timeout — this should be fast
3064
+ 9e4,
3065
+ // 90s timeout — MiniMax can be slow to respond
4232
3066
  "",
4233
3067
  options
4234
3068
  );
4235
- if (result2.outcome === "completed" && result2.output) {
4236
- const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
3069
+ if (result.outcome === "completed" && result.output) {
3070
+ const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
4237
3071
  const parseResult = parseJsonSafe(cleaned, ["classification"]);
4238
3072
  if (parseResult.ok) {
4239
3073
  const { data } = parseResult;
@@ -4260,6 +3094,29 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
4260
3094
  } catch (err) {
4261
3095
  logger.warn(` Diagnosis error: ${err instanceof Error ? err.message : err}`);
4262
3096
  }
3097
+ if (modifiedFiles.length > 0) {
3098
+ const errorLines = errorOutput.split("\n").filter(
3099
+ (l) => /error|Error|ERROR|failed|Failed|FAIL/i.test(l)
3100
+ );
3101
+ const errorFilePaths = errorLines.flatMap((line) => {
3102
+ const matches = line.match(/src\/[^\s(:]+\.[a-z]+/g);
3103
+ return matches ?? [];
3104
+ });
3105
+ if (errorFilePaths.length > 0) {
3106
+ const modifiedSet = new Set(modifiedFiles);
3107
+ const allPreExisting = errorFilePaths.every(
3108
+ (f) => !modifiedSet.has(f) && !modifiedFiles.some((m) => m.endsWith(f))
3109
+ );
3110
+ if (allPreExisting) {
3111
+ logger.warn(" Diagnosis fallback: all errors in unmodified files \u2192 pre-existing");
3112
+ return {
3113
+ classification: "pre-existing",
3114
+ reason: "All errors are in files not modified by the build stage",
3115
+ resolution: `The following files have pre-existing errors not introduced by this task: ${[...new Set(errorFilePaths)].join(", ")}`
3116
+ };
3117
+ }
3118
+ }
3119
+ }
4263
3120
  logger.warn(" Diagnosis failed \u2014 defaulting to retry");
4264
3121
  return {
4265
3122
  classification: "retry",
@@ -4269,13 +3126,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
4269
3126
  }
4270
3127
  function getModifiedFiles(projectDir) {
4271
3128
  try {
4272
- const staged = execFileSync13("git", ["diff", "--name-only", "--cached"], {
3129
+ const staged = execFileSync12("git", ["diff", "--name-only", "--cached"], {
4273
3130
  encoding: "utf-8",
4274
3131
  cwd: projectDir,
4275
3132
  timeout: 5e3,
4276
3133
  stdio: ["pipe", "pipe", "pipe"]
4277
3134
  }).trim();
4278
- const unstaged = execFileSync13("git", ["diff", "--name-only"], {
3135
+ const unstaged = execFileSync12("git", ["diff", "--name-only"], {
4279
3136
  encoding: "utf-8",
4280
3137
  cwd: projectDir,
4281
3138
  timeout: 5e3,
@@ -4318,8 +3175,8 @@ Error context:
4318
3175
  });
4319
3176
 
4320
3177
  // src/stages/gate.ts
4321
- import * as fs20 from "fs";
4322
- import * as path18 from "path";
3178
+ import * as fs18 from "fs";
3179
+ import * as path16 from "path";
4323
3180
  function executeGateStage(ctx, def) {
4324
3181
  if (ctx.input.dryRun) {
4325
3182
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -4362,7 +3219,7 @@ ${output}
4362
3219
  `);
4363
3220
  }
4364
3221
  }
4365
- fs20.writeFileSync(path18.join(ctx.taskDir, "verify.md"), lines.join(""));
3222
+ fs18.writeFileSync(path16.join(ctx.taskDir, "verify.md"), lines.join(""));
4366
3223
  return {
4367
3224
  outcome: verifyResult.pass ? "completed" : "failed",
4368
3225
  retries: 0
@@ -4377,9 +3234,9 @@ var init_gate = __esm({
4377
3234
  });
4378
3235
 
4379
3236
  // src/stages/verify.ts
4380
- import * as fs21 from "fs";
4381
- import * as path19 from "path";
4382
- import { execFileSync as execFileSync14 } from "child_process";
3237
+ import * as fs19 from "fs";
3238
+ import * as path17 from "path";
3239
+ import { execFileSync as execFileSync13 } from "child_process";
4383
3240
  async function executeVerifyWithAutofix(ctx, def) {
4384
3241
  const maxAttempts = def.maxRetries ?? 2;
4385
3242
  for (let attempt = 0; attempt <= maxAttempts; attempt++) {
@@ -4389,8 +3246,8 @@ async function executeVerifyWithAutofix(ctx, def) {
4389
3246
  return { ...gateResult, retries: attempt };
4390
3247
  }
4391
3248
  if (attempt < maxAttempts) {
4392
- const verifyPath = path19.join(ctx.taskDir, "verify.md");
4393
- const errorOutput = fs21.existsSync(verifyPath) ? fs21.readFileSync(verifyPath, "utf-8") : "Unknown error";
3249
+ const verifyPath = path17.join(ctx.taskDir, "verify.md");
3250
+ const errorOutput = fs19.existsSync(verifyPath) ? fs19.readFileSync(verifyPath, "utf-8") : "Unknown error";
4394
3251
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
4395
3252
  const defaultRunner = getRunnerForStage(ctx, "taskify");
4396
3253
  const diagConfig = getProjectConfig();
@@ -4433,7 +3290,7 @@ ${diagnosis.resolution}`);
4433
3290
  const parts = parseCommand(cmd);
4434
3291
  if (parts.length === 0) return;
4435
3292
  try {
4436
- execFileSync14(parts[0], parts.slice(1), {
3293
+ execFileSync13(parts[0], parts.slice(1), {
4437
3294
  stdio: "pipe",
4438
3295
  timeout: FIX_COMMAND_TIMEOUT_MS
4439
3296
  });
@@ -4486,8 +3343,8 @@ var init_verify = __esm({
4486
3343
  });
4487
3344
 
4488
3345
  // src/review-standalone.ts
4489
- import * as fs22 from "fs";
4490
- import * as path20 from "path";
3346
+ import * as fs20 from "fs";
3347
+ import * as path18 from "path";
4491
3348
  function resolveReviewTarget(input) {
4492
3349
  if (input.prs.length === 0) {
4493
3350
  return {
@@ -4511,8 +3368,8 @@ Or comment on the specific PR: \`@kody review\``
4511
3368
  }
4512
3369
  async function runStandaloneReview(input) {
4513
3370
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
4514
- const taskDir = path20.join(input.projectDir, ".kody", "tasks", taskId);
4515
- fs22.mkdirSync(taskDir, { recursive: true });
3371
+ const taskDir = path18.join(input.projectDir, ".kody", "tasks", taskId);
3372
+ fs20.mkdirSync(taskDir, { recursive: true });
4516
3373
  let diffInstruction = "";
4517
3374
  let filesChangedSection = "";
4518
3375
  if (input.baseBranch) {
@@ -4539,7 +3396,7 @@ ${fileList}`;
4539
3396
  const taskContent = `# ${input.prTitle}
4540
3397
 
4541
3398
  ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
4542
- fs22.writeFileSync(path20.join(taskDir, "task.md"), taskContent);
3399
+ fs20.writeFileSync(path18.join(taskDir, "task.md"), taskContent);
4543
3400
  const reviewDef = STAGES.find((s) => s.name === "review");
4544
3401
  const ctx = {
4545
3402
  taskId,
@@ -4553,18 +3410,18 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
4553
3410
  }
4554
3411
  };
4555
3412
  logger.info(`[review] standalone review for: ${input.prTitle}`);
4556
- const result2 = await executeAgentStage(ctx, reviewDef);
4557
- if (result2.outcome !== "completed") {
3413
+ const result = await executeAgentStage(ctx, reviewDef);
3414
+ if (result.outcome !== "completed") {
4558
3415
  return {
4559
3416
  outcome: "failed",
4560
3417
  taskDir,
4561
- error: result2.error ?? "Review stage failed"
3418
+ error: result.error ?? "Review stage failed"
4562
3419
  };
4563
3420
  }
4564
- const reviewPath = path20.join(taskDir, "review.md");
3421
+ const reviewPath = path18.join(taskDir, "review.md");
4565
3422
  let reviewContent;
4566
- if (fs22.existsSync(reviewPath)) {
4567
- reviewContent = fs22.readFileSync(reviewPath, "utf-8");
3423
+ if (fs20.existsSync(reviewPath)) {
3424
+ reviewContent = fs20.readFileSync(reviewPath, "utf-8");
4568
3425
  }
4569
3426
  return {
4570
3427
  outcome: "completed",
@@ -4604,8 +3461,8 @@ var init_review_standalone = __esm({
4604
3461
  });
4605
3462
 
4606
3463
  // src/stages/review.ts
4607
- import * as fs23 from "fs";
4608
- import * as path21 from "path";
3464
+ import * as fs21 from "fs";
3465
+ import * as path19 from "path";
4609
3466
  async function executeReviewWithFix(ctx, def) {
4610
3467
  if (ctx.input.dryRun) {
4611
3468
  return { outcome: "completed", retries: 0 };
@@ -4619,11 +3476,11 @@ async function executeReviewWithFix(ctx, def) {
4619
3476
  if (reviewResult.outcome !== "completed") {
4620
3477
  return reviewResult;
4621
3478
  }
4622
- const reviewFile = path21.join(ctx.taskDir, "review.md");
4623
- if (!fs23.existsSync(reviewFile)) {
3479
+ const reviewFile = path19.join(ctx.taskDir, "review.md");
3480
+ if (!fs21.existsSync(reviewFile)) {
4624
3481
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
4625
3482
  }
4626
- const content = fs23.readFileSync(reviewFile, "utf-8");
3483
+ const content = fs21.readFileSync(reviewFile, "utf-8");
4627
3484
  if (detectReviewVerdict(content) !== "fail") {
4628
3485
  return { ...reviewResult, retries: iteration };
4629
3486
  }
@@ -4652,15 +3509,15 @@ var init_review = __esm({
4652
3509
  });
4653
3510
 
4654
3511
  // src/stages/ship.ts
4655
- import * as fs24 from "fs";
4656
- import * as path22 from "path";
4657
- import { execFileSync as execFileSync15 } from "child_process";
3512
+ import * as fs22 from "fs";
3513
+ import * as path20 from "path";
3514
+ import { execFileSync as execFileSync14 } from "child_process";
4658
3515
  function buildPrBody(ctx) {
4659
3516
  const sections = [];
4660
- const taskJsonPath = path22.join(ctx.taskDir, "task.json");
4661
- if (fs24.existsSync(taskJsonPath)) {
3517
+ const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3518
+ if (fs22.existsSync(taskJsonPath)) {
4662
3519
  try {
4663
- const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3520
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
4664
3521
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
4665
3522
  const task = JSON.parse(cleaned);
4666
3523
  if (task.description) {
@@ -4679,9 +3536,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
4679
3536
  } catch {
4680
3537
  }
4681
3538
  }
4682
- const reviewPath = path22.join(ctx.taskDir, "review.md");
4683
- if (fs24.existsSync(reviewPath)) {
4684
- const review = fs24.readFileSync(reviewPath, "utf-8");
3539
+ const reviewPath = path20.join(ctx.taskDir, "review.md");
3540
+ if (fs22.existsSync(reviewPath)) {
3541
+ const review = fs22.readFileSync(reviewPath, "utf-8");
4685
3542
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
4686
3543
  if (summaryMatch) {
4687
3544
  const summary = summaryMatch[1].trim();
@@ -4698,14 +3555,14 @@ ${summary}`);
4698
3555
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
4699
3556
  }
4700
3557
  }
4701
- const verifyPath = path22.join(ctx.taskDir, "verify.md");
4702
- if (fs24.existsSync(verifyPath)) {
4703
- const verify = fs24.readFileSync(verifyPath, "utf-8");
3558
+ const verifyPath = path20.join(ctx.taskDir, "verify.md");
3559
+ if (fs22.existsSync(verifyPath)) {
3560
+ const verify = fs22.readFileSync(verifyPath, "utf-8");
4704
3561
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
4705
3562
  }
4706
- const planPath = path22.join(ctx.taskDir, "plan.md");
4707
- if (fs24.existsSync(planPath)) {
4708
- const plan = fs24.readFileSync(planPath, "utf-8").trim();
3563
+ const planPath = path20.join(ctx.taskDir, "plan.md");
3564
+ if (fs22.existsSync(planPath)) {
3565
+ const plan = fs22.readFileSync(planPath, "utf-8").trim();
4709
3566
  if (plan) {
4710
3567
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
4711
3568
  sections.push(`
@@ -4725,25 +3582,25 @@ Closes #${ctx.input.issueNumber}`);
4725
3582
  return sections.join("\n");
4726
3583
  }
4727
3584
  function executeShipStage(ctx, _def) {
4728
- const shipPath = path22.join(ctx.taskDir, "ship.md");
3585
+ const shipPath = path20.join(ctx.taskDir, "ship.md");
4729
3586
  if (ctx.input.dryRun) {
4730
- fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
3587
+ fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
4731
3588
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
4732
3589
  }
4733
3590
  if (ctx.input.local && !ctx.input.issueNumber) {
4734
- fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
3591
+ fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
4735
3592
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
4736
3593
  }
4737
3594
  try {
4738
3595
  const head = getCurrentBranch(ctx.projectDir);
4739
3596
  const base = getDefaultBranch(ctx.projectDir);
4740
3597
  try {
4741
- execFileSync15("git", ["add", ctx.taskDir], {
3598
+ execFileSync14("git", ["add", ctx.taskDir], {
4742
3599
  cwd: ctx.projectDir,
4743
3600
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
4744
3601
  stdio: "pipe"
4745
3602
  });
4746
- execFileSync15("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
3603
+ execFileSync14("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
4747
3604
  cwd: ctx.projectDir,
4748
3605
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
4749
3606
  stdio: "pipe"
@@ -4757,7 +3614,7 @@ function executeShipStage(ctx, _def) {
4757
3614
  let repo = config.github?.repo;
4758
3615
  if (!owner || !repo) {
4759
3616
  try {
4760
- const remoteUrl = execFileSync15("git", ["remote", "get-url", "origin"], {
3617
+ const remoteUrl = execFileSync14("git", ["remote", "get-url", "origin"], {
4761
3618
  encoding: "utf-8",
4762
3619
  cwd: ctx.projectDir
4763
3620
  }).trim();
@@ -4778,28 +3635,28 @@ function executeShipStage(ctx, _def) {
4778
3635
  chore: "chore"
4779
3636
  };
4780
3637
  let prefix = "chore";
4781
- const taskJsonPath = path22.join(ctx.taskDir, "task.json");
4782
- if (fs24.existsSync(taskJsonPath)) {
3638
+ const taskJsonPath = path20.join(ctx.taskDir, "task.json");
3639
+ if (fs22.existsSync(taskJsonPath)) {
4783
3640
  try {
4784
- const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3641
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
4785
3642
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
4786
3643
  const task = JSON.parse(cleaned);
4787
3644
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
4788
3645
  } catch {
4789
3646
  }
4790
3647
  }
4791
- const taskMdPath = path22.join(ctx.taskDir, "task.md");
4792
- if (fs24.existsSync(taskMdPath)) {
4793
- const content = fs24.readFileSync(taskMdPath, "utf-8");
3648
+ const taskMdPath = path20.join(ctx.taskDir, "task.md");
3649
+ if (fs22.existsSync(taskMdPath)) {
3650
+ const content = fs22.readFileSync(taskMdPath, "utf-8");
4794
3651
  const heading = content.split("\n").find((l) => l.startsWith("# "));
4795
3652
  if (heading) {
4796
3653
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
4797
3654
  }
4798
3655
  }
4799
3656
  if (title === "Update") {
4800
- if (fs24.existsSync(taskJsonPath)) {
3657
+ if (fs22.existsSync(taskJsonPath)) {
4801
3658
  try {
4802
- const raw = fs24.readFileSync(taskJsonPath, "utf-8");
3659
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
4803
3660
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
4804
3661
  const task = JSON.parse(cleaned);
4805
3662
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -4822,7 +3679,7 @@ function executeShipStage(ctx, _def) {
4822
3679
  } catch {
4823
3680
  }
4824
3681
  }
4825
- fs24.writeFileSync(shipPath, `# Ship
3682
+ fs22.writeFileSync(shipPath, `# Ship
4826
3683
 
4827
3684
  Updated existing PR: ${existingPr.url}
4828
3685
  PR #${existingPr.number}
@@ -4843,20 +3700,20 @@ PR #${existingPr.number}
4843
3700
  } catch {
4844
3701
  }
4845
3702
  }
4846
- fs24.writeFileSync(shipPath, `# Ship
3703
+ fs22.writeFileSync(shipPath, `# Ship
4847
3704
 
4848
3705
  PR created: ${pr.url}
4849
3706
  PR #${pr.number}
4850
3707
  `);
4851
3708
  } else {
4852
- fs24.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3709
+ fs22.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
4853
3710
  }
4854
3711
  }
4855
3712
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
4856
3713
  } catch (err) {
4857
3714
  const msg = err instanceof Error ? err.message : String(err);
4858
3715
  try {
4859
- fs24.writeFileSync(shipPath, `# Ship
3716
+ fs22.writeFileSync(shipPath, `# Ship
4860
3717
 
4861
3718
  Failed: ${msg}
4862
3719
  `);
@@ -4905,15 +3762,15 @@ var init_executor_registry = __esm({
4905
3762
  });
4906
3763
 
4907
3764
  // src/pipeline/questions.ts
4908
- import * as fs25 from "fs";
4909
- import * as path23 from "path";
3765
+ import * as fs23 from "fs";
3766
+ import * as path21 from "path";
4910
3767
  function checkForQuestions(ctx, stageName) {
4911
3768
  if (ctx.input.local || !ctx.input.issueNumber) return false;
4912
3769
  try {
4913
3770
  if (stageName === "taskify") {
4914
- const taskJsonPath = path23.join(ctx.taskDir, "task.json");
4915
- if (!fs25.existsSync(taskJsonPath)) return false;
4916
- const raw = fs25.readFileSync(taskJsonPath, "utf-8");
3771
+ const taskJsonPath = path21.join(ctx.taskDir, "task.json");
3772
+ if (!fs23.existsSync(taskJsonPath)) return false;
3773
+ const raw = fs23.readFileSync(taskJsonPath, "utf-8");
4917
3774
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
4918
3775
  const taskJson = JSON.parse(cleaned);
4919
3776
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -4928,9 +3785,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
4928
3785
  }
4929
3786
  }
4930
3787
  if (stageName === "plan") {
4931
- const planPath = path23.join(ctx.taskDir, "plan.md");
4932
- if (!fs25.existsSync(planPath)) return false;
4933
- const plan = fs25.readFileSync(planPath, "utf-8");
3788
+ const planPath = path21.join(ctx.taskDir, "plan.md");
3789
+ if (!fs23.existsSync(planPath)) return false;
3790
+ const plan = fs23.readFileSync(planPath, "utf-8");
4934
3791
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
4935
3792
  if (questionsMatch) {
4936
3793
  const questionsText = questionsMatch[1].trim();
@@ -4959,8 +3816,8 @@ var init_questions = __esm({
4959
3816
  });
4960
3817
 
4961
3818
  // src/pipeline/hooks.ts
4962
- import * as fs26 from "fs";
4963
- import * as path24 from "path";
3819
+ import * as fs24 from "fs";
3820
+ import * as path22 from "path";
4964
3821
  function applyPreStageLabel(ctx, def) {
4965
3822
  if (!ctx.input.issueNumber || ctx.input.local) return;
4966
3823
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
@@ -4998,9 +3855,9 @@ function autoDetectComplexity(ctx, def) {
4998
3855
  return { complexity, activeStages };
4999
3856
  }
5000
3857
  try {
5001
- const taskJsonPath = path24.join(ctx.taskDir, "task.json");
5002
- if (!fs26.existsSync(taskJsonPath)) return null;
5003
- const raw = fs26.readFileSync(taskJsonPath, "utf-8");
3858
+ const taskJsonPath = path22.join(ctx.taskDir, "task.json");
3859
+ if (!fs24.existsSync(taskJsonPath)) return null;
3860
+ const raw = fs24.readFileSync(taskJsonPath, "utf-8");
5004
3861
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
5005
3862
  const taskJson = JSON.parse(cleaned);
5006
3863
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -5030,8 +3887,8 @@ function checkRiskGate(ctx, def, state, complexity) {
5030
3887
  if (ctx.input.dryRun || ctx.input.local) return null;
5031
3888
  if (ctx.input.mode === "rerun") return null;
5032
3889
  if (!ctx.input.issueNumber) return null;
5033
- const planPath = path24.join(ctx.taskDir, "plan.md");
5034
- const plan = fs26.existsSync(planPath) ? fs26.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
3890
+ const planPath = path22.join(ctx.taskDir, "plan.md");
3891
+ const plan = fs24.existsSync(planPath) ? fs24.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
5035
3892
  try {
5036
3893
  postComment(
5037
3894
  ctx.input.issueNumber,
@@ -5098,22 +3955,22 @@ var init_hooks = __esm({
5098
3955
  });
5099
3956
 
5100
3957
  // src/learning/auto-learn.ts
5101
- import * as fs27 from "fs";
5102
- import * as path25 from "path";
3958
+ import * as fs25 from "fs";
3959
+ import * as path23 from "path";
5103
3960
  function stripAnsi(str) {
5104
3961
  return str.replace(/\x1b\[[0-9;]*m/g, "");
5105
3962
  }
5106
3963
  function autoLearn(ctx) {
5107
3964
  try {
5108
- const memoryDir = path25.join(ctx.projectDir, ".kody", "memory");
5109
- if (!fs27.existsSync(memoryDir)) {
5110
- fs27.mkdirSync(memoryDir, { recursive: true });
3965
+ const memoryDir = path23.join(ctx.projectDir, ".kody", "memory");
3966
+ if (!fs25.existsSync(memoryDir)) {
3967
+ fs25.mkdirSync(memoryDir, { recursive: true });
5111
3968
  }
5112
3969
  const learnings = [];
5113
3970
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5114
- const verifyPath = path25.join(ctx.taskDir, "verify.md");
5115
- if (fs27.existsSync(verifyPath)) {
5116
- const verify = stripAnsi(fs27.readFileSync(verifyPath, "utf-8"));
3971
+ const verifyPath = path23.join(ctx.taskDir, "verify.md");
3972
+ if (fs25.existsSync(verifyPath)) {
3973
+ const verify = stripAnsi(fs25.readFileSync(verifyPath, "utf-8"));
5117
3974
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
5118
3975
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
5119
3976
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -5122,18 +3979,18 @@ function autoLearn(ctx) {
5122
3979
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
5123
3980
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
5124
3981
  }
5125
- const reviewPath = path25.join(ctx.taskDir, "review.md");
5126
- if (fs27.existsSync(reviewPath)) {
5127
- const review = fs27.readFileSync(reviewPath, "utf-8");
3982
+ const reviewPath = path23.join(ctx.taskDir, "review.md");
3983
+ if (fs25.existsSync(reviewPath)) {
3984
+ const review = fs25.readFileSync(reviewPath, "utf-8");
5128
3985
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
5129
3986
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
5130
3987
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
5131
3988
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
5132
3989
  }
5133
- const taskJsonPath = path25.join(ctx.taskDir, "task.json");
5134
- if (fs27.existsSync(taskJsonPath)) {
3990
+ const taskJsonPath = path23.join(ctx.taskDir, "task.json");
3991
+ if (fs25.existsSync(taskJsonPath)) {
5135
3992
  try {
5136
- const raw = stripAnsi(fs27.readFileSync(taskJsonPath, "utf-8"));
3993
+ const raw = stripAnsi(fs25.readFileSync(taskJsonPath, "utf-8"));
5137
3994
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
5138
3995
  const task = JSON.parse(cleaned);
5139
3996
  if (task.scope && Array.isArray(task.scope)) {
@@ -5144,12 +4001,12 @@ function autoLearn(ctx) {
5144
4001
  }
5145
4002
  }
5146
4003
  if (learnings.length > 0) {
5147
- const conventionsPath = path25.join(memoryDir, "conventions.md");
4004
+ const conventionsPath = path23.join(memoryDir, "conventions.md");
5148
4005
  const entry = `
5149
4006
  ## Learned ${timestamp2} (task: ${ctx.taskId})
5150
4007
  ${learnings.join("\n")}
5151
4008
  `;
5152
- fs27.appendFileSync(conventionsPath, entry);
4009
+ fs25.appendFileSync(conventionsPath, entry);
5153
4010
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
5154
4011
  }
5155
4012
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
@@ -5157,8 +4014,8 @@ ${learnings.join("\n")}
5157
4014
  }
5158
4015
  }
5159
4016
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
5160
- const archPath = path25.join(memoryDir, "architecture.md");
5161
- if (fs27.existsSync(archPath)) return;
4017
+ const archPath = path23.join(memoryDir, "architecture.md");
4018
+ if (fs25.existsSync(archPath)) return;
5162
4019
  const detected = detectArchitectureBasic(projectDir);
5163
4020
  if (detected.length > 0) {
5164
4021
  const content = `# Architecture (auto-detected ${timestamp2})
@@ -5166,7 +4023,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
5166
4023
  ## Overview
5167
4024
  ${detected.join("\n")}
5168
4025
  `;
5169
- fs27.writeFileSync(archPath, content);
4026
+ fs25.writeFileSync(archPath, content);
5170
4027
  logger.info(`Auto-detected architecture (${detected.length} items)`);
5171
4028
  }
5172
4029
  }
@@ -5179,13 +4036,13 @@ var init_auto_learn = __esm({
5179
4036
  });
5180
4037
 
5181
4038
  // src/retrospective.ts
5182
- import * as fs28 from "fs";
5183
- import * as path26 from "path";
4039
+ import * as fs26 from "fs";
4040
+ import * as path24 from "path";
5184
4041
  function readArtifact(taskDir, filename, maxChars) {
5185
- const p = path26.join(taskDir, filename);
5186
- if (!fs28.existsSync(p)) return null;
4042
+ const p = path24.join(taskDir, filename);
4043
+ if (!fs26.existsSync(p)) return null;
5187
4044
  try {
5188
- const content = fs28.readFileSync(p, "utf-8");
4045
+ const content = fs26.readFileSync(p, "utf-8");
5189
4046
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
5190
4047
  } catch {
5191
4048
  return null;
@@ -5238,13 +4095,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
5238
4095
  return lines.join("\n");
5239
4096
  }
5240
4097
  function getLogPath(projectDir) {
5241
- return path26.join(projectDir, ".kody", "memory", "observer-log.jsonl");
4098
+ return path24.join(projectDir, ".kody", "memory", "observer-log.jsonl");
5242
4099
  }
5243
4100
  function readPreviousRetrospectives(projectDir, limit = 10) {
5244
4101
  const logPath = getLogPath(projectDir);
5245
- if (!fs28.existsSync(logPath)) return [];
4102
+ if (!fs26.existsSync(logPath)) return [];
5246
4103
  try {
5247
- const content = fs28.readFileSync(logPath, "utf-8");
4104
+ const content = fs26.readFileSync(logPath, "utf-8");
5248
4105
  const lines = content.split("\n").filter(Boolean);
5249
4106
  const entries = [];
5250
4107
  const start = Math.max(0, lines.length - limit);
@@ -5271,11 +4128,11 @@ function formatPreviousEntries(entries) {
5271
4128
  }
5272
4129
  function appendRetrospectiveEntry(projectDir, entry) {
5273
4130
  const logPath = getLogPath(projectDir);
5274
- const dir = path26.dirname(logPath);
5275
- if (!fs28.existsSync(dir)) {
5276
- fs28.mkdirSync(dir, { recursive: true });
4131
+ const dir = path24.dirname(logPath);
4132
+ if (!fs26.existsSync(dir)) {
4133
+ fs26.mkdirSync(dir, { recursive: true });
5277
4134
  }
5278
- fs28.appendFileSync(logPath, JSON.stringify(entry) + "\n");
4135
+ fs26.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5279
4136
  }
5280
4137
  async function runRetrospective(ctx, state, pipelineStartTime) {
5281
4138
  if (ctx.input.dryRun) return;
@@ -5297,7 +4154,7 @@ ${previousText}
5297
4154
  if (needsLitellmProxy(config)) {
5298
4155
  extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
5299
4156
  }
5300
- const result2 = await runner.run("retrospective", prompt, model, 3e4, "", {
4157
+ const result = await runner.run("retrospective", prompt, model, 3e4, "", {
5301
4158
  cwd: ctx.projectDir,
5302
4159
  env: extraEnv
5303
4160
  });
@@ -5305,8 +4162,8 @@ ${previousText}
5305
4162
  let patternMatch = null;
5306
4163
  let suggestion = "No suggestion";
5307
4164
  let pipelineFlaw = null;
5308
- if (result2.outcome === "completed" && result2.output) {
5309
- const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
4165
+ if (result.outcome === "completed" && result.output) {
4166
+ const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
5310
4167
  try {
5311
4168
  const parsed = JSON.parse(cleaned);
5312
4169
  observation = parsed.observation ?? observation;
@@ -5443,8 +4300,8 @@ var init_summary = __esm({
5443
4300
  });
5444
4301
 
5445
4302
  // src/pipeline.ts
5446
- import * as fs29 from "fs";
5447
- import * as path27 from "path";
4303
+ import * as fs27 from "fs";
4304
+ import * as path25 from "path";
5448
4305
  function ensureFeatureBranchIfNeeded(ctx) {
5449
4306
  if (ctx.input.dryRun) return;
5450
4307
  if (ctx.input.prNumber) {
@@ -5457,8 +4314,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
5457
4314
  }
5458
4315
  if (!ctx.input.issueNumber) return;
5459
4316
  try {
5460
- const taskMdPath = path27.join(ctx.taskDir, "task.md");
5461
- const title = fs29.existsSync(taskMdPath) ? fs29.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
4317
+ const taskMdPath = path25.join(ctx.taskDir, "task.md");
4318
+ const title = fs27.existsSync(taskMdPath) ? fs27.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
5462
4319
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
5463
4320
  syncWithDefault(ctx.projectDir);
5464
4321
  } catch (err) {
@@ -5472,10 +4329,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
5472
4329
  }
5473
4330
  }
5474
4331
  function acquireLock(taskDir) {
5475
- const lockPath = path27.join(taskDir, ".lock");
5476
- if (fs29.existsSync(lockPath)) {
4332
+ const lockPath = path25.join(taskDir, ".lock");
4333
+ if (fs27.existsSync(lockPath)) {
5477
4334
  try {
5478
- const pid = parseInt(fs29.readFileSync(lockPath, "utf-8").trim(), 10);
4335
+ const pid = parseInt(fs27.readFileSync(lockPath, "utf-8").trim(), 10);
5479
4336
  if (!isNaN(pid)) {
5480
4337
  try {
5481
4338
  process.kill(pid, 0);
@@ -5492,14 +4349,14 @@ function acquireLock(taskDir) {
5492
4349
  logger.warn(` Corrupt lock file \u2014 overwriting`);
5493
4350
  }
5494
4351
  try {
5495
- fs29.unlinkSync(lockPath);
4352
+ fs27.unlinkSync(lockPath);
5496
4353
  } catch {
5497
4354
  }
5498
4355
  }
5499
4356
  try {
5500
- const fd = fs29.openSync(lockPath, fs29.constants.O_WRONLY | fs29.constants.O_CREAT | fs29.constants.O_EXCL);
5501
- fs29.writeSync(fd, String(process.pid));
5502
- fs29.closeSync(fd);
4357
+ const fd = fs27.openSync(lockPath, fs27.constants.O_WRONLY | fs27.constants.O_CREAT | fs27.constants.O_EXCL);
4358
+ fs27.writeSync(fd, String(process.pid));
4359
+ fs27.closeSync(fd);
5503
4360
  } catch (err) {
5504
4361
  if (err.code === "EEXIST") {
5505
4362
  throw new Error("Pipeline already running (lock acquired by another process)");
@@ -5509,7 +4366,7 @@ function acquireLock(taskDir) {
5509
4366
  }
5510
4367
  function releaseLock(taskDir) {
5511
4368
  try {
5512
- fs29.unlinkSync(path27.join(taskDir, ".lock"));
4369
+ fs27.unlinkSync(path25.join(taskDir, ".lock"));
5513
4370
  } catch {
5514
4371
  }
5515
4372
  }
@@ -5598,23 +4455,23 @@ async function runPipelineInner(ctx) {
5598
4455
  writeState(state, ctx.taskDir);
5599
4456
  logger.info(`[${def.name}] starting...`);
5600
4457
  applyPreStageLabel(ctx, def);
5601
- let result2;
4458
+ let result;
5602
4459
  try {
5603
- result2 = await getExecutor(def.name)(ctx, def);
4460
+ result = await getExecutor(def.name)(ctx, def);
5604
4461
  } catch (error) {
5605
- result2 = {
4462
+ result = {
5606
4463
  outcome: "failed",
5607
4464
  retries: 0,
5608
4465
  error: error instanceof Error ? error.message : String(error)
5609
4466
  };
5610
4467
  }
5611
4468
  ciGroupEnd();
5612
- if (result2.outcome === "completed") {
4469
+ if (result.outcome === "completed") {
5613
4470
  state.stages[def.name] = {
5614
4471
  state: "completed",
5615
4472
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
5616
- retries: result2.retries,
5617
- outputFile: result2.outputFile
4473
+ retries: result.retries,
4474
+ outputFile: result.outputFile
5618
4475
  };
5619
4476
  logger.info(`[${def.name}] \u2713 completed`);
5620
4477
  const detected = autoDetectComplexity(ctx, def);
@@ -5628,16 +4485,16 @@ async function runPipelineInner(ctx) {
5628
4485
  if (gated) return gated;
5629
4486
  commitAfterStage(ctx, def);
5630
4487
  } else {
5631
- const isTimeout = result2.outcome === "timed_out";
4488
+ const isTimeout = result.outcome === "timed_out";
5632
4489
  state.stages[def.name] = {
5633
4490
  state: isTimeout ? "timeout" : "failed",
5634
- retries: result2.retries,
5635
- error: isTimeout ? "Stage timed out" : result2.error ?? "Stage failed"
4491
+ retries: result.retries,
4492
+ error: isTimeout ? "Stage timed out" : result.error ?? "Stage failed"
5636
4493
  };
5637
4494
  state.state = "failed";
5638
4495
  state.sessions = ctx.sessions;
5639
4496
  writeState(state, ctx.taskDir);
5640
- logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result2.error}`}`);
4497
+ logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result.error}`}`);
5641
4498
  if (ctx.input.issueNumber && !ctx.input.local) {
5642
4499
  setLifecycleLabel(ctx.input.issueNumber, "failed");
5643
4500
  }
@@ -5717,8 +4574,8 @@ var init_pipeline = __esm({
5717
4574
  });
5718
4575
 
5719
4576
  // src/preflight.ts
5720
- import { execFileSync as execFileSync16 } from "child_process";
5721
- import * as fs30 from "fs";
4577
+ import { execFileSync as execFileSync15 } from "child_process";
4578
+ import * as fs28 from "fs";
5722
4579
  function check(name, fn) {
5723
4580
  try {
5724
4581
  const detail = fn() ?? void 0;
@@ -5730,7 +4587,7 @@ function check(name, fn) {
5730
4587
  function runPreflight() {
5731
4588
  const checks = [
5732
4589
  check("claude CLI", () => {
5733
- const v = execFileSync16("claude", ["--version"], {
4590
+ const v = execFileSync15("claude", ["--version"], {
5734
4591
  encoding: "utf-8",
5735
4592
  timeout: 1e4,
5736
4593
  stdio: ["pipe", "pipe", "pipe"]
@@ -5738,14 +4595,14 @@ function runPreflight() {
5738
4595
  return v;
5739
4596
  }),
5740
4597
  check("git repo", () => {
5741
- execFileSync16("git", ["rev-parse", "--is-inside-work-tree"], {
4598
+ execFileSync15("git", ["rev-parse", "--is-inside-work-tree"], {
5742
4599
  encoding: "utf-8",
5743
4600
  timeout: 5e3,
5744
4601
  stdio: ["pipe", "pipe", "pipe"]
5745
4602
  });
5746
4603
  }),
5747
4604
  check("pnpm", () => {
5748
- const v = execFileSync16("pnpm", ["--version"], {
4605
+ const v = execFileSync15("pnpm", ["--version"], {
5749
4606
  encoding: "utf-8",
5750
4607
  timeout: 5e3,
5751
4608
  stdio: ["pipe", "pipe", "pipe"]
@@ -5753,7 +4610,7 @@ function runPreflight() {
5753
4610
  return v;
5754
4611
  }),
5755
4612
  check("node >= 18", () => {
5756
- const v = execFileSync16("node", ["--version"], {
4613
+ const v = execFileSync15("node", ["--version"], {
5757
4614
  encoding: "utf-8",
5758
4615
  timeout: 5e3,
5759
4616
  stdio: ["pipe", "pipe", "pipe"]
@@ -5763,7 +4620,7 @@ function runPreflight() {
5763
4620
  return v;
5764
4621
  }),
5765
4622
  check("gh CLI", () => {
5766
- const v = execFileSync16("gh", ["--version"], {
4623
+ const v = execFileSync15("gh", ["--version"], {
5767
4624
  encoding: "utf-8",
5768
4625
  timeout: 5e3,
5769
4626
  stdio: ["pipe", "pipe", "pipe"]
@@ -5771,7 +4628,7 @@ function runPreflight() {
5771
4628
  return v;
5772
4629
  }),
5773
4630
  check("package.json", () => {
5774
- if (!fs30.existsSync("package.json")) throw new Error("not found");
4631
+ if (!fs28.existsSync("package.json")) throw new Error("not found");
5775
4632
  })
5776
4633
  ];
5777
4634
  const failed = checks.filter((c) => !c.ok);
@@ -5848,8 +4705,8 @@ var init_args = __esm({
5848
4705
  });
5849
4706
 
5850
4707
  // src/cli/task-state.ts
5851
- import * as fs31 from "fs";
5852
- import * as path28 from "path";
4708
+ import * as fs29 from "fs";
4709
+ import * as path26 from "path";
5853
4710
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
5854
4711
  if (!existingTaskId || !existingState) {
5855
4712
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -5881,11 +4738,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
5881
4738
  function resolveForIssue(issueNumber, projectDir) {
5882
4739
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
5883
4740
  if (existingTaskId) {
5884
- const statusPath = path28.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
4741
+ const statusPath = path26.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
5885
4742
  let existingState = null;
5886
- if (fs31.existsSync(statusPath)) {
4743
+ if (fs29.existsSync(statusPath)) {
5887
4744
  try {
5888
- existingState = JSON.parse(fs31.readFileSync(statusPath, "utf-8"));
4745
+ existingState = JSON.parse(fs29.readFileSync(statusPath, "utf-8"));
5889
4746
  } catch {
5890
4747
  }
5891
4748
  }
@@ -5918,12 +4775,12 @@ var resolve_exports = {};
5918
4775
  __export(resolve_exports, {
5919
4776
  runResolve: () => runResolve
5920
4777
  });
5921
- import { execFileSync as execFileSync17 } from "child_process";
4778
+ import { execFileSync as execFileSync16 } from "child_process";
5922
4779
  function getConflictContext(cwd, files) {
5923
4780
  const parts = [];
5924
4781
  for (const file of files.slice(0, 10)) {
5925
4782
  try {
5926
- const content = execFileSync17("git", ["diff", file], {
4783
+ const content = execFileSync16("git", ["diff", file], {
5927
4784
  cwd,
5928
4785
  encoding: "utf-8",
5929
4786
  stdio: ["pipe", "pipe", "pipe"]
@@ -5973,12 +4830,12 @@ async function runResolve(options) {
5973
4830
  extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
5974
4831
  }
5975
4832
  logger.info(` Running agent to resolve conflicts (model=${model})...`);
5976
- const result2 = await runner.run("resolve", prompt, model, 3e5, projectDir, {
4833
+ const result = await runner.run("resolve", prompt, model, 3e5, projectDir, {
5977
4834
  cwd: projectDir,
5978
4835
  env: extraEnv
5979
4836
  });
5980
- if (result2.outcome !== "completed") {
5981
- return { outcome: "failed", error: `Agent failed: ${result2.error}` };
4837
+ if (result.outcome !== "completed") {
4838
+ return { outcome: "failed", error: `Agent failed: ${result.error}` };
5982
4839
  }
5983
4840
  logger.info(" Verifying resolution...");
5984
4841
  const verify = runQualityGates(projectDir, projectDir);
@@ -6042,8 +4899,8 @@ var init_resolve = __esm({
6042
4899
 
6043
4900
  // src/entry.ts
6044
4901
  var entry_exports = {};
6045
- import * as fs32 from "fs";
6046
- import * as path29 from "path";
4902
+ import * as fs30 from "fs";
4903
+ import * as path27 from "path";
6047
4904
  async function ensureLitellmProxy(config, projectDir) {
6048
4905
  if (!anyStageNeedsProxy(config)) return null;
6049
4906
  const litellmUrl = getLitellmUrl();
@@ -6078,7 +4935,7 @@ async function ensureLitellmProxy(config, projectDir) {
6078
4935
  return litellmProcess;
6079
4936
  }
6080
4937
  async function runModelHealthCheck(config) {
6081
- const usesProxy = anyStageNeedsProxy(config);
4938
+ const usesProxy = needsLitellmProxy(config);
6082
4939
  const baseUrl = usesProxy ? getLitellmUrl() : "https://api.anthropic.com";
6083
4940
  const apiKey = usesProxy ? process.env.ANTHROPIC_COMPATIBLE_API_KEY : process.env.ANTHROPIC_API_KEY;
6084
4941
  if (!apiKey) {
@@ -6088,19 +4945,19 @@ async function runModelHealthCheck(config) {
6088
4945
  }
6089
4946
  const model = config.agent.modelMap.cheap;
6090
4947
  logger.info(`Model health check (${model} via ${usesProxy ? "LiteLLM" : "Anthropic"})...`);
6091
- const result2 = await checkModelHealth(baseUrl, apiKey, model);
6092
- if (result2.ok) {
4948
+ const result = await checkModelHealth(baseUrl, apiKey, model);
4949
+ if (result.ok) {
6093
4950
  logger.info(" \u2713 Model responded");
6094
4951
  } else {
6095
- logger.error(` \u2717 Model health check failed: ${result2.error}`);
4952
+ logger.error(` \u2717 Model health check failed: ${result.error}`);
6096
4953
  process.exit(1);
6097
4954
  }
6098
4955
  }
6099
4956
  async function main() {
6100
4957
  const input = parseArgs();
6101
- const projectDir = input.cwd ? path29.resolve(input.cwd) : process.cwd();
4958
+ const projectDir = input.cwd ? path27.resolve(input.cwd) : process.cwd();
6102
4959
  if (input.cwd) {
6103
- if (!fs32.existsSync(projectDir)) {
4960
+ if (!fs30.existsSync(projectDir)) {
6104
4961
  console.error(`--cwd path does not exist: ${projectDir}`);
6105
4962
  process.exit(1);
6106
4963
  }
@@ -6109,7 +4966,7 @@ async function main() {
6109
4966
  logger.info(`Working directory: ${projectDir}`);
6110
4967
  }
6111
4968
  const isPRFix = (input.command === "fix" || input.command === "fix-ci") && !!input.prNumber;
6112
- const skipStateCheck = input.command === "review" || input.command === "resolve" || input.command === "rerun" || input.command === "status";
4969
+ const skipStateCheck = input.command === "review" || input.command === "resolve" || input.command === "rerun";
6113
4970
  if (input.issueNumber && !skipStateCheck && !isPRFix) {
6114
4971
  const taskAction = resolveForIssue(input.issueNumber, projectDir);
6115
4972
  logger.info(`Task action: ${taskAction.action}`);
@@ -6166,8 +5023,8 @@ async function main() {
6166
5023
  process.exit(1);
6167
5024
  }
6168
5025
  }
6169
- const taskDir = path29.join(projectDir, ".kody", "tasks", taskId);
6170
- fs32.mkdirSync(taskDir, { recursive: true });
5026
+ const taskDir = path27.join(projectDir, ".kody", "tasks", taskId);
5027
+ fs30.mkdirSync(taskDir, { recursive: true });
6171
5028
  if (input.command === "rerun" && isTaskifyRun(taskDir)) {
6172
5029
  const marker = readTaskifyMarker(taskDir);
6173
5030
  if (marker) {
@@ -6232,7 +5089,7 @@ async function main() {
6232
5089
  console.error(`Runner "${defaultRunnerName2}" health check failed`);
6233
5090
  process.exit(1);
6234
5091
  }
6235
- const result2 = await runStandaloneReview({
5092
+ const result = await runStandaloneReview({
6236
5093
  projectDir,
6237
5094
  runners: runners2,
6238
5095
  prTitle,
@@ -6242,15 +5099,15 @@ async function main() {
6242
5099
  taskId
6243
5100
  });
6244
5101
  if (litellmProcess2) litellmProcess2.kill();
6245
- if (result2.outcome === "failed") {
6246
- console.error(`Review failed: ${result2.error}`);
5102
+ if (result.outcome === "failed") {
5103
+ console.error(`Review failed: ${result.error}`);
6247
5104
  process.exit(1);
6248
5105
  }
6249
- if (result2.reviewContent) {
6250
- console.log(result2.reviewContent);
5106
+ if (result.reviewContent) {
5107
+ console.log(result.reviewContent);
6251
5108
  if (!input.local && prNumber) {
6252
- const comment = formatReviewComment(result2.reviewContent, taskId);
6253
- const verdict = detectReviewVerdict(result2.reviewContent);
5109
+ const comment = formatReviewComment(result.reviewContent, taskId);
5110
+ const verdict = detectReviewVerdict(result.reviewContent);
6254
5111
  const event = verdict === "fail" ? "request-changes" : "approve";
6255
5112
  const posted = submitPRReview(prNumber, comment, event);
6256
5113
  if (!posted) {
@@ -6282,48 +5139,48 @@ async function main() {
6282
5139
  process.exit(1);
6283
5140
  }
6284
5141
  const { runResolve: runResolve2 } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
6285
- const result2 = await runResolve2({
5142
+ const result = await runResolve2({
6286
5143
  prNumber: input.prNumber,
6287
5144
  projectDir,
6288
5145
  runners: runners2,
6289
5146
  local: input.local ?? true
6290
5147
  });
6291
5148
  if (litellmProcess2) litellmProcess2.kill();
6292
- if (result2.outcome === "failed") {
6293
- console.error(`Resolve failed: ${result2.error}`);
5149
+ if (result.outcome === "failed") {
5150
+ console.error(`Resolve failed: ${result.error}`);
6294
5151
  process.exit(1);
6295
5152
  }
6296
- console.log(`Resolve: ${result2.outcome}`);
5153
+ console.log(`Resolve: ${result.outcome}`);
6297
5154
  process.exit(0);
6298
5155
  }
6299
5156
  logger.info("Preflight checks:");
6300
5157
  runPreflight();
6301
5158
  if (input.task) {
6302
- fs32.writeFileSync(path29.join(taskDir, "task.md"), input.task);
5159
+ fs30.writeFileSync(path27.join(taskDir, "task.md"), input.task);
6303
5160
  }
6304
- const taskMdPath = path29.join(taskDir, "task.md");
6305
- if (!fs32.existsSync(taskMdPath) && isPRFix && input.prNumber) {
5161
+ const taskMdPath = path27.join(taskDir, "task.md");
5162
+ if (!fs30.existsSync(taskMdPath) && isPRFix && input.prNumber) {
6306
5163
  logger.info(`Fetching PR #${input.prNumber} details as task context...`);
6307
5164
  const prDetails = getPRDetails(input.prNumber);
6308
5165
  if (prDetails) {
6309
5166
  const taskContent = `# ${prDetails.title}
6310
5167
 
6311
5168
  ${prDetails.body ?? ""}`;
6312
- fs32.writeFileSync(taskMdPath, taskContent);
5169
+ fs30.writeFileSync(taskMdPath, taskContent);
6313
5170
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
6314
5171
  }
6315
- } else if (!fs32.existsSync(taskMdPath) && input.issueNumber) {
5172
+ } else if (!fs30.existsSync(taskMdPath) && input.issueNumber) {
6316
5173
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
6317
5174
  const issue = getIssue(input.issueNumber);
6318
5175
  if (issue) {
6319
5176
  const taskContent = `# ${issue.title}
6320
5177
 
6321
5178
  ${issue.body ?? ""}`;
6322
- fs32.writeFileSync(taskMdPath, taskContent);
5179
+ fs30.writeFileSync(taskMdPath, taskContent);
6323
5180
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
6324
5181
  }
6325
5182
  }
6326
- if (!fs32.existsSync(taskMdPath)) {
5183
+ if (!fs30.existsSync(taskMdPath)) {
6327
5184
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
6328
5185
  process.exit(1);
6329
5186
  }
@@ -6461,7 +5318,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
6461
5318
  }
6462
5319
  }
6463
5320
  const state = await runPipeline(ctx);
6464
- const files = fs32.readdirSync(taskDir);
5321
+ const files = fs30.readdirSync(taskDir);
6465
5322
  console.log(`
6466
5323
  Artifacts in ${taskDir}:`);
6467
5324
  for (const f of files) {
@@ -6526,8 +5383,8 @@ var init_entry = __esm({
6526
5383
  });
6527
5384
 
6528
5385
  // src/bin/cli.ts
6529
- import * as fs33 from "fs";
6530
- import * as path30 from "path";
5386
+ import * as fs31 from "fs";
5387
+ import * as path28 from "path";
6531
5388
  import { fileURLToPath as fileURLToPath2 } from "url";
6532
5389
 
6533
5390
  // src/bin/commands/init.ts
@@ -6699,7 +5556,7 @@ function buildConfig(cwd, basic) {
6699
5556
  github: { owner: basic.owner, repo: basic.repo },
6700
5557
  agent: {
6701
5558
  provider: "anthropic",
6702
- modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
5559
+ modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
6703
5560
  }
6704
5561
  };
6705
5562
  const mcp = detectMcpConfig(cwd, basic.pm, pkg);
@@ -6899,15 +5756,15 @@ function initCommand(opts, pkgRoot) {
6899
5756
 
6900
5757
  // src/bin/commands/bootstrap.ts
6901
5758
  init_architecture_detection();
6902
- import * as fs8 from "fs";
6903
- import * as path7 from "path";
5759
+ import * as fs7 from "fs";
5760
+ import * as path6 from "path";
6904
5761
  import { execFileSync as execFileSync5 } from "child_process";
6905
5762
 
6906
5763
  // src/bin/qa-guide.ts
6907
5764
  import * as fs5 from "fs";
6908
5765
  import * as path4 from "path";
6909
5766
  function discoverQaContext(cwd) {
6910
- const result2 = {
5767
+ const result = {
6911
5768
  routes: [],
6912
5769
  authFiles: [],
6913
5770
  loginPage: null,
@@ -6920,21 +5777,21 @@ function discoverQaContext(cwd) {
6920
5777
  const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
6921
5778
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
6922
5779
  const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
6923
- if (pkg.scripts?.dev) result2.devCommand = `${pm} dev`;
6924
- if (allDeps.next || allDeps.nuxt) result2.devPort = 3e3;
6925
- else if (allDeps.vite) result2.devPort = 5173;
5780
+ if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
5781
+ if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
5782
+ else if (allDeps.vite) result.devPort = 5173;
6926
5783
  } catch {
6927
5784
  }
6928
5785
  const appDirs = ["src/app", "app"];
6929
5786
  for (const appDir of appDirs) {
6930
5787
  const fullAppDir = path4.join(cwd, appDir);
6931
5788
  if (!fs5.existsSync(fullAppDir)) continue;
6932
- scanRoutes(fullAppDir, appDir, "", result2);
5789
+ scanRoutes(fullAppDir, appDir, "", result);
6933
5790
  break;
6934
5791
  }
6935
5792
  const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
6936
5793
  for (const p of authPatterns) {
6937
- if (fs5.existsSync(path4.join(cwd, p))) result2.authFiles.push(p);
5794
+ if (fs5.existsSync(path4.join(cwd, p))) result.authFiles.push(p);
6938
5795
  }
6939
5796
  const authConfigGlobs = [
6940
5797
  "src/app/api/auth",
@@ -6945,7 +5802,7 @@ function discoverQaContext(cwd) {
6945
5802
  "src/app/api/oauth"
6946
5803
  ];
6947
5804
  for (const g of authConfigGlobs) {
6948
- if (fs5.existsSync(path4.join(cwd, g))) result2.authFiles.push(g);
5805
+ if (fs5.existsSync(path4.join(cwd, g))) result.authFiles.push(g);
6949
5806
  }
6950
5807
  try {
6951
5808
  const rolePaths = [
@@ -6967,7 +5824,7 @@ function discoverQaContext(cwd) {
6967
5824
  if (roleMatches) {
6968
5825
  for (const m of roleMatches) {
6969
5826
  const val = m.match(/['"](\w+)['"]/);
6970
- if (val && !result2.roles.includes(val[1])) result2.roles.push(val[1]);
5827
+ if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
6971
5828
  }
6972
5829
  }
6973
5830
  const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
@@ -6976,7 +5833,7 @@ function discoverQaContext(cwd) {
6976
5833
  if (vals) {
6977
5834
  for (const v of vals) {
6978
5835
  const clean = v.replace(/['"]/g, "");
6979
- if (!result2.roles.includes(clean)) result2.roles.push(clean);
5836
+ if (!result.roles.includes(clean)) result.roles.push(clean);
6980
5837
  }
6981
5838
  }
6982
5839
  }
@@ -6986,9 +5843,9 @@ function discoverQaContext(cwd) {
6986
5843
  }
6987
5844
  } catch {
6988
5845
  }
6989
- return result2;
5846
+ return result;
6990
5847
  }
6991
- function scanRoutes(dir, baseDir, prefix, result2) {
5848
+ function scanRoutes(dir, baseDir, prefix, result) {
6992
5849
  let entries;
6993
5850
  try {
6994
5851
  entries = fs5.readdirSync(dir, { withFileTypes: true });
@@ -6999,16 +5856,16 @@ function scanRoutes(dir, baseDir, prefix, result2) {
6999
5856
  if (hasPage) {
7000
5857
  const routePath = prefix || "/";
7001
5858
  const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
7002
- result2.routes.push({ path: routePath, group });
7003
- if (prefix.includes("/login")) result2.loginPage = routePath;
7004
- if (prefix.startsWith("/admin") && !result2.adminPath) result2.adminPath = prefix;
5859
+ result.routes.push({ path: routePath, group });
5860
+ if (prefix.includes("/login")) result.loginPage = routePath;
5861
+ if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
7005
5862
  }
7006
5863
  for (const entry of entries) {
7007
5864
  if (!entry.isDirectory()) continue;
7008
5865
  if (entry.name === "node_modules" || entry.name === ".next") continue;
7009
5866
  let segment = entry.name;
7010
5867
  if (segment.startsWith("(") && segment.endsWith(")")) {
7011
- scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result2);
5868
+ scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result);
7012
5869
  continue;
7013
5870
  }
7014
5871
  if (segment.startsWith("[") && segment.endsWith("]")) {
@@ -7017,7 +5874,7 @@ function scanRoutes(dir, baseDir, prefix, result2) {
7017
5874
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
7018
5875
  segment = `:${segment.slice(2, -2)}?`;
7019
5876
  }
7020
- scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result2);
5877
+ scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
7021
5878
  }
7022
5879
  }
7023
5880
  function generateQaGuide(discovery) {
@@ -7174,23 +6031,22 @@ function installSkillsForProject(cwd) {
7174
6031
  }
7175
6032
 
7176
6033
  // src/bin/commands/bootstrap.ts
7177
- init_config();
7178
6034
  var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
7179
6035
  function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
7180
- const srcDir = path7.join(cwd, "src");
7181
- const baseDir = fs8.existsSync(srcDir) ? srcDir : cwd;
6036
+ const srcDir = path6.join(cwd, "src");
6037
+ const baseDir = fs7.existsSync(srcDir) ? srcDir : cwd;
7182
6038
  const results = [];
7183
6039
  function walk(dir) {
7184
6040
  const entries = [];
7185
6041
  try {
7186
- for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
6042
+ for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
7187
6043
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
7188
- const full = path7.join(dir, entry.name);
6044
+ const full = path6.join(dir, entry.name);
7189
6045
  if (entry.isDirectory()) {
7190
6046
  entries.push(...walk(full));
7191
6047
  } else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
7192
6048
  try {
7193
- const stat = fs8.statSync(full);
6049
+ const stat = fs7.statSync(full);
7194
6050
  if (stat.size >= 200 && stat.size <= 5e3) {
7195
6051
  entries.push({ filePath: full, size: stat.size });
7196
6052
  }
@@ -7204,8 +6060,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
7204
6060
  }
7205
6061
  const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
7206
6062
  for (const { filePath } of files) {
7207
- const rel = path7.relative(cwd, filePath);
7208
- const content = fs8.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
6063
+ const rel = path6.relative(cwd, filePath);
6064
+ const content = fs7.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
7209
6065
  results.push(`### File: ${rel}
7210
6066
  \`\`\`typescript
7211
6067
  ${content}
@@ -7217,9 +6073,9 @@ function ghComment(issueNumber, body, cwd) {
7217
6073
  try {
7218
6074
  let repoSlug = "";
7219
6075
  try {
7220
- const configPath = path7.join(cwd, "kody.config.json");
7221
- if (fs8.existsSync(configPath)) {
7222
- const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6076
+ const configPath = path6.join(cwd, "kody.config.json");
6077
+ if (fs7.existsSync(configPath)) {
6078
+ const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
7223
6079
  if (config.github?.owner && config.github?.repo) {
7224
6080
  repoSlug = `${config.github.owner}/${config.github.repo}`;
7225
6081
  }
@@ -7246,9 +6102,7 @@ function ghComment(issueNumber, body, cwd) {
7246
6102
  }
7247
6103
  function bootstrapCommand(opts, pkgRoot) {
7248
6104
  const cwd = process.cwd();
7249
- setConfigDir(cwd);
7250
6105
  const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
7251
- const bootstrapModel = resolveStageConfig(getProjectConfig(), "bootstrap", "cheap").model;
7252
6106
  console.log(`
7253
6107
  \u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
7254
6108
  `);
@@ -7256,8 +6110,8 @@ function bootstrapCommand(opts, pkgRoot) {
7256
6110
  ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
7257
6111
  }
7258
6112
  const readIfExists = (rel, maxChars = 3e3) => {
7259
- const p = path7.join(cwd, rel);
7260
- if (fs8.existsSync(p)) return fs8.readFileSync(p, "utf-8").slice(0, maxChars);
6113
+ const p = path6.join(cwd, rel);
6114
+ if (fs7.existsSync(p)) return fs7.readFileSync(p, "utf-8").slice(0, maxChars);
7261
6115
  return null;
7262
6116
  };
7263
6117
  let repoContext = "";
@@ -7292,14 +6146,14 @@ ${sampleFiles}
7292
6146
 
7293
6147
  `;
7294
6148
  try {
7295
- const topDirs = fs8.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
6149
+ const topDirs = fs7.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
7296
6150
  repoContext += `## Top-level directories
7297
6151
  ${topDirs.join(", ")}
7298
6152
 
7299
6153
  `;
7300
- const srcDir = path7.join(cwd, "src");
7301
- if (fs8.existsSync(srcDir)) {
7302
- const srcDirs = fs8.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6154
+ const srcDir = path6.join(cwd, "src");
6155
+ if (fs7.existsSync(srcDir)) {
6156
+ const srcDirs = fs7.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7303
6157
  if (srcDirs.length > 0) repoContext += `## src/ subdirectories
7304
6158
  ${srcDirs.join(", ")}
7305
6159
 
@@ -7309,19 +6163,19 @@ ${srcDirs.join(", ")}
7309
6163
  }
7310
6164
  const existingFiles = [];
7311
6165
  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"]) {
7312
- if (fs8.existsSync(path7.join(cwd, f))) existingFiles.push(f);
6166
+ if (fs7.existsSync(path6.join(cwd, f))) existingFiles.push(f);
7313
6167
  }
7314
6168
  if (existingFiles.length) repoContext += `## Config files present
7315
6169
  ${existingFiles.join(", ")}
7316
6170
 
7317
6171
  `;
7318
6172
  console.log("\u2500\u2500 Project Memory \u2500\u2500");
7319
- const memoryDir = path7.join(cwd, ".kody", "memory");
7320
- fs8.mkdirSync(memoryDir, { recursive: true });
7321
- const archPath = path7.join(memoryDir, "architecture.md");
7322
- const conventionsPath = path7.join(memoryDir, "conventions.md");
7323
- const existingArch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
7324
- const existingConv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
6173
+ const memoryDir = path6.join(cwd, ".kody", "memory");
6174
+ fs7.mkdirSync(memoryDir, { recursive: true });
6175
+ const archPath = path6.join(memoryDir, "architecture.md");
6176
+ const conventionsPath = path6.join(memoryDir, "conventions.md");
6177
+ const existingArch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
6178
+ const existingConv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
7325
6179
  const hasExisting = !!(existingArch || existingConv);
7326
6180
  const extendInstruction = hasExisting && !opts.force ? `
7327
6181
  ## Existing Documentation (EXTEND, do not replace)
@@ -7365,7 +6219,7 @@ ${repoContext}`;
7365
6219
  const output = execFileSync5("claude", [
7366
6220
  "--print",
7367
6221
  "--model",
7368
- bootstrapModel,
6222
+ "claude-haiku-4-5-20251001",
7369
6223
  "--dangerously-skip-permissions",
7370
6224
  memoryPrompt
7371
6225
  ], {
@@ -7377,12 +6231,12 @@ ${repoContext}`;
7377
6231
  const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
7378
6232
  const parsed = JSON.parse(cleaned);
7379
6233
  if (parsed.architecture) {
7380
- fs8.writeFileSync(archPath, parsed.architecture);
6234
+ fs7.writeFileSync(archPath, parsed.architecture);
7381
6235
  const lineCount = parsed.architecture.split("\n").length;
7382
6236
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
7383
6237
  }
7384
6238
  if (parsed.conventions) {
7385
- fs8.writeFileSync(conventionsPath, parsed.conventions);
6239
+ fs7.writeFileSync(conventionsPath, parsed.conventions);
7386
6240
  const lineCount = parsed.conventions.split("\n").length;
7387
6241
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
7388
6242
  }
@@ -7391,39 +6245,39 @@ ${repoContext}`;
7391
6245
  const detected = detectArchitectureBasic(cwd);
7392
6246
  if (detected.length > 0) {
7393
6247
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7394
- fs8.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
6248
+ fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
7395
6249
 
7396
6250
  ## Overview
7397
6251
  ${detected.join("\n")}
7398
6252
  `);
7399
6253
  console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
7400
6254
  }
7401
- fs8.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
6255
+ fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
7402
6256
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
7403
6257
  }
7404
6258
  console.log("\n\u2500\u2500 Step Files \u2500\u2500");
7405
- const stepsDir = path7.join(cwd, ".kody", "steps");
7406
- fs8.mkdirSync(stepsDir, { recursive: true });
7407
- const arch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
7408
- const conv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
6259
+ const stepsDir = path6.join(cwd, ".kody", "steps");
6260
+ fs7.mkdirSync(stepsDir, { recursive: true });
6261
+ const arch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
6262
+ const conv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
7409
6263
  console.log(" \u23F3 Customizing step files...");
7410
6264
  let stepCount = 0;
7411
6265
  for (const stage of STEP_STAGES) {
7412
- const templatePath = path7.join(pkgRoot, "prompts", `${stage}.md`);
7413
- if (!fs8.existsSync(templatePath)) {
6266
+ const templatePath = path6.join(pkgRoot, "prompts", `${stage}.md`);
6267
+ if (!fs7.existsSync(templatePath)) {
7414
6268
  console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
7415
6269
  continue;
7416
6270
  }
7417
- const stepOutputPath = path7.join(stepsDir, `${stage}.md`);
7418
- if (fs8.existsSync(stepOutputPath) && !opts.force) {
6271
+ const stepOutputPath = path6.join(stepsDir, `${stage}.md`);
6272
+ if (fs7.existsSync(stepOutputPath) && !opts.force) {
7419
6273
  console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
7420
6274
  continue;
7421
6275
  }
7422
- const defaultPrompt = fs8.readFileSync(templatePath, "utf-8");
6276
+ const defaultPrompt = fs7.readFileSync(templatePath, "utf-8");
7423
6277
  const contextPlaceholder = "{{TASK_CONTEXT}}";
7424
6278
  const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
7425
6279
  if (placeholderIdx === -1) {
7426
- fs8.copyFileSync(templatePath, stepOutputPath);
6280
+ fs7.copyFileSync(templatePath, stepOutputPath);
7427
6281
  stepCount++;
7428
6282
  console.log(` \u2713 ${stage}.md`);
7429
6283
  continue;
@@ -7468,7 +6322,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
7468
6322
  const output = execFileSync5("claude", [
7469
6323
  "--print",
7470
6324
  "--model",
7471
- bootstrapModel,
6325
+ "claude-haiku-4-5-20251001",
7472
6326
  "--dangerously-skip-permissions",
7473
6327
  customizationPrompt
7474
6328
  ], {
@@ -7480,23 +6334,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
7480
6334
  let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
7481
6335
  cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
7482
6336
  const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
7483
- fs8.writeFileSync(stepOutputPath, finalPrompt);
6337
+ fs7.writeFileSync(stepOutputPath, finalPrompt);
7484
6338
  stepCount++;
7485
6339
  console.log(` \u2713 ${stage}.md`);
7486
6340
  } catch {
7487
6341
  console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
7488
- fs8.copyFileSync(templatePath, stepOutputPath);
6342
+ fs7.copyFileSync(templatePath, stepOutputPath);
7489
6343
  stepCount++;
7490
6344
  }
7491
6345
  }
7492
6346
  console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
7493
6347
  console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
7494
- const qaGuidePath = path7.join(cwd, ".kody", "qa-guide.md");
7495
- if (!fs8.existsSync(qaGuidePath) || opts.force) {
6348
+ const qaGuidePath = path6.join(cwd, ".kody", "qa-guide.md");
6349
+ if (!fs7.existsSync(qaGuidePath) || opts.force) {
7496
6350
  const discovery = discoverQaContext(cwd);
7497
6351
  if (discovery.routes.length > 0) {
7498
6352
  const qaGuide = generateQaGuide(discovery);
7499
- fs8.writeFileSync(qaGuidePath, qaGuide);
6353
+ fs7.writeFileSync(qaGuidePath, qaGuide);
7500
6354
  console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
7501
6355
  if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
7502
6356
  if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
@@ -7511,9 +6365,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
7511
6365
  try {
7512
6366
  let repoSlug = "";
7513
6367
  try {
7514
- const configPath = path7.join(cwd, "kody.config.json");
7515
- if (fs8.existsSync(configPath)) {
7516
- const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6368
+ const configPath = path6.join(cwd, "kody.config.json");
6369
+ if (fs7.existsSync(configPath)) {
6370
+ const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
7517
6371
  if (config.github?.owner && config.github?.repo) {
7518
6372
  repoSlug = `${config.github.owner}/${config.github.repo}`;
7519
6373
  }
@@ -7586,19 +6440,19 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
7586
6440
  ".kody/memory/conventions.md",
7587
6441
  ".kody/qa-guide.md",
7588
6442
  ...installedSkillPaths
7589
- ].filter((f) => fs8.existsSync(path7.join(cwd, f)));
7590
- if (fs8.existsSync(path7.join(cwd, "skills-lock.json"))) {
6443
+ ].filter((f) => fs7.existsSync(path6.join(cwd, f)));
6444
+ if (fs7.existsSync(path6.join(cwd, "skills-lock.json"))) {
7591
6445
  filesToCommit.push("skills-lock.json");
7592
6446
  }
7593
6447
  for (const stage of STEP_STAGES) {
7594
6448
  const stepFile = `.kody/steps/${stage}.md`;
7595
- if (fs8.existsSync(path7.join(cwd, stepFile))) {
6449
+ if (fs7.existsSync(path6.join(cwd, stepFile))) {
7596
6450
  filesToCommit.push(stepFile);
7597
6451
  }
7598
6452
  }
7599
6453
  if (filesToCommit.length > 0) {
7600
6454
  try {
7601
- const fullPaths = filesToCommit.map((f) => path7.join(cwd, f));
6455
+ const fullPaths = filesToCommit.map((f) => path6.join(cwd, f));
7602
6456
  for (let pass = 0; pass < 2; pass++) {
7603
6457
  execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
7604
6458
  cwd,
@@ -7625,9 +6479,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
7625
6479
  console.log(` \u2713 Pushed branch: ${branchName}`);
7626
6480
  let baseBranch = "main";
7627
6481
  try {
7628
- const configPath = path7.join(cwd, "kody.config.json");
7629
- if (fs8.existsSync(configPath)) {
7630
- const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6482
+ const configPath = path6.join(cwd, "kody.config.json");
6483
+ if (fs7.existsSync(configPath)) {
6484
+ const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
7631
6485
  baseBranch = config.git?.defaultBranch ?? "main";
7632
6486
  }
7633
6487
  } catch {
@@ -7701,11 +6555,11 @@ Create it manually.`, cwd);
7701
6555
 
7702
6556
  // src/bin/cli.ts
7703
6557
  init_architecture_detection();
7704
- var __dirname2 = path30.dirname(fileURLToPath2(import.meta.url));
7705
- var PKG_ROOT = path30.resolve(__dirname2, "..", "..");
6558
+ var __dirname2 = path28.dirname(fileURLToPath2(import.meta.url));
6559
+ var PKG_ROOT = path28.resolve(__dirname2, "..", "..");
7706
6560
  function getVersion() {
7707
- const pkgPath = path30.join(PKG_ROOT, "package.json");
7708
- const pkg = JSON.parse(fs33.readFileSync(pkgPath, "utf-8"));
6561
+ const pkgPath = path28.join(PKG_ROOT, "package.json");
6562
+ const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
7709
6563
  return pkg.version;
7710
6564
  }
7711
6565
  var args = process.argv.slice(2);
@@ -7716,8 +6570,6 @@ if (command === "init") {
7716
6570
  bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
7717
6571
  } else if (command === "taskify") {
7718
6572
  Promise.resolve().then(() => (init_taskify_command(), taskify_command_exports)).then(({ runTaskifyCommand: runTaskifyCommand2 }) => runTaskifyCommand2());
7719
- } else if (command === "test-model") {
7720
- Promise.resolve().then(() => (init_test_model_command(), test_model_command_exports)).then(({ runTestModelCommand: runTestModelCommand2 }) => runTestModelCommand2());
7721
6573
  } else if (command === "ci-parse") {
7722
6574
  Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
7723
6575
  } else if (command === "version" || command === "--version" || command === "-v") {