@kody-ade/kody-engine 0.4.23 → 0.4.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/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.25",
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",
@@ -1002,6 +1002,32 @@ function getExecutablesRoot() {
1002
1002
  function getProjectExecutablesRoot() {
1003
1003
  return path6.join(process.cwd(), ".kody", "executables");
1004
1004
  }
1005
+ function getBuiltinJobsRoot() {
1006
+ const here = path6.dirname(new URL(import.meta.url).pathname);
1007
+ const candidates = [
1008
+ path6.join(here, "jobs"),
1009
+ // dev: src/
1010
+ path6.join(here, "..", "jobs"),
1011
+ // built: dist/bin → dist/jobs
1012
+ path6.join(here, "..", "src", "jobs")
1013
+ // fallback
1014
+ ];
1015
+ for (const c of candidates) {
1016
+ if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
1017
+ }
1018
+ return candidates[0];
1019
+ }
1020
+ function listBuiltinJobs(root = getBuiltinJobsRoot()) {
1021
+ if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
1022
+ const out = [];
1023
+ for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
1024
+ if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
1025
+ const slug = ent.name.slice(0, -3);
1026
+ out.push({ slug, filePath: path6.join(root, ent.name) });
1027
+ }
1028
+ out.sort((a, b) => a.slug.localeCompare(b.slug));
1029
+ return out;
1030
+ }
1005
1031
  function getExecutableRoots() {
1006
1032
  return [getProjectExecutablesRoot(), getExecutablesRoot()];
1007
1033
  }
@@ -1277,119 +1303,150 @@ import { execFileSync as execFileSync28, spawn as spawn5 } from "child_process";
1277
1303
  import * as fs26 from "fs";
1278
1304
  import * as path23 from "path";
1279
1305
 
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
- }
1306
+ // src/issue.ts
1307
+ import { execFileSync as execFileSync3 } from "child_process";
1308
+ var API_TIMEOUT_MS = 3e4;
1309
+ function ghToken() {
1310
+ return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
1292
1311
  }
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");
1312
+ function gh(args, options) {
1313
+ const token = ghToken();
1314
+ const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
1315
+ return execFileSync3("gh", args, {
1316
+ encoding: "utf-8",
1317
+ timeout: API_TIMEOUT_MS,
1318
+ cwd: options?.cwd,
1319
+ env,
1320
+ input: options?.input,
1321
+ stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
1322
+ }).trim();
1306
1323
  }
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
- } };
1324
+ function getIssue(issueNumber, cwd) {
1325
+ const output = gh(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
1326
+ const parsed = JSON.parse(output);
1327
+ if (typeof parsed?.title !== "string") {
1328
+ throw new Error(`Issue #${issueNumber}: unexpected response shape`);
1312
1329
  }
1313
- let cmd = "litellm";
1330
+ return {
1331
+ number: parsed.number ?? issueNumber,
1332
+ title: parsed.title,
1333
+ body: parsed.body ?? "",
1334
+ comments: (parsed.comments ?? []).map((c) => ({
1335
+ body: c.body ?? "",
1336
+ author: c.author?.login ?? "unknown",
1337
+ createdAt: c.createdAt ?? ""
1338
+ })),
1339
+ labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
1340
+ };
1341
+ }
1342
+ function stripKodyMentions(body) {
1343
+ return body.replace(/(@)(kody)/gi, "$1\u200B$2");
1344
+ }
1345
+ function postIssueComment(issueNumber, body, cwd) {
1314
1346
  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
- }
1347
+ gh(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
1348
+ } catch (err) {
1349
+ process.stderr.write(
1350
+ `[kody] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
1351
+ `
1352
+ );
1323
1353
  }
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 })
1354
+ }
1355
+ function truncate2(s, maxBytes) {
1356
+ if (s.length <= maxBytes) return s;
1357
+ return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
1358
+ }
1359
+ function parsePrNumber(url) {
1360
+ const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
1361
+ if (!m) return null;
1362
+ const n = parseInt(m[1], 10);
1363
+ return Number.isFinite(n) ? n : null;
1364
+ }
1365
+ function getPr(prNumber, cwd) {
1366
+ const output = gh(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
1367
+ cwd
1336
1368
  });
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
- }
1369
+ const parsed = JSON.parse(output);
1370
+ if (typeof parsed?.title !== "string") {
1371
+ throw new Error(`PR #${prNumber}: unexpected response shape`);
1351
1372
  }
1352
- let logTail = "";
1373
+ return {
1374
+ number: parsed.number ?? prNumber,
1375
+ title: parsed.title,
1376
+ body: parsed.body ?? "",
1377
+ headRefName: String(parsed.headRefName ?? ""),
1378
+ baseRefName: String(parsed.baseRefName ?? ""),
1379
+ state: String(parsed.state ?? "")
1380
+ };
1381
+ }
1382
+ function getPrDiff(prNumber, cwd) {
1353
1383
  try {
1354
- logTail = fs8.readFileSync(logPath, "utf-8").slice(-2e3);
1355
- } catch {
1384
+ return gh(["pr", "diff", String(prNumber)], { cwd });
1385
+ } catch (err) {
1386
+ process.stderr.write(
1387
+ `[kody] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
1388
+ `
1389
+ );
1390
+ return "";
1356
1391
  }
1392
+ }
1393
+ function getPrReviews(prNumber, cwd) {
1357
1394
  try {
1358
- child.kill();
1395
+ const output = gh(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
1396
+ const parsed = JSON.parse(output);
1397
+ if (!Array.isArray(parsed?.reviews)) return [];
1398
+ return parsed.reviews.map(
1399
+ (r) => ({
1400
+ body: r.body ?? "",
1401
+ state: r.state ?? "",
1402
+ author: r.author?.login ?? "unknown",
1403
+ submittedAt: r.submittedAt ?? ""
1404
+ })
1405
+ );
1359
1406
  } catch {
1407
+ return [];
1360
1408
  }
1361
- throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
1362
- ${logTail}`);
1363
1409
  }
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;
1410
+ function getPrComments(prNumber, cwd) {
1411
+ try {
1412
+ const output = gh(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
1413
+ const parsed = JSON.parse(output);
1414
+ if (!Array.isArray(parsed?.comments)) return [];
1415
+ return parsed.comments.map((c) => ({
1416
+ body: c.body ?? "",
1417
+ author: c.author?.login ?? "unknown",
1418
+ createdAt: c.createdAt ?? ""
1419
+ })).filter((c) => c.body.trim().length > 0);
1420
+ } catch {
1421
+ return [];
1380
1422
  }
1381
- return result;
1382
1423
  }
1383
- function stripBlockingEnv(env) {
1384
- const out = { ...env };
1385
- delete out.DATABASE_URL;
1386
- delete out.AI_BASE_URL;
1387
- return out;
1424
+ var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
1425
+ function isReviewShaped(body) {
1426
+ return VERDICT_HEADING.test(body);
1427
+ }
1428
+ function getPrLatestReviewBody(prNumber, cwd) {
1429
+ const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
1430
+ const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
1431
+ const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
1432
+ if (all.length > 0) return all[0].body;
1433
+ const pr = getPr(prNumber, cwd);
1434
+ return pr.body;
1435
+ }
1436
+ function postPrReviewComment(prNumber, body, cwd) {
1437
+ try {
1438
+ gh(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
1439
+ } catch (err) {
1440
+ process.stderr.write(
1441
+ `[kody] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
1442
+ `
1443
+ );
1444
+ }
1388
1445
  }
1389
1446
 
1390
1447
  // src/profile.ts
1391
- import * as fs9 from "fs";
1392
- import * as path8 from "path";
1448
+ import * as fs8 from "fs";
1449
+ import * as path7 from "path";
1393
1450
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
1394
1451
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
1395
1452
  var VALID_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "container", "watch", "utility"]);
@@ -1405,12 +1462,12 @@ var ProfileError = class extends Error {
1405
1462
  profilePath;
1406
1463
  };
1407
1464
  function loadProfile(profilePath) {
1408
- if (!fs9.existsSync(profilePath)) {
1465
+ if (!fs8.existsSync(profilePath)) {
1409
1466
  throw new ProfileError(profilePath, "file not found");
1410
1467
  }
1411
1468
  let raw;
1412
1469
  try {
1413
- raw = JSON.parse(fs9.readFileSync(profilePath, "utf-8"));
1470
+ raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
1414
1471
  } catch (err) {
1415
1472
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
1416
1473
  }
@@ -1449,7 +1506,7 @@ function loadProfile(profilePath) {
1449
1506
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
1450
1507
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
1451
1508
  children,
1452
- dir: path8.dirname(profilePath)
1509
+ dir: path7.dirname(profilePath)
1453
1510
  };
1454
1511
  return profile;
1455
1512
  }
@@ -1559,17 +1616,11 @@ function parseScripts(p, raw) {
1559
1616
  throw new ProfileError(p, `"scripts" must be an object with preflight and postflight arrays`);
1560
1617
  }
1561
1618
  const r = raw;
1562
- const preflight = parseScriptList(p, "preflight", r.preflight);
1563
- const postflight = parseScriptList(p, "postflight", r.postflight);
1564
1619
  return {
1565
- preflight,
1566
- postflight: pairLifecycleClears(preflight, postflight)
1620
+ preflight: parseScriptList(p, "preflight", r.preflight),
1621
+ postflight: parseScriptList(p, "postflight", r.postflight)
1567
1622
  };
1568
1623
  }
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
1624
  function parseInputArtifacts(p, raw) {
1574
1625
  if (raw === void 0 || raw === null) return [];
1575
1626
  if (typeof raw !== "object" || Array.isArray(raw)) {
@@ -1698,50 +1749,278 @@ function parseScriptList(p, key, raw) {
1698
1749
  return out;
1699
1750
  }
1700
1751
 
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 ?? "?";
1752
+ // src/lifecycleLabels.ts
1753
+ var KODY_NAMESPACE = "kody";
1754
+ function groupOf(label) {
1755
+ const idx = label.indexOf(":");
1756
+ return idx === -1 ? label : label.slice(0, idx + 1);
1757
+ }
1758
+ function collectProfileLabels() {
1759
+ const byLabel = /* @__PURE__ */ new Map();
1760
+ for (const exe of listExecutables()) {
1761
+ let profile;
1762
+ try {
1763
+ profile = loadProfile(exe.profilePath);
1764
+ } catch {
1765
+ continue;
1766
+ }
1767
+ for (const entry of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
1768
+ const spec = extractLabelSpec(entry);
1769
+ if (spec) byLabel.set(spec.label, spec);
1770
+ }
1771
+ }
1772
+ return [...byLabel.values()];
1773
+ }
1774
+ function extractLabelSpec(entry) {
1775
+ if (entry.script !== "setLifecycleLabel") return null;
1776
+ const w = entry.with;
1777
+ if (!w) return null;
1778
+ const label = typeof w.label === "string" ? w.label : null;
1779
+ if (!label || !label.startsWith(KODY_NAMESPACE)) return null;
1780
+ return {
1781
+ label,
1782
+ color: typeof w.color === "string" ? w.color : void 0,
1783
+ description: typeof w.description === "string" ? w.description : void 0
1784
+ };
1785
+ }
1786
+ function ensureLabels(cwd) {
1787
+ const result = { created: [], failed: [] };
1788
+ for (const spec of collectProfileLabels()) {
1789
+ try {
1790
+ createLabelInRepo(spec, cwd);
1791
+ result.created.push(spec.label);
1792
+ } catch (err) {
1793
+ result.failed.push({ label: spec.label, reason: errMsg(err) });
1794
+ }
1795
+ }
1796
+ return result;
1797
+ }
1798
+ function getIssueLabels(issueNumber, cwd) {
1799
+ try {
1800
+ const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
1801
+ return output.split("\n").filter(Boolean);
1802
+ } catch {
1803
+ return [];
1804
+ }
1805
+ }
1806
+ function addLabel(issueNumber, label, cwd) {
1807
+ gh(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
1808
+ }
1809
+ function removeLabel(issueNumber, label, cwd) {
1810
+ try {
1811
+ gh(["issue", "edit", String(issueNumber), "--remove-label", label], { cwd });
1812
+ } catch {
1813
+ }
1814
+ }
1815
+ function createLabelInRepo(spec, cwd) {
1816
+ const args = ["label", "create", spec.label, "--force"];
1817
+ if (spec.color) args.push("--color", spec.color);
1818
+ if (spec.description) args.push("--description", spec.description);
1819
+ gh(args, { cwd });
1820
+ }
1821
+ function setKodyLabel(issueNumber, spec, cwd) {
1822
+ const target = spec.label;
1823
+ if (!target.startsWith(KODY_NAMESPACE)) {
1824
+ process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
1825
+ `);
1826
+ return;
1827
+ }
1828
+ const targetGroup = groupOf(target);
1829
+ const present = getIssueLabels(issueNumber, cwd);
1830
+ for (const label of present) {
1831
+ if (label !== target && label.startsWith(KODY_NAMESPACE) && groupOf(label) === targetGroup) {
1832
+ removeLabel(issueNumber, label, cwd);
1833
+ }
1834
+ }
1835
+ try {
1836
+ addLabel(issueNumber, target, cwd);
1837
+ } catch (err) {
1838
+ if (looksLikeMissingLabel(err)) {
1839
+ try {
1840
+ createLabelInRepo(spec, cwd);
1841
+ addLabel(issueNumber, target, cwd);
1842
+ return;
1843
+ } catch (retryErr) {
1844
+ process.stderr.write(
1845
+ `[kody] setKodyLabel: create+retry failed for ${target} on #${issueNumber}: ${errMsg(retryErr)}
1846
+ `
1847
+ );
1848
+ return;
1849
+ }
1850
+ }
1851
+ process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
1852
+ `);
1853
+ }
1854
+ }
1855
+ function looksLikeMissingLabel(err) {
1856
+ const msg = errMsg(err).toLowerCase();
1857
+ return msg.includes("not found") || msg.includes("could not add label") || msg.includes("could not resolve to a label");
1858
+ }
1859
+ function errMsg(err) {
1860
+ if (err instanceof Error) return err.message;
1861
+ if (typeof err === "object" && err !== null) {
1862
+ const e = err;
1863
+ const stderr = e.stderr?.toString().trim();
1864
+ if (stderr) return stderr;
1865
+ if (e.message) return e.message;
1866
+ }
1867
+ return String(err);
1868
+ }
1869
+
1870
+ // src/litellm.ts
1871
+ import { execFileSync as execFileSync4, spawn } from "child_process";
1872
+ import * as fs9 from "fs";
1873
+ import * as os from "os";
1874
+ import * as path8 from "path";
1875
+ async function checkLitellmHealth(url) {
1876
+ try {
1877
+ const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
1878
+ return response.ok;
1879
+ } catch {
1880
+ return false;
1881
+ }
1882
+ }
1883
+ function generateLitellmConfigYaml(model) {
1884
+ const apiKeyVar = providerApiKeyEnvVar(model.provider);
1885
+ return [
1886
+ "model_list:",
1887
+ ` - model_name: ${model.model}`,
1888
+ ` litellm_params:`,
1889
+ ` model: ${model.provider}/${model.model}`,
1890
+ ` api_key: os.environ/${apiKeyVar}`,
1891
+ "",
1892
+ "litellm_settings:",
1893
+ " drop_params: true",
1894
+ ""
1895
+ ].join("\n");
1896
+ }
1897
+ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL) {
1898
+ if (!needsLitellmProxy(model)) return null;
1899
+ if (await checkLitellmHealth(url)) {
1900
+ return { url, kill: () => {
1901
+ } };
1902
+ }
1903
+ let cmd = "litellm";
1904
+ try {
1905
+ execFileSync4("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1906
+ } catch {
1907
+ try {
1908
+ execFileSync4("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
1909
+ cmd = "python3";
1910
+ } catch {
1911
+ throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
1912
+ }
1913
+ }
1914
+ const configPath = path8.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
1915
+ fs9.writeFileSync(configPath, generateLitellmConfigYaml(model));
1916
+ const portMatch = url.match(/:(\d+)/);
1917
+ const port = portMatch ? portMatch[1] : "4000";
1918
+ const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
1919
+ const dotenvVars = readDotenvApiKeys(projectDir);
1920
+ const logPath = path8.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
1921
+ const outFd = fs9.openSync(logPath, "w");
1922
+ const child = spawn(cmd, args, {
1923
+ stdio: ["ignore", outFd, outFd],
1924
+ detached: true,
1925
+ env: stripBlockingEnv({ ...process.env, ...dotenvVars })
1926
+ });
1927
+ fs9.closeSync(outFd);
1928
+ for (let i = 0; i < 30; i++) {
1929
+ await new Promise((r) => setTimeout(r, 2e3));
1930
+ if (await checkLitellmHealth(url)) {
1931
+ return {
1932
+ url,
1933
+ kill: () => {
1934
+ try {
1935
+ child.kill();
1936
+ } catch {
1937
+ }
1938
+ }
1939
+ };
1940
+ }
1941
+ }
1942
+ let logTail = "";
1943
+ try {
1944
+ logTail = fs9.readFileSync(logPath, "utf-8").slice(-2e3);
1945
+ } catch {
1946
+ }
1947
+ try {
1948
+ child.kill();
1949
+ } catch {
1950
+ }
1951
+ throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
1952
+ ${logTail}`);
1953
+ }
1954
+ function readDotenvApiKeys(projectDir) {
1955
+ const dotenvPath = path8.join(projectDir, ".env");
1956
+ if (!fs9.existsSync(dotenvPath)) return {};
1957
+ const result = {};
1958
+ for (const rawLine of fs9.readFileSync(dotenvPath, "utf-8").split("\n")) {
1959
+ const line = rawLine.trim();
1960
+ if (!line || line.startsWith("#")) continue;
1961
+ const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
1962
+ if (!match) continue;
1963
+ let value = match[2].trim();
1964
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1965
+ value = value.slice(1, -1);
1966
+ }
1967
+ const commentIdx = value.indexOf(" #");
1968
+ if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
1969
+ if (value) result[match[1]] = value;
1970
+ }
1971
+ return result;
1972
+ }
1973
+ function stripBlockingEnv(env) {
1974
+ const out = { ...env };
1975
+ delete out.DATABASE_URL;
1976
+ delete out.AI_BASE_URL;
1977
+ return out;
1978
+ }
1979
+
1980
+ // src/commit.ts
1981
+ import { execFileSync as execFileSync5 } from "child_process";
1982
+ import * as fs10 from "fs";
1983
+ import * as path9 from "path";
1984
+ var FORBIDDEN_PATH_PREFIXES = [
1985
+ ".kody/",
1986
+ ".kody-engine/",
1987
+ ".kody/",
1988
+ ".kody-lean/",
1989
+ // back-compat: stale runtime dir from kody-lean v0.5.x
1990
+ "node_modules/",
1991
+ "dist/",
1992
+ "build/"
1993
+ ];
1994
+ var ALLOWED_PATH_PREFIXES = [".kody/memory/"];
1995
+ var FORBIDDEN_PATH_EXACT = /* @__PURE__ */ new Set([".env", ".kody-pip-requirements.txt"]);
1996
+ var FORBIDDEN_PATH_SUFFIXES = [".log"];
1997
+ var CONVENTIONAL_PREFIXES = [
1998
+ "feat:",
1999
+ "fix:",
2000
+ "chore:",
2001
+ "docs:",
2002
+ "refactor:",
2003
+ "test:",
2004
+ "perf:",
2005
+ "ci:",
2006
+ "style:",
2007
+ "build:",
2008
+ "revert:"
2009
+ ];
2010
+ function git(args, cwd) {
2011
+ try {
2012
+ return execFileSync5("git", args, {
2013
+ encoding: "utf-8",
2014
+ timeout: 12e4,
2015
+ cwd,
2016
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2017
+ stdio: ["pipe", "pipe", "pipe"]
2018
+ }).trim();
2019
+ } catch (err) {
2020
+ const e = err;
2021
+ const stderr = e.stderr?.toString().trim() ?? "";
2022
+ const stdout = e.stdout?.toString().trim() ?? "";
2023
+ const status = e.status ?? "?";
1745
2024
  const detail = stderr || stdout || e.message || "(no output)";
1746
2025
  throw new Error(`git ${args.join(" ")} (exit ${status}):
1747
2026
  ${detail}`);
@@ -1789,7 +2068,7 @@ function isForbiddenPath(p) {
1789
2068
  return false;
1790
2069
  }
1791
2070
  function listChangedFiles(cwd) {
1792
- const raw = execFileSync4("git", ["status", "--porcelain=v1", "-z"], {
2071
+ const raw = execFileSync5("git", ["status", "--porcelain=v1", "-z"], {
1793
2072
  encoding: "utf-8",
1794
2073
  cwd,
1795
2074
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -1801,7 +2080,7 @@ function listChangedFiles(cwd) {
1801
2080
  }
1802
2081
  function listFilesInCommit(ref = "HEAD", cwd) {
1803
2082
  try {
1804
- const raw = execFileSync4("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
2083
+ const raw = execFileSync5("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
1805
2084
  encoding: "utf-8",
1806
2085
  cwd,
1807
2086
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -1890,14 +2169,14 @@ var abortUnfinishedGitOps2 = async (ctx) => {
1890
2169
  };
1891
2170
 
1892
2171
  // src/scripts/advanceFlow.ts
1893
- import { execFileSync as execFileSync6 } from "child_process";
2172
+ import { execFileSync as execFileSync7 } from "child_process";
1894
2173
 
1895
2174
  // src/state.ts
1896
- import { execFileSync as execFileSync5 } from "child_process";
2175
+ import { execFileSync as execFileSync6 } from "child_process";
1897
2176
  var STATE_BEGIN = "<!-- kody:state:v1:begin -->";
1898
2177
  var STATE_END = "<!-- kody:state:v1:end -->";
1899
2178
  var HISTORY_MAX_ENTRIES = 20;
1900
- var API_TIMEOUT_MS = 3e4;
2179
+ var API_TIMEOUT_MS2 = 3e4;
1901
2180
  function emptyState() {
1902
2181
  return {
1903
2182
  schemaVersion: 1,
@@ -1913,15 +2192,15 @@ function emptyState() {
1913
2192
  history: []
1914
2193
  };
1915
2194
  }
1916
- function ghToken() {
2195
+ function ghToken2() {
1917
2196
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
1918
2197
  }
1919
- function gh(args, input, cwd) {
1920
- const token = ghToken();
2198
+ function gh2(args, input, cwd) {
2199
+ const token = ghToken2();
1921
2200
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
1922
- return execFileSync5("gh", args, {
2201
+ return execFileSync6("gh", args, {
1923
2202
  encoding: "utf-8",
1924
- timeout: API_TIMEOUT_MS,
2203
+ timeout: API_TIMEOUT_MS2,
1925
2204
  cwd,
1926
2205
  env,
1927
2206
  input,
@@ -1931,7 +2210,7 @@ function gh(args, input, cwd) {
1931
2210
  function findStateComment(target, number, cwd) {
1932
2211
  const apiPath = target === "issue" ? `repos/{owner}/{repo}/issues/${number}/comments` : `repos/{owner}/{repo}/issues/${number}/comments`;
1933
2212
  try {
1934
- const raw = gh(["api", "--paginate", apiPath], void 0, cwd);
2213
+ const raw = gh2(["api", "--paginate", apiPath], void 0, cwd);
1935
2214
  const list = JSON.parse(raw);
1936
2215
  for (const c of list) {
1937
2216
  if (c.body?.includes(STATE_BEGIN)) {
@@ -2084,10 +2363,10 @@ function writeTaskState(target, number, state, cwd) {
2084
2363
  const existing = findStateComment(target, number, cwd);
2085
2364
  try {
2086
2365
  if (existing) {
2087
- gh(["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"], body, cwd);
2366
+ gh2(["api", `repos/{owner}/{repo}/issues/comments/${existing.id}`, "-X", "PATCH", "-F", "body=@-"], body, cwd);
2088
2367
  } else {
2089
2368
  const sub = target === "issue" ? "issue" : "pr";
2090
- gh([sub, "comment", String(number), "--body-file", "-"], body, cwd);
2369
+ gh2([sub, "comment", String(number), "--body-file", "-"], body, cwd);
2091
2370
  }
2092
2371
  } catch (err) {
2093
2372
  process.stderr.write(
@@ -2098,7 +2377,7 @@ function writeTaskState(target, number, state, cwd) {
2098
2377
  }
2099
2378
 
2100
2379
  // src/scripts/advanceFlow.ts
2101
- var API_TIMEOUT_MS2 = 3e4;
2380
+ var API_TIMEOUT_MS3 = 3e4;
2102
2381
  var advanceFlow = async (ctx, profile) => {
2103
2382
  const state = ctx.data.taskState;
2104
2383
  const flow = state?.flow;
@@ -2122,8 +2401,8 @@ var advanceFlow = async (ctx, profile) => {
2122
2401
  }
2123
2402
  const body = `@kody ${flow.name}`;
2124
2403
  try {
2125
- execFileSync6("gh", ["issue", "comment", String(flow.issueNumber), "--body", body], {
2126
- timeout: API_TIMEOUT_MS2,
2404
+ execFileSync7("gh", ["issue", "comment", String(flow.issueNumber), "--body", body], {
2405
+ timeout: API_TIMEOUT_MS3,
2127
2406
  cwd: ctx.cwd,
2128
2407
  stdio: ["ignore", "pipe", "pipe"]
2129
2408
  });
@@ -2231,7 +2510,7 @@ function copyDir(src, dst) {
2231
2510
  }
2232
2511
 
2233
2512
  // src/coverage.ts
2234
- import { execFileSync as execFileSync7 } from "child_process";
2513
+ import { execFileSync as execFileSync8 } from "child_process";
2235
2514
  function patternToRegex(pattern) {
2236
2515
  let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2237
2516
  s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
@@ -2249,7 +2528,7 @@ function renderSiblingPath(file, requireSibling) {
2249
2528
  }
2250
2529
  function safeGit(args, cwd) {
2251
2530
  try {
2252
- return execFileSync7("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
2531
+ return execFileSync8("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
2253
2532
  } catch {
2254
2533
  return "";
2255
2534
  }
@@ -2293,450 +2572,194 @@ function formatMissesForFeedback(misses) {
2293
2572
 
2294
2573
  // src/prompt.ts
2295
2574
  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;
2575
+ import * as path11 from "path";
2576
+ var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
2577
+ var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
2578
+ function loadProjectConventions(projectDir) {
2579
+ const out = [];
2580
+ for (const rel of CONVENTION_FILES) {
2581
+ const abs = path11.join(projectDir, rel);
2582
+ if (!fs12.existsSync(abs)) continue;
2583
+ let content;
2624
2584
  try {
2625
- profile = loadProfile(exe.profilePath);
2585
+ content = fs12.readFileSync(abs, "utf-8");
2626
2586
  } catch {
2627
2587
  continue;
2628
2588
  }
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
- }
2589
+ const truncated = content.length > CONVENTIONS_PER_FILE_MAX_BYTES;
2590
+ if (truncated) content = `${content.slice(0, CONVENTIONS_PER_FILE_MAX_BYTES)}
2591
+
2592
+ \u2026 (truncated)`;
2593
+ out.push({ path: rel, content, truncated });
2633
2594
  }
2634
- return [...byLabel.values()];
2595
+ return out;
2635
2596
  }
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;
2597
+ function parseAgentResult(finalText) {
2598
+ const text = (finalText || "").trim();
2599
+ if (!text)
2600
+ return {
2601
+ done: false,
2602
+ commitMessage: "",
2603
+ prSummary: "",
2604
+ feedbackActions: "",
2605
+ planDeviations: "",
2606
+ priorArt: "",
2607
+ failureReason: "agent produced no final message",
2608
+ markerMissing: false
2609
+ };
2610
+ const MARKDOWN_PREFIX = "[\\s>*_#`~\\-]*";
2611
+ const FAILED_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}FAILED${MARKDOWN_PREFIX}\\s*:\\s*(.+?)\\s*$`, "is");
2612
+ const DONE_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}DONE\\b`, "i");
2613
+ const failedMatch = text.match(FAILED_RE);
2614
+ if (failedMatch) {
2615
+ return {
2616
+ done: false,
2617
+ commitMessage: "",
2618
+ prSummary: "",
2619
+ feedbackActions: "",
2620
+ planDeviations: "",
2621
+ priorArt: "",
2622
+ failureReason: stripMarkdownEmphasis(failedMatch[1]),
2623
+ markerMissing: false
2624
+ };
2625
+ }
2626
+ const hasDoneMarker = DONE_RE.test(text);
2627
+ const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text);
2628
+ const hasPrSummary = /^[\s>*_#`~-]*PR_SUMMARY\s*:/im.test(text);
2629
+ if (!hasDoneMarker && !hasCommitMsg && !hasPrSummary) {
2630
+ const tail = text.length > 400 ? `\u2026${text.slice(-400)}` : text;
2631
+ return {
2632
+ done: false,
2633
+ commitMessage: "",
2634
+ prSummary: "",
2635
+ feedbackActions: "",
2636
+ planDeviations: "",
2637
+ priorArt: "",
2638
+ failureReason: `no DONE or FAILED marker in agent output \u2014 agent tail: ${tail}`,
2639
+ markerMissing: true
2640
+ };
2641
+ }
2642
+ const commitMatch = text.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
2643
+ const commitMessage = commitMatch ? stripMarkdownEmphasis(commitMatch[1]) : "";
2644
+ const feedbackActions = extractBlock(
2645
+ text,
2646
+ /(?:^|\n)[ \t]*FEEDBACK_ACTIONS\s*:[ \t]*\n/i,
2647
+ /(?:^|\n)[ \t]*(?:PLAN_DEVIATIONS|COMMIT_MSG|PR_SUMMARY|PRIOR_ART)\s*:/i
2648
+ );
2649
+ let planDeviations = extractBlock(
2650
+ text,
2651
+ /(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*\n/i,
2652
+ /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|FEEDBACK_ACTIONS|PRIOR_ART)\s*:/i
2653
+ );
2654
+ if (!planDeviations) {
2655
+ const inline = text.match(/(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2656
+ if (inline) planDeviations = inline[1].trim();
2657
+ }
2658
+ let priorArt = "";
2659
+ const priorArtInline = text.match(/(?:^|\n)[ \t]*PRIOR_ART\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2660
+ if (priorArtInline) priorArt = priorArtInline[1].trim();
2661
+ const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
2662
+ let prSummary = "";
2663
+ if (summaryStart !== -1) {
2664
+ const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
2665
+ prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
2666
+ }
2642
2667
  return {
2643
- label,
2644
- color: typeof w.color === "string" ? w.color : void 0,
2645
- description: typeof w.description === "string" ? w.description : void 0
2668
+ done: true,
2669
+ commitMessage,
2670
+ prSummary,
2671
+ feedbackActions,
2672
+ planDeviations,
2673
+ priorArt,
2674
+ failureReason: "",
2675
+ markerMissing: false
2646
2676
  };
2647
2677
  }
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
- }
2657
- }
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 [];
2666
- }
2678
+ function stripMarkdownEmphasis(s) {
2679
+ return s.trim().replace(/^[*_`~]+|[*_`~]+$/g, "").trim();
2667
2680
  }
2668
- function addLabel(issueNumber, label, cwd) {
2669
- gh2(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
2681
+ function extractBlock(text, startMarker, endMarker) {
2682
+ const startIdx = text.search(startMarker);
2683
+ if (startIdx === -1) return "";
2684
+ const afterStart = text.slice(startIdx).replace(startMarker, "");
2685
+ const endIdx = afterStart.search(endMarker);
2686
+ const body = endIdx === -1 ? afterStart : afterStart.slice(0, endIdx);
2687
+ return body.replace(/\n\s*```\s*$/g, "").trim();
2670
2688
  }
2671
- function removeLabel(issueNumber, label, cwd) {
2672
- try {
2673
- gh2(["issue", "edit", String(issueNumber), "--remove-label", label], { cwd });
2674
- } catch {
2689
+
2690
+ // src/scripts/checkCoverageWithRetry.ts
2691
+ var checkCoverageWithRetry = async (ctx) => {
2692
+ const reqs = ctx.data.coverageRules ?? [];
2693
+ if (reqs.length === 0) {
2694
+ ctx.data.coverageMisses = [];
2695
+ return;
2675
2696
  }
2676
- }
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 });
2682
- }
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
- `);
2697
+ if (!ctx.data.agentDone) {
2698
+ ctx.data.coverageMisses = [];
2688
2699
  return;
2689
2700
  }
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
- }
2701
+ const misses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
2702
+ if (misses.length === 0) {
2703
+ ctx.data.coverageMisses = [];
2704
+ return;
2696
2705
  }
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
- `);
2706
+ const invoker = ctx.data.__invokeAgent;
2707
+ const basePrompt = ctx.data.prompt;
2708
+ if (!invoker || !basePrompt) {
2709
+ ctx.data.coverageMisses = misses;
2710
+ return;
2715
2711
  }
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;
2712
+ process.stderr.write(`[kody] coverage check found ${misses.length} missing test(s); retrying agent once
2713
+ `);
2714
+ const retryPrompt = `${basePrompt}
2715
+
2716
+ # Coverage failure (retry)
2717
+ ${formatMissesForFeedback(misses)}`;
2718
+ const retry = await invoker(retryPrompt);
2719
+ const retryParsed = parseAgentResult(retry.finalText);
2720
+ if (retry.outcome === "completed" && retryParsed.done) {
2721
+ ctx.data.agentDone = true;
2722
+ ctx.data.commitMessage = retryParsed.commitMessage || ctx.data.commitMessage;
2723
+ ctx.data.prSummary = retryParsed.prSummary || ctx.data.prSummary;
2728
2724
  }
2729
- return String(err);
2730
- }
2725
+ const finalMisses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
2726
+ ctx.data.coverageMisses = finalMisses;
2727
+ };
2731
2728
 
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);
2729
+ // src/scripts/classifyByLabel.ts
2730
+ var VALID_CLASSES = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
2731
+ var classifyByLabel = async (ctx) => {
2732
+ const issue = ctx.data.issue;
2733
+ const labels = issue?.labels;
2734
+ if (!labels || labels.length === 0) return;
2735
+ const cfgMap = ctx.config.classify?.labelMap;
2736
+ const map = cfgMap ?? defaultLabelMap();
2737
+ for (const label of labels) {
2738
+ const candidate = map[label.toLowerCase()];
2739
+ if (candidate && VALID_CLASSES.has(candidate)) {
2740
+ ctx.data.classification = candidate;
2741
+ ctx.data.classificationSource = "label";
2742
+ ctx.data.classificationReason = `label \`${label}\` \u2192 ${candidate}`;
2743
+ ctx.skipAgent = true;
2744
+ return;
2745
+ }
2746
+ }
2739
2747
  };
2748
+ function defaultLabelMap() {
2749
+ return {
2750
+ bug: "bug",
2751
+ enhancement: "bug",
2752
+ refactor: "feature",
2753
+ feature: "feature",
2754
+ performance: "feature",
2755
+ rfc: "spec",
2756
+ design: "spec",
2757
+ spec: "spec",
2758
+ docs: "chore",
2759
+ chore: "chore",
2760
+ dependencies: "chore"
2761
+ };
2762
+ }
2740
2763
 
2741
2764
  // src/scripts/commitAndPush.ts
2742
2765
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
@@ -2746,11 +2769,15 @@ var commitAndPush2 = async (ctx) => {
2746
2769
  ctx.data.commitResult = { committed: false, pushed: false };
2747
2770
  return;
2748
2771
  }
2749
- if (ctx.data.agentDone === false) {
2772
+ const markerMissing = ctx.data.agentMarkerMissing === true;
2773
+ if (ctx.data.agentDone === false && !markerMissing) {
2750
2774
  ctx.data.commitResult = { committed: false, pushed: false, skippedReason: "agentDone=false" };
2751
2775
  ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
2752
2776
  return;
2753
2777
  }
2778
+ if (ctx.data.agentDone === false && markerMissing) {
2779
+ ctx.data.salvagedFromMissingMarker = true;
2780
+ }
2754
2781
  const message = ctx.data.commitMessage || DEFAULT_COMMIT_MESSAGE;
2755
2782
  try {
2756
2783
  const result = commitAndPush(branch, message, ctx.cwd);
@@ -3009,7 +3036,7 @@ function splitReport(text) {
3009
3036
  function loadManifest(cwd) {
3010
3037
  let issuesJson;
3011
3038
  try {
3012
- issuesJson = gh2(
3039
+ issuesJson = gh(
3013
3040
  ["issue", "list", "--label", MANIFEST_LABEL, "--state", "all", "--limit", "1", "--json", "number,body"],
3014
3041
  { cwd }
3015
3042
  );
@@ -3062,7 +3089,7 @@ ${MANIFEST_END}
3062
3089
  }
3063
3090
  function ensureLabel(name, color, description, cwd) {
3064
3091
  try {
3065
- gh2(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
3092
+ gh(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
3066
3093
  } catch {
3067
3094
  }
3068
3095
  }
@@ -3117,10 +3144,10 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
3117
3144
  ensureLabel(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
3118
3145
  const body = serializeManifestBody(manifest);
3119
3146
  if (number !== null) {
3120
- gh2(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
3147
+ gh(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
3121
3148
  return { number, created: false };
3122
3149
  }
3123
- const out = gh2(["issue", "create", "--title", MANIFEST_TITLE, "--label", MANIFEST_LABEL, "--body-file", "-"], {
3150
+ const out = gh(["issue", "create", "--title", MANIFEST_TITLE, "--label", MANIFEST_LABEL, "--body-file", "-"], {
3124
3151
  input: body,
3125
3152
  cwd
3126
3153
  });
@@ -3237,7 +3264,7 @@ function createTaskIssue(finding, goalId, manifestNumber, cwd) {
3237
3264
  for (const l of labels) {
3238
3265
  args.push("--label", l);
3239
3266
  }
3240
- const out = gh2(args, { input: body, cwd });
3267
+ const out = gh(args, { input: body, cwd });
3241
3268
  const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
3242
3269
  const m = url.match(/\/issues\/(\d+)\b/);
3243
3270
  if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
@@ -3294,7 +3321,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
3294
3321
  const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
3295
3322
  let url = "";
3296
3323
  try {
3297
- const out = gh2(
3324
+ const out = gh(
3298
3325
  ["issue", "create", "--title", title, "--label", FINDING_LABEL, "--body-file", "-"],
3299
3326
  { input: finalText, cwd: ctx.cwd }
3300
3327
  );
@@ -4142,7 +4169,7 @@ function parseStateCommentBody(marker, body) {
4142
4169
  return isStateEnvelope(parsed) ? parsed : null;
4143
4170
  }
4144
4171
  function listIssueComments(owner, repo, issueNumber, cwd) {
4145
- const raw = gh2(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
4172
+ const raw = gh(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
4146
4173
  let parsed;
4147
4174
  try {
4148
4175
  parsed = JSON.parse(raw);
@@ -4163,7 +4190,7 @@ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
4163
4190
  }
4164
4191
  function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
4165
4192
  const body = formatStateCommentBody(marker, state);
4166
- const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
4193
+ const raw = gh(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
4167
4194
  cwd,
4168
4195
  input: JSON.stringify({ body })
4169
4196
  });
@@ -4176,7 +4203,7 @@ function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
4176
4203
  }
4177
4204
  function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
4178
4205
  const body = formatStateCommentBody(marker, state);
4179
- gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
4206
+ gh(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
4180
4207
  cwd,
4181
4208
  input: JSON.stringify({ body })
4182
4209
  });
@@ -4187,7 +4214,7 @@ function updateStateComment(owner, repo, commentId, commentNodeId, marker, state
4187
4214
  }
4188
4215
  function minimizeComment(nodeId, cwd) {
4189
4216
  const mutation = "mutation($id: ID!) { minimizeComment(input: { classifier: OUTDATED, subjectId: $id }) { minimizedComment { isMinimized } } }";
4190
- gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
4217
+ gh(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
4191
4218
  }
4192
4219
 
4193
4220
  // src/scripts/jobState/backend.ts
@@ -4224,7 +4251,7 @@ var ContentsApiBackend = class {
4224
4251
  const filePath = stateFilePath(this.jobsDir, slug);
4225
4252
  let raw = "";
4226
4253
  try {
4227
- raw = gh2(["api", `/repos/${this.owner}/${this.repo}/contents/${filePath}`], { cwd: this.cwd });
4254
+ raw = gh(["api", `/repos/${this.owner}/${this.repo}/contents/${filePath}`], { cwd: this.cwd });
4228
4255
  } catch (err) {
4229
4256
  const msg = err instanceof Error ? err.message : String(err);
4230
4257
  if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
@@ -4268,7 +4295,7 @@ var ContentsApiBackend = class {
4268
4295
  content: Buffer.from(body, "utf-8").toString("base64")
4269
4296
  };
4270
4297
  if (typeof loaded.handle === "string") payload.sha = loaded.handle;
4271
- gh2(["api", "--method", "PUT", `/repos/${this.owner}/${this.repo}/contents/${loaded.path}`, "--input", "-"], {
4298
+ gh(["api", "--method", "PUT", `/repos/${this.owner}/${this.repo}/contents/${loaded.path}`, "--input", "-"], {
4272
4299
  cwd: this.cwd,
4273
4300
  input: JSON.stringify(payload)
4274
4301
  });
@@ -4614,7 +4641,7 @@ var dispatchJobTicks = async (ctx, _profile, args) => {
4614
4641
  function listIssuesByLabel(label, cwd) {
4615
4642
  let raw = "";
4616
4643
  try {
4617
- raw = gh2(["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"], {
4644
+ raw = gh(["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"], {
4618
4645
  cwd
4619
4646
  });
4620
4647
  } catch {
@@ -4698,7 +4725,7 @@ function firstLine(s) {
4698
4725
  }
4699
4726
  function findExistingPr(branch, cwd) {
4700
4727
  try {
4701
- const output = gh2(
4728
+ const output = gh(
4702
4729
  ["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,body", "--limit", "1"],
4703
4730
  { cwd }
4704
4731
  );
@@ -4736,7 +4763,7 @@ function ensurePr(opts) {
4736
4763
  const stripped = existing.url.replace(/^https:\/\/github\.com\//, "");
4737
4764
  const [owner, repo] = stripped.split("/");
4738
4765
  try {
4739
- gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/pulls/${existing.number}`, "-f", `body=${body}`], {
4766
+ gh(["api", "--method", "PATCH", `repos/${owner}/${repo}/pulls/${existing.number}`, "-f", `body=${body}`], {
4740
4767
  cwd: opts.cwd
4741
4768
  });
4742
4769
  } catch (err) {
@@ -4758,7 +4785,7 @@ function ensurePr(opts) {
4758
4785
  "-"
4759
4786
  ];
4760
4787
  if (opts.draft) args.push("--draft");
4761
- const output = gh2(args, { input: body, cwd: opts.cwd });
4788
+ const output = gh(args, { input: body, cwd: opts.cwd });
4762
4789
  const url = output.trim();
4763
4790
  const match = url.match(/\/pull\/(\d+)$/);
4764
4791
  const number = match ? parseInt(match[1], 10) : 0;
@@ -5475,6 +5502,21 @@ function performInit(cwd, force) {
5475
5502
  wrote.push(QA_GUIDE_REL_PATH);
5476
5503
  }
5477
5504
  }
5505
+ const builtinJobs = listBuiltinJobs();
5506
+ if (builtinJobs.length > 0) {
5507
+ const jobsDir = path20.join(cwd, ".kody", "jobs");
5508
+ fs22.mkdirSync(jobsDir, { recursive: true });
5509
+ for (const job of builtinJobs) {
5510
+ const rel = path20.join(".kody", "jobs", `${job.slug}.md`);
5511
+ const target = path20.join(cwd, rel);
5512
+ if (fs22.existsSync(target) && !force) {
5513
+ skipped.push(rel);
5514
+ continue;
5515
+ }
5516
+ fs22.writeFileSync(target, fs22.readFileSync(job.filePath, "utf-8"));
5517
+ wrote.push(rel);
5518
+ }
5519
+ }
5478
5520
  for (const exe of listExecutables()) {
5479
5521
  let profile;
5480
5522
  try {
@@ -5844,7 +5886,7 @@ function parsePrNumbers(raw) {
5844
5886
  }
5845
5887
  function fetchPrBlock(prNumber, cwd) {
5846
5888
  try {
5847
- const metaRaw = gh2(["pr", "view", String(prNumber), "--json", "title,state,url,mergedAt,closedAt"], { cwd });
5889
+ const metaRaw = gh(["pr", "view", String(prNumber), "--json", "title,state,url,mergedAt,closedAt"], { cwd });
5848
5890
  const meta = JSON.parse(metaRaw);
5849
5891
  const diff = truncate3(safeGh(["pr", "diff", String(prNumber)], cwd), PER_PR_DIFF_MAX_BYTES);
5850
5892
  const commentsRaw = safeGh(["pr", "view", String(prNumber), "--json", "comments,reviews"], cwd);
@@ -5871,7 +5913,7 @@ _Could not fetch \u2014 ${err instanceof Error ? err.message : String(err)}_`;
5871
5913
  }
5872
5914
  function safeGh(args, cwd) {
5873
5915
  try {
5874
- return gh2(args, { cwd });
5916
+ return gh(args, { cwd });
5875
5917
  } catch {
5876
5918
  return "";
5877
5919
  }
@@ -6059,7 +6101,7 @@ function buildIssueTitle(scope, verdict) {
6059
6101
  }
6060
6102
  function ensureLabel2(cwd) {
6061
6103
  try {
6062
- gh2(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
6104
+ gh(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
6063
6105
  return true;
6064
6106
  } catch {
6065
6107
  return false;
@@ -6068,7 +6110,7 @@ function ensureLabel2(cwd) {
6068
6110
  function createQaIssue(title, body, hasLabel, cwd) {
6069
6111
  const args = ["issue", "create", "--title", title, "--body-file", "-"];
6070
6112
  if (hasLabel) args.push("--label", QA_LABEL);
6071
- const out = gh2(args, { input: body, cwd });
6113
+ const out = gh(args, { input: body, cwd });
6072
6114
  const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
6073
6115
  const m = url.match(/\/issues\/(\d+)\b/);
6074
6116
  if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
@@ -6155,6 +6197,7 @@ var parseAgentResult2 = async (ctx, profile, agentResult) => {
6155
6197
  ctx.data.planDeviations = parsed.planDeviations;
6156
6198
  ctx.data.priorArt = parsed.priorArt;
6157
6199
  ctx.data.agentFailureReason = parsed.failureReason;
6200
+ ctx.data.agentMarkerMissing = parsed.markerMissing;
6158
6201
  ctx.data.agentOutcome = agentResult.outcome;
6159
6202
  ctx.data.agentError = agentResult.error;
6160
6203
  const modeSeg = (ctx.args.mode ?? profile.name).replace(/-/g, "_").toUpperCase();
@@ -6392,8 +6435,9 @@ var postIssueComment2 = async (ctx) => {
6392
6435
  const prUrl = ctx.output.prUrl;
6393
6436
  const prAction = ctx.data.prResult?.action;
6394
6437
  if (!commitResult?.committed && !hasCommits) {
6395
- const reason = "no changes to commit";
6396
- postWith(targetType, targetNumber, `\u26A0\uFE0F kody FAILED: ${reason}`, ctx.cwd);
6438
+ const specific = computeFailureReason2(ctx);
6439
+ const reason = specific.length > 0 ? specific : "no changes to commit";
6440
+ postWith(targetType, targetNumber, `\u26A0\uFE0F kody FAILED: ${truncate2(reason, 1500)}`, ctx.cwd);
6397
6441
  markRunFailed(ctx);
6398
6442
  ctx.output.exitCode = 3;
6399
6443
  ctx.output.reason = reason;
@@ -6842,7 +6886,7 @@ function latestSuccessUrl(deploymentId, cwd) {
6842
6886
  }
6843
6887
  function safeGh2(args, cwd) {
6844
6888
  try {
6845
- return gh2(args, { cwd });
6889
+ return gh(args, { cwd });
6846
6890
  } catch {
6847
6891
  return null;
6848
6892
  }
@@ -8026,8 +8070,7 @@ var postflightScripts = {
8026
8070
  recordOutcome,
8027
8071
  mergeReleasePr,
8028
8072
  waitForCi,
8029
- markFlowSuccess,
8030
- clearLifecycleLabel
8073
+ markFlowSuccess
8031
8074
  };
8032
8075
  var allScriptNames = /* @__PURE__ */ new Set([
8033
8076
  ...Object.keys(preflightScripts),
@@ -8225,12 +8268,26 @@ async function runExecutable(profileName, input) {
8225
8268
  reason: ctx.output.reason
8226
8269
  });
8227
8270
  } finally {
8271
+ clearStampedLifecycleLabels(profile, ctx);
8228
8272
  try {
8229
8273
  litellm?.kill();
8230
8274
  } catch {
8231
8275
  }
8232
8276
  }
8233
8277
  }
8278
+ function clearStampedLifecycleLabels(profile, ctx) {
8279
+ const target = ctx.args.issue ?? ctx.args.pr;
8280
+ if (typeof target !== "number" || !Number.isFinite(target)) return;
8281
+ for (const entry of profile.scripts.preflight) {
8282
+ if (entry.script !== "setLifecycleLabel") continue;
8283
+ const label = typeof entry.with?.label === "string" ? entry.with.label : void 0;
8284
+ if (!label || !label.startsWith(KODY_NAMESPACE)) continue;
8285
+ try {
8286
+ removeLabel(target, label, ctx.cwd);
8287
+ } catch {
8288
+ }
8289
+ }
8290
+ }
8234
8291
  function resolveProfilePath(profileName) {
8235
8292
  const found = resolveExecutable(profileName);
8236
8293
  if (found) return found;