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