@kody-ade/kody-engine-lite 0.1.125 → 0.1.126

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 (3) hide show
  1. package/README.md +5 -4
  2. package/dist/bin/cli.js +1000 -423
  3. package/package.json +3 -2
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;
@@ -225,10 +225,10 @@ function setConfigDir(dir) {
225
225
  }
226
226
  function getProjectConfig() {
227
227
  if (_config) return _config;
228
- const configPath = path6.join(_configDir ?? process.cwd(), "kody.config.json");
229
- if (fs7.existsSync(configPath)) {
228
+ const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
229
+ if (fs8.existsSync(configPath)) {
230
230
  try {
231
- const result2 = parseJsonSafe(fs7.readFileSync(configPath, "utf-8"));
231
+ const result2 = parseJsonSafe(fs8.readFileSync(configPath, "utf-8"));
232
232
  if (!result2.ok) {
233
233
  logger.warn(`kody.config.json: ${result2.error} \u2014 using defaults`);
234
234
  _config = { ...DEFAULT_CONFIG };
@@ -304,22 +304,22 @@ var init_config = __esm({
304
304
  // src/agent-runner.ts
305
305
  import { spawn, execFileSync as execFileSync6 } from "child_process";
306
306
  function writeStdin(child, prompt) {
307
- return new Promise((resolve5, reject) => {
307
+ return new Promise((resolve6, reject) => {
308
308
  if (!child.stdin) {
309
- resolve5();
309
+ resolve6();
310
310
  return;
311
311
  }
312
312
  child.stdin.write(prompt, (err) => {
313
313
  if (err) reject(err);
314
314
  else {
315
315
  child.stdin.end();
316
- resolve5();
316
+ resolve6();
317
317
  }
318
318
  });
319
319
  });
320
320
  }
321
321
  function waitForProcess(child, timeout) {
322
- return new Promise((resolve5) => {
322
+ return new Promise((resolve6) => {
323
323
  const stdoutChunks = [];
324
324
  const stderrChunks = [];
325
325
  child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
@@ -332,7 +332,7 @@ function waitForProcess(child, timeout) {
332
332
  }, timeout);
333
333
  child.on("exit", (code) => {
334
334
  clearTimeout(timer);
335
- resolve5({
335
+ resolve6({
336
336
  code,
337
337
  stdout: Buffer.concat(stdoutChunks).toString(),
338
338
  stderr: Buffer.concat(stderrChunks).toString()
@@ -340,7 +340,7 @@ function waitForProcess(child, timeout) {
340
340
  });
341
341
  child.on("error", (err) => {
342
342
  clearTimeout(timer);
343
- resolve5({ code: -1, stdout: "", stderr: err.message });
343
+ resolve6({ code: -1, stdout: "", stderr: err.message });
344
344
  });
345
345
  });
346
346
  }
@@ -934,7 +934,7 @@ var init_github_api = __esm({
934
934
  "use strict";
935
935
  init_logger();
936
936
  API_TIMEOUT_MS = 3e4;
937
- LIFECYCLE_LABELS = ["planning", "building", "review", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
937
+ LIFECYCLE_LABELS = ["backlog", "planning", "building", "verifying", "review", "fixing", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
938
938
  KODY_MARKERS = [
939
939
  "Kody Review",
940
940
  "\u{1F916} Generated by Kody",
@@ -949,13 +949,13 @@ var init_github_api = __esm({
949
949
  });
950
950
 
951
951
  // src/cli/task-resolution.ts
952
- import * as fs9 from "fs";
953
- import * as path8 from "path";
952
+ import * as fs10 from "fs";
953
+ import * as path9 from "path";
954
954
  import { execFileSync as execFileSync8 } from "child_process";
955
955
  function findLatestTaskForIssue(issueNumber, projectDir) {
956
- const tasksDir = path8.join(projectDir, ".kody", "tasks");
957
- if (!fs9.existsSync(tasksDir)) return null;
958
- const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
956
+ const tasksDir = path9.join(projectDir, ".kody", "tasks");
957
+ if (!fs10.existsSync(tasksDir)) return null;
958
+ const allDirs = fs10.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
959
959
  const prefix = `${issueNumber}-`;
960
960
  const direct = allDirs.find((d) => d.startsWith(prefix));
961
961
  if (direct) return direct;
@@ -999,14 +999,14 @@ function resolveTaskIdFromComments(issueNumber) {
999
999
  }
1000
1000
  }
1001
1001
  function findPausedTaskifyForIssue(issueNumber, projectDir) {
1002
- const tasksDir = path8.join(projectDir, ".kody", "tasks");
1003
- if (!fs9.existsSync(tasksDir)) return null;
1004
- const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
1002
+ const tasksDir = path9.join(projectDir, ".kody", "tasks");
1003
+ if (!fs10.existsSync(tasksDir)) return null;
1004
+ const allDirs = fs10.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
1005
1005
  for (const dir of allDirs) {
1006
- const markerPath = path8.join(tasksDir, dir, "taskify.marker");
1007
- if (!fs9.existsSync(markerPath)) continue;
1006
+ const markerPath = path9.join(tasksDir, dir, "taskify.marker");
1007
+ if (!fs10.existsSync(markerPath)) continue;
1008
1008
  try {
1009
- const marker = JSON.parse(fs9.readFileSync(markerPath, "utf-8"));
1009
+ const marker = JSON.parse(fs10.readFileSync(markerPath, "utf-8"));
1010
1010
  if (marker.issueNumber === issueNumber) return dir;
1011
1011
  } catch {
1012
1012
  }
@@ -1030,9 +1030,9 @@ var init_task_resolution = __esm({
1030
1030
  });
1031
1031
 
1032
1032
  // src/cli/litellm.ts
1033
- import * as fs10 from "fs";
1033
+ import * as fs11 from "fs";
1034
1034
  import * as os from "os";
1035
- import * as path9 from "path";
1035
+ import * as path10 from "path";
1036
1036
  import { execFileSync as execFileSync9 } from "child_process";
1037
1037
  async function checkLitellmHealth(url) {
1038
1038
  try {
@@ -1132,8 +1132,8 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
1132
1132
  logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
1133
1133
  return null;
1134
1134
  }
1135
- const configPath = path9.join(os.tmpdir(), "kody-litellm-config.yaml");
1136
- fs10.writeFileSync(configPath, generatedConfig);
1135
+ const configPath = path10.join(os.tmpdir(), "kody-litellm-config.yaml");
1136
+ fs11.writeFileSync(configPath, generatedConfig);
1137
1137
  const portMatch = url.match(/:(\d+)/);
1138
1138
  const port = portMatch ? portMatch[1] : "4000";
1139
1139
  let litellmFound = false;
@@ -1162,10 +1162,10 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
1162
1162
  cmd = "python3";
1163
1163
  args2 = ["-m", "litellm", "--config", configPath, "--port", port];
1164
1164
  }
1165
- const dotenvPath = path9.join(projectDir, ".env");
1165
+ const dotenvPath = path10.join(projectDir, ".env");
1166
1166
  const dotenvVars = {};
1167
- if (fs10.existsSync(dotenvPath)) {
1168
- for (const rawLine of fs10.readFileSync(dotenvPath, "utf-8").split("\n")) {
1167
+ if (fs11.existsSync(dotenvPath)) {
1168
+ for (const rawLine of fs11.readFileSync(dotenvPath, "utf-8").split("\n")) {
1169
1169
  const line = rawLine.trim();
1170
1170
  if (!line || line.startsWith("#")) continue;
1171
1171
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -1225,8 +1225,8 @@ __export(taskify_command_exports, {
1225
1225
  taskifyCommand: () => taskifyCommand,
1226
1226
  topoSort: () => topoSort
1227
1227
  });
1228
- import * as fs11 from "fs";
1229
- import * as path10 from "path";
1228
+ import * as fs12 from "fs";
1229
+ import * as path11 from "path";
1230
1230
  import { fileURLToPath } from "url";
1231
1231
  import { execSync } from "child_process";
1232
1232
  function topoSort(tasks) {
@@ -1270,10 +1270,10 @@ function hasFlag(args2, flag) {
1270
1270
  async function runTaskifyCommand() {
1271
1271
  const args2 = process.argv.slice(3);
1272
1272
  const cwdArg = getArg(args2, "--cwd") ?? process.cwd();
1273
- const projectDir = path10.resolve(cwdArg);
1273
+ const projectDir = path11.resolve(cwdArg);
1274
1274
  const ticketId = getArg(args2, "--ticket") ?? process.env.TICKET_ID;
1275
1275
  const prdFileArg = getArg(args2, "--file") ?? process.env.PRD_FILE;
1276
- const prdFile = prdFileArg ? path10.resolve(projectDir, prdFileArg) : void 0;
1276
+ const prdFile = prdFileArg ? path11.resolve(projectDir, prdFileArg) : void 0;
1277
1277
  const issueNumberStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER ?? "";
1278
1278
  const issueNumber = issueNumberStr ? parseInt(issueNumberStr, 10) : void 0;
1279
1279
  const feedback = getArg(args2, "--feedback") ?? process.env.FEEDBACK;
@@ -1284,7 +1284,7 @@ async function runTaskifyCommand() {
1284
1284
  logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md> OR kody taskify --issue-number <n>");
1285
1285
  process.exit(1);
1286
1286
  }
1287
- if (prdFile && !fs11.existsSync(prdFile)) {
1287
+ if (prdFile && !fs12.existsSync(prdFile)) {
1288
1288
  logger.error(`File not found: ${prdFile}`);
1289
1289
  process.exit(1);
1290
1290
  }
@@ -1325,8 +1325,8 @@ async function runTaskifyCommand() {
1325
1325
  async function taskifyCommand(opts) {
1326
1326
  const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
1327
1327
  const config = getProjectConfig();
1328
- const taskDir = path10.join(projectDir, ".kody", "tasks", taskId);
1329
- fs11.mkdirSync(taskDir, { recursive: true });
1328
+ const taskDir = path11.join(projectDir, ".kody", "tasks", taskId);
1329
+ fs12.mkdirSync(taskDir, { recursive: true });
1330
1330
  const mode = prdFile ? "file" : ticketId ? "ticket" : "issue";
1331
1331
  logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile ?? `issue#${issueNumber}`} issue=${issueNumber ?? "none"} task=${taskId}`);
1332
1332
  let mcpConfigJson;
@@ -1350,7 +1350,7 @@ Add the required MCP server config to \`kody.config.json\` and try again.`
1350
1350
  }
1351
1351
  const sc = resolveStageConfig(config, "taskify", "strong");
1352
1352
  const model = sc.model;
1353
- const fileContent = prdFile ? fs11.readFileSync(prdFile, "utf-8") : void 0;
1353
+ const fileContent = prdFile ? fs12.readFileSync(prdFile, "utf-8") : void 0;
1354
1354
  let issueBody;
1355
1355
  if (mode === "issue" && issueNumber) {
1356
1356
  const issue = getIssue(issueNumber);
@@ -1366,10 +1366,10 @@ ${issue.body}`;
1366
1366
  let projectContext;
1367
1367
  {
1368
1368
  const parts = [];
1369
- const memoryPath = path10.join(projectDir, ".kody", "memory.md");
1370
- if (fs11.existsSync(memoryPath)) {
1369
+ const memoryPath = path11.join(projectDir, ".kody", "memory.md");
1370
+ if (fs12.existsSync(memoryPath)) {
1371
1371
  try {
1372
- const content = fs11.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
1372
+ const content = fs12.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
1373
1373
  if (content.trim()) parts.push(`### Project Memory
1374
1374
  ${content}`);
1375
1375
  } catch {
@@ -1388,14 +1388,14 @@ ${lines.join("\n")}
1388
1388
  }
1389
1389
  const prompt = buildPrompt({ ticketId, fileContent, issueBody, taskDir, feedback, projectContext });
1390
1390
  if (issueNumber && !local) {
1391
- const src = mode === "file" ? `file \`${path10.basename(prdFile)}\`` : mode === "ticket" ? `ticket **${ticketId}**` : `issue #${issueNumber} description`;
1391
+ const src = mode === "file" ? `file \`${path11.basename(prdFile)}\`` : mode === "ticket" ? `ticket **${ticketId}**` : `issue #${issueNumber} description`;
1392
1392
  const runUrl = process.env.RUN_URL ? ` ([logs](${process.env.RUN_URL}))` : "";
1393
1393
  postComment(issueNumber, `\u{1F680} Kody pipeline started: \`${taskId}\`${runUrl}
1394
1394
 
1395
1395
  Kody is decomposing ${src} into tasks...`);
1396
1396
  setLifecycleLabel(issueNumber, "planning");
1397
1397
  }
1398
- fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1398
+ fs12.writeFileSync(path11.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
1399
1399
  const runner = opts.runner ?? createClaudeCodeRunner();
1400
1400
  logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
1401
1401
  const result2 = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
@@ -1413,8 +1413,8 @@ Kody is decomposing ${src} into tasks...`);
1413
1413
  }
1414
1414
  throw new TaskifyError(errMsg);
1415
1415
  }
1416
- const resultPath = path10.join(taskDir, RESULT_FILE);
1417
- if (!fs11.existsSync(resultPath)) {
1416
+ const resultPath = path11.join(taskDir, RESULT_FILE);
1417
+ if (!fs12.existsSync(resultPath)) {
1418
1418
  const errMsg = `Claude did not write ${RESULT_FILE}. Output:
1419
1419
 
1420
1420
  ${result2.output?.slice(0, 500) ?? "(none)"}`;
@@ -1428,7 +1428,7 @@ ${errMsg}`);
1428
1428
  }
1429
1429
  let parsed;
1430
1430
  try {
1431
- parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
1431
+ parsed = JSON.parse(fs12.readFileSync(resultPath, "utf-8"));
1432
1432
  } catch {
1433
1433
  const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
1434
1434
  if (issueNumber && !local) {
@@ -1437,7 +1437,7 @@ ${errMsg}`);
1437
1437
  }
1438
1438
  throw new TaskifyError(errMsg);
1439
1439
  }
1440
- const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : issueNumber ? `issue #${issueNumber}` : "spec");
1440
+ const sourceLabel = ticketId ?? (prdFile ? path11.basename(prdFile) : issueNumber ? `issue #${issueNumber}` : "spec");
1441
1441
  if (parsed.status === "questions") {
1442
1442
  handleQuestions(parsed, sourceLabel, issueNumber, local ?? false);
1443
1443
  } else if (parsed.status === "ready") {
@@ -1537,15 +1537,15 @@ function buildPrompt(opts) {
1537
1537
  const { ticketId, fileContent, issueBody, taskDir, feedback, projectContext } = opts;
1538
1538
  const scriptDir = new URL(".", import.meta.url).pathname;
1539
1539
  const candidates = [
1540
- path10.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
1541
- path10.resolve(scriptDir, "..", "..", "prompts", "taskify-ticket.md"),
1542
- path10.resolve(__dirname, "..", "..", "prompts", "taskify-ticket.md"),
1543
- path10.resolve(__dirname, "..", "prompts", "taskify-ticket.md")
1540
+ path11.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
1541
+ path11.resolve(scriptDir, "..", "..", "prompts", "taskify-ticket.md"),
1542
+ path11.resolve(__dirname, "..", "..", "prompts", "taskify-ticket.md"),
1543
+ path11.resolve(__dirname, "..", "prompts", "taskify-ticket.md")
1544
1544
  ];
1545
1545
  let template = "";
1546
1546
  for (const candidate of candidates) {
1547
- if (fs11.existsSync(candidate)) {
1548
- template = fs11.readFileSync(candidate, "utf-8");
1547
+ if (fs12.existsSync(candidate)) {
1548
+ template = fs12.readFileSync(candidate, "utf-8");
1549
1549
  break;
1550
1550
  }
1551
1551
  }
@@ -1569,13 +1569,13 @@ function buildPrompt(opts) {
1569
1569
  return template;
1570
1570
  }
1571
1571
  function isTaskifyRun(taskDir) {
1572
- return fs11.existsSync(path10.join(taskDir, MARKER_FILE));
1572
+ return fs12.existsSync(path11.join(taskDir, MARKER_FILE));
1573
1573
  }
1574
1574
  function readTaskifyMarker(taskDir) {
1575
- const markerPath = path10.join(taskDir, MARKER_FILE);
1576
- if (!fs11.existsSync(markerPath)) return null;
1575
+ const markerPath = path11.join(taskDir, MARKER_FILE);
1576
+ if (!fs12.existsSync(markerPath)) return null;
1577
1577
  try {
1578
- return JSON.parse(fs11.readFileSync(markerPath, "utf-8"));
1578
+ return JSON.parse(fs12.readFileSync(markerPath, "utf-8"));
1579
1579
  } catch {
1580
1580
  return null;
1581
1581
  }
@@ -1591,7 +1591,7 @@ var init_taskify_command = __esm({
1591
1591
  init_logger();
1592
1592
  init_task_resolution();
1593
1593
  init_litellm();
1594
- __dirname = path10.dirname(fileURLToPath(import.meta.url));
1594
+ __dirname = path11.dirname(fileURLToPath(import.meta.url));
1595
1595
  TaskifyError = class extends Error {
1596
1596
  constructor(message) {
1597
1597
  super(message);
@@ -1607,9 +1607,9 @@ var init_taskify_command = __esm({
1607
1607
  });
1608
1608
 
1609
1609
  // src/cli/test-model-tests.ts
1610
- import * as fs12 from "fs";
1610
+ import * as fs13 from "fs";
1611
1611
  import * as os2 from "os";
1612
- import * as path11 from "path";
1612
+ import * as path12 from "path";
1613
1613
  import * as zlib from "zlib";
1614
1614
  import { spawnSync, execSync as execSync2 } from "child_process";
1615
1615
  function canRunApiTests(ctx) {
@@ -1937,8 +1937,8 @@ async function testExtendedThinking(ctx) {
1937
1937
  async function testToolRead(ctx) {
1938
1938
  if (!canRunApiTests(ctx)) {
1939
1939
  const t2 = Date.now();
1940
- const testFile2 = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
1941
- fs12.writeFileSync(testFile2, "KODY_SECRET_CONTENT_42");
1940
+ const testFile2 = path12.join(os2.tmpdir(), "kody-test-model-read.txt");
1941
+ fs13.writeFileSync(testFile2, "KODY_SECRET_CONTENT_42");
1942
1942
  try {
1943
1943
  const r = runClaudeTest(ctx, `Read the file ${testFile2} and tell me its exact contents. Reply with ONLY the file contents.`);
1944
1944
  const ok = r.stdout.includes("KODY_SECRET_CONTENT_42");
@@ -1952,12 +1952,12 @@ async function testToolRead(ctx) {
1952
1952
  { toolSelection: ok ? 100 : 0 }
1953
1953
  );
1954
1954
  } finally {
1955
- fs12.rmSync(testFile2, { force: true });
1955
+ fs13.rmSync(testFile2, { force: true });
1956
1956
  }
1957
1957
  }
1958
1958
  const t = Date.now();
1959
- const testFile = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
1960
- fs12.writeFileSync(testFile, "KODY_SECRET_CONTENT_42");
1959
+ const testFile = path12.join(os2.tmpdir(), "kody-test-model-read.txt");
1960
+ fs13.writeFileSync(testFile, "KODY_SECRET_CONTENT_42");
1961
1961
  try {
1962
1962
  const conv = await runToolConversation(
1963
1963
  ctx,
@@ -1986,17 +1986,17 @@ async function testToolRead(ctx) {
1986
1986
  { toolSelection: calledRead ? 100 : 0 }
1987
1987
  );
1988
1988
  } finally {
1989
- fs12.rmSync(testFile, { force: true });
1989
+ fs13.rmSync(testFile, { force: true });
1990
1990
  }
1991
1991
  }
1992
1992
  async function testToolEdit(ctx) {
1993
1993
  if (!canRunApiTests(ctx)) {
1994
1994
  const t2 = Date.now();
1995
- const testFile = path11.join(os2.tmpdir(), "kody-test-model-edit.txt");
1996
- fs12.writeFileSync(testFile, "hello world");
1995
+ const testFile = path12.join(os2.tmpdir(), "kody-test-model-edit.txt");
1996
+ fs13.writeFileSync(testFile, "hello world");
1997
1997
  try {
1998
1998
  const r = runClaudeTest(ctx, `Use the Edit tool to replace "hello" with "goodbye" in ${testFile}. Do nothing else.`);
1999
- const content = fs12.existsSync(testFile) ? fs12.readFileSync(testFile, "utf-8") : "";
1999
+ const content = fs13.existsSync(testFile) ? fs13.readFileSync(testFile, "utf-8") : "";
2000
2000
  const ok = content.includes("goodbye");
2001
2001
  return result(
2002
2002
  "tool_edit",
@@ -2008,7 +2008,7 @@ async function testToolEdit(ctx) {
2008
2008
  { toolSelection: ok ? 100 : 0 }
2009
2009
  );
2010
2010
  } finally {
2011
- fs12.rmSync(testFile, { force: true });
2011
+ fs13.rmSync(testFile, { force: true });
2012
2012
  }
2013
2013
  }
2014
2014
  const t = Date.now();
@@ -2082,8 +2082,8 @@ async function testToolBash(ctx) {
2082
2082
  async function testImageAttachment(ctx) {
2083
2083
  if (!canRunApiTests(ctx)) {
2084
2084
  const t2 = Date.now();
2085
- const tmpPng = path11.join(os2.tmpdir(), "kody-test-image.png");
2086
- fs12.writeFileSync(tmpPng, createRedPng());
2085
+ const tmpPng = path12.join(os2.tmpdir(), "kody-test-image.png");
2086
+ fs13.writeFileSync(tmpPng, createRedPng());
2087
2087
  try {
2088
2088
  const r = runClaudeTest(ctx, `Read the image file at ${tmpPng} and tell me what color it is. Reply with just the color name.`);
2089
2089
  const text2 = r.stdout.toLowerCase();
@@ -2097,7 +2097,7 @@ async function testImageAttachment(ctx) {
2097
2097
  ok ? "Image processed correctly via CLI" : `Got: ${text2.slice(0, 80)}`
2098
2098
  );
2099
2099
  } finally {
2100
- fs12.rmSync(tmpPng, { force: true });
2100
+ fs13.rmSync(tmpPng, { force: true });
2101
2101
  }
2102
2102
  }
2103
2103
  const t = Date.now();
@@ -2312,10 +2312,10 @@ async function testReviewStage(ctx) {
2312
2312
  }
2313
2313
  async function testMcpTools(ctx) {
2314
2314
  const t = Date.now();
2315
- const mcpConfig = path11.join(os2.tmpdir(), `kody-test-mcp-${Date.now()}.json`);
2316
- const testFile = path11.join(ctx.projectDir, "kody-mcp-compat-test.txt");
2315
+ const mcpConfig = path12.join(os2.tmpdir(), `kody-test-mcp-${Date.now()}.json`);
2316
+ const testFile = path12.join(ctx.projectDir, "kody-mcp-compat-test.txt");
2317
2317
  try {
2318
- fs12.writeFileSync(mcpConfig, JSON.stringify({
2318
+ fs13.writeFileSync(mcpConfig, JSON.stringify({
2319
2319
  mcpServers: {
2320
2320
  filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", ctx.projectDir] }
2321
2321
  }
@@ -2326,8 +2326,8 @@ async function testMcpTools(ctx) {
2326
2326
  ["--mcp-config", mcpConfig],
2327
2327
  12e4
2328
2328
  );
2329
- const created = fs12.existsSync(testFile);
2330
- const content = created ? fs12.readFileSync(testFile, "utf-8").trim() : "";
2329
+ const created = fs13.existsSync(testFile);
2330
+ const content = created ? fs13.readFileSync(testFile, "utf-8").trim() : "";
2331
2331
  const correct = content.includes("mcp-ok");
2332
2332
  return result(
2333
2333
  "mcp_tools",
@@ -2340,8 +2340,8 @@ async function testMcpTools(ctx) {
2340
2340
  } catch (err) {
2341
2341
  return result("mcp_tools", "advanced", "warn", 0, Date.now() - t, `MCP test error: ${err instanceof Error ? err.message : String(err)}`);
2342
2342
  } finally {
2343
- fs12.rmSync(mcpConfig, { force: true });
2344
- fs12.rmSync(testFile, { force: true });
2343
+ fs13.rmSync(mcpConfig, { force: true });
2344
+ fs13.rmSync(testFile, { force: true });
2345
2345
  revertChanges(ctx.projectDir);
2346
2346
  }
2347
2347
  }
@@ -2522,9 +2522,9 @@ var test_model_command_exports = {};
2522
2522
  __export(test_model_command_exports, {
2523
2523
  runTestModelCommand: () => runTestModelCommand
2524
2524
  });
2525
- import * as fs13 from "fs";
2525
+ import * as fs14 from "fs";
2526
2526
  import * as os3 from "os";
2527
- import * as path12 from "path";
2527
+ import * as path13 from "path";
2528
2528
  import { execFileSync as execFileSync10 } from "child_process";
2529
2529
  function parseTestModelArgs() {
2530
2530
  const args2 = process.argv.slice(3);
@@ -2607,7 +2607,7 @@ async function startProxy(config, url) {
2607
2607
  return null;
2608
2608
  }
2609
2609
  }
2610
- fs13.writeFileSync(CONFIG_PATH, config);
2610
+ fs14.writeFileSync(CONFIG_PATH, config);
2611
2611
  const portMatch = url.match(/:(\d+)/);
2612
2612
  const port = portMatch ? portMatch[1] : "4099";
2613
2613
  const { spawn: spawn2 } = await import("child_process");
@@ -2649,7 +2649,7 @@ async function quickApiTest(url, model, apiKey) {
2649
2649
  }
2650
2650
  }
2651
2651
  function delay(ms) {
2652
- return new Promise((resolve5) => setTimeout(resolve5, ms));
2652
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
2653
2653
  }
2654
2654
  async function runTestModelCommand() {
2655
2655
  const opts = parseTestModelArgs();
@@ -2663,7 +2663,7 @@ async function runTestModelCommand() {
2663
2663
  proxyProcess.kill();
2664
2664
  proxyProcess = null;
2665
2665
  }
2666
- fs13.rmSync(CONFIG_PATH, { force: true });
2666
+ fs14.rmSync(CONFIG_PATH, { force: true });
2667
2667
  };
2668
2668
  process.on("SIGINT", () => {
2669
2669
  cleanup();
@@ -2757,7 +2757,7 @@ var init_test_model_command = __esm({
2757
2757
  init_test_model_report();
2758
2758
  TEST_PORT = 4099;
2759
2759
  TEST_URL = `http://localhost:${TEST_PORT}`;
2760
- CONFIG_PATH = path12.join(os3.tmpdir(), "kody-test-model-config.yaml");
2760
+ CONFIG_PATH = path13.join(os3.tmpdir(), "kody-test-model-config.yaml");
2761
2761
  }
2762
2762
  });
2763
2763
 
@@ -2768,7 +2768,7 @@ __export(parse_inputs_exports, {
2768
2768
  runCiParse: () => runCiParse,
2769
2769
  writeOutputs: () => writeOutputs
2770
2770
  });
2771
- import * as fs14 from "fs";
2771
+ import * as fs15 from "fs";
2772
2772
  function generateTimestamp() {
2773
2773
  const now = /* @__PURE__ */ new Date();
2774
2774
  const pad2 = (n) => String(n).padStart(2, "0");
@@ -2917,12 +2917,12 @@ function writeOutputs(result2) {
2917
2917
  function output(key, value) {
2918
2918
  if (outputFile) {
2919
2919
  if (value.includes("\n")) {
2920
- fs14.appendFileSync(outputFile, `${key}<<KODY_EOF
2920
+ fs15.appendFileSync(outputFile, `${key}<<KODY_EOF
2921
2921
  ${value}
2922
2922
  KODY_EOF
2923
2923
  `);
2924
2924
  } else {
2925
- fs14.appendFileSync(outputFile, `${key}=${value}
2925
+ fs15.appendFileSync(outputFile, `${key}=${value}
2926
2926
  `);
2927
2927
  }
2928
2928
  }
@@ -3241,14 +3241,14 @@ var init_git_utils = __esm({
3241
3241
  });
3242
3242
 
3243
3243
  // src/pipeline/state.ts
3244
- import * as fs15 from "fs";
3245
- import * as path13 from "path";
3244
+ import * as fs16 from "fs";
3245
+ import * as path14 from "path";
3246
3246
  function loadState(taskId, taskDir) {
3247
- const p = path13.join(taskDir, "status.json");
3248
- if (!fs15.existsSync(p)) return null;
3247
+ const p = path14.join(taskDir, "status.json");
3248
+ if (!fs16.existsSync(p)) return null;
3249
3249
  try {
3250
3250
  const result2 = parseJsonSafe(
3251
- fs15.readFileSync(p, "utf-8"),
3251
+ fs16.readFileSync(p, "utf-8"),
3252
3252
  ["taskId", "state", "stages", "createdAt", "updatedAt"]
3253
3253
  );
3254
3254
  if (!result2.ok) {
@@ -3266,10 +3266,10 @@ function writeState(state, taskDir) {
3266
3266
  ...state,
3267
3267
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3268
3268
  };
3269
- const target = path13.join(taskDir, "status.json");
3269
+ const target = path14.join(taskDir, "status.json");
3270
3270
  const tmp = target + ".tmp";
3271
- fs15.writeFileSync(tmp, JSON.stringify(updated, null, 2));
3272
- fs15.renameSync(tmp, target);
3271
+ fs16.writeFileSync(tmp, JSON.stringify(updated, null, 2));
3272
+ fs16.renameSync(tmp, target);
3273
3273
  return updated;
3274
3274
  }
3275
3275
  function initState(taskId) {
@@ -3310,16 +3310,16 @@ var init_complexity = __esm({
3310
3310
  });
3311
3311
 
3312
3312
  // src/memory.ts
3313
- import * as fs16 from "fs";
3314
- import * as path14 from "path";
3313
+ import * as fs17 from "fs";
3314
+ import * as path15 from "path";
3315
3315
  function readProjectMemory(projectDir) {
3316
- const memoryDir = path14.join(projectDir, ".kody", "memory");
3317
- if (!fs16.existsSync(memoryDir)) return "";
3318
- const files = fs16.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
3316
+ const memoryDir = path15.join(projectDir, ".kody", "memory");
3317
+ if (!fs17.existsSync(memoryDir)) return "";
3318
+ const files = fs17.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
3319
3319
  if (files.length === 0) return "";
3320
3320
  const sections = [];
3321
3321
  for (const file of files) {
3322
- const content = fs16.readFileSync(path14.join(memoryDir, file), "utf-8").trim();
3322
+ const content = fs17.readFileSync(path15.join(memoryDir, file), "utf-8").trim();
3323
3323
  if (content) {
3324
3324
  sections.push(`## ${file.replace(".md", "")}
3325
3325
  ${content}`);
@@ -3338,8 +3338,8 @@ var init_memory = __esm({
3338
3338
  });
3339
3339
 
3340
3340
  // src/context-tiers.ts
3341
- import * as fs17 from "fs";
3342
- import * as path15 from "path";
3341
+ import * as fs18 from "fs";
3342
+ import * as path16 from "path";
3343
3343
  function estimateTokens(text) {
3344
3344
  return Math.ceil(text.length / 4);
3345
3345
  }
@@ -3430,7 +3430,7 @@ function generateL1Json(content) {
3430
3430
  }
3431
3431
  }
3432
3432
  function getTieredContent(filePath, content) {
3433
- const key = path15.basename(filePath);
3433
+ const key = path16.basename(filePath);
3434
3434
  return {
3435
3435
  source: filePath,
3436
3436
  L0: generateL0(content, key),
@@ -3442,15 +3442,15 @@ function selectTier(tiered, tier) {
3442
3442
  return tiered[tier];
3443
3443
  }
3444
3444
  function readProjectMemoryTiered(projectDir, tier) {
3445
- const memoryDir = path15.join(projectDir, ".kody", "memory");
3446
- if (!fs17.existsSync(memoryDir)) return "";
3447
- const files = fs17.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
3445
+ const memoryDir = path16.join(projectDir, ".kody", "memory");
3446
+ if (!fs18.existsSync(memoryDir)) return "";
3447
+ const files = fs18.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
3448
3448
  if (files.length === 0) return "";
3449
3449
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
3450
3450
  const sections = [];
3451
3451
  for (const file of files) {
3452
- const filePath = path15.join(memoryDir, file);
3453
- const content = fs17.readFileSync(filePath, "utf-8").trim();
3452
+ const filePath = path16.join(memoryDir, file);
3453
+ const content = fs18.readFileSync(filePath, "utf-8").trim();
3454
3454
  if (!content) continue;
3455
3455
  const tiered = getTieredContent(filePath, content);
3456
3456
  const selected = selectTier(tiered, tier);
@@ -3473,9 +3473,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
3473
3473
  `;
3474
3474
  context += `Task Directory: ${taskDir}
3475
3475
  `;
3476
- const taskMdPath = path15.join(taskDir, "task.md");
3477
- if (fs17.existsSync(taskMdPath)) {
3478
- const content = fs17.readFileSync(taskMdPath, "utf-8");
3476
+ const taskMdPath = path16.join(taskDir, "task.md");
3477
+ if (fs18.existsSync(taskMdPath)) {
3478
+ const content = fs18.readFileSync(taskMdPath, "utf-8");
3479
3479
  const selected = selectContent(taskMdPath, content, policy.taskDescription);
3480
3480
  const label = tierLabel("Task Description", policy.taskDescription);
3481
3481
  context += `
@@ -3483,9 +3483,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
3483
3483
  ${selected}
3484
3484
  `;
3485
3485
  }
3486
- const taskJsonPath = path15.join(taskDir, "task.json");
3487
- if (fs17.existsSync(taskJsonPath)) {
3488
- const content = fs17.readFileSync(taskJsonPath, "utf-8");
3486
+ const taskJsonPath = path16.join(taskDir, "task.json");
3487
+ if (fs18.existsSync(taskJsonPath)) {
3488
+ const content = fs18.readFileSync(taskJsonPath, "utf-8");
3489
3489
  if (policy.taskClassification === "L2") {
3490
3490
  try {
3491
3491
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -3511,9 +3511,9 @@ ${selected}
3511
3511
  }
3512
3512
  }
3513
3513
  }
3514
- const specPath = path15.join(taskDir, "spec.md");
3515
- if (fs17.existsSync(specPath)) {
3516
- const content = fs17.readFileSync(specPath, "utf-8");
3514
+ const specPath = path16.join(taskDir, "spec.md");
3515
+ if (fs18.existsSync(specPath)) {
3516
+ const content = fs18.readFileSync(specPath, "utf-8");
3517
3517
  const selected = selectContent(specPath, content, policy.spec);
3518
3518
  const label = tierLabel("Spec", policy.spec);
3519
3519
  context += `
@@ -3521,9 +3521,9 @@ ${selected}
3521
3521
  ${selected}
3522
3522
  `;
3523
3523
  }
3524
- const planPath = path15.join(taskDir, "plan.md");
3525
- if (fs17.existsSync(planPath)) {
3526
- const content = fs17.readFileSync(planPath, "utf-8");
3524
+ const planPath = path16.join(taskDir, "plan.md");
3525
+ if (fs18.existsSync(planPath)) {
3526
+ const content = fs18.readFileSync(planPath, "utf-8");
3527
3527
  const selected = selectContent(planPath, content, policy.plan);
3528
3528
  const label = tierLabel("Plan", policy.plan);
3529
3529
  context += `
@@ -3531,9 +3531,9 @@ ${selected}
3531
3531
  ${selected}
3532
3532
  `;
3533
3533
  }
3534
- const contextMdPath = path15.join(taskDir, "context.md");
3535
- if (fs17.existsSync(contextMdPath)) {
3536
- const content = fs17.readFileSync(contextMdPath, "utf-8");
3534
+ const contextMdPath = path16.join(taskDir, "context.md");
3535
+ if (fs18.existsSync(contextMdPath)) {
3536
+ const content = fs18.readFileSync(contextMdPath, "utf-8");
3537
3537
  const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
3538
3538
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
3539
3539
  context += `
@@ -3618,25 +3618,117 @@ var init_context_tiers = __esm({
3618
3618
  }
3619
3619
  });
3620
3620
 
3621
+ // src/tools.ts
3622
+ import * as fs19 from "fs";
3623
+ import * as path17 from "path";
3624
+ import { execSync as execSync3 } from "child_process";
3625
+ import { parse as parseYaml } from "yaml";
3626
+ function loadToolDeclarations(projectDir) {
3627
+ const toolsPath = path17.join(projectDir, ".kody", "tools.yml");
3628
+ if (!fs19.existsSync(toolsPath)) return [];
3629
+ try {
3630
+ const raw = fs19.readFileSync(toolsPath, "utf-8");
3631
+ const parsed = parseYaml(raw);
3632
+ if (!parsed || typeof parsed !== "object") return [];
3633
+ return Object.entries(parsed).map(([name, value]) => {
3634
+ const v = value;
3635
+ return {
3636
+ name,
3637
+ detect: Array.isArray(v.detect) ? v.detect : [],
3638
+ stages: Array.isArray(v.stages) ? v.stages : [],
3639
+ setup: typeof v.setup === "string" ? v.setup : "",
3640
+ skill: typeof v.skill === "string" ? v.skill : ""
3641
+ };
3642
+ });
3643
+ } catch (err) {
3644
+ logger.warn(`Failed to parse .kody/tools.yml: ${err instanceof Error ? err.message : String(err)}`);
3645
+ return [];
3646
+ }
3647
+ }
3648
+ function resolveSkillContent(skillFilename, projectDir) {
3649
+ if (!skillFilename) return "";
3650
+ const projectSkill = path17.join(projectDir, ".kody", "skills", skillFilename);
3651
+ if (fs19.existsSync(projectSkill)) {
3652
+ return fs19.readFileSync(projectSkill, "utf-8");
3653
+ }
3654
+ const scriptDir = new URL(".", import.meta.url).pathname;
3655
+ const candidates = [
3656
+ path17.resolve(scriptDir, "..", "skills", skillFilename),
3657
+ path17.resolve(scriptDir, "..", "..", "skills", skillFilename)
3658
+ ];
3659
+ for (const candidate of candidates) {
3660
+ if (fs19.existsSync(candidate)) {
3661
+ return fs19.readFileSync(candidate, "utf-8");
3662
+ }
3663
+ }
3664
+ logger.warn(`Skill file not found: ${skillFilename}`);
3665
+ return "";
3666
+ }
3667
+ function detectTools(declarations, projectDir) {
3668
+ const resolved = [];
3669
+ for (const decl of declarations) {
3670
+ const detected = decl.detect.some((pattern) => fs19.existsSync(path17.join(projectDir, pattern)));
3671
+ if (!detected) continue;
3672
+ const skillContent = resolveSkillContent(decl.skill, projectDir);
3673
+ resolved.push({
3674
+ name: decl.name,
3675
+ stages: decl.stages,
3676
+ setup: decl.setup,
3677
+ skillContent
3678
+ });
3679
+ }
3680
+ return resolved;
3681
+ }
3682
+ function runToolSetup(tools, projectDir) {
3683
+ for (const tool of tools) {
3684
+ if (!tool.setup) continue;
3685
+ try {
3686
+ logger.info(` Setting up ${tool.name}: ${tool.setup}`);
3687
+ execSync3(tool.setup, { cwd: projectDir, timeout: 12e4, stdio: "pipe" });
3688
+ logger.info(` \u2713 ${tool.name} setup complete`);
3689
+ } catch (err) {
3690
+ logger.warn(` \u26A0 ${tool.name} setup failed: ${err instanceof Error ? err.message : String(err)}`);
3691
+ }
3692
+ }
3693
+ }
3694
+ function getToolSkillsForStage(tools, stageName) {
3695
+ const matched = tools.filter((t) => t.stages.includes(stageName) && t.skillContent);
3696
+ if (matched.length === 0) return "";
3697
+ const sections = matched.map((t) => `### ${t.name}
3698
+
3699
+ ${t.skillContent}`);
3700
+ return `## Available Tools
3701
+
3702
+ The following tools are installed and ready to use in this environment.
3703
+
3704
+ ${sections.join("\n\n")}`;
3705
+ }
3706
+ var init_tools = __esm({
3707
+ "src/tools.ts"() {
3708
+ "use strict";
3709
+ init_logger();
3710
+ }
3711
+ });
3712
+
3621
3713
  // src/context.ts
3622
- import * as fs18 from "fs";
3623
- import * as path16 from "path";
3714
+ import * as fs20 from "fs";
3715
+ import * as path18 from "path";
3624
3716
  function readPromptFile(stageName, projectDir) {
3625
3717
  if (projectDir) {
3626
- const stepFile = path16.join(projectDir, ".kody", "steps", `${stageName}.md`);
3627
- if (fs18.existsSync(stepFile)) {
3628
- return fs18.readFileSync(stepFile, "utf-8");
3718
+ const stepFile = path18.join(projectDir, ".kody", "steps", `${stageName}.md`);
3719
+ if (fs20.existsSync(stepFile)) {
3720
+ return fs20.readFileSync(stepFile, "utf-8");
3629
3721
  }
3630
3722
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
3631
3723
  }
3632
3724
  const scriptDir = new URL(".", import.meta.url).pathname;
3633
3725
  const candidates = [
3634
- path16.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
3635
- path16.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
3726
+ path18.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
3727
+ path18.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
3636
3728
  ];
3637
3729
  for (const candidate of candidates) {
3638
- if (fs18.existsSync(candidate)) {
3639
- return fs18.readFileSync(candidate, "utf-8");
3730
+ if (fs20.existsSync(candidate)) {
3731
+ return fs20.readFileSync(candidate, "utf-8");
3640
3732
  }
3641
3733
  }
3642
3734
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -3648,18 +3740,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
3648
3740
  `;
3649
3741
  context += `Task Directory: ${taskDir}
3650
3742
  `;
3651
- const taskMdPath = path16.join(taskDir, "task.md");
3652
- if (fs18.existsSync(taskMdPath)) {
3653
- const taskMd = fs18.readFileSync(taskMdPath, "utf-8");
3743
+ const taskMdPath = path18.join(taskDir, "task.md");
3744
+ if (fs20.existsSync(taskMdPath)) {
3745
+ const taskMd = fs20.readFileSync(taskMdPath, "utf-8");
3654
3746
  context += `
3655
3747
  ## Task Description
3656
3748
  ${taskMd}
3657
3749
  `;
3658
3750
  }
3659
- const taskJsonPath = path16.join(taskDir, "task.json");
3660
- if (fs18.existsSync(taskJsonPath)) {
3751
+ const taskJsonPath = path18.join(taskDir, "task.json");
3752
+ if (fs20.existsSync(taskJsonPath)) {
3661
3753
  try {
3662
- const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
3754
+ const taskDef = JSON.parse(fs20.readFileSync(taskJsonPath, "utf-8"));
3663
3755
  context += `
3664
3756
  ## Task Classification
3665
3757
  `;
@@ -3672,27 +3764,27 @@ ${taskMd}
3672
3764
  } catch {
3673
3765
  }
3674
3766
  }
3675
- const specPath = path16.join(taskDir, "spec.md");
3676
- if (fs18.existsSync(specPath)) {
3677
- const spec = fs18.readFileSync(specPath, "utf-8");
3767
+ const specPath = path18.join(taskDir, "spec.md");
3768
+ if (fs20.existsSync(specPath)) {
3769
+ const spec = fs20.readFileSync(specPath, "utf-8");
3678
3770
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
3679
3771
  context += `
3680
3772
  ## Spec Summary
3681
3773
  ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
3682
3774
  `;
3683
3775
  }
3684
- const planPath = path16.join(taskDir, "plan.md");
3685
- if (fs18.existsSync(planPath)) {
3686
- const plan = fs18.readFileSync(planPath, "utf-8");
3776
+ const planPath = path18.join(taskDir, "plan.md");
3777
+ if (fs20.existsSync(planPath)) {
3778
+ const plan = fs20.readFileSync(planPath, "utf-8");
3687
3779
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
3688
3780
  context += `
3689
3781
  ## Plan Summary
3690
3782
  ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
3691
3783
  `;
3692
3784
  }
3693
- const contextMdPath = path16.join(taskDir, "context.md");
3694
- if (fs18.existsSync(contextMdPath)) {
3695
- const accumulated = fs18.readFileSync(contextMdPath, "utf-8");
3785
+ const contextMdPath = path18.join(taskDir, "context.md");
3786
+ if (fs20.existsSync(contextMdPath)) {
3787
+ const accumulated = fs20.readFileSync(contextMdPath, "utf-8");
3696
3788
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
3697
3789
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
3698
3790
  context += `
@@ -3710,17 +3802,17 @@ ${feedback}
3710
3802
  }
3711
3803
  function inferHasUIFromScope(scope) {
3712
3804
  return scope.some((filePath) => {
3713
- const ext = path16.extname(filePath).toLowerCase();
3805
+ const ext = path18.extname(filePath).toLowerCase();
3714
3806
  if (UI_EXTENSIONS.has(ext)) return true;
3715
3807
  const normalized = filePath.replace(/\\/g, "/");
3716
3808
  return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
3717
3809
  });
3718
3810
  }
3719
3811
  function taskHasUI(taskDir) {
3720
- const taskJsonPath = path16.join(taskDir, "task.json");
3721
- if (!fs18.existsSync(taskJsonPath)) return true;
3812
+ const taskJsonPath = path18.join(taskDir, "task.json");
3813
+ if (!fs20.existsSync(taskJsonPath)) return true;
3722
3814
  try {
3723
- const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
3815
+ const taskDef = JSON.parse(fs20.readFileSync(taskJsonPath, "utf-8"));
3724
3816
  const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
3725
3817
  if (scope.length === 0) return true;
3726
3818
  return inferHasUIFromScope(scope);
@@ -3826,7 +3918,7 @@ ${devServerBlock}
3826
3918
 
3827
3919
  Use browser tools to navigate to pages and take snapshots to verify UI output.`;
3828
3920
  }
3829
- function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
3921
+ function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback, tools) {
3830
3922
  const config = getProjectConfig();
3831
3923
  let assembled;
3832
3924
  if (config.contextTiers?.enabled) {
@@ -3842,12 +3934,18 @@ ${prompt}` : prompt;
3842
3934
  }
3843
3935
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
3844
3936
  assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
3845
- const qaGuidePath = path16.join(projectDir, ".kody", "qa-guide.md");
3846
- if (fs18.existsSync(qaGuidePath)) {
3847
- const qaGuide = fs18.readFileSync(qaGuidePath, "utf-8").trim();
3937
+ const qaGuidePath = path18.join(projectDir, ".kody", "qa-guide.md");
3938
+ if (fs20.existsSync(qaGuidePath)) {
3939
+ const qaGuide = fs20.readFileSync(qaGuidePath, "utf-8").trim();
3848
3940
  assembled = assembled + "\n\n" + qaGuide;
3849
3941
  }
3850
3942
  }
3943
+ if (tools?.length) {
3944
+ const toolSkills = getToolSkillsForStage(tools, stageName);
3945
+ if (toolSkills) {
3946
+ assembled = assembled + "\n\n" + toolSkills;
3947
+ }
3948
+ }
3851
3949
  return assembled;
3852
3950
  }
3853
3951
  function buildFullPromptTiered(stageName, taskId, taskDir, projectDir, feedback) {
@@ -3887,6 +3985,7 @@ var init_context = __esm({
3887
3985
  init_config();
3888
3986
  init_context_tiers();
3889
3987
  init_mcp_config();
3988
+ init_tools();
3890
3989
  MAX_TASK_CONTEXT_PLAN = 1500;
3891
3990
  MAX_TASK_CONTEXT_SPEC = 2e3;
3892
3991
  MAX_ACCUMULATED_CONTEXT = 4e3;
@@ -3936,8 +4035,8 @@ var init_runner_selection = __esm({
3936
4035
  });
3937
4036
 
3938
4037
  // src/stages/agent.ts
3939
- import * as fs19 from "fs";
3940
- import * as path17 from "path";
4038
+ import * as fs21 from "fs";
4039
+ import * as path19 from "path";
3941
4040
  function getSessionInfo(stageName, sessions) {
3942
4041
  const group = SESSION_GROUP[stageName];
3943
4042
  if (!group) return void 0;
@@ -3966,7 +4065,7 @@ async function executeAgentStage(ctx, def) {
3966
4065
  logger.info(` [dry-run] skipping ${def.name}`);
3967
4066
  return { outcome: "completed", retries: 0 };
3968
4067
  }
3969
- const prompt = buildFullPrompt(def.name, ctx.taskId, ctx.taskDir, ctx.projectDir, ctx.input.feedback);
4068
+ const prompt = buildFullPrompt(def.name, ctx.taskId, ctx.taskDir, ctx.projectDir, ctx.input.feedback, ctx.tools);
3970
4069
  let currentModelTier = def.modelTier;
3971
4070
  if (ctx.input.feedback && def.name === "build") {
3972
4071
  logger.info(` feedback: ${ctx.input.feedback.slice(0, 200)}${ctx.input.feedback.length > 200 ? "..." : ""}`);
@@ -4024,27 +4123,27 @@ async function executeAgentStage(ctx, def) {
4024
4123
  }
4025
4124
  const result2 = lastResult;
4026
4125
  if (def.outputFile && result2.output) {
4027
- fs19.writeFileSync(path17.join(ctx.taskDir, def.outputFile), result2.output);
4126
+ fs21.writeFileSync(path19.join(ctx.taskDir, def.outputFile), result2.output);
4028
4127
  }
4029
4128
  if (def.outputFile) {
4030
- const outputPath = path17.join(ctx.taskDir, def.outputFile);
4031
- if (!fs19.existsSync(outputPath)) {
4032
- const ext = path17.extname(def.outputFile);
4033
- const base = path17.basename(def.outputFile, ext);
4034
- const files = fs19.readdirSync(ctx.taskDir);
4129
+ const outputPath = path19.join(ctx.taskDir, def.outputFile);
4130
+ if (!fs21.existsSync(outputPath)) {
4131
+ const ext = path19.extname(def.outputFile);
4132
+ const base = path19.basename(def.outputFile, ext);
4133
+ const files = fs21.readdirSync(ctx.taskDir);
4035
4134
  const variant = files.find(
4036
4135
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
4037
4136
  );
4038
4137
  if (variant) {
4039
- fs19.renameSync(path17.join(ctx.taskDir, variant), outputPath);
4138
+ fs21.renameSync(path19.join(ctx.taskDir, variant), outputPath);
4040
4139
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
4041
4140
  }
4042
4141
  }
4043
4142
  }
4044
4143
  if (def.outputFile) {
4045
- const outputPath = path17.join(ctx.taskDir, def.outputFile);
4046
- if (fs19.existsSync(outputPath)) {
4047
- const content = fs19.readFileSync(outputPath, "utf-8");
4144
+ const outputPath = path19.join(ctx.taskDir, def.outputFile);
4145
+ if (fs21.existsSync(outputPath)) {
4146
+ const content = fs21.readFileSync(outputPath, "utf-8");
4048
4147
  const validation = validateStageOutput(def.name, content);
4049
4148
  if (!validation.valid) {
4050
4149
  if (def.name === "taskify") {
@@ -4058,7 +4157,7 @@ async function executeAgentStage(ctx, def) {
4058
4157
  const stripped = stripFences(retryResult.output);
4059
4158
  const retryValidation = validateTaskJson(stripped);
4060
4159
  if (retryValidation.valid) {
4061
- fs19.writeFileSync(outputPath, retryResult.output);
4160
+ fs21.writeFileSync(outputPath, retryResult.output);
4062
4161
  logger.info(` taskify retry produced valid JSON`);
4063
4162
  } else {
4064
4163
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -4071,7 +4170,7 @@ async function executeAgentStage(ctx, def) {
4071
4170
  risk_level: "low",
4072
4171
  questions: []
4073
4172
  }, null, 2);
4074
- fs19.writeFileSync(outputPath, fallback);
4173
+ fs21.writeFileSync(outputPath, fallback);
4075
4174
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
4076
4175
  }
4077
4176
  }
@@ -4085,7 +4184,7 @@ async function executeAgentStage(ctx, def) {
4085
4184
  return { outcome: "completed", outputFile: def.outputFile, retries };
4086
4185
  }
4087
4186
  function appendStageContext(taskDir, stageName, output) {
4088
- const contextPath = path17.join(taskDir, "context.md");
4187
+ const contextPath = path19.join(taskDir, "context.md");
4089
4188
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
4090
4189
  let summary;
4091
4190
  if (output && output.trim()) {
@@ -4098,7 +4197,7 @@ function appendStageContext(taskDir, stageName, output) {
4098
4197
  ### ${stageName} (${timestamp2})
4099
4198
  ${summary}
4100
4199
  `;
4101
- fs19.appendFileSync(contextPath, entry);
4200
+ fs21.appendFileSync(contextPath, entry);
4102
4201
  }
4103
4202
  var SESSION_GROUP;
4104
4203
  var init_agent = __esm({
@@ -4378,8 +4477,8 @@ Error context:
4378
4477
  });
4379
4478
 
4380
4479
  // src/stages/gate.ts
4381
- import * as fs20 from "fs";
4382
- import * as path18 from "path";
4480
+ import * as fs22 from "fs";
4481
+ import * as path20 from "path";
4383
4482
  function executeGateStage(ctx, def) {
4384
4483
  if (ctx.input.dryRun) {
4385
4484
  logger.info(` [dry-run] skipping ${def.name}`);
@@ -4422,7 +4521,7 @@ ${output}
4422
4521
  `);
4423
4522
  }
4424
4523
  }
4425
- fs20.writeFileSync(path18.join(ctx.taskDir, "verify.md"), lines.join(""));
4524
+ fs22.writeFileSync(path20.join(ctx.taskDir, "verify.md"), lines.join(""));
4426
4525
  return {
4427
4526
  outcome: verifyResult.pass ? "completed" : "failed",
4428
4527
  retries: 0
@@ -4437,8 +4536,8 @@ var init_gate = __esm({
4437
4536
  });
4438
4537
 
4439
4538
  // src/stages/verify.ts
4440
- import * as fs21 from "fs";
4441
- import * as path19 from "path";
4539
+ import * as fs23 from "fs";
4540
+ import * as path21 from "path";
4442
4541
  import { execFileSync as execFileSync14 } from "child_process";
4443
4542
  async function executeVerifyWithAutofix(ctx, def) {
4444
4543
  const maxAttempts = def.maxRetries ?? 2;
@@ -4449,8 +4548,8 @@ async function executeVerifyWithAutofix(ctx, def) {
4449
4548
  return { ...gateResult, retries: attempt };
4450
4549
  }
4451
4550
  if (attempt < maxAttempts) {
4452
- const verifyPath = path19.join(ctx.taskDir, "verify.md");
4453
- const errorOutput = fs21.existsSync(verifyPath) ? fs21.readFileSync(verifyPath, "utf-8") : "Unknown error";
4551
+ const verifyPath = path21.join(ctx.taskDir, "verify.md");
4552
+ const errorOutput = fs23.existsSync(verifyPath) ? fs23.readFileSync(verifyPath, "utf-8") : "Unknown error";
4454
4553
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
4455
4554
  const defaultRunner = getRunnerForStage(ctx, "taskify");
4456
4555
  const diagConfig = getProjectConfig();
@@ -4546,8 +4645,8 @@ var init_verify = __esm({
4546
4645
  });
4547
4646
 
4548
4647
  // src/review-standalone.ts
4549
- import * as fs22 from "fs";
4550
- import * as path20 from "path";
4648
+ import * as fs24 from "fs";
4649
+ import * as path22 from "path";
4551
4650
  function resolveReviewTarget(input) {
4552
4651
  if (input.prs.length === 0) {
4553
4652
  return {
@@ -4571,8 +4670,8 @@ Or comment on the specific PR: \`@kody review\``
4571
4670
  }
4572
4671
  async function runStandaloneReview(input) {
4573
4672
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
4574
- const taskDir = path20.join(input.projectDir, ".kody", "tasks", taskId);
4575
- fs22.mkdirSync(taskDir, { recursive: true });
4673
+ const taskDir = path22.join(input.projectDir, ".kody", "tasks", taskId);
4674
+ fs24.mkdirSync(taskDir, { recursive: true });
4576
4675
  let diffInstruction = "";
4577
4676
  let filesChangedSection = "";
4578
4677
  if (input.baseBranch) {
@@ -4599,7 +4698,7 @@ ${fileList}`;
4599
4698
  const taskContent = `# ${input.prTitle}
4600
4699
 
4601
4700
  ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
4602
- fs22.writeFileSync(path20.join(taskDir, "task.md"), taskContent);
4701
+ fs24.writeFileSync(path22.join(taskDir, "task.md"), taskContent);
4603
4702
  const reviewDef = STAGES.find((s) => s.name === "review");
4604
4703
  const ctx = {
4605
4704
  taskId,
@@ -4621,10 +4720,10 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
4621
4720
  error: result2.error ?? "Review stage failed"
4622
4721
  };
4623
4722
  }
4624
- const reviewPath = path20.join(taskDir, "review.md");
4723
+ const reviewPath = path22.join(taskDir, "review.md");
4625
4724
  let reviewContent;
4626
- if (fs22.existsSync(reviewPath)) {
4627
- reviewContent = fs22.readFileSync(reviewPath, "utf-8");
4725
+ if (fs24.existsSync(reviewPath)) {
4726
+ reviewContent = fs24.readFileSync(reviewPath, "utf-8");
4628
4727
  }
4629
4728
  return {
4630
4729
  outcome: "completed",
@@ -4664,8 +4763,8 @@ var init_review_standalone = __esm({
4664
4763
  });
4665
4764
 
4666
4765
  // src/stages/review.ts
4667
- import * as fs23 from "fs";
4668
- import * as path21 from "path";
4766
+ import * as fs25 from "fs";
4767
+ import * as path23 from "path";
4669
4768
  async function executeReviewWithFix(ctx, def) {
4670
4769
  if (ctx.input.dryRun) {
4671
4770
  return { outcome: "completed", retries: 0 };
@@ -4679,11 +4778,11 @@ async function executeReviewWithFix(ctx, def) {
4679
4778
  if (reviewResult.outcome !== "completed") {
4680
4779
  return reviewResult;
4681
4780
  }
4682
- const reviewFile = path21.join(ctx.taskDir, "review.md");
4683
- if (!fs23.existsSync(reviewFile)) {
4781
+ const reviewFile = path23.join(ctx.taskDir, "review.md");
4782
+ if (!fs25.existsSync(reviewFile)) {
4684
4783
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
4685
4784
  }
4686
- const content = fs23.readFileSync(reviewFile, "utf-8");
4785
+ const content = fs25.readFileSync(reviewFile, "utf-8");
4687
4786
  if (detectReviewVerdict(content) !== "fail") {
4688
4787
  return { ...reviewResult, retries: iteration };
4689
4788
  }
@@ -4712,15 +4811,15 @@ var init_review = __esm({
4712
4811
  });
4713
4812
 
4714
4813
  // src/stages/ship.ts
4715
- import * as fs24 from "fs";
4716
- import * as path22 from "path";
4814
+ import * as fs26 from "fs";
4815
+ import * as path24 from "path";
4717
4816
  import { execFileSync as execFileSync15 } from "child_process";
4718
4817
  function buildPrBody(ctx) {
4719
4818
  const sections = [];
4720
- const taskJsonPath = path22.join(ctx.taskDir, "task.json");
4721
- if (fs24.existsSync(taskJsonPath)) {
4819
+ const taskJsonPath = path24.join(ctx.taskDir, "task.json");
4820
+ if (fs26.existsSync(taskJsonPath)) {
4722
4821
  try {
4723
- const raw = fs24.readFileSync(taskJsonPath, "utf-8");
4822
+ const raw = fs26.readFileSync(taskJsonPath, "utf-8");
4724
4823
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
4725
4824
  const task = JSON.parse(cleaned);
4726
4825
  if (task.description) {
@@ -4739,9 +4838,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
4739
4838
  } catch {
4740
4839
  }
4741
4840
  }
4742
- const reviewPath = path22.join(ctx.taskDir, "review.md");
4743
- if (fs24.existsSync(reviewPath)) {
4744
- const review = fs24.readFileSync(reviewPath, "utf-8");
4841
+ const reviewPath = path24.join(ctx.taskDir, "review.md");
4842
+ if (fs26.existsSync(reviewPath)) {
4843
+ const review = fs26.readFileSync(reviewPath, "utf-8");
4745
4844
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
4746
4845
  if (summaryMatch) {
4747
4846
  const summary = summaryMatch[1].trim();
@@ -4758,14 +4857,14 @@ ${summary}`);
4758
4857
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
4759
4858
  }
4760
4859
  }
4761
- const verifyPath = path22.join(ctx.taskDir, "verify.md");
4762
- if (fs24.existsSync(verifyPath)) {
4763
- const verify = fs24.readFileSync(verifyPath, "utf-8");
4860
+ const verifyPath = path24.join(ctx.taskDir, "verify.md");
4861
+ if (fs26.existsSync(verifyPath)) {
4862
+ const verify = fs26.readFileSync(verifyPath, "utf-8");
4764
4863
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
4765
4864
  }
4766
- const planPath = path22.join(ctx.taskDir, "plan.md");
4767
- if (fs24.existsSync(planPath)) {
4768
- const plan = fs24.readFileSync(planPath, "utf-8").trim();
4865
+ const planPath = path24.join(ctx.taskDir, "plan.md");
4866
+ if (fs26.existsSync(planPath)) {
4867
+ const plan = fs26.readFileSync(planPath, "utf-8").trim();
4769
4868
  if (plan) {
4770
4869
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
4771
4870
  sections.push(`
@@ -4785,13 +4884,13 @@ Closes #${ctx.input.issueNumber}`);
4785
4884
  return sections.join("\n");
4786
4885
  }
4787
4886
  function executeShipStage(ctx, _def) {
4788
- const shipPath = path22.join(ctx.taskDir, "ship.md");
4887
+ const shipPath = path24.join(ctx.taskDir, "ship.md");
4789
4888
  if (ctx.input.dryRun) {
4790
- fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
4889
+ fs26.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
4791
4890
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
4792
4891
  }
4793
4892
  if (ctx.input.local && !ctx.input.issueNumber) {
4794
- fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
4893
+ fs26.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
4795
4894
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
4796
4895
  }
4797
4896
  try {
@@ -4838,28 +4937,28 @@ function executeShipStage(ctx, _def) {
4838
4937
  chore: "chore"
4839
4938
  };
4840
4939
  let prefix = "chore";
4841
- const taskJsonPath = path22.join(ctx.taskDir, "task.json");
4842
- if (fs24.existsSync(taskJsonPath)) {
4940
+ const taskJsonPath = path24.join(ctx.taskDir, "task.json");
4941
+ if (fs26.existsSync(taskJsonPath)) {
4843
4942
  try {
4844
- const raw = fs24.readFileSync(taskJsonPath, "utf-8");
4943
+ const raw = fs26.readFileSync(taskJsonPath, "utf-8");
4845
4944
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
4846
4945
  const task = JSON.parse(cleaned);
4847
4946
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
4848
4947
  } catch {
4849
4948
  }
4850
4949
  }
4851
- const taskMdPath = path22.join(ctx.taskDir, "task.md");
4852
- if (fs24.existsSync(taskMdPath)) {
4853
- const content = fs24.readFileSync(taskMdPath, "utf-8");
4950
+ const taskMdPath = path24.join(ctx.taskDir, "task.md");
4951
+ if (fs26.existsSync(taskMdPath)) {
4952
+ const content = fs26.readFileSync(taskMdPath, "utf-8");
4854
4953
  const heading = content.split("\n").find((l) => l.startsWith("# "));
4855
4954
  if (heading) {
4856
4955
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
4857
4956
  }
4858
4957
  }
4859
4958
  if (title === "Update") {
4860
- if (fs24.existsSync(taskJsonPath)) {
4959
+ if (fs26.existsSync(taskJsonPath)) {
4861
4960
  try {
4862
- const raw = fs24.readFileSync(taskJsonPath, "utf-8");
4961
+ const raw = fs26.readFileSync(taskJsonPath, "utf-8");
4863
4962
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
4864
4963
  const task = JSON.parse(cleaned);
4865
4964
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -4882,7 +4981,7 @@ function executeShipStage(ctx, _def) {
4882
4981
  } catch {
4883
4982
  }
4884
4983
  }
4885
- fs24.writeFileSync(shipPath, `# Ship
4984
+ fs26.writeFileSync(shipPath, `# Ship
4886
4985
 
4887
4986
  Updated existing PR: ${existingPr.url}
4888
4987
  PR #${existingPr.number}
@@ -4903,20 +5002,20 @@ PR #${existingPr.number}
4903
5002
  } catch {
4904
5003
  }
4905
5004
  }
4906
- fs24.writeFileSync(shipPath, `# Ship
5005
+ fs26.writeFileSync(shipPath, `# Ship
4907
5006
 
4908
5007
  PR created: ${pr.url}
4909
5008
  PR #${pr.number}
4910
5009
  `);
4911
5010
  } else {
4912
- fs24.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
5011
+ fs26.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
4913
5012
  }
4914
5013
  }
4915
5014
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
4916
5015
  } catch (err) {
4917
5016
  const msg = err instanceof Error ? err.message : String(err);
4918
5017
  try {
4919
- fs24.writeFileSync(shipPath, `# Ship
5018
+ fs26.writeFileSync(shipPath, `# Ship
4920
5019
 
4921
5020
  Failed: ${msg}
4922
5021
  `);
@@ -4965,15 +5064,15 @@ var init_executor_registry = __esm({
4965
5064
  });
4966
5065
 
4967
5066
  // src/pipeline/questions.ts
4968
- import * as fs25 from "fs";
4969
- import * as path23 from "path";
5067
+ import * as fs27 from "fs";
5068
+ import * as path25 from "path";
4970
5069
  function checkForQuestions(ctx, stageName) {
4971
5070
  if (ctx.input.local || !ctx.input.issueNumber) return false;
4972
5071
  try {
4973
5072
  if (stageName === "taskify") {
4974
- const taskJsonPath = path23.join(ctx.taskDir, "task.json");
4975
- if (!fs25.existsSync(taskJsonPath)) return false;
4976
- const raw = fs25.readFileSync(taskJsonPath, "utf-8");
5073
+ const taskJsonPath = path25.join(ctx.taskDir, "task.json");
5074
+ if (!fs27.existsSync(taskJsonPath)) return false;
5075
+ const raw = fs27.readFileSync(taskJsonPath, "utf-8");
4977
5076
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
4978
5077
  const taskJson = JSON.parse(cleaned);
4979
5078
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -4988,9 +5087,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
4988
5087
  }
4989
5088
  }
4990
5089
  if (stageName === "plan") {
4991
- const planPath = path23.join(ctx.taskDir, "plan.md");
4992
- if (!fs25.existsSync(planPath)) return false;
4993
- const plan = fs25.readFileSync(planPath, "utf-8");
5090
+ const planPath = path25.join(ctx.taskDir, "plan.md");
5091
+ if (!fs27.existsSync(planPath)) return false;
5092
+ const plan = fs27.readFileSync(planPath, "utf-8");
4994
5093
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
4995
5094
  if (questionsMatch) {
4996
5095
  const questionsText = questionsMatch[1].trim();
@@ -5019,12 +5118,15 @@ var init_questions = __esm({
5019
5118
  });
5020
5119
 
5021
5120
  // src/pipeline/hooks.ts
5022
- import * as fs26 from "fs";
5023
- import * as path24 from "path";
5121
+ import * as fs28 from "fs";
5122
+ import * as path26 from "path";
5024
5123
  function applyPreStageLabel(ctx, def) {
5025
5124
  if (!ctx.input.issueNumber || ctx.input.local) return;
5125
+ if (def.name === "plan") setLifecycleLabel(ctx.input.issueNumber, "planning");
5026
5126
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
5127
+ if (def.name === "verify") setLifecycleLabel(ctx.input.issueNumber, "verifying");
5027
5128
  if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
5129
+ if (def.name === "review-fix") setLifecycleLabel(ctx.input.issueNumber, "fixing");
5028
5130
  if (def.name === "ship") setLifecycleLabel(ctx.input.issueNumber, "shipping");
5029
5131
  }
5030
5132
  function checkQuestionsAfterStage(ctx, def, state) {
@@ -5058,9 +5160,9 @@ function autoDetectComplexity(ctx, def) {
5058
5160
  return { complexity, activeStages };
5059
5161
  }
5060
5162
  try {
5061
- const taskJsonPath = path24.join(ctx.taskDir, "task.json");
5062
- if (!fs26.existsSync(taskJsonPath)) return null;
5063
- const raw = fs26.readFileSync(taskJsonPath, "utf-8");
5163
+ const taskJsonPath = path26.join(ctx.taskDir, "task.json");
5164
+ if (!fs28.existsSync(taskJsonPath)) return null;
5165
+ const raw = fs28.readFileSync(taskJsonPath, "utf-8");
5064
5166
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
5065
5167
  const taskJson = JSON.parse(cleaned);
5066
5168
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -5090,8 +5192,8 @@ function checkRiskGate(ctx, def, state, complexity) {
5090
5192
  if (ctx.input.dryRun || ctx.input.local) return null;
5091
5193
  if (ctx.input.mode === "rerun") return null;
5092
5194
  if (!ctx.input.issueNumber) return null;
5093
- const planPath = path24.join(ctx.taskDir, "plan.md");
5094
- const plan = fs26.existsSync(planPath) ? fs26.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
5195
+ const planPath = path26.join(ctx.taskDir, "plan.md");
5196
+ const plan = fs28.existsSync(planPath) ? fs28.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
5095
5197
  try {
5096
5198
  postComment(
5097
5199
  ctx.input.issueNumber,
@@ -5158,22 +5260,22 @@ var init_hooks = __esm({
5158
5260
  });
5159
5261
 
5160
5262
  // src/learning/auto-learn.ts
5161
- import * as fs27 from "fs";
5162
- import * as path25 from "path";
5263
+ import * as fs29 from "fs";
5264
+ import * as path27 from "path";
5163
5265
  function stripAnsi(str) {
5164
5266
  return str.replace(/\x1b\[[0-9;]*m/g, "");
5165
5267
  }
5166
5268
  function autoLearn(ctx) {
5167
5269
  try {
5168
- const memoryDir = path25.join(ctx.projectDir, ".kody", "memory");
5169
- if (!fs27.existsSync(memoryDir)) {
5170
- fs27.mkdirSync(memoryDir, { recursive: true });
5270
+ const memoryDir = path27.join(ctx.projectDir, ".kody", "memory");
5271
+ if (!fs29.existsSync(memoryDir)) {
5272
+ fs29.mkdirSync(memoryDir, { recursive: true });
5171
5273
  }
5172
5274
  const learnings = [];
5173
5275
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5174
- const verifyPath = path25.join(ctx.taskDir, "verify.md");
5175
- if (fs27.existsSync(verifyPath)) {
5176
- const verify = stripAnsi(fs27.readFileSync(verifyPath, "utf-8"));
5276
+ const verifyPath = path27.join(ctx.taskDir, "verify.md");
5277
+ if (fs29.existsSync(verifyPath)) {
5278
+ const verify = stripAnsi(fs29.readFileSync(verifyPath, "utf-8"));
5177
5279
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
5178
5280
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
5179
5281
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -5182,18 +5284,18 @@ function autoLearn(ctx) {
5182
5284
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
5183
5285
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
5184
5286
  }
5185
- const reviewPath = path25.join(ctx.taskDir, "review.md");
5186
- if (fs27.existsSync(reviewPath)) {
5187
- const review = fs27.readFileSync(reviewPath, "utf-8");
5287
+ const reviewPath = path27.join(ctx.taskDir, "review.md");
5288
+ if (fs29.existsSync(reviewPath)) {
5289
+ const review = fs29.readFileSync(reviewPath, "utf-8");
5188
5290
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
5189
5291
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
5190
5292
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
5191
5293
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
5192
5294
  }
5193
- const taskJsonPath = path25.join(ctx.taskDir, "task.json");
5194
- if (fs27.existsSync(taskJsonPath)) {
5295
+ const taskJsonPath = path27.join(ctx.taskDir, "task.json");
5296
+ if (fs29.existsSync(taskJsonPath)) {
5195
5297
  try {
5196
- const raw = stripAnsi(fs27.readFileSync(taskJsonPath, "utf-8"));
5298
+ const raw = stripAnsi(fs29.readFileSync(taskJsonPath, "utf-8"));
5197
5299
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
5198
5300
  const task = JSON.parse(cleaned);
5199
5301
  if (task.scope && Array.isArray(task.scope)) {
@@ -5204,12 +5306,12 @@ function autoLearn(ctx) {
5204
5306
  }
5205
5307
  }
5206
5308
  if (learnings.length > 0) {
5207
- const conventionsPath = path25.join(memoryDir, "conventions.md");
5309
+ const conventionsPath = path27.join(memoryDir, "conventions.md");
5208
5310
  const entry = `
5209
5311
  ## Learned ${timestamp2} (task: ${ctx.taskId})
5210
5312
  ${learnings.join("\n")}
5211
5313
  `;
5212
- fs27.appendFileSync(conventionsPath, entry);
5314
+ fs29.appendFileSync(conventionsPath, entry);
5213
5315
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
5214
5316
  }
5215
5317
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
@@ -5217,8 +5319,8 @@ ${learnings.join("\n")}
5217
5319
  }
5218
5320
  }
5219
5321
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
5220
- const archPath = path25.join(memoryDir, "architecture.md");
5221
- if (fs27.existsSync(archPath)) return;
5322
+ const archPath = path27.join(memoryDir, "architecture.md");
5323
+ if (fs29.existsSync(archPath)) return;
5222
5324
  const detected = detectArchitectureBasic(projectDir);
5223
5325
  if (detected.length > 0) {
5224
5326
  const content = `# Architecture (auto-detected ${timestamp2})
@@ -5226,7 +5328,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
5226
5328
  ## Overview
5227
5329
  ${detected.join("\n")}
5228
5330
  `;
5229
- fs27.writeFileSync(archPath, content);
5331
+ fs29.writeFileSync(archPath, content);
5230
5332
  logger.info(`Auto-detected architecture (${detected.length} items)`);
5231
5333
  }
5232
5334
  }
@@ -5239,13 +5341,13 @@ var init_auto_learn = __esm({
5239
5341
  });
5240
5342
 
5241
5343
  // src/retrospective.ts
5242
- import * as fs28 from "fs";
5243
- import * as path26 from "path";
5344
+ import * as fs30 from "fs";
5345
+ import * as path28 from "path";
5244
5346
  function readArtifact(taskDir, filename, maxChars) {
5245
- const p = path26.join(taskDir, filename);
5246
- if (!fs28.existsSync(p)) return null;
5347
+ const p = path28.join(taskDir, filename);
5348
+ if (!fs30.existsSync(p)) return null;
5247
5349
  try {
5248
- const content = fs28.readFileSync(p, "utf-8");
5350
+ const content = fs30.readFileSync(p, "utf-8");
5249
5351
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
5250
5352
  } catch {
5251
5353
  return null;
@@ -5298,13 +5400,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
5298
5400
  return lines.join("\n");
5299
5401
  }
5300
5402
  function getLogPath(projectDir) {
5301
- return path26.join(projectDir, ".kody", "memory", "observer-log.jsonl");
5403
+ return path28.join(projectDir, ".kody", "memory", "observer-log.jsonl");
5302
5404
  }
5303
5405
  function readPreviousRetrospectives(projectDir, limit = 10) {
5304
5406
  const logPath = getLogPath(projectDir);
5305
- if (!fs28.existsSync(logPath)) return [];
5407
+ if (!fs30.existsSync(logPath)) return [];
5306
5408
  try {
5307
- const content = fs28.readFileSync(logPath, "utf-8");
5409
+ const content = fs30.readFileSync(logPath, "utf-8");
5308
5410
  const lines = content.split("\n").filter(Boolean);
5309
5411
  const entries = [];
5310
5412
  const start = Math.max(0, lines.length - limit);
@@ -5331,11 +5433,11 @@ function formatPreviousEntries(entries) {
5331
5433
  }
5332
5434
  function appendRetrospectiveEntry(projectDir, entry) {
5333
5435
  const logPath = getLogPath(projectDir);
5334
- const dir = path26.dirname(logPath);
5335
- if (!fs28.existsSync(dir)) {
5336
- fs28.mkdirSync(dir, { recursive: true });
5436
+ const dir = path28.dirname(logPath);
5437
+ if (!fs30.existsSync(dir)) {
5438
+ fs30.mkdirSync(dir, { recursive: true });
5337
5439
  }
5338
- fs28.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5440
+ fs30.appendFileSync(logPath, JSON.stringify(entry) + "\n");
5339
5441
  }
5340
5442
  async function runRetrospective(ctx, state, pipelineStartTime) {
5341
5443
  if (ctx.input.dryRun) return;
@@ -5503,8 +5605,8 @@ var init_summary = __esm({
5503
5605
  });
5504
5606
 
5505
5607
  // src/pipeline.ts
5506
- import * as fs29 from "fs";
5507
- import * as path27 from "path";
5608
+ import * as fs31 from "fs";
5609
+ import * as path29 from "path";
5508
5610
  function ensureFeatureBranchIfNeeded(ctx) {
5509
5611
  if (ctx.input.dryRun) return;
5510
5612
  if (ctx.input.prNumber) {
@@ -5517,8 +5619,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
5517
5619
  }
5518
5620
  if (!ctx.input.issueNumber) return;
5519
5621
  try {
5520
- const taskMdPath = path27.join(ctx.taskDir, "task.md");
5521
- const title = fs29.existsSync(taskMdPath) ? fs29.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
5622
+ const taskMdPath = path29.join(ctx.taskDir, "task.md");
5623
+ const title = fs31.existsSync(taskMdPath) ? fs31.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
5522
5624
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
5523
5625
  syncWithDefault(ctx.projectDir);
5524
5626
  } catch (err) {
@@ -5532,10 +5634,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
5532
5634
  }
5533
5635
  }
5534
5636
  function acquireLock(taskDir) {
5535
- const lockPath = path27.join(taskDir, ".lock");
5536
- if (fs29.existsSync(lockPath)) {
5637
+ const lockPath = path29.join(taskDir, ".lock");
5638
+ if (fs31.existsSync(lockPath)) {
5537
5639
  try {
5538
- const pid = parseInt(fs29.readFileSync(lockPath, "utf-8").trim(), 10);
5640
+ const pid = parseInt(fs31.readFileSync(lockPath, "utf-8").trim(), 10);
5539
5641
  if (!isNaN(pid)) {
5540
5642
  try {
5541
5643
  process.kill(pid, 0);
@@ -5552,14 +5654,14 @@ function acquireLock(taskDir) {
5552
5654
  logger.warn(` Corrupt lock file \u2014 overwriting`);
5553
5655
  }
5554
5656
  try {
5555
- fs29.unlinkSync(lockPath);
5657
+ fs31.unlinkSync(lockPath);
5556
5658
  } catch {
5557
5659
  }
5558
5660
  }
5559
5661
  try {
5560
- const fd = fs29.openSync(lockPath, fs29.constants.O_WRONLY | fs29.constants.O_CREAT | fs29.constants.O_EXCL);
5561
- fs29.writeSync(fd, String(process.pid));
5562
- fs29.closeSync(fd);
5662
+ const fd = fs31.openSync(lockPath, fs31.constants.O_WRONLY | fs31.constants.O_CREAT | fs31.constants.O_EXCL);
5663
+ fs31.writeSync(fd, String(process.pid));
5664
+ fs31.closeSync(fd);
5563
5665
  } catch (err) {
5564
5666
  if (err.code === "EEXIST") {
5565
5667
  throw new Error("Pipeline already running (lock acquired by another process)");
@@ -5569,7 +5671,7 @@ function acquireLock(taskDir) {
5569
5671
  }
5570
5672
  function releaseLock(taskDir) {
5571
5673
  try {
5572
- fs29.unlinkSync(path27.join(taskDir, ".lock"));
5674
+ fs31.unlinkSync(path29.join(taskDir, ".lock"));
5573
5675
  } catch {
5574
5676
  }
5575
5677
  }
@@ -5628,6 +5730,11 @@ async function runPipelineInner(ctx) {
5628
5730
  setLifecycleLabel(ctx.input.issueNumber, initialPhase);
5629
5731
  }
5630
5732
  ensureFeatureBranchIfNeeded(ctx);
5733
+ if (ctx.tools?.length) {
5734
+ ciGroup("Tool Setup");
5735
+ runToolSetup(ctx.tools, ctx.projectDir);
5736
+ ciGroupEnd();
5737
+ }
5631
5738
  let complexity = ctx.input.complexity ?? "high";
5632
5739
  let activeStages = filterByComplexity(STAGES, complexity);
5633
5740
  let skippedStagesCommentPosted = false;
@@ -5773,12 +5880,13 @@ var init_pipeline = __esm({
5773
5880
  init_retrospective();
5774
5881
  init_summary();
5775
5882
  init_config();
5883
+ init_tools();
5776
5884
  }
5777
5885
  });
5778
5886
 
5779
5887
  // src/preflight.ts
5780
5888
  import { execFileSync as execFileSync16 } from "child_process";
5781
- import * as fs30 from "fs";
5889
+ import * as fs32 from "fs";
5782
5890
  function check(name, fn) {
5783
5891
  try {
5784
5892
  const detail = fn() ?? void 0;
@@ -5831,7 +5939,7 @@ function runPreflight() {
5831
5939
  return v;
5832
5940
  }),
5833
5941
  check("package.json", () => {
5834
- if (!fs30.existsSync("package.json")) throw new Error("not found");
5942
+ if (!fs32.existsSync("package.json")) throw new Error("not found");
5835
5943
  })
5836
5944
  ];
5837
5945
  const failed = checks.filter((c) => !c.ok);
@@ -5908,8 +6016,8 @@ var init_args = __esm({
5908
6016
  });
5909
6017
 
5910
6018
  // src/cli/task-state.ts
5911
- import * as fs31 from "fs";
5912
- import * as path28 from "path";
6019
+ import * as fs33 from "fs";
6020
+ import * as path30 from "path";
5913
6021
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
5914
6022
  if (!existingTaskId || !existingState) {
5915
6023
  return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
@@ -5941,11 +6049,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
5941
6049
  function resolveForIssue(issueNumber, projectDir) {
5942
6050
  const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
5943
6051
  if (existingTaskId) {
5944
- const statusPath = path28.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
6052
+ const statusPath = path30.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
5945
6053
  let existingState = null;
5946
- if (fs31.existsSync(statusPath)) {
6054
+ if (fs33.existsSync(statusPath)) {
5947
6055
  try {
5948
- existingState = JSON.parse(fs31.readFileSync(statusPath, "utf-8"));
6056
+ existingState = JSON.parse(fs33.readFileSync(statusPath, "utf-8"));
5949
6057
  } catch {
5950
6058
  }
5951
6059
  }
@@ -6102,8 +6210,8 @@ var init_resolve = __esm({
6102
6210
 
6103
6211
  // src/entry.ts
6104
6212
  var entry_exports = {};
6105
- import * as fs32 from "fs";
6106
- import * as path29 from "path";
6213
+ import * as fs34 from "fs";
6214
+ import * as path31 from "path";
6107
6215
  async function ensureLitellmProxy(config, projectDir) {
6108
6216
  if (!anyStageNeedsProxy(config)) return null;
6109
6217
  const litellmUrl = getLitellmUrl();
@@ -6158,9 +6266,9 @@ async function runModelHealthCheck(config) {
6158
6266
  }
6159
6267
  async function main() {
6160
6268
  const input = parseArgs();
6161
- const projectDir = input.cwd ? path29.resolve(input.cwd) : process.cwd();
6269
+ const projectDir = input.cwd ? path31.resolve(input.cwd) : process.cwd();
6162
6270
  if (input.cwd) {
6163
- if (!fs32.existsSync(projectDir)) {
6271
+ if (!fs34.existsSync(projectDir)) {
6164
6272
  console.error(`--cwd path does not exist: ${projectDir}`);
6165
6273
  process.exit(1);
6166
6274
  }
@@ -6220,8 +6328,8 @@ async function main() {
6220
6328
  process.exit(1);
6221
6329
  }
6222
6330
  }
6223
- const taskDir = path29.join(projectDir, ".kody", "tasks", taskId);
6224
- fs32.mkdirSync(taskDir, { recursive: true });
6331
+ const taskDir = path31.join(projectDir, ".kody", "tasks", taskId);
6332
+ fs34.mkdirSync(taskDir, { recursive: true });
6225
6333
  if (input.command === "rerun" && isTaskifyRun(taskDir)) {
6226
6334
  const marker = readTaskifyMarker(taskDir);
6227
6335
  if (marker) {
@@ -6353,31 +6461,31 @@ async function main() {
6353
6461
  logger.info("Preflight checks:");
6354
6462
  runPreflight();
6355
6463
  if (input.task) {
6356
- fs32.writeFileSync(path29.join(taskDir, "task.md"), input.task);
6464
+ fs34.writeFileSync(path31.join(taskDir, "task.md"), input.task);
6357
6465
  }
6358
- const taskMdPath = path29.join(taskDir, "task.md");
6359
- if (!fs32.existsSync(taskMdPath) && isPRFix && input.prNumber) {
6466
+ const taskMdPath = path31.join(taskDir, "task.md");
6467
+ if (!fs34.existsSync(taskMdPath) && isPRFix && input.prNumber) {
6360
6468
  logger.info(`Fetching PR #${input.prNumber} details as task context...`);
6361
6469
  const prDetails = getPRDetails(input.prNumber);
6362
6470
  if (prDetails) {
6363
6471
  const taskContent = `# ${prDetails.title}
6364
6472
 
6365
6473
  ${prDetails.body ?? ""}`;
6366
- fs32.writeFileSync(taskMdPath, taskContent);
6474
+ fs34.writeFileSync(taskMdPath, taskContent);
6367
6475
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
6368
6476
  }
6369
- } else if (!fs32.existsSync(taskMdPath) && input.issueNumber) {
6477
+ } else if (!fs34.existsSync(taskMdPath) && input.issueNumber) {
6370
6478
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
6371
6479
  const issue = getIssue(input.issueNumber);
6372
6480
  if (issue) {
6373
6481
  const taskContent = `# ${issue.title}
6374
6482
 
6375
6483
  ${issue.body ?? ""}`;
6376
- fs32.writeFileSync(taskMdPath, taskContent);
6484
+ fs34.writeFileSync(taskMdPath, taskContent);
6377
6485
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
6378
6486
  }
6379
6487
  }
6380
- if (!fs32.existsSync(taskMdPath)) {
6488
+ if (!fs34.existsSync(taskMdPath)) {
6381
6489
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
6382
6490
  process.exit(1);
6383
6491
  }
@@ -6481,11 +6589,17 @@ ${input.feedback}`);
6481
6589
  prBaseBranch = prDetails.baseBranch;
6482
6590
  }
6483
6591
  }
6592
+ const toolDeclarations = loadToolDeclarations(projectDir);
6593
+ const detectedTools = detectTools(toolDeclarations, projectDir);
6594
+ if (detectedTools.length > 0) {
6595
+ logger.info(`Tools detected: ${detectedTools.map((t) => t.name).join(", ")}`);
6596
+ }
6484
6597
  const ctx = {
6485
6598
  taskId,
6486
6599
  taskDir,
6487
6600
  projectDir,
6488
6601
  runners,
6602
+ tools: detectedTools.length > 0 ? detectedTools : void 0,
6489
6603
  input: {
6490
6604
  mode: input.command === "rerun" || input.command === "fix" || input.command === "fix-ci" ? "rerun" : "full",
6491
6605
  fromStage: input.fromStage,
@@ -6515,7 +6629,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
6515
6629
  }
6516
6630
  }
6517
6631
  const state = await runPipeline(ctx);
6518
- const files = fs32.readdirSync(taskDir);
6632
+ const files = fs34.readdirSync(taskDir);
6519
6633
  console.log(`
6520
6634
  Artifacts in ${taskDir}:`);
6521
6635
  for (const f of files) {
@@ -6563,6 +6677,7 @@ var init_entry = __esm({
6563
6677
  init_task_state();
6564
6678
  init_taskify_command();
6565
6679
  init_config();
6680
+ init_tools();
6566
6681
  main().catch(async (err) => {
6567
6682
  const msg = err instanceof Error ? err.message : String(err);
6568
6683
  console.error(msg);
@@ -6580,8 +6695,8 @@ var init_entry = __esm({
6580
6695
  });
6581
6696
 
6582
6697
  // src/bin/cli.ts
6583
- import * as fs33 from "fs";
6584
- import * as path30 from "path";
6698
+ import * as fs35 from "fs";
6699
+ import * as path32 from "path";
6585
6700
  import { fileURLToPath as fileURLToPath2 } from "url";
6586
6701
 
6587
6702
  // src/bin/commands/init.ts
@@ -6953,13 +7068,239 @@ function initCommand(opts, pkgRoot) {
6953
7068
 
6954
7069
  // src/bin/commands/bootstrap.ts
6955
7070
  init_architecture_detection();
6956
- import * as fs8 from "fs";
6957
- import * as path7 from "path";
7071
+ import * as fs9 from "fs";
7072
+ import * as path8 from "path";
6958
7073
  import { execFileSync as execFileSync5 } from "child_process";
6959
7074
 
6960
7075
  // src/bin/qa-guide.ts
7076
+ import * as fs6 from "fs";
7077
+ import * as path5 from "path";
7078
+
7079
+ // src/bin/framework-detectors.ts
6961
7080
  import * as fs5 from "fs";
6962
7081
  import * as path4 from "path";
7082
+ function detectFrameworks(cwd) {
7083
+ const frameworks = [];
7084
+ let deps = {};
7085
+ try {
7086
+ const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
7087
+ deps = { ...pkg.dependencies, ...pkg.devDependencies };
7088
+ } catch {
7089
+ return frameworks;
7090
+ }
7091
+ if (deps.payload || deps["@payloadcms/next"]) {
7092
+ frameworks.push({
7093
+ name: "payload-cms",
7094
+ version: deps.payload ?? deps["@payloadcms/next"] ?? null,
7095
+ configFile: findFile(cwd, ["payload.config.ts", "payload-config.ts", "src/payload.config.ts"])
7096
+ });
7097
+ }
7098
+ if (deps["next-auth"]) {
7099
+ frameworks.push({
7100
+ name: "nextauth",
7101
+ version: deps["next-auth"] ?? null,
7102
+ configFile: findFile(cwd, ["auth.ts", "auth.config.ts", "src/auth.ts", "src/auth.config.ts"])
7103
+ });
7104
+ }
7105
+ if (deps.prisma || deps["@prisma/client"]) {
7106
+ frameworks.push({
7107
+ name: "prisma",
7108
+ version: deps.prisma ?? deps["@prisma/client"] ?? null,
7109
+ configFile: findFile(cwd, ["prisma/schema.prisma"])
7110
+ });
7111
+ }
7112
+ return frameworks;
7113
+ }
7114
+ function findFile(cwd, candidates) {
7115
+ for (const c of candidates) {
7116
+ if (fs5.existsSync(path4.join(cwd, c))) return c;
7117
+ }
7118
+ return null;
7119
+ }
7120
+ var COLLECTION_DIRS = [
7121
+ "src/server/payload/collections",
7122
+ "src/payload/collections",
7123
+ "src/collections",
7124
+ "payload/collections"
7125
+ ];
7126
+ function discoverPayloadCollections(cwd) {
7127
+ const collections = [];
7128
+ for (const dir of COLLECTION_DIRS) {
7129
+ const fullDir = path4.join(cwd, dir);
7130
+ if (!fs5.existsSync(fullDir)) continue;
7131
+ let files;
7132
+ try {
7133
+ files = fs5.readdirSync(fullDir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
7134
+ } catch {
7135
+ continue;
7136
+ }
7137
+ for (const file of files) {
7138
+ try {
7139
+ const filePath = path4.join(fullDir, file);
7140
+ const content = fs5.readFileSync(filePath, "utf-8").slice(0, 1e4);
7141
+ const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
7142
+ if (!slugMatch) continue;
7143
+ const slug = slugMatch[1];
7144
+ const name = file.replace(/\.(ts|tsx)$/, "");
7145
+ const fields = [];
7146
+ const fieldMatches = content.matchAll(/name:\s*['"]([a-zA-Z_][a-zA-Z0-9_]*)['"]/g);
7147
+ for (const m of fieldMatches) {
7148
+ if (!fields.includes(m[1])) fields.push(m[1]);
7149
+ }
7150
+ const hasAdmin = /components:\s*\{/.test(content) || /Field:\s*['"]/.test(content) || /Cell:\s*['"]/.test(content) || /views:\s*\{/.test(content);
7151
+ collections.push({
7152
+ name,
7153
+ slug,
7154
+ filePath: path4.relative(cwd, filePath),
7155
+ fields: fields.slice(0, 20),
7156
+ hasAdmin
7157
+ });
7158
+ } catch {
7159
+ }
7160
+ }
7161
+ }
7162
+ return collections;
7163
+ }
7164
+ var ADMIN_COMPONENT_DIRS = [
7165
+ "src/ui/admin",
7166
+ "src/admin/components",
7167
+ "src/components/admin"
7168
+ ];
7169
+ function discoverAdminComponents(cwd, collections) {
7170
+ const components = [];
7171
+ for (const dir of ADMIN_COMPONENT_DIRS) {
7172
+ const fullDir = path4.join(cwd, dir);
7173
+ if (!fs5.existsSync(fullDir)) continue;
7174
+ let entries;
7175
+ try {
7176
+ entries = fs5.readdirSync(fullDir, { withFileTypes: true });
7177
+ } catch {
7178
+ continue;
7179
+ }
7180
+ for (const entry of entries) {
7181
+ const entryPath = path4.join(fullDir, entry.name);
7182
+ let name;
7183
+ let filePath;
7184
+ if (entry.isDirectory()) {
7185
+ const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find((f) => fs5.existsSync(path4.join(entryPath, f)));
7186
+ if (!indexFile) continue;
7187
+ name = entry.name;
7188
+ filePath = path4.relative(cwd, path4.join(entryPath, indexFile));
7189
+ } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
7190
+ name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
7191
+ filePath = path4.relative(cwd, entryPath);
7192
+ } else {
7193
+ continue;
7194
+ }
7195
+ let usedInCollection = null;
7196
+ if (collections) {
7197
+ for (const col of collections) {
7198
+ try {
7199
+ const colContent = fs5.readFileSync(path4.join(cwd, col.filePath), "utf-8");
7200
+ if (colContent.includes(name)) {
7201
+ usedInCollection = col.slug;
7202
+ break;
7203
+ }
7204
+ } catch {
7205
+ }
7206
+ }
7207
+ }
7208
+ components.push({ name, filePath, usedInCollection });
7209
+ }
7210
+ }
7211
+ return components;
7212
+ }
7213
+ var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
7214
+ function scanApiRoutes(cwd) {
7215
+ const routes = [];
7216
+ const appDirs = ["src/app", "app"];
7217
+ for (const appDir of appDirs) {
7218
+ const apiDir = path4.join(cwd, appDir, "api");
7219
+ if (!fs5.existsSync(apiDir)) continue;
7220
+ walkApiRoutes(apiDir, "/api", cwd, routes);
7221
+ break;
7222
+ }
7223
+ return routes;
7224
+ }
7225
+ function walkApiRoutes(dir, prefix, cwd, routes) {
7226
+ let entries;
7227
+ try {
7228
+ entries = fs5.readdirSync(dir, { withFileTypes: true });
7229
+ } catch {
7230
+ return;
7231
+ }
7232
+ const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
7233
+ if (routeFile) {
7234
+ try {
7235
+ const content = fs5.readFileSync(path4.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
7236
+ const methods = HTTP_METHODS.filter(
7237
+ (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
7238
+ );
7239
+ if (methods.length > 0) {
7240
+ routes.push({
7241
+ path: prefix,
7242
+ methods,
7243
+ filePath: path4.relative(cwd, path4.join(dir, routeFile.name))
7244
+ });
7245
+ }
7246
+ } catch {
7247
+ }
7248
+ }
7249
+ for (const entry of entries) {
7250
+ if (!entry.isDirectory()) continue;
7251
+ if (entry.name === "node_modules" || entry.name === ".next") continue;
7252
+ let segment = entry.name;
7253
+ if (segment.startsWith("(") && segment.endsWith(")")) {
7254
+ walkApiRoutes(path4.join(dir, entry.name), prefix, cwd, routes);
7255
+ continue;
7256
+ }
7257
+ if (segment.startsWith("[[") && segment.endsWith("]]")) {
7258
+ segment = `:${segment.slice(2, -2)}?`;
7259
+ } else if (segment.startsWith("[") && segment.endsWith("]")) {
7260
+ segment = `:${segment.slice(1, -1)}`;
7261
+ }
7262
+ walkApiRoutes(path4.join(dir, entry.name), `${prefix}/${segment}`, cwd, routes);
7263
+ }
7264
+ }
7265
+ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
7266
+ "NODE_ENV",
7267
+ "HOME",
7268
+ "PATH",
7269
+ "USER",
7270
+ "SHELL",
7271
+ "TERM",
7272
+ "LANG",
7273
+ "PWD",
7274
+ "HOSTNAME",
7275
+ "PORT",
7276
+ "CI",
7277
+ "GITHUB_ACTIONS"
7278
+ ]);
7279
+ function scanEnvVars(cwd) {
7280
+ const envFiles = [".env.example", ".env.local.example", ".env.template"];
7281
+ for (const envFile of envFiles) {
7282
+ const envPath = path4.join(cwd, envFile);
7283
+ if (!fs5.existsSync(envPath)) continue;
7284
+ try {
7285
+ const content = fs5.readFileSync(envPath, "utf-8");
7286
+ const vars = [];
7287
+ for (const line of content.split("\n")) {
7288
+ const trimmed = line.trim();
7289
+ if (!trimmed || trimmed.startsWith("#")) continue;
7290
+ const match = trimmed.match(/^([A-Z][A-Z0-9_]*)=/);
7291
+ if (match && !BUILTIN_ENV_VARS.has(match[1])) {
7292
+ vars.push(match[1]);
7293
+ }
7294
+ }
7295
+ return vars;
7296
+ } catch {
7297
+ return [];
7298
+ }
7299
+ }
7300
+ return [];
7301
+ }
7302
+
7303
+ // src/bin/qa-guide.ts
6963
7304
  function discoverQaContext(cwd) {
6964
7305
  const result2 = {
6965
7306
  routes: [],
@@ -6968,12 +7309,17 @@ function discoverQaContext(cwd) {
6968
7309
  adminPath: null,
6969
7310
  roles: [],
6970
7311
  devCommand: "",
6971
- devPort: 3e3
7312
+ devPort: 3e3,
7313
+ frameworks: [],
7314
+ collections: [],
7315
+ adminComponents: [],
7316
+ apiRoutes: [],
7317
+ envVars: []
6972
7318
  };
6973
7319
  try {
6974
- const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
7320
+ const pkg = JSON.parse(fs6.readFileSync(path5.join(cwd, "package.json"), "utf-8"));
6975
7321
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
6976
- const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
7322
+ const pm = fs6.existsSync(path5.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs6.existsSync(path5.join(cwd, "yarn.lock")) ? "yarn" : "npm";
6977
7323
  if (pkg.scripts?.dev) result2.devCommand = `${pm} dev`;
6978
7324
  if (allDeps.next || allDeps.nuxt) result2.devPort = 3e3;
6979
7325
  else if (allDeps.vite) result2.devPort = 5173;
@@ -6981,14 +7327,14 @@ function discoverQaContext(cwd) {
6981
7327
  }
6982
7328
  const appDirs = ["src/app", "app"];
6983
7329
  for (const appDir of appDirs) {
6984
- const fullAppDir = path4.join(cwd, appDir);
6985
- if (!fs5.existsSync(fullAppDir)) continue;
7330
+ const fullAppDir = path5.join(cwd, appDir);
7331
+ if (!fs6.existsSync(fullAppDir)) continue;
6986
7332
  scanRoutes(fullAppDir, appDir, "", result2);
6987
7333
  break;
6988
7334
  }
6989
7335
  const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
6990
7336
  for (const p of authPatterns) {
6991
- if (fs5.existsSync(path4.join(cwd, p))) result2.authFiles.push(p);
7337
+ if (fs6.existsSync(path5.join(cwd, p))) result2.authFiles.push(p);
6992
7338
  }
6993
7339
  const authConfigGlobs = [
6994
7340
  "src/app/api/auth",
@@ -6999,7 +7345,7 @@ function discoverQaContext(cwd) {
6999
7345
  "src/app/api/oauth"
7000
7346
  ];
7001
7347
  for (const g of authConfigGlobs) {
7002
- if (fs5.existsSync(path4.join(cwd, g))) result2.authFiles.push(g);
7348
+ if (fs6.existsSync(path5.join(cwd, g))) result2.authFiles.push(g);
7003
7349
  }
7004
7350
  try {
7005
7351
  const rolePaths = [
@@ -7011,12 +7357,12 @@ function discoverQaContext(cwd) {
7011
7357
  "src/collections"
7012
7358
  ];
7013
7359
  for (const rp of rolePaths) {
7014
- const dir = path4.join(cwd, rp);
7015
- if (!fs5.existsSync(dir)) continue;
7016
- const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
7360
+ const dir = path5.join(cwd, rp);
7361
+ if (!fs6.existsSync(dir)) continue;
7362
+ const files = fs6.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
7017
7363
  for (const f of files) {
7018
7364
  try {
7019
- const content = fs5.readFileSync(path4.join(dir, f), "utf-8").slice(0, 5e3);
7365
+ const content = fs6.readFileSync(path5.join(dir, f), "utf-8").slice(0, 5e3);
7020
7366
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
7021
7367
  if (roleMatches) {
7022
7368
  for (const m of roleMatches) {
@@ -7040,12 +7386,20 @@ function discoverQaContext(cwd) {
7040
7386
  }
7041
7387
  } catch {
7042
7388
  }
7389
+ result2.frameworks = detectFrameworks(cwd);
7390
+ const hasPayload = result2.frameworks.some((f) => f.name === "payload-cms");
7391
+ if (hasPayload) {
7392
+ result2.collections = discoverPayloadCollections(cwd);
7393
+ }
7394
+ result2.adminComponents = discoverAdminComponents(cwd, result2.collections.length > 0 ? result2.collections : void 0);
7395
+ result2.apiRoutes = scanApiRoutes(cwd);
7396
+ result2.envVars = scanEnvVars(cwd);
7043
7397
  return result2;
7044
7398
  }
7045
7399
  function scanRoutes(dir, baseDir, prefix, result2) {
7046
7400
  let entries;
7047
7401
  try {
7048
- entries = fs5.readdirSync(dir, { withFileTypes: true });
7402
+ entries = fs6.readdirSync(dir, { withFileTypes: true });
7049
7403
  } catch {
7050
7404
  return;
7051
7405
  }
@@ -7062,19 +7416,18 @@ function scanRoutes(dir, baseDir, prefix, result2) {
7062
7416
  if (entry.name === "node_modules" || entry.name === ".next") continue;
7063
7417
  let segment = entry.name;
7064
7418
  if (segment.startsWith("(") && segment.endsWith(")")) {
7065
- scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result2);
7419
+ scanRoutes(path5.join(dir, entry.name), baseDir, prefix, result2);
7066
7420
  continue;
7067
7421
  }
7068
- if (segment.startsWith("[") && segment.endsWith("]")) {
7069
- segment = `:${segment.slice(1, -1)}`;
7070
- }
7071
7422
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
7072
7423
  segment = `:${segment.slice(2, -2)}?`;
7424
+ } else if (segment.startsWith("[") && segment.endsWith("]")) {
7425
+ segment = `:${segment.slice(1, -1)}`;
7073
7426
  }
7074
- scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result2);
7427
+ scanRoutes(path5.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result2);
7075
7428
  }
7076
7429
  }
7077
- function generateQaGuide(discovery) {
7430
+ function generateQaGuideFallback(discovery) {
7078
7431
  const lines = ["# QA Guide", "", "## Authentication", ""];
7079
7432
  if (discovery.loginPage) {
7080
7433
  lines.push(`- Login page: \`${discovery.loginPage}\``);
@@ -7123,6 +7476,39 @@ function generateQaGuide(discovery) {
7123
7476
  }
7124
7477
  lines.push("");
7125
7478
  }
7479
+ if (discovery.collections.length > 0) {
7480
+ lines.push("## Admin Collections", "");
7481
+ for (const col of discovery.collections) {
7482
+ lines.push(`### \`/admin/collections/${col.slug}\``);
7483
+ lines.push(`- **Name:** ${col.name}`);
7484
+ lines.push(`- **Fields:** ${col.fields.join(", ")}`);
7485
+ if (col.hasAdmin) lines.push("- **Custom admin components:** yes");
7486
+ lines.push("");
7487
+ }
7488
+ }
7489
+ if (discovery.apiRoutes.length > 0) {
7490
+ lines.push("## API Endpoints", "");
7491
+ for (const route of discovery.apiRoutes) {
7492
+ lines.push(`- \`${route.methods.join(", ")} ${route.path}\` \u2014 \`${route.filePath}\``);
7493
+ }
7494
+ lines.push("");
7495
+ }
7496
+ if (discovery.adminComponents.length > 0) {
7497
+ lines.push("## Custom Admin Components", "");
7498
+ for (const comp of discovery.adminComponents) {
7499
+ let line = `- **${comp.name}** (\`${comp.filePath}\`)`;
7500
+ if (comp.usedInCollection) line += ` \u2014 used in \`${comp.usedInCollection}\` collection`;
7501
+ lines.push(line);
7502
+ }
7503
+ lines.push("");
7504
+ }
7505
+ if (discovery.envVars.length > 0) {
7506
+ lines.push("## Required Environment Variables", "");
7507
+ for (const v of discovery.envVars) {
7508
+ lines.push(`- \`${v}\``);
7509
+ }
7510
+ lines.push("");
7511
+ }
7126
7512
  lines.push(
7127
7513
  "## Dev Server",
7128
7514
  "",
@@ -7132,10 +7518,71 @@ function generateQaGuide(discovery) {
7132
7518
  );
7133
7519
  return lines.join("\n");
7134
7520
  }
7521
+ var MAX_SERIALIZED_LENGTH = 8e3;
7522
+ function serializeDiscoveryForLLM(discovery) {
7523
+ const sections = [];
7524
+ sections.push(`Dev server: ${discovery.devCommand || "pnpm dev"} at http://localhost:${discovery.devPort}`);
7525
+ if (discovery.loginPage) sections.push(`Login page: ${discovery.loginPage}`);
7526
+ if (discovery.adminPath) sections.push(`Admin panel: ${discovery.adminPath}`);
7527
+ if (discovery.roles.length > 0) sections.push(`Roles: ${discovery.roles.join(", ")}`);
7528
+ if (discovery.frameworks.length > 0) {
7529
+ sections.push(`
7530
+ Frameworks: ${discovery.frameworks.map((f) => `${f.name}${f.version ? ` (${f.version})` : ""}`).join(", ")}`);
7531
+ }
7532
+ if (discovery.collections.length > 0) {
7533
+ sections.push("\nCollections (Payload CMS):");
7534
+ for (const col of discovery.collections.slice(0, 15)) {
7535
+ const fields = col.fields.slice(0, 10).join(", ");
7536
+ let line = `- ${col.slug}: fields=[${fields}]`;
7537
+ if (col.hasAdmin) line += " (has custom admin components)";
7538
+ line += ` \u2014 ${col.filePath}`;
7539
+ sections.push(line);
7540
+ }
7541
+ if (discovery.collections.length > 15) {
7542
+ sections.push(`- ... and ${discovery.collections.length - 15} more collections`);
7543
+ }
7544
+ }
7545
+ if (discovery.adminComponents.length > 0) {
7546
+ sections.push("\nCustom Admin Components:");
7547
+ for (const comp of discovery.adminComponents.slice(0, 10)) {
7548
+ let line = `- ${comp.name} (${comp.filePath})`;
7549
+ if (comp.usedInCollection) line += ` \u2192 used in "${comp.usedInCollection}" collection`;
7550
+ sections.push(line);
7551
+ }
7552
+ }
7553
+ if (discovery.apiRoutes.length > 0) {
7554
+ sections.push("\nAPI Routes:");
7555
+ for (const route of discovery.apiRoutes.slice(0, 20)) {
7556
+ sections.push(`- ${route.methods.join("/")} ${route.path} \u2014 ${route.filePath}`);
7557
+ }
7558
+ if (discovery.apiRoutes.length > 20) {
7559
+ sections.push(`- ... and ${discovery.apiRoutes.length - 20} more routes`);
7560
+ }
7561
+ }
7562
+ if (discovery.routes.length > 0) {
7563
+ sections.push("\nFrontend Routes:");
7564
+ for (const route of discovery.routes.slice(0, 30)) {
7565
+ sections.push(`- [${route.group}] ${route.path}`);
7566
+ }
7567
+ if (discovery.routes.length > 30) {
7568
+ sections.push(`- ... and ${discovery.routes.length - 30} more routes`);
7569
+ }
7570
+ }
7571
+ if (discovery.envVars.length > 0) {
7572
+ sections.push(`
7573
+ Required env vars: ${discovery.envVars.join(", ")}`);
7574
+ }
7575
+ let result2 = sections.join("\n");
7576
+ if (result2.length > MAX_SERIALIZED_LENGTH) {
7577
+ const cutoff = result2.lastIndexOf("\n", MAX_SERIALIZED_LENGTH - 20);
7578
+ result2 = result2.slice(0, cutoff > 0 ? cutoff : MAX_SERIALIZED_LENGTH - 20) + "\n... (truncated)";
7579
+ }
7580
+ return result2;
7581
+ }
7135
7582
 
7136
7583
  // src/bin/skills.ts
7137
- import * as fs6 from "fs";
7138
- import * as path5 from "path";
7584
+ import * as fs7 from "fs";
7585
+ import * as path6 from "path";
7139
7586
  import { execFileSync as execFileSync4 } from "child_process";
7140
7587
  var SKILL_MAPPINGS = [
7141
7588
  {
@@ -7158,10 +7605,10 @@ var SKILL_MAPPINGS = [
7158
7605
  }
7159
7606
  ];
7160
7607
  function detectSkillsForProject(cwd) {
7161
- const pkgPath = path5.join(cwd, "package.json");
7162
- if (!fs6.existsSync(pkgPath)) return [];
7608
+ const pkgPath = path6.join(cwd, "package.json");
7609
+ if (!fs7.existsSync(pkgPath)) return [];
7163
7610
  try {
7164
- const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
7611
+ const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
7165
7612
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7166
7613
  const seen = /* @__PURE__ */ new Set();
7167
7614
  const skills = [];
@@ -7187,10 +7634,10 @@ function installSkillsForProject(cwd) {
7187
7634
  return [];
7188
7635
  }
7189
7636
  let installedSkills = {};
7190
- const lockPath = path5.join(cwd, "skills-lock.json");
7191
- if (fs6.existsSync(lockPath)) {
7637
+ const lockPath = path6.join(cwd, "skills-lock.json");
7638
+ if (fs7.existsSync(lockPath)) {
7192
7639
  try {
7193
- const lock = JSON.parse(fs6.readFileSync(lockPath, "utf-8"));
7640
+ const lock = JSON.parse(fs7.readFileSync(lockPath, "utf-8"));
7194
7641
  installedSkills = lock.skills ?? {};
7195
7642
  } catch {
7196
7643
  }
@@ -7202,8 +7649,8 @@ function installSkillsForProject(cwd) {
7202
7649
  console.log(` \u25CB ${skill.label} \u2014 already installed`);
7203
7650
  const agentPath = `.agents/skills/${skillName}`;
7204
7651
  const claudePath = `.claude/skills/${skillName}`;
7205
- if (fs6.existsSync(path5.join(cwd, agentPath))) installedPaths.push(agentPath);
7206
- if (fs6.existsSync(path5.join(cwd, claudePath))) installedPaths.push(claudePath);
7652
+ if (fs7.existsSync(path6.join(cwd, agentPath))) installedPaths.push(agentPath);
7653
+ if (fs7.existsSync(path6.join(cwd, claudePath))) installedPaths.push(claudePath);
7207
7654
  continue;
7208
7655
  }
7209
7656
  try {
@@ -7217,8 +7664,8 @@ function installSkillsForProject(cwd) {
7217
7664
  const installedName = skill.package.split("@").pop() ?? "";
7218
7665
  const agentPath = `.agents/skills/${installedName}`;
7219
7666
  const claudePath = `.claude/skills/${installedName}`;
7220
- if (fs6.existsSync(path5.join(cwd, agentPath))) installedPaths.push(agentPath);
7221
- if (fs6.existsSync(path5.join(cwd, claudePath))) installedPaths.push(claudePath);
7667
+ if (fs7.existsSync(path6.join(cwd, agentPath))) installedPaths.push(agentPath);
7668
+ if (fs7.existsSync(path6.join(cwd, claudePath))) installedPaths.push(claudePath);
7222
7669
  console.log(` \u2713 ${skill.label}`);
7223
7670
  } catch {
7224
7671
  console.log(` \u2717 ${skill.label} \u2014 failed to install`);
@@ -7229,22 +7676,47 @@ function installSkillsForProject(cwd) {
7229
7676
 
7230
7677
  // src/bin/commands/bootstrap.ts
7231
7678
  init_config();
7679
+
7680
+ // src/bin/extend-helpers.ts
7681
+ var MAX_EXISTING_CONTENT_LENGTH = 6e3;
7682
+ function buildExtendInstruction(existingContent, fileDescription) {
7683
+ if (!existingContent.trim()) return "";
7684
+ let content = existingContent;
7685
+ if (content.length > MAX_EXISTING_CONTENT_LENGTH) {
7686
+ const cutoff = content.lastIndexOf("\n", MAX_EXISTING_CONTENT_LENGTH);
7687
+ content = content.slice(0, cutoff > 0 ? cutoff : MAX_EXISTING_CONTENT_LENGTH) + "\n... (truncated)";
7688
+ }
7689
+ return `
7690
+ ## Existing ${fileDescription} (EXTEND, do not replace)
7691
+ You are UPDATING an existing ${fileDescription}. Follow these rules strictly:
7692
+ - PRESERVE all existing sections and content that are still accurate \u2014 keep them verbatim
7693
+ - PRESERVE any manually-added sections, custom notes, or user edits
7694
+ - REMOVE only lines that reference files, patterns, or dependencies that no longer exist in the project
7695
+ - APPEND new sections or lines for newly discovered patterns, files, or conventions
7696
+ - Do NOT rewrite sections that are still correct
7697
+
7698
+ ### Existing content:
7699
+ ${content}
7700
+ `;
7701
+ }
7702
+
7703
+ // src/bin/commands/bootstrap.ts
7232
7704
  var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
7233
7705
  function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
7234
- const srcDir = path7.join(cwd, "src");
7235
- const baseDir = fs8.existsSync(srcDir) ? srcDir : cwd;
7706
+ const srcDir = path8.join(cwd, "src");
7707
+ const baseDir = fs9.existsSync(srcDir) ? srcDir : cwd;
7236
7708
  const results = [];
7237
7709
  function walk(dir) {
7238
7710
  const entries = [];
7239
7711
  try {
7240
- for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
7712
+ for (const entry of fs9.readdirSync(dir, { withFileTypes: true })) {
7241
7713
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
7242
- const full = path7.join(dir, entry.name);
7714
+ const full = path8.join(dir, entry.name);
7243
7715
  if (entry.isDirectory()) {
7244
7716
  entries.push(...walk(full));
7245
7717
  } else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
7246
7718
  try {
7247
- const stat = fs8.statSync(full);
7719
+ const stat = fs9.statSync(full);
7248
7720
  if (stat.size >= 200 && stat.size <= 5e3) {
7249
7721
  entries.push({ filePath: full, size: stat.size });
7250
7722
  }
@@ -7258,8 +7730,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
7258
7730
  }
7259
7731
  const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
7260
7732
  for (const { filePath } of files) {
7261
- const rel = path7.relative(cwd, filePath);
7262
- const content = fs8.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
7733
+ const rel = path8.relative(cwd, filePath);
7734
+ const content = fs9.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
7263
7735
  results.push(`### File: ${rel}
7264
7736
  \`\`\`typescript
7265
7737
  ${content}
@@ -7271,9 +7743,9 @@ function ghComment(issueNumber, body, cwd) {
7271
7743
  try {
7272
7744
  let repoSlug = "";
7273
7745
  try {
7274
- const configPath = path7.join(cwd, "kody.config.json");
7275
- if (fs8.existsSync(configPath)) {
7276
- const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
7746
+ const configPath = path8.join(cwd, "kody.config.json");
7747
+ if (fs9.existsSync(configPath)) {
7748
+ const config = JSON.parse(fs9.readFileSync(configPath, "utf-8"));
7277
7749
  if (config.github?.owner && config.github?.repo) {
7278
7750
  repoSlug = `${config.github.owner}/${config.github.repo}`;
7279
7751
  }
@@ -7310,8 +7782,8 @@ function bootstrapCommand(opts, pkgRoot) {
7310
7782
  ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
7311
7783
  }
7312
7784
  const readIfExists = (rel, maxChars = 3e3) => {
7313
- const p = path7.join(cwd, rel);
7314
- if (fs8.existsSync(p)) return fs8.readFileSync(p, "utf-8").slice(0, maxChars);
7785
+ const p = path8.join(cwd, rel);
7786
+ if (fs9.existsSync(p)) return fs9.readFileSync(p, "utf-8").slice(0, maxChars);
7315
7787
  return null;
7316
7788
  };
7317
7789
  let repoContext = "";
@@ -7346,14 +7818,14 @@ ${sampleFiles}
7346
7818
 
7347
7819
  `;
7348
7820
  try {
7349
- const topDirs = fs8.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
7821
+ const topDirs = fs9.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
7350
7822
  repoContext += `## Top-level directories
7351
7823
  ${topDirs.join(", ")}
7352
7824
 
7353
7825
  `;
7354
- const srcDir = path7.join(cwd, "src");
7355
- if (fs8.existsSync(srcDir)) {
7356
- const srcDirs = fs8.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7826
+ const srcDir = path8.join(cwd, "src");
7827
+ if (fs9.existsSync(srcDir)) {
7828
+ const srcDirs = fs9.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
7357
7829
  if (srcDirs.length > 0) repoContext += `## src/ subdirectories
7358
7830
  ${srcDirs.join(", ")}
7359
7831
 
@@ -7363,34 +7835,28 @@ ${srcDirs.join(", ")}
7363
7835
  }
7364
7836
  const existingFiles = [];
7365
7837
  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"]) {
7366
- if (fs8.existsSync(path7.join(cwd, f))) existingFiles.push(f);
7838
+ if (fs9.existsSync(path8.join(cwd, f))) existingFiles.push(f);
7367
7839
  }
7368
7840
  if (existingFiles.length) repoContext += `## Config files present
7369
7841
  ${existingFiles.join(", ")}
7370
7842
 
7371
7843
  `;
7372
7844
  console.log("\u2500\u2500 Project Memory \u2500\u2500");
7373
- const memoryDir = path7.join(cwd, ".kody", "memory");
7374
- fs8.mkdirSync(memoryDir, { recursive: true });
7375
- const archPath = path7.join(memoryDir, "architecture.md");
7376
- const conventionsPath = path7.join(memoryDir, "conventions.md");
7377
- const existingArch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
7378
- const existingConv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
7845
+ const memoryDir = path8.join(cwd, ".kody", "memory");
7846
+ fs9.mkdirSync(memoryDir, { recursive: true });
7847
+ const archPath = path8.join(memoryDir, "architecture.md");
7848
+ const conventionsPath = path8.join(memoryDir, "conventions.md");
7849
+ const existingArch = fs9.existsSync(archPath) ? fs9.readFileSync(archPath, "utf-8") : "";
7850
+ const existingConv = fs9.existsSync(conventionsPath) ? fs9.readFileSync(conventionsPath, "utf-8") : "";
7379
7851
  const hasExisting = !!(existingArch || existingConv);
7380
- const extendInstruction = hasExisting && !opts.force ? `
7381
- ## Existing Documentation (EXTEND, do not replace)
7382
- You are UPDATING existing documentation. Follow these rules strictly:
7383
- - PRESERVE all existing sections and content that are still accurate
7384
- - REMOVE only lines that reference files, patterns, or dependencies that no longer exist in the project
7385
- - APPEND new sections or lines for newly discovered patterns, files, or conventions
7386
- - Do NOT rewrite sections that are still correct \u2014 keep them verbatim
7387
-
7388
- ### Existing architecture.md:
7852
+ const extendInstruction = hasExisting && !opts.force ? buildExtendInstruction(
7853
+ `### architecture.md:
7389
7854
  ${existingArch}
7390
7855
 
7391
- ### Existing conventions.md:
7392
- ${existingConv}
7393
- ` : "";
7856
+ ### conventions.md:
7857
+ ${existingConv}`,
7858
+ "project documentation"
7859
+ ) : "";
7394
7860
  const memoryPrompt = `You are analyzing a project to generate documentation for an autonomous SDLC pipeline.
7395
7861
 
7396
7862
  Given this project context, output ONLY a JSON object with EXACTLY this structure:
@@ -7431,12 +7897,12 @@ ${repoContext}`;
7431
7897
  const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
7432
7898
  const parsed = JSON.parse(cleaned);
7433
7899
  if (parsed.architecture) {
7434
- fs8.writeFileSync(archPath, parsed.architecture);
7900
+ fs9.writeFileSync(archPath, parsed.architecture);
7435
7901
  const lineCount = parsed.architecture.split("\n").length;
7436
7902
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
7437
7903
  }
7438
7904
  if (parsed.conventions) {
7439
- fs8.writeFileSync(conventionsPath, parsed.conventions);
7905
+ fs9.writeFileSync(conventionsPath, parsed.conventions);
7440
7906
  const lineCount = parsed.conventions.split("\n").length;
7441
7907
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
7442
7908
  }
@@ -7445,45 +7911,44 @@ ${repoContext}`;
7445
7911
  const detected = detectArchitectureBasic(cwd);
7446
7912
  if (detected.length > 0) {
7447
7913
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7448
- fs8.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
7914
+ fs9.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
7449
7915
 
7450
7916
  ## Overview
7451
7917
  ${detected.join("\n")}
7452
7918
  `);
7453
7919
  console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
7454
7920
  }
7455
- fs8.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
7921
+ fs9.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
7456
7922
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
7457
7923
  }
7458
7924
  console.log("\n\u2500\u2500 Step Files \u2500\u2500");
7459
- const stepsDir = path7.join(cwd, ".kody", "steps");
7460
- fs8.mkdirSync(stepsDir, { recursive: true });
7461
- const arch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
7462
- const conv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
7925
+ const stepsDir = path8.join(cwd, ".kody", "steps");
7926
+ fs9.mkdirSync(stepsDir, { recursive: true });
7927
+ const arch = fs9.existsSync(archPath) ? fs9.readFileSync(archPath, "utf-8") : "";
7928
+ const conv = fs9.existsSync(conventionsPath) ? fs9.readFileSync(conventionsPath, "utf-8") : "";
7463
7929
  console.log(" \u23F3 Customizing step files...");
7464
7930
  let stepCount = 0;
7465
7931
  for (const stage of STEP_STAGES) {
7466
- const templatePath = path7.join(pkgRoot, "prompts", `${stage}.md`);
7467
- if (!fs8.existsSync(templatePath)) {
7932
+ const templatePath = path8.join(pkgRoot, "prompts", `${stage}.md`);
7933
+ if (!fs9.existsSync(templatePath)) {
7468
7934
  console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
7469
7935
  continue;
7470
7936
  }
7471
- const stepOutputPath = path7.join(stepsDir, `${stage}.md`);
7472
- if (fs8.existsSync(stepOutputPath) && !opts.force) {
7473
- console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
7474
- continue;
7475
- }
7476
- const defaultPrompt = fs8.readFileSync(templatePath, "utf-8");
7937
+ const stepOutputPath = path8.join(stepsDir, `${stage}.md`);
7938
+ const existingStep = fs9.existsSync(stepOutputPath) ? fs9.readFileSync(stepOutputPath, "utf-8") : "";
7939
+ const isExtend = !!existingStep && !opts.force;
7940
+ const defaultPrompt = fs9.readFileSync(templatePath, "utf-8");
7477
7941
  const contextPlaceholder = "{{TASK_CONTEXT}}";
7478
7942
  const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
7479
7943
  if (placeholderIdx === -1) {
7480
- fs8.copyFileSync(templatePath, stepOutputPath);
7944
+ fs9.copyFileSync(templatePath, stepOutputPath);
7481
7945
  stepCount++;
7482
7946
  console.log(` \u2713 ${stage}.md`);
7483
7947
  continue;
7484
7948
  }
7485
7949
  const beforePlaceholder = defaultPrompt.slice(0, placeholderIdx).trimEnd();
7486
7950
  const afterPlaceholder = defaultPrompt.slice(placeholderIdx);
7951
+ const stepExtendBlock = isExtend ? buildExtendInstruction(existingStep, `${stage} step file`) : "";
7487
7952
  const customizationPrompt = `You are customizing a Kody pipeline prompt for a specific repository.
7488
7953
 
7489
7954
  ## Your Task
@@ -7499,7 +7964,7 @@ Take the prompt template below and APPEND repository-specific sections to it.
7499
7964
  4. Keep each appended section concise (10-20 lines max).
7500
7965
  5. Output ONLY the customized prompt markdown. No explanation before or after.
7501
7966
  6. Do NOT include the text "${contextPlaceholder}" \u2014 it will be appended automatically after your output.
7502
-
7967
+ ${stepExtendBlock}
7503
7968
  ## Stage Being Customized
7504
7969
  Stage: ${stage}
7505
7970
 
@@ -7534,40 +7999,130 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
7534
7999
  let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
7535
8000
  cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
7536
8001
  const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
7537
- fs8.writeFileSync(stepOutputPath, finalPrompt);
8002
+ fs9.writeFileSync(stepOutputPath, finalPrompt);
7538
8003
  stepCount++;
7539
- console.log(` \u2713 ${stage}.md`);
8004
+ console.log(` \u2713 ${stage}.md (${isExtend ? "extended" : "generated"})`);
7540
8005
  } catch {
7541
- console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
7542
- fs8.copyFileSync(templatePath, stepOutputPath);
7543
- stepCount++;
8006
+ if (!isExtend) {
8007
+ console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
8008
+ fs9.copyFileSync(templatePath, stepOutputPath);
8009
+ stepCount++;
8010
+ } else {
8011
+ console.log(` \u26A0 ${stage}.md \u2014 extend failed, keeping existing`);
8012
+ }
7544
8013
  }
7545
8014
  }
7546
8015
  console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
7547
8016
  console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
7548
- const qaGuidePath = path7.join(cwd, ".kody", "qa-guide.md");
7549
- if (!fs8.existsSync(qaGuidePath) || opts.force) {
7550
- const discovery = discoverQaContext(cwd);
7551
- if (discovery.routes.length > 0) {
7552
- const qaGuide = generateQaGuide(discovery);
7553
- fs8.writeFileSync(qaGuidePath, qaGuide);
7554
- console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
7555
- if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
7556
- if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
7557
- console.log(" \u2139 Add QA_ADMIN_EMAIL, QA_ADMIN_PASSWORD, QA_USER_EMAIL, QA_USER_PASSWORD as GitHub secrets");
7558
- } else {
7559
- console.log(" \u25CB No routes detected \u2014 skipping QA guide");
7560
- }
8017
+ const qaGuidePath = path8.join(cwd, ".kody", "qa-guide.md");
8018
+ const discovery = discoverQaContext(cwd);
8019
+ const hasRoutes = discovery.routes.length > 0 || discovery.collections.length > 0;
8020
+ if (hasRoutes) {
8021
+ const existingQaGuide = fs9.existsSync(qaGuidePath) ? fs9.readFileSync(qaGuidePath, "utf-8") : "";
8022
+ const isQaExtend = !!existingQaGuide && !opts.force;
8023
+ const serializedDiscovery = serializeDiscoveryForLLM(discovery);
8024
+ const qaExtendBlock = isQaExtend ? buildExtendInstruction(existingQaGuide, "QA guide") : "";
8025
+ const qaPrompt = `You are generating a QA guide for an autonomous coding agent that will use Playwright browser tools to visually verify UI changes.
8026
+
8027
+ ## Discovery Data
8028
+ ${serializedDiscovery}
8029
+
8030
+ ## Architecture
8031
+ ${arch}
8032
+
8033
+ ## Conventions
8034
+ ${conv}
8035
+ ${qaExtendBlock}
8036
+ ## Output Format
8037
+ Generate a markdown QA guide with EXACTLY these sections:
8038
+
8039
+ # QA Guide
8040
+
8041
+ ## Quick Reference
8042
+ - Dev server command and URL
8043
+ - Login page URL
8044
+ - Admin panel URL (if applicable)
8045
+
8046
+ ## Authentication
8047
+ ### Test Accounts
8048
+ Table with Role, Email, Password columns.
8049
+ Use env var references (QA_ADMIN_EMAIL, QA_ADMIN_PASSWORD, etc.) \u2014 NOT hardcoded credentials.
8050
+ If the existing guide has real credentials, PRESERVE them exactly.
8051
+
8052
+ ### Login Steps
8053
+ Specific steps for this project's auth system.
8054
+
8055
+ ### Auth Files
8056
+ List of auth-related source files.
8057
+
8058
+ ## Navigation Map
8059
+ ### Admin Panel
8060
+ For each collection/admin page: the exact URL path, what elements to expect on the page, key fields visible in the form, any custom components.
8061
+ Example: "/admin/collections/courses \u2014 Course edit form with title, slug, description fields. Custom CourseLessonsSorter component shows drag-sortable lessons grouped by chapter."
8062
+
8063
+ ### Frontend Pages
8064
+ For each key public route: path, expected content, key interactions to test.
8065
+
8066
+ ### API Endpoints
8067
+ For each API route: path, HTTP methods, purpose.
8068
+
8069
+ ## Component Verification Patterns
8070
+ For each custom admin component: where to find it in the UI, how to navigate there, what visual elements to verify, interaction tests (click, drag, type).
8071
+
8072
+ ## Common Test Scenarios
8073
+ CRUD workflows, auth flows, specific feature verification patterns relevant to this project.
8074
+
8075
+ ## Environment Setup
8076
+ Required env vars to start the dev server successfully.
8077
+
8078
+ ## Dev Server
8079
+ Command and URL.
8080
+
8081
+ ## Rules
8082
+ - Be SPECIFIC to this project \u2014 reference actual URLs, collection names, component names
8083
+ - For admin panels (Payload CMS, etc.), include the exact /admin/collections/{slug} paths
8084
+ - Include visual assertions: "you should see X", "verify Y is visible"
8085
+ - Include interaction tests: "click button X", "fill field Y", "drag item Z"
8086
+ - Keep under 200 lines total
8087
+ - Output ONLY the markdown. No explanation before or after.`;
8088
+ console.log(" \u23F3 Generating QA guide...");
8089
+ try {
8090
+ const output = execFileSync5("claude", [
8091
+ "--print",
8092
+ "--model",
8093
+ bootstrapModel,
8094
+ "--dangerously-skip-permissions",
8095
+ qaPrompt
8096
+ ], {
8097
+ encoding: "utf-8",
8098
+ timeout: 9e4,
8099
+ cwd,
8100
+ stdio: ["pipe", "pipe", "pipe"]
8101
+ }).trim();
8102
+ const cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
8103
+ fs9.writeFileSync(qaGuidePath, cleaned);
8104
+ console.log(` \u2713 .kody/qa-guide.md (${isQaExtend ? "extended" : "generated"}, ${discovery.routes.length} routes, ${discovery.collections.length} collections)`);
8105
+ } catch {
8106
+ console.log(" \u26A0 LLM QA generation failed \u2014 using template fallback");
8107
+ const qaGuide = generateQaGuideFallback(discovery);
8108
+ fs9.writeFileSync(qaGuidePath, qaGuide);
8109
+ console.log(` \u2713 .kody/qa-guide.md (fallback, ${discovery.routes.length} routes)`);
8110
+ }
8111
+ if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
8112
+ if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
8113
+ if (discovery.collections.length > 0) console.log(` \u2713 ${discovery.collections.length} Payload CMS collections detected`);
8114
+ if (discovery.adminComponents.length > 0) console.log(` \u2713 ${discovery.adminComponents.length} custom admin components detected`);
8115
+ console.log(" \u2139 Add QA_ADMIN_EMAIL, QA_ADMIN_PASSWORD, QA_USER_EMAIL, QA_USER_PASSWORD as GitHub secrets");
7561
8116
  } else {
7562
- console.log(" \u25CB .kody/qa-guide.md already exists (use --force to regenerate)");
8117
+ console.log(" \u25CB No routes or collections detected \u2014 skipping QA guide");
7563
8118
  }
7564
8119
  console.log("\n\u2500\u2500 Labels \u2500\u2500");
7565
8120
  try {
7566
8121
  let repoSlug = "";
7567
8122
  try {
7568
- const configPath = path7.join(cwd, "kody.config.json");
7569
- if (fs8.existsSync(configPath)) {
7570
- const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
8123
+ const configPath = path8.join(cwd, "kody.config.json");
8124
+ if (fs9.existsSync(configPath)) {
8125
+ const config = JSON.parse(fs9.readFileSync(configPath, "utf-8"));
7571
8126
  if (config.github?.owner && config.github?.repo) {
7572
8127
  repoSlug = `${config.github.owner}/${config.github.repo}`;
7573
8128
  }
@@ -7576,9 +8131,12 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
7576
8131
  }
7577
8132
  if (repoSlug) {
7578
8133
  const labels = [
8134
+ { name: "kody:backlog", color: "e4e669", description: "Issue created, not yet assigned to Kody" },
7579
8135
  { name: "kody:planning", color: "c5def5", description: "Kody is analyzing and planning" },
7580
8136
  { name: "kody:building", color: "0e8a16", description: "Kody is building code" },
8137
+ { name: "kody:verifying", color: "fbca04", description: "Kody is verifying (lint/test/typecheck)" },
7581
8138
  { name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
8139
+ { name: "kody:fixing", color: "0e8a16", description: "Kody is applying review fixes" },
7582
8140
  { name: "kody:shipping", color: "1d76db", description: "Kody is creating the pull request" },
7583
8141
  { name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
7584
8142
  { name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
@@ -7632,6 +8190,24 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
7632
8190
  } catch {
7633
8191
  console.log(" \u25CB Label creation skipped");
7634
8192
  }
8193
+ console.log("\n\u2500\u2500 Tools \u2500\u2500");
8194
+ const toolsYmlPath = path8.join(cwd, ".kody", "tools.yml");
8195
+ if (!fs9.existsSync(toolsYmlPath) || opts.force) {
8196
+ const toolsTemplate = `# Kody Tools Configuration
8197
+ # Uncomment and configure tools that your project uses.
8198
+ # The engine will detect, install, and inject tool skills into pipeline stages.
8199
+ #
8200
+ # playwright:
8201
+ # detect: ["playwright.config.ts", "playwright.config.js"]
8202
+ # stages: [verify]
8203
+ # setup: "npx playwright install --with-deps chromium"
8204
+ # skill: playwright-cli.md
8205
+ `;
8206
+ fs9.writeFileSync(toolsYmlPath, toolsTemplate);
8207
+ console.log(" \u2713 .kody/tools.yml (template created)");
8208
+ } else {
8209
+ console.log(" \u25CB .kody/tools.yml (already exists, keeping)");
8210
+ }
7635
8211
  console.log("\n\u2500\u2500 Skills \u2500\u2500");
7636
8212
  const installedSkillPaths = installSkillsForProject(cwd);
7637
8213
  console.log("\n\u2500\u2500 Git \u2500\u2500");
@@ -7639,20 +8215,21 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
7639
8215
  ".kody/memory/architecture.md",
7640
8216
  ".kody/memory/conventions.md",
7641
8217
  ".kody/qa-guide.md",
8218
+ ".kody/tools.yml",
7642
8219
  ...installedSkillPaths
7643
- ].filter((f) => fs8.existsSync(path7.join(cwd, f)));
7644
- if (fs8.existsSync(path7.join(cwd, "skills-lock.json"))) {
8220
+ ].filter((f) => fs9.existsSync(path8.join(cwd, f)));
8221
+ if (fs9.existsSync(path8.join(cwd, "skills-lock.json"))) {
7645
8222
  filesToCommit.push("skills-lock.json");
7646
8223
  }
7647
8224
  for (const stage of STEP_STAGES) {
7648
8225
  const stepFile = `.kody/steps/${stage}.md`;
7649
- if (fs8.existsSync(path7.join(cwd, stepFile))) {
8226
+ if (fs9.existsSync(path8.join(cwd, stepFile))) {
7650
8227
  filesToCommit.push(stepFile);
7651
8228
  }
7652
8229
  }
7653
8230
  if (filesToCommit.length > 0) {
7654
8231
  try {
7655
- const fullPaths = filesToCommit.map((f) => path7.join(cwd, f));
8232
+ const fullPaths = filesToCommit.map((f) => path8.join(cwd, f));
7656
8233
  for (let pass = 0; pass < 2; pass++) {
7657
8234
  execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
7658
8235
  cwd,
@@ -7679,9 +8256,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
7679
8256
  console.log(` \u2713 Pushed branch: ${branchName}`);
7680
8257
  let baseBranch = "main";
7681
8258
  try {
7682
- const configPath = path7.join(cwd, "kody.config.json");
7683
- if (fs8.existsSync(configPath)) {
7684
- const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
8259
+ const configPath = path8.join(cwd, "kody.config.json");
8260
+ if (fs9.existsSync(configPath)) {
8261
+ const config = JSON.parse(fs9.readFileSync(configPath, "utf-8"));
7685
8262
  baseBranch = config.git?.defaultBranch ?? "main";
7686
8263
  }
7687
8264
  } catch {
@@ -7755,11 +8332,11 @@ Create it manually.`, cwd);
7755
8332
 
7756
8333
  // src/bin/cli.ts
7757
8334
  init_architecture_detection();
7758
- var __dirname2 = path30.dirname(fileURLToPath2(import.meta.url));
7759
- var PKG_ROOT = path30.resolve(__dirname2, "..", "..");
8335
+ var __dirname2 = path32.dirname(fileURLToPath2(import.meta.url));
8336
+ var PKG_ROOT = path32.resolve(__dirname2, "..", "..");
7760
8337
  function getVersion() {
7761
- const pkgPath = path30.join(PKG_ROOT, "package.json");
7762
- const pkg = JSON.parse(fs33.readFileSync(pkgPath, "utf-8"));
8338
+ const pkgPath = path32.join(PKG_ROOT, "package.json");
8339
+ const pkg = JSON.parse(fs35.readFileSync(pkgPath, "utf-8"));
7763
8340
  return pkg.version;
7764
8341
  }
7765
8342
  var args = process.argv.slice(2);