@kody-ade/kody-engine 0.4.21 → 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.
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.21",
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",
@@ -1002,32 +1002,6 @@ 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
- }
1031
1005
  function getExecutableRoots() {
1032
1006
  return [getProjectExecutablesRoot(), getExecutablesRoot()];
1033
1007
  }
@@ -1303,119 +1277,150 @@ import { execFileSync as execFileSync28, spawn as spawn5 } from "child_process";
1303
1277
  import * as fs26 from "fs";
1304
1278
  import * as path23 from "path";
1305
1279
 
1306
- // src/litellm.ts
1307
- import { execFileSync as execFileSync3, spawn } from "child_process";
1308
- import * as fs8 from "fs";
1309
- import * as os from "os";
1310
- import * as path7 from "path";
1311
- async function checkLitellmHealth(url) {
1312
- try {
1313
- const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
1314
- return response.ok;
1315
- } catch {
1316
- return false;
1317
- }
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;
1318
1285
  }
1319
- function generateLitellmConfigYaml(model) {
1320
- const apiKeyVar = providerApiKeyEnvVar(model.provider);
1321
- return [
1322
- "model_list:",
1323
- ` - model_name: ${model.model}`,
1324
- ` litellm_params:`,
1325
- ` model: ${model.provider}/${model.model}`,
1326
- ` api_key: os.environ/${apiKeyVar}`,
1327
- "",
1328
- "litellm_settings:",
1329
- " drop_params: true",
1330
- ""
1331
- ].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();
1332
1297
  }
1333
- async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL) {
1334
- if (!needsLitellmProxy(model)) return null;
1335
- if (await checkLitellmHealth(url)) {
1336
- return { url, kill: () => {
1337
- } };
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`);
1338
1303
  }
1339
- 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) {
1340
1320
  try {
1341
- execFileSync3("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1342
- } catch {
1343
- try {
1344
- execFileSync3("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
1345
- cmd = "python3";
1346
- } catch {
1347
- throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
1348
- }
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
+ );
1349
1327
  }
1350
- const configPath = path7.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
1351
- fs8.writeFileSync(configPath, generateLitellmConfigYaml(model));
1352
- const portMatch = url.match(/:(\d+)/);
1353
- const port = portMatch ? portMatch[1] : "4000";
1354
- const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
1355
- const dotenvVars = readDotenvApiKeys(projectDir);
1356
- const logPath = path7.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
1357
- const outFd = fs8.openSync(logPath, "w");
1358
- const child = spawn(cmd, args, {
1359
- stdio: ["ignore", outFd, outFd],
1360
- detached: true,
1361
- 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
1362
1342
  });
1363
- fs8.closeSync(outFd);
1364
- for (let i = 0; i < 30; i++) {
1365
- await new Promise((r) => setTimeout(r, 2e3));
1366
- if (await checkLitellmHealth(url)) {
1367
- return {
1368
- url,
1369
- kill: () => {
1370
- try {
1371
- child.kill();
1372
- } catch {
1373
- }
1374
- }
1375
- };
1376
- }
1343
+ const parsed = JSON.parse(output);
1344
+ if (typeof parsed?.title !== "string") {
1345
+ throw new Error(`PR #${prNumber}: unexpected response shape`);
1377
1346
  }
1378
- 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) {
1379
1357
  try {
1380
- logTail = fs8.readFileSync(logPath, "utf-8").slice(-2e3);
1381
- } 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 "";
1382
1365
  }
1366
+ }
1367
+ function getPrReviews(prNumber, cwd) {
1383
1368
  try {
1384
- 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
+ );
1385
1380
  } catch {
1381
+ return [];
1386
1382
  }
1387
- throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
1388
- ${logTail}`);
1389
1383
  }
1390
- function readDotenvApiKeys(projectDir) {
1391
- const dotenvPath = path7.join(projectDir, ".env");
1392
- if (!fs8.existsSync(dotenvPath)) return {};
1393
- const result = {};
1394
- for (const rawLine of fs8.readFileSync(dotenvPath, "utf-8").split("\n")) {
1395
- const line = rawLine.trim();
1396
- if (!line || line.startsWith("#")) continue;
1397
- const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
1398
- if (!match) continue;
1399
- let value = match[2].trim();
1400
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1401
- value = value.slice(1, -1);
1402
- }
1403
- const commentIdx = value.indexOf(" #");
1404
- if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
1405
- 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 [];
1406
1396
  }
1407
- return result;
1408
1397
  }
1409
- function stripBlockingEnv(env) {
1410
- const out = { ...env };
1411
- delete out.DATABASE_URL;
1412
- delete out.AI_BASE_URL;
1413
- 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
+ }
1414
1419
  }
1415
1420
 
1416
1421
  // src/profile.ts
1417
- import * as fs9 from "fs";
1418
- import * as path8 from "path";
1422
+ import * as fs8 from "fs";
1423
+ import * as path7 from "path";
1419
1424
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
1420
1425
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
1421
1426
  var VALID_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "container", "watch", "utility"]);
@@ -1431,12 +1436,12 @@ var ProfileError = class extends Error {
1431
1436
  profilePath;
1432
1437
  };
1433
1438
  function loadProfile(profilePath) {
1434
- if (!fs9.existsSync(profilePath)) {
1439
+ if (!fs8.existsSync(profilePath)) {
1435
1440
  throw new ProfileError(profilePath, "file not found");
1436
1441
  }
1437
1442
  let raw;
1438
1443
  try {
1439
- raw = JSON.parse(fs9.readFileSync(profilePath, "utf-8"));
1444
+ raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
1440
1445
  } catch (err) {
1441
1446
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
1442
1447
  }
@@ -1475,7 +1480,7 @@ function loadProfile(profilePath) {
1475
1480
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
1476
1481
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
1477
1482
  children,
1478
- dir: path8.dirname(profilePath)
1483
+ dir: path7.dirname(profilePath)
1479
1484
  };
1480
1485
  return profile;
1481
1486
  }
@@ -1718,8 +1723,236 @@ function parseScriptList(p, key, raw) {
1718
1723
  return out;
1719
1724
  }
1720
1725
 
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);
1731
+ }
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
+ }
1745
+ }
1746
+ return [...byLabel.values()];
1747
+ }
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
+ }
1769
+ }
1770
+ return result;
1771
+ }
1772
+ function getIssueLabels(issueNumber, cwd) {
1773
+ try {
1774
+ const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
1775
+ return output.split("\n").filter(Boolean);
1776
+ } catch {
1777
+ return [];
1778
+ }
1779
+ }
1780
+ function addLabel(issueNumber, label, cwd) {
1781
+ gh(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
1782
+ }
1783
+ function removeLabel(issueNumber, label, cwd) {
1784
+ try {
1785
+ gh(["issue", "edit", String(issueNumber), "--remove-label", label], { cwd });
1786
+ } catch {
1787
+ }
1788
+ }
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 });
1794
+ }
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;
1801
+ }
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);
1807
+ }
1808
+ }
1809
+ try {
1810
+ addLabel(issueNumber, target, cwd);
1811
+ } catch (err) {
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
+ }
1824
+ }
1825
+ process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
1826
+ `);
1827
+ }
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) {
1850
+ try {
1851
+ const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
1852
+ return response.ok;
1853
+ } catch {
1854
+ return false;
1855
+ }
1856
+ }
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";
1878
+ try {
1879
+ execFileSync4("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
1880
+ } catch {
1881
+ try {
1882
+ execFileSync4("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
1883
+ cmd = "python3";
1884
+ } catch {
1885
+ throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
1886
+ }
1887
+ }
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
+ }
1915
+ }
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
+
1721
1954
  // src/commit.ts
1722
- import { execFileSync as execFileSync4 } from "child_process";
1955
+ import { execFileSync as execFileSync5 } from "child_process";
1723
1956
  import * as fs10 from "fs";
1724
1957
  import * as path9 from "path";
1725
1958
  var FORBIDDEN_PATH_PREFIXES = [
@@ -1750,7 +1983,7 @@ var CONVENTIONAL_PREFIXES = [
1750
1983
  ];
1751
1984
  function git(args, cwd) {
1752
1985
  try {
1753
- return execFileSync4("git", args, {
1986
+ return execFileSync5("git", args, {
1754
1987
  encoding: "utf-8",
1755
1988
  timeout: 12e4,
1756
1989
  cwd,
@@ -1809,7 +2042,7 @@ function isForbiddenPath(p) {
1809
2042
  return false;
1810
2043
  }
1811
2044
  function listChangedFiles(cwd) {
1812
- const raw = execFileSync4("git", ["status", "--porcelain=v1", "-z"], {
2045
+ const raw = execFileSync5("git", ["status", "--porcelain=v1", "-z"], {
1813
2046
  encoding: "utf-8",
1814
2047
  cwd,
1815
2048
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -1821,7 +2054,7 @@ function listChangedFiles(cwd) {
1821
2054
  }
1822
2055
  function listFilesInCommit(ref = "HEAD", cwd) {
1823
2056
  try {
1824
- const raw = execFileSync4("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
2057
+ const raw = execFileSync5("git", ["show", "--name-only", "--pretty=format:", "-z", ref], {
1825
2058
  encoding: "utf-8",
1826
2059
  cwd,
1827
2060
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
@@ -1910,14 +2143,14 @@ var abortUnfinishedGitOps2 = async (ctx) => {
1910
2143
  };
1911
2144
 
1912
2145
  // src/scripts/advanceFlow.ts
1913
- import { execFileSync as execFileSync6 } from "child_process";
2146
+ import { execFileSync as execFileSync7 } from "child_process";
1914
2147
 
1915
2148
  // src/state.ts
1916
- import { execFileSync as execFileSync5 } from "child_process";
2149
+ import { execFileSync as execFileSync6 } from "child_process";
1917
2150
  var STATE_BEGIN = "<!-- kody:state:v1:begin -->";
1918
2151
  var STATE_END = "<!-- kody:state:v1:end -->";
1919
2152
  var HISTORY_MAX_ENTRIES = 20;
1920
- var API_TIMEOUT_MS = 3e4;
2153
+ var API_TIMEOUT_MS2 = 3e4;
1921
2154
  function emptyState() {
1922
2155
  return {
1923
2156
  schemaVersion: 1,
@@ -1933,15 +2166,15 @@ function emptyState() {
1933
2166
  history: []
1934
2167
  };
1935
2168
  }
1936
- function ghToken() {
2169
+ function ghToken2() {
1937
2170
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
1938
2171
  }
1939
- function gh(args, input, cwd) {
1940
- const token = ghToken();
2172
+ function gh2(args, input, cwd) {
2173
+ const token = ghToken2();
1941
2174
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
1942
- return execFileSync5("gh", args, {
2175
+ return execFileSync6("gh", args, {
1943
2176
  encoding: "utf-8",
1944
- timeout: API_TIMEOUT_MS,
2177
+ timeout: API_TIMEOUT_MS2,
1945
2178
  cwd,
1946
2179
  env,
1947
2180
  input,
@@ -1951,7 +2184,7 @@ function gh(args, input, cwd) {
1951
2184
  function findStateComment(target, number, cwd) {
1952
2185
  const apiPath = target === "issue" ? `repos/{owner}/{repo}/issues/${number}/comments` : `repos/{owner}/{repo}/issues/${number}/comments`;
1953
2186
  try {
1954
- const raw = gh(["api", "--paginate", apiPath], void 0, cwd);
2187
+ const raw = gh2(["api", "--paginate", apiPath], void 0, cwd);
1955
2188
  const list = JSON.parse(raw);
1956
2189
  for (const c of list) {
1957
2190
  if (c.body?.includes(STATE_BEGIN)) {
@@ -2104,10 +2337,10 @@ function writeTaskState(target, number, state, cwd) {
2104
2337
  const existing = findStateComment(target, number, cwd);
2105
2338
  try {
2106
2339
  if (existing) {
2107
- 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);
2108
2341
  } else {
2109
2342
  const sub = target === "issue" ? "issue" : "pr";
2110
- gh([sub, "comment", String(number), "--body-file", "-"], body, cwd);
2343
+ gh2([sub, "comment", String(number), "--body-file", "-"], body, cwd);
2111
2344
  }
2112
2345
  } catch (err) {
2113
2346
  process.stderr.write(
@@ -2118,7 +2351,7 @@ function writeTaskState(target, number, state, cwd) {
2118
2351
  }
2119
2352
 
2120
2353
  // src/scripts/advanceFlow.ts
2121
- var API_TIMEOUT_MS2 = 3e4;
2354
+ var API_TIMEOUT_MS3 = 3e4;
2122
2355
  var advanceFlow = async (ctx, profile) => {
2123
2356
  const state = ctx.data.taskState;
2124
2357
  const flow = state?.flow;
@@ -2142,8 +2375,8 @@ var advanceFlow = async (ctx, profile) => {
2142
2375
  }
2143
2376
  const body = `@kody ${flow.name}`;
2144
2377
  try {
2145
- execFileSync6("gh", ["issue", "comment", String(flow.issueNumber), "--body", body], {
2146
- timeout: API_TIMEOUT_MS2,
2378
+ execFileSync7("gh", ["issue", "comment", String(flow.issueNumber), "--body", body], {
2379
+ timeout: API_TIMEOUT_MS3,
2147
2380
  cwd: ctx.cwd,
2148
2381
  stdio: ["ignore", "pipe", "pipe"]
2149
2382
  });
@@ -2251,7 +2484,7 @@ function copyDir(src, dst) {
2251
2484
  }
2252
2485
 
2253
2486
  // src/coverage.ts
2254
- import { execFileSync as execFileSync7 } from "child_process";
2487
+ import { execFileSync as execFileSync8 } from "child_process";
2255
2488
  function patternToRegex(pattern) {
2256
2489
  let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2257
2490
  s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
@@ -2269,7 +2502,7 @@ function renderSiblingPath(file, requireSibling) {
2269
2502
  }
2270
2503
  function safeGit(args, cwd) {
2271
2504
  try {
2272
- 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();
2273
2506
  } catch {
2274
2507
  return "";
2275
2508
  }
@@ -2632,147 +2865,6 @@ import { execFileSync as execFileSync9 } from "child_process";
2632
2865
  import * as fs14 from "fs";
2633
2866
  import * as path13 from "path";
2634
2867
 
2635
- // src/issue.ts
2636
- import { execFileSync as execFileSync8 } from "child_process";
2637
- var API_TIMEOUT_MS3 = 3e4;
2638
- function ghToken2() {
2639
- return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
2640
- }
2641
- function gh2(args, options) {
2642
- const token = ghToken2();
2643
- const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
2644
- return execFileSync8("gh", args, {
2645
- encoding: "utf-8",
2646
- timeout: API_TIMEOUT_MS3,
2647
- cwd: options?.cwd,
2648
- env,
2649
- input: options?.input,
2650
- stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
2651
- }).trim();
2652
- }
2653
- function getIssue(issueNumber, cwd) {
2654
- const output = gh2(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
2655
- const parsed = JSON.parse(output);
2656
- if (typeof parsed?.title !== "string") {
2657
- throw new Error(`Issue #${issueNumber}: unexpected response shape`);
2658
- }
2659
- return {
2660
- number: parsed.number ?? issueNumber,
2661
- title: parsed.title,
2662
- body: parsed.body ?? "",
2663
- comments: (parsed.comments ?? []).map((c) => ({
2664
- body: c.body ?? "",
2665
- author: c.author?.login ?? "unknown",
2666
- createdAt: c.createdAt ?? ""
2667
- })),
2668
- labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
2669
- };
2670
- }
2671
- function stripKodyMentions(body) {
2672
- return body.replace(/(@)(kody)/gi, "$1\u200B$2");
2673
- }
2674
- function postIssueComment(issueNumber, body, cwd) {
2675
- try {
2676
- gh2(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
2677
- } catch (err) {
2678
- process.stderr.write(
2679
- `[kody] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
2680
- `
2681
- );
2682
- }
2683
- }
2684
- function truncate2(s, maxBytes) {
2685
- if (s.length <= maxBytes) return s;
2686
- return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
2687
- }
2688
- function parsePrNumber(url) {
2689
- const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
2690
- if (!m) return null;
2691
- const n = parseInt(m[1], 10);
2692
- return Number.isFinite(n) ? n : null;
2693
- }
2694
- function getPr(prNumber, cwd) {
2695
- const output = gh2(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
2696
- cwd
2697
- });
2698
- const parsed = JSON.parse(output);
2699
- if (typeof parsed?.title !== "string") {
2700
- throw new Error(`PR #${prNumber}: unexpected response shape`);
2701
- }
2702
- return {
2703
- number: parsed.number ?? prNumber,
2704
- title: parsed.title,
2705
- body: parsed.body ?? "",
2706
- headRefName: String(parsed.headRefName ?? ""),
2707
- baseRefName: String(parsed.baseRefName ?? ""),
2708
- state: String(parsed.state ?? "")
2709
- };
2710
- }
2711
- function getPrDiff(prNumber, cwd) {
2712
- try {
2713
- return gh2(["pr", "diff", String(prNumber)], { cwd });
2714
- } catch (err) {
2715
- process.stderr.write(
2716
- `[kody] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
2717
- `
2718
- );
2719
- return "";
2720
- }
2721
- }
2722
- function getPrReviews(prNumber, cwd) {
2723
- try {
2724
- const output = gh2(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
2725
- const parsed = JSON.parse(output);
2726
- if (!Array.isArray(parsed?.reviews)) return [];
2727
- return parsed.reviews.map(
2728
- (r) => ({
2729
- body: r.body ?? "",
2730
- state: r.state ?? "",
2731
- author: r.author?.login ?? "unknown",
2732
- submittedAt: r.submittedAt ?? ""
2733
- })
2734
- );
2735
- } catch {
2736
- return [];
2737
- }
2738
- }
2739
- function getPrComments(prNumber, cwd) {
2740
- try {
2741
- const output = gh2(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
2742
- const parsed = JSON.parse(output);
2743
- if (!Array.isArray(parsed?.comments)) return [];
2744
- return parsed.comments.map((c) => ({
2745
- body: c.body ?? "",
2746
- author: c.author?.login ?? "unknown",
2747
- createdAt: c.createdAt ?? ""
2748
- })).filter((c) => c.body.trim().length > 0);
2749
- } catch {
2750
- return [];
2751
- }
2752
- }
2753
- var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
2754
- function isReviewShaped(body) {
2755
- return VERDICT_HEADING.test(body);
2756
- }
2757
- function getPrLatestReviewBody(prNumber, cwd) {
2758
- const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
2759
- const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
2760
- const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
2761
- if (all.length > 0) return all[0].body;
2762
- const pr = getPr(prNumber, cwd);
2763
- return pr.body;
2764
- }
2765
- function postPrReviewComment(prNumber, body, cwd) {
2766
- try {
2767
- gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
2768
- } catch (err) {
2769
- process.stderr.write(
2770
- `[kody] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
2771
- `
2772
- );
2773
- }
2774
- }
2775
-
2776
2868
  // src/scripts/postReviewResult.ts
2777
2869
  function detectVerdict(body) {
2778
2870
  const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
@@ -2902,7 +2994,7 @@ function splitReport(text) {
2902
2994
  function loadManifest(cwd) {
2903
2995
  let issuesJson;
2904
2996
  try {
2905
- issuesJson = gh2(
2997
+ issuesJson = gh(
2906
2998
  ["issue", "list", "--label", MANIFEST_LABEL, "--state", "all", "--limit", "1", "--json", "number,body"],
2907
2999
  { cwd }
2908
3000
  );
@@ -2955,7 +3047,7 @@ ${MANIFEST_END}
2955
3047
  }
2956
3048
  function ensureLabel(name, color, description, cwd) {
2957
3049
  try {
2958
- gh2(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
3050
+ gh(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
2959
3051
  } catch {
2960
3052
  }
2961
3053
  }
@@ -3010,10 +3102,10 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
3010
3102
  ensureLabel(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
3011
3103
  const body = serializeManifestBody(manifest);
3012
3104
  if (number !== null) {
3013
- gh2(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
3105
+ gh(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
3014
3106
  return { number, created: false };
3015
3107
  }
3016
- 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", "-"], {
3017
3109
  input: body,
3018
3110
  cwd
3019
3111
  });
@@ -3130,7 +3222,7 @@ function createTaskIssue(finding, goalId, manifestNumber, cwd) {
3130
3222
  for (const l of labels) {
3131
3223
  args.push("--label", l);
3132
3224
  }
3133
- const out = gh2(args, { input: body, cwd });
3225
+ const out = gh(args, { input: body, cwd });
3134
3226
  const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
3135
3227
  const m = url.match(/\/issues\/(\d+)\b/);
3136
3228
  if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
@@ -3187,7 +3279,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
3187
3279
  const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
3188
3280
  let url = "";
3189
3281
  try {
3190
- const out = gh2(
3282
+ const out = gh(
3191
3283
  ["issue", "create", "--title", title, "--label", FINDING_LABEL, "--body-file", "-"],
3192
3284
  { input: finalText, cwd: ctx.cwd }
3193
3285
  );
@@ -4035,7 +4127,7 @@ function parseStateCommentBody(marker, body) {
4035
4127
  return isStateEnvelope(parsed) ? parsed : null;
4036
4128
  }
4037
4129
  function listIssueComments(owner, repo, issueNumber, cwd) {
4038
- 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 });
4039
4131
  let parsed;
4040
4132
  try {
4041
4133
  parsed = JSON.parse(raw);
@@ -4056,7 +4148,7 @@ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
4056
4148
  }
4057
4149
  function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
4058
4150
  const body = formatStateCommentBody(marker, state);
4059
- 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", "-"], {
4060
4152
  cwd,
4061
4153
  input: JSON.stringify({ body })
4062
4154
  });
@@ -4069,7 +4161,7 @@ function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
4069
4161
  }
4070
4162
  function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
4071
4163
  const body = formatStateCommentBody(marker, state);
4072
- gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
4164
+ gh(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
4073
4165
  cwd,
4074
4166
  input: JSON.stringify({ body })
4075
4167
  });
@@ -4080,7 +4172,7 @@ function updateStateComment(owner, repo, commentId, commentNodeId, marker, state
4080
4172
  }
4081
4173
  function minimizeComment(nodeId, cwd) {
4082
4174
  const mutation = "mutation($id: ID!) { minimizeComment(input: { classifier: OUTDATED, subjectId: $id }) { minimizedComment { isMinimized } } }";
4083
- gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
4175
+ gh(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
4084
4176
  }
4085
4177
 
4086
4178
  // src/scripts/jobState/backend.ts
@@ -4117,7 +4209,7 @@ var ContentsApiBackend = class {
4117
4209
  const filePath = stateFilePath(this.jobsDir, slug);
4118
4210
  let raw = "";
4119
4211
  try {
4120
- 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 });
4121
4213
  } catch (err) {
4122
4214
  const msg = err instanceof Error ? err.message : String(err);
4123
4215
  if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
@@ -4161,7 +4253,7 @@ var ContentsApiBackend = class {
4161
4253
  content: Buffer.from(body, "utf-8").toString("base64")
4162
4254
  };
4163
4255
  if (typeof loaded.handle === "string") payload.sha = loaded.handle;
4164
- 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", "-"], {
4165
4257
  cwd: this.cwd,
4166
4258
  input: JSON.stringify(payload)
4167
4259
  });
@@ -4507,7 +4599,7 @@ var dispatchJobTicks = async (ctx, _profile, args) => {
4507
4599
  function listIssuesByLabel(label, cwd) {
4508
4600
  let raw = "";
4509
4601
  try {
4510
- 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"], {
4511
4603
  cwd
4512
4604
  });
4513
4605
  } catch {
@@ -4591,7 +4683,7 @@ function firstLine(s) {
4591
4683
  }
4592
4684
  function findExistingPr(branch, cwd) {
4593
4685
  try {
4594
- const output = gh2(
4686
+ const output = gh(
4595
4687
  ["pr", "list", "--head", branch, "--state", "open", "--json", "number,url,body", "--limit", "1"],
4596
4688
  { cwd }
4597
4689
  );
@@ -4629,7 +4721,7 @@ function ensurePr(opts) {
4629
4721
  const stripped = existing.url.replace(/^https:\/\/github\.com\//, "");
4630
4722
  const [owner, repo] = stripped.split("/");
4631
4723
  try {
4632
- 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}`], {
4633
4725
  cwd: opts.cwd
4634
4726
  });
4635
4727
  } catch (err) {
@@ -4651,7 +4743,7 @@ function ensurePr(opts) {
4651
4743
  "-"
4652
4744
  ];
4653
4745
  if (opts.draft) args.push("--draft");
4654
- const output = gh2(args, { input: body, cwd: opts.cwd });
4746
+ const output = gh(args, { input: body, cwd: opts.cwd });
4655
4747
  const url = output.trim();
4656
4748
  const match = url.match(/\/pull\/(\d+)$/);
4657
4749
  const number = match ? parseInt(match[1], 10) : 0;
@@ -4738,125 +4830,6 @@ function collectExpectedTests(raw) {
4738
4830
 
4739
4831
  // src/scripts/finishFlow.ts
4740
4832
  import { execFileSync as execFileSync13 } from "child_process";
4741
-
4742
- // src/lifecycleLabels.ts
4743
- var KODY_NAMESPACE = "kody";
4744
- function groupOf(label) {
4745
- const idx = label.indexOf(":");
4746
- return idx === -1 ? label : label.slice(0, idx + 1);
4747
- }
4748
- function collectProfileLabels() {
4749
- const byLabel = /* @__PURE__ */ new Map();
4750
- for (const exe of listExecutables()) {
4751
- let profile;
4752
- try {
4753
- profile = loadProfile(exe.profilePath);
4754
- } catch {
4755
- continue;
4756
- }
4757
- for (const entry of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
4758
- const spec = extractLabelSpec(entry);
4759
- if (spec) byLabel.set(spec.label, spec);
4760
- }
4761
- }
4762
- return [...byLabel.values()];
4763
- }
4764
- function extractLabelSpec(entry) {
4765
- const w = entry.with;
4766
- if (!w) return null;
4767
- const label = typeof w.label === "string" ? w.label : null;
4768
- if (!label || !label.startsWith(KODY_NAMESPACE)) return null;
4769
- return {
4770
- label,
4771
- color: typeof w.color === "string" ? w.color : void 0,
4772
- description: typeof w.description === "string" ? w.description : void 0
4773
- };
4774
- }
4775
- function ensureLabels(cwd) {
4776
- const result = { created: [], failed: [] };
4777
- for (const spec of collectProfileLabels()) {
4778
- try {
4779
- createLabelInRepo(spec, cwd);
4780
- result.created.push(spec.label);
4781
- } catch (err) {
4782
- result.failed.push({ label: spec.label, reason: errMsg(err) });
4783
- }
4784
- }
4785
- return result;
4786
- }
4787
- function getIssueLabels(issueNumber, cwd) {
4788
- try {
4789
- const output = gh2(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
4790
- return output.split("\n").filter(Boolean);
4791
- } catch {
4792
- return [];
4793
- }
4794
- }
4795
- function addLabel(issueNumber, label, cwd) {
4796
- gh2(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
4797
- }
4798
- function removeLabel(issueNumber, label, cwd) {
4799
- try {
4800
- gh2(["issue", "edit", String(issueNumber), "--remove-label", label], { cwd });
4801
- } catch {
4802
- }
4803
- }
4804
- function createLabelInRepo(spec, cwd) {
4805
- const args = ["label", "create", spec.label, "--force"];
4806
- if (spec.color) args.push("--color", spec.color);
4807
- if (spec.description) args.push("--description", spec.description);
4808
- gh2(args, { cwd });
4809
- }
4810
- function setKodyLabel(issueNumber, spec, cwd) {
4811
- const target = spec.label;
4812
- if (!target.startsWith(KODY_NAMESPACE)) {
4813
- process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
4814
- `);
4815
- return;
4816
- }
4817
- const targetGroup = groupOf(target);
4818
- const present = getIssueLabels(issueNumber, cwd);
4819
- for (const label of present) {
4820
- if (label !== target && label.startsWith(KODY_NAMESPACE) && groupOf(label) === targetGroup) {
4821
- removeLabel(issueNumber, label, cwd);
4822
- }
4823
- }
4824
- try {
4825
- addLabel(issueNumber, target, cwd);
4826
- } catch (err) {
4827
- if (looksLikeMissingLabel(err)) {
4828
- try {
4829
- createLabelInRepo(spec, cwd);
4830
- addLabel(issueNumber, target, cwd);
4831
- return;
4832
- } catch (retryErr) {
4833
- process.stderr.write(
4834
- `[kody] setKodyLabel: create+retry failed for ${target} on #${issueNumber}: ${errMsg(retryErr)}
4835
- `
4836
- );
4837
- return;
4838
- }
4839
- }
4840
- process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
4841
- `);
4842
- }
4843
- }
4844
- function looksLikeMissingLabel(err) {
4845
- const msg = errMsg(err).toLowerCase();
4846
- return msg.includes("not found") || msg.includes("could not add label") || msg.includes("could not resolve to a label");
4847
- }
4848
- function errMsg(err) {
4849
- if (err instanceof Error) return err.message;
4850
- if (typeof err === "object" && err !== null) {
4851
- const e = err;
4852
- const stderr = e.stderr?.toString().trim();
4853
- if (stderr) return stderr;
4854
- if (e.message) return e.message;
4855
- }
4856
- return String(err);
4857
- }
4858
-
4859
- // src/scripts/finishFlow.ts
4860
4833
  var API_TIMEOUT_MS6 = 3e4;
4861
4834
  var STATUS_ICON = {
4862
4835
  "review-passed": "\u2705",
@@ -5487,21 +5460,6 @@ function performInit(cwd, force) {
5487
5460
  wrote.push(QA_GUIDE_REL_PATH);
5488
5461
  }
5489
5462
  }
5490
- const builtinJobs = listBuiltinJobs();
5491
- if (builtinJobs.length > 0) {
5492
- const jobsDir = path20.join(cwd, ".kody", "jobs");
5493
- fs22.mkdirSync(jobsDir, { recursive: true });
5494
- for (const job of builtinJobs) {
5495
- const rel = path20.join(".kody", "jobs", `${job.slug}.md`);
5496
- const target = path20.join(cwd, rel);
5497
- if (fs22.existsSync(target) && !force) {
5498
- skipped.push(rel);
5499
- continue;
5500
- }
5501
- fs22.writeFileSync(target, fs22.readFileSync(job.filePath, "utf-8"));
5502
- wrote.push(rel);
5503
- }
5504
- }
5505
5463
  for (const exe of listExecutables()) {
5506
5464
  let profile;
5507
5465
  try {
@@ -5871,7 +5829,7 @@ function parsePrNumbers(raw) {
5871
5829
  }
5872
5830
  function fetchPrBlock(prNumber, cwd) {
5873
5831
  try {
5874
- 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 });
5875
5833
  const meta = JSON.parse(metaRaw);
5876
5834
  const diff = truncate3(safeGh(["pr", "diff", String(prNumber)], cwd), PER_PR_DIFF_MAX_BYTES);
5877
5835
  const commentsRaw = safeGh(["pr", "view", String(prNumber), "--json", "comments,reviews"], cwd);
@@ -5898,7 +5856,7 @@ _Could not fetch \u2014 ${err instanceof Error ? err.message : String(err)}_`;
5898
5856
  }
5899
5857
  function safeGh(args, cwd) {
5900
5858
  try {
5901
- return gh2(args, { cwd });
5859
+ return gh(args, { cwd });
5902
5860
  } catch {
5903
5861
  return "";
5904
5862
  }
@@ -6086,7 +6044,7 @@ function buildIssueTitle(scope, verdict) {
6086
6044
  }
6087
6045
  function ensureLabel2(cwd) {
6088
6046
  try {
6089
- 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 });
6090
6048
  return true;
6091
6049
  } catch {
6092
6050
  return false;
@@ -6095,7 +6053,7 @@ function ensureLabel2(cwd) {
6095
6053
  function createQaIssue(title, body, hasLabel, cwd) {
6096
6054
  const args = ["issue", "create", "--title", title, "--body-file", "-"];
6097
6055
  if (hasLabel) args.push("--label", QA_LABEL);
6098
- const out = gh2(args, { input: body, cwd });
6056
+ const out = gh(args, { input: body, cwd });
6099
6057
  const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
6100
6058
  const m = url.match(/\/issues\/(\d+)\b/);
6101
6059
  if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
@@ -6869,7 +6827,7 @@ function latestSuccessUrl(deploymentId, cwd) {
6869
6827
  }
6870
6828
  function safeGh2(args, cwd) {
6871
6829
  try {
6872
- return gh2(args, { cwd });
6830
+ return gh(args, { cwd });
6873
6831
  } catch {
6874
6832
  return null;
6875
6833
  }
@@ -8251,12 +8209,26 @@ async function runExecutable(profileName, input) {
8251
8209
  reason: ctx.output.reason
8252
8210
  });
8253
8211
  } finally {
8212
+ clearStampedLifecycleLabels(profile, ctx);
8254
8213
  try {
8255
8214
  litellm?.kill();
8256
8215
  } catch {
8257
8216
  }
8258
8217
  }
8259
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
+ }
8260
8232
  function resolveProfilePath(profileName) {
8261
8233
  const found = resolveExecutable(profileName);
8262
8234
  if (found) return found;
@@ -31,7 +31,17 @@
31
31
  set -euo pipefail
32
32
 
33
33
  goal_id="${KODY_ARG_GOAL:-}"
34
- default_branch="${KODY_CFG_GIT_DEFAULTBRANCH:-main}"
34
+ # Default branch: prefer KODY_CFG_GIT_DEFAULTBRANCH (config), then ask the
35
+ # repo via the GitHub API, finally fall back to "main". Past regression: a
36
+ # hardcoded "main" fallback opened goal PRs against `main` for repos whose
37
+ # real default is `dev`, which then needed manual retargeting.
38
+ default_branch="${KODY_CFG_GIT_DEFAULTBRANCH:-}"
39
+ if [ -z "$default_branch" ]; then
40
+ default_branch=$(gh api "repos/{owner}/{repo}" --jq .default_branch 2>/dev/null || echo "")
41
+ fi
42
+ if [ -z "$default_branch" ]; then
43
+ default_branch="main"
44
+ fi
35
45
 
36
46
  if [ -z "$goal_id" ]; then
37
47
  echo "KODY_REASON=missing --goal"
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "watch-stale-prs",
3
+ "role": "watch",
4
+ "describe": "Scheduled: list open PRs untouched for N days and report. No agent invocation.",
5
+ "kind": "scheduled",
6
+ "schedule": "0 8 * * MON",
7
+ "inputs": [],
8
+ "claudeCode": {
9
+ "model": "inherit",
10
+ "permissionMode": "default",
11
+ "maxTurns": null,
12
+ "systemPromptAppend": null,
13
+ "tools": [],
14
+ "hooks": [],
15
+ "skills": [],
16
+ "commands": [],
17
+ "subagents": [],
18
+ "plugins": [],
19
+ "mcpServers": []
20
+ },
21
+ "cliTools": [],
22
+ "scripts": {
23
+ "preflight": [
24
+ {
25
+ "script": "watchStalePrsFlow"
26
+ }
27
+ ],
28
+ "postflight": []
29
+ }
30
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.21",
3
+ "version": "0.4.24",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",