@kody-ade/kody-engine 0.4.23 → 0.4.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin/kody.js +737 -739
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.4.23",
6
+ version: "0.4.24",
7
7
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -1277,119 +1277,150 @@ import { execFileSync as execFileSync28, spawn as spawn5 } from "child_process";
1277
1277
  import * as fs26 from "fs";
1278
1278
  import * as path23 from "path";
1279
1279
 
1280
- // src/litellm.ts
1281
- import { execFileSync as execFileSync3, spawn } from "child_process";
1282
- import * as fs8 from "fs";
1283
- import * as os from "os";
1284
- import * as path7 from "path";
1285
- async function checkLitellmHealth(url) {
1286
- try {
1287
- const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
1288
- return response.ok;
1289
- } catch {
1290
- return false;
1291
- }
1280
+ // src/issue.ts
1281
+ import { execFileSync as execFileSync3 } from "child_process";
1282
+ var API_TIMEOUT_MS = 3e4;
1283
+ function ghToken() {
1284
+ return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
1292
1285
  }
1293
- function generateLitellmConfigYaml(model) {
1294
- const apiKeyVar = providerApiKeyEnvVar(model.provider);
1295
- return [
1296
- "model_list:",
1297
- ` - model_name: ${model.model}`,
1298
- ` litellm_params:`,
1299
- ` model: ${model.provider}/${model.model}`,
1300
- ` api_key: os.environ/${apiKeyVar}`,
1301
- "",
1302
- "litellm_settings:",
1303
- " drop_params: true",
1304
- ""
1305
- ].join("\n");
1286
+ function gh(args, options) {
1287
+ const token = ghToken();
1288
+ const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
1289
+ return execFileSync3("gh", args, {
1290
+ encoding: "utf-8",
1291
+ timeout: API_TIMEOUT_MS,
1292
+ cwd: options?.cwd,
1293
+ env,
1294
+ input: options?.input,
1295
+ stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
1296
+ }).trim();
1306
1297
  }
1307
- async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL) {
1308
- if (!needsLitellmProxy(model)) return null;
1309
- if (await checkLitellmHealth(url)) {
1310
- return { url, kill: () => {
1311
- } };
1298
+ function getIssue(issueNumber, cwd) {
1299
+ const output = gh(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
1300
+ const parsed = JSON.parse(output);
1301
+ if (typeof parsed?.title !== "string") {
1302
+ throw new Error(`Issue #${issueNumber}: unexpected response shape`);
1312
1303
  }
1313
- let cmd = "litellm";
1304
+ return {
1305
+ number: parsed.number ?? issueNumber,
1306
+ title: parsed.title,
1307
+ body: parsed.body ?? "",
1308
+ comments: (parsed.comments ?? []).map((c) => ({
1309
+ body: c.body ?? "",
1310
+ author: c.author?.login ?? "unknown",
1311
+ createdAt: c.createdAt ?? ""
1312
+ })),
1313
+ labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
1314
+ };
1315
+ }
1316
+ function stripKodyMentions(body) {
1317
+ return body.replace(/(@)(kody)/gi, "$1\u200B$2");
1318
+ }
1319
+ function postIssueComment(issueNumber, body, cwd) {
1314
1320
  try {
1315
- execFileSync3("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1316
- } catch {
1317
- try {
1318
- execFileSync3("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
1319
- cmd = "python3";
1320
- } catch {
1321
- throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
1322
- }
1321
+ gh(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
1322
+ } catch (err) {
1323
+ process.stderr.write(
1324
+ `[kody] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
1325
+ `
1326
+ );
1323
1327
  }
1324
- const configPath = path7.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
1325
- fs8.writeFileSync(configPath, generateLitellmConfigYaml(model));
1326
- const portMatch = url.match(/:(\d+)/);
1327
- const port = portMatch ? portMatch[1] : "4000";
1328
- const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
1329
- const dotenvVars = readDotenvApiKeys(projectDir);
1330
- const logPath = path7.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
1331
- const outFd = fs8.openSync(logPath, "w");
1332
- const child = spawn(cmd, args, {
1333
- stdio: ["ignore", outFd, outFd],
1334
- detached: true,
1335
- env: stripBlockingEnv({ ...process.env, ...dotenvVars })
1328
+ }
1329
+ function truncate2(s, maxBytes) {
1330
+ if (s.length <= maxBytes) return s;
1331
+ return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
1332
+ }
1333
+ function parsePrNumber(url) {
1334
+ const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
1335
+ if (!m) return null;
1336
+ const n = parseInt(m[1], 10);
1337
+ return Number.isFinite(n) ? n : null;
1338
+ }
1339
+ function getPr(prNumber, cwd) {
1340
+ const output = gh(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
1341
+ cwd
1336
1342
  });
1337
- fs8.closeSync(outFd);
1338
- for (let i = 0; i < 30; i++) {
1339
- await new Promise((r) => setTimeout(r, 2e3));
1340
- if (await checkLitellmHealth(url)) {
1341
- return {
1342
- url,
1343
- kill: () => {
1344
- try {
1345
- child.kill();
1346
- } catch {
1347
- }
1348
- }
1349
- };
1350
- }
1343
+ const parsed = JSON.parse(output);
1344
+ if (typeof parsed?.title !== "string") {
1345
+ throw new Error(`PR #${prNumber}: unexpected response shape`);
1351
1346
  }
1352
- let logTail = "";
1347
+ return {
1348
+ number: parsed.number ?? prNumber,
1349
+ title: parsed.title,
1350
+ body: parsed.body ?? "",
1351
+ headRefName: String(parsed.headRefName ?? ""),
1352
+ baseRefName: String(parsed.baseRefName ?? ""),
1353
+ state: String(parsed.state ?? "")
1354
+ };
1355
+ }
1356
+ function getPrDiff(prNumber, cwd) {
1353
1357
  try {
1354
- logTail = fs8.readFileSync(logPath, "utf-8").slice(-2e3);
1355
- } catch {
1358
+ return gh(["pr", "diff", String(prNumber)], { cwd });
1359
+ } catch (err) {
1360
+ process.stderr.write(
1361
+ `[kody] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
1362
+ `
1363
+ );
1364
+ return "";
1356
1365
  }
1366
+ }
1367
+ function getPrReviews(prNumber, cwd) {
1357
1368
  try {
1358
- child.kill();
1369
+ const output = gh(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
1370
+ const parsed = JSON.parse(output);
1371
+ if (!Array.isArray(parsed?.reviews)) return [];
1372
+ return parsed.reviews.map(
1373
+ (r) => ({
1374
+ body: r.body ?? "",
1375
+ state: r.state ?? "",
1376
+ author: r.author?.login ?? "unknown",
1377
+ submittedAt: r.submittedAt ?? ""
1378
+ })
1379
+ );
1359
1380
  } catch {
1381
+ return [];
1360
1382
  }
1361
- throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
1362
- ${logTail}`);
1363
1383
  }
1364
- function readDotenvApiKeys(projectDir) {
1365
- const dotenvPath = path7.join(projectDir, ".env");
1366
- if (!fs8.existsSync(dotenvPath)) return {};
1367
- const result = {};
1368
- for (const rawLine of fs8.readFileSync(dotenvPath, "utf-8").split("\n")) {
1369
- const line = rawLine.trim();
1370
- if (!line || line.startsWith("#")) continue;
1371
- const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
1372
- if (!match) continue;
1373
- let value = match[2].trim();
1374
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1375
- value = value.slice(1, -1);
1376
- }
1377
- const commentIdx = value.indexOf(" #");
1378
- if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
1379
- if (value) result[match[1]] = value;
1384
+ function getPrComments(prNumber, cwd) {
1385
+ try {
1386
+ const output = gh(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
1387
+ const parsed = JSON.parse(output);
1388
+ if (!Array.isArray(parsed?.comments)) return [];
1389
+ return parsed.comments.map((c) => ({
1390
+ body: c.body ?? "",
1391
+ author: c.author?.login ?? "unknown",
1392
+ createdAt: c.createdAt ?? ""
1393
+ })).filter((c) => c.body.trim().length > 0);
1394
+ } catch {
1395
+ return [];
1380
1396
  }
1381
- return result;
1382
1397
  }
1383
- function stripBlockingEnv(env) {
1384
- const out = { ...env };
1385
- delete out.DATABASE_URL;
1386
- delete out.AI_BASE_URL;
1387
- return out;
1398
+ var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
1399
+ function isReviewShaped(body) {
1400
+ return VERDICT_HEADING.test(body);
1401
+ }
1402
+ function getPrLatestReviewBody(prNumber, cwd) {
1403
+ const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
1404
+ const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
1405
+ const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
1406
+ if (all.length > 0) return all[0].body;
1407
+ const pr = getPr(prNumber, cwd);
1408
+ return pr.body;
1409
+ }
1410
+ function postPrReviewComment(prNumber, body, cwd) {
1411
+ try {
1412
+ gh(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
1413
+ } catch (err) {
1414
+ process.stderr.write(
1415
+ `[kody] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
1416
+ `
1417
+ );
1418
+ }
1388
1419
  }
1389
1420
 
1390
1421
  // src/profile.ts
1391
- import * as fs9 from "fs";
1392
- import * as path8 from "path";
1422
+ import * as fs8 from "fs";
1423
+ import * as path7 from "path";
1393
1424
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
1394
1425
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
1395
1426
  var VALID_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "container", "watch", "utility"]);
@@ -1405,12 +1436,12 @@ var ProfileError = class extends Error {
1405
1436
  profilePath;
1406
1437
  };
1407
1438
  function loadProfile(profilePath) {
1408
- if (!fs9.existsSync(profilePath)) {
1439
+ if (!fs8.existsSync(profilePath)) {
1409
1440
  throw new ProfileError(profilePath, "file not found");
1410
1441
  }
1411
1442
  let raw;
1412
1443
  try {
1413
- raw = JSON.parse(fs9.readFileSync(profilePath, "utf-8"));
1444
+ raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
1414
1445
  } catch (err) {
1415
1446
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
1416
1447
  }
@@ -1449,7 +1480,7 @@ function loadProfile(profilePath) {
1449
1480
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
1450
1481
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
1451
1482
  children,
1452
- dir: path8.dirname(profilePath)
1483
+ dir: path7.dirname(profilePath)
1453
1484
  };
1454
1485
  return profile;
1455
1486
  }
@@ -1559,17 +1590,11 @@ function parseScripts(p, raw) {
1559
1590
  throw new ProfileError(p, `"scripts" must be an object with preflight and postflight arrays`);
1560
1591
  }
1561
1592
  const r = raw;
1562
- const preflight = parseScriptList(p, "preflight", r.preflight);
1563
- const postflight = parseScriptList(p, "postflight", r.postflight);
1564
1593
  return {
1565
- preflight,
1566
- postflight: pairLifecycleClears(preflight, postflight)
1594
+ preflight: parseScriptList(p, "preflight", r.preflight),
1595
+ postflight: parseScriptList(p, "postflight", r.postflight)
1567
1596
  };
1568
1597
  }
1569
- function pairLifecycleClears(preflight, postflight) {
1570
- const clears = preflight.filter((e) => e.script === "setLifecycleLabel" && typeof e.with?.label === "string").map((e) => ({ script: "clearLifecycleLabel", with: { label: e.with.label } }));
1571
- return [...postflight, ...clears];
1572
- }
1573
1598
  function parseInputArtifacts(p, raw) {
1574
1599
  if (raw === void 0 || raw === null) return [];
1575
1600
  if (typeof raw !== "object" || Array.isArray(raw)) {
@@ -1698,206 +1723,434 @@ function parseScriptList(p, key, raw) {
1698
1723
  return out;
1699
1724
  }
1700
1725
 
1701
- // src/commit.ts
1702
- import { execFileSync as execFileSync4 } from "child_process";
1703
- import * as fs10 from "fs";
1704
- import * as path9 from "path";
1705
- var FORBIDDEN_PATH_PREFIXES = [
1706
- ".kody/",
1707
- ".kody-engine/",
1708
- ".kody/",
1709
- ".kody-lean/",
1710
- // back-compat: stale runtime dir from kody-lean v0.5.x
1711
- "node_modules/",
1712
- "dist/",
1713
- "build/"
1714
- ];
1715
- var ALLOWED_PATH_PREFIXES = [".kody/memory/"];
1716
- var FORBIDDEN_PATH_EXACT = /* @__PURE__ */ new Set([".env", ".kody-pip-requirements.txt"]);
1717
- var FORBIDDEN_PATH_SUFFIXES = [".log"];
1718
- var CONVENTIONAL_PREFIXES = [
1719
- "feat:",
1720
- "fix:",
1721
- "chore:",
1722
- "docs:",
1723
- "refactor:",
1724
- "test:",
1725
- "perf:",
1726
- "ci:",
1727
- "style:",
1728
- "build:",
1729
- "revert:"
1730
- ];
1731
- function git(args, cwd) {
1732
- try {
1733
- return execFileSync4("git", args, {
1734
- encoding: "utf-8",
1735
- timeout: 12e4,
1736
- cwd,
1737
- env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1738
- stdio: ["pipe", "pipe", "pipe"]
1739
- }).trim();
1740
- } catch (err) {
1741
- const e = err;
1742
- const stderr = e.stderr?.toString().trim() ?? "";
1743
- const stdout = e.stdout?.toString().trim() ?? "";
1744
- const status = e.status ?? "?";
1745
- const detail = stderr || stdout || e.message || "(no output)";
1746
- throw new Error(`git ${args.join(" ")} (exit ${status}):
1747
- ${detail}`);
1748
- }
1726
+ // src/lifecycleLabels.ts
1727
+ var KODY_NAMESPACE = "kody";
1728
+ function groupOf(label) {
1729
+ const idx = label.indexOf(":");
1730
+ return idx === -1 ? label : label.slice(0, idx + 1);
1749
1731
  }
1750
- function tryGit(args, cwd) {
1751
- try {
1752
- git(args, cwd);
1753
- return true;
1754
- } catch {
1755
- return false;
1732
+ function collectProfileLabels() {
1733
+ const byLabel = /* @__PURE__ */ new Map();
1734
+ for (const exe of listExecutables()) {
1735
+ let profile;
1736
+ try {
1737
+ profile = loadProfile(exe.profilePath);
1738
+ } catch {
1739
+ continue;
1740
+ }
1741
+ for (const entry of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
1742
+ const spec = extractLabelSpec(entry);
1743
+ if (spec) byLabel.set(spec.label, spec);
1744
+ }
1756
1745
  }
1746
+ return [...byLabel.values()];
1757
1747
  }
1758
- function abortUnfinishedGitOps(cwd) {
1759
- const aborted = [];
1760
- const gitDir = path9.join(cwd ?? process.cwd(), ".git");
1761
- if (!fs10.existsSync(gitDir)) return aborted;
1762
- if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
1763
- if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
1764
- }
1765
- if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
1766
- if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
1767
- }
1768
- if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
1769
- if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
1770
- }
1771
- if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
1772
- if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
1748
+ function extractLabelSpec(entry) {
1749
+ if (entry.script !== "setLifecycleLabel") return null;
1750
+ const w = entry.with;
1751
+ if (!w) return null;
1752
+ const label = typeof w.label === "string" ? w.label : null;
1753
+ if (!label || !label.startsWith(KODY_NAMESPACE)) return null;
1754
+ return {
1755
+ label,
1756
+ color: typeof w.color === "string" ? w.color : void 0,
1757
+ description: typeof w.description === "string" ? w.description : void 0
1758
+ };
1759
+ }
1760
+ function ensureLabels(cwd) {
1761
+ const result = { created: [], failed: [] };
1762
+ for (const spec of collectProfileLabels()) {
1763
+ try {
1764
+ createLabelInRepo(spec, cwd);
1765
+ result.created.push(spec.label);
1766
+ } catch (err) {
1767
+ result.failed.push({ label: spec.label, reason: errMsg(err) });
1768
+ }
1773
1769
  }
1770
+ return result;
1771
+ }
1772
+ function getIssueLabels(issueNumber, cwd) {
1774
1773
  try {
1775
- const unmerged = git(["diff", "--name-only", "--diff-filter=U"], cwd);
1776
- if (unmerged) {
1777
- tryGit(["reset", "--mixed", "HEAD"], cwd);
1778
- aborted.push("unmerged-paths-reset");
1779
- }
1774
+ const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
1775
+ return output.split("\n").filter(Boolean);
1780
1776
  } catch {
1777
+ return [];
1781
1778
  }
1782
- return aborted;
1783
- }
1784
- function isForbiddenPath(p) {
1785
- if (FORBIDDEN_PATH_EXACT.has(p)) return true;
1786
- for (const pre of ALLOWED_PATH_PREFIXES) if (p.startsWith(pre)) return false;
1787
- for (const pre of FORBIDDEN_PATH_PREFIXES) if (p.startsWith(pre)) return true;
1788
- for (const suf of FORBIDDEN_PATH_SUFFIXES) if (p.endsWith(suf)) return true;
1789
- return false;
1790
1779
  }
1791
- function listChangedFiles(cwd) {
1792
- const raw = execFileSync4("git", ["status", "--porcelain=v1", "-z"], {
1793
- encoding: "utf-8",
1794
- cwd,
1795
- env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1796
- stdio: ["pipe", "pipe", "pipe"]
1797
- });
1798
- if (!raw) return [];
1799
- const entries = raw.split("\0").filter((e) => e.length > 0);
1800
- return entries.map((e) => e.slice(3)).filter(Boolean);
1780
+ function addLabel(issueNumber, label, cwd) {
1781
+ gh(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
1801
1782
  }
1802
- function listFilesInCommit(ref = "HEAD", cwd) {
1783
+ function removeLabel(issueNumber, label, cwd) {
1803
1784
  try {
1804
- const raw = execFileSync4("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
1805
- encoding: "utf-8",
1806
- cwd,
1807
- env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1808
- stdio: ["pipe", "pipe", "pipe"]
1809
- });
1810
- return raw.split("\0").map((s) => s.trim()).filter(Boolean);
1785
+ gh(["issue", "edit", String(issueNumber), "--remove-label", label], { cwd });
1811
1786
  } catch {
1812
- return [];
1813
1787
  }
1814
1788
  }
1815
- function normalizeCommitMessage(raw) {
1816
- const trimmed = raw.trim().replace(/^['"]|['"]$/g, "").trim();
1817
- if (!trimmed) return "chore: kody update";
1818
- const firstLine2 = trimmed.split("\n")[0];
1819
- for (const prefix of CONVENTIONAL_PREFIXES) {
1820
- if (firstLine2.toLowerCase().startsWith(prefix)) return trimmed;
1821
- }
1822
- return `chore: ${trimmed}`;
1789
+ function createLabelInRepo(spec, cwd) {
1790
+ const args = ["label", "create", spec.label, "--force"];
1791
+ if (spec.color) args.push("--color", spec.color);
1792
+ if (spec.description) args.push("--description", spec.description);
1793
+ gh(args, { cwd });
1823
1794
  }
1824
- function commitAndPush(branch, agentMessage, cwd) {
1825
- const allChanged = listChangedFiles(cwd);
1826
- const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
1827
- const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1828
- if (allowedFiles.length === 0 && !mergeHeadExists) {
1829
- return { committed: false, pushed: false, sha: "", message: "" };
1795
+ function setKodyLabel(issueNumber, spec, cwd) {
1796
+ const target = spec.label;
1797
+ if (!target.startsWith(KODY_NAMESPACE)) {
1798
+ process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
1799
+ `);
1800
+ return;
1830
1801
  }
1831
- for (const f of allowedFiles) {
1832
- try {
1833
- git(["add", "--", f], cwd);
1834
- } catch {
1802
+ const targetGroup = groupOf(target);
1803
+ const present = getIssueLabels(issueNumber, cwd);
1804
+ for (const label of present) {
1805
+ if (label !== target && label.startsWith(KODY_NAMESPACE) && groupOf(label) === targetGroup) {
1806
+ removeLabel(issueNumber, label, cwd);
1835
1807
  }
1836
1808
  }
1837
- const message = normalizeCommitMessage(agentMessage);
1838
1809
  try {
1839
- git(["commit", "--no-gpg-sign", "-m", message], cwd);
1810
+ addLabel(issueNumber, target, cwd);
1840
1811
  } catch (err) {
1841
- const msg = err instanceof Error ? err.message : String(err);
1842
- if (/nothing to commit/i.test(msg)) {
1843
- return { committed: false, pushed: false, sha: "", message };
1812
+ if (looksLikeMissingLabel(err)) {
1813
+ try {
1814
+ createLabelInRepo(spec, cwd);
1815
+ addLabel(issueNumber, target, cwd);
1816
+ return;
1817
+ } catch (retryErr) {
1818
+ process.stderr.write(
1819
+ `[kody] setKodyLabel: create+retry failed for ${target} on #${issueNumber}: ${errMsg(retryErr)}
1820
+ `
1821
+ );
1822
+ return;
1823
+ }
1844
1824
  }
1845
- throw err;
1825
+ process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
1826
+ `);
1846
1827
  }
1847
- const sha = git(["rev-parse", "HEAD"], cwd).slice(0, 7);
1828
+ }
1829
+ function looksLikeMissingLabel(err) {
1830
+ const msg = errMsg(err).toLowerCase();
1831
+ return msg.includes("not found") || msg.includes("could not add label") || msg.includes("could not resolve to a label");
1832
+ }
1833
+ function errMsg(err) {
1834
+ if (err instanceof Error) return err.message;
1835
+ if (typeof err === "object" && err !== null) {
1836
+ const e = err;
1837
+ const stderr = e.stderr?.toString().trim();
1838
+ if (stderr) return stderr;
1839
+ if (e.message) return e.message;
1840
+ }
1841
+ return String(err);
1842
+ }
1843
+
1844
+ // src/litellm.ts
1845
+ import { execFileSync as execFileSync4, spawn } from "child_process";
1846
+ import * as fs9 from "fs";
1847
+ import * as os from "os";
1848
+ import * as path8 from "path";
1849
+ async function checkLitellmHealth(url) {
1848
1850
  try {
1849
- git(["push", "-u", "origin", branch], cwd);
1850
- return { committed: true, pushed: true, sha, message };
1851
- } catch (firstErr) {
1852
- try {
1853
- git(["push", "--force-with-lease", "-u", "origin", branch], cwd);
1854
- return { committed: true, pushed: true, sha, message };
1855
- } catch (secondErr) {
1856
- const tail = (secondErr instanceof Error ? secondErr.message : String(secondErr)).slice(-400);
1857
- const initial = firstErr instanceof Error ? firstErr.message : String(firstErr);
1858
- return {
1859
- committed: true,
1860
- pushed: false,
1861
- sha,
1862
- message,
1863
- pushError: `push failed: ${initial.slice(-200)} | force-with-lease failed: ${tail}`
1864
- };
1865
- }
1851
+ const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
1852
+ return response.ok;
1853
+ } catch {
1854
+ return false;
1866
1855
  }
1867
1856
  }
1868
- function hasCommitsAhead(branch, defaultBranch, cwd) {
1857
+ function generateLitellmConfigYaml(model) {
1858
+ const apiKeyVar = providerApiKeyEnvVar(model.provider);
1859
+ return [
1860
+ "model_list:",
1861
+ ` - model_name: ${model.model}`,
1862
+ ` litellm_params:`,
1863
+ ` model: ${model.provider}/${model.model}`,
1864
+ ` api_key: os.environ/${apiKeyVar}`,
1865
+ "",
1866
+ "litellm_settings:",
1867
+ " drop_params: true",
1868
+ ""
1869
+ ].join("\n");
1870
+ }
1871
+ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL) {
1872
+ if (!needsLitellmProxy(model)) return null;
1873
+ if (await checkLitellmHealth(url)) {
1874
+ return { url, kill: () => {
1875
+ } };
1876
+ }
1877
+ let cmd = "litellm";
1869
1878
  try {
1870
- const out = git(["rev-list", "--count", `origin/${defaultBranch}..${branch}`], cwd);
1871
- return parseInt(out, 10) > 0;
1879
+ execFileSync4("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1872
1880
  } catch {
1873
1881
  try {
1874
- const out = git(["rev-list", "--count", `${defaultBranch}..${branch}`], cwd);
1875
- return parseInt(out, 10) > 0;
1882
+ execFileSync4("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
1883
+ cmd = "python3";
1876
1884
  } catch {
1877
- return false;
1885
+ throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
1878
1886
  }
1879
1887
  }
1880
- }
1881
-
1882
- // src/scripts/abortUnfinishedGitOps.ts
1883
- var abortUnfinishedGitOps2 = async (ctx) => {
1884
- if (ctx.data.agentDone === false) return;
1885
- const aborted = abortUnfinishedGitOps(ctx.cwd);
1886
- if (aborted.length > 0) {
1887
- process.stderr.write(`[kody] cleaned up unfinished git ops: ${aborted.join(", ")}
1888
- `);
1888
+ const configPath = path8.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
1889
+ fs9.writeFileSync(configPath, generateLitellmConfigYaml(model));
1890
+ const portMatch = url.match(/:(\d+)/);
1891
+ const port = portMatch ? portMatch[1] : "4000";
1892
+ const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
1893
+ const dotenvVars = readDotenvApiKeys(projectDir);
1894
+ const logPath = path8.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
1895
+ const outFd = fs9.openSync(logPath, "w");
1896
+ const child = spawn(cmd, args, {
1897
+ stdio: ["ignore", outFd, outFd],
1898
+ detached: true,
1899
+ env: stripBlockingEnv({ ...process.env, ...dotenvVars })
1900
+ });
1901
+ fs9.closeSync(outFd);
1902
+ for (let i = 0; i < 30; i++) {
1903
+ await new Promise((r) => setTimeout(r, 2e3));
1904
+ if (await checkLitellmHealth(url)) {
1905
+ return {
1906
+ url,
1907
+ kill: () => {
1908
+ try {
1909
+ child.kill();
1910
+ } catch {
1911
+ }
1912
+ }
1913
+ };
1914
+ }
1889
1915
  }
1890
- };
1891
-
1892
- // src/scripts/advanceFlow.ts
1893
- import { execFileSync as execFileSync6 } from "child_process";
1894
-
1895
- // src/state.ts
1896
- import { execFileSync as execFileSync5 } from "child_process";
1897
- var STATE_BEGIN = "<!-- kody:state:v1:begin -->";
1898
- var STATE_END = "<!-- kody:state:v1:end -->";
1899
- var HISTORY_MAX_ENTRIES = 20;
1900
- var API_TIMEOUT_MS = 3e4;
1916
+ let logTail = "";
1917
+ try {
1918
+ logTail = fs9.readFileSync(logPath, "utf-8").slice(-2e3);
1919
+ } catch {
1920
+ }
1921
+ try {
1922
+ child.kill();
1923
+ } catch {
1924
+ }
1925
+ throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
1926
+ ${logTail}`);
1927
+ }
1928
+ function readDotenvApiKeys(projectDir) {
1929
+ const dotenvPath = path8.join(projectDir, ".env");
1930
+ if (!fs9.existsSync(dotenvPath)) return {};
1931
+ const result = {};
1932
+ for (const rawLine of fs9.readFileSync(dotenvPath, "utf-8").split("\n")) {
1933
+ const line = rawLine.trim();
1934
+ if (!line || line.startsWith("#")) continue;
1935
+ const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
1936
+ if (!match) continue;
1937
+ let value = match[2].trim();
1938
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1939
+ value = value.slice(1, -1);
1940
+ }
1941
+ const commentIdx = value.indexOf(" #");
1942
+ if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
1943
+ if (value) result[match[1]] = value;
1944
+ }
1945
+ return result;
1946
+ }
1947
+ function stripBlockingEnv(env) {
1948
+ const out = { ...env };
1949
+ delete out.DATABASE_URL;
1950
+ delete out.AI_BASE_URL;
1951
+ return out;
1952
+ }
1953
+
1954
+ // src/commit.ts
1955
+ import { execFileSync as execFileSync5 } from "child_process";
1956
+ import * as fs10 from "fs";
1957
+ import * as path9 from "path";
1958
+ var FORBIDDEN_PATH_PREFIXES = [
1959
+ ".kody/",
1960
+ ".kody-engine/",
1961
+ ".kody/",
1962
+ ".kody-lean/",
1963
+ // back-compat: stale runtime dir from kody-lean v0.5.x
1964
+ "node_modules/",
1965
+ "dist/",
1966
+ "build/"
1967
+ ];
1968
+ var ALLOWED_PATH_PREFIXES = [".kody/memory/"];
1969
+ var FORBIDDEN_PATH_EXACT = /* @__PURE__ */ new Set([".env", ".kody-pip-requirements.txt"]);
1970
+ var FORBIDDEN_PATH_SUFFIXES = [".log"];
1971
+ var CONVENTIONAL_PREFIXES = [
1972
+ "feat:",
1973
+ "fix:",
1974
+ "chore:",
1975
+ "docs:",
1976
+ "refactor:",
1977
+ "test:",
1978
+ "perf:",
1979
+ "ci:",
1980
+ "style:",
1981
+ "build:",
1982
+ "revert:"
1983
+ ];
1984
+ function git(args, cwd) {
1985
+ try {
1986
+ return execFileSync5("git", args, {
1987
+ encoding: "utf-8",
1988
+ timeout: 12e4,
1989
+ cwd,
1990
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
1991
+ stdio: ["pipe", "pipe", "pipe"]
1992
+ }).trim();
1993
+ } catch (err) {
1994
+ const e = err;
1995
+ const stderr = e.stderr?.toString().trim() ?? "";
1996
+ const stdout = e.stdout?.toString().trim() ?? "";
1997
+ const status = e.status ?? "?";
1998
+ const detail = stderr || stdout || e.message || "(no output)";
1999
+ throw new Error(`git ${args.join(" ")} (exit ${status}):
2000
+ ${detail}`);
2001
+ }
2002
+ }
2003
+ function tryGit(args, cwd) {
2004
+ try {
2005
+ git(args, cwd);
2006
+ return true;
2007
+ } catch {
2008
+ return false;
2009
+ }
2010
+ }
2011
+ function abortUnfinishedGitOps(cwd) {
2012
+ const aborted = [];
2013
+ const gitDir = path9.join(cwd ?? process.cwd(), ".git");
2014
+ if (!fs10.existsSync(gitDir)) return aborted;
2015
+ if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
2016
+ if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
2017
+ }
2018
+ if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
2019
+ if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
2020
+ }
2021
+ if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
2022
+ if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
2023
+ }
2024
+ if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
2025
+ if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
2026
+ }
2027
+ try {
2028
+ const unmerged = git(["diff", "--name-only", "--diff-filter=U"], cwd);
2029
+ if (unmerged) {
2030
+ tryGit(["reset", "--mixed", "HEAD"], cwd);
2031
+ aborted.push("unmerged-paths-reset");
2032
+ }
2033
+ } catch {
2034
+ }
2035
+ return aborted;
2036
+ }
2037
+ function isForbiddenPath(p) {
2038
+ if (FORBIDDEN_PATH_EXACT.has(p)) return true;
2039
+ for (const pre of ALLOWED_PATH_PREFIXES) if (p.startsWith(pre)) return false;
2040
+ for (const pre of FORBIDDEN_PATH_PREFIXES) if (p.startsWith(pre)) return true;
2041
+ for (const suf of FORBIDDEN_PATH_SUFFIXES) if (p.endsWith(suf)) return true;
2042
+ return false;
2043
+ }
2044
+ function listChangedFiles(cwd) {
2045
+ const raw = execFileSync5("git", ["status", "--porcelain=v1", "-z"], {
2046
+ encoding: "utf-8",
2047
+ cwd,
2048
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2049
+ stdio: ["pipe", "pipe", "pipe"]
2050
+ });
2051
+ if (!raw) return [];
2052
+ const entries = raw.split("\0").filter((e) => e.length > 0);
2053
+ return entries.map((e) => e.slice(3)).filter(Boolean);
2054
+ }
2055
+ function listFilesInCommit(ref = "HEAD", cwd) {
2056
+ try {
2057
+ const raw = execFileSync5("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
2058
+ encoding: "utf-8",
2059
+ cwd,
2060
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2061
+ stdio: ["pipe", "pipe", "pipe"]
2062
+ });
2063
+ return raw.split("\0").map((s) => s.trim()).filter(Boolean);
2064
+ } catch {
2065
+ return [];
2066
+ }
2067
+ }
2068
+ function normalizeCommitMessage(raw) {
2069
+ const trimmed = raw.trim().replace(/^['"]|['"]$/g, "").trim();
2070
+ if (!trimmed) return "chore: kody update";
2071
+ const firstLine2 = trimmed.split("\n")[0];
2072
+ for (const prefix of CONVENTIONAL_PREFIXES) {
2073
+ if (firstLine2.toLowerCase().startsWith(prefix)) return trimmed;
2074
+ }
2075
+ return `chore: ${trimmed}`;
2076
+ }
2077
+ function commitAndPush(branch, agentMessage, cwd) {
2078
+ const allChanged = listChangedFiles(cwd);
2079
+ const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
2080
+ const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
2081
+ if (allowedFiles.length === 0 && !mergeHeadExists) {
2082
+ return { committed: false, pushed: false, sha: "", message: "" };
2083
+ }
2084
+ for (const f of allowedFiles) {
2085
+ try {
2086
+ git(["add", "--", f], cwd);
2087
+ } catch {
2088
+ }
2089
+ }
2090
+ const message = normalizeCommitMessage(agentMessage);
2091
+ try {
2092
+ git(["commit", "--no-gpg-sign", "-m", message], cwd);
2093
+ } catch (err) {
2094
+ const msg = err instanceof Error ? err.message : String(err);
2095
+ if (/nothing to commit/i.test(msg)) {
2096
+ return { committed: false, pushed: false, sha: "", message };
2097
+ }
2098
+ throw err;
2099
+ }
2100
+ const sha = git(["rev-parse", "HEAD"], cwd).slice(0, 7);
2101
+ try {
2102
+ git(["push", "-u", "origin", branch], cwd);
2103
+ return { committed: true, pushed: true, sha, message };
2104
+ } catch (firstErr) {
2105
+ try {
2106
+ git(["push", "--force-with-lease", "-u", "origin", branch], cwd);
2107
+ return { committed: true, pushed: true, sha, message };
2108
+ } catch (secondErr) {
2109
+ const tail = (secondErr instanceof Error ? secondErr.message : String(secondErr)).slice(-400);
2110
+ const initial = firstErr instanceof Error ? firstErr.message : String(firstErr);
2111
+ return {
2112
+ committed: true,
2113
+ pushed: false,
2114
+ sha,
2115
+ message,
2116
+ pushError: `push failed: ${initial.slice(-200)} | force-with-lease failed: ${tail}`
2117
+ };
2118
+ }
2119
+ }
2120
+ }
2121
+ function hasCommitsAhead(branch, defaultBranch, cwd) {
2122
+ try {
2123
+ const out = git(["rev-list", "--count", `origin/${defaultBranch}..${branch}`], cwd);
2124
+ return parseInt(out, 10) > 0;
2125
+ } catch {
2126
+ try {
2127
+ const out = git(["rev-list", "--count", `${defaultBranch}..${branch}`], cwd);
2128
+ return parseInt(out, 10) > 0;
2129
+ } catch {
2130
+ return false;
2131
+ }
2132
+ }
2133
+ }
2134
+
2135
+ // src/scripts/abortUnfinishedGitOps.ts
2136
+ var abortUnfinishedGitOps2 = async (ctx) => {
2137
+ if (ctx.data.agentDone === false) return;
2138
+ const aborted = abortUnfinishedGitOps(ctx.cwd);
2139
+ if (aborted.length > 0) {
2140
+ process.stderr.write(`[kody] cleaned up unfinished git ops: ${aborted.join(", ")}
2141
+ `);
2142
+ }
2143
+ };
2144
+
2145
+ // src/scripts/advanceFlow.ts
2146
+ import { execFileSync as execFileSync7 } from "child_process";
2147
+
2148
+ // src/state.ts
2149
+ import { execFileSync as execFileSync6 } from "child_process";
2150
+ var STATE_BEGIN = "<!-- kody:state:v1:begin -->";
2151
+ var STATE_END = "<!-- kody:state:v1:end -->";
2152
+ var HISTORY_MAX_ENTRIES = 20;
2153
+ var API_TIMEOUT_MS2 = 3e4;
1901
2154
  function emptyState() {
1902
2155
  return {
1903
2156
  schemaVersion: 1,
@@ -1913,15 +2166,15 @@ function emptyState() {
1913
2166
  history: []
1914
2167
  };
1915
2168
  }
1916
- function ghToken() {
2169
+ function ghToken2() {
1917
2170
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
1918
2171
  }
1919
- function gh(args, input, cwd) {
1920
- const token = ghToken();
2172
+ function gh2(args, input, cwd) {
2173
+ const token = ghToken2();
1921
2174
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
1922
- return execFileSync5("gh", args, {
2175
+ return execFileSync6("gh", args, {
1923
2176
  encoding: "utf-8",
1924
- timeout: API_TIMEOUT_MS,
2177
+ timeout: API_TIMEOUT_MS2,
1925
2178
  cwd,
1926
2179
  env,
1927
2180
  input,
@@ -1931,7 +2184,7 @@ function gh(args, input, cwd) {
1931
2184
  function findStateComment(target, number, cwd) {
1932
2185
  const apiPath = target === "issue" ? `repos/{owner}/{repo}/issues/${number}/comments` : `repos/{owner}/{repo}/issues/${number}/comments`;
1933
2186
  try {
1934
- const raw = gh(["api", "--paginate", apiPath], void 0, cwd);
2187
+ const raw = gh2(["api", "--paginate", apiPath], void 0, cwd);
1935
2188
  const list = JSON.parse(raw);
1936
2189
  for (const c of list) {
1937
2190
  if (c.body?.includes(STATE_BEGIN)) {
@@ -2084,10 +2337,10 @@ function writeTaskState(target, number, state, cwd) {
2084
2337
  const existing = findStateComment(target, number, cwd);
2085
2338
  try {
2086
2339
  if (existing) {
2087
- gh(["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"], body, cwd);
2340
+ gh2(["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"], body, cwd);
2088
2341
  } else {
2089
2342
  const sub = target === "issue" ? "issue" : "pr";
2090
- gh([sub, "comment", String(number), "--body-file", "-"], body, cwd);
2343
+ gh2([sub, "comment", String(number), "--body-file", "-"], body, cwd);
2091
2344
  }
2092
2345
  } catch (err) {
2093
2346
  process.stderr.write(
@@ -2098,7 +2351,7 @@ function writeTaskState(target, number, state, cwd) {
2098
2351
  }
2099
2352
 
2100
2353
  // src/scripts/advanceFlow.ts
2101
- var API_TIMEOUT_MS2 = 3e4;
2354
+ var API_TIMEOUT_MS3 = 3e4;
2102
2355
  var advanceFlow = async (ctx, profile) => {
2103
2356
  const state = ctx.data.taskState;
2104
2357
  const flow = state?.flow;
@@ -2122,8 +2375,8 @@ var advanceFlow = async (ctx, profile) => {
2122
2375
  }
2123
2376
  const body = `@kody ${flow.name}`;
2124
2377
  try {
2125
- execFileSync6("gh", ["issue", "comment", String(flow.issueNumber), "--body", body], {
2126
- timeout: API_TIMEOUT_MS2,
2378
+ execFileSync7("gh", ["issue", "comment", String(flow.issueNumber), "--body", body], {
2379
+ timeout: API_TIMEOUT_MS3,
2127
2380
  cwd: ctx.cwd,
2128
2381
  stdio: ["ignore", "pipe", "pipe"]
2129
2382
  });
@@ -2231,7 +2484,7 @@ function copyDir(src, dst) {
2231
2484
  }
2232
2485
 
2233
2486
  // src/coverage.ts
2234
- import { execFileSync as execFileSync7 } from "child_process";
2487
+ import { execFileSync as execFileSync8 } from "child_process";
2235
2488
  function patternToRegex(pattern) {
2236
2489
  let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2237
2490
  s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
@@ -2249,7 +2502,7 @@ function renderSiblingPath(file, requireSibling) {
2249
2502
  }
2250
2503
  function safeGit(args, cwd) {
2251
2504
  try {
2252
- return execFileSync7("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
2505
+ return execFileSync8("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
2253
2506
  } catch {
2254
2507
  return "";
2255
2508
  }
@@ -2292,451 +2545,183 @@ function formatMissesForFeedback(misses) {
2292
2545
  }
2293
2546
 
2294
2547
  // src/prompt.ts
2295
- import * as fs12 from "fs";
2296
- import * as path11 from "path";
2297
- var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
2298
- var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
2299
- function loadProjectConventions(projectDir) {
2300
- const out = [];
2301
- for (const rel of CONVENTION_FILES) {
2302
- const abs = path11.join(projectDir, rel);
2303
- if (!fs12.existsSync(abs)) continue;
2304
- let content;
2305
- try {
2306
- content = fs12.readFileSync(abs, "utf-8");
2307
- } catch {
2308
- continue;
2309
- }
2310
- const truncated = content.length > CONVENTIONS_PER_FILE_MAX_BYTES;
2311
- if (truncated) content = `${content.slice(0, CONVENTIONS_PER_FILE_MAX_BYTES)}
2312
-
2313
- \u2026 (truncated)`;
2314
- out.push({ path: rel, content, truncated });
2315
- }
2316
- return out;
2317
- }
2318
- function parseAgentResult(finalText) {
2319
- const text = (finalText || "").trim();
2320
- if (!text)
2321
- return {
2322
- done: false,
2323
- commitMessage: "",
2324
- prSummary: "",
2325
- feedbackActions: "",
2326
- planDeviations: "",
2327
- priorArt: "",
2328
- failureReason: "agent produced no final message"
2329
- };
2330
- const MARKDOWN_PREFIX = "[\\s>*_#`~\\-]*";
2331
- const FAILED_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}FAILED${MARKDOWN_PREFIX}\\s*:\\s*(.+?)\\s*$`, "is");
2332
- const DONE_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}DONE\\b`, "i");
2333
- const failedMatch = text.match(FAILED_RE);
2334
- if (failedMatch) {
2335
- return {
2336
- done: false,
2337
- commitMessage: "",
2338
- prSummary: "",
2339
- feedbackActions: "",
2340
- planDeviations: "",
2341
- priorArt: "",
2342
- failureReason: stripMarkdownEmphasis(failedMatch[1])
2343
- };
2344
- }
2345
- const hasDoneMarker = DONE_RE.test(text);
2346
- const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text);
2347
- const hasPrSummary = /^[\s>*_#`~-]*PR_SUMMARY\s*:/im.test(text);
2348
- if (!hasDoneMarker && !hasCommitMsg && !hasPrSummary) {
2349
- const tail = text.length > 400 ? `\u2026${text.slice(-400)}` : text;
2350
- return {
2351
- done: false,
2352
- commitMessage: "",
2353
- prSummary: "",
2354
- feedbackActions: "",
2355
- planDeviations: "",
2356
- priorArt: "",
2357
- failureReason: `no DONE or FAILED marker in agent output \u2014 agent tail: ${tail}`
2358
- };
2359
- }
2360
- const commitMatch = text.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
2361
- const commitMessage = commitMatch ? stripMarkdownEmphasis(commitMatch[1]) : "";
2362
- const feedbackActions = extractBlock(
2363
- text,
2364
- /(?:^|\n)[ \t]*FEEDBACK_ACTIONS\s*:[ \t]*\n/i,
2365
- /(?:^|\n)[ \t]*(?:PLAN_DEVIATIONS|COMMIT_MSG|PR_SUMMARY|PRIOR_ART)\s*:/i
2366
- );
2367
- let planDeviations = extractBlock(
2368
- text,
2369
- /(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*\n/i,
2370
- /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|FEEDBACK_ACTIONS|PRIOR_ART)\s*:/i
2371
- );
2372
- if (!planDeviations) {
2373
- const inline = text.match(/(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2374
- if (inline) planDeviations = inline[1].trim();
2375
- }
2376
- let priorArt = "";
2377
- const priorArtInline = text.match(/(?:^|\n)[ \t]*PRIOR_ART\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2378
- if (priorArtInline) priorArt = priorArtInline[1].trim();
2379
- const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
2380
- let prSummary = "";
2381
- if (summaryStart !== -1) {
2382
- const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
2383
- prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
2384
- }
2385
- return { done: true, commitMessage, prSummary, feedbackActions, planDeviations, priorArt, failureReason: "" };
2386
- }
2387
- function stripMarkdownEmphasis(s) {
2388
- return s.trim().replace(/^[*_`~]+|[*_`~]+$/g, "").trim();
2389
- }
2390
- function extractBlock(text, startMarker, endMarker) {
2391
- const startIdx = text.search(startMarker);
2392
- if (startIdx === -1) return "";
2393
- const afterStart = text.slice(startIdx).replace(startMarker, "");
2394
- const endIdx = afterStart.search(endMarker);
2395
- const body = endIdx === -1 ? afterStart : afterStart.slice(0, endIdx);
2396
- return body.replace(/\n\s*```\s*$/g, "").trim();
2397
- }
2398
-
2399
- // src/scripts/checkCoverageWithRetry.ts
2400
- var checkCoverageWithRetry = async (ctx) => {
2401
- const reqs = ctx.data.coverageRules ?? [];
2402
- if (reqs.length === 0) {
2403
- ctx.data.coverageMisses = [];
2404
- return;
2405
- }
2406
- if (!ctx.data.agentDone) {
2407
- ctx.data.coverageMisses = [];
2408
- return;
2409
- }
2410
- const misses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
2411
- if (misses.length === 0) {
2412
- ctx.data.coverageMisses = [];
2413
- return;
2414
- }
2415
- const invoker = ctx.data.__invokeAgent;
2416
- const basePrompt = ctx.data.prompt;
2417
- if (!invoker || !basePrompt) {
2418
- ctx.data.coverageMisses = misses;
2419
- return;
2420
- }
2421
- process.stderr.write(`[kody] coverage check found ${misses.length} missing test(s); retrying agent once
2422
- `);
2423
- const retryPrompt = `${basePrompt}
2424
-
2425
- # Coverage failure (retry)
2426
- ${formatMissesForFeedback(misses)}`;
2427
- const retry = await invoker(retryPrompt);
2428
- const retryParsed = parseAgentResult(retry.finalText);
2429
- if (retry.outcome === "completed" && retryParsed.done) {
2430
- ctx.data.agentDone = true;
2431
- ctx.data.commitMessage = retryParsed.commitMessage || ctx.data.commitMessage;
2432
- ctx.data.prSummary = retryParsed.prSummary || ctx.data.prSummary;
2433
- }
2434
- const finalMisses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
2435
- ctx.data.coverageMisses = finalMisses;
2436
- };
2437
-
2438
- // src/scripts/classifyByLabel.ts
2439
- var VALID_CLASSES = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
2440
- var classifyByLabel = async (ctx) => {
2441
- const issue = ctx.data.issue;
2442
- const labels = issue?.labels;
2443
- if (!labels || labels.length === 0) return;
2444
- const cfgMap = ctx.config.classify?.labelMap;
2445
- const map = cfgMap ?? defaultLabelMap();
2446
- for (const label of labels) {
2447
- const candidate = map[label.toLowerCase()];
2448
- if (candidate && VALID_CLASSES.has(candidate)) {
2449
- ctx.data.classification = candidate;
2450
- ctx.data.classificationSource = "label";
2451
- ctx.data.classificationReason = `label \`${label}\` \u2192 ${candidate}`;
2452
- ctx.skipAgent = true;
2453
- return;
2454
- }
2455
- }
2456
- };
2457
- function defaultLabelMap() {
2458
- return {
2459
- bug: "bug",
2460
- enhancement: "bug",
2461
- refactor: "feature",
2462
- feature: "feature",
2463
- performance: "feature",
2464
- rfc: "spec",
2465
- design: "spec",
2466
- spec: "spec",
2467
- docs: "chore",
2468
- chore: "chore",
2469
- dependencies: "chore"
2470
- };
2471
- }
2472
-
2473
- // src/issue.ts
2474
- import { execFileSync as execFileSync8 } from "child_process";
2475
- var API_TIMEOUT_MS3 = 3e4;
2476
- function ghToken2() {
2477
- return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
2478
- }
2479
- function gh2(args, options) {
2480
- const token = ghToken2();
2481
- const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
2482
- return execFileSync8("gh", args, {
2483
- encoding: "utf-8",
2484
- timeout: API_TIMEOUT_MS3,
2485
- cwd: options?.cwd,
2486
- env,
2487
- input: options?.input,
2488
- stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
2489
- }).trim();
2490
- }
2491
- function getIssue(issueNumber, cwd) {
2492
- const output = gh2(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
2493
- const parsed = JSON.parse(output);
2494
- if (typeof parsed?.title !== "string") {
2495
- throw new Error(`Issue #${issueNumber}: unexpected response shape`);
2496
- }
2497
- return {
2498
- number: parsed.number ?? issueNumber,
2499
- title: parsed.title,
2500
- body: parsed.body ?? "",
2501
- comments: (parsed.comments ?? []).map((c) => ({
2502
- body: c.body ?? "",
2503
- author: c.author?.login ?? "unknown",
2504
- createdAt: c.createdAt ?? ""
2505
- })),
2506
- labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
2507
- };
2508
- }
2509
- function stripKodyMentions(body) {
2510
- return body.replace(/(@)(kody)/gi, "$1\u200B$2");
2511
- }
2512
- function postIssueComment(issueNumber, body, cwd) {
2513
- try {
2514
- gh2(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
2515
- } catch (err) {
2516
- process.stderr.write(
2517
- `[kody] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
2518
- `
2519
- );
2520
- }
2521
- }
2522
- function truncate2(s, maxBytes) {
2523
- if (s.length <= maxBytes) return s;
2524
- return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
2525
- }
2526
- function parsePrNumber(url) {
2527
- const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
2528
- if (!m) return null;
2529
- const n = parseInt(m[1], 10);
2530
- return Number.isFinite(n) ? n : null;
2531
- }
2532
- function getPr(prNumber, cwd) {
2533
- const output = gh2(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
2534
- cwd
2535
- });
2536
- const parsed = JSON.parse(output);
2537
- if (typeof parsed?.title !== "string") {
2538
- throw new Error(`PR #${prNumber}: unexpected response shape`);
2539
- }
2540
- return {
2541
- number: parsed.number ?? prNumber,
2542
- title: parsed.title,
2543
- body: parsed.body ?? "",
2544
- headRefName: String(parsed.headRefName ?? ""),
2545
- baseRefName: String(parsed.baseRefName ?? ""),
2546
- state: String(parsed.state ?? "")
2547
- };
2548
- }
2549
- function getPrDiff(prNumber, cwd) {
2550
- try {
2551
- return gh2(["pr", "diff", String(prNumber)], { cwd });
2552
- } catch (err) {
2553
- process.stderr.write(
2554
- `[kody] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
2555
- `
2556
- );
2557
- return "";
2558
- }
2559
- }
2560
- function getPrReviews(prNumber, cwd) {
2561
- try {
2562
- const output = gh2(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
2563
- const parsed = JSON.parse(output);
2564
- if (!Array.isArray(parsed?.reviews)) return [];
2565
- return parsed.reviews.map(
2566
- (r) => ({
2567
- body: r.body ?? "",
2568
- state: r.state ?? "",
2569
- author: r.author?.login ?? "unknown",
2570
- submittedAt: r.submittedAt ?? ""
2571
- })
2572
- );
2573
- } catch {
2574
- return [];
2575
- }
2576
- }
2577
- function getPrComments(prNumber, cwd) {
2578
- try {
2579
- const output = gh2(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
2580
- const parsed = JSON.parse(output);
2581
- if (!Array.isArray(parsed?.comments)) return [];
2582
- return parsed.comments.map((c) => ({
2583
- body: c.body ?? "",
2584
- author: c.author?.login ?? "unknown",
2585
- createdAt: c.createdAt ?? ""
2586
- })).filter((c) => c.body.trim().length > 0);
2587
- } catch {
2588
- return [];
2589
- }
2590
- }
2591
- var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
2592
- function isReviewShaped(body) {
2593
- return VERDICT_HEADING.test(body);
2594
- }
2595
- function getPrLatestReviewBody(prNumber, cwd) {
2596
- const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
2597
- const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
2598
- const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
2599
- if (all.length > 0) return all[0].body;
2600
- const pr = getPr(prNumber, cwd);
2601
- return pr.body;
2602
- }
2603
- function postPrReviewComment(prNumber, body, cwd) {
2604
- try {
2605
- gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
2606
- } catch (err) {
2607
- process.stderr.write(
2608
- `[kody] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
2609
- `
2610
- );
2611
- }
2612
- }
2613
-
2614
- // src/lifecycleLabels.ts
2615
- var KODY_NAMESPACE = "kody";
2616
- function groupOf(label) {
2617
- const idx = label.indexOf(":");
2618
- return idx === -1 ? label : label.slice(0, idx + 1);
2619
- }
2620
- function collectProfileLabels() {
2621
- const byLabel = /* @__PURE__ */ new Map();
2622
- for (const exe of listExecutables()) {
2623
- let profile;
2548
+ import * as fs12 from "fs";
2549
+ import * as path11 from "path";
2550
+ var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
2551
+ var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
2552
+ function loadProjectConventions(projectDir) {
2553
+ const out = [];
2554
+ for (const rel of CONVENTION_FILES) {
2555
+ const abs = path11.join(projectDir, rel);
2556
+ if (!fs12.existsSync(abs)) continue;
2557
+ let content;
2624
2558
  try {
2625
- profile = loadProfile(exe.profilePath);
2559
+ content = fs12.readFileSync(abs, "utf-8");
2626
2560
  } catch {
2627
2561
  continue;
2628
2562
  }
2629
- for (const entry of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
2630
- const spec = extractLabelSpec(entry);
2631
- if (spec) byLabel.set(spec.label, spec);
2632
- }
2563
+ const truncated = content.length > CONVENTIONS_PER_FILE_MAX_BYTES;
2564
+ if (truncated) content = `${content.slice(0, CONVENTIONS_PER_FILE_MAX_BYTES)}
2565
+
2566
+ \u2026 (truncated)`;
2567
+ out.push({ path: rel, content, truncated });
2633
2568
  }
2634
- return [...byLabel.values()];
2635
- }
2636
- function extractLabelSpec(entry) {
2637
- if (entry.script !== "setLifecycleLabel") return null;
2638
- const w = entry.with;
2639
- if (!w) return null;
2640
- const label = typeof w.label === "string" ? w.label : null;
2641
- if (!label || !label.startsWith(KODY_NAMESPACE)) return null;
2642
- return {
2643
- label,
2644
- color: typeof w.color === "string" ? w.color : void 0,
2645
- description: typeof w.description === "string" ? w.description : void 0
2646
- };
2569
+ return out;
2647
2570
  }
2648
- function ensureLabels(cwd) {
2649
- const result = { created: [], failed: [] };
2650
- for (const spec of collectProfileLabels()) {
2651
- try {
2652
- createLabelInRepo(spec, cwd);
2653
- result.created.push(spec.label);
2654
- } catch (err) {
2655
- result.failed.push({ label: spec.label, reason: errMsg(err) });
2656
- }
2571
+ function parseAgentResult(finalText) {
2572
+ const text = (finalText || "").trim();
2573
+ if (!text)
2574
+ return {
2575
+ done: false,
2576
+ commitMessage: "",
2577
+ prSummary: "",
2578
+ feedbackActions: "",
2579
+ planDeviations: "",
2580
+ priorArt: "",
2581
+ failureReason: "agent produced no final message"
2582
+ };
2583
+ const MARKDOWN_PREFIX = "[\\s>*_#`~\\-]*";
2584
+ const FAILED_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}FAILED${MARKDOWN_PREFIX}\\s*:\\s*(.+?)\\s*$`, "is");
2585
+ const DONE_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}DONE\\b`, "i");
2586
+ const failedMatch = text.match(FAILED_RE);
2587
+ if (failedMatch) {
2588
+ return {
2589
+ done: false,
2590
+ commitMessage: "",
2591
+ prSummary: "",
2592
+ feedbackActions: "",
2593
+ planDeviations: "",
2594
+ priorArt: "",
2595
+ failureReason: stripMarkdownEmphasis(failedMatch[1])
2596
+ };
2657
2597
  }
2658
- return result;
2659
- }
2660
- function getIssueLabels(issueNumber, cwd) {
2661
- try {
2662
- const output = gh2(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
2663
- return output.split("\n").filter(Boolean);
2664
- } catch {
2665
- return [];
2598
+ const hasDoneMarker = DONE_RE.test(text);
2599
+ const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text);
2600
+ const hasPrSummary = /^[\s>*_#`~-]*PR_SUMMARY\s*:/im.test(text);
2601
+ if (!hasDoneMarker && !hasCommitMsg && !hasPrSummary) {
2602
+ const tail = text.length > 400 ? `\u2026${text.slice(-400)}` : text;
2603
+ return {
2604
+ done: false,
2605
+ commitMessage: "",
2606
+ prSummary: "",
2607
+ feedbackActions: "",
2608
+ planDeviations: "",
2609
+ priorArt: "",
2610
+ failureReason: `no DONE or FAILED marker in agent output \u2014 agent tail: ${tail}`
2611
+ };
2666
2612
  }
2667
- }
2668
- function addLabel(issueNumber, label, cwd) {
2669
- gh2(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
2670
- }
2671
- function removeLabel(issueNumber, label, cwd) {
2672
- try {
2673
- gh2(["issue", "edit", String(issueNumber), "--remove-label", label], { cwd });
2674
- } catch {
2613
+ const commitMatch = text.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
2614
+ const commitMessage = commitMatch ? stripMarkdownEmphasis(commitMatch[1]) : "";
2615
+ const feedbackActions = extractBlock(
2616
+ text,
2617
+ /(?:^|\n)[ \t]*FEEDBACK_ACTIONS\s*:[ \t]*\n/i,
2618
+ /(?:^|\n)[ \t]*(?:PLAN_DEVIATIONS|COMMIT_MSG|PR_SUMMARY|PRIOR_ART)\s*:/i
2619
+ );
2620
+ let planDeviations = extractBlock(
2621
+ text,
2622
+ /(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*\n/i,
2623
+ /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|FEEDBACK_ACTIONS|PRIOR_ART)\s*:/i
2624
+ );
2625
+ if (!planDeviations) {
2626
+ const inline = text.match(/(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2627
+ if (inline) planDeviations = inline[1].trim();
2675
2628
  }
2629
+ let priorArt = "";
2630
+ const priorArtInline = text.match(/(?:^|\n)[ \t]*PRIOR_ART\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2631
+ if (priorArtInline) priorArt = priorArtInline[1].trim();
2632
+ const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
2633
+ let prSummary = "";
2634
+ if (summaryStart !== -1) {
2635
+ const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
2636
+ prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
2637
+ }
2638
+ return { done: true, commitMessage, prSummary, feedbackActions, planDeviations, priorArt, failureReason: "" };
2676
2639
  }
2677
- function createLabelInRepo(spec, cwd) {
2678
- const args = ["label", "create", spec.label, "--force"];
2679
- if (spec.color) args.push("--color", spec.color);
2680
- if (spec.description) args.push("--description", spec.description);
2681
- gh2(args, { cwd });
2640
+ function stripMarkdownEmphasis(s) {
2641
+ return s.trim().replace(/^[*_`~]+|[*_`~]+$/g, "").trim();
2682
2642
  }
2683
- function setKodyLabel(issueNumber, spec, cwd) {
2684
- const target = spec.label;
2685
- if (!target.startsWith(KODY_NAMESPACE)) {
2686
- process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
2687
- `);
2643
+ function extractBlock(text, startMarker, endMarker) {
2644
+ const startIdx = text.search(startMarker);
2645
+ if (startIdx === -1) return "";
2646
+ const afterStart = text.slice(startIdx).replace(startMarker, "");
2647
+ const endIdx = afterStart.search(endMarker);
2648
+ const body = endIdx === -1 ? afterStart : afterStart.slice(0, endIdx);
2649
+ return body.replace(/\n\s*```\s*$/g, "").trim();
2650
+ }
2651
+
2652
+ // src/scripts/checkCoverageWithRetry.ts
2653
+ var checkCoverageWithRetry = async (ctx) => {
2654
+ const reqs = ctx.data.coverageRules ?? [];
2655
+ if (reqs.length === 0) {
2656
+ ctx.data.coverageMisses = [];
2688
2657
  return;
2689
2658
  }
2690
- const targetGroup = groupOf(target);
2691
- const present = getIssueLabels(issueNumber, cwd);
2692
- for (const label of present) {
2693
- if (label !== target && label.startsWith(KODY_NAMESPACE) && groupOf(label) === targetGroup) {
2694
- removeLabel(issueNumber, label, cwd);
2695
- }
2659
+ if (!ctx.data.agentDone) {
2660
+ ctx.data.coverageMisses = [];
2661
+ return;
2696
2662
  }
2697
- try {
2698
- addLabel(issueNumber, target, cwd);
2699
- } catch (err) {
2700
- if (looksLikeMissingLabel(err)) {
2701
- try {
2702
- createLabelInRepo(spec, cwd);
2703
- addLabel(issueNumber, target, cwd);
2704
- return;
2705
- } catch (retryErr) {
2706
- process.stderr.write(
2707
- `[kody] setKodyLabel: create+retry failed for ${target} on #${issueNumber}: ${errMsg(retryErr)}
2708
- `
2709
- );
2710
- return;
2711
- }
2712
- }
2713
- process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
2714
- `);
2663
+ const misses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
2664
+ if (misses.length === 0) {
2665
+ ctx.data.coverageMisses = [];
2666
+ return;
2715
2667
  }
2716
- }
2717
- function looksLikeMissingLabel(err) {
2718
- const msg = errMsg(err).toLowerCase();
2719
- return msg.includes("not found") || msg.includes("could not add label") || msg.includes("could not resolve to a label");
2720
- }
2721
- function errMsg(err) {
2722
- if (err instanceof Error) return err.message;
2723
- if (typeof err === "object" && err !== null) {
2724
- const e = err;
2725
- const stderr = e.stderr?.toString().trim();
2726
- if (stderr) return stderr;
2727
- if (e.message) return e.message;
2668
+ const invoker = ctx.data.__invokeAgent;
2669
+ const basePrompt = ctx.data.prompt;
2670
+ if (!invoker || !basePrompt) {
2671
+ ctx.data.coverageMisses = misses;
2672
+ return;
2728
2673
  }
2729
- return String(err);
2730
- }
2674
+ process.stderr.write(`[kody] coverage check found ${misses.length} missing test(s); retrying agent once
2675
+ `);
2676
+ const retryPrompt = `${basePrompt}
2731
2677
 
2732
- // src/scripts/clearLifecycleLabel.ts
2733
- var clearLifecycleLabel = async (ctx, _profile, _agentResult, args) => {
2734
- const label = args?.label;
2735
- if (typeof label !== "string" || !label.startsWith(KODY_NAMESPACE)) return;
2736
- const target = ctx.args.issue ?? ctx.args.pr;
2737
- if (typeof target !== "number" || !Number.isFinite(target)) return;
2738
- removeLabel(target, label, ctx.cwd);
2678
+ # Coverage failure (retry)
2679
+ ${formatMissesForFeedback(misses)}`;
2680
+ const retry = await invoker(retryPrompt);
2681
+ const retryParsed = parseAgentResult(retry.finalText);
2682
+ if (retry.outcome === "completed" && retryParsed.done) {
2683
+ ctx.data.agentDone = true;
2684
+ ctx.data.commitMessage = retryParsed.commitMessage || ctx.data.commitMessage;
2685
+ ctx.data.prSummary = retryParsed.prSummary || ctx.data.prSummary;
2686
+ }
2687
+ const finalMisses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
2688
+ ctx.data.coverageMisses = finalMisses;
2689
+ };
2690
+
2691
+ // src/scripts/classifyByLabel.ts
2692
+ var VALID_CLASSES = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
2693
+ var classifyByLabel = async (ctx) => {
2694
+ const issue = ctx.data.issue;
2695
+ const labels = issue?.labels;
2696
+ if (!labels || labels.length === 0) return;
2697
+ const cfgMap = ctx.config.classify?.labelMap;
2698
+ const map = cfgMap ?? defaultLabelMap();
2699
+ for (const label of labels) {
2700
+ const candidate = map[label.toLowerCase()];
2701
+ if (candidate && VALID_CLASSES.has(candidate)) {
2702
+ ctx.data.classification = candidate;
2703
+ ctx.data.classificationSource = "label";
2704
+ ctx.data.classificationReason = `label \`${label}\` \u2192 ${candidate}`;
2705
+ ctx.skipAgent = true;
2706
+ return;
2707
+ }
2708
+ }
2739
2709
  };
2710
+ function defaultLabelMap() {
2711
+ return {
2712
+ bug: "bug",
2713
+ enhancement: "bug",
2714
+ refactor: "feature",
2715
+ feature: "feature",
2716
+ performance: "feature",
2717
+ rfc: "spec",
2718
+ design: "spec",
2719
+ spec: "spec",
2720
+ docs: "chore",
2721
+ chore: "chore",
2722
+ dependencies: "chore"
2723
+ };
2724
+ }
2740
2725
 
2741
2726
  // src/scripts/commitAndPush.ts
2742
2727
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
@@ -3009,7 +2994,7 @@ function splitReport(text) {
3009
2994
  function loadManifest(cwd) {
3010
2995
  let issuesJson;
3011
2996
  try {
3012
- issuesJson = gh2(
2997
+ issuesJson = gh(
3013
2998
  ["issue", "list", "--label", MANIFEST_LABEL, "--state", "all", "--limit", "1", "--json", "number,body"],
3014
2999
  { cwd }
3015
3000
  );
@@ -3062,7 +3047,7 @@ ${MANIFEST_END}
3062
3047
  }
3063
3048
  function ensureLabel(name, color, description, cwd) {
3064
3049
  try {
3065
- gh2(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
3050
+ gh(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
3066
3051
  } catch {
3067
3052
  }
3068
3053
  }
@@ -3117,10 +3102,10 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
3117
3102
  ensureLabel(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
3118
3103
  const body = serializeManifestBody(manifest);
3119
3104
  if (number !== null) {
3120
- gh2(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
3105
+ gh(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
3121
3106
  return { number, created: false };
3122
3107
  }
3123
- const out = gh2(["issue", "create", "--title", MANIFEST_TITLE, "--label", MANIFEST_LABEL, "--body-file", "-"], {
3108
+ const out = gh(["issue", "create", "--title", MANIFEST_TITLE, "--label", MANIFEST_LABEL, "--body-file", "-"], {
3124
3109
  input: body,
3125
3110
  cwd
3126
3111
  });
@@ -3237,7 +3222,7 @@ function createTaskIssue(finding, goalId, manifestNumber, cwd) {
3237
3222
  for (const l of labels) {
3238
3223
  args.push("--label", l);
3239
3224
  }
3240
- const out = gh2(args, { input: body, cwd });
3225
+ const out = gh(args, { input: body, cwd });
3241
3226
  const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
3242
3227
  const m = url.match(/\/issues\/(\d+)\b/);
3243
3228
  if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
@@ -3294,7 +3279,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
3294
3279
  const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
3295
3280
  let url = "";
3296
3281
  try {
3297
- const out = gh2(
3282
+ const out = gh(
3298
3283
  ["issue", "create", "--title", title, "--label", FINDING_LABEL, "--body-file", "-"],
3299
3284
  { input: finalText, cwd: ctx.cwd }
3300
3285
  );
@@ -4142,7 +4127,7 @@ function parseStateCommentBody(marker, body) {
4142
4127
  return isStateEnvelope(parsed) ? parsed : null;
4143
4128
  }
4144
4129
  function listIssueComments(owner, repo, issueNumber, cwd) {
4145
- const raw = gh2(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
4130
+ const raw = gh(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
4146
4131
  let parsed;
4147
4132
  try {
4148
4133
  parsed = JSON.parse(raw);
@@ -4163,7 +4148,7 @@ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
4163
4148
  }
4164
4149
  function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
4165
4150
  const body = formatStateCommentBody(marker, state);
4166
- const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
4151
+ const raw = gh(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
4167
4152
  cwd,
4168
4153
  input: JSON.stringify({ body })
4169
4154
  });
@@ -4176,7 +4161,7 @@ function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
4176
4161
  }
4177
4162
  function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
4178
4163
  const body = formatStateCommentBody(marker, state);
4179
- gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
4164
+ gh(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
4180
4165
  cwd,
4181
4166
  input: JSON.stringify({ body })
4182
4167
  });
@@ -4187,7 +4172,7 @@ function updateStateComment(owner, repo, commentId, commentNodeId, marker, state
4187
4172
  }
4188
4173
  function minimizeComment(nodeId, cwd) {
4189
4174
  const mutation = "mutation($id: ID!) { minimizeComment(input: { classifier: OUTDATED, subjectId: $id }) { minimizedComment { isMinimized } } }";
4190
- gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
4175
+ gh(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
4191
4176
  }
4192
4177
 
4193
4178
  // src/scripts/jobState/backend.ts
@@ -4224,7 +4209,7 @@ var ContentsApiBackend = class {
4224
4209
  const filePath = stateFilePath(this.jobsDir, slug);
4225
4210
  let raw = "";
4226
4211
  try {
4227
- raw = gh2(["api", `/repos/${this.owner}/${this.repo}/contents/${filePath}`], { cwd: this.cwd });
4212
+ raw = gh(["api", `/repos/${this.owner}/${this.repo}/contents/${filePath}`], { cwd: this.cwd });
4228
4213
  } catch (err) {
4229
4214
  const msg = err instanceof Error ? err.message : String(err);
4230
4215
  if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
@@ -4268,7 +4253,7 @@ var ContentsApiBackend = class {
4268
4253
  content: Buffer.from(body, "utf-8").toString("base64")
4269
4254
  };
4270
4255
  if (typeof loaded.handle === "string") payload.sha = loaded.handle;
4271
- gh2(["api", "--method", "PUT", `/repos/${this.owner}/${this.repo}/contents/${loaded.path}`, "--input", "-"], {
4256
+ gh(["api", "--method", "PUT", `/repos/${this.owner}/${this.repo}/contents/${loaded.path}`, "--input", "-"], {
4272
4257
  cwd: this.cwd,
4273
4258
  input: JSON.stringify(payload)
4274
4259
  });
@@ -4614,7 +4599,7 @@ var dispatchJobTicks = async (ctx, _profile, args) => {
4614
4599
  function listIssuesByLabel(label, cwd) {
4615
4600
  let raw = "";
4616
4601
  try {
4617
- raw = gh2(["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"], {
4602
+ raw = gh(["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"], {
4618
4603
  cwd
4619
4604
  });
4620
4605
  } catch {
@@ -4698,7 +4683,7 @@ function firstLine(s) {
4698
4683
  }
4699
4684
  function findExistingPr(branch, cwd) {
4700
4685
  try {
4701
- const output = gh2(
4686
+ const output = gh(
4702
4687
  ["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,body", "--limit", "1"],
4703
4688
  { cwd }
4704
4689
  );
@@ -4736,7 +4721,7 @@ function ensurePr(opts) {
4736
4721
  const stripped = existing.url.replace(/^https:\/\/github\.com\//, "");
4737
4722
  const [owner, repo] = stripped.split("/");
4738
4723
  try {
4739
- gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/pulls/${existing.number}`, "-f", `body=${body}`], {
4724
+ gh(["api", "--method", "PATCH", `repos/${owner}/${repo}/pulls/${existing.number}`, "-f", `body=${body}`], {
4740
4725
  cwd: opts.cwd
4741
4726
  });
4742
4727
  } catch (err) {
@@ -4758,7 +4743,7 @@ function ensurePr(opts) {
4758
4743
  "-"
4759
4744
  ];
4760
4745
  if (opts.draft) args.push("--draft");
4761
- const output = gh2(args, { input: body, cwd: opts.cwd });
4746
+ const output = gh(args, { input: body, cwd: opts.cwd });
4762
4747
  const url = output.trim();
4763
4748
  const match = url.match(/\/pull\/(\d+)$/);
4764
4749
  const number = match ? parseInt(match[1], 10) : 0;
@@ -5844,7 +5829,7 @@ function parsePrNumbers(raw) {
5844
5829
  }
5845
5830
  function fetchPrBlock(prNumber, cwd) {
5846
5831
  try {
5847
- const metaRaw = gh2(["pr", "view", String(prNumber), "--json", "title,state,url,mergedAt,closedAt"], { cwd });
5832
+ const metaRaw = gh(["pr", "view", String(prNumber), "--json", "title,state,url,mergedAt,closedAt"], { cwd });
5848
5833
  const meta = JSON.parse(metaRaw);
5849
5834
  const diff = truncate3(safeGh(["pr", "diff", String(prNumber)], cwd), PER_PR_DIFF_MAX_BYTES);
5850
5835
  const commentsRaw = safeGh(["pr", "view", String(prNumber), "--json", "comments,reviews"], cwd);
@@ -5871,7 +5856,7 @@ _Could not fetch \u2014 ${err instanceof Error ? err.message : String(err)}_`;
5871
5856
  }
5872
5857
  function safeGh(args, cwd) {
5873
5858
  try {
5874
- return gh2(args, { cwd });
5859
+ return gh(args, { cwd });
5875
5860
  } catch {
5876
5861
  return "";
5877
5862
  }
@@ -6059,7 +6044,7 @@ function buildIssueTitle(scope, verdict) {
6059
6044
  }
6060
6045
  function ensureLabel2(cwd) {
6061
6046
  try {
6062
- gh2(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
6047
+ gh(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
6063
6048
  return true;
6064
6049
  } catch {
6065
6050
  return false;
@@ -6068,7 +6053,7 @@ function ensureLabel2(cwd) {
6068
6053
  function createQaIssue(title, body, hasLabel, cwd) {
6069
6054
  const args = ["issue", "create", "--title", title, "--body-file", "-"];
6070
6055
  if (hasLabel) args.push("--label", QA_LABEL);
6071
- const out = gh2(args, { input: body, cwd });
6056
+ const out = gh(args, { input: body, cwd });
6072
6057
  const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
6073
6058
  const m = url.match(/\/issues\/(\d+)\b/);
6074
6059
  if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
@@ -6842,7 +6827,7 @@ function latestSuccessUrl(deploymentId, cwd) {
6842
6827
  }
6843
6828
  function safeGh2(args, cwd) {
6844
6829
  try {
6845
- return gh2(args, { cwd });
6830
+ return gh(args, { cwd });
6846
6831
  } catch {
6847
6832
  return null;
6848
6833
  }
@@ -8026,8 +8011,7 @@ var postflightScripts = {
8026
8011
  recordOutcome,
8027
8012
  mergeReleasePr,
8028
8013
  waitForCi,
8029
- markFlowSuccess,
8030
- clearLifecycleLabel
8014
+ markFlowSuccess
8031
8015
  };
8032
8016
  var allScriptNames = /* @__PURE__ */ new Set([
8033
8017
  ...Object.keys(preflightScripts),
@@ -8225,12 +8209,26 @@ async function runExecutable(profileName, input) {
8225
8209
  reason: ctx.output.reason
8226
8210
  });
8227
8211
  } finally {
8212
+ clearStampedLifecycleLabels(profile, ctx);
8228
8213
  try {
8229
8214
  litellm?.kill();
8230
8215
  } catch {
8231
8216
  }
8232
8217
  }
8233
8218
  }
8219
+ function clearStampedLifecycleLabels(profile, ctx) {
8220
+ const target = ctx.args.issue ?? ctx.args.pr;
8221
+ if (typeof target !== "number" || !Number.isFinite(target)) return;
8222
+ for (const entry of profile.scripts.preflight) {
8223
+ if (entry.script !== "setLifecycleLabel") continue;
8224
+ const label = typeof entry.with?.label === "string" ? entry.with.label : void 0;
8225
+ if (!label || !label.startsWith(KODY_NAMESPACE)) continue;
8226
+ try {
8227
+ removeLabel(target, label, ctx.cwd);
8228
+ } catch {
8229
+ }
8230
+ }
8231
+ }
8234
8232
  function resolveProfilePath(profileName) {
8235
8233
  const found = resolveExecutable(profileName);
8236
8234
  if (found) return found;