@kody-ade/kody-engine-lite 0.1.115 → 0.1.117
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 +455 -1439
- package/kody.config.schema.json +0 -31
- package/package.json +1 -1
- package/templates/kody.yml +0 -1
- package/dist/agent-runner.d.ts +0 -4
- package/dist/agent-runner.js +0 -122
- package/dist/ci/parse-inputs.d.ts +0 -6
- package/dist/ci/parse-inputs.js +0 -76
- package/dist/ci/parse-safety.d.ts +0 -6
- package/dist/ci/parse-safety.js +0 -22
- package/dist/cli/args.d.ts +0 -13
- package/dist/cli/args.js +0 -42
- package/dist/cli/litellm.d.ts +0 -2
- package/dist/cli/litellm.js +0 -85
- package/dist/cli/task-resolution.d.ts +0 -2
- package/dist/cli/task-resolution.js +0 -41
- package/dist/config.d.ts +0 -49
- package/dist/config.js +0 -72
- package/dist/context.d.ts +0 -4
- package/dist/context.js +0 -83
- package/dist/definitions.d.ts +0 -3
- package/dist/definitions.js +0 -59
- package/dist/entry.d.ts +0 -1
- package/dist/entry.js +0 -236
- package/dist/git-utils.d.ts +0 -13
- package/dist/git-utils.js +0 -174
- package/dist/github-api.d.ts +0 -14
- package/dist/github-api.js +0 -114
- package/dist/kody-utils.d.ts +0 -1
- package/dist/kody-utils.js +0 -9
- package/dist/learning/auto-learn.d.ts +0 -2
- package/dist/learning/auto-learn.js +0 -169
- package/dist/logger.d.ts +0 -14
- package/dist/logger.js +0 -51
- package/dist/memory.d.ts +0 -1
- package/dist/memory.js +0 -20
- package/dist/observer.d.ts +0 -9
- package/dist/observer.js +0 -80
- package/dist/pipeline/complexity.d.ts +0 -3
- package/dist/pipeline/complexity.js +0 -12
- package/dist/pipeline/executor-registry.d.ts +0 -3
- package/dist/pipeline/executor-registry.js +0 -20
- package/dist/pipeline/hooks.d.ts +0 -17
- package/dist/pipeline/hooks.js +0 -110
- package/dist/pipeline/questions.d.ts +0 -2
- package/dist/pipeline/questions.js +0 -44
- package/dist/pipeline/runner-selection.d.ts +0 -2
- package/dist/pipeline/runner-selection.js +0 -13
- package/dist/pipeline/state.d.ts +0 -4
- package/dist/pipeline/state.js +0 -37
- package/dist/pipeline.d.ts +0 -3
- package/dist/pipeline.js +0 -213
- package/dist/preflight.d.ts +0 -1
- package/dist/preflight.js +0 -69
- package/dist/retrospective.d.ts +0 -26
- package/dist/retrospective.js +0 -211
- package/dist/stages/agent.d.ts +0 -2
- package/dist/stages/agent.js +0 -94
- package/dist/stages/gate.d.ts +0 -2
- package/dist/stages/gate.js +0 -32
- package/dist/stages/review.d.ts +0 -2
- package/dist/stages/review.js +0 -32
- package/dist/stages/ship.d.ts +0 -3
- package/dist/stages/ship.js +0 -154
- package/dist/stages/verify.d.ts +0 -2
- package/dist/stages/verify.js +0 -94
- package/dist/types.d.ts +0 -61
- package/dist/types.js +0 -1
- package/dist/validators.d.ts +0 -8
- package/dist/validators.js +0 -42
- package/dist/verify-runner.d.ts +0 -11
- package/dist/verify-runner.js +0 -110
package/dist/bin/cli.js
CHANGED
|
@@ -180,8 +180,8 @@ var init_validators = __esm({
|
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
// src/config.ts
|
|
183
|
-
import * as
|
|
184
|
-
import * as
|
|
183
|
+
import * as fs8 from "fs";
|
|
184
|
+
import * as path7 from "path";
|
|
185
185
|
function resolveStageConfig(config, stageName, modelTier) {
|
|
186
186
|
const stageOverride = config.agent.stages?.[stageName];
|
|
187
187
|
if (stageOverride) return stageOverride;
|
|
@@ -223,16 +223,16 @@ function setConfigDir(dir) {
|
|
|
223
223
|
}
|
|
224
224
|
function getProjectConfig() {
|
|
225
225
|
if (_config) return _config;
|
|
226
|
-
const configPath =
|
|
227
|
-
if (
|
|
226
|
+
const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
227
|
+
if (fs8.existsSync(configPath)) {
|
|
228
228
|
try {
|
|
229
|
-
const
|
|
230
|
-
if (!
|
|
231
|
-
logger.warn(`kody.config.json: ${
|
|
229
|
+
const result = parseJsonSafe(fs8.readFileSync(configPath, "utf-8"));
|
|
230
|
+
if (!result.ok) {
|
|
231
|
+
logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
|
|
232
232
|
_config = { ...DEFAULT_CONFIG };
|
|
233
233
|
return _config;
|
|
234
234
|
}
|
|
235
|
-
const raw =
|
|
235
|
+
const raw = result.data;
|
|
236
236
|
_config = {
|
|
237
237
|
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
238
238
|
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
@@ -283,7 +283,7 @@ var init_config = __esm({
|
|
|
283
283
|
repo: ""
|
|
284
284
|
},
|
|
285
285
|
agent: {
|
|
286
|
-
modelMap: { cheap: "
|
|
286
|
+
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
287
287
|
},
|
|
288
288
|
contextTiers: {
|
|
289
289
|
enabled: true,
|
|
@@ -389,11 +389,10 @@ function createClaudeCodeRunner() {
|
|
|
389
389
|
model,
|
|
390
390
|
"--dangerously-skip-permissions"
|
|
391
391
|
];
|
|
392
|
-
const baseTools = "Bash,Edit,Read,Write,Glob,Grep";
|
|
393
392
|
if (options?.mcpConfigJson) {
|
|
394
393
|
args2.push("--mcp-config", options.mcpConfigJson);
|
|
395
394
|
} else {
|
|
396
|
-
args2.push("--allowedTools",
|
|
395
|
+
args2.push("--allowedTools", "Bash,Edit,Read,Write,Glob,Grep");
|
|
397
396
|
}
|
|
398
397
|
if (options?.sessionId) {
|
|
399
398
|
if (options.resumeSession) {
|
|
@@ -966,8 +965,8 @@ function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
|
966
965
|
}
|
|
967
966
|
function generateTaskId() {
|
|
968
967
|
const now = /* @__PURE__ */ new Date();
|
|
969
|
-
const
|
|
970
|
-
return `${String(now.getFullYear()).slice(2)}${
|
|
968
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
969
|
+
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
971
970
|
}
|
|
972
971
|
function resolveTaskIdFromComments(issueNumber) {
|
|
973
972
|
try {
|
|
@@ -1193,7 +1192,6 @@ var init_litellm = __esm({
|
|
|
1193
1192
|
// src/cli/taskify-command.ts
|
|
1194
1193
|
var taskify_command_exports = {};
|
|
1195
1194
|
__export(taskify_command_exports, {
|
|
1196
|
-
TaskifyError: () => TaskifyError,
|
|
1197
1195
|
isTaskifyRun: () => isTaskifyRun,
|
|
1198
1196
|
readTaskifyMarker: () => readTaskifyMarker,
|
|
1199
1197
|
runTaskifyCommand: () => runTaskifyCommand,
|
|
@@ -1285,17 +1283,8 @@ async function runTaskifyCommand() {
|
|
|
1285
1283
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
|
|
1286
1284
|
};
|
|
1287
1285
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
} catch (err) {
|
|
1291
|
-
if (err instanceof TaskifyError) {
|
|
1292
|
-
logger.error(`[taskify] ${err.message}`);
|
|
1293
|
-
process.exit(1);
|
|
1294
|
-
}
|
|
1295
|
-
throw err;
|
|
1296
|
-
} finally {
|
|
1297
|
-
litellmProcess?.kill();
|
|
1298
|
-
}
|
|
1286
|
+
await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
|
|
1287
|
+
litellmProcess?.kill();
|
|
1299
1288
|
}
|
|
1300
1289
|
async function taskifyCommand(opts) {
|
|
1301
1290
|
const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
|
|
@@ -1310,6 +1299,7 @@ async function taskifyCommand(opts) {
|
|
|
1310
1299
|
mcpConfigJson = buildTaskifyMcpConfigJson(config);
|
|
1311
1300
|
} catch (err) {
|
|
1312
1301
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1302
|
+
logger.error(`[taskify] MCP config error: ${msg}`);
|
|
1313
1303
|
if (issueNumber && !local) {
|
|
1314
1304
|
postComment(
|
|
1315
1305
|
issueNumber,
|
|
@@ -1320,7 +1310,7 @@ async function taskifyCommand(opts) {
|
|
|
1320
1310
|
Add the required MCP server config to \`kody.config.json\` and try again.`
|
|
1321
1311
|
);
|
|
1322
1312
|
}
|
|
1323
|
-
|
|
1313
|
+
process.exit(1);
|
|
1324
1314
|
}
|
|
1325
1315
|
}
|
|
1326
1316
|
const sc = resolveStageConfig(config, "taskify", "strong");
|
|
@@ -1361,44 +1351,47 @@ Kody is decomposing ${src} into tasks...`);
|
|
|
1361
1351
|
fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
|
|
1362
1352
|
const runner = opts.runner ?? createClaudeCodeRunner();
|
|
1363
1353
|
logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
|
|
1364
|
-
const
|
|
1354
|
+
const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
|
|
1365
1355
|
cwd: projectDir,
|
|
1366
1356
|
mcpConfigJson,
|
|
1367
1357
|
env: opts.runnerEnv
|
|
1368
1358
|
});
|
|
1369
|
-
if (
|
|
1370
|
-
const errMsg =
|
|
1359
|
+
if (result.outcome !== "completed") {
|
|
1360
|
+
const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
|
|
1361
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1371
1362
|
if (issueNumber && !local) {
|
|
1372
1363
|
postComment(issueNumber, `Kody taskify failed:
|
|
1373
1364
|
|
|
1374
1365
|
> ${errMsg}`);
|
|
1375
1366
|
setLifecycleLabel(issueNumber, "failed");
|
|
1376
1367
|
}
|
|
1377
|
-
|
|
1368
|
+
process.exit(1);
|
|
1378
1369
|
}
|
|
1379
1370
|
const resultPath = path10.join(taskDir, RESULT_FILE);
|
|
1380
1371
|
if (!fs11.existsSync(resultPath)) {
|
|
1381
1372
|
const errMsg = `Claude did not write ${RESULT_FILE}. Output:
|
|
1382
1373
|
|
|
1383
|
-
${
|
|
1374
|
+
${result.output?.slice(0, 500) ?? "(none)"}`;
|
|
1375
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1384
1376
|
if (issueNumber && !local) {
|
|
1385
1377
|
postComment(issueNumber, `Kody taskify failed: result file not found.
|
|
1386
1378
|
|
|
1387
1379
|
${errMsg}`);
|
|
1388
1380
|
setLifecycleLabel(issueNumber, "failed");
|
|
1389
1381
|
}
|
|
1390
|
-
|
|
1382
|
+
process.exit(1);
|
|
1391
1383
|
}
|
|
1392
1384
|
let parsed;
|
|
1393
1385
|
try {
|
|
1394
1386
|
parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
|
|
1395
1387
|
} catch {
|
|
1396
1388
|
const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
|
|
1389
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1397
1390
|
if (issueNumber && !local) {
|
|
1398
1391
|
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1399
1392
|
setLifecycleLabel(issueNumber, "failed");
|
|
1400
1393
|
}
|
|
1401
|
-
|
|
1394
|
+
process.exit(1);
|
|
1402
1395
|
}
|
|
1403
1396
|
const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
|
|
1404
1397
|
if (parsed.status === "questions") {
|
|
@@ -1407,11 +1400,12 @@ ${errMsg}`);
|
|
|
1407
1400
|
await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
|
|
1408
1401
|
} else {
|
|
1409
1402
|
const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
|
|
1403
|
+
logger.error(`[taskify] ${errMsg}`);
|
|
1410
1404
|
if (issueNumber && !local) {
|
|
1411
1405
|
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1412
1406
|
setLifecycleLabel(issueNumber, "failed");
|
|
1413
1407
|
}
|
|
1414
|
-
|
|
1408
|
+
process.exit(1);
|
|
1415
1409
|
}
|
|
1416
1410
|
}
|
|
1417
1411
|
function handleQuestions(parsed, ticketId, issueNumber, local) {
|
|
@@ -1542,7 +1536,7 @@ function readTaskifyMarker(taskDir) {
|
|
|
1542
1536
|
return null;
|
|
1543
1537
|
}
|
|
1544
1538
|
}
|
|
1545
|
-
var __dirname,
|
|
1539
|
+
var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
|
|
1546
1540
|
var init_taskify_command = __esm({
|
|
1547
1541
|
"src/cli/taskify-command.ts"() {
|
|
1548
1542
|
"use strict";
|
|
@@ -1554,12 +1548,6 @@ var init_taskify_command = __esm({
|
|
|
1554
1548
|
init_task_resolution();
|
|
1555
1549
|
init_litellm();
|
|
1556
1550
|
__dirname = path10.dirname(fileURLToPath(import.meta.url));
|
|
1557
|
-
TaskifyError = class extends Error {
|
|
1558
|
-
constructor(message) {
|
|
1559
|
-
super(message);
|
|
1560
|
-
this.name = "TaskifyError";
|
|
1561
|
-
}
|
|
1562
|
-
};
|
|
1563
1551
|
AUTO_TRIGGER_THRESHOLD = 5;
|
|
1564
1552
|
MAX_TASKS_GUARD = 20;
|
|
1565
1553
|
TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -1568,999 +1556,6 @@ var init_taskify_command = __esm({
|
|
|
1568
1556
|
}
|
|
1569
1557
|
});
|
|
1570
1558
|
|
|
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
|
-
|
|
2564
1559
|
// src/ci/parse-inputs.ts
|
|
2565
1560
|
var parse_inputs_exports = {};
|
|
2566
1561
|
__export(parse_inputs_exports, {
|
|
@@ -2568,16 +1563,16 @@ __export(parse_inputs_exports, {
|
|
|
2568
1563
|
runCiParse: () => runCiParse,
|
|
2569
1564
|
writeOutputs: () => writeOutputs
|
|
2570
1565
|
});
|
|
2571
|
-
import * as
|
|
1566
|
+
import * as fs12 from "fs";
|
|
2572
1567
|
function generateTimestamp() {
|
|
2573
1568
|
const now = /* @__PURE__ */ new Date();
|
|
2574
|
-
const
|
|
1569
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2575
1570
|
const y = String(now.getFullYear()).slice(2);
|
|
2576
|
-
const m =
|
|
2577
|
-
const d =
|
|
2578
|
-
const H =
|
|
2579
|
-
const M =
|
|
2580
|
-
const S =
|
|
1571
|
+
const m = pad(now.getMonth() + 1);
|
|
1572
|
+
const d = pad(now.getDate());
|
|
1573
|
+
const H = pad(now.getHours());
|
|
1574
|
+
const M = pad(now.getMinutes());
|
|
1575
|
+
const S = pad(now.getSeconds());
|
|
2581
1576
|
return `${y}${m}${d}-${H}${M}${S}`;
|
|
2582
1577
|
}
|
|
2583
1578
|
function parseCommentInputs() {
|
|
@@ -2729,40 +1724,40 @@ function parseCommentInputs() {
|
|
|
2729
1724
|
trigger_type: "comment"
|
|
2730
1725
|
};
|
|
2731
1726
|
}
|
|
2732
|
-
function writeOutputs(
|
|
1727
|
+
function writeOutputs(result) {
|
|
2733
1728
|
const outputFile = process.env.GITHUB_OUTPUT;
|
|
2734
1729
|
function output(key, value) {
|
|
2735
1730
|
if (outputFile) {
|
|
2736
1731
|
if (value.includes("\n")) {
|
|
2737
|
-
|
|
1732
|
+
fs12.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
2738
1733
|
${value}
|
|
2739
1734
|
KODY_EOF
|
|
2740
1735
|
`);
|
|
2741
1736
|
} else {
|
|
2742
|
-
|
|
1737
|
+
fs12.appendFileSync(outputFile, `${key}=${value}
|
|
2743
1738
|
`);
|
|
2744
1739
|
}
|
|
2745
1740
|
}
|
|
2746
1741
|
const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
|
|
2747
1742
|
console.log(`${key}=${display}`);
|
|
2748
1743
|
}
|
|
2749
|
-
output("task_id",
|
|
2750
|
-
output("mode",
|
|
2751
|
-
output("from_stage",
|
|
2752
|
-
output("issue_number",
|
|
2753
|
-
output("pr_number",
|
|
2754
|
-
output("feedback",
|
|
2755
|
-
output("complexity",
|
|
2756
|
-
output("ci_run_id",
|
|
2757
|
-
output("ticket_id",
|
|
2758
|
-
output("prd_file",
|
|
2759
|
-
output("dry_run",
|
|
2760
|
-
output("valid",
|
|
2761
|
-
output("trigger_type",
|
|
1744
|
+
output("task_id", result.task_id);
|
|
1745
|
+
output("mode", result.mode);
|
|
1746
|
+
output("from_stage", result.from_stage);
|
|
1747
|
+
output("issue_number", result.issue_number);
|
|
1748
|
+
output("pr_number", result.pr_number);
|
|
1749
|
+
output("feedback", result.feedback);
|
|
1750
|
+
output("complexity", result.complexity);
|
|
1751
|
+
output("ci_run_id", result.ci_run_id);
|
|
1752
|
+
output("ticket_id", result.ticket_id);
|
|
1753
|
+
output("prd_file", result.prd_file);
|
|
1754
|
+
output("dry_run", result.dry_run ? "true" : "false");
|
|
1755
|
+
output("valid", result.valid ? "true" : "false");
|
|
1756
|
+
output("trigger_type", result.trigger_type);
|
|
2762
1757
|
}
|
|
2763
1758
|
function runCiParse() {
|
|
2764
|
-
const
|
|
2765
|
-
writeOutputs(
|
|
1759
|
+
const result = parseCommentInputs();
|
|
1760
|
+
writeOutputs(result);
|
|
2766
1761
|
}
|
|
2767
1762
|
var VALID_MODES;
|
|
2768
1763
|
var init_parse_inputs = __esm({
|
|
@@ -2864,7 +1859,7 @@ var init_definitions = __esm({
|
|
|
2864
1859
|
});
|
|
2865
1860
|
|
|
2866
1861
|
// src/git-utils.ts
|
|
2867
|
-
import { execFileSync as
|
|
1862
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
2868
1863
|
function getHookSafeEnv() {
|
|
2869
1864
|
if (!_hookSafeEnv) {
|
|
2870
1865
|
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
@@ -2872,7 +1867,7 @@ function getHookSafeEnv() {
|
|
|
2872
1867
|
return _hookSafeEnv;
|
|
2873
1868
|
}
|
|
2874
1869
|
function git(args2, options) {
|
|
2875
|
-
return
|
|
1870
|
+
return execFileSync10("git", args2, {
|
|
2876
1871
|
encoding: "utf-8",
|
|
2877
1872
|
timeout: options?.timeout ?? 3e4,
|
|
2878
1873
|
cwd: options?.cwd,
|
|
@@ -3058,22 +2053,22 @@ var init_git_utils = __esm({
|
|
|
3058
2053
|
});
|
|
3059
2054
|
|
|
3060
2055
|
// src/pipeline/state.ts
|
|
3061
|
-
import * as
|
|
3062
|
-
import * as
|
|
2056
|
+
import * as fs13 from "fs";
|
|
2057
|
+
import * as path11 from "path";
|
|
3063
2058
|
function loadState(taskId, taskDir) {
|
|
3064
|
-
const p =
|
|
3065
|
-
if (!
|
|
2059
|
+
const p = path11.join(taskDir, "status.json");
|
|
2060
|
+
if (!fs13.existsSync(p)) return null;
|
|
3066
2061
|
try {
|
|
3067
|
-
const
|
|
3068
|
-
|
|
2062
|
+
const result = parseJsonSafe(
|
|
2063
|
+
fs13.readFileSync(p, "utf-8"),
|
|
3069
2064
|
["taskId", "state", "stages", "createdAt", "updatedAt"]
|
|
3070
2065
|
);
|
|
3071
|
-
if (!
|
|
3072
|
-
logger.warn(` Corrupt status.json: ${
|
|
2066
|
+
if (!result.ok) {
|
|
2067
|
+
logger.warn(` Corrupt status.json: ${result.error}`);
|
|
3073
2068
|
return null;
|
|
3074
2069
|
}
|
|
3075
|
-
if (
|
|
3076
|
-
return
|
|
2070
|
+
if (result.data.taskId !== taskId) return null;
|
|
2071
|
+
return result.data;
|
|
3077
2072
|
} catch {
|
|
3078
2073
|
return null;
|
|
3079
2074
|
}
|
|
@@ -3083,10 +2078,10 @@ function writeState(state, taskDir) {
|
|
|
3083
2078
|
...state,
|
|
3084
2079
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3085
2080
|
};
|
|
3086
|
-
const target =
|
|
2081
|
+
const target = path11.join(taskDir, "status.json");
|
|
3087
2082
|
const tmp = target + ".tmp";
|
|
3088
|
-
|
|
3089
|
-
|
|
2083
|
+
fs13.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
2084
|
+
fs13.renameSync(tmp, target);
|
|
3090
2085
|
return updated;
|
|
3091
2086
|
}
|
|
3092
2087
|
function initState(taskId) {
|
|
@@ -3127,16 +2122,16 @@ var init_complexity = __esm({
|
|
|
3127
2122
|
});
|
|
3128
2123
|
|
|
3129
2124
|
// src/memory.ts
|
|
3130
|
-
import * as
|
|
3131
|
-
import * as
|
|
2125
|
+
import * as fs14 from "fs";
|
|
2126
|
+
import * as path12 from "path";
|
|
3132
2127
|
function readProjectMemory(projectDir) {
|
|
3133
|
-
const memoryDir =
|
|
3134
|
-
if (!
|
|
3135
|
-
const files =
|
|
2128
|
+
const memoryDir = path12.join(projectDir, ".kody", "memory");
|
|
2129
|
+
if (!fs14.existsSync(memoryDir)) return "";
|
|
2130
|
+
const files = fs14.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
3136
2131
|
if (files.length === 0) return "";
|
|
3137
2132
|
const sections = [];
|
|
3138
2133
|
for (const file of files) {
|
|
3139
|
-
const content =
|
|
2134
|
+
const content = fs14.readFileSync(path12.join(memoryDir, file), "utf-8").trim();
|
|
3140
2135
|
if (content) {
|
|
3141
2136
|
sections.push(`## ${file.replace(".md", "")}
|
|
3142
2137
|
${content}`);
|
|
@@ -3155,8 +2150,8 @@ var init_memory = __esm({
|
|
|
3155
2150
|
});
|
|
3156
2151
|
|
|
3157
2152
|
// src/context-tiers.ts
|
|
3158
|
-
import * as
|
|
3159
|
-
import * as
|
|
2153
|
+
import * as fs15 from "fs";
|
|
2154
|
+
import * as path13 from "path";
|
|
3160
2155
|
function estimateTokens(text) {
|
|
3161
2156
|
return Math.ceil(text.length / 4);
|
|
3162
2157
|
}
|
|
@@ -3183,8 +2178,8 @@ function generateL0(content, filename) {
|
|
|
3183
2178
|
break;
|
|
3184
2179
|
}
|
|
3185
2180
|
}
|
|
3186
|
-
const
|
|
3187
|
-
return
|
|
2181
|
+
const result = parts.join("\n");
|
|
2182
|
+
return result.slice(0, L0_MAX_CHARS);
|
|
3188
2183
|
}
|
|
3189
2184
|
function generateL0Json(content) {
|
|
3190
2185
|
try {
|
|
@@ -3226,8 +2221,8 @@ function generateL1(content, filename) {
|
|
|
3226
2221
|
inSection = false;
|
|
3227
2222
|
}
|
|
3228
2223
|
}
|
|
3229
|
-
const
|
|
3230
|
-
return
|
|
2224
|
+
const result = parts.join("\n");
|
|
2225
|
+
return result.slice(0, L1_MAX_CHARS);
|
|
3231
2226
|
}
|
|
3232
2227
|
function generateL1Json(content) {
|
|
3233
2228
|
try {
|
|
@@ -3247,7 +2242,7 @@ function generateL1Json(content) {
|
|
|
3247
2242
|
}
|
|
3248
2243
|
}
|
|
3249
2244
|
function getTieredContent(filePath, content) {
|
|
3250
|
-
const key =
|
|
2245
|
+
const key = path13.basename(filePath);
|
|
3251
2246
|
return {
|
|
3252
2247
|
source: filePath,
|
|
3253
2248
|
L0: generateL0(content, key),
|
|
@@ -3259,15 +2254,15 @@ function selectTier(tiered, tier) {
|
|
|
3259
2254
|
return tiered[tier];
|
|
3260
2255
|
}
|
|
3261
2256
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
3262
|
-
const memoryDir =
|
|
3263
|
-
if (!
|
|
3264
|
-
const files =
|
|
2257
|
+
const memoryDir = path13.join(projectDir, ".kody", "memory");
|
|
2258
|
+
if (!fs15.existsSync(memoryDir)) return "";
|
|
2259
|
+
const files = fs15.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
3265
2260
|
if (files.length === 0) return "";
|
|
3266
2261
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
3267
2262
|
const sections = [];
|
|
3268
2263
|
for (const file of files) {
|
|
3269
|
-
const filePath =
|
|
3270
|
-
const content =
|
|
2264
|
+
const filePath = path13.join(memoryDir, file);
|
|
2265
|
+
const content = fs15.readFileSync(filePath, "utf-8").trim();
|
|
3271
2266
|
if (!content) continue;
|
|
3272
2267
|
const tiered = getTieredContent(filePath, content);
|
|
3273
2268
|
const selected = selectTier(tiered, tier);
|
|
@@ -3290,9 +2285,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
3290
2285
|
`;
|
|
3291
2286
|
context += `Task Directory: ${taskDir}
|
|
3292
2287
|
`;
|
|
3293
|
-
const taskMdPath =
|
|
3294
|
-
if (
|
|
3295
|
-
const content =
|
|
2288
|
+
const taskMdPath = path13.join(taskDir, "task.md");
|
|
2289
|
+
if (fs15.existsSync(taskMdPath)) {
|
|
2290
|
+
const content = fs15.readFileSync(taskMdPath, "utf-8");
|
|
3296
2291
|
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
3297
2292
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
3298
2293
|
context += `
|
|
@@ -3300,9 +2295,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
3300
2295
|
${selected}
|
|
3301
2296
|
`;
|
|
3302
2297
|
}
|
|
3303
|
-
const taskJsonPath =
|
|
3304
|
-
if (
|
|
3305
|
-
const content =
|
|
2298
|
+
const taskJsonPath = path13.join(taskDir, "task.json");
|
|
2299
|
+
if (fs15.existsSync(taskJsonPath)) {
|
|
2300
|
+
const content = fs15.readFileSync(taskJsonPath, "utf-8");
|
|
3306
2301
|
if (policy.taskClassification === "L2") {
|
|
3307
2302
|
try {
|
|
3308
2303
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -3328,9 +2323,9 @@ ${selected}
|
|
|
3328
2323
|
}
|
|
3329
2324
|
}
|
|
3330
2325
|
}
|
|
3331
|
-
const specPath =
|
|
3332
|
-
if (
|
|
3333
|
-
const content =
|
|
2326
|
+
const specPath = path13.join(taskDir, "spec.md");
|
|
2327
|
+
if (fs15.existsSync(specPath)) {
|
|
2328
|
+
const content = fs15.readFileSync(specPath, "utf-8");
|
|
3334
2329
|
const selected = selectContent(specPath, content, policy.spec);
|
|
3335
2330
|
const label = tierLabel("Spec", policy.spec);
|
|
3336
2331
|
context += `
|
|
@@ -3338,9 +2333,9 @@ ${selected}
|
|
|
3338
2333
|
${selected}
|
|
3339
2334
|
`;
|
|
3340
2335
|
}
|
|
3341
|
-
const planPath =
|
|
3342
|
-
if (
|
|
3343
|
-
const content =
|
|
2336
|
+
const planPath = path13.join(taskDir, "plan.md");
|
|
2337
|
+
if (fs15.existsSync(planPath)) {
|
|
2338
|
+
const content = fs15.readFileSync(planPath, "utf-8");
|
|
3344
2339
|
const selected = selectContent(planPath, content, policy.plan);
|
|
3345
2340
|
const label = tierLabel("Plan", policy.plan);
|
|
3346
2341
|
context += `
|
|
@@ -3348,9 +2343,9 @@ ${selected}
|
|
|
3348
2343
|
${selected}
|
|
3349
2344
|
`;
|
|
3350
2345
|
}
|
|
3351
|
-
const contextMdPath =
|
|
3352
|
-
if (
|
|
3353
|
-
const content =
|
|
2346
|
+
const contextMdPath = path13.join(taskDir, "context.md");
|
|
2347
|
+
if (fs15.existsSync(contextMdPath)) {
|
|
2348
|
+
const content = fs15.readFileSync(contextMdPath, "utf-8");
|
|
3354
2349
|
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
3355
2350
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
3356
2351
|
context += `
|
|
@@ -3436,24 +2431,24 @@ var init_context_tiers = __esm({
|
|
|
3436
2431
|
});
|
|
3437
2432
|
|
|
3438
2433
|
// src/context.ts
|
|
3439
|
-
import * as
|
|
3440
|
-
import * as
|
|
2434
|
+
import * as fs16 from "fs";
|
|
2435
|
+
import * as path14 from "path";
|
|
3441
2436
|
function readPromptFile(stageName, projectDir) {
|
|
3442
2437
|
if (projectDir) {
|
|
3443
|
-
const stepFile =
|
|
3444
|
-
if (
|
|
3445
|
-
return
|
|
2438
|
+
const stepFile = path14.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
2439
|
+
if (fs16.existsSync(stepFile)) {
|
|
2440
|
+
return fs16.readFileSync(stepFile, "utf-8");
|
|
3446
2441
|
}
|
|
3447
2442
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
3448
2443
|
}
|
|
3449
2444
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
3450
2445
|
const candidates = [
|
|
3451
|
-
|
|
3452
|
-
|
|
2446
|
+
path14.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
2447
|
+
path14.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
3453
2448
|
];
|
|
3454
2449
|
for (const candidate of candidates) {
|
|
3455
|
-
if (
|
|
3456
|
-
return
|
|
2450
|
+
if (fs16.existsSync(candidate)) {
|
|
2451
|
+
return fs16.readFileSync(candidate, "utf-8");
|
|
3457
2452
|
}
|
|
3458
2453
|
}
|
|
3459
2454
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -3465,18 +2460,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
3465
2460
|
`;
|
|
3466
2461
|
context += `Task Directory: ${taskDir}
|
|
3467
2462
|
`;
|
|
3468
|
-
const taskMdPath =
|
|
3469
|
-
if (
|
|
3470
|
-
const taskMd =
|
|
2463
|
+
const taskMdPath = path14.join(taskDir, "task.md");
|
|
2464
|
+
if (fs16.existsSync(taskMdPath)) {
|
|
2465
|
+
const taskMd = fs16.readFileSync(taskMdPath, "utf-8");
|
|
3471
2466
|
context += `
|
|
3472
2467
|
## Task Description
|
|
3473
2468
|
${taskMd}
|
|
3474
2469
|
`;
|
|
3475
2470
|
}
|
|
3476
|
-
const taskJsonPath =
|
|
3477
|
-
if (
|
|
2471
|
+
const taskJsonPath = path14.join(taskDir, "task.json");
|
|
2472
|
+
if (fs16.existsSync(taskJsonPath)) {
|
|
3478
2473
|
try {
|
|
3479
|
-
const taskDef = JSON.parse(
|
|
2474
|
+
const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
|
|
3480
2475
|
context += `
|
|
3481
2476
|
## Task Classification
|
|
3482
2477
|
`;
|
|
@@ -3489,27 +2484,27 @@ ${taskMd}
|
|
|
3489
2484
|
} catch {
|
|
3490
2485
|
}
|
|
3491
2486
|
}
|
|
3492
|
-
const specPath =
|
|
3493
|
-
if (
|
|
3494
|
-
const spec =
|
|
2487
|
+
const specPath = path14.join(taskDir, "spec.md");
|
|
2488
|
+
if (fs16.existsSync(specPath)) {
|
|
2489
|
+
const spec = fs16.readFileSync(specPath, "utf-8");
|
|
3495
2490
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
3496
2491
|
context += `
|
|
3497
2492
|
## Spec Summary
|
|
3498
2493
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
3499
2494
|
`;
|
|
3500
2495
|
}
|
|
3501
|
-
const planPath =
|
|
3502
|
-
if (
|
|
3503
|
-
const plan =
|
|
2496
|
+
const planPath = path14.join(taskDir, "plan.md");
|
|
2497
|
+
if (fs16.existsSync(planPath)) {
|
|
2498
|
+
const plan = fs16.readFileSync(planPath, "utf-8");
|
|
3504
2499
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
3505
2500
|
context += `
|
|
3506
2501
|
## Plan Summary
|
|
3507
2502
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
3508
2503
|
`;
|
|
3509
2504
|
}
|
|
3510
|
-
const contextMdPath =
|
|
3511
|
-
if (
|
|
3512
|
-
const accumulated =
|
|
2505
|
+
const contextMdPath = path14.join(taskDir, "context.md");
|
|
2506
|
+
if (fs16.existsSync(contextMdPath)) {
|
|
2507
|
+
const accumulated = fs16.readFileSync(contextMdPath, "utf-8");
|
|
3513
2508
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
3514
2509
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
3515
2510
|
context += `
|
|
@@ -3527,17 +2522,17 @@ ${feedback}
|
|
|
3527
2522
|
}
|
|
3528
2523
|
function inferHasUIFromScope(scope) {
|
|
3529
2524
|
return scope.some((filePath) => {
|
|
3530
|
-
const ext =
|
|
2525
|
+
const ext = path14.extname(filePath).toLowerCase();
|
|
3531
2526
|
if (UI_EXTENSIONS.has(ext)) return true;
|
|
3532
2527
|
const normalized = filePath.replace(/\\/g, "/");
|
|
3533
2528
|
return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
|
|
3534
2529
|
});
|
|
3535
2530
|
}
|
|
3536
2531
|
function taskHasUI(taskDir) {
|
|
3537
|
-
const taskJsonPath =
|
|
3538
|
-
if (!
|
|
2532
|
+
const taskJsonPath = path14.join(taskDir, "task.json");
|
|
2533
|
+
if (!fs16.existsSync(taskJsonPath)) return true;
|
|
3539
2534
|
try {
|
|
3540
|
-
const taskDef = JSON.parse(
|
|
2535
|
+
const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
|
|
3541
2536
|
const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
|
|
3542
2537
|
if (scope.length === 0) return true;
|
|
3543
2538
|
return inferHasUIFromScope(scope);
|
|
@@ -3659,9 +2654,9 @@ ${prompt}` : prompt;
|
|
|
3659
2654
|
}
|
|
3660
2655
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
3661
2656
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
3662
|
-
const qaGuidePath =
|
|
3663
|
-
if (
|
|
3664
|
-
const qaGuide =
|
|
2657
|
+
const qaGuidePath = path14.join(projectDir, ".kody", "qa-guide.md");
|
|
2658
|
+
if (fs16.existsSync(qaGuidePath)) {
|
|
2659
|
+
const qaGuide = fs16.readFileSync(qaGuidePath, "utf-8").trim();
|
|
3665
2660
|
assembled = assembled + "\n\n" + qaGuide;
|
|
3666
2661
|
}
|
|
3667
2662
|
}
|
|
@@ -3691,12 +2686,10 @@ function escalateModelTier(currentTier) {
|
|
|
3691
2686
|
function resolveModel(modelTier, stageName) {
|
|
3692
2687
|
const config = getProjectConfig();
|
|
3693
2688
|
const mapped = config.agent.modelMap[modelTier];
|
|
3694
|
-
if (
|
|
3695
|
-
|
|
3696
|
-
}
|
|
3697
|
-
return mapped;
|
|
2689
|
+
if (mapped) return mapped;
|
|
2690
|
+
return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
|
|
3698
2691
|
}
|
|
3699
|
-
var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
|
|
2692
|
+
var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION, DEFAULT_MODEL_MAP;
|
|
3700
2693
|
var init_context = __esm({
|
|
3701
2694
|
"src/context.ts"() {
|
|
3702
2695
|
"use strict";
|
|
@@ -3730,6 +2723,11 @@ var init_context = __esm({
|
|
|
3730
2723
|
mid: "strong",
|
|
3731
2724
|
strong: "strong"
|
|
3732
2725
|
};
|
|
2726
|
+
DEFAULT_MODEL_MAP = {
|
|
2727
|
+
cheap: "haiku",
|
|
2728
|
+
mid: "sonnet",
|
|
2729
|
+
strong: "opus"
|
|
2730
|
+
};
|
|
3733
2731
|
}
|
|
3734
2732
|
});
|
|
3735
2733
|
|
|
@@ -3753,8 +2751,8 @@ var init_runner_selection = __esm({
|
|
|
3753
2751
|
});
|
|
3754
2752
|
|
|
3755
2753
|
// src/stages/agent.ts
|
|
3756
|
-
import * as
|
|
3757
|
-
import * as
|
|
2754
|
+
import * as fs17 from "fs";
|
|
2755
|
+
import * as path15 from "path";
|
|
3758
2756
|
function getSessionInfo(stageName, sessions) {
|
|
3759
2757
|
const group = SESSION_GROUP[stageName];
|
|
3760
2758
|
if (!group) return void 0;
|
|
@@ -3839,29 +2837,29 @@ async function executeAgentStage(ctx, def) {
|
|
|
3839
2837
|
if (lastResult.outcome !== "completed") {
|
|
3840
2838
|
return { outcome: lastResult.outcome, error: lastResult.error, retries };
|
|
3841
2839
|
}
|
|
3842
|
-
const
|
|
3843
|
-
if (def.outputFile &&
|
|
3844
|
-
|
|
2840
|
+
const result = lastResult;
|
|
2841
|
+
if (def.outputFile && result.output) {
|
|
2842
|
+
fs17.writeFileSync(path15.join(ctx.taskDir, def.outputFile), result.output);
|
|
3845
2843
|
}
|
|
3846
2844
|
if (def.outputFile) {
|
|
3847
|
-
const outputPath =
|
|
3848
|
-
if (!
|
|
3849
|
-
const ext =
|
|
3850
|
-
const base =
|
|
3851
|
-
const files =
|
|
2845
|
+
const outputPath = path15.join(ctx.taskDir, def.outputFile);
|
|
2846
|
+
if (!fs17.existsSync(outputPath)) {
|
|
2847
|
+
const ext = path15.extname(def.outputFile);
|
|
2848
|
+
const base = path15.basename(def.outputFile, ext);
|
|
2849
|
+
const files = fs17.readdirSync(ctx.taskDir);
|
|
3852
2850
|
const variant = files.find(
|
|
3853
2851
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
3854
2852
|
);
|
|
3855
2853
|
if (variant) {
|
|
3856
|
-
|
|
2854
|
+
fs17.renameSync(path15.join(ctx.taskDir, variant), outputPath);
|
|
3857
2855
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
3858
2856
|
}
|
|
3859
2857
|
}
|
|
3860
2858
|
}
|
|
3861
2859
|
if (def.outputFile) {
|
|
3862
|
-
const outputPath =
|
|
3863
|
-
if (
|
|
3864
|
-
const content =
|
|
2860
|
+
const outputPath = path15.join(ctx.taskDir, def.outputFile);
|
|
2861
|
+
if (fs17.existsSync(outputPath)) {
|
|
2862
|
+
const content = fs17.readFileSync(outputPath, "utf-8");
|
|
3865
2863
|
const validation = validateStageOutput(def.name, content);
|
|
3866
2864
|
if (!validation.valid) {
|
|
3867
2865
|
if (def.name === "taskify") {
|
|
@@ -3875,7 +2873,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
3875
2873
|
const stripped = stripFences(retryResult.output);
|
|
3876
2874
|
const retryValidation = validateTaskJson(stripped);
|
|
3877
2875
|
if (retryValidation.valid) {
|
|
3878
|
-
|
|
2876
|
+
fs17.writeFileSync(outputPath, retryResult.output);
|
|
3879
2877
|
logger.info(` taskify retry produced valid JSON`);
|
|
3880
2878
|
} else {
|
|
3881
2879
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -3888,7 +2886,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
3888
2886
|
risk_level: "low",
|
|
3889
2887
|
questions: []
|
|
3890
2888
|
}, null, 2);
|
|
3891
|
-
|
|
2889
|
+
fs17.writeFileSync(outputPath, fallback);
|
|
3892
2890
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
3893
2891
|
}
|
|
3894
2892
|
}
|
|
@@ -3898,11 +2896,11 @@ async function executeAgentStage(ctx, def) {
|
|
|
3898
2896
|
}
|
|
3899
2897
|
}
|
|
3900
2898
|
}
|
|
3901
|
-
appendStageContext(ctx.taskDir, def.name,
|
|
2899
|
+
appendStageContext(ctx.taskDir, def.name, result.output);
|
|
3902
2900
|
return { outcome: "completed", outputFile: def.outputFile, retries };
|
|
3903
2901
|
}
|
|
3904
2902
|
function appendStageContext(taskDir, stageName, output) {
|
|
3905
|
-
const contextPath =
|
|
2903
|
+
const contextPath = path15.join(taskDir, "context.md");
|
|
3906
2904
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
3907
2905
|
let summary;
|
|
3908
2906
|
if (output && output.trim()) {
|
|
@@ -3915,7 +2913,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
3915
2913
|
### ${stageName} (${timestamp2})
|
|
3916
2914
|
${summary}
|
|
3917
2915
|
`;
|
|
3918
|
-
|
|
2916
|
+
fs17.appendFileSync(contextPath, entry);
|
|
3919
2917
|
}
|
|
3920
2918
|
var SESSION_GROUP;
|
|
3921
2919
|
var init_agent = __esm({
|
|
@@ -3938,7 +2936,7 @@ var init_agent = __esm({
|
|
|
3938
2936
|
});
|
|
3939
2937
|
|
|
3940
2938
|
// src/verify-runner.ts
|
|
3941
|
-
import { execFileSync as
|
|
2939
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
3942
2940
|
function isExecError(err) {
|
|
3943
2941
|
return typeof err === "object" && err !== null;
|
|
3944
2942
|
}
|
|
@@ -3974,7 +2972,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
3974
2972
|
return { success: true, output: "", timedOut: false };
|
|
3975
2973
|
}
|
|
3976
2974
|
try {
|
|
3977
|
-
const output =
|
|
2975
|
+
const output = execFileSync11(parts[0], parts.slice(1), {
|
|
3978
2976
|
cwd,
|
|
3979
2977
|
timeout,
|
|
3980
2978
|
encoding: "utf-8",
|
|
@@ -4020,19 +3018,19 @@ function runQualityGates(taskDir, projectRoot) {
|
|
|
4020
3018
|
for (const { name, cmd } of commands) {
|
|
4021
3019
|
if (!cmd) continue;
|
|
4022
3020
|
logger.info(` Running ${name}: ${cmd}`);
|
|
4023
|
-
const
|
|
4024
|
-
if (
|
|
3021
|
+
const result = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
|
|
3022
|
+
if (result.timedOut) {
|
|
4025
3023
|
allErrors.push(`${name}: timed out after ${VERIFY_COMMAND_TIMEOUT_MS / 1e3}s`);
|
|
4026
3024
|
allPass = false;
|
|
4027
3025
|
continue;
|
|
4028
3026
|
}
|
|
4029
|
-
if (!
|
|
3027
|
+
if (!result.success) {
|
|
4030
3028
|
allPass = false;
|
|
4031
|
-
const errors = parseErrors(
|
|
3029
|
+
const errors = parseErrors(result.output);
|
|
4032
3030
|
allErrors.push(...errors.map((e) => `[${name}] ${e}`));
|
|
4033
|
-
rawOutputs.push({ name, output:
|
|
3031
|
+
rawOutputs.push({ name, output: result.output.slice(-3e3) });
|
|
4034
3032
|
}
|
|
4035
|
-
allSummary.push(...extractSummary(
|
|
3033
|
+
allSummary.push(...extractSummary(result.output, name));
|
|
4036
3034
|
}
|
|
4037
3035
|
return { pass: allPass, errors: allErrors, summary: allSummary, rawOutputs };
|
|
4038
3036
|
}
|
|
@@ -4045,7 +3043,7 @@ var init_verify_runner = __esm({
|
|
|
4045
3043
|
});
|
|
4046
3044
|
|
|
4047
3045
|
// src/observer.ts
|
|
4048
|
-
import { execFileSync as
|
|
3046
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
4049
3047
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
4050
3048
|
const context = [
|
|
4051
3049
|
`Stage: ${stageName}`,
|
|
@@ -4059,17 +3057,17 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
4059
3057
|
].join("\n");
|
|
4060
3058
|
const prompt = DIAGNOSIS_PROMPT + context;
|
|
4061
3059
|
try {
|
|
4062
|
-
const
|
|
3060
|
+
const result = await runner.run(
|
|
4063
3061
|
"diagnosis",
|
|
4064
3062
|
prompt,
|
|
4065
3063
|
model,
|
|
4066
|
-
|
|
4067
|
-
//
|
|
3064
|
+
9e4,
|
|
3065
|
+
// 90s timeout — MiniMax can be slow to respond
|
|
4068
3066
|
"",
|
|
4069
3067
|
options
|
|
4070
3068
|
);
|
|
4071
|
-
if (
|
|
4072
|
-
const cleaned =
|
|
3069
|
+
if (result.outcome === "completed" && result.output) {
|
|
3070
|
+
const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
4073
3071
|
const parseResult = parseJsonSafe(cleaned, ["classification"]);
|
|
4074
3072
|
if (parseResult.ok) {
|
|
4075
3073
|
const { data } = parseResult;
|
|
@@ -4096,6 +3094,29 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
4096
3094
|
} catch (err) {
|
|
4097
3095
|
logger.warn(` Diagnosis error: ${err instanceof Error ? err.message : err}`);
|
|
4098
3096
|
}
|
|
3097
|
+
if (modifiedFiles.length > 0) {
|
|
3098
|
+
const errorLines = errorOutput.split("\n").filter(
|
|
3099
|
+
(l) => /error|Error|ERROR|failed|Failed|FAIL/i.test(l)
|
|
3100
|
+
);
|
|
3101
|
+
const errorFilePaths = errorLines.flatMap((line) => {
|
|
3102
|
+
const matches = line.match(/src\/[^\s(:]+\.[a-z]+/g);
|
|
3103
|
+
return matches ?? [];
|
|
3104
|
+
});
|
|
3105
|
+
if (errorFilePaths.length > 0) {
|
|
3106
|
+
const modifiedSet = new Set(modifiedFiles);
|
|
3107
|
+
const allPreExisting = errorFilePaths.every(
|
|
3108
|
+
(f) => !modifiedSet.has(f) && !modifiedFiles.some((m) => m.endsWith(f))
|
|
3109
|
+
);
|
|
3110
|
+
if (allPreExisting) {
|
|
3111
|
+
logger.warn(" Diagnosis fallback: all errors in unmodified files \u2192 pre-existing");
|
|
3112
|
+
return {
|
|
3113
|
+
classification: "pre-existing",
|
|
3114
|
+
reason: "All errors are in files not modified by the build stage",
|
|
3115
|
+
resolution: `The following files have pre-existing errors not introduced by this task: ${[...new Set(errorFilePaths)].join(", ")}`
|
|
3116
|
+
};
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
4099
3120
|
logger.warn(" Diagnosis failed \u2014 defaulting to retry");
|
|
4100
3121
|
return {
|
|
4101
3122
|
classification: "retry",
|
|
@@ -4105,13 +3126,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
4105
3126
|
}
|
|
4106
3127
|
function getModifiedFiles(projectDir) {
|
|
4107
3128
|
try {
|
|
4108
|
-
const staged =
|
|
3129
|
+
const staged = execFileSync12("git", ["diff", "--name-only", "--cached"], {
|
|
4109
3130
|
encoding: "utf-8",
|
|
4110
3131
|
cwd: projectDir,
|
|
4111
3132
|
timeout: 5e3,
|
|
4112
3133
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4113
3134
|
}).trim();
|
|
4114
|
-
const unstaged =
|
|
3135
|
+
const unstaged = execFileSync12("git", ["diff", "--name-only"], {
|
|
4115
3136
|
encoding: "utf-8",
|
|
4116
3137
|
cwd: projectDir,
|
|
4117
3138
|
timeout: 5e3,
|
|
@@ -4154,8 +3175,8 @@ Error context:
|
|
|
4154
3175
|
});
|
|
4155
3176
|
|
|
4156
3177
|
// src/stages/gate.ts
|
|
4157
|
-
import * as
|
|
4158
|
-
import * as
|
|
3178
|
+
import * as fs18 from "fs";
|
|
3179
|
+
import * as path16 from "path";
|
|
4159
3180
|
function executeGateStage(ctx, def) {
|
|
4160
3181
|
if (ctx.input.dryRun) {
|
|
4161
3182
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -4198,7 +3219,7 @@ ${output}
|
|
|
4198
3219
|
`);
|
|
4199
3220
|
}
|
|
4200
3221
|
}
|
|
4201
|
-
|
|
3222
|
+
fs18.writeFileSync(path16.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
4202
3223
|
return {
|
|
4203
3224
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
4204
3225
|
retries: 0
|
|
@@ -4213,9 +3234,9 @@ var init_gate = __esm({
|
|
|
4213
3234
|
});
|
|
4214
3235
|
|
|
4215
3236
|
// src/stages/verify.ts
|
|
4216
|
-
import * as
|
|
4217
|
-
import * as
|
|
4218
|
-
import { execFileSync as
|
|
3237
|
+
import * as fs19 from "fs";
|
|
3238
|
+
import * as path17 from "path";
|
|
3239
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
4219
3240
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
4220
3241
|
const maxAttempts = def.maxRetries ?? 2;
|
|
4221
3242
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -4225,8 +3246,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
4225
3246
|
return { ...gateResult, retries: attempt };
|
|
4226
3247
|
}
|
|
4227
3248
|
if (attempt < maxAttempts) {
|
|
4228
|
-
const verifyPath =
|
|
4229
|
-
const errorOutput =
|
|
3249
|
+
const verifyPath = path17.join(ctx.taskDir, "verify.md");
|
|
3250
|
+
const errorOutput = fs19.existsSync(verifyPath) ? fs19.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
4230
3251
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
4231
3252
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
4232
3253
|
const diagConfig = getProjectConfig();
|
|
@@ -4269,7 +3290,7 @@ ${diagnosis.resolution}`);
|
|
|
4269
3290
|
const parts = parseCommand(cmd);
|
|
4270
3291
|
if (parts.length === 0) return;
|
|
4271
3292
|
try {
|
|
4272
|
-
|
|
3293
|
+
execFileSync13(parts[0], parts.slice(1), {
|
|
4273
3294
|
stdio: "pipe",
|
|
4274
3295
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
4275
3296
|
});
|
|
@@ -4322,8 +3343,8 @@ var init_verify = __esm({
|
|
|
4322
3343
|
});
|
|
4323
3344
|
|
|
4324
3345
|
// src/review-standalone.ts
|
|
4325
|
-
import * as
|
|
4326
|
-
import * as
|
|
3346
|
+
import * as fs20 from "fs";
|
|
3347
|
+
import * as path18 from "path";
|
|
4327
3348
|
function resolveReviewTarget(input) {
|
|
4328
3349
|
if (input.prs.length === 0) {
|
|
4329
3350
|
return {
|
|
@@ -4347,8 +3368,8 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
4347
3368
|
}
|
|
4348
3369
|
async function runStandaloneReview(input) {
|
|
4349
3370
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
4350
|
-
const taskDir =
|
|
4351
|
-
|
|
3371
|
+
const taskDir = path18.join(input.projectDir, ".kody", "tasks", taskId);
|
|
3372
|
+
fs20.mkdirSync(taskDir, { recursive: true });
|
|
4352
3373
|
let diffInstruction = "";
|
|
4353
3374
|
let filesChangedSection = "";
|
|
4354
3375
|
if (input.baseBranch) {
|
|
@@ -4375,7 +3396,7 @@ ${fileList}`;
|
|
|
4375
3396
|
const taskContent = `# ${input.prTitle}
|
|
4376
3397
|
|
|
4377
3398
|
${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
4378
|
-
|
|
3399
|
+
fs20.writeFileSync(path18.join(taskDir, "task.md"), taskContent);
|
|
4379
3400
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
4380
3401
|
const ctx = {
|
|
4381
3402
|
taskId,
|
|
@@ -4389,18 +3410,18 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
|
4389
3410
|
}
|
|
4390
3411
|
};
|
|
4391
3412
|
logger.info(`[review] standalone review for: ${input.prTitle}`);
|
|
4392
|
-
const
|
|
4393
|
-
if (
|
|
3413
|
+
const result = await executeAgentStage(ctx, reviewDef);
|
|
3414
|
+
if (result.outcome !== "completed") {
|
|
4394
3415
|
return {
|
|
4395
3416
|
outcome: "failed",
|
|
4396
3417
|
taskDir,
|
|
4397
|
-
error:
|
|
3418
|
+
error: result.error ?? "Review stage failed"
|
|
4398
3419
|
};
|
|
4399
3420
|
}
|
|
4400
|
-
const reviewPath =
|
|
3421
|
+
const reviewPath = path18.join(taskDir, "review.md");
|
|
4401
3422
|
let reviewContent;
|
|
4402
|
-
if (
|
|
4403
|
-
reviewContent =
|
|
3423
|
+
if (fs20.existsSync(reviewPath)) {
|
|
3424
|
+
reviewContent = fs20.readFileSync(reviewPath, "utf-8");
|
|
4404
3425
|
}
|
|
4405
3426
|
return {
|
|
4406
3427
|
outcome: "completed",
|
|
@@ -4440,8 +3461,8 @@ var init_review_standalone = __esm({
|
|
|
4440
3461
|
});
|
|
4441
3462
|
|
|
4442
3463
|
// src/stages/review.ts
|
|
4443
|
-
import * as
|
|
4444
|
-
import * as
|
|
3464
|
+
import * as fs21 from "fs";
|
|
3465
|
+
import * as path19 from "path";
|
|
4445
3466
|
async function executeReviewWithFix(ctx, def) {
|
|
4446
3467
|
if (ctx.input.dryRun) {
|
|
4447
3468
|
return { outcome: "completed", retries: 0 };
|
|
@@ -4455,11 +3476,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
4455
3476
|
if (reviewResult.outcome !== "completed") {
|
|
4456
3477
|
return reviewResult;
|
|
4457
3478
|
}
|
|
4458
|
-
const reviewFile =
|
|
4459
|
-
if (!
|
|
3479
|
+
const reviewFile = path19.join(ctx.taskDir, "review.md");
|
|
3480
|
+
if (!fs21.existsSync(reviewFile)) {
|
|
4460
3481
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
4461
3482
|
}
|
|
4462
|
-
const content =
|
|
3483
|
+
const content = fs21.readFileSync(reviewFile, "utf-8");
|
|
4463
3484
|
if (detectReviewVerdict(content) !== "fail") {
|
|
4464
3485
|
return { ...reviewResult, retries: iteration };
|
|
4465
3486
|
}
|
|
@@ -4488,15 +3509,15 @@ var init_review = __esm({
|
|
|
4488
3509
|
});
|
|
4489
3510
|
|
|
4490
3511
|
// src/stages/ship.ts
|
|
4491
|
-
import * as
|
|
4492
|
-
import * as
|
|
4493
|
-
import { execFileSync as
|
|
3512
|
+
import * as fs22 from "fs";
|
|
3513
|
+
import * as path20 from "path";
|
|
3514
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
4494
3515
|
function buildPrBody(ctx) {
|
|
4495
3516
|
const sections = [];
|
|
4496
|
-
const taskJsonPath =
|
|
4497
|
-
if (
|
|
3517
|
+
const taskJsonPath = path20.join(ctx.taskDir, "task.json");
|
|
3518
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
4498
3519
|
try {
|
|
4499
|
-
const raw =
|
|
3520
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
4500
3521
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4501
3522
|
const task = JSON.parse(cleaned);
|
|
4502
3523
|
if (task.description) {
|
|
@@ -4515,9 +3536,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
4515
3536
|
} catch {
|
|
4516
3537
|
}
|
|
4517
3538
|
}
|
|
4518
|
-
const reviewPath =
|
|
4519
|
-
if (
|
|
4520
|
-
const review =
|
|
3539
|
+
const reviewPath = path20.join(ctx.taskDir, "review.md");
|
|
3540
|
+
if (fs22.existsSync(reviewPath)) {
|
|
3541
|
+
const review = fs22.readFileSync(reviewPath, "utf-8");
|
|
4521
3542
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
4522
3543
|
if (summaryMatch) {
|
|
4523
3544
|
const summary = summaryMatch[1].trim();
|
|
@@ -4534,14 +3555,14 @@ ${summary}`);
|
|
|
4534
3555
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
4535
3556
|
}
|
|
4536
3557
|
}
|
|
4537
|
-
const verifyPath =
|
|
4538
|
-
if (
|
|
4539
|
-
const verify =
|
|
3558
|
+
const verifyPath = path20.join(ctx.taskDir, "verify.md");
|
|
3559
|
+
if (fs22.existsSync(verifyPath)) {
|
|
3560
|
+
const verify = fs22.readFileSync(verifyPath, "utf-8");
|
|
4540
3561
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
4541
3562
|
}
|
|
4542
|
-
const planPath =
|
|
4543
|
-
if (
|
|
4544
|
-
const plan =
|
|
3563
|
+
const planPath = path20.join(ctx.taskDir, "plan.md");
|
|
3564
|
+
if (fs22.existsSync(planPath)) {
|
|
3565
|
+
const plan = fs22.readFileSync(planPath, "utf-8").trim();
|
|
4545
3566
|
if (plan) {
|
|
4546
3567
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
4547
3568
|
sections.push(`
|
|
@@ -4561,25 +3582,25 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
4561
3582
|
return sections.join("\n");
|
|
4562
3583
|
}
|
|
4563
3584
|
function executeShipStage(ctx, _def) {
|
|
4564
|
-
const shipPath =
|
|
3585
|
+
const shipPath = path20.join(ctx.taskDir, "ship.md");
|
|
4565
3586
|
if (ctx.input.dryRun) {
|
|
4566
|
-
|
|
3587
|
+
fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
4567
3588
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
4568
3589
|
}
|
|
4569
3590
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
4570
|
-
|
|
3591
|
+
fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
4571
3592
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
4572
3593
|
}
|
|
4573
3594
|
try {
|
|
4574
3595
|
const head = getCurrentBranch(ctx.projectDir);
|
|
4575
3596
|
const base = getDefaultBranch(ctx.projectDir);
|
|
4576
3597
|
try {
|
|
4577
|
-
|
|
3598
|
+
execFileSync14("git", ["add", ctx.taskDir], {
|
|
4578
3599
|
cwd: ctx.projectDir,
|
|
4579
3600
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
4580
3601
|
stdio: "pipe"
|
|
4581
3602
|
});
|
|
4582
|
-
|
|
3603
|
+
execFileSync14("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
4583
3604
|
cwd: ctx.projectDir,
|
|
4584
3605
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
4585
3606
|
stdio: "pipe"
|
|
@@ -4593,7 +3614,7 @@ function executeShipStage(ctx, _def) {
|
|
|
4593
3614
|
let repo = config.github?.repo;
|
|
4594
3615
|
if (!owner || !repo) {
|
|
4595
3616
|
try {
|
|
4596
|
-
const remoteUrl =
|
|
3617
|
+
const remoteUrl = execFileSync14("git", ["remote", "get-url", "origin"], {
|
|
4597
3618
|
encoding: "utf-8",
|
|
4598
3619
|
cwd: ctx.projectDir
|
|
4599
3620
|
}).trim();
|
|
@@ -4614,28 +3635,28 @@ function executeShipStage(ctx, _def) {
|
|
|
4614
3635
|
chore: "chore"
|
|
4615
3636
|
};
|
|
4616
3637
|
let prefix = "chore";
|
|
4617
|
-
const taskJsonPath =
|
|
4618
|
-
if (
|
|
3638
|
+
const taskJsonPath = path20.join(ctx.taskDir, "task.json");
|
|
3639
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
4619
3640
|
try {
|
|
4620
|
-
const raw =
|
|
3641
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
4621
3642
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4622
3643
|
const task = JSON.parse(cleaned);
|
|
4623
3644
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
4624
3645
|
} catch {
|
|
4625
3646
|
}
|
|
4626
3647
|
}
|
|
4627
|
-
const taskMdPath =
|
|
4628
|
-
if (
|
|
4629
|
-
const content =
|
|
3648
|
+
const taskMdPath = path20.join(ctx.taskDir, "task.md");
|
|
3649
|
+
if (fs22.existsSync(taskMdPath)) {
|
|
3650
|
+
const content = fs22.readFileSync(taskMdPath, "utf-8");
|
|
4630
3651
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
4631
3652
|
if (heading) {
|
|
4632
3653
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
4633
3654
|
}
|
|
4634
3655
|
}
|
|
4635
3656
|
if (title === "Update") {
|
|
4636
|
-
if (
|
|
3657
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
4637
3658
|
try {
|
|
4638
|
-
const raw =
|
|
3659
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
4639
3660
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4640
3661
|
const task = JSON.parse(cleaned);
|
|
4641
3662
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -4658,7 +3679,7 @@ function executeShipStage(ctx, _def) {
|
|
|
4658
3679
|
} catch {
|
|
4659
3680
|
}
|
|
4660
3681
|
}
|
|
4661
|
-
|
|
3682
|
+
fs22.writeFileSync(shipPath, `# Ship
|
|
4662
3683
|
|
|
4663
3684
|
Updated existing PR: ${existingPr.url}
|
|
4664
3685
|
PR #${existingPr.number}
|
|
@@ -4679,20 +3700,20 @@ PR #${existingPr.number}
|
|
|
4679
3700
|
} catch {
|
|
4680
3701
|
}
|
|
4681
3702
|
}
|
|
4682
|
-
|
|
3703
|
+
fs22.writeFileSync(shipPath, `# Ship
|
|
4683
3704
|
|
|
4684
3705
|
PR created: ${pr.url}
|
|
4685
3706
|
PR #${pr.number}
|
|
4686
3707
|
`);
|
|
4687
3708
|
} else {
|
|
4688
|
-
|
|
3709
|
+
fs22.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
4689
3710
|
}
|
|
4690
3711
|
}
|
|
4691
3712
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
4692
3713
|
} catch (err) {
|
|
4693
3714
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4694
3715
|
try {
|
|
4695
|
-
|
|
3716
|
+
fs22.writeFileSync(shipPath, `# Ship
|
|
4696
3717
|
|
|
4697
3718
|
Failed: ${msg}
|
|
4698
3719
|
`);
|
|
@@ -4741,15 +3762,15 @@ var init_executor_registry = __esm({
|
|
|
4741
3762
|
});
|
|
4742
3763
|
|
|
4743
3764
|
// src/pipeline/questions.ts
|
|
4744
|
-
import * as
|
|
4745
|
-
import * as
|
|
3765
|
+
import * as fs23 from "fs";
|
|
3766
|
+
import * as path21 from "path";
|
|
4746
3767
|
function checkForQuestions(ctx, stageName) {
|
|
4747
3768
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
4748
3769
|
try {
|
|
4749
3770
|
if (stageName === "taskify") {
|
|
4750
|
-
const taskJsonPath =
|
|
4751
|
-
if (!
|
|
4752
|
-
const raw =
|
|
3771
|
+
const taskJsonPath = path21.join(ctx.taskDir, "task.json");
|
|
3772
|
+
if (!fs23.existsSync(taskJsonPath)) return false;
|
|
3773
|
+
const raw = fs23.readFileSync(taskJsonPath, "utf-8");
|
|
4753
3774
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4754
3775
|
const taskJson = JSON.parse(cleaned);
|
|
4755
3776
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -4764,9 +3785,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
4764
3785
|
}
|
|
4765
3786
|
}
|
|
4766
3787
|
if (stageName === "plan") {
|
|
4767
|
-
const planPath =
|
|
4768
|
-
if (!
|
|
4769
|
-
const plan =
|
|
3788
|
+
const planPath = path21.join(ctx.taskDir, "plan.md");
|
|
3789
|
+
if (!fs23.existsSync(planPath)) return false;
|
|
3790
|
+
const plan = fs23.readFileSync(planPath, "utf-8");
|
|
4770
3791
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
4771
3792
|
if (questionsMatch) {
|
|
4772
3793
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -4795,8 +3816,8 @@ var init_questions = __esm({
|
|
|
4795
3816
|
});
|
|
4796
3817
|
|
|
4797
3818
|
// src/pipeline/hooks.ts
|
|
4798
|
-
import * as
|
|
4799
|
-
import * as
|
|
3819
|
+
import * as fs24 from "fs";
|
|
3820
|
+
import * as path22 from "path";
|
|
4800
3821
|
function applyPreStageLabel(ctx, def) {
|
|
4801
3822
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
4802
3823
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
@@ -4834,9 +3855,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
4834
3855
|
return { complexity, activeStages };
|
|
4835
3856
|
}
|
|
4836
3857
|
try {
|
|
4837
|
-
const taskJsonPath =
|
|
4838
|
-
if (!
|
|
4839
|
-
const raw =
|
|
3858
|
+
const taskJsonPath = path22.join(ctx.taskDir, "task.json");
|
|
3859
|
+
if (!fs24.existsSync(taskJsonPath)) return null;
|
|
3860
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
4840
3861
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4841
3862
|
const taskJson = JSON.parse(cleaned);
|
|
4842
3863
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -4866,8 +3887,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
4866
3887
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
4867
3888
|
if (ctx.input.mode === "rerun") return null;
|
|
4868
3889
|
if (!ctx.input.issueNumber) return null;
|
|
4869
|
-
const planPath =
|
|
4870
|
-
const plan =
|
|
3890
|
+
const planPath = path22.join(ctx.taskDir, "plan.md");
|
|
3891
|
+
const plan = fs24.existsSync(planPath) ? fs24.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
4871
3892
|
try {
|
|
4872
3893
|
postComment(
|
|
4873
3894
|
ctx.input.issueNumber,
|
|
@@ -4934,22 +3955,22 @@ var init_hooks = __esm({
|
|
|
4934
3955
|
});
|
|
4935
3956
|
|
|
4936
3957
|
// src/learning/auto-learn.ts
|
|
4937
|
-
import * as
|
|
4938
|
-
import * as
|
|
3958
|
+
import * as fs25 from "fs";
|
|
3959
|
+
import * as path23 from "path";
|
|
4939
3960
|
function stripAnsi(str) {
|
|
4940
3961
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
4941
3962
|
}
|
|
4942
3963
|
function autoLearn(ctx) {
|
|
4943
3964
|
try {
|
|
4944
|
-
const memoryDir =
|
|
4945
|
-
if (!
|
|
4946
|
-
|
|
3965
|
+
const memoryDir = path23.join(ctx.projectDir, ".kody", "memory");
|
|
3966
|
+
if (!fs25.existsSync(memoryDir)) {
|
|
3967
|
+
fs25.mkdirSync(memoryDir, { recursive: true });
|
|
4947
3968
|
}
|
|
4948
3969
|
const learnings = [];
|
|
4949
3970
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4950
|
-
const verifyPath =
|
|
4951
|
-
if (
|
|
4952
|
-
const verify = stripAnsi(
|
|
3971
|
+
const verifyPath = path23.join(ctx.taskDir, "verify.md");
|
|
3972
|
+
if (fs25.existsSync(verifyPath)) {
|
|
3973
|
+
const verify = stripAnsi(fs25.readFileSync(verifyPath, "utf-8"));
|
|
4953
3974
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
4954
3975
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
4955
3976
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -4958,18 +3979,18 @@ function autoLearn(ctx) {
|
|
|
4958
3979
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
4959
3980
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
4960
3981
|
}
|
|
4961
|
-
const reviewPath =
|
|
4962
|
-
if (
|
|
4963
|
-
const review =
|
|
3982
|
+
const reviewPath = path23.join(ctx.taskDir, "review.md");
|
|
3983
|
+
if (fs25.existsSync(reviewPath)) {
|
|
3984
|
+
const review = fs25.readFileSync(reviewPath, "utf-8");
|
|
4964
3985
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
4965
3986
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
4966
3987
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
4967
3988
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
4968
3989
|
}
|
|
4969
|
-
const taskJsonPath =
|
|
4970
|
-
if (
|
|
3990
|
+
const taskJsonPath = path23.join(ctx.taskDir, "task.json");
|
|
3991
|
+
if (fs25.existsSync(taskJsonPath)) {
|
|
4971
3992
|
try {
|
|
4972
|
-
const raw = stripAnsi(
|
|
3993
|
+
const raw = stripAnsi(fs25.readFileSync(taskJsonPath, "utf-8"));
|
|
4973
3994
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4974
3995
|
const task = JSON.parse(cleaned);
|
|
4975
3996
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -4980,12 +4001,12 @@ function autoLearn(ctx) {
|
|
|
4980
4001
|
}
|
|
4981
4002
|
}
|
|
4982
4003
|
if (learnings.length > 0) {
|
|
4983
|
-
const conventionsPath =
|
|
4004
|
+
const conventionsPath = path23.join(memoryDir, "conventions.md");
|
|
4984
4005
|
const entry = `
|
|
4985
4006
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
4986
4007
|
${learnings.join("\n")}
|
|
4987
4008
|
`;
|
|
4988
|
-
|
|
4009
|
+
fs25.appendFileSync(conventionsPath, entry);
|
|
4989
4010
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
4990
4011
|
}
|
|
4991
4012
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
@@ -4993,8 +4014,8 @@ ${learnings.join("\n")}
|
|
|
4993
4014
|
}
|
|
4994
4015
|
}
|
|
4995
4016
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
4996
|
-
const archPath =
|
|
4997
|
-
if (
|
|
4017
|
+
const archPath = path23.join(memoryDir, "architecture.md");
|
|
4018
|
+
if (fs25.existsSync(archPath)) return;
|
|
4998
4019
|
const detected = detectArchitectureBasic(projectDir);
|
|
4999
4020
|
if (detected.length > 0) {
|
|
5000
4021
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
@@ -5002,7 +4023,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
5002
4023
|
## Overview
|
|
5003
4024
|
${detected.join("\n")}
|
|
5004
4025
|
`;
|
|
5005
|
-
|
|
4026
|
+
fs25.writeFileSync(archPath, content);
|
|
5006
4027
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
5007
4028
|
}
|
|
5008
4029
|
}
|
|
@@ -5015,13 +4036,13 @@ var init_auto_learn = __esm({
|
|
|
5015
4036
|
});
|
|
5016
4037
|
|
|
5017
4038
|
// src/retrospective.ts
|
|
5018
|
-
import * as
|
|
5019
|
-
import * as
|
|
4039
|
+
import * as fs26 from "fs";
|
|
4040
|
+
import * as path24 from "path";
|
|
5020
4041
|
function readArtifact(taskDir, filename, maxChars) {
|
|
5021
|
-
const p =
|
|
5022
|
-
if (!
|
|
4042
|
+
const p = path24.join(taskDir, filename);
|
|
4043
|
+
if (!fs26.existsSync(p)) return null;
|
|
5023
4044
|
try {
|
|
5024
|
-
const content =
|
|
4045
|
+
const content = fs26.readFileSync(p, "utf-8");
|
|
5025
4046
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
5026
4047
|
} catch {
|
|
5027
4048
|
return null;
|
|
@@ -5074,13 +4095,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
5074
4095
|
return lines.join("\n");
|
|
5075
4096
|
}
|
|
5076
4097
|
function getLogPath(projectDir) {
|
|
5077
|
-
return
|
|
4098
|
+
return path24.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
5078
4099
|
}
|
|
5079
4100
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
5080
4101
|
const logPath = getLogPath(projectDir);
|
|
5081
|
-
if (!
|
|
4102
|
+
if (!fs26.existsSync(logPath)) return [];
|
|
5082
4103
|
try {
|
|
5083
|
-
const content =
|
|
4104
|
+
const content = fs26.readFileSync(logPath, "utf-8");
|
|
5084
4105
|
const lines = content.split("\n").filter(Boolean);
|
|
5085
4106
|
const entries = [];
|
|
5086
4107
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -5107,11 +4128,11 @@ function formatPreviousEntries(entries) {
|
|
|
5107
4128
|
}
|
|
5108
4129
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
5109
4130
|
const logPath = getLogPath(projectDir);
|
|
5110
|
-
const dir =
|
|
5111
|
-
if (!
|
|
5112
|
-
|
|
4131
|
+
const dir = path24.dirname(logPath);
|
|
4132
|
+
if (!fs26.existsSync(dir)) {
|
|
4133
|
+
fs26.mkdirSync(dir, { recursive: true });
|
|
5113
4134
|
}
|
|
5114
|
-
|
|
4135
|
+
fs26.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
5115
4136
|
}
|
|
5116
4137
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
5117
4138
|
if (ctx.input.dryRun) return;
|
|
@@ -5133,7 +4154,7 @@ ${previousText}
|
|
|
5133
4154
|
if (needsLitellmProxy(config)) {
|
|
5134
4155
|
extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
|
|
5135
4156
|
}
|
|
5136
|
-
const
|
|
4157
|
+
const result = await runner.run("retrospective", prompt, model, 3e4, "", {
|
|
5137
4158
|
cwd: ctx.projectDir,
|
|
5138
4159
|
env: extraEnv
|
|
5139
4160
|
});
|
|
@@ -5141,8 +4162,8 @@ ${previousText}
|
|
|
5141
4162
|
let patternMatch = null;
|
|
5142
4163
|
let suggestion = "No suggestion";
|
|
5143
4164
|
let pipelineFlaw = null;
|
|
5144
|
-
if (
|
|
5145
|
-
const cleaned =
|
|
4165
|
+
if (result.outcome === "completed" && result.output) {
|
|
4166
|
+
const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
5146
4167
|
try {
|
|
5147
4168
|
const parsed = JSON.parse(cleaned);
|
|
5148
4169
|
observation = parsed.observation ?? observation;
|
|
@@ -5279,8 +4300,8 @@ var init_summary = __esm({
|
|
|
5279
4300
|
});
|
|
5280
4301
|
|
|
5281
4302
|
// src/pipeline.ts
|
|
5282
|
-
import * as
|
|
5283
|
-
import * as
|
|
4303
|
+
import * as fs27 from "fs";
|
|
4304
|
+
import * as path25 from "path";
|
|
5284
4305
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
5285
4306
|
if (ctx.input.dryRun) return;
|
|
5286
4307
|
if (ctx.input.prNumber) {
|
|
@@ -5293,8 +4314,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
5293
4314
|
}
|
|
5294
4315
|
if (!ctx.input.issueNumber) return;
|
|
5295
4316
|
try {
|
|
5296
|
-
const taskMdPath =
|
|
5297
|
-
const title =
|
|
4317
|
+
const taskMdPath = path25.join(ctx.taskDir, "task.md");
|
|
4318
|
+
const title = fs27.existsSync(taskMdPath) ? fs27.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
5298
4319
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
5299
4320
|
syncWithDefault(ctx.projectDir);
|
|
5300
4321
|
} catch (err) {
|
|
@@ -5308,10 +4329,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
5308
4329
|
}
|
|
5309
4330
|
}
|
|
5310
4331
|
function acquireLock(taskDir) {
|
|
5311
|
-
const lockPath =
|
|
5312
|
-
if (
|
|
4332
|
+
const lockPath = path25.join(taskDir, ".lock");
|
|
4333
|
+
if (fs27.existsSync(lockPath)) {
|
|
5313
4334
|
try {
|
|
5314
|
-
const pid = parseInt(
|
|
4335
|
+
const pid = parseInt(fs27.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
5315
4336
|
if (!isNaN(pid)) {
|
|
5316
4337
|
try {
|
|
5317
4338
|
process.kill(pid, 0);
|
|
@@ -5328,14 +4349,14 @@ function acquireLock(taskDir) {
|
|
|
5328
4349
|
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
5329
4350
|
}
|
|
5330
4351
|
try {
|
|
5331
|
-
|
|
4352
|
+
fs27.unlinkSync(lockPath);
|
|
5332
4353
|
} catch {
|
|
5333
4354
|
}
|
|
5334
4355
|
}
|
|
5335
4356
|
try {
|
|
5336
|
-
const fd =
|
|
5337
|
-
|
|
5338
|
-
|
|
4357
|
+
const fd = fs27.openSync(lockPath, fs27.constants.O_WRONLY | fs27.constants.O_CREAT | fs27.constants.O_EXCL);
|
|
4358
|
+
fs27.writeSync(fd, String(process.pid));
|
|
4359
|
+
fs27.closeSync(fd);
|
|
5339
4360
|
} catch (err) {
|
|
5340
4361
|
if (err.code === "EEXIST") {
|
|
5341
4362
|
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
@@ -5345,7 +4366,7 @@ function acquireLock(taskDir) {
|
|
|
5345
4366
|
}
|
|
5346
4367
|
function releaseLock(taskDir) {
|
|
5347
4368
|
try {
|
|
5348
|
-
|
|
4369
|
+
fs27.unlinkSync(path25.join(taskDir, ".lock"));
|
|
5349
4370
|
} catch {
|
|
5350
4371
|
}
|
|
5351
4372
|
}
|
|
@@ -5434,23 +4455,23 @@ async function runPipelineInner(ctx) {
|
|
|
5434
4455
|
writeState(state, ctx.taskDir);
|
|
5435
4456
|
logger.info(`[${def.name}] starting...`);
|
|
5436
4457
|
applyPreStageLabel(ctx, def);
|
|
5437
|
-
let
|
|
4458
|
+
let result;
|
|
5438
4459
|
try {
|
|
5439
|
-
|
|
4460
|
+
result = await getExecutor(def.name)(ctx, def);
|
|
5440
4461
|
} catch (error) {
|
|
5441
|
-
|
|
4462
|
+
result = {
|
|
5442
4463
|
outcome: "failed",
|
|
5443
4464
|
retries: 0,
|
|
5444
4465
|
error: error instanceof Error ? error.message : String(error)
|
|
5445
4466
|
};
|
|
5446
4467
|
}
|
|
5447
4468
|
ciGroupEnd();
|
|
5448
|
-
if (
|
|
4469
|
+
if (result.outcome === "completed") {
|
|
5449
4470
|
state.stages[def.name] = {
|
|
5450
4471
|
state: "completed",
|
|
5451
4472
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5452
|
-
retries:
|
|
5453
|
-
outputFile:
|
|
4473
|
+
retries: result.retries,
|
|
4474
|
+
outputFile: result.outputFile
|
|
5454
4475
|
};
|
|
5455
4476
|
logger.info(`[${def.name}] \u2713 completed`);
|
|
5456
4477
|
const detected = autoDetectComplexity(ctx, def);
|
|
@@ -5464,16 +4485,16 @@ async function runPipelineInner(ctx) {
|
|
|
5464
4485
|
if (gated) return gated;
|
|
5465
4486
|
commitAfterStage(ctx, def);
|
|
5466
4487
|
} else {
|
|
5467
|
-
const isTimeout =
|
|
4488
|
+
const isTimeout = result.outcome === "timed_out";
|
|
5468
4489
|
state.stages[def.name] = {
|
|
5469
4490
|
state: isTimeout ? "timeout" : "failed",
|
|
5470
|
-
retries:
|
|
5471
|
-
error: isTimeout ? "Stage timed out" :
|
|
4491
|
+
retries: result.retries,
|
|
4492
|
+
error: isTimeout ? "Stage timed out" : result.error ?? "Stage failed"
|
|
5472
4493
|
};
|
|
5473
4494
|
state.state = "failed";
|
|
5474
4495
|
state.sessions = ctx.sessions;
|
|
5475
4496
|
writeState(state, ctx.taskDir);
|
|
5476
|
-
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${
|
|
4497
|
+
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result.error}`}`);
|
|
5477
4498
|
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
5478
4499
|
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
5479
4500
|
}
|
|
@@ -5553,8 +4574,8 @@ var init_pipeline = __esm({
|
|
|
5553
4574
|
});
|
|
5554
4575
|
|
|
5555
4576
|
// src/preflight.ts
|
|
5556
|
-
import { execFileSync as
|
|
5557
|
-
import * as
|
|
4577
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
4578
|
+
import * as fs28 from "fs";
|
|
5558
4579
|
function check(name, fn) {
|
|
5559
4580
|
try {
|
|
5560
4581
|
const detail = fn() ?? void 0;
|
|
@@ -5566,7 +4587,7 @@ function check(name, fn) {
|
|
|
5566
4587
|
function runPreflight() {
|
|
5567
4588
|
const checks = [
|
|
5568
4589
|
check("claude CLI", () => {
|
|
5569
|
-
const v =
|
|
4590
|
+
const v = execFileSync15("claude", ["--version"], {
|
|
5570
4591
|
encoding: "utf-8",
|
|
5571
4592
|
timeout: 1e4,
|
|
5572
4593
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5574,14 +4595,14 @@ function runPreflight() {
|
|
|
5574
4595
|
return v;
|
|
5575
4596
|
}),
|
|
5576
4597
|
check("git repo", () => {
|
|
5577
|
-
|
|
4598
|
+
execFileSync15("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
5578
4599
|
encoding: "utf-8",
|
|
5579
4600
|
timeout: 5e3,
|
|
5580
4601
|
stdio: ["pipe", "pipe", "pipe"]
|
|
5581
4602
|
});
|
|
5582
4603
|
}),
|
|
5583
4604
|
check("pnpm", () => {
|
|
5584
|
-
const v =
|
|
4605
|
+
const v = execFileSync15("pnpm", ["--version"], {
|
|
5585
4606
|
encoding: "utf-8",
|
|
5586
4607
|
timeout: 5e3,
|
|
5587
4608
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5589,7 +4610,7 @@ function runPreflight() {
|
|
|
5589
4610
|
return v;
|
|
5590
4611
|
}),
|
|
5591
4612
|
check("node >= 18", () => {
|
|
5592
|
-
const v =
|
|
4613
|
+
const v = execFileSync15("node", ["--version"], {
|
|
5593
4614
|
encoding: "utf-8",
|
|
5594
4615
|
timeout: 5e3,
|
|
5595
4616
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5599,7 +4620,7 @@ function runPreflight() {
|
|
|
5599
4620
|
return v;
|
|
5600
4621
|
}),
|
|
5601
4622
|
check("gh CLI", () => {
|
|
5602
|
-
const v =
|
|
4623
|
+
const v = execFileSync15("gh", ["--version"], {
|
|
5603
4624
|
encoding: "utf-8",
|
|
5604
4625
|
timeout: 5e3,
|
|
5605
4626
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5607,7 +4628,7 @@ function runPreflight() {
|
|
|
5607
4628
|
return v;
|
|
5608
4629
|
}),
|
|
5609
4630
|
check("package.json", () => {
|
|
5610
|
-
if (!
|
|
4631
|
+
if (!fs28.existsSync("package.json")) throw new Error("not found");
|
|
5611
4632
|
})
|
|
5612
4633
|
];
|
|
5613
4634
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -5684,8 +4705,8 @@ var init_args = __esm({
|
|
|
5684
4705
|
});
|
|
5685
4706
|
|
|
5686
4707
|
// src/cli/task-state.ts
|
|
5687
|
-
import * as
|
|
5688
|
-
import * as
|
|
4708
|
+
import * as fs29 from "fs";
|
|
4709
|
+
import * as path26 from "path";
|
|
5689
4710
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
5690
4711
|
if (!existingTaskId || !existingState) {
|
|
5691
4712
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -5717,11 +4738,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
5717
4738
|
function resolveForIssue(issueNumber, projectDir) {
|
|
5718
4739
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
5719
4740
|
if (existingTaskId) {
|
|
5720
|
-
const statusPath =
|
|
4741
|
+
const statusPath = path26.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
5721
4742
|
let existingState = null;
|
|
5722
|
-
if (
|
|
4743
|
+
if (fs29.existsSync(statusPath)) {
|
|
5723
4744
|
try {
|
|
5724
|
-
existingState = JSON.parse(
|
|
4745
|
+
existingState = JSON.parse(fs29.readFileSync(statusPath, "utf-8"));
|
|
5725
4746
|
} catch {
|
|
5726
4747
|
}
|
|
5727
4748
|
}
|
|
@@ -5754,12 +4775,12 @@ var resolve_exports = {};
|
|
|
5754
4775
|
__export(resolve_exports, {
|
|
5755
4776
|
runResolve: () => runResolve
|
|
5756
4777
|
});
|
|
5757
|
-
import { execFileSync as
|
|
4778
|
+
import { execFileSync as execFileSync16 } from "child_process";
|
|
5758
4779
|
function getConflictContext(cwd, files) {
|
|
5759
4780
|
const parts = [];
|
|
5760
4781
|
for (const file of files.slice(0, 10)) {
|
|
5761
4782
|
try {
|
|
5762
|
-
const content =
|
|
4783
|
+
const content = execFileSync16("git", ["diff", file], {
|
|
5763
4784
|
cwd,
|
|
5764
4785
|
encoding: "utf-8",
|
|
5765
4786
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5809,12 +4830,12 @@ async function runResolve(options) {
|
|
|
5809
4830
|
extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
|
|
5810
4831
|
}
|
|
5811
4832
|
logger.info(` Running agent to resolve conflicts (model=${model})...`);
|
|
5812
|
-
const
|
|
4833
|
+
const result = await runner.run("resolve", prompt, model, 3e5, projectDir, {
|
|
5813
4834
|
cwd: projectDir,
|
|
5814
4835
|
env: extraEnv
|
|
5815
4836
|
});
|
|
5816
|
-
if (
|
|
5817
|
-
return { outcome: "failed", error: `Agent failed: ${
|
|
4837
|
+
if (result.outcome !== "completed") {
|
|
4838
|
+
return { outcome: "failed", error: `Agent failed: ${result.error}` };
|
|
5818
4839
|
}
|
|
5819
4840
|
logger.info(" Verifying resolution...");
|
|
5820
4841
|
const verify = runQualityGates(projectDir, projectDir);
|
|
@@ -5878,8 +4899,8 @@ var init_resolve = __esm({
|
|
|
5878
4899
|
|
|
5879
4900
|
// src/entry.ts
|
|
5880
4901
|
var entry_exports = {};
|
|
5881
|
-
import * as
|
|
5882
|
-
import * as
|
|
4902
|
+
import * as fs30 from "fs";
|
|
4903
|
+
import * as path27 from "path";
|
|
5883
4904
|
async function ensureLitellmProxy(config, projectDir) {
|
|
5884
4905
|
if (!anyStageNeedsProxy(config)) return null;
|
|
5885
4906
|
const litellmUrl = getLitellmUrl();
|
|
@@ -5914,7 +4935,7 @@ async function ensureLitellmProxy(config, projectDir) {
|
|
|
5914
4935
|
return litellmProcess;
|
|
5915
4936
|
}
|
|
5916
4937
|
async function runModelHealthCheck(config) {
|
|
5917
|
-
const usesProxy =
|
|
4938
|
+
const usesProxy = needsLitellmProxy(config);
|
|
5918
4939
|
const baseUrl = usesProxy ? getLitellmUrl() : "https://api.anthropic.com";
|
|
5919
4940
|
const apiKey = usesProxy ? process.env.ANTHROPIC_COMPATIBLE_API_KEY : process.env.ANTHROPIC_API_KEY;
|
|
5920
4941
|
if (!apiKey) {
|
|
@@ -5924,19 +4945,19 @@ async function runModelHealthCheck(config) {
|
|
|
5924
4945
|
}
|
|
5925
4946
|
const model = config.agent.modelMap.cheap;
|
|
5926
4947
|
logger.info(`Model health check (${model} via ${usesProxy ? "LiteLLM" : "Anthropic"})...`);
|
|
5927
|
-
const
|
|
5928
|
-
if (
|
|
4948
|
+
const result = await checkModelHealth(baseUrl, apiKey, model);
|
|
4949
|
+
if (result.ok) {
|
|
5929
4950
|
logger.info(" \u2713 Model responded");
|
|
5930
4951
|
} else {
|
|
5931
|
-
logger.error(` \u2717 Model health check failed: ${
|
|
4952
|
+
logger.error(` \u2717 Model health check failed: ${result.error}`);
|
|
5932
4953
|
process.exit(1);
|
|
5933
4954
|
}
|
|
5934
4955
|
}
|
|
5935
4956
|
async function main() {
|
|
5936
4957
|
const input = parseArgs();
|
|
5937
|
-
const projectDir = input.cwd ?
|
|
4958
|
+
const projectDir = input.cwd ? path27.resolve(input.cwd) : process.cwd();
|
|
5938
4959
|
if (input.cwd) {
|
|
5939
|
-
if (!
|
|
4960
|
+
if (!fs30.existsSync(projectDir)) {
|
|
5940
4961
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
5941
4962
|
process.exit(1);
|
|
5942
4963
|
}
|
|
@@ -5945,7 +4966,7 @@ async function main() {
|
|
|
5945
4966
|
logger.info(`Working directory: ${projectDir}`);
|
|
5946
4967
|
}
|
|
5947
4968
|
const isPRFix = (input.command === "fix" || input.command === "fix-ci") && !!input.prNumber;
|
|
5948
|
-
const skipStateCheck = input.command === "review" || input.command === "resolve" || input.command === "rerun"
|
|
4969
|
+
const skipStateCheck = input.command === "review" || input.command === "resolve" || input.command === "rerun";
|
|
5949
4970
|
if (input.issueNumber && !skipStateCheck && !isPRFix) {
|
|
5950
4971
|
const taskAction = resolveForIssue(input.issueNumber, projectDir);
|
|
5951
4972
|
logger.info(`Task action: ${taskAction.action}`);
|
|
@@ -6002,8 +5023,8 @@ async function main() {
|
|
|
6002
5023
|
process.exit(1);
|
|
6003
5024
|
}
|
|
6004
5025
|
}
|
|
6005
|
-
const taskDir =
|
|
6006
|
-
|
|
5026
|
+
const taskDir = path27.join(projectDir, ".kody", "tasks", taskId);
|
|
5027
|
+
fs30.mkdirSync(taskDir, { recursive: true });
|
|
6007
5028
|
if (input.command === "rerun" && isTaskifyRun(taskDir)) {
|
|
6008
5029
|
const marker = readTaskifyMarker(taskDir);
|
|
6009
5030
|
if (marker) {
|
|
@@ -6068,7 +5089,7 @@ async function main() {
|
|
|
6068
5089
|
console.error(`Runner "${defaultRunnerName2}" health check failed`);
|
|
6069
5090
|
process.exit(1);
|
|
6070
5091
|
}
|
|
6071
|
-
const
|
|
5092
|
+
const result = await runStandaloneReview({
|
|
6072
5093
|
projectDir,
|
|
6073
5094
|
runners: runners2,
|
|
6074
5095
|
prTitle,
|
|
@@ -6078,15 +5099,15 @@ async function main() {
|
|
|
6078
5099
|
taskId
|
|
6079
5100
|
});
|
|
6080
5101
|
if (litellmProcess2) litellmProcess2.kill();
|
|
6081
|
-
if (
|
|
6082
|
-
console.error(`Review failed: ${
|
|
5102
|
+
if (result.outcome === "failed") {
|
|
5103
|
+
console.error(`Review failed: ${result.error}`);
|
|
6083
5104
|
process.exit(1);
|
|
6084
5105
|
}
|
|
6085
|
-
if (
|
|
6086
|
-
console.log(
|
|
5106
|
+
if (result.reviewContent) {
|
|
5107
|
+
console.log(result.reviewContent);
|
|
6087
5108
|
if (!input.local && prNumber) {
|
|
6088
|
-
const comment = formatReviewComment(
|
|
6089
|
-
const verdict = detectReviewVerdict(
|
|
5109
|
+
const comment = formatReviewComment(result.reviewContent, taskId);
|
|
5110
|
+
const verdict = detectReviewVerdict(result.reviewContent);
|
|
6090
5111
|
const event = verdict === "fail" ? "request-changes" : "approve";
|
|
6091
5112
|
const posted = submitPRReview(prNumber, comment, event);
|
|
6092
5113
|
if (!posted) {
|
|
@@ -6118,48 +5139,48 @@ async function main() {
|
|
|
6118
5139
|
process.exit(1);
|
|
6119
5140
|
}
|
|
6120
5141
|
const { runResolve: runResolve2 } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
|
|
6121
|
-
const
|
|
5142
|
+
const result = await runResolve2({
|
|
6122
5143
|
prNumber: input.prNumber,
|
|
6123
5144
|
projectDir,
|
|
6124
5145
|
runners: runners2,
|
|
6125
5146
|
local: input.local ?? true
|
|
6126
5147
|
});
|
|
6127
5148
|
if (litellmProcess2) litellmProcess2.kill();
|
|
6128
|
-
if (
|
|
6129
|
-
console.error(`Resolve failed: ${
|
|
5149
|
+
if (result.outcome === "failed") {
|
|
5150
|
+
console.error(`Resolve failed: ${result.error}`);
|
|
6130
5151
|
process.exit(1);
|
|
6131
5152
|
}
|
|
6132
|
-
console.log(`Resolve: ${
|
|
5153
|
+
console.log(`Resolve: ${result.outcome}`);
|
|
6133
5154
|
process.exit(0);
|
|
6134
5155
|
}
|
|
6135
5156
|
logger.info("Preflight checks:");
|
|
6136
5157
|
runPreflight();
|
|
6137
5158
|
if (input.task) {
|
|
6138
|
-
|
|
5159
|
+
fs30.writeFileSync(path27.join(taskDir, "task.md"), input.task);
|
|
6139
5160
|
}
|
|
6140
|
-
const taskMdPath =
|
|
6141
|
-
if (!
|
|
5161
|
+
const taskMdPath = path27.join(taskDir, "task.md");
|
|
5162
|
+
if (!fs30.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
6142
5163
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
6143
5164
|
const prDetails = getPRDetails(input.prNumber);
|
|
6144
5165
|
if (prDetails) {
|
|
6145
5166
|
const taskContent = `# ${prDetails.title}
|
|
6146
5167
|
|
|
6147
5168
|
${prDetails.body ?? ""}`;
|
|
6148
|
-
|
|
5169
|
+
fs30.writeFileSync(taskMdPath, taskContent);
|
|
6149
5170
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
6150
5171
|
}
|
|
6151
|
-
} else if (!
|
|
5172
|
+
} else if (!fs30.existsSync(taskMdPath) && input.issueNumber) {
|
|
6152
5173
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
6153
5174
|
const issue = getIssue(input.issueNumber);
|
|
6154
5175
|
if (issue) {
|
|
6155
5176
|
const taskContent = `# ${issue.title}
|
|
6156
5177
|
|
|
6157
5178
|
${issue.body ?? ""}`;
|
|
6158
|
-
|
|
5179
|
+
fs30.writeFileSync(taskMdPath, taskContent);
|
|
6159
5180
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
6160
5181
|
}
|
|
6161
5182
|
}
|
|
6162
|
-
if (!
|
|
5183
|
+
if (!fs30.existsSync(taskMdPath)) {
|
|
6163
5184
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
6164
5185
|
process.exit(1);
|
|
6165
5186
|
}
|
|
@@ -6297,7 +5318,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
6297
5318
|
}
|
|
6298
5319
|
}
|
|
6299
5320
|
const state = await runPipeline(ctx);
|
|
6300
|
-
const files =
|
|
5321
|
+
const files = fs30.readdirSync(taskDir);
|
|
6301
5322
|
console.log(`
|
|
6302
5323
|
Artifacts in ${taskDir}:`);
|
|
6303
5324
|
for (const f of files) {
|
|
@@ -6362,8 +5383,8 @@ var init_entry = __esm({
|
|
|
6362
5383
|
});
|
|
6363
5384
|
|
|
6364
5385
|
// src/bin/cli.ts
|
|
6365
|
-
import * as
|
|
6366
|
-
import * as
|
|
5386
|
+
import * as fs31 from "fs";
|
|
5387
|
+
import * as path28 from "path";
|
|
6367
5388
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6368
5389
|
|
|
6369
5390
|
// src/bin/commands/init.ts
|
|
@@ -6535,7 +5556,7 @@ function buildConfig(cwd, basic) {
|
|
|
6535
5556
|
github: { owner: basic.owner, repo: basic.repo },
|
|
6536
5557
|
agent: {
|
|
6537
5558
|
provider: "anthropic",
|
|
6538
|
-
modelMap: { cheap: "
|
|
5559
|
+
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
6539
5560
|
}
|
|
6540
5561
|
};
|
|
6541
5562
|
const mcp = detectMcpConfig(cwd, basic.pm, pkg);
|
|
@@ -6735,15 +5756,15 @@ function initCommand(opts, pkgRoot) {
|
|
|
6735
5756
|
|
|
6736
5757
|
// src/bin/commands/bootstrap.ts
|
|
6737
5758
|
init_architecture_detection();
|
|
6738
|
-
import * as
|
|
6739
|
-
import * as
|
|
5759
|
+
import * as fs7 from "fs";
|
|
5760
|
+
import * as path6 from "path";
|
|
6740
5761
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
6741
5762
|
|
|
6742
5763
|
// src/bin/qa-guide.ts
|
|
6743
5764
|
import * as fs5 from "fs";
|
|
6744
5765
|
import * as path4 from "path";
|
|
6745
5766
|
function discoverQaContext(cwd) {
|
|
6746
|
-
const
|
|
5767
|
+
const result = {
|
|
6747
5768
|
routes: [],
|
|
6748
5769
|
authFiles: [],
|
|
6749
5770
|
loginPage: null,
|
|
@@ -6756,21 +5777,21 @@ function discoverQaContext(cwd) {
|
|
|
6756
5777
|
const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
|
|
6757
5778
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
6758
5779
|
const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
6759
|
-
if (pkg.scripts?.dev)
|
|
6760
|
-
if (allDeps.next || allDeps.nuxt)
|
|
6761
|
-
else if (allDeps.vite)
|
|
5780
|
+
if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
|
|
5781
|
+
if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
|
|
5782
|
+
else if (allDeps.vite) result.devPort = 5173;
|
|
6762
5783
|
} catch {
|
|
6763
5784
|
}
|
|
6764
5785
|
const appDirs = ["src/app", "app"];
|
|
6765
5786
|
for (const appDir of appDirs) {
|
|
6766
5787
|
const fullAppDir = path4.join(cwd, appDir);
|
|
6767
5788
|
if (!fs5.existsSync(fullAppDir)) continue;
|
|
6768
|
-
scanRoutes(fullAppDir, appDir, "",
|
|
5789
|
+
scanRoutes(fullAppDir, appDir, "", result);
|
|
6769
5790
|
break;
|
|
6770
5791
|
}
|
|
6771
5792
|
const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
6772
5793
|
for (const p of authPatterns) {
|
|
6773
|
-
if (fs5.existsSync(path4.join(cwd, p)))
|
|
5794
|
+
if (fs5.existsSync(path4.join(cwd, p))) result.authFiles.push(p);
|
|
6774
5795
|
}
|
|
6775
5796
|
const authConfigGlobs = [
|
|
6776
5797
|
"src/app/api/auth",
|
|
@@ -6781,7 +5802,7 @@ function discoverQaContext(cwd) {
|
|
|
6781
5802
|
"src/app/api/oauth"
|
|
6782
5803
|
];
|
|
6783
5804
|
for (const g of authConfigGlobs) {
|
|
6784
|
-
if (fs5.existsSync(path4.join(cwd, g)))
|
|
5805
|
+
if (fs5.existsSync(path4.join(cwd, g))) result.authFiles.push(g);
|
|
6785
5806
|
}
|
|
6786
5807
|
try {
|
|
6787
5808
|
const rolePaths = [
|
|
@@ -6803,7 +5824,7 @@ function discoverQaContext(cwd) {
|
|
|
6803
5824
|
if (roleMatches) {
|
|
6804
5825
|
for (const m of roleMatches) {
|
|
6805
5826
|
const val = m.match(/['"](\w+)['"]/);
|
|
6806
|
-
if (val && !
|
|
5827
|
+
if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
|
|
6807
5828
|
}
|
|
6808
5829
|
}
|
|
6809
5830
|
const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
|
|
@@ -6812,7 +5833,7 @@ function discoverQaContext(cwd) {
|
|
|
6812
5833
|
if (vals) {
|
|
6813
5834
|
for (const v of vals) {
|
|
6814
5835
|
const clean = v.replace(/['"]/g, "");
|
|
6815
|
-
if (!
|
|
5836
|
+
if (!result.roles.includes(clean)) result.roles.push(clean);
|
|
6816
5837
|
}
|
|
6817
5838
|
}
|
|
6818
5839
|
}
|
|
@@ -6822,9 +5843,9 @@ function discoverQaContext(cwd) {
|
|
|
6822
5843
|
}
|
|
6823
5844
|
} catch {
|
|
6824
5845
|
}
|
|
6825
|
-
return
|
|
5846
|
+
return result;
|
|
6826
5847
|
}
|
|
6827
|
-
function scanRoutes(dir, baseDir, prefix,
|
|
5848
|
+
function scanRoutes(dir, baseDir, prefix, result) {
|
|
6828
5849
|
let entries;
|
|
6829
5850
|
try {
|
|
6830
5851
|
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
@@ -6835,16 +5856,16 @@ function scanRoutes(dir, baseDir, prefix, result2) {
|
|
|
6835
5856
|
if (hasPage) {
|
|
6836
5857
|
const routePath = prefix || "/";
|
|
6837
5858
|
const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
|
|
6838
|
-
|
|
6839
|
-
if (prefix.includes("/login"))
|
|
6840
|
-
if (prefix.startsWith("/admin") && !
|
|
5859
|
+
result.routes.push({ path: routePath, group });
|
|
5860
|
+
if (prefix.includes("/login")) result.loginPage = routePath;
|
|
5861
|
+
if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
|
|
6841
5862
|
}
|
|
6842
5863
|
for (const entry of entries) {
|
|
6843
5864
|
if (!entry.isDirectory()) continue;
|
|
6844
5865
|
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
6845
5866
|
let segment = entry.name;
|
|
6846
5867
|
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
6847
|
-
scanRoutes(path4.join(dir, entry.name), baseDir, prefix,
|
|
5868
|
+
scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result);
|
|
6848
5869
|
continue;
|
|
6849
5870
|
}
|
|
6850
5871
|
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
@@ -6853,7 +5874,7 @@ function scanRoutes(dir, baseDir, prefix, result2) {
|
|
|
6853
5874
|
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
6854
5875
|
segment = `:${segment.slice(2, -2)}?`;
|
|
6855
5876
|
}
|
|
6856
|
-
scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`,
|
|
5877
|
+
scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
|
|
6857
5878
|
}
|
|
6858
5879
|
}
|
|
6859
5880
|
function generateQaGuide(discovery) {
|
|
@@ -7010,23 +6031,22 @@ function installSkillsForProject(cwd) {
|
|
|
7010
6031
|
}
|
|
7011
6032
|
|
|
7012
6033
|
// src/bin/commands/bootstrap.ts
|
|
7013
|
-
init_config();
|
|
7014
6034
|
var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
|
|
7015
6035
|
function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
7016
|
-
const srcDir =
|
|
7017
|
-
const baseDir =
|
|
6036
|
+
const srcDir = path6.join(cwd, "src");
|
|
6037
|
+
const baseDir = fs7.existsSync(srcDir) ? srcDir : cwd;
|
|
7018
6038
|
const results = [];
|
|
7019
6039
|
function walk(dir) {
|
|
7020
6040
|
const entries = [];
|
|
7021
6041
|
try {
|
|
7022
|
-
for (const entry of
|
|
6042
|
+
for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
|
|
7023
6043
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
7024
|
-
const full =
|
|
6044
|
+
const full = path6.join(dir, entry.name);
|
|
7025
6045
|
if (entry.isDirectory()) {
|
|
7026
6046
|
entries.push(...walk(full));
|
|
7027
6047
|
} else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
|
|
7028
6048
|
try {
|
|
7029
|
-
const stat =
|
|
6049
|
+
const stat = fs7.statSync(full);
|
|
7030
6050
|
if (stat.size >= 200 && stat.size <= 5e3) {
|
|
7031
6051
|
entries.push({ filePath: full, size: stat.size });
|
|
7032
6052
|
}
|
|
@@ -7040,8 +6060,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
|
7040
6060
|
}
|
|
7041
6061
|
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
7042
6062
|
for (const { filePath } of files) {
|
|
7043
|
-
const rel =
|
|
7044
|
-
const content =
|
|
6063
|
+
const rel = path6.relative(cwd, filePath);
|
|
6064
|
+
const content = fs7.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
7045
6065
|
results.push(`### File: ${rel}
|
|
7046
6066
|
\`\`\`typescript
|
|
7047
6067
|
${content}
|
|
@@ -7053,9 +6073,9 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
7053
6073
|
try {
|
|
7054
6074
|
let repoSlug = "";
|
|
7055
6075
|
try {
|
|
7056
|
-
const configPath =
|
|
7057
|
-
if (
|
|
7058
|
-
const config = JSON.parse(
|
|
6076
|
+
const configPath = path6.join(cwd, "kody.config.json");
|
|
6077
|
+
if (fs7.existsSync(configPath)) {
|
|
6078
|
+
const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
7059
6079
|
if (config.github?.owner && config.github?.repo) {
|
|
7060
6080
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
7061
6081
|
}
|
|
@@ -7082,9 +6102,7 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
7082
6102
|
}
|
|
7083
6103
|
function bootstrapCommand(opts, pkgRoot) {
|
|
7084
6104
|
const cwd = process.cwd();
|
|
7085
|
-
setConfigDir(cwd);
|
|
7086
6105
|
const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
|
|
7087
|
-
const bootstrapModel = resolveStageConfig(getProjectConfig(), "bootstrap", "cheap").model;
|
|
7088
6106
|
console.log(`
|
|
7089
6107
|
\u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
|
|
7090
6108
|
`);
|
|
@@ -7092,8 +6110,8 @@ function bootstrapCommand(opts, pkgRoot) {
|
|
|
7092
6110
|
ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
|
|
7093
6111
|
}
|
|
7094
6112
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
7095
|
-
const p =
|
|
7096
|
-
if (
|
|
6113
|
+
const p = path6.join(cwd, rel);
|
|
6114
|
+
if (fs7.existsSync(p)) return fs7.readFileSync(p, "utf-8").slice(0, maxChars);
|
|
7097
6115
|
return null;
|
|
7098
6116
|
};
|
|
7099
6117
|
let repoContext = "";
|
|
@@ -7128,14 +6146,14 @@ ${sampleFiles}
|
|
|
7128
6146
|
|
|
7129
6147
|
`;
|
|
7130
6148
|
try {
|
|
7131
|
-
const topDirs =
|
|
6149
|
+
const topDirs = fs7.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
7132
6150
|
repoContext += `## Top-level directories
|
|
7133
6151
|
${topDirs.join(", ")}
|
|
7134
6152
|
|
|
7135
6153
|
`;
|
|
7136
|
-
const srcDir =
|
|
7137
|
-
if (
|
|
7138
|
-
const srcDirs =
|
|
6154
|
+
const srcDir = path6.join(cwd, "src");
|
|
6155
|
+
if (fs7.existsSync(srcDir)) {
|
|
6156
|
+
const srcDirs = fs7.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
7139
6157
|
if (srcDirs.length > 0) repoContext += `## src/ subdirectories
|
|
7140
6158
|
${srcDirs.join(", ")}
|
|
7141
6159
|
|
|
@@ -7145,19 +6163,19 @@ ${srcDirs.join(", ")}
|
|
|
7145
6163
|
}
|
|
7146
6164
|
const existingFiles = [];
|
|
7147
6165
|
for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "vitest.config.ts", "vitest.config.mts", "jest.config.ts", "playwright.config.ts", ".eslintrc.js", "eslint.config.mjs", ".prettierrc"]) {
|
|
7148
|
-
if (
|
|
6166
|
+
if (fs7.existsSync(path6.join(cwd, f))) existingFiles.push(f);
|
|
7149
6167
|
}
|
|
7150
6168
|
if (existingFiles.length) repoContext += `## Config files present
|
|
7151
6169
|
${existingFiles.join(", ")}
|
|
7152
6170
|
|
|
7153
6171
|
`;
|
|
7154
6172
|
console.log("\u2500\u2500 Project Memory \u2500\u2500");
|
|
7155
|
-
const memoryDir =
|
|
7156
|
-
|
|
7157
|
-
const archPath =
|
|
7158
|
-
const conventionsPath =
|
|
7159
|
-
const existingArch =
|
|
7160
|
-
const existingConv =
|
|
6173
|
+
const memoryDir = path6.join(cwd, ".kody", "memory");
|
|
6174
|
+
fs7.mkdirSync(memoryDir, { recursive: true });
|
|
6175
|
+
const archPath = path6.join(memoryDir, "architecture.md");
|
|
6176
|
+
const conventionsPath = path6.join(memoryDir, "conventions.md");
|
|
6177
|
+
const existingArch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
|
|
6178
|
+
const existingConv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
|
|
7161
6179
|
const hasExisting = !!(existingArch || existingConv);
|
|
7162
6180
|
const extendInstruction = hasExisting && !opts.force ? `
|
|
7163
6181
|
## Existing Documentation (EXTEND, do not replace)
|
|
@@ -7201,7 +6219,7 @@ ${repoContext}`;
|
|
|
7201
6219
|
const output = execFileSync5("claude", [
|
|
7202
6220
|
"--print",
|
|
7203
6221
|
"--model",
|
|
7204
|
-
|
|
6222
|
+
"claude-haiku-4-5-20251001",
|
|
7205
6223
|
"--dangerously-skip-permissions",
|
|
7206
6224
|
memoryPrompt
|
|
7207
6225
|
], {
|
|
@@ -7213,12 +6231,12 @@ ${repoContext}`;
|
|
|
7213
6231
|
const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
7214
6232
|
const parsed = JSON.parse(cleaned);
|
|
7215
6233
|
if (parsed.architecture) {
|
|
7216
|
-
|
|
6234
|
+
fs7.writeFileSync(archPath, parsed.architecture);
|
|
7217
6235
|
const lineCount = parsed.architecture.split("\n").length;
|
|
7218
6236
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
|
|
7219
6237
|
}
|
|
7220
6238
|
if (parsed.conventions) {
|
|
7221
|
-
|
|
6239
|
+
fs7.writeFileSync(conventionsPath, parsed.conventions);
|
|
7222
6240
|
const lineCount = parsed.conventions.split("\n").length;
|
|
7223
6241
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
|
|
7224
6242
|
}
|
|
@@ -7227,39 +6245,39 @@ ${repoContext}`;
|
|
|
7227
6245
|
const detected = detectArchitectureBasic(cwd);
|
|
7228
6246
|
if (detected.length > 0) {
|
|
7229
6247
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7230
|
-
|
|
6248
|
+
fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
7231
6249
|
|
|
7232
6250
|
## Overview
|
|
7233
6251
|
${detected.join("\n")}
|
|
7234
6252
|
`);
|
|
7235
6253
|
console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
|
|
7236
6254
|
}
|
|
7237
|
-
|
|
6255
|
+
fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
7238
6256
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
7239
6257
|
}
|
|
7240
6258
|
console.log("\n\u2500\u2500 Step Files \u2500\u2500");
|
|
7241
|
-
const stepsDir =
|
|
7242
|
-
|
|
7243
|
-
const arch =
|
|
7244
|
-
const conv =
|
|
6259
|
+
const stepsDir = path6.join(cwd, ".kody", "steps");
|
|
6260
|
+
fs7.mkdirSync(stepsDir, { recursive: true });
|
|
6261
|
+
const arch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
|
|
6262
|
+
const conv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
|
|
7245
6263
|
console.log(" \u23F3 Customizing step files...");
|
|
7246
6264
|
let stepCount = 0;
|
|
7247
6265
|
for (const stage of STEP_STAGES) {
|
|
7248
|
-
const templatePath =
|
|
7249
|
-
if (!
|
|
6266
|
+
const templatePath = path6.join(pkgRoot, "prompts", `${stage}.md`);
|
|
6267
|
+
if (!fs7.existsSync(templatePath)) {
|
|
7250
6268
|
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
7251
6269
|
continue;
|
|
7252
6270
|
}
|
|
7253
|
-
const stepOutputPath =
|
|
7254
|
-
if (
|
|
6271
|
+
const stepOutputPath = path6.join(stepsDir, `${stage}.md`);
|
|
6272
|
+
if (fs7.existsSync(stepOutputPath) && !opts.force) {
|
|
7255
6273
|
console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
|
|
7256
6274
|
continue;
|
|
7257
6275
|
}
|
|
7258
|
-
const defaultPrompt =
|
|
6276
|
+
const defaultPrompt = fs7.readFileSync(templatePath, "utf-8");
|
|
7259
6277
|
const contextPlaceholder = "{{TASK_CONTEXT}}";
|
|
7260
6278
|
const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
|
|
7261
6279
|
if (placeholderIdx === -1) {
|
|
7262
|
-
|
|
6280
|
+
fs7.copyFileSync(templatePath, stepOutputPath);
|
|
7263
6281
|
stepCount++;
|
|
7264
6282
|
console.log(` \u2713 ${stage}.md`);
|
|
7265
6283
|
continue;
|
|
@@ -7304,7 +6322,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
7304
6322
|
const output = execFileSync5("claude", [
|
|
7305
6323
|
"--print",
|
|
7306
6324
|
"--model",
|
|
7307
|
-
|
|
6325
|
+
"claude-haiku-4-5-20251001",
|
|
7308
6326
|
"--dangerously-skip-permissions",
|
|
7309
6327
|
customizationPrompt
|
|
7310
6328
|
], {
|
|
@@ -7316,23 +6334,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
7316
6334
|
let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
7317
6335
|
cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
|
|
7318
6336
|
const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
|
|
7319
|
-
|
|
6337
|
+
fs7.writeFileSync(stepOutputPath, finalPrompt);
|
|
7320
6338
|
stepCount++;
|
|
7321
6339
|
console.log(` \u2713 ${stage}.md`);
|
|
7322
6340
|
} catch {
|
|
7323
6341
|
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
7324
|
-
|
|
6342
|
+
fs7.copyFileSync(templatePath, stepOutputPath);
|
|
7325
6343
|
stepCount++;
|
|
7326
6344
|
}
|
|
7327
6345
|
}
|
|
7328
6346
|
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
7329
6347
|
console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
|
|
7330
|
-
const qaGuidePath =
|
|
7331
|
-
if (!
|
|
6348
|
+
const qaGuidePath = path6.join(cwd, ".kody", "qa-guide.md");
|
|
6349
|
+
if (!fs7.existsSync(qaGuidePath) || opts.force) {
|
|
7332
6350
|
const discovery = discoverQaContext(cwd);
|
|
7333
6351
|
if (discovery.routes.length > 0) {
|
|
7334
6352
|
const qaGuide = generateQaGuide(discovery);
|
|
7335
|
-
|
|
6353
|
+
fs7.writeFileSync(qaGuidePath, qaGuide);
|
|
7336
6354
|
console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
|
|
7337
6355
|
if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
|
|
7338
6356
|
if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
|
|
@@ -7347,9 +6365,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
7347
6365
|
try {
|
|
7348
6366
|
let repoSlug = "";
|
|
7349
6367
|
try {
|
|
7350
|
-
const configPath =
|
|
7351
|
-
if (
|
|
7352
|
-
const config = JSON.parse(
|
|
6368
|
+
const configPath = path6.join(cwd, "kody.config.json");
|
|
6369
|
+
if (fs7.existsSync(configPath)) {
|
|
6370
|
+
const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
7353
6371
|
if (config.github?.owner && config.github?.repo) {
|
|
7354
6372
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
7355
6373
|
}
|
|
@@ -7422,19 +6440,19 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
7422
6440
|
".kody/memory/conventions.md",
|
|
7423
6441
|
".kody/qa-guide.md",
|
|
7424
6442
|
...installedSkillPaths
|
|
7425
|
-
].filter((f) =>
|
|
7426
|
-
if (
|
|
6443
|
+
].filter((f) => fs7.existsSync(path6.join(cwd, f)));
|
|
6444
|
+
if (fs7.existsSync(path6.join(cwd, "skills-lock.json"))) {
|
|
7427
6445
|
filesToCommit.push("skills-lock.json");
|
|
7428
6446
|
}
|
|
7429
6447
|
for (const stage of STEP_STAGES) {
|
|
7430
6448
|
const stepFile = `.kody/steps/${stage}.md`;
|
|
7431
|
-
if (
|
|
6449
|
+
if (fs7.existsSync(path6.join(cwd, stepFile))) {
|
|
7432
6450
|
filesToCommit.push(stepFile);
|
|
7433
6451
|
}
|
|
7434
6452
|
}
|
|
7435
6453
|
if (filesToCommit.length > 0) {
|
|
7436
6454
|
try {
|
|
7437
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
6455
|
+
const fullPaths = filesToCommit.map((f) => path6.join(cwd, f));
|
|
7438
6456
|
for (let pass = 0; pass < 2; pass++) {
|
|
7439
6457
|
execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
|
|
7440
6458
|
cwd,
|
|
@@ -7461,9 +6479,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
7461
6479
|
console.log(` \u2713 Pushed branch: ${branchName}`);
|
|
7462
6480
|
let baseBranch = "main";
|
|
7463
6481
|
try {
|
|
7464
|
-
const configPath =
|
|
7465
|
-
if (
|
|
7466
|
-
const config = JSON.parse(
|
|
6482
|
+
const configPath = path6.join(cwd, "kody.config.json");
|
|
6483
|
+
if (fs7.existsSync(configPath)) {
|
|
6484
|
+
const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
7467
6485
|
baseBranch = config.git?.defaultBranch ?? "main";
|
|
7468
6486
|
}
|
|
7469
6487
|
} catch {
|
|
@@ -7537,11 +6555,11 @@ Create it manually.`, cwd);
|
|
|
7537
6555
|
|
|
7538
6556
|
// src/bin/cli.ts
|
|
7539
6557
|
init_architecture_detection();
|
|
7540
|
-
var __dirname2 =
|
|
7541
|
-
var PKG_ROOT =
|
|
6558
|
+
var __dirname2 = path28.dirname(fileURLToPath2(import.meta.url));
|
|
6559
|
+
var PKG_ROOT = path28.resolve(__dirname2, "..", "..");
|
|
7542
6560
|
function getVersion() {
|
|
7543
|
-
const pkgPath =
|
|
7544
|
-
const pkg = JSON.parse(
|
|
6561
|
+
const pkgPath = path28.join(PKG_ROOT, "package.json");
|
|
6562
|
+
const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
|
|
7545
6563
|
return pkg.version;
|
|
7546
6564
|
}
|
|
7547
6565
|
var args = process.argv.slice(2);
|
|
@@ -7552,8 +6570,6 @@ if (command === "init") {
|
|
|
7552
6570
|
bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
7553
6571
|
} else if (command === "taskify") {
|
|
7554
6572
|
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());
|
|
7557
6573
|
} else if (command === "ci-parse") {
|
|
7558
6574
|
Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
|
|
7559
6575
|
} else if (command === "version" || command === "--version" || command === "-v") {
|