@kody-ade/kody-engine-lite 0.1.113 → 0.1.114
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/cli.js +1339 -344
- package/package.json +1 -1
- package/templates/kody.yml +1 -0
package/dist/bin/cli.js
CHANGED
|
@@ -226,13 +226,13 @@ function getProjectConfig() {
|
|
|
226
226
|
const configPath = path6.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
227
227
|
if (fs7.existsSync(configPath)) {
|
|
228
228
|
try {
|
|
229
|
-
const
|
|
230
|
-
if (!
|
|
231
|
-
logger.warn(`kody.config.json: ${
|
|
229
|
+
const result2 = parseJsonSafe(fs7.readFileSync(configPath, "utf-8"));
|
|
230
|
+
if (!result2.ok) {
|
|
231
|
+
logger.warn(`kody.config.json: ${result2.error} \u2014 using defaults`);
|
|
232
232
|
_config = { ...DEFAULT_CONFIG };
|
|
233
233
|
return _config;
|
|
234
234
|
}
|
|
235
|
-
const raw =
|
|
235
|
+
const raw = result2.data;
|
|
236
236
|
_config = {
|
|
237
237
|
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
238
238
|
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
@@ -966,8 +966,8 @@ function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
|
966
966
|
}
|
|
967
967
|
function generateTaskId() {
|
|
968
968
|
const now = /* @__PURE__ */ new Date();
|
|
969
|
-
const
|
|
970
|
-
return `${String(now.getFullYear()).slice(2)}${
|
|
969
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
970
|
+
return `${String(now.getFullYear()).slice(2)}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
971
971
|
}
|
|
972
972
|
function resolveTaskIdFromComments(issueNumber) {
|
|
973
973
|
try {
|
|
@@ -1361,13 +1361,13 @@ Kody is decomposing ${src} into tasks...`);
|
|
|
1361
1361
|
fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
|
|
1362
1362
|
const runner = opts.runner ?? createClaudeCodeRunner();
|
|
1363
1363
|
logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
|
|
1364
|
-
const
|
|
1364
|
+
const result2 = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
|
|
1365
1365
|
cwd: projectDir,
|
|
1366
1366
|
mcpConfigJson,
|
|
1367
1367
|
env: opts.runnerEnv
|
|
1368
1368
|
});
|
|
1369
|
-
if (
|
|
1370
|
-
const errMsg =
|
|
1369
|
+
if (result2.outcome !== "completed") {
|
|
1370
|
+
const errMsg = result2.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result2.error}`;
|
|
1371
1371
|
if (issueNumber && !local) {
|
|
1372
1372
|
postComment(issueNumber, `Kody taskify failed:
|
|
1373
1373
|
|
|
@@ -1380,7 +1380,7 @@ Kody is decomposing ${src} into tasks...`);
|
|
|
1380
1380
|
if (!fs11.existsSync(resultPath)) {
|
|
1381
1381
|
const errMsg = `Claude did not write ${RESULT_FILE}. Output:
|
|
1382
1382
|
|
|
1383
|
-
${
|
|
1383
|
+
${result2.output?.slice(0, 500) ?? "(none)"}`;
|
|
1384
1384
|
if (issueNumber && !local) {
|
|
1385
1385
|
postComment(issueNumber, `Kody taskify failed: result file not found.
|
|
1386
1386
|
|
|
@@ -1568,6 +1568,999 @@ var init_taskify_command = __esm({
|
|
|
1568
1568
|
}
|
|
1569
1569
|
});
|
|
1570
1570
|
|
|
1571
|
+
// src/cli/test-model-tests.ts
|
|
1572
|
+
import * as fs12 from "fs";
|
|
1573
|
+
import * as os2 from "os";
|
|
1574
|
+
import * as path11 from "path";
|
|
1575
|
+
import * as zlib from "zlib";
|
|
1576
|
+
import { spawnSync, execSync as execSync2 } from "child_process";
|
|
1577
|
+
async function apiCall(ctx, body) {
|
|
1578
|
+
try {
|
|
1579
|
+
const res = await fetch(`${ctx.proxyUrl}/v1/messages`, {
|
|
1580
|
+
method: "POST",
|
|
1581
|
+
headers: {
|
|
1582
|
+
"Content-Type": "application/json",
|
|
1583
|
+
"x-api-key": ctx.apiKey,
|
|
1584
|
+
"anthropic-version": "2023-06-01"
|
|
1585
|
+
},
|
|
1586
|
+
body: JSON.stringify({ model: ctx.model, ...body }),
|
|
1587
|
+
signal: AbortSignal.timeout(6e4)
|
|
1588
|
+
});
|
|
1589
|
+
const data = await res.json();
|
|
1590
|
+
if (!res.ok) {
|
|
1591
|
+
return { ok: false, data, status: res.status, errorMsg: data?.error?.message ?? `HTTP ${res.status}` };
|
|
1592
|
+
}
|
|
1593
|
+
return { ok: true, data, status: res.status };
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
return { ok: false, data: null, status: 0, errorMsg: err instanceof Error ? err.message : String(err) };
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
function extractText(data) {
|
|
1599
|
+
if (!data?.content) return "";
|
|
1600
|
+
return data.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
|
|
1601
|
+
}
|
|
1602
|
+
async function runToolConversation(ctx, tools, userPrompt, simulate, opts) {
|
|
1603
|
+
const messages = [{ role: "user", content: userPrompt }];
|
|
1604
|
+
const allCalls = [];
|
|
1605
|
+
for (let turn = 0; turn < (opts?.maxTurns ?? 5); turn++) {
|
|
1606
|
+
const body = {
|
|
1607
|
+
max_tokens: 1024,
|
|
1608
|
+
temperature: 0,
|
|
1609
|
+
messages,
|
|
1610
|
+
tools
|
|
1611
|
+
};
|
|
1612
|
+
if (opts?.system) body.system = opts.system;
|
|
1613
|
+
const res = await apiCall(ctx, body);
|
|
1614
|
+
if (!res.ok) return { finalText: "", toolCalls: allCalls, error: res.errorMsg };
|
|
1615
|
+
const content = res.data.content ?? [];
|
|
1616
|
+
const toolBlocks = content.filter((b) => b.type === "tool_use");
|
|
1617
|
+
const textBlocks = content.filter((b) => b.type === "text");
|
|
1618
|
+
if (toolBlocks.length === 0) {
|
|
1619
|
+
return { finalText: textBlocks.map((b) => b.text ?? "").join(""), toolCalls: allCalls };
|
|
1620
|
+
}
|
|
1621
|
+
for (const tc of toolBlocks) allCalls.push({ name: tc.name, input: tc.input });
|
|
1622
|
+
messages.push({ role: "assistant", content });
|
|
1623
|
+
messages.push({
|
|
1624
|
+
role: "user",
|
|
1625
|
+
content: toolBlocks.map((tc) => ({
|
|
1626
|
+
type: "tool_result",
|
|
1627
|
+
tool_use_id: tc.id,
|
|
1628
|
+
content: simulate(tc.name, tc.input)
|
|
1629
|
+
}))
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
return { finalText: "", toolCalls: allCalls, error: "Max turns reached" };
|
|
1633
|
+
}
|
|
1634
|
+
function filterStderr(stderr) {
|
|
1635
|
+
return stderr.split("\n").filter((l) => !l.includes("CPU lacks AVX") && !l.includes("bun-darwin") && !l.includes("Warning: no stdin data") && l.trim().length > 0).join("\n").trim();
|
|
1636
|
+
}
|
|
1637
|
+
function runClaudeTest(ctx, prompt, extraFlags = [], timeout = 9e4) {
|
|
1638
|
+
try {
|
|
1639
|
+
const result2 = spawnSync("claude", [
|
|
1640
|
+
"--print",
|
|
1641
|
+
"--model",
|
|
1642
|
+
ctx.model,
|
|
1643
|
+
"--dangerously-skip-permissions",
|
|
1644
|
+
...extraFlags,
|
|
1645
|
+
"-p",
|
|
1646
|
+
prompt
|
|
1647
|
+
], {
|
|
1648
|
+
env: { ...process.env, ANTHROPIC_BASE_URL: ctx.proxyUrl, ANTHROPIC_API_KEY: ctx.apiKey },
|
|
1649
|
+
timeout,
|
|
1650
|
+
encoding: "utf-8",
|
|
1651
|
+
cwd: ctx.projectDir
|
|
1652
|
+
});
|
|
1653
|
+
return {
|
|
1654
|
+
stdout: result2.stdout ?? "",
|
|
1655
|
+
stderr: filterStderr(result2.stderr ?? ""),
|
|
1656
|
+
exitCode: result2.status ?? 1
|
|
1657
|
+
};
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
return { stdout: "", stderr: String(err), exitCode: 1 };
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
function isGitClean(dir) {
|
|
1663
|
+
try {
|
|
1664
|
+
const out = execSync2("git diff --name-only", { cwd: dir, encoding: "utf-8", timeout: 5e3 });
|
|
1665
|
+
return out.trim().length === 0;
|
|
1666
|
+
} catch {
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function revertChanges(dir) {
|
|
1671
|
+
try {
|
|
1672
|
+
execSync2("git checkout -- src/logger.ts", { cwd: dir, timeout: 5e3, stdio: "pipe" });
|
|
1673
|
+
} catch {
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function result(name, category, status, accuracy, durationMs, detail, metrics) {
|
|
1677
|
+
return { name, category, status, accuracy, durationMs, detail, metrics };
|
|
1678
|
+
}
|
|
1679
|
+
function crc32(buf) {
|
|
1680
|
+
let c = 4294967295;
|
|
1681
|
+
for (const b of buf) c = CRC_TABLE[(c ^ b) & 255] ^ c >>> 8;
|
|
1682
|
+
return (c ^ 4294967295) >>> 0;
|
|
1683
|
+
}
|
|
1684
|
+
function createRedPng() {
|
|
1685
|
+
const w = 4, h = 4;
|
|
1686
|
+
const scanlines = Buffer.alloc(h * (1 + w * 3));
|
|
1687
|
+
for (let y = 0; y < h; y++) {
|
|
1688
|
+
const off = y * (1 + w * 3);
|
|
1689
|
+
scanlines[off] = 0;
|
|
1690
|
+
for (let x = 0; x < w; x++) {
|
|
1691
|
+
scanlines[off + 1 + x * 3] = 255;
|
|
1692
|
+
scanlines[off + 1 + x * 3 + 1] = 0;
|
|
1693
|
+
scanlines[off + 1 + x * 3 + 2] = 0;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
function chunk(type, data) {
|
|
1697
|
+
const tb = Buffer.from(type, "ascii");
|
|
1698
|
+
const merged = Buffer.concat([tb, data]);
|
|
1699
|
+
const len = Buffer.alloc(4);
|
|
1700
|
+
len.writeUInt32BE(data.length);
|
|
1701
|
+
const crcBuf = Buffer.alloc(4);
|
|
1702
|
+
crcBuf.writeUInt32BE(crc32(merged));
|
|
1703
|
+
return Buffer.concat([len, tb, data, crcBuf]);
|
|
1704
|
+
}
|
|
1705
|
+
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
1706
|
+
const ihdr = Buffer.alloc(13);
|
|
1707
|
+
ihdr.writeUInt32BE(w, 0);
|
|
1708
|
+
ihdr.writeUInt32BE(h, 4);
|
|
1709
|
+
ihdr[8] = 8;
|
|
1710
|
+
ihdr[9] = 2;
|
|
1711
|
+
return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(scanlines)), chunk("IEND", Buffer.alloc(0))]);
|
|
1712
|
+
}
|
|
1713
|
+
async function testSimplePrompt(ctx) {
|
|
1714
|
+
const t = Date.now();
|
|
1715
|
+
const res = await apiCall(ctx, {
|
|
1716
|
+
max_tokens: 50,
|
|
1717
|
+
temperature: 0,
|
|
1718
|
+
messages: [{ role: "user", content: "Reply with exactly: KODY_TEST_OK" }]
|
|
1719
|
+
});
|
|
1720
|
+
if (!res.ok) return result("simple_prompt", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
|
|
1721
|
+
const text = extractText(res.data);
|
|
1722
|
+
const ok = text.includes("KODY_TEST_OK");
|
|
1723
|
+
return result(
|
|
1724
|
+
"simple_prompt",
|
|
1725
|
+
"basic",
|
|
1726
|
+
ok ? "pass" : "fail",
|
|
1727
|
+
ok ? 100 : 0,
|
|
1728
|
+
Date.now() - t,
|
|
1729
|
+
ok ? "Model responded correctly" : `Expected KODY_TEST_OK, got: ${text.slice(0, 80)}`
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
async function testJsonOutput(ctx) {
|
|
1733
|
+
const t = Date.now();
|
|
1734
|
+
const res = await apiCall(ctx, {
|
|
1735
|
+
max_tokens: 200,
|
|
1736
|
+
temperature: 0,
|
|
1737
|
+
system: "Respond with ONLY valid JSON. No markdown fences, no explanation. Just raw JSON.",
|
|
1738
|
+
messages: [{ role: "user", content: 'Return a JSON object with keys "status" (string "ok") and "model" (string, your model name).' }]
|
|
1739
|
+
});
|
|
1740
|
+
if (!res.ok) return result("json_output", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
|
|
1741
|
+
let text = extractText(res.data).trim();
|
|
1742
|
+
text = text.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
1743
|
+
try {
|
|
1744
|
+
const parsed = JSON.parse(text);
|
|
1745
|
+
const hasKeys = typeof parsed.status === "string" && typeof parsed.model === "string";
|
|
1746
|
+
return result(
|
|
1747
|
+
"json_output",
|
|
1748
|
+
"basic",
|
|
1749
|
+
"pass",
|
|
1750
|
+
hasKeys ? 100 : 70,
|
|
1751
|
+
Date.now() - t,
|
|
1752
|
+
hasKeys ? "Valid JSON with correct keys" : "Valid JSON but missing expected keys"
|
|
1753
|
+
);
|
|
1754
|
+
} catch {
|
|
1755
|
+
return result("json_output", "basic", "fail", 0, Date.now() - t, `Invalid JSON: ${text.slice(0, 80)}`);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
async function testSystemPromptRules(ctx) {
|
|
1759
|
+
const t = Date.now();
|
|
1760
|
+
const res = await apiCall(ctx, {
|
|
1761
|
+
max_tokens: 200,
|
|
1762
|
+
temperature: 0,
|
|
1763
|
+
system: [
|
|
1764
|
+
"STRICT RULES \u2014 violating ANY will crash the system:",
|
|
1765
|
+
"1) Start every response with 'KODY:'",
|
|
1766
|
+
"2) Never use the word 'the'",
|
|
1767
|
+
"3) Keep response under 50 words",
|
|
1768
|
+
"4) End your response with 'END'",
|
|
1769
|
+
"5) Use ONLY lowercase letters (no uppercase anywhere)"
|
|
1770
|
+
].join("\n"),
|
|
1771
|
+
messages: [{ role: "user", content: "Describe what a compiler does." }]
|
|
1772
|
+
});
|
|
1773
|
+
if (!res.ok) return result("system_prompt_rules", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
|
|
1774
|
+
const text = extractText(res.data).trim();
|
|
1775
|
+
let score = 0;
|
|
1776
|
+
const checks = [];
|
|
1777
|
+
if (text.startsWith("KODY:") || text.startsWith("kody:")) {
|
|
1778
|
+
score += 20;
|
|
1779
|
+
checks.push("starts-with-kody");
|
|
1780
|
+
}
|
|
1781
|
+
if (!text.toLowerCase().split(/\s+/).includes("the")) {
|
|
1782
|
+
score += 20;
|
|
1783
|
+
checks.push("no-the");
|
|
1784
|
+
}
|
|
1785
|
+
if (text.split(/\s+/).length <= 55) {
|
|
1786
|
+
score += 20;
|
|
1787
|
+
checks.push("under-50-words");
|
|
1788
|
+
}
|
|
1789
|
+
if (text.endsWith("END") || text.endsWith("end")) {
|
|
1790
|
+
score += 20;
|
|
1791
|
+
checks.push("ends-with-end");
|
|
1792
|
+
}
|
|
1793
|
+
if (text === text.toLowerCase()) {
|
|
1794
|
+
score += 20;
|
|
1795
|
+
checks.push("all-lowercase");
|
|
1796
|
+
}
|
|
1797
|
+
const status = score >= 80 ? "pass" : score >= 40 ? "warn" : "fail";
|
|
1798
|
+
return result(
|
|
1799
|
+
"system_prompt_rules",
|
|
1800
|
+
"basic",
|
|
1801
|
+
status,
|
|
1802
|
+
score,
|
|
1803
|
+
Date.now() - t,
|
|
1804
|
+
`${score / 20}/5 rules followed: ${checks.join(", ")}`,
|
|
1805
|
+
{ instructionCompliance: score }
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
async function testExtendedThinking(ctx) {
|
|
1809
|
+
const t = Date.now();
|
|
1810
|
+
const res = await apiCall(ctx, {
|
|
1811
|
+
max_tokens: 200,
|
|
1812
|
+
thinking: { type: "enabled", budget_tokens: 2e3 },
|
|
1813
|
+
messages: [{ role: "user", content: "What is 15 * 23?" }]
|
|
1814
|
+
});
|
|
1815
|
+
if (!res.ok) return result(
|
|
1816
|
+
"extended_thinking",
|
|
1817
|
+
"infrastructure",
|
|
1818
|
+
"warn",
|
|
1819
|
+
50,
|
|
1820
|
+
Date.now() - t,
|
|
1821
|
+
`Request failed (model may not support thinking): ${res.errorMsg?.slice(0, 80)}`
|
|
1822
|
+
);
|
|
1823
|
+
const hasThinking = Array.isArray(res.data.content) && res.data.content.some((b) => b.type === "thinking");
|
|
1824
|
+
const hasText = extractText(res.data).length > 0;
|
|
1825
|
+
if (hasThinking) return result("extended_thinking", "infrastructure", "pass", 100, Date.now() - t, "Thinking block present in response");
|
|
1826
|
+
if (hasText) return result("extended_thinking", "infrastructure", "warn", 70, Date.now() - t, "Response OK but no thinking block");
|
|
1827
|
+
return result("extended_thinking", "infrastructure", "fail", 0, Date.now() - t, "No content in response");
|
|
1828
|
+
}
|
|
1829
|
+
async function testToolRead(ctx) {
|
|
1830
|
+
const t = Date.now();
|
|
1831
|
+
const testFile = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
|
|
1832
|
+
fs12.writeFileSync(testFile, "KODY_SECRET_CONTENT_42");
|
|
1833
|
+
try {
|
|
1834
|
+
const conv = await runToolConversation(
|
|
1835
|
+
ctx,
|
|
1836
|
+
[TOOL_READ],
|
|
1837
|
+
`Read the file ${testFile} and tell me what it contains.`,
|
|
1838
|
+
(name, input) => {
|
|
1839
|
+
if (name === "Read" && input.path === testFile) return "KODY_SECRET_CONTENT_42";
|
|
1840
|
+
return "Error: File not found";
|
|
1841
|
+
}
|
|
1842
|
+
);
|
|
1843
|
+
if (conv.error) return result("tool_read", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
1844
|
+
const calledRead = conv.toolCalls.some((tc) => tc.name === "Read");
|
|
1845
|
+
const correctPath = conv.toolCalls.some((tc) => tc.name === "Read" && tc.input.path === testFile);
|
|
1846
|
+
const mentionsContent = conv.finalText.includes("KODY_SECRET_CONTENT_42") || conv.finalText.includes("42");
|
|
1847
|
+
let acc = 0;
|
|
1848
|
+
if (calledRead) acc += 30;
|
|
1849
|
+
if (correctPath) acc += 30;
|
|
1850
|
+
if (mentionsContent) acc += 40;
|
|
1851
|
+
return result(
|
|
1852
|
+
"tool_read",
|
|
1853
|
+
"tool-use",
|
|
1854
|
+
acc >= 60 ? "pass" : "fail",
|
|
1855
|
+
acc,
|
|
1856
|
+
Date.now() - t,
|
|
1857
|
+
`Read called: ${calledRead}, correct path: ${correctPath}, content referenced: ${mentionsContent}`,
|
|
1858
|
+
{ toolSelection: calledRead ? 100 : 0 }
|
|
1859
|
+
);
|
|
1860
|
+
} finally {
|
|
1861
|
+
fs12.rmSync(testFile, { force: true });
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
async function testToolEdit(ctx) {
|
|
1865
|
+
const t = Date.now();
|
|
1866
|
+
const conv = await runToolConversation(
|
|
1867
|
+
ctx,
|
|
1868
|
+
[TOOL_READ, TOOL_EDIT],
|
|
1869
|
+
'Read the file /tmp/kody-edit-test.txt, then use Edit to replace "hello" with "goodbye" in it.',
|
|
1870
|
+
(name, input) => {
|
|
1871
|
+
if (name === "Read") return "hello world";
|
|
1872
|
+
if (name === "Edit") return "File edited successfully";
|
|
1873
|
+
return "Unknown tool";
|
|
1874
|
+
}
|
|
1875
|
+
);
|
|
1876
|
+
if (conv.error) return result("tool_edit", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
1877
|
+
const editCall = conv.toolCalls.find((tc) => tc.name === "Edit");
|
|
1878
|
+
let acc = 0;
|
|
1879
|
+
if (editCall) {
|
|
1880
|
+
acc += 40;
|
|
1881
|
+
if (editCall.input.old_string === "hello") acc += 30;
|
|
1882
|
+
if (editCall.input.new_string === "goodbye") acc += 30;
|
|
1883
|
+
}
|
|
1884
|
+
return result(
|
|
1885
|
+
"tool_edit",
|
|
1886
|
+
"tool-use",
|
|
1887
|
+
acc >= 70 ? "pass" : acc > 0 ? "warn" : "fail",
|
|
1888
|
+
acc,
|
|
1889
|
+
Date.now() - t,
|
|
1890
|
+
editCall ? `Edit called with old="${editCall.input.old_string}" new="${editCall.input.new_string}"` : "Edit tool was not called",
|
|
1891
|
+
{ toolSelection: editCall ? 100 : 0 }
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
async function testToolBash(ctx) {
|
|
1895
|
+
const t = Date.now();
|
|
1896
|
+
const conv = await runToolConversation(
|
|
1897
|
+
ctx,
|
|
1898
|
+
[TOOL_BASH],
|
|
1899
|
+
"Run this exact bash command: echo KODY_BASH_OK",
|
|
1900
|
+
(name, input) => {
|
|
1901
|
+
if (name === "Bash") return "KODY_BASH_OK\n";
|
|
1902
|
+
return "Error";
|
|
1903
|
+
}
|
|
1904
|
+
);
|
|
1905
|
+
if (conv.error) return result("tool_bash", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
1906
|
+
const bashCall = conv.toolCalls.find((tc) => tc.name === "Bash");
|
|
1907
|
+
const correctCmd = bashCall && String(bashCall.input.command).includes("echo KODY_BASH_OK");
|
|
1908
|
+
const acc = bashCall ? correctCmd ? 100 : 50 : 0;
|
|
1909
|
+
return result(
|
|
1910
|
+
"tool_bash",
|
|
1911
|
+
"tool-use",
|
|
1912
|
+
acc >= 50 ? "pass" : "fail",
|
|
1913
|
+
acc,
|
|
1914
|
+
Date.now() - t,
|
|
1915
|
+
bashCall ? `Bash called: ${bashCall.input.command}` : "Bash tool was not called",
|
|
1916
|
+
{ toolSelection: bashCall ? 100 : 0 }
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
async function testImageAttachment(ctx) {
|
|
1920
|
+
const t = Date.now();
|
|
1921
|
+
const pngData = createRedPng().toString("base64");
|
|
1922
|
+
const res = await apiCall(ctx, {
|
|
1923
|
+
max_tokens: 100,
|
|
1924
|
+
temperature: 0,
|
|
1925
|
+
messages: [{
|
|
1926
|
+
role: "user",
|
|
1927
|
+
content: [
|
|
1928
|
+
{ type: "image", source: { type: "base64", media_type: "image/png", data: pngData } },
|
|
1929
|
+
{ type: "text", text: "What color is this image? Reply with just the color name." }
|
|
1930
|
+
]
|
|
1931
|
+
}]
|
|
1932
|
+
});
|
|
1933
|
+
if (!res.ok) return result(
|
|
1934
|
+
"image_attachment",
|
|
1935
|
+
"tool-use",
|
|
1936
|
+
"fail",
|
|
1937
|
+
0,
|
|
1938
|
+
Date.now() - t,
|
|
1939
|
+
`API error (model may not support vision): ${res.errorMsg?.slice(0, 80)}`
|
|
1940
|
+
);
|
|
1941
|
+
const text = extractText(res.data).toLowerCase();
|
|
1942
|
+
const mentionsRed = text.includes("red");
|
|
1943
|
+
const mentionsColor = mentionsRed || text.includes("color") || text.includes("image") || text.includes("pixel");
|
|
1944
|
+
const acc = mentionsRed ? 100 : mentionsColor ? 50 : 20;
|
|
1945
|
+
return result(
|
|
1946
|
+
"image_attachment",
|
|
1947
|
+
"tool-use",
|
|
1948
|
+
mentionsRed ? "pass" : mentionsColor ? "warn" : "fail",
|
|
1949
|
+
acc,
|
|
1950
|
+
Date.now() - t,
|
|
1951
|
+
`Response: ${text.slice(0, 80)}`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
async function testErrorRecovery(ctx) {
|
|
1955
|
+
const t = Date.now();
|
|
1956
|
+
let errorGiven = false;
|
|
1957
|
+
const conv = await runToolConversation(
|
|
1958
|
+
ctx,
|
|
1959
|
+
[TOOL_READ, TOOL_BASH],
|
|
1960
|
+
"Read the file /tmp/nonexistent-kody-file.txt and tell me what's in it. If the file doesn't exist, say so.",
|
|
1961
|
+
(name, input) => {
|
|
1962
|
+
if (name === "Read" && !errorGiven) {
|
|
1963
|
+
errorGiven = true;
|
|
1964
|
+
return "Error: ENOENT: no such file or directory";
|
|
1965
|
+
}
|
|
1966
|
+
if (name === "Bash") return "ls: /tmp/nonexistent-kody-file.txt: No such file or directory";
|
|
1967
|
+
return "Error: File not found";
|
|
1968
|
+
}
|
|
1969
|
+
);
|
|
1970
|
+
if (conv.error) return result("error_recovery", "advanced", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
1971
|
+
const reported = conv.finalText.toLowerCase().includes("not found") || conv.finalText.toLowerCase().includes("doesn't exist") || conv.finalText.toLowerCase().includes("does not exist") || conv.finalText.toLowerCase().includes("no such file");
|
|
1972
|
+
const tried = conv.toolCalls.length >= 1;
|
|
1973
|
+
const acc = reported ? tried ? 100 : 70 : 20;
|
|
1974
|
+
return result(
|
|
1975
|
+
"error_recovery",
|
|
1976
|
+
"advanced",
|
|
1977
|
+
reported ? "pass" : "warn",
|
|
1978
|
+
acc,
|
|
1979
|
+
Date.now() - t,
|
|
1980
|
+
reported ? "Gracefully reported missing file" : `Response: ${conv.finalText.slice(0, 80)}`
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
async function testToolMultiStep(ctx) {
|
|
1984
|
+
const t = Date.now();
|
|
1985
|
+
const r = runClaudeTest(
|
|
1986
|
+
ctx,
|
|
1987
|
+
"Do these steps in order: 1) Read kody.config.json 2) Tell me the value of git.defaultBranch. Reply with ONLY the branch name, nothing else."
|
|
1988
|
+
);
|
|
1989
|
+
if (!r.stdout.trim() && r.exitCode !== 0) return result(
|
|
1990
|
+
"tool_multi_step",
|
|
1991
|
+
"tool-use",
|
|
1992
|
+
"fail",
|
|
1993
|
+
0,
|
|
1994
|
+
Date.now() - t,
|
|
1995
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
1996
|
+
);
|
|
1997
|
+
const out = r.stdout.trim().toLowerCase();
|
|
1998
|
+
const correct = out.includes("main");
|
|
1999
|
+
return result(
|
|
2000
|
+
"tool_multi_step",
|
|
2001
|
+
"tool-use",
|
|
2002
|
+
correct ? "pass" : "fail",
|
|
2003
|
+
correct ? 100 : 20,
|
|
2004
|
+
Date.now() - t,
|
|
2005
|
+
correct ? "Correct: main" : `Got: ${out.slice(0, 80)}`
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
async function testPlanStage(ctx) {
|
|
2009
|
+
const t = Date.now();
|
|
2010
|
+
const wasClean = isGitClean(ctx.projectDir);
|
|
2011
|
+
const r = runClaudeTest(ctx, [
|
|
2012
|
+
"You are a planning agent. Your ONLY job is to output a markdown plan.",
|
|
2013
|
+
"CRITICAL: Do NOT use Edit, Write, or Bash tools. Do NOT modify any files. ONLY use Read, Glob, and Grep for research.",
|
|
2014
|
+
"If you modify any files, the system will crash.",
|
|
2015
|
+
"",
|
|
2016
|
+
"Task: Plan adding a /health endpoint to an Express app.",
|
|
2017
|
+
"Output a markdown plan with ## Step N sections. Each step must have File, Change, and Why fields.",
|
|
2018
|
+
"Keep it to 3 steps maximum."
|
|
2019
|
+
].join("\n"), [], 12e4);
|
|
2020
|
+
const filesModified = wasClean && !isGitClean(ctx.projectDir);
|
|
2021
|
+
if (filesModified) revertChanges(ctx.projectDir);
|
|
2022
|
+
if (!r.stdout.trim() && r.exitCode !== 0) return result(
|
|
2023
|
+
"plan_stage",
|
|
2024
|
+
"stage-simulation",
|
|
2025
|
+
"fail",
|
|
2026
|
+
0,
|
|
2027
|
+
Date.now() - t,
|
|
2028
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
2029
|
+
);
|
|
2030
|
+
const out = r.stdout;
|
|
2031
|
+
const hasStepFormat = /##\s*Step/i.test(out);
|
|
2032
|
+
const hasStructure = hasStepFormat || /\*\*File\*\*/i.test(out) && /\*\*Change\*\*/i.test(out);
|
|
2033
|
+
const boundary = filesModified ? 0 : 100;
|
|
2034
|
+
const format = hasStructure ? 100 : hasStepFormat ? 70 : out.length > 50 ? 30 : 0;
|
|
2035
|
+
const acc = Math.round(boundary * 0.6 + format * 0.4);
|
|
2036
|
+
const status = filesModified ? "fail" : hasStructure ? "pass" : "warn";
|
|
2037
|
+
return result(
|
|
2038
|
+
"plan_stage",
|
|
2039
|
+
"stage-simulation",
|
|
2040
|
+
status,
|
|
2041
|
+
acc,
|
|
2042
|
+
Date.now() - t,
|
|
2043
|
+
filesModified ? "FAIL: Model modified files during plan stage (instruction violation)" : hasStructure ? "Plan output with correct structure, no files modified" : "Output lacks expected ## Step structure",
|
|
2044
|
+
{ boundaryRespect: boundary, outputFormat: format, instructionCompliance: boundary }
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
async function testBuildStage(ctx) {
|
|
2048
|
+
const t = Date.now();
|
|
2049
|
+
const r = runClaudeTest(ctx, "Add a comment '// kody-build-test' as the very first line of src/logger.ts. That is your only task.");
|
|
2050
|
+
const diff = (() => {
|
|
2051
|
+
try {
|
|
2052
|
+
return execSync2("git diff src/logger.ts", { cwd: ctx.projectDir, encoding: "utf-8", timeout: 5e3 });
|
|
2053
|
+
} catch {
|
|
2054
|
+
return "";
|
|
2055
|
+
}
|
|
2056
|
+
})();
|
|
2057
|
+
const edited = diff.includes("kody-build-test");
|
|
2058
|
+
revertChanges(ctx.projectDir);
|
|
2059
|
+
if (!r.stdout.trim() && r.exitCode !== 0 && !edited) return result(
|
|
2060
|
+
"build_stage",
|
|
2061
|
+
"stage-simulation",
|
|
2062
|
+
"fail",
|
|
2063
|
+
0,
|
|
2064
|
+
Date.now() - t,
|
|
2065
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
2066
|
+
);
|
|
2067
|
+
return result(
|
|
2068
|
+
"build_stage",
|
|
2069
|
+
"stage-simulation",
|
|
2070
|
+
edited ? "pass" : "fail",
|
|
2071
|
+
edited ? 100 : 0,
|
|
2072
|
+
Date.now() - t,
|
|
2073
|
+
edited ? "File correctly modified with expected comment" : "File was not modified as expected"
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
async function testReviewStage(ctx) {
|
|
2077
|
+
const t = Date.now();
|
|
2078
|
+
const wasClean = isGitClean(ctx.projectDir);
|
|
2079
|
+
const r = runClaudeTest(ctx, [
|
|
2080
|
+
"You are a code review agent. Review the file src/logger.ts.",
|
|
2081
|
+
"CRITICAL: Do NOT modify any files. Only READ and analyze.",
|
|
2082
|
+
"Output your review as markdown with this exact format:",
|
|
2083
|
+
"## Summary",
|
|
2084
|
+
"<1-2 sentence summary>",
|
|
2085
|
+
"## Issues Found",
|
|
2086
|
+
"- <issues>",
|
|
2087
|
+
"## Verdict",
|
|
2088
|
+
"APPROVE or REQUEST_CHANGES"
|
|
2089
|
+
].join("\n"));
|
|
2090
|
+
const filesModified = wasClean && !isGitClean(ctx.projectDir);
|
|
2091
|
+
if (filesModified) revertChanges(ctx.projectDir);
|
|
2092
|
+
if (!r.stdout.trim() && r.exitCode !== 0) return result(
|
|
2093
|
+
"review_stage",
|
|
2094
|
+
"stage-simulation",
|
|
2095
|
+
"fail",
|
|
2096
|
+
0,
|
|
2097
|
+
Date.now() - t,
|
|
2098
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
2099
|
+
);
|
|
2100
|
+
const out = r.stdout;
|
|
2101
|
+
const hasVerdict = /verdict/i.test(out);
|
|
2102
|
+
const hasSummary = /summary/i.test(out);
|
|
2103
|
+
const boundary = filesModified ? 0 : 100;
|
|
2104
|
+
const format = (hasVerdict ? 50 : 0) + (hasSummary ? 50 : 0);
|
|
2105
|
+
const acc = Math.round(boundary * 0.5 + format * 0.5);
|
|
2106
|
+
const status = filesModified ? "fail" : hasVerdict && hasSummary ? "pass" : "warn";
|
|
2107
|
+
return result(
|
|
2108
|
+
"review_stage",
|
|
2109
|
+
"stage-simulation",
|
|
2110
|
+
status,
|
|
2111
|
+
acc,
|
|
2112
|
+
Date.now() - t,
|
|
2113
|
+
filesModified ? "FAIL: Model modified files during review (instruction violation)" : `Summary: ${hasSummary}, Verdict: ${hasVerdict}, no files modified`,
|
|
2114
|
+
{ boundaryRespect: boundary, outputFormat: format }
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
async function testMcpTools(ctx) {
|
|
2118
|
+
const t = Date.now();
|
|
2119
|
+
const mcpConfig = path11.join(os2.tmpdir(), `kody-test-mcp-${Date.now()}.json`);
|
|
2120
|
+
const testFile = path11.join(ctx.projectDir, "kody-mcp-compat-test.txt");
|
|
2121
|
+
try {
|
|
2122
|
+
fs12.writeFileSync(mcpConfig, JSON.stringify({
|
|
2123
|
+
mcpServers: {
|
|
2124
|
+
filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", ctx.projectDir] }
|
|
2125
|
+
}
|
|
2126
|
+
}));
|
|
2127
|
+
const r = runClaudeTest(
|
|
2128
|
+
ctx,
|
|
2129
|
+
`Use the MCP filesystem write_file tool to create a file at ${testFile} with the content 'mcp-ok'. Do not use the built-in Write tool.`,
|
|
2130
|
+
["--mcp-config", mcpConfig],
|
|
2131
|
+
12e4
|
|
2132
|
+
);
|
|
2133
|
+
const created = fs12.existsSync(testFile);
|
|
2134
|
+
const content = created ? fs12.readFileSync(testFile, "utf-8").trim() : "";
|
|
2135
|
+
const correct = content.includes("mcp-ok");
|
|
2136
|
+
return result(
|
|
2137
|
+
"mcp_tools",
|
|
2138
|
+
"advanced",
|
|
2139
|
+
created ? "pass" : "fail",
|
|
2140
|
+
correct ? 100 : created ? 70 : 0,
|
|
2141
|
+
Date.now() - t,
|
|
2142
|
+
created ? `File created, content: ${content.slice(0, 50)}` : `MCP test failed: ${r.stderr.slice(0, 80)}`
|
|
2143
|
+
);
|
|
2144
|
+
} catch (err) {
|
|
2145
|
+
return result("mcp_tools", "advanced", "warn", 0, Date.now() - t, `MCP test error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2146
|
+
} finally {
|
|
2147
|
+
fs12.rmSync(mcpConfig, { force: true });
|
|
2148
|
+
fs12.rmSync(testFile, { force: true });
|
|
2149
|
+
revertChanges(ctx.projectDir);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
var TOOL_READ, TOOL_EDIT, TOOL_BASH, CRC_TABLE, ALL_TESTS;
|
|
2153
|
+
var init_test_model_tests = __esm({
|
|
2154
|
+
"src/cli/test-model-tests.ts"() {
|
|
2155
|
+
"use strict";
|
|
2156
|
+
TOOL_READ = {
|
|
2157
|
+
name: "Read",
|
|
2158
|
+
description: "Read a file from the filesystem",
|
|
2159
|
+
input_schema: {
|
|
2160
|
+
type: "object",
|
|
2161
|
+
properties: { path: { type: "string", description: "Absolute file path" } },
|
|
2162
|
+
required: ["path"]
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
TOOL_EDIT = {
|
|
2166
|
+
name: "Edit",
|
|
2167
|
+
description: "Replace old_string with new_string in a file",
|
|
2168
|
+
input_schema: {
|
|
2169
|
+
type: "object",
|
|
2170
|
+
properties: {
|
|
2171
|
+
file_path: { type: "string" },
|
|
2172
|
+
old_string: { type: "string" },
|
|
2173
|
+
new_string: { type: "string" }
|
|
2174
|
+
},
|
|
2175
|
+
required: ["file_path", "old_string", "new_string"]
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
TOOL_BASH = {
|
|
2179
|
+
name: "Bash",
|
|
2180
|
+
description: "Execute a bash command and return output",
|
|
2181
|
+
input_schema: {
|
|
2182
|
+
type: "object",
|
|
2183
|
+
properties: { command: { type: "string", description: "The command to run" } },
|
|
2184
|
+
required: ["command"]
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
CRC_TABLE = new Uint32Array(256);
|
|
2188
|
+
for (let n = 0; n < 256; n++) {
|
|
2189
|
+
let c = n;
|
|
2190
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
2191
|
+
CRC_TABLE[n] = c >>> 0;
|
|
2192
|
+
}
|
|
2193
|
+
ALL_TESTS = [
|
|
2194
|
+
// Infrastructure
|
|
2195
|
+
{ name: "extended_thinking", category: "infrastructure", description: "Extended thinking parameter support", run: testExtendedThinking },
|
|
2196
|
+
// Basic
|
|
2197
|
+
{ name: "simple_prompt", category: "basic", description: "Basic text prompt and response", run: testSimplePrompt },
|
|
2198
|
+
{ name: "json_output", category: "basic", description: "JSON-only output constraint", run: testJsonOutput },
|
|
2199
|
+
{ name: "system_prompt_rules", category: "basic", description: "Multi-rule system prompt adherence", run: testSystemPromptRules },
|
|
2200
|
+
// Tool use
|
|
2201
|
+
{ name: "tool_read", category: "tool-use", description: "Read tool: file reading", run: testToolRead },
|
|
2202
|
+
{ name: "tool_edit", category: "tool-use", description: "Edit tool: old/new string replacement", run: testToolEdit },
|
|
2203
|
+
{ name: "tool_bash", category: "tool-use", description: "Bash tool: command execution", run: testToolBash },
|
|
2204
|
+
{ name: "tool_multi_step", category: "tool-use", description: "Multi-step tool chain via CLI", run: testToolMultiStep },
|
|
2205
|
+
{ name: "image_attachment", category: "tool-use", description: "Vision: image content processing", run: testImageAttachment },
|
|
2206
|
+
// Stage simulation
|
|
2207
|
+
{ name: "plan_stage", category: "stage-simulation", description: "Plan stage: read-only research + structured output", run: testPlanStage },
|
|
2208
|
+
{ name: "build_stage", category: "stage-simulation", description: "Build stage: code editing", run: testBuildStage },
|
|
2209
|
+
{ name: "review_stage", category: "stage-simulation", description: "Review stage: read-only + structured verdict", run: testReviewStage },
|
|
2210
|
+
// Advanced
|
|
2211
|
+
{ name: "mcp_tools", category: "advanced", description: "MCP server tool integration", run: testMcpTools },
|
|
2212
|
+
{ name: "error_recovery", category: "advanced", description: "Graceful error handling on tool failure", run: testErrorRecovery }
|
|
2213
|
+
];
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
// src/cli/test-model-report.ts
|
|
2218
|
+
function pad(str, len) {
|
|
2219
|
+
return str.padEnd(len);
|
|
2220
|
+
}
|
|
2221
|
+
function fmtDuration(ms) {
|
|
2222
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
2223
|
+
}
|
|
2224
|
+
function formatReport(report) {
|
|
2225
|
+
const W = 74;
|
|
2226
|
+
const lines = [];
|
|
2227
|
+
lines.push("=".repeat(W));
|
|
2228
|
+
lines.push("");
|
|
2229
|
+
lines.push(" Model Compatibility Report");
|
|
2230
|
+
lines.push(` Provider: ${report.provider} | Model: ${report.model}`);
|
|
2231
|
+
lines.push(` Date: ${report.timestamp}`);
|
|
2232
|
+
lines.push(` Duration: ${fmtDuration(report.totalDurationMs)}`);
|
|
2233
|
+
lines.push("");
|
|
2234
|
+
lines.push("-".repeat(W));
|
|
2235
|
+
for (const cat of CATEGORY_ORDER) {
|
|
2236
|
+
const catResults = report.results.filter((r) => r.category === cat);
|
|
2237
|
+
if (catResults.length === 0) continue;
|
|
2238
|
+
lines.push("");
|
|
2239
|
+
lines.push(` ${CATEGORY_LABELS[cat]}`);
|
|
2240
|
+
lines.push("");
|
|
2241
|
+
for (const r of catResults) {
|
|
2242
|
+
const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
|
|
2243
|
+
const name = pad(r.name, 28);
|
|
2244
|
+
const status = pad(r.status.toUpperCase(), 6);
|
|
2245
|
+
const acc = pad(`${r.accuracy}%`, 5);
|
|
2246
|
+
const dur = fmtDuration(r.durationMs);
|
|
2247
|
+
lines.push(` [${icon}] ${name} ${status} ${acc} ${dur}`);
|
|
2248
|
+
if (r.status !== "pass" && r.detail) {
|
|
2249
|
+
lines.push(` ${r.detail.slice(0, W - 8)}`);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
const passed = report.results.filter((r) => r.status === "pass").length;
|
|
2254
|
+
const failed = report.results.filter((r) => r.status === "fail").length;
|
|
2255
|
+
const warned = report.results.filter((r) => r.status === "warn").length;
|
|
2256
|
+
const total = report.results.length;
|
|
2257
|
+
const avgAccuracy = total > 0 ? Math.round(report.results.reduce((s, r) => s + r.accuracy, 0) / total) : 0;
|
|
2258
|
+
lines.push("");
|
|
2259
|
+
lines.push("-".repeat(W));
|
|
2260
|
+
lines.push("");
|
|
2261
|
+
lines.push(` RESULTS: ${passed}/${total} PASS | ${failed} FAIL | ${warned} WARN`);
|
|
2262
|
+
lines.push(` OVERALL ACCURACY: ${avgAccuracy}%`);
|
|
2263
|
+
lines.push(` drop_params required: ${report.dropParamsRequired ? "YES" : "NO"}`);
|
|
2264
|
+
lines.push("");
|
|
2265
|
+
lines.push(" ACCURACY BY CATEGORY:");
|
|
2266
|
+
for (const cat of CATEGORY_ORDER) {
|
|
2267
|
+
const cr = report.results.filter((r) => r.category === cat);
|
|
2268
|
+
if (cr.length === 0) continue;
|
|
2269
|
+
const avg = Math.round(cr.reduce((s, r) => s + r.accuracy, 0) / cr.length);
|
|
2270
|
+
lines.push(` ${pad(CATEGORY_LABELS[cat], 22)} ${avg}%`);
|
|
2271
|
+
}
|
|
2272
|
+
lines.push("");
|
|
2273
|
+
lines.push(" RECOMMENDATION:");
|
|
2274
|
+
for (const line of getRecommendation(report)) {
|
|
2275
|
+
lines.push(` ${line}`);
|
|
2276
|
+
}
|
|
2277
|
+
lines.push("");
|
|
2278
|
+
lines.push("=".repeat(W));
|
|
2279
|
+
return lines.join("\n");
|
|
2280
|
+
}
|
|
2281
|
+
function getRecommendation(report) {
|
|
2282
|
+
const lines = [];
|
|
2283
|
+
const failedTests = report.results.filter((r) => r.status === "fail");
|
|
2284
|
+
const avg = report.results.length > 0 ? Math.round(report.results.reduce((s, r) => s + r.accuracy, 0) / report.results.length) : 0;
|
|
2285
|
+
if (avg >= 90 && failedTests.length === 0) {
|
|
2286
|
+
lines.push("[+] Fully compatible -- suitable for all pipeline stages");
|
|
2287
|
+
return lines;
|
|
2288
|
+
}
|
|
2289
|
+
const stageResults = report.results.filter((r) => r.category === "stage-simulation");
|
|
2290
|
+
const workingStages = stageResults.filter((r) => r.status === "pass").map((r) => r.name.replace("_stage", ""));
|
|
2291
|
+
const failingStages = stageResults.filter((r) => r.status !== "pass").map((r) => r.name.replace("_stage", ""));
|
|
2292
|
+
if (workingStages.length > 0) {
|
|
2293
|
+
lines.push(`[+] Suitable for: ${workingStages.join(", ")} stages`);
|
|
2294
|
+
}
|
|
2295
|
+
if (failingStages.length > 0) {
|
|
2296
|
+
lines.push(`[x] Not recommended for: ${failingStages.join(", ")} stages`);
|
|
2297
|
+
}
|
|
2298
|
+
if (failedTests.length > 0) {
|
|
2299
|
+
lines.push("");
|
|
2300
|
+
lines.push("Failed tests:");
|
|
2301
|
+
for (const t of failedTests) {
|
|
2302
|
+
lines.push(` - ${t.name}: ${t.detail.slice(0, 60)}`);
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
return lines;
|
|
2306
|
+
}
|
|
2307
|
+
var CATEGORY_ORDER, CATEGORY_LABELS;
|
|
2308
|
+
var init_test_model_report = __esm({
|
|
2309
|
+
"src/cli/test-model-report.ts"() {
|
|
2310
|
+
"use strict";
|
|
2311
|
+
CATEGORY_ORDER = ["infrastructure", "basic", "tool-use", "stage-simulation", "advanced"];
|
|
2312
|
+
CATEGORY_LABELS = {
|
|
2313
|
+
infrastructure: "INFRASTRUCTURE",
|
|
2314
|
+
basic: "BASIC CAPABILITIES",
|
|
2315
|
+
"tool-use": "TOOL USE",
|
|
2316
|
+
"stage-simulation": "STAGE SIMULATION",
|
|
2317
|
+
advanced: "ADVANCED"
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
// src/cli/test-model-command.ts
|
|
2323
|
+
var test_model_command_exports = {};
|
|
2324
|
+
__export(test_model_command_exports, {
|
|
2325
|
+
runTestModelCommand: () => runTestModelCommand
|
|
2326
|
+
});
|
|
2327
|
+
import * as fs13 from "fs";
|
|
2328
|
+
import * as os3 from "os";
|
|
2329
|
+
import * as path12 from "path";
|
|
2330
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
2331
|
+
function parseTestModelArgs() {
|
|
2332
|
+
const args2 = process.argv.slice(3);
|
|
2333
|
+
function getArg3(flag) {
|
|
2334
|
+
const idx = args2.indexOf(flag);
|
|
2335
|
+
if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) return args2[idx + 1];
|
|
2336
|
+
return void 0;
|
|
2337
|
+
}
|
|
2338
|
+
const hasFlag3 = (f) => args2.includes(f);
|
|
2339
|
+
if (hasFlag3("--help") || hasFlag3("-h")) {
|
|
2340
|
+
logger.info([
|
|
2341
|
+
"Usage: kody test-model --provider <provider> --model <model> --key <api-key> [options]",
|
|
2342
|
+
"",
|
|
2343
|
+
"Options:",
|
|
2344
|
+
" --provider LLM provider name (e.g. gemini, openai, mistral)",
|
|
2345
|
+
" --model Model identifier (e.g. gemini-2.5-flash)",
|
|
2346
|
+
" --key API key for the provider",
|
|
2347
|
+
" --key-env Read API key from this environment variable",
|
|
2348
|
+
" --skip-proxy Use an already-running LiteLLM proxy (don't start one)",
|
|
2349
|
+
" --litellm-url LiteLLM proxy URL (default: http://localhost:4099)",
|
|
2350
|
+
" --filter Comma-separated test names to run (default: all)",
|
|
2351
|
+
" --list List all available tests and exit"
|
|
2352
|
+
].join("\n"));
|
|
2353
|
+
process.exit(0);
|
|
2354
|
+
}
|
|
2355
|
+
if (hasFlag3("--list")) {
|
|
2356
|
+
for (const t of ALL_TESTS) {
|
|
2357
|
+
logger.info(` ${t.name.padEnd(24)} [${t.category}] ${t.description}`);
|
|
2358
|
+
}
|
|
2359
|
+
process.exit(0);
|
|
2360
|
+
}
|
|
2361
|
+
const provider = getArg3("--provider");
|
|
2362
|
+
const model = getArg3("--model");
|
|
2363
|
+
const key = getArg3("--key");
|
|
2364
|
+
const keyEnv = getArg3("--key-env");
|
|
2365
|
+
if (!provider || !model) {
|
|
2366
|
+
logger.error("Required: --provider <provider> --model <model> --key <key>");
|
|
2367
|
+
logger.error("Run with --help for usage.");
|
|
2368
|
+
process.exit(1);
|
|
2369
|
+
}
|
|
2370
|
+
let apiKey = key;
|
|
2371
|
+
if (!apiKey && keyEnv) apiKey = process.env[keyEnv];
|
|
2372
|
+
if (!apiKey) {
|
|
2373
|
+
logger.error("API key required: use --key <value> or --key-env <ENV_VAR>");
|
|
2374
|
+
process.exit(1);
|
|
2375
|
+
}
|
|
2376
|
+
return {
|
|
2377
|
+
provider,
|
|
2378
|
+
model,
|
|
2379
|
+
apiKey,
|
|
2380
|
+
proxyUrl: getArg3("--litellm-url") ?? TEST_URL,
|
|
2381
|
+
skipProxy: hasFlag3("--skip-proxy"),
|
|
2382
|
+
filter: getArg3("--filter")?.split(",")
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
function generateConfig(provider, model, dropParams) {
|
|
2386
|
+
const lines = [];
|
|
2387
|
+
if (dropParams) {
|
|
2388
|
+
lines.push("litellm_settings:");
|
|
2389
|
+
lines.push(" drop_params: true");
|
|
2390
|
+
lines.push("");
|
|
2391
|
+
}
|
|
2392
|
+
lines.push("model_list:");
|
|
2393
|
+
lines.push(` - model_name: ${model}`);
|
|
2394
|
+
lines.push(" litellm_params:");
|
|
2395
|
+
lines.push(` model: ${provider}/${model}`);
|
|
2396
|
+
lines.push(" api_key: os.environ/ANTHROPIC_COMPATIBLE_API_KEY");
|
|
2397
|
+
return lines.join("\n") + "\n";
|
|
2398
|
+
}
|
|
2399
|
+
async function startProxy(config, url) {
|
|
2400
|
+
try {
|
|
2401
|
+
execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
2402
|
+
} catch {
|
|
2403
|
+
try {
|
|
2404
|
+
execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
2405
|
+
} catch {
|
|
2406
|
+
logger.error("litellm not installed. Install: pip install 'litellm[proxy]'");
|
|
2407
|
+
return null;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
fs13.writeFileSync(CONFIG_PATH, config);
|
|
2411
|
+
const portMatch = url.match(/:(\d+)/);
|
|
2412
|
+
const port = portMatch ? portMatch[1] : "4099";
|
|
2413
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
2414
|
+
const child = spawn2("litellm", ["--config", CONFIG_PATH, "--port", port], {
|
|
2415
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2416
|
+
detached: true,
|
|
2417
|
+
env: process.env
|
|
2418
|
+
});
|
|
2419
|
+
for (let i = 0; i < 30; i++) {
|
|
2420
|
+
await delay(2e3);
|
|
2421
|
+
if (await checkLitellmHealth(url)) {
|
|
2422
|
+
logger.info(`LiteLLM proxy ready at ${url}`);
|
|
2423
|
+
return child;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
child.kill();
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
async function quickApiTest(url, model, apiKey) {
|
|
2430
|
+
try {
|
|
2431
|
+
const res = await fetch(`${url}/v1/messages`, {
|
|
2432
|
+
method: "POST",
|
|
2433
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
2434
|
+
body: JSON.stringify({
|
|
2435
|
+
model,
|
|
2436
|
+
max_tokens: 10,
|
|
2437
|
+
messages: [{ role: "user", content: "Say ok" }],
|
|
2438
|
+
context_management: { policy: "smart" }
|
|
2439
|
+
}),
|
|
2440
|
+
signal: AbortSignal.timeout(3e4)
|
|
2441
|
+
});
|
|
2442
|
+
if (!res.ok) {
|
|
2443
|
+
const body = await res.text();
|
|
2444
|
+
return { ok: false, error: body.slice(0, 200) };
|
|
2445
|
+
}
|
|
2446
|
+
return { ok: true };
|
|
2447
|
+
} catch (err) {
|
|
2448
|
+
return { ok: false, error: String(err) };
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function delay(ms) {
|
|
2452
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2453
|
+
}
|
|
2454
|
+
async function runTestModelCommand() {
|
|
2455
|
+
const opts = parseTestModelArgs();
|
|
2456
|
+
const startTime = Date.now();
|
|
2457
|
+
logger.info(`Testing model compatibility: ${opts.provider}/${opts.model}`);
|
|
2458
|
+
logger.info("");
|
|
2459
|
+
let proxyProcess = null;
|
|
2460
|
+
let dropParamsRequired = false;
|
|
2461
|
+
const cleanup = () => {
|
|
2462
|
+
if (proxyProcess) {
|
|
2463
|
+
proxyProcess.kill();
|
|
2464
|
+
proxyProcess = null;
|
|
2465
|
+
}
|
|
2466
|
+
fs13.rmSync(CONFIG_PATH, { force: true });
|
|
2467
|
+
};
|
|
2468
|
+
process.on("SIGINT", () => {
|
|
2469
|
+
cleanup();
|
|
2470
|
+
process.exit(1);
|
|
2471
|
+
});
|
|
2472
|
+
process.on("SIGTERM", () => {
|
|
2473
|
+
cleanup();
|
|
2474
|
+
process.exit(1);
|
|
2475
|
+
});
|
|
2476
|
+
try {
|
|
2477
|
+
if (!opts.skipProxy) {
|
|
2478
|
+
process.env.ANTHROPIC_COMPATIBLE_API_KEY = opts.apiKey;
|
|
2479
|
+
logger.info("Starting LiteLLM proxy (without drop_params)...");
|
|
2480
|
+
proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, false), opts.proxyUrl);
|
|
2481
|
+
if (!proxyProcess) {
|
|
2482
|
+
logger.error("Failed to start LiteLLM proxy");
|
|
2483
|
+
process.exit(1);
|
|
2484
|
+
}
|
|
2485
|
+
const quickRes = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
|
|
2486
|
+
if (!quickRes.ok) {
|
|
2487
|
+
logger.info("Model needs drop_params: true -- restarting proxy...");
|
|
2488
|
+
proxyProcess.kill();
|
|
2489
|
+
proxyProcess = null;
|
|
2490
|
+
await delay(2e3);
|
|
2491
|
+
proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, true), opts.proxyUrl);
|
|
2492
|
+
dropParamsRequired = true;
|
|
2493
|
+
if (!proxyProcess) {
|
|
2494
|
+
logger.error("Failed to start LiteLLM proxy with drop_params");
|
|
2495
|
+
process.exit(1);
|
|
2496
|
+
}
|
|
2497
|
+
const retry = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
|
|
2498
|
+
if (!retry.ok) {
|
|
2499
|
+
logger.error(`Model not accessible: ${retry.error}`);
|
|
2500
|
+
process.exit(1);
|
|
2501
|
+
}
|
|
2502
|
+
logger.info("Proxy restarted with drop_params: true");
|
|
2503
|
+
} else {
|
|
2504
|
+
logger.info("drop_params not required");
|
|
2505
|
+
}
|
|
2506
|
+
} else {
|
|
2507
|
+
logger.info(`Using existing proxy at ${opts.proxyUrl}`);
|
|
2508
|
+
}
|
|
2509
|
+
const tests = opts.filter ? ALL_TESTS.filter((t) => opts.filter.includes(t.name)) : ALL_TESTS;
|
|
2510
|
+
logger.info(`Running ${tests.length} compatibility tests...`);
|
|
2511
|
+
logger.info("");
|
|
2512
|
+
const ctx = { proxyUrl: opts.proxyUrl, model: opts.model, apiKey: opts.apiKey, projectDir: process.cwd() };
|
|
2513
|
+
const results = [];
|
|
2514
|
+
for (const test of tests) {
|
|
2515
|
+
process.stdout.write(` ${test.name.padEnd(28)} `);
|
|
2516
|
+
try {
|
|
2517
|
+
const r = await test.run(ctx);
|
|
2518
|
+
results.push(r);
|
|
2519
|
+
const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
|
|
2520
|
+
logger.info(`[${icon}] ${r.status.toUpperCase()} ${r.accuracy}% (${(r.durationMs / 1e3).toFixed(1)}s)`);
|
|
2521
|
+
} catch (err) {
|
|
2522
|
+
const r = {
|
|
2523
|
+
name: test.name,
|
|
2524
|
+
category: test.category,
|
|
2525
|
+
status: "fail",
|
|
2526
|
+
accuracy: 0,
|
|
2527
|
+
durationMs: 0,
|
|
2528
|
+
detail: `Crash: ${err instanceof Error ? err.message : String(err)}`
|
|
2529
|
+
};
|
|
2530
|
+
results.push(r);
|
|
2531
|
+
logger.info("[x] CRASH");
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
const report = {
|
|
2535
|
+
provider: opts.provider,
|
|
2536
|
+
model: opts.model,
|
|
2537
|
+
results,
|
|
2538
|
+
totalDurationMs: Date.now() - startTime,
|
|
2539
|
+
dropParamsRequired,
|
|
2540
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
|
|
2541
|
+
};
|
|
2542
|
+
console.log("");
|
|
2543
|
+
console.log(formatReport(report));
|
|
2544
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
2545
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
2546
|
+
} finally {
|
|
2547
|
+
cleanup();
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
var TEST_PORT, TEST_URL, CONFIG_PATH;
|
|
2551
|
+
var init_test_model_command = __esm({
|
|
2552
|
+
"src/cli/test-model-command.ts"() {
|
|
2553
|
+
"use strict";
|
|
2554
|
+
init_logger();
|
|
2555
|
+
init_litellm();
|
|
2556
|
+
init_test_model_tests();
|
|
2557
|
+
init_test_model_report();
|
|
2558
|
+
TEST_PORT = 4099;
|
|
2559
|
+
TEST_URL = `http://localhost:${TEST_PORT}`;
|
|
2560
|
+
CONFIG_PATH = path12.join(os3.tmpdir(), "kody-test-model-config.yaml");
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2563
|
+
|
|
1571
2564
|
// src/ci/parse-inputs.ts
|
|
1572
2565
|
var parse_inputs_exports = {};
|
|
1573
2566
|
__export(parse_inputs_exports, {
|
|
@@ -1575,16 +2568,16 @@ __export(parse_inputs_exports, {
|
|
|
1575
2568
|
runCiParse: () => runCiParse,
|
|
1576
2569
|
writeOutputs: () => writeOutputs
|
|
1577
2570
|
});
|
|
1578
|
-
import * as
|
|
2571
|
+
import * as fs14 from "fs";
|
|
1579
2572
|
function generateTimestamp() {
|
|
1580
2573
|
const now = /* @__PURE__ */ new Date();
|
|
1581
|
-
const
|
|
2574
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
1582
2575
|
const y = String(now.getFullYear()).slice(2);
|
|
1583
|
-
const m =
|
|
1584
|
-
const d =
|
|
1585
|
-
const H =
|
|
1586
|
-
const M =
|
|
1587
|
-
const S =
|
|
2576
|
+
const m = pad2(now.getMonth() + 1);
|
|
2577
|
+
const d = pad2(now.getDate());
|
|
2578
|
+
const H = pad2(now.getHours());
|
|
2579
|
+
const M = pad2(now.getMinutes());
|
|
2580
|
+
const S = pad2(now.getSeconds());
|
|
1588
2581
|
return `${y}${m}${d}-${H}${M}${S}`;
|
|
1589
2582
|
}
|
|
1590
2583
|
function parseCommentInputs() {
|
|
@@ -1736,40 +2729,40 @@ function parseCommentInputs() {
|
|
|
1736
2729
|
trigger_type: "comment"
|
|
1737
2730
|
};
|
|
1738
2731
|
}
|
|
1739
|
-
function writeOutputs(
|
|
2732
|
+
function writeOutputs(result2) {
|
|
1740
2733
|
const outputFile = process.env.GITHUB_OUTPUT;
|
|
1741
2734
|
function output(key, value) {
|
|
1742
2735
|
if (outputFile) {
|
|
1743
2736
|
if (value.includes("\n")) {
|
|
1744
|
-
|
|
2737
|
+
fs14.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
1745
2738
|
${value}
|
|
1746
2739
|
KODY_EOF
|
|
1747
2740
|
`);
|
|
1748
2741
|
} else {
|
|
1749
|
-
|
|
2742
|
+
fs14.appendFileSync(outputFile, `${key}=${value}
|
|
1750
2743
|
`);
|
|
1751
2744
|
}
|
|
1752
2745
|
}
|
|
1753
2746
|
const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
|
|
1754
2747
|
console.log(`${key}=${display}`);
|
|
1755
2748
|
}
|
|
1756
|
-
output("task_id",
|
|
1757
|
-
output("mode",
|
|
1758
|
-
output("from_stage",
|
|
1759
|
-
output("issue_number",
|
|
1760
|
-
output("pr_number",
|
|
1761
|
-
output("feedback",
|
|
1762
|
-
output("complexity",
|
|
1763
|
-
output("ci_run_id",
|
|
1764
|
-
output("ticket_id",
|
|
1765
|
-
output("prd_file",
|
|
1766
|
-
output("dry_run",
|
|
1767
|
-
output("valid",
|
|
1768
|
-
output("trigger_type",
|
|
2749
|
+
output("task_id", result2.task_id);
|
|
2750
|
+
output("mode", result2.mode);
|
|
2751
|
+
output("from_stage", result2.from_stage);
|
|
2752
|
+
output("issue_number", result2.issue_number);
|
|
2753
|
+
output("pr_number", result2.pr_number);
|
|
2754
|
+
output("feedback", result2.feedback);
|
|
2755
|
+
output("complexity", result2.complexity);
|
|
2756
|
+
output("ci_run_id", result2.ci_run_id);
|
|
2757
|
+
output("ticket_id", result2.ticket_id);
|
|
2758
|
+
output("prd_file", result2.prd_file);
|
|
2759
|
+
output("dry_run", result2.dry_run ? "true" : "false");
|
|
2760
|
+
output("valid", result2.valid ? "true" : "false");
|
|
2761
|
+
output("trigger_type", result2.trigger_type);
|
|
1769
2762
|
}
|
|
1770
2763
|
function runCiParse() {
|
|
1771
|
-
const
|
|
1772
|
-
writeOutputs(
|
|
2764
|
+
const result2 = parseCommentInputs();
|
|
2765
|
+
writeOutputs(result2);
|
|
1773
2766
|
}
|
|
1774
2767
|
var VALID_MODES;
|
|
1775
2768
|
var init_parse_inputs = __esm({
|
|
@@ -1871,7 +2864,7 @@ var init_definitions = __esm({
|
|
|
1871
2864
|
});
|
|
1872
2865
|
|
|
1873
2866
|
// src/git-utils.ts
|
|
1874
|
-
import { execFileSync as
|
|
2867
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
1875
2868
|
function getHookSafeEnv() {
|
|
1876
2869
|
if (!_hookSafeEnv) {
|
|
1877
2870
|
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
@@ -1879,7 +2872,7 @@ function getHookSafeEnv() {
|
|
|
1879
2872
|
return _hookSafeEnv;
|
|
1880
2873
|
}
|
|
1881
2874
|
function git(args2, options) {
|
|
1882
|
-
return
|
|
2875
|
+
return execFileSync11("git", args2, {
|
|
1883
2876
|
encoding: "utf-8",
|
|
1884
2877
|
timeout: options?.timeout ?? 3e4,
|
|
1885
2878
|
cwd: options?.cwd,
|
|
@@ -2065,22 +3058,22 @@ var init_git_utils = __esm({
|
|
|
2065
3058
|
});
|
|
2066
3059
|
|
|
2067
3060
|
// src/pipeline/state.ts
|
|
2068
|
-
import * as
|
|
2069
|
-
import * as
|
|
3061
|
+
import * as fs15 from "fs";
|
|
3062
|
+
import * as path13 from "path";
|
|
2070
3063
|
function loadState(taskId, taskDir) {
|
|
2071
|
-
const p =
|
|
2072
|
-
if (!
|
|
3064
|
+
const p = path13.join(taskDir, "status.json");
|
|
3065
|
+
if (!fs15.existsSync(p)) return null;
|
|
2073
3066
|
try {
|
|
2074
|
-
const
|
|
2075
|
-
|
|
3067
|
+
const result2 = parseJsonSafe(
|
|
3068
|
+
fs15.readFileSync(p, "utf-8"),
|
|
2076
3069
|
["taskId", "state", "stages", "createdAt", "updatedAt"]
|
|
2077
3070
|
);
|
|
2078
|
-
if (!
|
|
2079
|
-
logger.warn(` Corrupt status.json: ${
|
|
3071
|
+
if (!result2.ok) {
|
|
3072
|
+
logger.warn(` Corrupt status.json: ${result2.error}`);
|
|
2080
3073
|
return null;
|
|
2081
3074
|
}
|
|
2082
|
-
if (
|
|
2083
|
-
return
|
|
3075
|
+
if (result2.data.taskId !== taskId) return null;
|
|
3076
|
+
return result2.data;
|
|
2084
3077
|
} catch {
|
|
2085
3078
|
return null;
|
|
2086
3079
|
}
|
|
@@ -2090,10 +3083,10 @@ function writeState(state, taskDir) {
|
|
|
2090
3083
|
...state,
|
|
2091
3084
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2092
3085
|
};
|
|
2093
|
-
const target =
|
|
3086
|
+
const target = path13.join(taskDir, "status.json");
|
|
2094
3087
|
const tmp = target + ".tmp";
|
|
2095
|
-
|
|
2096
|
-
|
|
3088
|
+
fs15.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
3089
|
+
fs15.renameSync(tmp, target);
|
|
2097
3090
|
return updated;
|
|
2098
3091
|
}
|
|
2099
3092
|
function initState(taskId) {
|
|
@@ -2134,16 +3127,16 @@ var init_complexity = __esm({
|
|
|
2134
3127
|
});
|
|
2135
3128
|
|
|
2136
3129
|
// src/memory.ts
|
|
2137
|
-
import * as
|
|
2138
|
-
import * as
|
|
3130
|
+
import * as fs16 from "fs";
|
|
3131
|
+
import * as path14 from "path";
|
|
2139
3132
|
function readProjectMemory(projectDir) {
|
|
2140
|
-
const memoryDir =
|
|
2141
|
-
if (!
|
|
2142
|
-
const files =
|
|
3133
|
+
const memoryDir = path14.join(projectDir, ".kody", "memory");
|
|
3134
|
+
if (!fs16.existsSync(memoryDir)) return "";
|
|
3135
|
+
const files = fs16.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
2143
3136
|
if (files.length === 0) return "";
|
|
2144
3137
|
const sections = [];
|
|
2145
3138
|
for (const file of files) {
|
|
2146
|
-
const content =
|
|
3139
|
+
const content = fs16.readFileSync(path14.join(memoryDir, file), "utf-8").trim();
|
|
2147
3140
|
if (content) {
|
|
2148
3141
|
sections.push(`## ${file.replace(".md", "")}
|
|
2149
3142
|
${content}`);
|
|
@@ -2162,8 +3155,8 @@ var init_memory = __esm({
|
|
|
2162
3155
|
});
|
|
2163
3156
|
|
|
2164
3157
|
// src/context-tiers.ts
|
|
2165
|
-
import * as
|
|
2166
|
-
import * as
|
|
3158
|
+
import * as fs17 from "fs";
|
|
3159
|
+
import * as path15 from "path";
|
|
2167
3160
|
function estimateTokens(text) {
|
|
2168
3161
|
return Math.ceil(text.length / 4);
|
|
2169
3162
|
}
|
|
@@ -2190,8 +3183,8 @@ function generateL0(content, filename) {
|
|
|
2190
3183
|
break;
|
|
2191
3184
|
}
|
|
2192
3185
|
}
|
|
2193
|
-
const
|
|
2194
|
-
return
|
|
3186
|
+
const result2 = parts.join("\n");
|
|
3187
|
+
return result2.slice(0, L0_MAX_CHARS);
|
|
2195
3188
|
}
|
|
2196
3189
|
function generateL0Json(content) {
|
|
2197
3190
|
try {
|
|
@@ -2233,8 +3226,8 @@ function generateL1(content, filename) {
|
|
|
2233
3226
|
inSection = false;
|
|
2234
3227
|
}
|
|
2235
3228
|
}
|
|
2236
|
-
const
|
|
2237
|
-
return
|
|
3229
|
+
const result2 = parts.join("\n");
|
|
3230
|
+
return result2.slice(0, L1_MAX_CHARS);
|
|
2238
3231
|
}
|
|
2239
3232
|
function generateL1Json(content) {
|
|
2240
3233
|
try {
|
|
@@ -2254,7 +3247,7 @@ function generateL1Json(content) {
|
|
|
2254
3247
|
}
|
|
2255
3248
|
}
|
|
2256
3249
|
function getTieredContent(filePath, content) {
|
|
2257
|
-
const key =
|
|
3250
|
+
const key = path15.basename(filePath);
|
|
2258
3251
|
return {
|
|
2259
3252
|
source: filePath,
|
|
2260
3253
|
L0: generateL0(content, key),
|
|
@@ -2266,15 +3259,15 @@ function selectTier(tiered, tier) {
|
|
|
2266
3259
|
return tiered[tier];
|
|
2267
3260
|
}
|
|
2268
3261
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
2269
|
-
const memoryDir =
|
|
2270
|
-
if (!
|
|
2271
|
-
const files =
|
|
3262
|
+
const memoryDir = path15.join(projectDir, ".kody", "memory");
|
|
3263
|
+
if (!fs17.existsSync(memoryDir)) return "";
|
|
3264
|
+
const files = fs17.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
2272
3265
|
if (files.length === 0) return "";
|
|
2273
3266
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
2274
3267
|
const sections = [];
|
|
2275
3268
|
for (const file of files) {
|
|
2276
|
-
const filePath =
|
|
2277
|
-
const content =
|
|
3269
|
+
const filePath = path15.join(memoryDir, file);
|
|
3270
|
+
const content = fs17.readFileSync(filePath, "utf-8").trim();
|
|
2278
3271
|
if (!content) continue;
|
|
2279
3272
|
const tiered = getTieredContent(filePath, content);
|
|
2280
3273
|
const selected = selectTier(tiered, tier);
|
|
@@ -2297,9 +3290,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
2297
3290
|
`;
|
|
2298
3291
|
context += `Task Directory: ${taskDir}
|
|
2299
3292
|
`;
|
|
2300
|
-
const taskMdPath =
|
|
2301
|
-
if (
|
|
2302
|
-
const content =
|
|
3293
|
+
const taskMdPath = path15.join(taskDir, "task.md");
|
|
3294
|
+
if (fs17.existsSync(taskMdPath)) {
|
|
3295
|
+
const content = fs17.readFileSync(taskMdPath, "utf-8");
|
|
2303
3296
|
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
2304
3297
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
2305
3298
|
context += `
|
|
@@ -2307,9 +3300,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
2307
3300
|
${selected}
|
|
2308
3301
|
`;
|
|
2309
3302
|
}
|
|
2310
|
-
const taskJsonPath =
|
|
2311
|
-
if (
|
|
2312
|
-
const content =
|
|
3303
|
+
const taskJsonPath = path15.join(taskDir, "task.json");
|
|
3304
|
+
if (fs17.existsSync(taskJsonPath)) {
|
|
3305
|
+
const content = fs17.readFileSync(taskJsonPath, "utf-8");
|
|
2313
3306
|
if (policy.taskClassification === "L2") {
|
|
2314
3307
|
try {
|
|
2315
3308
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -2335,9 +3328,9 @@ ${selected}
|
|
|
2335
3328
|
}
|
|
2336
3329
|
}
|
|
2337
3330
|
}
|
|
2338
|
-
const specPath =
|
|
2339
|
-
if (
|
|
2340
|
-
const content =
|
|
3331
|
+
const specPath = path15.join(taskDir, "spec.md");
|
|
3332
|
+
if (fs17.existsSync(specPath)) {
|
|
3333
|
+
const content = fs17.readFileSync(specPath, "utf-8");
|
|
2341
3334
|
const selected = selectContent(specPath, content, policy.spec);
|
|
2342
3335
|
const label = tierLabel("Spec", policy.spec);
|
|
2343
3336
|
context += `
|
|
@@ -2345,9 +3338,9 @@ ${selected}
|
|
|
2345
3338
|
${selected}
|
|
2346
3339
|
`;
|
|
2347
3340
|
}
|
|
2348
|
-
const planPath =
|
|
2349
|
-
if (
|
|
2350
|
-
const content =
|
|
3341
|
+
const planPath = path15.join(taskDir, "plan.md");
|
|
3342
|
+
if (fs17.existsSync(planPath)) {
|
|
3343
|
+
const content = fs17.readFileSync(planPath, "utf-8");
|
|
2351
3344
|
const selected = selectContent(planPath, content, policy.plan);
|
|
2352
3345
|
const label = tierLabel("Plan", policy.plan);
|
|
2353
3346
|
context += `
|
|
@@ -2355,9 +3348,9 @@ ${selected}
|
|
|
2355
3348
|
${selected}
|
|
2356
3349
|
`;
|
|
2357
3350
|
}
|
|
2358
|
-
const contextMdPath =
|
|
2359
|
-
if (
|
|
2360
|
-
const content =
|
|
3351
|
+
const contextMdPath = path15.join(taskDir, "context.md");
|
|
3352
|
+
if (fs17.existsSync(contextMdPath)) {
|
|
3353
|
+
const content = fs17.readFileSync(contextMdPath, "utf-8");
|
|
2361
3354
|
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
2362
3355
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
2363
3356
|
context += `
|
|
@@ -2443,24 +3436,24 @@ var init_context_tiers = __esm({
|
|
|
2443
3436
|
});
|
|
2444
3437
|
|
|
2445
3438
|
// src/context.ts
|
|
2446
|
-
import * as
|
|
2447
|
-
import * as
|
|
3439
|
+
import * as fs18 from "fs";
|
|
3440
|
+
import * as path16 from "path";
|
|
2448
3441
|
function readPromptFile(stageName, projectDir) {
|
|
2449
3442
|
if (projectDir) {
|
|
2450
|
-
const stepFile =
|
|
2451
|
-
if (
|
|
2452
|
-
return
|
|
3443
|
+
const stepFile = path16.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
3444
|
+
if (fs18.existsSync(stepFile)) {
|
|
3445
|
+
return fs18.readFileSync(stepFile, "utf-8");
|
|
2453
3446
|
}
|
|
2454
3447
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
2455
3448
|
}
|
|
2456
3449
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
2457
3450
|
const candidates = [
|
|
2458
|
-
|
|
2459
|
-
|
|
3451
|
+
path16.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
3452
|
+
path16.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
2460
3453
|
];
|
|
2461
3454
|
for (const candidate of candidates) {
|
|
2462
|
-
if (
|
|
2463
|
-
return
|
|
3455
|
+
if (fs18.existsSync(candidate)) {
|
|
3456
|
+
return fs18.readFileSync(candidate, "utf-8");
|
|
2464
3457
|
}
|
|
2465
3458
|
}
|
|
2466
3459
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -2472,18 +3465,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
2472
3465
|
`;
|
|
2473
3466
|
context += `Task Directory: ${taskDir}
|
|
2474
3467
|
`;
|
|
2475
|
-
const taskMdPath =
|
|
2476
|
-
if (
|
|
2477
|
-
const taskMd =
|
|
3468
|
+
const taskMdPath = path16.join(taskDir, "task.md");
|
|
3469
|
+
if (fs18.existsSync(taskMdPath)) {
|
|
3470
|
+
const taskMd = fs18.readFileSync(taskMdPath, "utf-8");
|
|
2478
3471
|
context += `
|
|
2479
3472
|
## Task Description
|
|
2480
3473
|
${taskMd}
|
|
2481
3474
|
`;
|
|
2482
3475
|
}
|
|
2483
|
-
const taskJsonPath =
|
|
2484
|
-
if (
|
|
3476
|
+
const taskJsonPath = path16.join(taskDir, "task.json");
|
|
3477
|
+
if (fs18.existsSync(taskJsonPath)) {
|
|
2485
3478
|
try {
|
|
2486
|
-
const taskDef = JSON.parse(
|
|
3479
|
+
const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
|
|
2487
3480
|
context += `
|
|
2488
3481
|
## Task Classification
|
|
2489
3482
|
`;
|
|
@@ -2496,27 +3489,27 @@ ${taskMd}
|
|
|
2496
3489
|
} catch {
|
|
2497
3490
|
}
|
|
2498
3491
|
}
|
|
2499
|
-
const specPath =
|
|
2500
|
-
if (
|
|
2501
|
-
const spec =
|
|
3492
|
+
const specPath = path16.join(taskDir, "spec.md");
|
|
3493
|
+
if (fs18.existsSync(specPath)) {
|
|
3494
|
+
const spec = fs18.readFileSync(specPath, "utf-8");
|
|
2502
3495
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
2503
3496
|
context += `
|
|
2504
3497
|
## Spec Summary
|
|
2505
3498
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
2506
3499
|
`;
|
|
2507
3500
|
}
|
|
2508
|
-
const planPath =
|
|
2509
|
-
if (
|
|
2510
|
-
const plan =
|
|
3501
|
+
const planPath = path16.join(taskDir, "plan.md");
|
|
3502
|
+
if (fs18.existsSync(planPath)) {
|
|
3503
|
+
const plan = fs18.readFileSync(planPath, "utf-8");
|
|
2511
3504
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
2512
3505
|
context += `
|
|
2513
3506
|
## Plan Summary
|
|
2514
3507
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
2515
3508
|
`;
|
|
2516
3509
|
}
|
|
2517
|
-
const contextMdPath =
|
|
2518
|
-
if (
|
|
2519
|
-
const accumulated =
|
|
3510
|
+
const contextMdPath = path16.join(taskDir, "context.md");
|
|
3511
|
+
if (fs18.existsSync(contextMdPath)) {
|
|
3512
|
+
const accumulated = fs18.readFileSync(contextMdPath, "utf-8");
|
|
2520
3513
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
2521
3514
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
2522
3515
|
context += `
|
|
@@ -2534,17 +3527,17 @@ ${feedback}
|
|
|
2534
3527
|
}
|
|
2535
3528
|
function inferHasUIFromScope(scope) {
|
|
2536
3529
|
return scope.some((filePath) => {
|
|
2537
|
-
const ext =
|
|
3530
|
+
const ext = path16.extname(filePath).toLowerCase();
|
|
2538
3531
|
if (UI_EXTENSIONS.has(ext)) return true;
|
|
2539
3532
|
const normalized = filePath.replace(/\\/g, "/");
|
|
2540
3533
|
return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
|
|
2541
3534
|
});
|
|
2542
3535
|
}
|
|
2543
3536
|
function taskHasUI(taskDir) {
|
|
2544
|
-
const taskJsonPath =
|
|
2545
|
-
if (!
|
|
3537
|
+
const taskJsonPath = path16.join(taskDir, "task.json");
|
|
3538
|
+
if (!fs18.existsSync(taskJsonPath)) return true;
|
|
2546
3539
|
try {
|
|
2547
|
-
const taskDef = JSON.parse(
|
|
3540
|
+
const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
|
|
2548
3541
|
const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
|
|
2549
3542
|
if (scope.length === 0) return true;
|
|
2550
3543
|
return inferHasUIFromScope(scope);
|
|
@@ -2666,9 +3659,9 @@ ${prompt}` : prompt;
|
|
|
2666
3659
|
}
|
|
2667
3660
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
2668
3661
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
2669
|
-
const qaGuidePath =
|
|
2670
|
-
if (
|
|
2671
|
-
const qaGuide =
|
|
3662
|
+
const qaGuidePath = path16.join(projectDir, ".kody", "qa-guide.md");
|
|
3663
|
+
if (fs18.existsSync(qaGuidePath)) {
|
|
3664
|
+
const qaGuide = fs18.readFileSync(qaGuidePath, "utf-8").trim();
|
|
2672
3665
|
assembled = assembled + "\n\n" + qaGuide;
|
|
2673
3666
|
}
|
|
2674
3667
|
}
|
|
@@ -2760,8 +3753,8 @@ var init_runner_selection = __esm({
|
|
|
2760
3753
|
});
|
|
2761
3754
|
|
|
2762
3755
|
// src/stages/agent.ts
|
|
2763
|
-
import * as
|
|
2764
|
-
import * as
|
|
3756
|
+
import * as fs19 from "fs";
|
|
3757
|
+
import * as path17 from "path";
|
|
2765
3758
|
function getSessionInfo(stageName, sessions) {
|
|
2766
3759
|
const group = SESSION_GROUP[stageName];
|
|
2767
3760
|
if (!group) return void 0;
|
|
@@ -2846,29 +3839,29 @@ async function executeAgentStage(ctx, def) {
|
|
|
2846
3839
|
if (lastResult.outcome !== "completed") {
|
|
2847
3840
|
return { outcome: lastResult.outcome, error: lastResult.error, retries };
|
|
2848
3841
|
}
|
|
2849
|
-
const
|
|
2850
|
-
if (def.outputFile &&
|
|
2851
|
-
|
|
3842
|
+
const result2 = lastResult;
|
|
3843
|
+
if (def.outputFile && result2.output) {
|
|
3844
|
+
fs19.writeFileSync(path17.join(ctx.taskDir, def.outputFile), result2.output);
|
|
2852
3845
|
}
|
|
2853
3846
|
if (def.outputFile) {
|
|
2854
|
-
const outputPath =
|
|
2855
|
-
if (!
|
|
2856
|
-
const ext =
|
|
2857
|
-
const base =
|
|
2858
|
-
const files =
|
|
3847
|
+
const outputPath = path17.join(ctx.taskDir, def.outputFile);
|
|
3848
|
+
if (!fs19.existsSync(outputPath)) {
|
|
3849
|
+
const ext = path17.extname(def.outputFile);
|
|
3850
|
+
const base = path17.basename(def.outputFile, ext);
|
|
3851
|
+
const files = fs19.readdirSync(ctx.taskDir);
|
|
2859
3852
|
const variant = files.find(
|
|
2860
3853
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
2861
3854
|
);
|
|
2862
3855
|
if (variant) {
|
|
2863
|
-
|
|
3856
|
+
fs19.renameSync(path17.join(ctx.taskDir, variant), outputPath);
|
|
2864
3857
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
2865
3858
|
}
|
|
2866
3859
|
}
|
|
2867
3860
|
}
|
|
2868
3861
|
if (def.outputFile) {
|
|
2869
|
-
const outputPath =
|
|
2870
|
-
if (
|
|
2871
|
-
const content =
|
|
3862
|
+
const outputPath = path17.join(ctx.taskDir, def.outputFile);
|
|
3863
|
+
if (fs19.existsSync(outputPath)) {
|
|
3864
|
+
const content = fs19.readFileSync(outputPath, "utf-8");
|
|
2872
3865
|
const validation = validateStageOutput(def.name, content);
|
|
2873
3866
|
if (!validation.valid) {
|
|
2874
3867
|
if (def.name === "taskify") {
|
|
@@ -2882,7 +3875,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2882
3875
|
const stripped = stripFences(retryResult.output);
|
|
2883
3876
|
const retryValidation = validateTaskJson(stripped);
|
|
2884
3877
|
if (retryValidation.valid) {
|
|
2885
|
-
|
|
3878
|
+
fs19.writeFileSync(outputPath, retryResult.output);
|
|
2886
3879
|
logger.info(` taskify retry produced valid JSON`);
|
|
2887
3880
|
} else {
|
|
2888
3881
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -2895,7 +3888,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2895
3888
|
risk_level: "low",
|
|
2896
3889
|
questions: []
|
|
2897
3890
|
}, null, 2);
|
|
2898
|
-
|
|
3891
|
+
fs19.writeFileSync(outputPath, fallback);
|
|
2899
3892
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
2900
3893
|
}
|
|
2901
3894
|
}
|
|
@@ -2905,11 +3898,11 @@ async function executeAgentStage(ctx, def) {
|
|
|
2905
3898
|
}
|
|
2906
3899
|
}
|
|
2907
3900
|
}
|
|
2908
|
-
appendStageContext(ctx.taskDir, def.name,
|
|
3901
|
+
appendStageContext(ctx.taskDir, def.name, result2.output);
|
|
2909
3902
|
return { outcome: "completed", outputFile: def.outputFile, retries };
|
|
2910
3903
|
}
|
|
2911
3904
|
function appendStageContext(taskDir, stageName, output) {
|
|
2912
|
-
const contextPath =
|
|
3905
|
+
const contextPath = path17.join(taskDir, "context.md");
|
|
2913
3906
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
2914
3907
|
let summary;
|
|
2915
3908
|
if (output && output.trim()) {
|
|
@@ -2922,7 +3915,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
2922
3915
|
### ${stageName} (${timestamp2})
|
|
2923
3916
|
${summary}
|
|
2924
3917
|
`;
|
|
2925
|
-
|
|
3918
|
+
fs19.appendFileSync(contextPath, entry);
|
|
2926
3919
|
}
|
|
2927
3920
|
var SESSION_GROUP;
|
|
2928
3921
|
var init_agent = __esm({
|
|
@@ -2945,7 +3938,7 @@ var init_agent = __esm({
|
|
|
2945
3938
|
});
|
|
2946
3939
|
|
|
2947
3940
|
// src/verify-runner.ts
|
|
2948
|
-
import { execFileSync as
|
|
3941
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
2949
3942
|
function isExecError(err) {
|
|
2950
3943
|
return typeof err === "object" && err !== null;
|
|
2951
3944
|
}
|
|
@@ -2981,7 +3974,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
2981
3974
|
return { success: true, output: "", timedOut: false };
|
|
2982
3975
|
}
|
|
2983
3976
|
try {
|
|
2984
|
-
const output =
|
|
3977
|
+
const output = execFileSync12(parts[0], parts.slice(1), {
|
|
2985
3978
|
cwd,
|
|
2986
3979
|
timeout,
|
|
2987
3980
|
encoding: "utf-8",
|
|
@@ -3027,19 +4020,19 @@ function runQualityGates(taskDir, projectRoot) {
|
|
|
3027
4020
|
for (const { name, cmd } of commands) {
|
|
3028
4021
|
if (!cmd) continue;
|
|
3029
4022
|
logger.info(` Running ${name}: ${cmd}`);
|
|
3030
|
-
const
|
|
3031
|
-
if (
|
|
4023
|
+
const result2 = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
|
|
4024
|
+
if (result2.timedOut) {
|
|
3032
4025
|
allErrors.push(`${name}: timed out after ${VERIFY_COMMAND_TIMEOUT_MS / 1e3}s`);
|
|
3033
4026
|
allPass = false;
|
|
3034
4027
|
continue;
|
|
3035
4028
|
}
|
|
3036
|
-
if (!
|
|
4029
|
+
if (!result2.success) {
|
|
3037
4030
|
allPass = false;
|
|
3038
|
-
const errors = parseErrors(
|
|
4031
|
+
const errors = parseErrors(result2.output);
|
|
3039
4032
|
allErrors.push(...errors.map((e) => `[${name}] ${e}`));
|
|
3040
|
-
rawOutputs.push({ name, output:
|
|
4033
|
+
rawOutputs.push({ name, output: result2.output.slice(-3e3) });
|
|
3041
4034
|
}
|
|
3042
|
-
allSummary.push(...extractSummary(
|
|
4035
|
+
allSummary.push(...extractSummary(result2.output, name));
|
|
3043
4036
|
}
|
|
3044
4037
|
return { pass: allPass, errors: allErrors, summary: allSummary, rawOutputs };
|
|
3045
4038
|
}
|
|
@@ -3052,7 +4045,7 @@ var init_verify_runner = __esm({
|
|
|
3052
4045
|
});
|
|
3053
4046
|
|
|
3054
4047
|
// src/observer.ts
|
|
3055
|
-
import { execFileSync as
|
|
4048
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
3056
4049
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
3057
4050
|
const context = [
|
|
3058
4051
|
`Stage: ${stageName}`,
|
|
@@ -3066,7 +4059,7 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
3066
4059
|
].join("\n");
|
|
3067
4060
|
const prompt = DIAGNOSIS_PROMPT + context;
|
|
3068
4061
|
try {
|
|
3069
|
-
const
|
|
4062
|
+
const result2 = await runner.run(
|
|
3070
4063
|
"diagnosis",
|
|
3071
4064
|
prompt,
|
|
3072
4065
|
model,
|
|
@@ -3075,8 +4068,8 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
3075
4068
|
"",
|
|
3076
4069
|
options
|
|
3077
4070
|
);
|
|
3078
|
-
if (
|
|
3079
|
-
const cleaned =
|
|
4071
|
+
if (result2.outcome === "completed" && result2.output) {
|
|
4072
|
+
const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
3080
4073
|
const parseResult = parseJsonSafe(cleaned, ["classification"]);
|
|
3081
4074
|
if (parseResult.ok) {
|
|
3082
4075
|
const { data } = parseResult;
|
|
@@ -3112,13 +4105,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
3112
4105
|
}
|
|
3113
4106
|
function getModifiedFiles(projectDir) {
|
|
3114
4107
|
try {
|
|
3115
|
-
const staged =
|
|
4108
|
+
const staged = execFileSync13("git", ["diff", "--name-only", "--cached"], {
|
|
3116
4109
|
encoding: "utf-8",
|
|
3117
4110
|
cwd: projectDir,
|
|
3118
4111
|
timeout: 5e3,
|
|
3119
4112
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3120
4113
|
}).trim();
|
|
3121
|
-
const unstaged =
|
|
4114
|
+
const unstaged = execFileSync13("git", ["diff", "--name-only"], {
|
|
3122
4115
|
encoding: "utf-8",
|
|
3123
4116
|
cwd: projectDir,
|
|
3124
4117
|
timeout: 5e3,
|
|
@@ -3161,8 +4154,8 @@ Error context:
|
|
|
3161
4154
|
});
|
|
3162
4155
|
|
|
3163
4156
|
// src/stages/gate.ts
|
|
3164
|
-
import * as
|
|
3165
|
-
import * as
|
|
4157
|
+
import * as fs20 from "fs";
|
|
4158
|
+
import * as path18 from "path";
|
|
3166
4159
|
function executeGateStage(ctx, def) {
|
|
3167
4160
|
if (ctx.input.dryRun) {
|
|
3168
4161
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -3205,7 +4198,7 @@ ${output}
|
|
|
3205
4198
|
`);
|
|
3206
4199
|
}
|
|
3207
4200
|
}
|
|
3208
|
-
|
|
4201
|
+
fs20.writeFileSync(path18.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
3209
4202
|
return {
|
|
3210
4203
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
3211
4204
|
retries: 0
|
|
@@ -3220,9 +4213,9 @@ var init_gate = __esm({
|
|
|
3220
4213
|
});
|
|
3221
4214
|
|
|
3222
4215
|
// src/stages/verify.ts
|
|
3223
|
-
import * as
|
|
3224
|
-
import * as
|
|
3225
|
-
import { execFileSync as
|
|
4216
|
+
import * as fs21 from "fs";
|
|
4217
|
+
import * as path19 from "path";
|
|
4218
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
3226
4219
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
3227
4220
|
const maxAttempts = def.maxRetries ?? 2;
|
|
3228
4221
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -3232,8 +4225,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
3232
4225
|
return { ...gateResult, retries: attempt };
|
|
3233
4226
|
}
|
|
3234
4227
|
if (attempt < maxAttempts) {
|
|
3235
|
-
const verifyPath =
|
|
3236
|
-
const errorOutput =
|
|
4228
|
+
const verifyPath = path19.join(ctx.taskDir, "verify.md");
|
|
4229
|
+
const errorOutput = fs21.existsSync(verifyPath) ? fs21.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
3237
4230
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
3238
4231
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
3239
4232
|
const diagConfig = getProjectConfig();
|
|
@@ -3276,7 +4269,7 @@ ${diagnosis.resolution}`);
|
|
|
3276
4269
|
const parts = parseCommand(cmd);
|
|
3277
4270
|
if (parts.length === 0) return;
|
|
3278
4271
|
try {
|
|
3279
|
-
|
|
4272
|
+
execFileSync14(parts[0], parts.slice(1), {
|
|
3280
4273
|
stdio: "pipe",
|
|
3281
4274
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
3282
4275
|
});
|
|
@@ -3329,8 +4322,8 @@ var init_verify = __esm({
|
|
|
3329
4322
|
});
|
|
3330
4323
|
|
|
3331
4324
|
// src/review-standalone.ts
|
|
3332
|
-
import * as
|
|
3333
|
-
import * as
|
|
4325
|
+
import * as fs22 from "fs";
|
|
4326
|
+
import * as path20 from "path";
|
|
3334
4327
|
function resolveReviewTarget(input) {
|
|
3335
4328
|
if (input.prs.length === 0) {
|
|
3336
4329
|
return {
|
|
@@ -3354,8 +4347,8 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
3354
4347
|
}
|
|
3355
4348
|
async function runStandaloneReview(input) {
|
|
3356
4349
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
3357
|
-
const taskDir =
|
|
3358
|
-
|
|
4350
|
+
const taskDir = path20.join(input.projectDir, ".kody", "tasks", taskId);
|
|
4351
|
+
fs22.mkdirSync(taskDir, { recursive: true });
|
|
3359
4352
|
let diffInstruction = "";
|
|
3360
4353
|
let filesChangedSection = "";
|
|
3361
4354
|
if (input.baseBranch) {
|
|
@@ -3382,7 +4375,7 @@ ${fileList}`;
|
|
|
3382
4375
|
const taskContent = `# ${input.prTitle}
|
|
3383
4376
|
|
|
3384
4377
|
${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
3385
|
-
|
|
4378
|
+
fs22.writeFileSync(path20.join(taskDir, "task.md"), taskContent);
|
|
3386
4379
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
3387
4380
|
const ctx = {
|
|
3388
4381
|
taskId,
|
|
@@ -3396,18 +4389,18 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
|
3396
4389
|
}
|
|
3397
4390
|
};
|
|
3398
4391
|
logger.info(`[review] standalone review for: ${input.prTitle}`);
|
|
3399
|
-
const
|
|
3400
|
-
if (
|
|
4392
|
+
const result2 = await executeAgentStage(ctx, reviewDef);
|
|
4393
|
+
if (result2.outcome !== "completed") {
|
|
3401
4394
|
return {
|
|
3402
4395
|
outcome: "failed",
|
|
3403
4396
|
taskDir,
|
|
3404
|
-
error:
|
|
4397
|
+
error: result2.error ?? "Review stage failed"
|
|
3405
4398
|
};
|
|
3406
4399
|
}
|
|
3407
|
-
const reviewPath =
|
|
4400
|
+
const reviewPath = path20.join(taskDir, "review.md");
|
|
3408
4401
|
let reviewContent;
|
|
3409
|
-
if (
|
|
3410
|
-
reviewContent =
|
|
4402
|
+
if (fs22.existsSync(reviewPath)) {
|
|
4403
|
+
reviewContent = fs22.readFileSync(reviewPath, "utf-8");
|
|
3411
4404
|
}
|
|
3412
4405
|
return {
|
|
3413
4406
|
outcome: "completed",
|
|
@@ -3447,8 +4440,8 @@ var init_review_standalone = __esm({
|
|
|
3447
4440
|
});
|
|
3448
4441
|
|
|
3449
4442
|
// src/stages/review.ts
|
|
3450
|
-
import * as
|
|
3451
|
-
import * as
|
|
4443
|
+
import * as fs23 from "fs";
|
|
4444
|
+
import * as path21 from "path";
|
|
3452
4445
|
async function executeReviewWithFix(ctx, def) {
|
|
3453
4446
|
if (ctx.input.dryRun) {
|
|
3454
4447
|
return { outcome: "completed", retries: 0 };
|
|
@@ -3462,11 +4455,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
3462
4455
|
if (reviewResult.outcome !== "completed") {
|
|
3463
4456
|
return reviewResult;
|
|
3464
4457
|
}
|
|
3465
|
-
const reviewFile =
|
|
3466
|
-
if (!
|
|
4458
|
+
const reviewFile = path21.join(ctx.taskDir, "review.md");
|
|
4459
|
+
if (!fs23.existsSync(reviewFile)) {
|
|
3467
4460
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
3468
4461
|
}
|
|
3469
|
-
const content =
|
|
4462
|
+
const content = fs23.readFileSync(reviewFile, "utf-8");
|
|
3470
4463
|
if (detectReviewVerdict(content) !== "fail") {
|
|
3471
4464
|
return { ...reviewResult, retries: iteration };
|
|
3472
4465
|
}
|
|
@@ -3495,15 +4488,15 @@ var init_review = __esm({
|
|
|
3495
4488
|
});
|
|
3496
4489
|
|
|
3497
4490
|
// src/stages/ship.ts
|
|
3498
|
-
import * as
|
|
3499
|
-
import * as
|
|
3500
|
-
import { execFileSync as
|
|
4491
|
+
import * as fs24 from "fs";
|
|
4492
|
+
import * as path22 from "path";
|
|
4493
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
3501
4494
|
function buildPrBody(ctx) {
|
|
3502
4495
|
const sections = [];
|
|
3503
|
-
const taskJsonPath =
|
|
3504
|
-
if (
|
|
4496
|
+
const taskJsonPath = path22.join(ctx.taskDir, "task.json");
|
|
4497
|
+
if (fs24.existsSync(taskJsonPath)) {
|
|
3505
4498
|
try {
|
|
3506
|
-
const raw =
|
|
4499
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3507
4500
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3508
4501
|
const task = JSON.parse(cleaned);
|
|
3509
4502
|
if (task.description) {
|
|
@@ -3522,9 +4515,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
3522
4515
|
} catch {
|
|
3523
4516
|
}
|
|
3524
4517
|
}
|
|
3525
|
-
const reviewPath =
|
|
3526
|
-
if (
|
|
3527
|
-
const review =
|
|
4518
|
+
const reviewPath = path22.join(ctx.taskDir, "review.md");
|
|
4519
|
+
if (fs24.existsSync(reviewPath)) {
|
|
4520
|
+
const review = fs24.readFileSync(reviewPath, "utf-8");
|
|
3528
4521
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
3529
4522
|
if (summaryMatch) {
|
|
3530
4523
|
const summary = summaryMatch[1].trim();
|
|
@@ -3541,14 +4534,14 @@ ${summary}`);
|
|
|
3541
4534
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
3542
4535
|
}
|
|
3543
4536
|
}
|
|
3544
|
-
const verifyPath =
|
|
3545
|
-
if (
|
|
3546
|
-
const verify =
|
|
4537
|
+
const verifyPath = path22.join(ctx.taskDir, "verify.md");
|
|
4538
|
+
if (fs24.existsSync(verifyPath)) {
|
|
4539
|
+
const verify = fs24.readFileSync(verifyPath, "utf-8");
|
|
3547
4540
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
3548
4541
|
}
|
|
3549
|
-
const planPath =
|
|
3550
|
-
if (
|
|
3551
|
-
const plan =
|
|
4542
|
+
const planPath = path22.join(ctx.taskDir, "plan.md");
|
|
4543
|
+
if (fs24.existsSync(planPath)) {
|
|
4544
|
+
const plan = fs24.readFileSync(planPath, "utf-8").trim();
|
|
3552
4545
|
if (plan) {
|
|
3553
4546
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
3554
4547
|
sections.push(`
|
|
@@ -3568,25 +4561,25 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
3568
4561
|
return sections.join("\n");
|
|
3569
4562
|
}
|
|
3570
4563
|
function executeShipStage(ctx, _def) {
|
|
3571
|
-
const shipPath =
|
|
4564
|
+
const shipPath = path22.join(ctx.taskDir, "ship.md");
|
|
3572
4565
|
if (ctx.input.dryRun) {
|
|
3573
|
-
|
|
4566
|
+
fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
3574
4567
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3575
4568
|
}
|
|
3576
4569
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
3577
|
-
|
|
4570
|
+
fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
3578
4571
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3579
4572
|
}
|
|
3580
4573
|
try {
|
|
3581
4574
|
const head = getCurrentBranch(ctx.projectDir);
|
|
3582
4575
|
const base = getDefaultBranch(ctx.projectDir);
|
|
3583
4576
|
try {
|
|
3584
|
-
|
|
4577
|
+
execFileSync15("git", ["add", ctx.taskDir], {
|
|
3585
4578
|
cwd: ctx.projectDir,
|
|
3586
4579
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
3587
4580
|
stdio: "pipe"
|
|
3588
4581
|
});
|
|
3589
|
-
|
|
4582
|
+
execFileSync15("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
3590
4583
|
cwd: ctx.projectDir,
|
|
3591
4584
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
3592
4585
|
stdio: "pipe"
|
|
@@ -3600,7 +4593,7 @@ function executeShipStage(ctx, _def) {
|
|
|
3600
4593
|
let repo = config.github?.repo;
|
|
3601
4594
|
if (!owner || !repo) {
|
|
3602
4595
|
try {
|
|
3603
|
-
const remoteUrl =
|
|
4596
|
+
const remoteUrl = execFileSync15("git", ["remote", "get-url", "origin"], {
|
|
3604
4597
|
encoding: "utf-8",
|
|
3605
4598
|
cwd: ctx.projectDir
|
|
3606
4599
|
}).trim();
|
|
@@ -3621,28 +4614,28 @@ function executeShipStage(ctx, _def) {
|
|
|
3621
4614
|
chore: "chore"
|
|
3622
4615
|
};
|
|
3623
4616
|
let prefix = "chore";
|
|
3624
|
-
const taskJsonPath =
|
|
3625
|
-
if (
|
|
4617
|
+
const taskJsonPath = path22.join(ctx.taskDir, "task.json");
|
|
4618
|
+
if (fs24.existsSync(taskJsonPath)) {
|
|
3626
4619
|
try {
|
|
3627
|
-
const raw =
|
|
4620
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3628
4621
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3629
4622
|
const task = JSON.parse(cleaned);
|
|
3630
4623
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
3631
4624
|
} catch {
|
|
3632
4625
|
}
|
|
3633
4626
|
}
|
|
3634
|
-
const taskMdPath =
|
|
3635
|
-
if (
|
|
3636
|
-
const content =
|
|
4627
|
+
const taskMdPath = path22.join(ctx.taskDir, "task.md");
|
|
4628
|
+
if (fs24.existsSync(taskMdPath)) {
|
|
4629
|
+
const content = fs24.readFileSync(taskMdPath, "utf-8");
|
|
3637
4630
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
3638
4631
|
if (heading) {
|
|
3639
4632
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
3640
4633
|
}
|
|
3641
4634
|
}
|
|
3642
4635
|
if (title === "Update") {
|
|
3643
|
-
if (
|
|
4636
|
+
if (fs24.existsSync(taskJsonPath)) {
|
|
3644
4637
|
try {
|
|
3645
|
-
const raw =
|
|
4638
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3646
4639
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3647
4640
|
const task = JSON.parse(cleaned);
|
|
3648
4641
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -3665,7 +4658,7 @@ function executeShipStage(ctx, _def) {
|
|
|
3665
4658
|
} catch {
|
|
3666
4659
|
}
|
|
3667
4660
|
}
|
|
3668
|
-
|
|
4661
|
+
fs24.writeFileSync(shipPath, `# Ship
|
|
3669
4662
|
|
|
3670
4663
|
Updated existing PR: ${existingPr.url}
|
|
3671
4664
|
PR #${existingPr.number}
|
|
@@ -3686,20 +4679,20 @@ PR #${existingPr.number}
|
|
|
3686
4679
|
} catch {
|
|
3687
4680
|
}
|
|
3688
4681
|
}
|
|
3689
|
-
|
|
4682
|
+
fs24.writeFileSync(shipPath, `# Ship
|
|
3690
4683
|
|
|
3691
4684
|
PR created: ${pr.url}
|
|
3692
4685
|
PR #${pr.number}
|
|
3693
4686
|
`);
|
|
3694
4687
|
} else {
|
|
3695
|
-
|
|
4688
|
+
fs24.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
3696
4689
|
}
|
|
3697
4690
|
}
|
|
3698
4691
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3699
4692
|
} catch (err) {
|
|
3700
4693
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3701
4694
|
try {
|
|
3702
|
-
|
|
4695
|
+
fs24.writeFileSync(shipPath, `# Ship
|
|
3703
4696
|
|
|
3704
4697
|
Failed: ${msg}
|
|
3705
4698
|
`);
|
|
@@ -3748,15 +4741,15 @@ var init_executor_registry = __esm({
|
|
|
3748
4741
|
});
|
|
3749
4742
|
|
|
3750
4743
|
// src/pipeline/questions.ts
|
|
3751
|
-
import * as
|
|
3752
|
-
import * as
|
|
4744
|
+
import * as fs25 from "fs";
|
|
4745
|
+
import * as path23 from "path";
|
|
3753
4746
|
function checkForQuestions(ctx, stageName) {
|
|
3754
4747
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
3755
4748
|
try {
|
|
3756
4749
|
if (stageName === "taskify") {
|
|
3757
|
-
const taskJsonPath =
|
|
3758
|
-
if (!
|
|
3759
|
-
const raw =
|
|
4750
|
+
const taskJsonPath = path23.join(ctx.taskDir, "task.json");
|
|
4751
|
+
if (!fs25.existsSync(taskJsonPath)) return false;
|
|
4752
|
+
const raw = fs25.readFileSync(taskJsonPath, "utf-8");
|
|
3760
4753
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3761
4754
|
const taskJson = JSON.parse(cleaned);
|
|
3762
4755
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -3771,9 +4764,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
3771
4764
|
}
|
|
3772
4765
|
}
|
|
3773
4766
|
if (stageName === "plan") {
|
|
3774
|
-
const planPath =
|
|
3775
|
-
if (!
|
|
3776
|
-
const plan =
|
|
4767
|
+
const planPath = path23.join(ctx.taskDir, "plan.md");
|
|
4768
|
+
if (!fs25.existsSync(planPath)) return false;
|
|
4769
|
+
const plan = fs25.readFileSync(planPath, "utf-8");
|
|
3777
4770
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
3778
4771
|
if (questionsMatch) {
|
|
3779
4772
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -3802,8 +4795,8 @@ var init_questions = __esm({
|
|
|
3802
4795
|
});
|
|
3803
4796
|
|
|
3804
4797
|
// src/pipeline/hooks.ts
|
|
3805
|
-
import * as
|
|
3806
|
-
import * as
|
|
4798
|
+
import * as fs26 from "fs";
|
|
4799
|
+
import * as path24 from "path";
|
|
3807
4800
|
function applyPreStageLabel(ctx, def) {
|
|
3808
4801
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
3809
4802
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
@@ -3841,9 +4834,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
3841
4834
|
return { complexity, activeStages };
|
|
3842
4835
|
}
|
|
3843
4836
|
try {
|
|
3844
|
-
const taskJsonPath =
|
|
3845
|
-
if (!
|
|
3846
|
-
const raw =
|
|
4837
|
+
const taskJsonPath = path24.join(ctx.taskDir, "task.json");
|
|
4838
|
+
if (!fs26.existsSync(taskJsonPath)) return null;
|
|
4839
|
+
const raw = fs26.readFileSync(taskJsonPath, "utf-8");
|
|
3847
4840
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3848
4841
|
const taskJson = JSON.parse(cleaned);
|
|
3849
4842
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -3873,8 +4866,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
3873
4866
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
3874
4867
|
if (ctx.input.mode === "rerun") return null;
|
|
3875
4868
|
if (!ctx.input.issueNumber) return null;
|
|
3876
|
-
const planPath =
|
|
3877
|
-
const plan =
|
|
4869
|
+
const planPath = path24.join(ctx.taskDir, "plan.md");
|
|
4870
|
+
const plan = fs26.existsSync(planPath) ? fs26.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
3878
4871
|
try {
|
|
3879
4872
|
postComment(
|
|
3880
4873
|
ctx.input.issueNumber,
|
|
@@ -3941,22 +4934,22 @@ var init_hooks = __esm({
|
|
|
3941
4934
|
});
|
|
3942
4935
|
|
|
3943
4936
|
// src/learning/auto-learn.ts
|
|
3944
|
-
import * as
|
|
3945
|
-
import * as
|
|
4937
|
+
import * as fs27 from "fs";
|
|
4938
|
+
import * as path25 from "path";
|
|
3946
4939
|
function stripAnsi(str) {
|
|
3947
4940
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
3948
4941
|
}
|
|
3949
4942
|
function autoLearn(ctx) {
|
|
3950
4943
|
try {
|
|
3951
|
-
const memoryDir =
|
|
3952
|
-
if (!
|
|
3953
|
-
|
|
4944
|
+
const memoryDir = path25.join(ctx.projectDir, ".kody", "memory");
|
|
4945
|
+
if (!fs27.existsSync(memoryDir)) {
|
|
4946
|
+
fs27.mkdirSync(memoryDir, { recursive: true });
|
|
3954
4947
|
}
|
|
3955
4948
|
const learnings = [];
|
|
3956
4949
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3957
|
-
const verifyPath =
|
|
3958
|
-
if (
|
|
3959
|
-
const verify = stripAnsi(
|
|
4950
|
+
const verifyPath = path25.join(ctx.taskDir, "verify.md");
|
|
4951
|
+
if (fs27.existsSync(verifyPath)) {
|
|
4952
|
+
const verify = stripAnsi(fs27.readFileSync(verifyPath, "utf-8"));
|
|
3960
4953
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
3961
4954
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
3962
4955
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -3965,18 +4958,18 @@ function autoLearn(ctx) {
|
|
|
3965
4958
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
3966
4959
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
3967
4960
|
}
|
|
3968
|
-
const reviewPath =
|
|
3969
|
-
if (
|
|
3970
|
-
const review =
|
|
4961
|
+
const reviewPath = path25.join(ctx.taskDir, "review.md");
|
|
4962
|
+
if (fs27.existsSync(reviewPath)) {
|
|
4963
|
+
const review = fs27.readFileSync(reviewPath, "utf-8");
|
|
3971
4964
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
3972
4965
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
3973
4966
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
3974
4967
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
3975
4968
|
}
|
|
3976
|
-
const taskJsonPath =
|
|
3977
|
-
if (
|
|
4969
|
+
const taskJsonPath = path25.join(ctx.taskDir, "task.json");
|
|
4970
|
+
if (fs27.existsSync(taskJsonPath)) {
|
|
3978
4971
|
try {
|
|
3979
|
-
const raw = stripAnsi(
|
|
4972
|
+
const raw = stripAnsi(fs27.readFileSync(taskJsonPath, "utf-8"));
|
|
3980
4973
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3981
4974
|
const task = JSON.parse(cleaned);
|
|
3982
4975
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -3987,12 +4980,12 @@ function autoLearn(ctx) {
|
|
|
3987
4980
|
}
|
|
3988
4981
|
}
|
|
3989
4982
|
if (learnings.length > 0) {
|
|
3990
|
-
const conventionsPath =
|
|
4983
|
+
const conventionsPath = path25.join(memoryDir, "conventions.md");
|
|
3991
4984
|
const entry = `
|
|
3992
4985
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
3993
4986
|
${learnings.join("\n")}
|
|
3994
4987
|
`;
|
|
3995
|
-
|
|
4988
|
+
fs27.appendFileSync(conventionsPath, entry);
|
|
3996
4989
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
3997
4990
|
}
|
|
3998
4991
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
@@ -4000,8 +4993,8 @@ ${learnings.join("\n")}
|
|
|
4000
4993
|
}
|
|
4001
4994
|
}
|
|
4002
4995
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
4003
|
-
const archPath =
|
|
4004
|
-
if (
|
|
4996
|
+
const archPath = path25.join(memoryDir, "architecture.md");
|
|
4997
|
+
if (fs27.existsSync(archPath)) return;
|
|
4005
4998
|
const detected = detectArchitectureBasic(projectDir);
|
|
4006
4999
|
if (detected.length > 0) {
|
|
4007
5000
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
@@ -4009,7 +5002,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
4009
5002
|
## Overview
|
|
4010
5003
|
${detected.join("\n")}
|
|
4011
5004
|
`;
|
|
4012
|
-
|
|
5005
|
+
fs27.writeFileSync(archPath, content);
|
|
4013
5006
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
4014
5007
|
}
|
|
4015
5008
|
}
|
|
@@ -4022,13 +5015,13 @@ var init_auto_learn = __esm({
|
|
|
4022
5015
|
});
|
|
4023
5016
|
|
|
4024
5017
|
// src/retrospective.ts
|
|
4025
|
-
import * as
|
|
4026
|
-
import * as
|
|
5018
|
+
import * as fs28 from "fs";
|
|
5019
|
+
import * as path26 from "path";
|
|
4027
5020
|
function readArtifact(taskDir, filename, maxChars) {
|
|
4028
|
-
const p =
|
|
4029
|
-
if (!
|
|
5021
|
+
const p = path26.join(taskDir, filename);
|
|
5022
|
+
if (!fs28.existsSync(p)) return null;
|
|
4030
5023
|
try {
|
|
4031
|
-
const content =
|
|
5024
|
+
const content = fs28.readFileSync(p, "utf-8");
|
|
4032
5025
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
4033
5026
|
} catch {
|
|
4034
5027
|
return null;
|
|
@@ -4081,13 +5074,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
4081
5074
|
return lines.join("\n");
|
|
4082
5075
|
}
|
|
4083
5076
|
function getLogPath(projectDir) {
|
|
4084
|
-
return
|
|
5077
|
+
return path26.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
4085
5078
|
}
|
|
4086
5079
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
4087
5080
|
const logPath = getLogPath(projectDir);
|
|
4088
|
-
if (!
|
|
5081
|
+
if (!fs28.existsSync(logPath)) return [];
|
|
4089
5082
|
try {
|
|
4090
|
-
const content =
|
|
5083
|
+
const content = fs28.readFileSync(logPath, "utf-8");
|
|
4091
5084
|
const lines = content.split("\n").filter(Boolean);
|
|
4092
5085
|
const entries = [];
|
|
4093
5086
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -4114,11 +5107,11 @@ function formatPreviousEntries(entries) {
|
|
|
4114
5107
|
}
|
|
4115
5108
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
4116
5109
|
const logPath = getLogPath(projectDir);
|
|
4117
|
-
const dir =
|
|
4118
|
-
if (!
|
|
4119
|
-
|
|
5110
|
+
const dir = path26.dirname(logPath);
|
|
5111
|
+
if (!fs28.existsSync(dir)) {
|
|
5112
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
4120
5113
|
}
|
|
4121
|
-
|
|
5114
|
+
fs28.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4122
5115
|
}
|
|
4123
5116
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
4124
5117
|
if (ctx.input.dryRun) return;
|
|
@@ -4140,7 +5133,7 @@ ${previousText}
|
|
|
4140
5133
|
if (needsLitellmProxy(config)) {
|
|
4141
5134
|
extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
|
|
4142
5135
|
}
|
|
4143
|
-
const
|
|
5136
|
+
const result2 = await runner.run("retrospective", prompt, model, 3e4, "", {
|
|
4144
5137
|
cwd: ctx.projectDir,
|
|
4145
5138
|
env: extraEnv
|
|
4146
5139
|
});
|
|
@@ -4148,8 +5141,8 @@ ${previousText}
|
|
|
4148
5141
|
let patternMatch = null;
|
|
4149
5142
|
let suggestion = "No suggestion";
|
|
4150
5143
|
let pipelineFlaw = null;
|
|
4151
|
-
if (
|
|
4152
|
-
const cleaned =
|
|
5144
|
+
if (result2.outcome === "completed" && result2.output) {
|
|
5145
|
+
const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
4153
5146
|
try {
|
|
4154
5147
|
const parsed = JSON.parse(cleaned);
|
|
4155
5148
|
observation = parsed.observation ?? observation;
|
|
@@ -4286,8 +5279,8 @@ var init_summary = __esm({
|
|
|
4286
5279
|
});
|
|
4287
5280
|
|
|
4288
5281
|
// src/pipeline.ts
|
|
4289
|
-
import * as
|
|
4290
|
-
import * as
|
|
5282
|
+
import * as fs29 from "fs";
|
|
5283
|
+
import * as path27 from "path";
|
|
4291
5284
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
4292
5285
|
if (ctx.input.dryRun) return;
|
|
4293
5286
|
if (ctx.input.prNumber) {
|
|
@@ -4300,8 +5293,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
4300
5293
|
}
|
|
4301
5294
|
if (!ctx.input.issueNumber) return;
|
|
4302
5295
|
try {
|
|
4303
|
-
const taskMdPath =
|
|
4304
|
-
const title =
|
|
5296
|
+
const taskMdPath = path27.join(ctx.taskDir, "task.md");
|
|
5297
|
+
const title = fs29.existsSync(taskMdPath) ? fs29.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
4305
5298
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
4306
5299
|
syncWithDefault(ctx.projectDir);
|
|
4307
5300
|
} catch (err) {
|
|
@@ -4315,10 +5308,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
4315
5308
|
}
|
|
4316
5309
|
}
|
|
4317
5310
|
function acquireLock(taskDir) {
|
|
4318
|
-
const lockPath =
|
|
4319
|
-
if (
|
|
5311
|
+
const lockPath = path27.join(taskDir, ".lock");
|
|
5312
|
+
if (fs29.existsSync(lockPath)) {
|
|
4320
5313
|
try {
|
|
4321
|
-
const pid = parseInt(
|
|
5314
|
+
const pid = parseInt(fs29.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
4322
5315
|
if (!isNaN(pid)) {
|
|
4323
5316
|
try {
|
|
4324
5317
|
process.kill(pid, 0);
|
|
@@ -4335,14 +5328,14 @@ function acquireLock(taskDir) {
|
|
|
4335
5328
|
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
4336
5329
|
}
|
|
4337
5330
|
try {
|
|
4338
|
-
|
|
5331
|
+
fs29.unlinkSync(lockPath);
|
|
4339
5332
|
} catch {
|
|
4340
5333
|
}
|
|
4341
5334
|
}
|
|
4342
5335
|
try {
|
|
4343
|
-
const fd =
|
|
4344
|
-
|
|
4345
|
-
|
|
5336
|
+
const fd = fs29.openSync(lockPath, fs29.constants.O_WRONLY | fs29.constants.O_CREAT | fs29.constants.O_EXCL);
|
|
5337
|
+
fs29.writeSync(fd, String(process.pid));
|
|
5338
|
+
fs29.closeSync(fd);
|
|
4346
5339
|
} catch (err) {
|
|
4347
5340
|
if (err.code === "EEXIST") {
|
|
4348
5341
|
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
@@ -4352,7 +5345,7 @@ function acquireLock(taskDir) {
|
|
|
4352
5345
|
}
|
|
4353
5346
|
function releaseLock(taskDir) {
|
|
4354
5347
|
try {
|
|
4355
|
-
|
|
5348
|
+
fs29.unlinkSync(path27.join(taskDir, ".lock"));
|
|
4356
5349
|
} catch {
|
|
4357
5350
|
}
|
|
4358
5351
|
}
|
|
@@ -4441,23 +5434,23 @@ async function runPipelineInner(ctx) {
|
|
|
4441
5434
|
writeState(state, ctx.taskDir);
|
|
4442
5435
|
logger.info(`[${def.name}] starting...`);
|
|
4443
5436
|
applyPreStageLabel(ctx, def);
|
|
4444
|
-
let
|
|
5437
|
+
let result2;
|
|
4445
5438
|
try {
|
|
4446
|
-
|
|
5439
|
+
result2 = await getExecutor(def.name)(ctx, def);
|
|
4447
5440
|
} catch (error) {
|
|
4448
|
-
|
|
5441
|
+
result2 = {
|
|
4449
5442
|
outcome: "failed",
|
|
4450
5443
|
retries: 0,
|
|
4451
5444
|
error: error instanceof Error ? error.message : String(error)
|
|
4452
5445
|
};
|
|
4453
5446
|
}
|
|
4454
5447
|
ciGroupEnd();
|
|
4455
|
-
if (
|
|
5448
|
+
if (result2.outcome === "completed") {
|
|
4456
5449
|
state.stages[def.name] = {
|
|
4457
5450
|
state: "completed",
|
|
4458
5451
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4459
|
-
retries:
|
|
4460
|
-
outputFile:
|
|
5452
|
+
retries: result2.retries,
|
|
5453
|
+
outputFile: result2.outputFile
|
|
4461
5454
|
};
|
|
4462
5455
|
logger.info(`[${def.name}] \u2713 completed`);
|
|
4463
5456
|
const detected = autoDetectComplexity(ctx, def);
|
|
@@ -4471,16 +5464,16 @@ async function runPipelineInner(ctx) {
|
|
|
4471
5464
|
if (gated) return gated;
|
|
4472
5465
|
commitAfterStage(ctx, def);
|
|
4473
5466
|
} else {
|
|
4474
|
-
const isTimeout =
|
|
5467
|
+
const isTimeout = result2.outcome === "timed_out";
|
|
4475
5468
|
state.stages[def.name] = {
|
|
4476
5469
|
state: isTimeout ? "timeout" : "failed",
|
|
4477
|
-
retries:
|
|
4478
|
-
error: isTimeout ? "Stage timed out" :
|
|
5470
|
+
retries: result2.retries,
|
|
5471
|
+
error: isTimeout ? "Stage timed out" : result2.error ?? "Stage failed"
|
|
4479
5472
|
};
|
|
4480
5473
|
state.state = "failed";
|
|
4481
5474
|
state.sessions = ctx.sessions;
|
|
4482
5475
|
writeState(state, ctx.taskDir);
|
|
4483
|
-
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${
|
|
5476
|
+
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result2.error}`}`);
|
|
4484
5477
|
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
4485
5478
|
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
4486
5479
|
}
|
|
@@ -4560,8 +5553,8 @@ var init_pipeline = __esm({
|
|
|
4560
5553
|
});
|
|
4561
5554
|
|
|
4562
5555
|
// src/preflight.ts
|
|
4563
|
-
import { execFileSync as
|
|
4564
|
-
import * as
|
|
5556
|
+
import { execFileSync as execFileSync16 } from "child_process";
|
|
5557
|
+
import * as fs30 from "fs";
|
|
4565
5558
|
function check(name, fn) {
|
|
4566
5559
|
try {
|
|
4567
5560
|
const detail = fn() ?? void 0;
|
|
@@ -4573,7 +5566,7 @@ function check(name, fn) {
|
|
|
4573
5566
|
function runPreflight() {
|
|
4574
5567
|
const checks = [
|
|
4575
5568
|
check("claude CLI", () => {
|
|
4576
|
-
const v =
|
|
5569
|
+
const v = execFileSync16("claude", ["--version"], {
|
|
4577
5570
|
encoding: "utf-8",
|
|
4578
5571
|
timeout: 1e4,
|
|
4579
5572
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4581,14 +5574,14 @@ function runPreflight() {
|
|
|
4581
5574
|
return v;
|
|
4582
5575
|
}),
|
|
4583
5576
|
check("git repo", () => {
|
|
4584
|
-
|
|
5577
|
+
execFileSync16("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
4585
5578
|
encoding: "utf-8",
|
|
4586
5579
|
timeout: 5e3,
|
|
4587
5580
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4588
5581
|
});
|
|
4589
5582
|
}),
|
|
4590
5583
|
check("pnpm", () => {
|
|
4591
|
-
const v =
|
|
5584
|
+
const v = execFileSync16("pnpm", ["--version"], {
|
|
4592
5585
|
encoding: "utf-8",
|
|
4593
5586
|
timeout: 5e3,
|
|
4594
5587
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4596,7 +5589,7 @@ function runPreflight() {
|
|
|
4596
5589
|
return v;
|
|
4597
5590
|
}),
|
|
4598
5591
|
check("node >= 18", () => {
|
|
4599
|
-
const v =
|
|
5592
|
+
const v = execFileSync16("node", ["--version"], {
|
|
4600
5593
|
encoding: "utf-8",
|
|
4601
5594
|
timeout: 5e3,
|
|
4602
5595
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4606,7 +5599,7 @@ function runPreflight() {
|
|
|
4606
5599
|
return v;
|
|
4607
5600
|
}),
|
|
4608
5601
|
check("gh CLI", () => {
|
|
4609
|
-
const v =
|
|
5602
|
+
const v = execFileSync16("gh", ["--version"], {
|
|
4610
5603
|
encoding: "utf-8",
|
|
4611
5604
|
timeout: 5e3,
|
|
4612
5605
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4614,7 +5607,7 @@ function runPreflight() {
|
|
|
4614
5607
|
return v;
|
|
4615
5608
|
}),
|
|
4616
5609
|
check("package.json", () => {
|
|
4617
|
-
if (!
|
|
5610
|
+
if (!fs30.existsSync("package.json")) throw new Error("not found");
|
|
4618
5611
|
})
|
|
4619
5612
|
];
|
|
4620
5613
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -4691,8 +5684,8 @@ var init_args = __esm({
|
|
|
4691
5684
|
});
|
|
4692
5685
|
|
|
4693
5686
|
// src/cli/task-state.ts
|
|
4694
|
-
import * as
|
|
4695
|
-
import * as
|
|
5687
|
+
import * as fs31 from "fs";
|
|
5688
|
+
import * as path28 from "path";
|
|
4696
5689
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
4697
5690
|
if (!existingTaskId || !existingState) {
|
|
4698
5691
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -4724,11 +5717,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
4724
5717
|
function resolveForIssue(issueNumber, projectDir) {
|
|
4725
5718
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
4726
5719
|
if (existingTaskId) {
|
|
4727
|
-
const statusPath =
|
|
5720
|
+
const statusPath = path28.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
4728
5721
|
let existingState = null;
|
|
4729
|
-
if (
|
|
5722
|
+
if (fs31.existsSync(statusPath)) {
|
|
4730
5723
|
try {
|
|
4731
|
-
existingState = JSON.parse(
|
|
5724
|
+
existingState = JSON.parse(fs31.readFileSync(statusPath, "utf-8"));
|
|
4732
5725
|
} catch {
|
|
4733
5726
|
}
|
|
4734
5727
|
}
|
|
@@ -4761,12 +5754,12 @@ var resolve_exports = {};
|
|
|
4761
5754
|
__export(resolve_exports, {
|
|
4762
5755
|
runResolve: () => runResolve
|
|
4763
5756
|
});
|
|
4764
|
-
import { execFileSync as
|
|
5757
|
+
import { execFileSync as execFileSync17 } from "child_process";
|
|
4765
5758
|
function getConflictContext(cwd, files) {
|
|
4766
5759
|
const parts = [];
|
|
4767
5760
|
for (const file of files.slice(0, 10)) {
|
|
4768
5761
|
try {
|
|
4769
|
-
const content =
|
|
5762
|
+
const content = execFileSync17("git", ["diff", file], {
|
|
4770
5763
|
cwd,
|
|
4771
5764
|
encoding: "utf-8",
|
|
4772
5765
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4816,12 +5809,12 @@ async function runResolve(options) {
|
|
|
4816
5809
|
extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
|
|
4817
5810
|
}
|
|
4818
5811
|
logger.info(` Running agent to resolve conflicts (model=${model})...`);
|
|
4819
|
-
const
|
|
5812
|
+
const result2 = await runner.run("resolve", prompt, model, 3e5, projectDir, {
|
|
4820
5813
|
cwd: projectDir,
|
|
4821
5814
|
env: extraEnv
|
|
4822
5815
|
});
|
|
4823
|
-
if (
|
|
4824
|
-
return { outcome: "failed", error: `Agent failed: ${
|
|
5816
|
+
if (result2.outcome !== "completed") {
|
|
5817
|
+
return { outcome: "failed", error: `Agent failed: ${result2.error}` };
|
|
4825
5818
|
}
|
|
4826
5819
|
logger.info(" Verifying resolution...");
|
|
4827
5820
|
const verify = runQualityGates(projectDir, projectDir);
|
|
@@ -4885,8 +5878,8 @@ var init_resolve = __esm({
|
|
|
4885
5878
|
|
|
4886
5879
|
// src/entry.ts
|
|
4887
5880
|
var entry_exports = {};
|
|
4888
|
-
import * as
|
|
4889
|
-
import * as
|
|
5881
|
+
import * as fs32 from "fs";
|
|
5882
|
+
import * as path29 from "path";
|
|
4890
5883
|
async function ensureLitellmProxy(config, projectDir) {
|
|
4891
5884
|
if (!anyStageNeedsProxy(config)) return null;
|
|
4892
5885
|
const litellmUrl = getLitellmUrl();
|
|
@@ -4931,19 +5924,19 @@ async function runModelHealthCheck(config) {
|
|
|
4931
5924
|
}
|
|
4932
5925
|
const model = config.agent.modelMap.cheap;
|
|
4933
5926
|
logger.info(`Model health check (${model} via ${usesProxy ? "LiteLLM" : "Anthropic"})...`);
|
|
4934
|
-
const
|
|
4935
|
-
if (
|
|
5927
|
+
const result2 = await checkModelHealth(baseUrl, apiKey, model);
|
|
5928
|
+
if (result2.ok) {
|
|
4936
5929
|
logger.info(" \u2713 Model responded");
|
|
4937
5930
|
} else {
|
|
4938
|
-
logger.error(` \u2717 Model health check failed: ${
|
|
5931
|
+
logger.error(` \u2717 Model health check failed: ${result2.error}`);
|
|
4939
5932
|
process.exit(1);
|
|
4940
5933
|
}
|
|
4941
5934
|
}
|
|
4942
5935
|
async function main() {
|
|
4943
5936
|
const input = parseArgs();
|
|
4944
|
-
const projectDir = input.cwd ?
|
|
5937
|
+
const projectDir = input.cwd ? path29.resolve(input.cwd) : process.cwd();
|
|
4945
5938
|
if (input.cwd) {
|
|
4946
|
-
if (!
|
|
5939
|
+
if (!fs32.existsSync(projectDir)) {
|
|
4947
5940
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
4948
5941
|
process.exit(1);
|
|
4949
5942
|
}
|
|
@@ -5009,8 +6002,8 @@ async function main() {
|
|
|
5009
6002
|
process.exit(1);
|
|
5010
6003
|
}
|
|
5011
6004
|
}
|
|
5012
|
-
const taskDir =
|
|
5013
|
-
|
|
6005
|
+
const taskDir = path29.join(projectDir, ".kody", "tasks", taskId);
|
|
6006
|
+
fs32.mkdirSync(taskDir, { recursive: true });
|
|
5014
6007
|
if (input.command === "rerun" && isTaskifyRun(taskDir)) {
|
|
5015
6008
|
const marker = readTaskifyMarker(taskDir);
|
|
5016
6009
|
if (marker) {
|
|
@@ -5075,7 +6068,7 @@ async function main() {
|
|
|
5075
6068
|
console.error(`Runner "${defaultRunnerName2}" health check failed`);
|
|
5076
6069
|
process.exit(1);
|
|
5077
6070
|
}
|
|
5078
|
-
const
|
|
6071
|
+
const result2 = await runStandaloneReview({
|
|
5079
6072
|
projectDir,
|
|
5080
6073
|
runners: runners2,
|
|
5081
6074
|
prTitle,
|
|
@@ -5085,15 +6078,15 @@ async function main() {
|
|
|
5085
6078
|
taskId
|
|
5086
6079
|
});
|
|
5087
6080
|
if (litellmProcess2) litellmProcess2.kill();
|
|
5088
|
-
if (
|
|
5089
|
-
console.error(`Review failed: ${
|
|
6081
|
+
if (result2.outcome === "failed") {
|
|
6082
|
+
console.error(`Review failed: ${result2.error}`);
|
|
5090
6083
|
process.exit(1);
|
|
5091
6084
|
}
|
|
5092
|
-
if (
|
|
5093
|
-
console.log(
|
|
6085
|
+
if (result2.reviewContent) {
|
|
6086
|
+
console.log(result2.reviewContent);
|
|
5094
6087
|
if (!input.local && prNumber) {
|
|
5095
|
-
const comment = formatReviewComment(
|
|
5096
|
-
const verdict = detectReviewVerdict(
|
|
6088
|
+
const comment = formatReviewComment(result2.reviewContent, taskId);
|
|
6089
|
+
const verdict = detectReviewVerdict(result2.reviewContent);
|
|
5097
6090
|
const event = verdict === "fail" ? "request-changes" : "approve";
|
|
5098
6091
|
const posted = submitPRReview(prNumber, comment, event);
|
|
5099
6092
|
if (!posted) {
|
|
@@ -5125,48 +6118,48 @@ async function main() {
|
|
|
5125
6118
|
process.exit(1);
|
|
5126
6119
|
}
|
|
5127
6120
|
const { runResolve: runResolve2 } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
|
|
5128
|
-
const
|
|
6121
|
+
const result2 = await runResolve2({
|
|
5129
6122
|
prNumber: input.prNumber,
|
|
5130
6123
|
projectDir,
|
|
5131
6124
|
runners: runners2,
|
|
5132
6125
|
local: input.local ?? true
|
|
5133
6126
|
});
|
|
5134
6127
|
if (litellmProcess2) litellmProcess2.kill();
|
|
5135
|
-
if (
|
|
5136
|
-
console.error(`Resolve failed: ${
|
|
6128
|
+
if (result2.outcome === "failed") {
|
|
6129
|
+
console.error(`Resolve failed: ${result2.error}`);
|
|
5137
6130
|
process.exit(1);
|
|
5138
6131
|
}
|
|
5139
|
-
console.log(`Resolve: ${
|
|
6132
|
+
console.log(`Resolve: ${result2.outcome}`);
|
|
5140
6133
|
process.exit(0);
|
|
5141
6134
|
}
|
|
5142
6135
|
logger.info("Preflight checks:");
|
|
5143
6136
|
runPreflight();
|
|
5144
6137
|
if (input.task) {
|
|
5145
|
-
|
|
6138
|
+
fs32.writeFileSync(path29.join(taskDir, "task.md"), input.task);
|
|
5146
6139
|
}
|
|
5147
|
-
const taskMdPath =
|
|
5148
|
-
if (!
|
|
6140
|
+
const taskMdPath = path29.join(taskDir, "task.md");
|
|
6141
|
+
if (!fs32.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
5149
6142
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
5150
6143
|
const prDetails = getPRDetails(input.prNumber);
|
|
5151
6144
|
if (prDetails) {
|
|
5152
6145
|
const taskContent = `# ${prDetails.title}
|
|
5153
6146
|
|
|
5154
6147
|
${prDetails.body ?? ""}`;
|
|
5155
|
-
|
|
6148
|
+
fs32.writeFileSync(taskMdPath, taskContent);
|
|
5156
6149
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
5157
6150
|
}
|
|
5158
|
-
} else if (!
|
|
6151
|
+
} else if (!fs32.existsSync(taskMdPath) && input.issueNumber) {
|
|
5159
6152
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
5160
6153
|
const issue = getIssue(input.issueNumber);
|
|
5161
6154
|
if (issue) {
|
|
5162
6155
|
const taskContent = `# ${issue.title}
|
|
5163
6156
|
|
|
5164
6157
|
${issue.body ?? ""}`;
|
|
5165
|
-
|
|
6158
|
+
fs32.writeFileSync(taskMdPath, taskContent);
|
|
5166
6159
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
5167
6160
|
}
|
|
5168
6161
|
}
|
|
5169
|
-
if (!
|
|
6162
|
+
if (!fs32.existsSync(taskMdPath)) {
|
|
5170
6163
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
5171
6164
|
process.exit(1);
|
|
5172
6165
|
}
|
|
@@ -5304,7 +6297,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
5304
6297
|
}
|
|
5305
6298
|
}
|
|
5306
6299
|
const state = await runPipeline(ctx);
|
|
5307
|
-
const files =
|
|
6300
|
+
const files = fs32.readdirSync(taskDir);
|
|
5308
6301
|
console.log(`
|
|
5309
6302
|
Artifacts in ${taskDir}:`);
|
|
5310
6303
|
for (const f of files) {
|
|
@@ -5369,8 +6362,8 @@ var init_entry = __esm({
|
|
|
5369
6362
|
});
|
|
5370
6363
|
|
|
5371
6364
|
// src/bin/cli.ts
|
|
5372
|
-
import * as
|
|
5373
|
-
import * as
|
|
6365
|
+
import * as fs33 from "fs";
|
|
6366
|
+
import * as path30 from "path";
|
|
5374
6367
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5375
6368
|
|
|
5376
6369
|
// src/bin/commands/init.ts
|
|
@@ -5750,7 +6743,7 @@ import { execFileSync as execFileSync5 } from "child_process";
|
|
|
5750
6743
|
import * as fs5 from "fs";
|
|
5751
6744
|
import * as path4 from "path";
|
|
5752
6745
|
function discoverQaContext(cwd) {
|
|
5753
|
-
const
|
|
6746
|
+
const result2 = {
|
|
5754
6747
|
routes: [],
|
|
5755
6748
|
authFiles: [],
|
|
5756
6749
|
loginPage: null,
|
|
@@ -5763,21 +6756,21 @@ function discoverQaContext(cwd) {
|
|
|
5763
6756
|
const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
|
|
5764
6757
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5765
6758
|
const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
5766
|
-
if (pkg.scripts?.dev)
|
|
5767
|
-
if (allDeps.next || allDeps.nuxt)
|
|
5768
|
-
else if (allDeps.vite)
|
|
6759
|
+
if (pkg.scripts?.dev) result2.devCommand = `${pm} dev`;
|
|
6760
|
+
if (allDeps.next || allDeps.nuxt) result2.devPort = 3e3;
|
|
6761
|
+
else if (allDeps.vite) result2.devPort = 5173;
|
|
5769
6762
|
} catch {
|
|
5770
6763
|
}
|
|
5771
6764
|
const appDirs = ["src/app", "app"];
|
|
5772
6765
|
for (const appDir of appDirs) {
|
|
5773
6766
|
const fullAppDir = path4.join(cwd, appDir);
|
|
5774
6767
|
if (!fs5.existsSync(fullAppDir)) continue;
|
|
5775
|
-
scanRoutes(fullAppDir, appDir, "",
|
|
6768
|
+
scanRoutes(fullAppDir, appDir, "", result2);
|
|
5776
6769
|
break;
|
|
5777
6770
|
}
|
|
5778
6771
|
const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
5779
6772
|
for (const p of authPatterns) {
|
|
5780
|
-
if (fs5.existsSync(path4.join(cwd, p)))
|
|
6773
|
+
if (fs5.existsSync(path4.join(cwd, p))) result2.authFiles.push(p);
|
|
5781
6774
|
}
|
|
5782
6775
|
const authConfigGlobs = [
|
|
5783
6776
|
"src/app/api/auth",
|
|
@@ -5788,7 +6781,7 @@ function discoverQaContext(cwd) {
|
|
|
5788
6781
|
"src/app/api/oauth"
|
|
5789
6782
|
];
|
|
5790
6783
|
for (const g of authConfigGlobs) {
|
|
5791
|
-
if (fs5.existsSync(path4.join(cwd, g)))
|
|
6784
|
+
if (fs5.existsSync(path4.join(cwd, g))) result2.authFiles.push(g);
|
|
5792
6785
|
}
|
|
5793
6786
|
try {
|
|
5794
6787
|
const rolePaths = [
|
|
@@ -5810,7 +6803,7 @@ function discoverQaContext(cwd) {
|
|
|
5810
6803
|
if (roleMatches) {
|
|
5811
6804
|
for (const m of roleMatches) {
|
|
5812
6805
|
const val = m.match(/['"](\w+)['"]/);
|
|
5813
|
-
if (val && !
|
|
6806
|
+
if (val && !result2.roles.includes(val[1])) result2.roles.push(val[1]);
|
|
5814
6807
|
}
|
|
5815
6808
|
}
|
|
5816
6809
|
const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
|
|
@@ -5819,7 +6812,7 @@ function discoverQaContext(cwd) {
|
|
|
5819
6812
|
if (vals) {
|
|
5820
6813
|
for (const v of vals) {
|
|
5821
6814
|
const clean = v.replace(/['"]/g, "");
|
|
5822
|
-
if (!
|
|
6815
|
+
if (!result2.roles.includes(clean)) result2.roles.push(clean);
|
|
5823
6816
|
}
|
|
5824
6817
|
}
|
|
5825
6818
|
}
|
|
@@ -5829,9 +6822,9 @@ function discoverQaContext(cwd) {
|
|
|
5829
6822
|
}
|
|
5830
6823
|
} catch {
|
|
5831
6824
|
}
|
|
5832
|
-
return
|
|
6825
|
+
return result2;
|
|
5833
6826
|
}
|
|
5834
|
-
function scanRoutes(dir, baseDir, prefix,
|
|
6827
|
+
function scanRoutes(dir, baseDir, prefix, result2) {
|
|
5835
6828
|
let entries;
|
|
5836
6829
|
try {
|
|
5837
6830
|
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
@@ -5842,16 +6835,16 @@ function scanRoutes(dir, baseDir, prefix, result) {
|
|
|
5842
6835
|
if (hasPage) {
|
|
5843
6836
|
const routePath = prefix || "/";
|
|
5844
6837
|
const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
|
|
5845
|
-
|
|
5846
|
-
if (prefix.includes("/login"))
|
|
5847
|
-
if (prefix.startsWith("/admin") && !
|
|
6838
|
+
result2.routes.push({ path: routePath, group });
|
|
6839
|
+
if (prefix.includes("/login")) result2.loginPage = routePath;
|
|
6840
|
+
if (prefix.startsWith("/admin") && !result2.adminPath) result2.adminPath = prefix;
|
|
5848
6841
|
}
|
|
5849
6842
|
for (const entry of entries) {
|
|
5850
6843
|
if (!entry.isDirectory()) continue;
|
|
5851
6844
|
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
5852
6845
|
let segment = entry.name;
|
|
5853
6846
|
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
5854
|
-
scanRoutes(path4.join(dir, entry.name), baseDir, prefix,
|
|
6847
|
+
scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result2);
|
|
5855
6848
|
continue;
|
|
5856
6849
|
}
|
|
5857
6850
|
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
@@ -5860,7 +6853,7 @@ function scanRoutes(dir, baseDir, prefix, result) {
|
|
|
5860
6853
|
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
5861
6854
|
segment = `:${segment.slice(2, -2)}?`;
|
|
5862
6855
|
}
|
|
5863
|
-
scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`,
|
|
6856
|
+
scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result2);
|
|
5864
6857
|
}
|
|
5865
6858
|
}
|
|
5866
6859
|
function generateQaGuide(discovery) {
|
|
@@ -6544,11 +7537,11 @@ Create it manually.`, cwd);
|
|
|
6544
7537
|
|
|
6545
7538
|
// src/bin/cli.ts
|
|
6546
7539
|
init_architecture_detection();
|
|
6547
|
-
var __dirname2 =
|
|
6548
|
-
var PKG_ROOT =
|
|
7540
|
+
var __dirname2 = path30.dirname(fileURLToPath2(import.meta.url));
|
|
7541
|
+
var PKG_ROOT = path30.resolve(__dirname2, "..", "..");
|
|
6549
7542
|
function getVersion() {
|
|
6550
|
-
const pkgPath =
|
|
6551
|
-
const pkg = JSON.parse(
|
|
7543
|
+
const pkgPath = path30.join(PKG_ROOT, "package.json");
|
|
7544
|
+
const pkg = JSON.parse(fs33.readFileSync(pkgPath, "utf-8"));
|
|
6552
7545
|
return pkg.version;
|
|
6553
7546
|
}
|
|
6554
7547
|
var args = process.argv.slice(2);
|
|
@@ -6559,6 +7552,8 @@ if (command === "init") {
|
|
|
6559
7552
|
bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
6560
7553
|
} else if (command === "taskify") {
|
|
6561
7554
|
Promise.resolve().then(() => (init_taskify_command(), taskify_command_exports)).then(({ runTaskifyCommand: runTaskifyCommand2 }) => runTaskifyCommand2());
|
|
7555
|
+
} else if (command === "test-model") {
|
|
7556
|
+
Promise.resolve().then(() => (init_test_model_command(), test_model_command_exports)).then(({ runTestModelCommand: runTestModelCommand2 }) => runTestModelCommand2());
|
|
6562
7557
|
} else if (command === "ci-parse") {
|
|
6563
7558
|
Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
|
|
6564
7559
|
} else if (command === "version" || command === "--version" || command === "-v") {
|