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