@kody-ade/kody-engine-lite 0.1.117 → 0.1.118
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 +1623 -456
- package/kody.config.schema.json +31 -0
- package/package.json +2 -1
- package/prompts/taskify-ticket.md +10 -0
- package/templates/kody.yml +1 -0
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 fs7 from "fs";
|
|
184
|
+
import * as path6 from "path";
|
|
185
185
|
function resolveStageConfig(config, stageName, modelTier) {
|
|
186
186
|
const stageOverride = config.agent.stages?.[stageName];
|
|
187
187
|
if (stageOverride) return stageOverride;
|
|
@@ -214,7 +214,9 @@ function getLitellmUrl() {
|
|
|
214
214
|
return LITELLM_DEFAULT_URL;
|
|
215
215
|
}
|
|
216
216
|
function providerApiKeyEnvVar(provider) {
|
|
217
|
-
if (provider === "anthropic") return "ANTHROPIC_API_KEY";
|
|
217
|
+
if (provider === "anthropic" || provider === "claude") return "ANTHROPIC_API_KEY";
|
|
218
|
+
const derived = `${provider.toUpperCase()}_API_KEY`;
|
|
219
|
+
if (process.env[derived]) return derived;
|
|
218
220
|
return "ANTHROPIC_COMPATIBLE_API_KEY";
|
|
219
221
|
}
|
|
220
222
|
function setConfigDir(dir) {
|
|
@@ -223,16 +225,16 @@ function setConfigDir(dir) {
|
|
|
223
225
|
}
|
|
224
226
|
function getProjectConfig() {
|
|
225
227
|
if (_config) return _config;
|
|
226
|
-
const configPath =
|
|
227
|
-
if (
|
|
228
|
+
const configPath = path6.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
229
|
+
if (fs7.existsSync(configPath)) {
|
|
228
230
|
try {
|
|
229
|
-
const
|
|
230
|
-
if (!
|
|
231
|
-
logger.warn(`kody.config.json: ${
|
|
231
|
+
const result2 = parseJsonSafe(fs7.readFileSync(configPath, "utf-8"));
|
|
232
|
+
if (!result2.ok) {
|
|
233
|
+
logger.warn(`kody.config.json: ${result2.error} \u2014 using defaults`);
|
|
232
234
|
_config = { ...DEFAULT_CONFIG };
|
|
233
235
|
return _config;
|
|
234
236
|
}
|
|
235
|
-
const raw =
|
|
237
|
+
const raw = result2.data;
|
|
236
238
|
_config = {
|
|
237
239
|
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
238
240
|
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
@@ -283,7 +285,7 @@ var init_config = __esm({
|
|
|
283
285
|
repo: ""
|
|
284
286
|
},
|
|
285
287
|
agent: {
|
|
286
|
-
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
288
|
+
modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
|
|
287
289
|
},
|
|
288
290
|
contextTiers: {
|
|
289
291
|
enabled: true,
|
|
@@ -389,10 +391,11 @@ function createClaudeCodeRunner() {
|
|
|
389
391
|
model,
|
|
390
392
|
"--dangerously-skip-permissions"
|
|
391
393
|
];
|
|
394
|
+
const baseTools = "Bash,Edit,Read,Write,Glob,Grep";
|
|
392
395
|
if (options?.mcpConfigJson) {
|
|
393
396
|
args2.push("--mcp-config", options.mcpConfigJson);
|
|
394
397
|
} else {
|
|
395
|
-
args2.push("--allowedTools",
|
|
398
|
+
args2.push("--allowedTools", baseTools);
|
|
396
399
|
}
|
|
397
400
|
if (options?.sessionId) {
|
|
398
401
|
if (options.resumeSession) {
|
|
@@ -965,8 +968,8 @@ function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
|
965
968
|
}
|
|
966
969
|
function generateTaskId() {
|
|
967
970
|
const now = /* @__PURE__ */ new Date();
|
|
968
|
-
const
|
|
969
|
-
return `${String(now.getFullYear()).slice(2)}${
|
|
971
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
972
|
+
return `${String(now.getFullYear()).slice(2)}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
970
973
|
}
|
|
971
974
|
function resolveTaskIdFromComments(issueNumber) {
|
|
972
975
|
try {
|
|
@@ -1192,6 +1195,7 @@ var init_litellm = __esm({
|
|
|
1192
1195
|
// src/cli/taskify-command.ts
|
|
1193
1196
|
var taskify_command_exports = {};
|
|
1194
1197
|
__export(taskify_command_exports, {
|
|
1198
|
+
TaskifyError: () => TaskifyError,
|
|
1195
1199
|
isTaskifyRun: () => isTaskifyRun,
|
|
1196
1200
|
readTaskifyMarker: () => readTaskifyMarker,
|
|
1197
1201
|
runTaskifyCommand: () => runTaskifyCommand,
|
|
@@ -1253,8 +1257,8 @@ async function runTaskifyCommand() {
|
|
|
1253
1257
|
const local = hasFlag(args2, "--local") || !process.env.CI;
|
|
1254
1258
|
const taskIdArg = getArg(args2, "--task-id") ?? process.env.TASK_ID;
|
|
1255
1259
|
const taskId = taskIdArg ?? (issueNumber ? `taskify-${issueNumber}-${generateTaskId()}` : `taskify-${generateTaskId()}`);
|
|
1256
|
-
if (!ticketId && !prdFile) {
|
|
1257
|
-
logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md>");
|
|
1260
|
+
if (!ticketId && !prdFile && !issueNumber) {
|
|
1261
|
+
logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md> OR kody taskify --issue-number <n>");
|
|
1258
1262
|
process.exit(1);
|
|
1259
1263
|
}
|
|
1260
1264
|
if (prdFile && !fs11.existsSync(prdFile)) {
|
|
@@ -1283,23 +1287,31 @@ async function runTaskifyCommand() {
|
|
|
1283
1287
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
|
|
1284
1288
|
};
|
|
1285
1289
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1290
|
+
try {
|
|
1291
|
+
await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
if (err instanceof TaskifyError) {
|
|
1294
|
+
logger.error(`[taskify] ${err.message}`);
|
|
1295
|
+
process.exit(1);
|
|
1296
|
+
}
|
|
1297
|
+
throw err;
|
|
1298
|
+
} finally {
|
|
1299
|
+
litellmProcess?.kill();
|
|
1300
|
+
}
|
|
1288
1301
|
}
|
|
1289
1302
|
async function taskifyCommand(opts) {
|
|
1290
1303
|
const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
|
|
1291
1304
|
const config = getProjectConfig();
|
|
1292
1305
|
const taskDir = path10.join(projectDir, ".kody", "tasks", taskId);
|
|
1293
1306
|
fs11.mkdirSync(taskDir, { recursive: true });
|
|
1294
|
-
const mode = prdFile ? "file" : "ticket";
|
|
1295
|
-
logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile} issue=${issueNumber ?? "none"} task=${taskId}`);
|
|
1307
|
+
const mode = prdFile ? "file" : ticketId ? "ticket" : "issue";
|
|
1308
|
+
logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile ?? `issue#${issueNumber}`} issue=${issueNumber ?? "none"} task=${taskId}`);
|
|
1296
1309
|
let mcpConfigJson;
|
|
1297
1310
|
if (mode === "ticket") {
|
|
1298
1311
|
try {
|
|
1299
1312
|
mcpConfigJson = buildTaskifyMcpConfigJson(config);
|
|
1300
1313
|
} catch (err) {
|
|
1301
1314
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1302
|
-
logger.error(`[taskify] MCP config error: ${msg}`);
|
|
1303
1315
|
if (issueNumber && !local) {
|
|
1304
1316
|
postComment(
|
|
1305
1317
|
issueNumber,
|
|
@@ -1310,12 +1322,24 @@ async function taskifyCommand(opts) {
|
|
|
1310
1322
|
Add the required MCP server config to \`kody.config.json\` and try again.`
|
|
1311
1323
|
);
|
|
1312
1324
|
}
|
|
1313
|
-
|
|
1325
|
+
throw new TaskifyError(`MCP config error: ${msg}`);
|
|
1314
1326
|
}
|
|
1315
1327
|
}
|
|
1316
1328
|
const sc = resolveStageConfig(config, "taskify", "strong");
|
|
1317
1329
|
const model = sc.model;
|
|
1318
1330
|
const fileContent = prdFile ? fs11.readFileSync(prdFile, "utf-8") : void 0;
|
|
1331
|
+
let issueBody;
|
|
1332
|
+
if (mode === "issue" && issueNumber) {
|
|
1333
|
+
const issue = getIssue(issueNumber);
|
|
1334
|
+
if (issue) {
|
|
1335
|
+
issueBody = `# ${issue.title}
|
|
1336
|
+
|
|
1337
|
+
${issue.body}`;
|
|
1338
|
+
logger.info(` Fetched issue #${issueNumber} body (${issueBody.length} chars)`);
|
|
1339
|
+
} else {
|
|
1340
|
+
throw new TaskifyError(`Could not fetch issue #${issueNumber}`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1319
1343
|
let projectContext;
|
|
1320
1344
|
{
|
|
1321
1345
|
const parts = [];
|
|
@@ -1339,9 +1363,9 @@ ${lines.join("\n")}
|
|
|
1339
1363
|
}
|
|
1340
1364
|
if (parts.length > 0) projectContext = parts.join("\n\n");
|
|
1341
1365
|
}
|
|
1342
|
-
const prompt = buildPrompt({ ticketId, fileContent, taskDir, feedback, projectContext });
|
|
1366
|
+
const prompt = buildPrompt({ ticketId, fileContent, issueBody, taskDir, feedback, projectContext });
|
|
1343
1367
|
if (issueNumber && !local) {
|
|
1344
|
-
const src = mode === "file" ? `file \`${path10.basename(prdFile)}\`` : `ticket **${ticketId}
|
|
1368
|
+
const src = mode === "file" ? `file \`${path10.basename(prdFile)}\`` : mode === "ticket" ? `ticket **${ticketId}**` : `issue #${issueNumber} description`;
|
|
1345
1369
|
const runUrl = process.env.RUN_URL ? ` ([logs](${process.env.RUN_URL}))` : "";
|
|
1346
1370
|
postComment(issueNumber, `\u{1F680} Kody pipeline started: \`${taskId}\`${runUrl}
|
|
1347
1371
|
|
|
@@ -1351,61 +1375,57 @@ Kody is decomposing ${src} into tasks...`);
|
|
|
1351
1375
|
fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
|
|
1352
1376
|
const runner = opts.runner ?? createClaudeCodeRunner();
|
|
1353
1377
|
logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
|
|
1354
|
-
const
|
|
1378
|
+
const result2 = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
|
|
1355
1379
|
cwd: projectDir,
|
|
1356
1380
|
mcpConfigJson,
|
|
1357
1381
|
env: opts.runnerEnv
|
|
1358
1382
|
});
|
|
1359
|
-
if (
|
|
1360
|
-
const errMsg =
|
|
1361
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1383
|
+
if (result2.outcome !== "completed") {
|
|
1384
|
+
const errMsg = result2.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result2.error}`;
|
|
1362
1385
|
if (issueNumber && !local) {
|
|
1363
1386
|
postComment(issueNumber, `Kody taskify failed:
|
|
1364
1387
|
|
|
1365
1388
|
> ${errMsg}`);
|
|
1366
1389
|
setLifecycleLabel(issueNumber, "failed");
|
|
1367
1390
|
}
|
|
1368
|
-
|
|
1391
|
+
throw new TaskifyError(errMsg);
|
|
1369
1392
|
}
|
|
1370
1393
|
const resultPath = path10.join(taskDir, RESULT_FILE);
|
|
1371
1394
|
if (!fs11.existsSync(resultPath)) {
|
|
1372
1395
|
const errMsg = `Claude did not write ${RESULT_FILE}. Output:
|
|
1373
1396
|
|
|
1374
|
-
${
|
|
1375
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1397
|
+
${result2.output?.slice(0, 500) ?? "(none)"}`;
|
|
1376
1398
|
if (issueNumber && !local) {
|
|
1377
1399
|
postComment(issueNumber, `Kody taskify failed: result file not found.
|
|
1378
1400
|
|
|
1379
1401
|
${errMsg}`);
|
|
1380
1402
|
setLifecycleLabel(issueNumber, "failed");
|
|
1381
1403
|
}
|
|
1382
|
-
|
|
1404
|
+
throw new TaskifyError(errMsg);
|
|
1383
1405
|
}
|
|
1384
1406
|
let parsed;
|
|
1385
1407
|
try {
|
|
1386
1408
|
parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
|
|
1387
1409
|
} catch {
|
|
1388
1410
|
const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
|
|
1389
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1390
1411
|
if (issueNumber && !local) {
|
|
1391
1412
|
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1392
1413
|
setLifecycleLabel(issueNumber, "failed");
|
|
1393
1414
|
}
|
|
1394
|
-
|
|
1415
|
+
throw new TaskifyError(errMsg);
|
|
1395
1416
|
}
|
|
1396
|
-
const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
|
|
1417
|
+
const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : issueNumber ? `issue #${issueNumber}` : "spec");
|
|
1397
1418
|
if (parsed.status === "questions") {
|
|
1398
1419
|
handleQuestions(parsed, sourceLabel, issueNumber, local ?? false);
|
|
1399
1420
|
} else if (parsed.status === "ready") {
|
|
1400
1421
|
await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
|
|
1401
1422
|
} else {
|
|
1402
1423
|
const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
|
|
1403
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1404
1424
|
if (issueNumber && !local) {
|
|
1405
1425
|
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1406
1426
|
setLifecycleLabel(issueNumber, "failed");
|
|
1407
1427
|
}
|
|
1408
|
-
|
|
1428
|
+
throw new TaskifyError(errMsg);
|
|
1409
1429
|
}
|
|
1410
1430
|
}
|
|
1411
1431
|
function handleQuestions(parsed, ticketId, issueNumber, local) {
|
|
@@ -1491,7 +1511,7 @@ ${links}${triggerNote}`
|
|
|
1491
1511
|
}
|
|
1492
1512
|
}
|
|
1493
1513
|
function buildPrompt(opts) {
|
|
1494
|
-
const { ticketId, fileContent, taskDir, feedback, projectContext } = opts;
|
|
1514
|
+
const { ticketId, fileContent, issueBody, taskDir, feedback, projectContext } = opts;
|
|
1495
1515
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
1496
1516
|
const candidates = [
|
|
1497
1517
|
path10.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
|
|
@@ -1520,6 +1540,7 @@ function buildPrompt(opts) {
|
|
|
1520
1540
|
resolveBlock("PROJECT_CONTEXT", projectContext);
|
|
1521
1541
|
resolveBlock("TICKET_ID", ticketId);
|
|
1522
1542
|
resolveBlock("FILE_CONTENT", fileContent);
|
|
1543
|
+
resolveBlock("ISSUE_BODY", issueBody);
|
|
1523
1544
|
resolveBlock("FEEDBACK", feedback);
|
|
1524
1545
|
template = template.replace(/\{\{TASK_DIR\}\}/g, taskDir);
|
|
1525
1546
|
return template;
|
|
@@ -1536,7 +1557,7 @@ function readTaskifyMarker(taskDir) {
|
|
|
1536
1557
|
return null;
|
|
1537
1558
|
}
|
|
1538
1559
|
}
|
|
1539
|
-
var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
|
|
1560
|
+
var __dirname, TaskifyError, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
|
|
1540
1561
|
var init_taskify_command = __esm({
|
|
1541
1562
|
"src/cli/taskify-command.ts"() {
|
|
1542
1563
|
"use strict";
|
|
@@ -1548,6 +1569,12 @@ var init_taskify_command = __esm({
|
|
|
1548
1569
|
init_task_resolution();
|
|
1549
1570
|
init_litellm();
|
|
1550
1571
|
__dirname = path10.dirname(fileURLToPath(import.meta.url));
|
|
1572
|
+
TaskifyError = class extends Error {
|
|
1573
|
+
constructor(message) {
|
|
1574
|
+
super(message);
|
|
1575
|
+
this.name = "TaskifyError";
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1551
1578
|
AUTO_TRIGGER_THRESHOLD = 5;
|
|
1552
1579
|
MAX_TASKS_GUARD = 20;
|
|
1553
1580
|
TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -1556,6 +1583,1161 @@ var init_taskify_command = __esm({
|
|
|
1556
1583
|
}
|
|
1557
1584
|
});
|
|
1558
1585
|
|
|
1586
|
+
// src/cli/test-model-tests.ts
|
|
1587
|
+
import * as fs12 from "fs";
|
|
1588
|
+
import * as os2 from "os";
|
|
1589
|
+
import * as path11 from "path";
|
|
1590
|
+
import * as zlib from "zlib";
|
|
1591
|
+
import { spawnSync, execSync as execSync2 } from "child_process";
|
|
1592
|
+
function canRunApiTests(ctx) {
|
|
1593
|
+
return !!ctx.apiKey;
|
|
1594
|
+
}
|
|
1595
|
+
async function apiCall(ctx, body) {
|
|
1596
|
+
try {
|
|
1597
|
+
const res = await fetch(`${ctx.proxyUrl}/v1/messages`, {
|
|
1598
|
+
method: "POST",
|
|
1599
|
+
headers: {
|
|
1600
|
+
"Content-Type": "application/json",
|
|
1601
|
+
"x-api-key": ctx.apiKey,
|
|
1602
|
+
"anthropic-version": "2023-06-01"
|
|
1603
|
+
},
|
|
1604
|
+
body: JSON.stringify({ model: ctx.model, ...body }),
|
|
1605
|
+
signal: AbortSignal.timeout(6e4)
|
|
1606
|
+
});
|
|
1607
|
+
const data = await res.json();
|
|
1608
|
+
if (!res.ok) {
|
|
1609
|
+
return { ok: false, data, status: res.status, errorMsg: data?.error?.message ?? `HTTP ${res.status}` };
|
|
1610
|
+
}
|
|
1611
|
+
return { ok: true, data, status: res.status };
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
return { ok: false, data: null, status: 0, errorMsg: err instanceof Error ? err.message : String(err) };
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
function extractText(data) {
|
|
1617
|
+
if (!data?.content) return "";
|
|
1618
|
+
return data.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
|
|
1619
|
+
}
|
|
1620
|
+
async function runToolConversation(ctx, tools, userPrompt, simulate, opts) {
|
|
1621
|
+
const messages = [{ role: "user", content: userPrompt }];
|
|
1622
|
+
const allCalls = [];
|
|
1623
|
+
for (let turn = 0; turn < (opts?.maxTurns ?? 5); turn++) {
|
|
1624
|
+
const body = {
|
|
1625
|
+
max_tokens: 1024,
|
|
1626
|
+
temperature: 0,
|
|
1627
|
+
messages,
|
|
1628
|
+
tools
|
|
1629
|
+
};
|
|
1630
|
+
if (opts?.system) body.system = opts.system;
|
|
1631
|
+
const res = await apiCall(ctx, body);
|
|
1632
|
+
if (!res.ok) return { finalText: "", toolCalls: allCalls, error: res.errorMsg };
|
|
1633
|
+
const content = res.data.content ?? [];
|
|
1634
|
+
const toolBlocks = content.filter((b) => b.type === "tool_use");
|
|
1635
|
+
const textBlocks = content.filter((b) => b.type === "text");
|
|
1636
|
+
if (toolBlocks.length === 0) {
|
|
1637
|
+
return { finalText: textBlocks.map((b) => b.text ?? "").join(""), toolCalls: allCalls };
|
|
1638
|
+
}
|
|
1639
|
+
for (const tc of toolBlocks) allCalls.push({ name: tc.name, input: tc.input });
|
|
1640
|
+
messages.push({ role: "assistant", content });
|
|
1641
|
+
messages.push({
|
|
1642
|
+
role: "user",
|
|
1643
|
+
content: toolBlocks.map((tc) => ({
|
|
1644
|
+
type: "tool_result",
|
|
1645
|
+
tool_use_id: tc.id,
|
|
1646
|
+
content: simulate(tc.name, tc.input)
|
|
1647
|
+
}))
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
return { finalText: "", toolCalls: allCalls, error: "Max turns reached" };
|
|
1651
|
+
}
|
|
1652
|
+
function filterStderr(stderr) {
|
|
1653
|
+
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();
|
|
1654
|
+
}
|
|
1655
|
+
function runClaudeTest(ctx, prompt, extraFlags = [], timeout = 9e4) {
|
|
1656
|
+
try {
|
|
1657
|
+
const isDirectAnthropic = ctx.proxyUrl.includes("api.anthropic.com");
|
|
1658
|
+
const envOverrides = isDirectAnthropic ? {} : { ANTHROPIC_BASE_URL: ctx.proxyUrl, ANTHROPIC_API_KEY: ctx.apiKey };
|
|
1659
|
+
const result2 = spawnSync("claude", [
|
|
1660
|
+
"--print",
|
|
1661
|
+
"--model",
|
|
1662
|
+
ctx.model,
|
|
1663
|
+
"--dangerously-skip-permissions",
|
|
1664
|
+
...extraFlags,
|
|
1665
|
+
"-p",
|
|
1666
|
+
prompt
|
|
1667
|
+
], {
|
|
1668
|
+
env: { ...process.env, ...envOverrides },
|
|
1669
|
+
timeout,
|
|
1670
|
+
encoding: "utf-8",
|
|
1671
|
+
cwd: ctx.projectDir
|
|
1672
|
+
});
|
|
1673
|
+
return {
|
|
1674
|
+
stdout: result2.stdout ?? "",
|
|
1675
|
+
stderr: filterStderr(result2.stderr ?? ""),
|
|
1676
|
+
exitCode: result2.status ?? 1
|
|
1677
|
+
};
|
|
1678
|
+
} catch (err) {
|
|
1679
|
+
return { stdout: "", stderr: String(err), exitCode: 1 };
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
function isGitClean(dir) {
|
|
1683
|
+
try {
|
|
1684
|
+
const out = execSync2("git diff --name-only", { cwd: dir, encoding: "utf-8", timeout: 5e3 });
|
|
1685
|
+
return out.trim().length === 0;
|
|
1686
|
+
} catch {
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
function revertChanges(dir) {
|
|
1691
|
+
try {
|
|
1692
|
+
execSync2("git checkout -- src/logger.ts", { cwd: dir, timeout: 5e3, stdio: "pipe" });
|
|
1693
|
+
} catch {
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
function result(name, category, status, accuracy, durationMs, detail, metrics) {
|
|
1697
|
+
return { name, category, status, accuracy, durationMs, detail, metrics };
|
|
1698
|
+
}
|
|
1699
|
+
function crc32(buf) {
|
|
1700
|
+
let c = 4294967295;
|
|
1701
|
+
for (const b of buf) c = CRC_TABLE[(c ^ b) & 255] ^ c >>> 8;
|
|
1702
|
+
return (c ^ 4294967295) >>> 0;
|
|
1703
|
+
}
|
|
1704
|
+
function createRedPng() {
|
|
1705
|
+
const w = 4, h = 4;
|
|
1706
|
+
const scanlines = Buffer.alloc(h * (1 + w * 3));
|
|
1707
|
+
for (let y = 0; y < h; y++) {
|
|
1708
|
+
const off = y * (1 + w * 3);
|
|
1709
|
+
scanlines[off] = 0;
|
|
1710
|
+
for (let x = 0; x < w; x++) {
|
|
1711
|
+
scanlines[off + 1 + x * 3] = 255;
|
|
1712
|
+
scanlines[off + 1 + x * 3 + 1] = 0;
|
|
1713
|
+
scanlines[off + 1 + x * 3 + 2] = 0;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
function chunk(type, data) {
|
|
1717
|
+
const tb = Buffer.from(type, "ascii");
|
|
1718
|
+
const merged = Buffer.concat([tb, data]);
|
|
1719
|
+
const len = Buffer.alloc(4);
|
|
1720
|
+
len.writeUInt32BE(data.length);
|
|
1721
|
+
const crcBuf = Buffer.alloc(4);
|
|
1722
|
+
crcBuf.writeUInt32BE(crc32(merged));
|
|
1723
|
+
return Buffer.concat([len, tb, data, crcBuf]);
|
|
1724
|
+
}
|
|
1725
|
+
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
1726
|
+
const ihdr = Buffer.alloc(13);
|
|
1727
|
+
ihdr.writeUInt32BE(w, 0);
|
|
1728
|
+
ihdr.writeUInt32BE(h, 4);
|
|
1729
|
+
ihdr[8] = 8;
|
|
1730
|
+
ihdr[9] = 2;
|
|
1731
|
+
return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(scanlines)), chunk("IEND", Buffer.alloc(0))]);
|
|
1732
|
+
}
|
|
1733
|
+
async function testSimplePrompt(ctx) {
|
|
1734
|
+
const t = Date.now();
|
|
1735
|
+
if (!canRunApiTests(ctx)) {
|
|
1736
|
+
const r = runClaudeTest(ctx, "Reply with exactly: KODY_TEST_OK");
|
|
1737
|
+
const ok2 = r.stdout.includes("KODY_TEST_OK");
|
|
1738
|
+
return result(
|
|
1739
|
+
"simple_prompt",
|
|
1740
|
+
"basic",
|
|
1741
|
+
ok2 ? "pass" : "fail",
|
|
1742
|
+
ok2 ? 100 : 0,
|
|
1743
|
+
Date.now() - t,
|
|
1744
|
+
ok2 ? "Model responded correctly (via CLI)" : `Got: ${r.stdout.slice(0, 80)}`
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
const res = await apiCall(ctx, {
|
|
1748
|
+
max_tokens: 50,
|
|
1749
|
+
temperature: 0,
|
|
1750
|
+
messages: [{ role: "user", content: "Reply with exactly: KODY_TEST_OK" }]
|
|
1751
|
+
});
|
|
1752
|
+
if (!res.ok) return result("simple_prompt", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
|
|
1753
|
+
const text = extractText(res.data);
|
|
1754
|
+
const ok = text.includes("KODY_TEST_OK");
|
|
1755
|
+
return result(
|
|
1756
|
+
"simple_prompt",
|
|
1757
|
+
"basic",
|
|
1758
|
+
ok ? "pass" : "fail",
|
|
1759
|
+
ok ? 100 : 0,
|
|
1760
|
+
Date.now() - t,
|
|
1761
|
+
ok ? "Model responded correctly" : `Expected KODY_TEST_OK, got: ${text.slice(0, 80)}`
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
async function testJsonOutput(ctx) {
|
|
1765
|
+
if (!canRunApiTests(ctx)) {
|
|
1766
|
+
const t2 = Date.now();
|
|
1767
|
+
const r = runClaudeTest(ctx, 'Respond with ONLY valid JSON, no markdown fences. Return: {"status":"ok","model":"your name"}');
|
|
1768
|
+
let text2 = r.stdout.trim().replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
1769
|
+
try {
|
|
1770
|
+
JSON.parse(text2);
|
|
1771
|
+
return result("json_output", "basic", "pass", 100, Date.now() - t2, "Valid JSON via CLI");
|
|
1772
|
+
} catch {
|
|
1773
|
+
return result("json_output", "basic", "fail", 0, Date.now() - t2, `Invalid JSON: ${text2.slice(0, 80)}`);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
const t = Date.now();
|
|
1777
|
+
const res = await apiCall(ctx, {
|
|
1778
|
+
max_tokens: 200,
|
|
1779
|
+
temperature: 0,
|
|
1780
|
+
system: "Respond with ONLY valid JSON. No markdown fences, no explanation. Just raw JSON.",
|
|
1781
|
+
messages: [{ role: "user", content: 'Return a JSON object with keys "status" (string "ok") and "model" (string, your model name).' }]
|
|
1782
|
+
});
|
|
1783
|
+
if (!res.ok) return result("json_output", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
|
|
1784
|
+
let text = extractText(res.data).trim();
|
|
1785
|
+
text = text.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
1786
|
+
try {
|
|
1787
|
+
const parsed = JSON.parse(text);
|
|
1788
|
+
const hasKeys = typeof parsed.status === "string" && typeof parsed.model === "string";
|
|
1789
|
+
return result(
|
|
1790
|
+
"json_output",
|
|
1791
|
+
"basic",
|
|
1792
|
+
"pass",
|
|
1793
|
+
hasKeys ? 100 : 70,
|
|
1794
|
+
Date.now() - t,
|
|
1795
|
+
hasKeys ? "Valid JSON with correct keys" : "Valid JSON but missing expected keys"
|
|
1796
|
+
);
|
|
1797
|
+
} catch {
|
|
1798
|
+
return result("json_output", "basic", "fail", 0, Date.now() - t, `Invalid JSON: ${text.slice(0, 80)}`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function scoreRules(text) {
|
|
1802
|
+
let score = 0;
|
|
1803
|
+
const checks = [];
|
|
1804
|
+
if (text.startsWith("KODY:") || text.startsWith("kody:")) {
|
|
1805
|
+
score += 20;
|
|
1806
|
+
checks.push("starts-with-kody");
|
|
1807
|
+
}
|
|
1808
|
+
if (!text.toLowerCase().split(/\s+/).includes("the")) {
|
|
1809
|
+
score += 20;
|
|
1810
|
+
checks.push("no-the");
|
|
1811
|
+
}
|
|
1812
|
+
if (text.split(/\s+/).length <= 55) {
|
|
1813
|
+
score += 20;
|
|
1814
|
+
checks.push("under-50-words");
|
|
1815
|
+
}
|
|
1816
|
+
if (text.endsWith("END") || text.endsWith("end")) {
|
|
1817
|
+
score += 20;
|
|
1818
|
+
checks.push("ends-with-end");
|
|
1819
|
+
}
|
|
1820
|
+
if (text === text.toLowerCase()) {
|
|
1821
|
+
score += 20;
|
|
1822
|
+
checks.push("all-lowercase");
|
|
1823
|
+
}
|
|
1824
|
+
return { score, checks };
|
|
1825
|
+
}
|
|
1826
|
+
async function testSystemPromptRules(ctx) {
|
|
1827
|
+
const rulesPrompt = [
|
|
1828
|
+
"STRICT RULES \u2014 violating ANY will crash the system:",
|
|
1829
|
+
"1) Start every response with 'KODY:'",
|
|
1830
|
+
"2) Never use the word 'the'",
|
|
1831
|
+
"3) Keep response under 50 words",
|
|
1832
|
+
"4) End your response with 'END'",
|
|
1833
|
+
"5) Use ONLY lowercase letters (no uppercase anywhere)"
|
|
1834
|
+
].join("\n");
|
|
1835
|
+
if (!canRunApiTests(ctx)) {
|
|
1836
|
+
const t2 = Date.now();
|
|
1837
|
+
const r = runClaudeTest(ctx, [
|
|
1838
|
+
"Follow ALL these rules in your response:",
|
|
1839
|
+
"1) Your response must start with the word 'KODY:'",
|
|
1840
|
+
"2) Do not use the word 'the' anywhere",
|
|
1841
|
+
"3) Keep your response under 50 words total",
|
|
1842
|
+
"4) End your response with the word 'END'",
|
|
1843
|
+
"5) Use only lowercase letters throughout",
|
|
1844
|
+
"",
|
|
1845
|
+
"Now describe what a compiler does. Remember: follow ALL 5 rules above exactly."
|
|
1846
|
+
].join("\n"));
|
|
1847
|
+
const { score: score2, checks: checks2 } = scoreRules(r.stdout.trim());
|
|
1848
|
+
const status2 = score2 >= 80 ? "pass" : score2 >= 40 ? "warn" : "fail";
|
|
1849
|
+
return result(
|
|
1850
|
+
"system_prompt_rules",
|
|
1851
|
+
"basic",
|
|
1852
|
+
status2,
|
|
1853
|
+
score2,
|
|
1854
|
+
Date.now() - t2,
|
|
1855
|
+
`${score2 / 20}/5 rules followed: ${checks2.join(", ")}`,
|
|
1856
|
+
{ instructionCompliance: score2 }
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
const t = Date.now();
|
|
1860
|
+
const res = await apiCall(ctx, {
|
|
1861
|
+
max_tokens: 200,
|
|
1862
|
+
temperature: 0,
|
|
1863
|
+
system: rulesPrompt,
|
|
1864
|
+
messages: [{ role: "user", content: "Describe what a compiler does." }]
|
|
1865
|
+
});
|
|
1866
|
+
if (!res.ok) return result("system_prompt_rules", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
|
|
1867
|
+
const text = extractText(res.data).trim();
|
|
1868
|
+
const { score, checks } = scoreRules(text);
|
|
1869
|
+
const status = score >= 80 ? "pass" : score >= 40 ? "warn" : "fail";
|
|
1870
|
+
return result(
|
|
1871
|
+
"system_prompt_rules",
|
|
1872
|
+
"basic",
|
|
1873
|
+
status,
|
|
1874
|
+
score,
|
|
1875
|
+
Date.now() - t,
|
|
1876
|
+
`${score / 20}/5 rules followed: ${checks.join(", ")}`,
|
|
1877
|
+
{ instructionCompliance: score }
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
async function testExtendedThinking(ctx) {
|
|
1881
|
+
if (!canRunApiTests(ctx)) {
|
|
1882
|
+
const t2 = Date.now();
|
|
1883
|
+
const r = runClaudeTest(ctx, "What is 15 * 23? Reply with just the number.");
|
|
1884
|
+
const ok = r.stdout.includes("345");
|
|
1885
|
+
return result(
|
|
1886
|
+
"extended_thinking",
|
|
1887
|
+
"infrastructure",
|
|
1888
|
+
ok ? "pass" : "warn",
|
|
1889
|
+
ok ? 100 : 50,
|
|
1890
|
+
Date.now() - t2,
|
|
1891
|
+
ok ? "Model responded correctly (thinking assumed via CLI)" : `Got: ${r.stdout.slice(0, 80)}`
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
const t = Date.now();
|
|
1895
|
+
const res = await apiCall(ctx, {
|
|
1896
|
+
max_tokens: 200,
|
|
1897
|
+
thinking: { type: "enabled", budget_tokens: 2e3 },
|
|
1898
|
+
messages: [{ role: "user", content: "What is 15 * 23?" }]
|
|
1899
|
+
});
|
|
1900
|
+
if (!res.ok) return result(
|
|
1901
|
+
"extended_thinking",
|
|
1902
|
+
"infrastructure",
|
|
1903
|
+
"warn",
|
|
1904
|
+
50,
|
|
1905
|
+
Date.now() - t,
|
|
1906
|
+
`Request failed (model may not support thinking): ${res.errorMsg?.slice(0, 80)}`
|
|
1907
|
+
);
|
|
1908
|
+
const hasThinking = Array.isArray(res.data.content) && res.data.content.some((b) => b.type === "thinking");
|
|
1909
|
+
const hasText = extractText(res.data).length > 0;
|
|
1910
|
+
if (hasThinking) return result("extended_thinking", "infrastructure", "pass", 100, Date.now() - t, "Thinking block present in response");
|
|
1911
|
+
if (hasText) return result("extended_thinking", "infrastructure", "warn", 70, Date.now() - t, "Response OK but no thinking block");
|
|
1912
|
+
return result("extended_thinking", "infrastructure", "fail", 0, Date.now() - t, "No content in response");
|
|
1913
|
+
}
|
|
1914
|
+
async function testToolRead(ctx) {
|
|
1915
|
+
if (!canRunApiTests(ctx)) {
|
|
1916
|
+
const t2 = Date.now();
|
|
1917
|
+
const testFile2 = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
|
|
1918
|
+
fs12.writeFileSync(testFile2, "KODY_SECRET_CONTENT_42");
|
|
1919
|
+
try {
|
|
1920
|
+
const r = runClaudeTest(ctx, `Read the file ${testFile2} and tell me its exact contents. Reply with ONLY the file contents.`);
|
|
1921
|
+
const ok = r.stdout.includes("KODY_SECRET_CONTENT_42");
|
|
1922
|
+
return result(
|
|
1923
|
+
"tool_read",
|
|
1924
|
+
"tool-use",
|
|
1925
|
+
ok ? "pass" : "fail",
|
|
1926
|
+
ok ? 100 : 0,
|
|
1927
|
+
Date.now() - t2,
|
|
1928
|
+
ok ? "Read tool works via CLI" : `Got: ${r.stdout.slice(0, 80)}`,
|
|
1929
|
+
{ toolSelection: ok ? 100 : 0 }
|
|
1930
|
+
);
|
|
1931
|
+
} finally {
|
|
1932
|
+
fs12.rmSync(testFile2, { force: true });
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
const t = Date.now();
|
|
1936
|
+
const testFile = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
|
|
1937
|
+
fs12.writeFileSync(testFile, "KODY_SECRET_CONTENT_42");
|
|
1938
|
+
try {
|
|
1939
|
+
const conv = await runToolConversation(
|
|
1940
|
+
ctx,
|
|
1941
|
+
[TOOL_READ],
|
|
1942
|
+
`Read the file ${testFile} and tell me what it contains.`,
|
|
1943
|
+
(name, input) => {
|
|
1944
|
+
if (name === "Read" && input.path === testFile) return "KODY_SECRET_CONTENT_42";
|
|
1945
|
+
return "Error: File not found";
|
|
1946
|
+
}
|
|
1947
|
+
);
|
|
1948
|
+
if (conv.error) return result("tool_read", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
1949
|
+
const calledRead = conv.toolCalls.some((tc) => tc.name === "Read");
|
|
1950
|
+
const correctPath = conv.toolCalls.some((tc) => tc.name === "Read" && tc.input.path === testFile);
|
|
1951
|
+
const mentionsContent = conv.finalText.includes("KODY_SECRET_CONTENT_42") || conv.finalText.includes("42");
|
|
1952
|
+
let acc = 0;
|
|
1953
|
+
if (calledRead) acc += 30;
|
|
1954
|
+
if (correctPath) acc += 30;
|
|
1955
|
+
if (mentionsContent) acc += 40;
|
|
1956
|
+
return result(
|
|
1957
|
+
"tool_read",
|
|
1958
|
+
"tool-use",
|
|
1959
|
+
acc >= 60 ? "pass" : "fail",
|
|
1960
|
+
acc,
|
|
1961
|
+
Date.now() - t,
|
|
1962
|
+
`Read called: ${calledRead}, correct path: ${correctPath}, content referenced: ${mentionsContent}`,
|
|
1963
|
+
{ toolSelection: calledRead ? 100 : 0 }
|
|
1964
|
+
);
|
|
1965
|
+
} finally {
|
|
1966
|
+
fs12.rmSync(testFile, { force: true });
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
async function testToolEdit(ctx) {
|
|
1970
|
+
if (!canRunApiTests(ctx)) {
|
|
1971
|
+
const t2 = Date.now();
|
|
1972
|
+
const testFile = path11.join(os2.tmpdir(), "kody-test-model-edit.txt");
|
|
1973
|
+
fs12.writeFileSync(testFile, "hello world");
|
|
1974
|
+
try {
|
|
1975
|
+
const r = runClaudeTest(ctx, `Use the Edit tool to replace "hello" with "goodbye" in ${testFile}. Do nothing else.`);
|
|
1976
|
+
const content = fs12.existsSync(testFile) ? fs12.readFileSync(testFile, "utf-8") : "";
|
|
1977
|
+
const ok = content.includes("goodbye");
|
|
1978
|
+
return result(
|
|
1979
|
+
"tool_edit",
|
|
1980
|
+
"tool-use",
|
|
1981
|
+
ok ? "pass" : "fail",
|
|
1982
|
+
ok ? 100 : 0,
|
|
1983
|
+
Date.now() - t2,
|
|
1984
|
+
ok ? "Edit tool works via CLI" : `File content: ${content.slice(0, 80)}`,
|
|
1985
|
+
{ toolSelection: ok ? 100 : 0 }
|
|
1986
|
+
);
|
|
1987
|
+
} finally {
|
|
1988
|
+
fs12.rmSync(testFile, { force: true });
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
const t = Date.now();
|
|
1992
|
+
const conv = await runToolConversation(
|
|
1993
|
+
ctx,
|
|
1994
|
+
[TOOL_READ, TOOL_EDIT],
|
|
1995
|
+
'Read the file /tmp/kody-edit-test.txt, then use Edit to replace "hello" with "goodbye" in it.',
|
|
1996
|
+
(name, input) => {
|
|
1997
|
+
if (name === "Read") return "hello world";
|
|
1998
|
+
if (name === "Edit") return "File edited successfully";
|
|
1999
|
+
return "Unknown tool";
|
|
2000
|
+
}
|
|
2001
|
+
);
|
|
2002
|
+
if (conv.error) return result("tool_edit", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
2003
|
+
const editCall = conv.toolCalls.find((tc) => tc.name === "Edit");
|
|
2004
|
+
let acc = 0;
|
|
2005
|
+
if (editCall) {
|
|
2006
|
+
acc += 40;
|
|
2007
|
+
if (editCall.input.old_string === "hello") acc += 30;
|
|
2008
|
+
if (editCall.input.new_string === "goodbye") acc += 30;
|
|
2009
|
+
}
|
|
2010
|
+
return result(
|
|
2011
|
+
"tool_edit",
|
|
2012
|
+
"tool-use",
|
|
2013
|
+
acc >= 70 ? "pass" : acc > 0 ? "warn" : "fail",
|
|
2014
|
+
acc,
|
|
2015
|
+
Date.now() - t,
|
|
2016
|
+
editCall ? `Edit called with old="${editCall.input.old_string}" new="${editCall.input.new_string}"` : "Edit tool was not called",
|
|
2017
|
+
{ toolSelection: editCall ? 100 : 0 }
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
async function testToolBash(ctx) {
|
|
2021
|
+
if (!canRunApiTests(ctx)) {
|
|
2022
|
+
const t2 = Date.now();
|
|
2023
|
+
const r = runClaudeTest(ctx, "Run this bash command and tell me its output: echo KODY_BASH_OK");
|
|
2024
|
+
const ok = r.stdout.includes("KODY_BASH_OK");
|
|
2025
|
+
return result(
|
|
2026
|
+
"tool_bash",
|
|
2027
|
+
"tool-use",
|
|
2028
|
+
ok ? "pass" : "fail",
|
|
2029
|
+
ok ? 100 : 0,
|
|
2030
|
+
Date.now() - t2,
|
|
2031
|
+
ok ? "Bash tool works via CLI" : `Got: ${r.stdout.slice(0, 80)}`,
|
|
2032
|
+
{ toolSelection: ok ? 100 : 0 }
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
const t = Date.now();
|
|
2036
|
+
const conv = await runToolConversation(
|
|
2037
|
+
ctx,
|
|
2038
|
+
[TOOL_BASH],
|
|
2039
|
+
"Run this exact bash command: echo KODY_BASH_OK",
|
|
2040
|
+
(name, input) => {
|
|
2041
|
+
if (name === "Bash") return "KODY_BASH_OK\n";
|
|
2042
|
+
return "Error";
|
|
2043
|
+
}
|
|
2044
|
+
);
|
|
2045
|
+
if (conv.error) return result("tool_bash", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
2046
|
+
const bashCall = conv.toolCalls.find((tc) => tc.name === "Bash");
|
|
2047
|
+
const correctCmd = bashCall && String(bashCall.input.command).includes("echo KODY_BASH_OK");
|
|
2048
|
+
const acc = bashCall ? correctCmd ? 100 : 50 : 0;
|
|
2049
|
+
return result(
|
|
2050
|
+
"tool_bash",
|
|
2051
|
+
"tool-use",
|
|
2052
|
+
acc >= 50 ? "pass" : "fail",
|
|
2053
|
+
acc,
|
|
2054
|
+
Date.now() - t,
|
|
2055
|
+
bashCall ? `Bash called: ${bashCall.input.command}` : "Bash tool was not called",
|
|
2056
|
+
{ toolSelection: bashCall ? 100 : 0 }
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
async function testImageAttachment(ctx) {
|
|
2060
|
+
if (!canRunApiTests(ctx)) {
|
|
2061
|
+
const t2 = Date.now();
|
|
2062
|
+
const tmpPng = path11.join(os2.tmpdir(), "kody-test-image.png");
|
|
2063
|
+
fs12.writeFileSync(tmpPng, createRedPng());
|
|
2064
|
+
try {
|
|
2065
|
+
const r = runClaudeTest(ctx, `Read the image file at ${tmpPng} and tell me what color it is. Reply with just the color name.`);
|
|
2066
|
+
const text2 = r.stdout.toLowerCase();
|
|
2067
|
+
const ok = text2.includes("red");
|
|
2068
|
+
return result(
|
|
2069
|
+
"image_attachment",
|
|
2070
|
+
"tool-use",
|
|
2071
|
+
ok ? "pass" : "warn",
|
|
2072
|
+
ok ? 100 : 50,
|
|
2073
|
+
Date.now() - t2,
|
|
2074
|
+
ok ? "Image processed correctly via CLI" : `Got: ${text2.slice(0, 80)}`
|
|
2075
|
+
);
|
|
2076
|
+
} finally {
|
|
2077
|
+
fs12.rmSync(tmpPng, { force: true });
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
const t = Date.now();
|
|
2081
|
+
const pngData = createRedPng().toString("base64");
|
|
2082
|
+
const res = await apiCall(ctx, {
|
|
2083
|
+
max_tokens: 100,
|
|
2084
|
+
temperature: 0,
|
|
2085
|
+
messages: [{
|
|
2086
|
+
role: "user",
|
|
2087
|
+
content: [
|
|
2088
|
+
{ type: "image", source: { type: "base64", media_type: "image/png", data: pngData } },
|
|
2089
|
+
{ type: "text", text: "What color is this image? Reply with just the color name." }
|
|
2090
|
+
]
|
|
2091
|
+
}]
|
|
2092
|
+
});
|
|
2093
|
+
if (!res.ok) return result(
|
|
2094
|
+
"image_attachment",
|
|
2095
|
+
"tool-use",
|
|
2096
|
+
"fail",
|
|
2097
|
+
0,
|
|
2098
|
+
Date.now() - t,
|
|
2099
|
+
`API error (model may not support vision): ${res.errorMsg?.slice(0, 80)}`
|
|
2100
|
+
);
|
|
2101
|
+
const text = extractText(res.data).toLowerCase();
|
|
2102
|
+
const mentionsRed = text.includes("red");
|
|
2103
|
+
const mentionsColor = mentionsRed || text.includes("color") || text.includes("image") || text.includes("pixel");
|
|
2104
|
+
const acc = mentionsRed ? 100 : mentionsColor ? 50 : 20;
|
|
2105
|
+
return result(
|
|
2106
|
+
"image_attachment",
|
|
2107
|
+
"tool-use",
|
|
2108
|
+
mentionsRed ? "pass" : mentionsColor ? "warn" : "fail",
|
|
2109
|
+
acc,
|
|
2110
|
+
Date.now() - t,
|
|
2111
|
+
`Response: ${text.slice(0, 80)}`
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
async function testErrorRecovery(ctx) {
|
|
2115
|
+
if (!canRunApiTests(ctx)) {
|
|
2116
|
+
const t2 = Date.now();
|
|
2117
|
+
const r = runClaudeTest(ctx, "Read the file /tmp/kody-nonexistent-test-file-xyz.txt and tell me what's in it. If it doesn't exist, say 'FILE_NOT_FOUND'.");
|
|
2118
|
+
const ok = r.stdout.includes("FILE_NOT_FOUND") || r.stdout.toLowerCase().includes("not found") || r.stdout.toLowerCase().includes("does not exist") || r.stdout.toLowerCase().includes("doesn't exist");
|
|
2119
|
+
return result(
|
|
2120
|
+
"error_recovery",
|
|
2121
|
+
"advanced",
|
|
2122
|
+
ok ? "pass" : "warn",
|
|
2123
|
+
ok ? 100 : 50,
|
|
2124
|
+
Date.now() - t2,
|
|
2125
|
+
ok ? "Graceful error handling via CLI" : `Got: ${r.stdout.slice(0, 80)}`
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
const t = Date.now();
|
|
2129
|
+
let errorGiven = false;
|
|
2130
|
+
const conv = await runToolConversation(
|
|
2131
|
+
ctx,
|
|
2132
|
+
[TOOL_READ, TOOL_BASH],
|
|
2133
|
+
"Read the file /tmp/nonexistent-kody-file.txt and tell me what's in it. If the file doesn't exist, say so.",
|
|
2134
|
+
(name, input) => {
|
|
2135
|
+
if (name === "Read" && !errorGiven) {
|
|
2136
|
+
errorGiven = true;
|
|
2137
|
+
return "Error: ENOENT: no such file or directory";
|
|
2138
|
+
}
|
|
2139
|
+
if (name === "Bash") return "ls: /tmp/nonexistent-kody-file.txt: No such file or directory";
|
|
2140
|
+
return "Error: File not found";
|
|
2141
|
+
}
|
|
2142
|
+
);
|
|
2143
|
+
if (conv.error) return result("error_recovery", "advanced", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
2144
|
+
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");
|
|
2145
|
+
const tried = conv.toolCalls.length >= 1;
|
|
2146
|
+
const acc = reported ? tried ? 100 : 70 : 20;
|
|
2147
|
+
return result(
|
|
2148
|
+
"error_recovery",
|
|
2149
|
+
"advanced",
|
|
2150
|
+
reported ? "pass" : "warn",
|
|
2151
|
+
acc,
|
|
2152
|
+
Date.now() - t,
|
|
2153
|
+
reported ? "Gracefully reported missing file" : `Response: ${conv.finalText.slice(0, 80)}`
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
async function testToolMultiStep(ctx) {
|
|
2157
|
+
const t = Date.now();
|
|
2158
|
+
const r = runClaudeTest(
|
|
2159
|
+
ctx,
|
|
2160
|
+
"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."
|
|
2161
|
+
);
|
|
2162
|
+
if (!r.stdout.trim() && r.exitCode !== 0) return result(
|
|
2163
|
+
"tool_multi_step",
|
|
2164
|
+
"tool-use",
|
|
2165
|
+
"fail",
|
|
2166
|
+
0,
|
|
2167
|
+
Date.now() - t,
|
|
2168
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
2169
|
+
);
|
|
2170
|
+
const out = r.stdout.trim().toLowerCase();
|
|
2171
|
+
const correct = out.includes("main");
|
|
2172
|
+
return result(
|
|
2173
|
+
"tool_multi_step",
|
|
2174
|
+
"tool-use",
|
|
2175
|
+
correct ? "pass" : "fail",
|
|
2176
|
+
correct ? 100 : 20,
|
|
2177
|
+
Date.now() - t,
|
|
2178
|
+
correct ? "Correct: main" : `Got: ${out.slice(0, 80)}`
|
|
2179
|
+
);
|
|
2180
|
+
}
|
|
2181
|
+
async function testPlanStage(ctx) {
|
|
2182
|
+
const t = Date.now();
|
|
2183
|
+
const wasClean = isGitClean(ctx.projectDir);
|
|
2184
|
+
const r = runClaudeTest(ctx, [
|
|
2185
|
+
"You are a planning agent. Your ONLY job is to output a markdown plan.",
|
|
2186
|
+
"CRITICAL: Do NOT use Edit, Write, or Bash tools. Do NOT modify any files. ONLY use Read, Glob, and Grep for research.",
|
|
2187
|
+
"If you modify any files, the system will crash.",
|
|
2188
|
+
"",
|
|
2189
|
+
"Task: Plan adding a /health endpoint to an Express app.",
|
|
2190
|
+
"Output a markdown plan with ## Step N sections. Each step must have File, Change, and Why fields.",
|
|
2191
|
+
"Keep it to 3 steps maximum."
|
|
2192
|
+
].join("\n"), [], 12e4);
|
|
2193
|
+
const filesModified = wasClean && !isGitClean(ctx.projectDir);
|
|
2194
|
+
if (filesModified) revertChanges(ctx.projectDir);
|
|
2195
|
+
if (!r.stdout.trim() && r.exitCode !== 0) return result(
|
|
2196
|
+
"plan_stage",
|
|
2197
|
+
"stage-simulation",
|
|
2198
|
+
"fail",
|
|
2199
|
+
0,
|
|
2200
|
+
Date.now() - t,
|
|
2201
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
2202
|
+
);
|
|
2203
|
+
const out = r.stdout;
|
|
2204
|
+
const hasStepFormat = /##\s*Step/i.test(out);
|
|
2205
|
+
const hasStructure = hasStepFormat || /\*\*File\*\*/i.test(out) && /\*\*Change\*\*/i.test(out);
|
|
2206
|
+
const boundary = filesModified ? 0 : 100;
|
|
2207
|
+
const format = hasStructure ? 100 : hasStepFormat ? 70 : out.length > 50 ? 30 : 0;
|
|
2208
|
+
const acc = Math.round(boundary * 0.6 + format * 0.4);
|
|
2209
|
+
const status = filesModified ? "fail" : hasStructure ? "pass" : "warn";
|
|
2210
|
+
return result(
|
|
2211
|
+
"plan_stage",
|
|
2212
|
+
"stage-simulation",
|
|
2213
|
+
status,
|
|
2214
|
+
acc,
|
|
2215
|
+
Date.now() - t,
|
|
2216
|
+
filesModified ? "FAIL: Model modified files during plan stage (instruction violation)" : hasStructure ? "Plan output with correct structure, no files modified" : "Output lacks expected ## Step structure",
|
|
2217
|
+
{ boundaryRespect: boundary, outputFormat: format, instructionCompliance: boundary }
|
|
2218
|
+
);
|
|
2219
|
+
}
|
|
2220
|
+
async function testBuildStage(ctx) {
|
|
2221
|
+
const t = Date.now();
|
|
2222
|
+
const r = runClaudeTest(ctx, "Add a comment '// kody-build-test' as the very first line of src/logger.ts. That is your only task.");
|
|
2223
|
+
const diff = (() => {
|
|
2224
|
+
try {
|
|
2225
|
+
return execSync2("git diff src/logger.ts", { cwd: ctx.projectDir, encoding: "utf-8", timeout: 5e3 });
|
|
2226
|
+
} catch {
|
|
2227
|
+
return "";
|
|
2228
|
+
}
|
|
2229
|
+
})();
|
|
2230
|
+
const edited = diff.includes("kody-build-test");
|
|
2231
|
+
revertChanges(ctx.projectDir);
|
|
2232
|
+
if (!r.stdout.trim() && r.exitCode !== 0 && !edited) return result(
|
|
2233
|
+
"build_stage",
|
|
2234
|
+
"stage-simulation",
|
|
2235
|
+
"fail",
|
|
2236
|
+
0,
|
|
2237
|
+
Date.now() - t,
|
|
2238
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
2239
|
+
);
|
|
2240
|
+
return result(
|
|
2241
|
+
"build_stage",
|
|
2242
|
+
"stage-simulation",
|
|
2243
|
+
edited ? "pass" : "fail",
|
|
2244
|
+
edited ? 100 : 0,
|
|
2245
|
+
Date.now() - t,
|
|
2246
|
+
edited ? "File correctly modified with expected comment" : "File was not modified as expected"
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
async function testReviewStage(ctx) {
|
|
2250
|
+
const t = Date.now();
|
|
2251
|
+
const wasClean = isGitClean(ctx.projectDir);
|
|
2252
|
+
const r = runClaudeTest(ctx, [
|
|
2253
|
+
"You are a code review agent. Review the file src/logger.ts.",
|
|
2254
|
+
"CRITICAL: Do NOT modify any files. Only READ and analyze.",
|
|
2255
|
+
"Output your review as markdown with this exact format:",
|
|
2256
|
+
"## Summary",
|
|
2257
|
+
"<1-2 sentence summary>",
|
|
2258
|
+
"## Issues Found",
|
|
2259
|
+
"- <issues>",
|
|
2260
|
+
"## Verdict",
|
|
2261
|
+
"APPROVE or REQUEST_CHANGES"
|
|
2262
|
+
].join("\n"));
|
|
2263
|
+
const filesModified = wasClean && !isGitClean(ctx.projectDir);
|
|
2264
|
+
if (filesModified) revertChanges(ctx.projectDir);
|
|
2265
|
+
if (!r.stdout.trim() && r.exitCode !== 0) return result(
|
|
2266
|
+
"review_stage",
|
|
2267
|
+
"stage-simulation",
|
|
2268
|
+
"fail",
|
|
2269
|
+
0,
|
|
2270
|
+
Date.now() - t,
|
|
2271
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
2272
|
+
);
|
|
2273
|
+
const out = r.stdout;
|
|
2274
|
+
const hasVerdict = /verdict/i.test(out);
|
|
2275
|
+
const hasSummary = /summary/i.test(out);
|
|
2276
|
+
const boundary = filesModified ? 0 : 100;
|
|
2277
|
+
const format = (hasVerdict ? 50 : 0) + (hasSummary ? 50 : 0);
|
|
2278
|
+
const acc = Math.round(boundary * 0.5 + format * 0.5);
|
|
2279
|
+
const status = filesModified ? "fail" : hasVerdict && hasSummary ? "pass" : "warn";
|
|
2280
|
+
return result(
|
|
2281
|
+
"review_stage",
|
|
2282
|
+
"stage-simulation",
|
|
2283
|
+
status,
|
|
2284
|
+
acc,
|
|
2285
|
+
Date.now() - t,
|
|
2286
|
+
filesModified ? "FAIL: Model modified files during review (instruction violation)" : `Summary: ${hasSummary}, Verdict: ${hasVerdict}, no files modified`,
|
|
2287
|
+
{ boundaryRespect: boundary, outputFormat: format }
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
async function testMcpTools(ctx) {
|
|
2291
|
+
const t = Date.now();
|
|
2292
|
+
const mcpConfig = path11.join(os2.tmpdir(), `kody-test-mcp-${Date.now()}.json`);
|
|
2293
|
+
const testFile = path11.join(ctx.projectDir, "kody-mcp-compat-test.txt");
|
|
2294
|
+
try {
|
|
2295
|
+
fs12.writeFileSync(mcpConfig, JSON.stringify({
|
|
2296
|
+
mcpServers: {
|
|
2297
|
+
filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", ctx.projectDir] }
|
|
2298
|
+
}
|
|
2299
|
+
}));
|
|
2300
|
+
const r = runClaudeTest(
|
|
2301
|
+
ctx,
|
|
2302
|
+
`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.`,
|
|
2303
|
+
["--mcp-config", mcpConfig],
|
|
2304
|
+
12e4
|
|
2305
|
+
);
|
|
2306
|
+
const created = fs12.existsSync(testFile);
|
|
2307
|
+
const content = created ? fs12.readFileSync(testFile, "utf-8").trim() : "";
|
|
2308
|
+
const correct = content.includes("mcp-ok");
|
|
2309
|
+
return result(
|
|
2310
|
+
"mcp_tools",
|
|
2311
|
+
"advanced",
|
|
2312
|
+
created ? "pass" : "fail",
|
|
2313
|
+
correct ? 100 : created ? 70 : 0,
|
|
2314
|
+
Date.now() - t,
|
|
2315
|
+
created ? `File created, content: ${content.slice(0, 50)}` : `MCP test failed: ${r.stderr.slice(0, 80)}`
|
|
2316
|
+
);
|
|
2317
|
+
} catch (err) {
|
|
2318
|
+
return result("mcp_tools", "advanced", "warn", 0, Date.now() - t, `MCP test error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2319
|
+
} finally {
|
|
2320
|
+
fs12.rmSync(mcpConfig, { force: true });
|
|
2321
|
+
fs12.rmSync(testFile, { force: true });
|
|
2322
|
+
revertChanges(ctx.projectDir);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
var TOOL_READ, TOOL_EDIT, TOOL_BASH, CRC_TABLE, ALL_TESTS;
|
|
2326
|
+
var init_test_model_tests = __esm({
|
|
2327
|
+
"src/cli/test-model-tests.ts"() {
|
|
2328
|
+
"use strict";
|
|
2329
|
+
TOOL_READ = {
|
|
2330
|
+
name: "Read",
|
|
2331
|
+
description: "Read a file from the filesystem",
|
|
2332
|
+
input_schema: {
|
|
2333
|
+
type: "object",
|
|
2334
|
+
properties: { path: { type: "string", description: "Absolute file path" } },
|
|
2335
|
+
required: ["path"]
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
TOOL_EDIT = {
|
|
2339
|
+
name: "Edit",
|
|
2340
|
+
description: "Replace old_string with new_string in a file",
|
|
2341
|
+
input_schema: {
|
|
2342
|
+
type: "object",
|
|
2343
|
+
properties: {
|
|
2344
|
+
file_path: { type: "string" },
|
|
2345
|
+
old_string: { type: "string" },
|
|
2346
|
+
new_string: { type: "string" }
|
|
2347
|
+
},
|
|
2348
|
+
required: ["file_path", "old_string", "new_string"]
|
|
2349
|
+
}
|
|
2350
|
+
};
|
|
2351
|
+
TOOL_BASH = {
|
|
2352
|
+
name: "Bash",
|
|
2353
|
+
description: "Execute a bash command and return output",
|
|
2354
|
+
input_schema: {
|
|
2355
|
+
type: "object",
|
|
2356
|
+
properties: { command: { type: "string", description: "The command to run" } },
|
|
2357
|
+
required: ["command"]
|
|
2358
|
+
}
|
|
2359
|
+
};
|
|
2360
|
+
CRC_TABLE = new Uint32Array(256);
|
|
2361
|
+
for (let n = 0; n < 256; n++) {
|
|
2362
|
+
let c = n;
|
|
2363
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
2364
|
+
CRC_TABLE[n] = c >>> 0;
|
|
2365
|
+
}
|
|
2366
|
+
ALL_TESTS = [
|
|
2367
|
+
// Infrastructure
|
|
2368
|
+
{ name: "extended_thinking", category: "infrastructure", description: "Extended thinking parameter support", run: testExtendedThinking },
|
|
2369
|
+
// Basic
|
|
2370
|
+
{ name: "simple_prompt", category: "basic", description: "Basic text prompt and response", run: testSimplePrompt },
|
|
2371
|
+
{ name: "json_output", category: "basic", description: "JSON-only output constraint", run: testJsonOutput },
|
|
2372
|
+
{ name: "system_prompt_rules", category: "basic", description: "Multi-rule system prompt adherence", run: testSystemPromptRules },
|
|
2373
|
+
// Tool use
|
|
2374
|
+
{ name: "tool_read", category: "tool-use", description: "Read tool: file reading", run: testToolRead },
|
|
2375
|
+
{ name: "tool_edit", category: "tool-use", description: "Edit tool: old/new string replacement", run: testToolEdit },
|
|
2376
|
+
{ name: "tool_bash", category: "tool-use", description: "Bash tool: command execution", run: testToolBash },
|
|
2377
|
+
{ name: "tool_multi_step", category: "tool-use", description: "Multi-step tool chain via CLI", run: testToolMultiStep },
|
|
2378
|
+
{ name: "image_attachment", category: "tool-use", description: "Vision: image content processing", run: testImageAttachment },
|
|
2379
|
+
// Stage simulation
|
|
2380
|
+
{ name: "plan_stage", category: "stage-simulation", description: "Plan stage: read-only research + structured output", run: testPlanStage },
|
|
2381
|
+
{ name: "build_stage", category: "stage-simulation", description: "Build stage: code editing", run: testBuildStage },
|
|
2382
|
+
{ name: "review_stage", category: "stage-simulation", description: "Review stage: read-only + structured verdict", run: testReviewStage },
|
|
2383
|
+
// Advanced
|
|
2384
|
+
{ name: "mcp_tools", category: "advanced", description: "MCP server tool integration", run: testMcpTools },
|
|
2385
|
+
{ name: "error_recovery", category: "advanced", description: "Graceful error handling on tool failure", run: testErrorRecovery }
|
|
2386
|
+
];
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
// src/cli/test-model-report.ts
|
|
2391
|
+
function pad(str, len) {
|
|
2392
|
+
return str.padEnd(len);
|
|
2393
|
+
}
|
|
2394
|
+
function fmtDuration(ms) {
|
|
2395
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
2396
|
+
}
|
|
2397
|
+
function formatReport(report) {
|
|
2398
|
+
const W = 74;
|
|
2399
|
+
const lines = [];
|
|
2400
|
+
lines.push("=".repeat(W));
|
|
2401
|
+
lines.push("");
|
|
2402
|
+
lines.push(" Model Compatibility Report");
|
|
2403
|
+
lines.push(` Provider: ${report.provider} | Model: ${report.model}`);
|
|
2404
|
+
lines.push(` Date: ${report.timestamp}`);
|
|
2405
|
+
lines.push(` Duration: ${fmtDuration(report.totalDurationMs)}`);
|
|
2406
|
+
lines.push("");
|
|
2407
|
+
lines.push("-".repeat(W));
|
|
2408
|
+
for (const cat of CATEGORY_ORDER) {
|
|
2409
|
+
const catResults = report.results.filter((r) => r.category === cat);
|
|
2410
|
+
if (catResults.length === 0) continue;
|
|
2411
|
+
lines.push("");
|
|
2412
|
+
lines.push(` ${CATEGORY_LABELS[cat]}`);
|
|
2413
|
+
lines.push("");
|
|
2414
|
+
for (const r of catResults) {
|
|
2415
|
+
const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
|
|
2416
|
+
const name = pad(r.name, 28);
|
|
2417
|
+
const status = pad(r.status.toUpperCase(), 6);
|
|
2418
|
+
const acc = pad(`${r.accuracy}%`, 5);
|
|
2419
|
+
const dur = fmtDuration(r.durationMs);
|
|
2420
|
+
lines.push(` [${icon}] ${name} ${status} ${acc} ${dur}`);
|
|
2421
|
+
if (r.status !== "pass" && r.detail) {
|
|
2422
|
+
lines.push(` ${r.detail.slice(0, W - 8)}`);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
const passed = report.results.filter((r) => r.status === "pass").length;
|
|
2427
|
+
const failed = report.results.filter((r) => r.status === "fail").length;
|
|
2428
|
+
const skipped = report.results.filter((r) => r.status === "warn" && r.durationMs === 0 && r.detail.includes("Skipped")).length;
|
|
2429
|
+
const warned = report.results.filter((r) => r.status === "warn").length - skipped;
|
|
2430
|
+
const total = report.results.length;
|
|
2431
|
+
const scored = report.results.filter((r) => !(r.status === "warn" && r.durationMs === 0 && r.detail.includes("Skipped")));
|
|
2432
|
+
const avgAccuracy = scored.length > 0 ? Math.round(scored.reduce((s, r) => s + r.accuracy, 0) / scored.length) : 0;
|
|
2433
|
+
lines.push("");
|
|
2434
|
+
lines.push("-".repeat(W));
|
|
2435
|
+
lines.push("");
|
|
2436
|
+
lines.push(` RESULTS: ${passed}/${total - skipped} PASS | ${failed} FAIL | ${warned} WARN${skipped > 0 ? ` | ${skipped} SKIPPED` : ""}`);
|
|
2437
|
+
lines.push(` OVERALL ACCURACY: ${avgAccuracy}%`);
|
|
2438
|
+
lines.push(` drop_params required: ${report.dropParamsRequired ? "YES" : "NO"}`);
|
|
2439
|
+
lines.push("");
|
|
2440
|
+
lines.push(" ACCURACY BY CATEGORY:");
|
|
2441
|
+
for (const cat of CATEGORY_ORDER) {
|
|
2442
|
+
const cr = report.results.filter((r) => r.category === cat && !(r.status === "warn" && r.durationMs === 0 && r.detail.includes("Skipped")));
|
|
2443
|
+
if (cr.length === 0) continue;
|
|
2444
|
+
const avg = Math.round(cr.reduce((s, r) => s + r.accuracy, 0) / cr.length);
|
|
2445
|
+
lines.push(` ${pad(CATEGORY_LABELS[cat], 22)} ${avg}%`);
|
|
2446
|
+
}
|
|
2447
|
+
lines.push("");
|
|
2448
|
+
lines.push(" RECOMMENDATION:");
|
|
2449
|
+
for (const line of getRecommendation(report)) {
|
|
2450
|
+
lines.push(` ${line}`);
|
|
2451
|
+
}
|
|
2452
|
+
lines.push("");
|
|
2453
|
+
lines.push("=".repeat(W));
|
|
2454
|
+
return lines.join("\n");
|
|
2455
|
+
}
|
|
2456
|
+
function getRecommendation(report) {
|
|
2457
|
+
const lines = [];
|
|
2458
|
+
const failedTests = report.results.filter((r) => r.status === "fail");
|
|
2459
|
+
const avg = report.results.length > 0 ? Math.round(report.results.reduce((s, r) => s + r.accuracy, 0) / report.results.length) : 0;
|
|
2460
|
+
if (avg >= 90 && failedTests.length === 0) {
|
|
2461
|
+
lines.push("[+] Fully compatible -- suitable for all pipeline stages");
|
|
2462
|
+
return lines;
|
|
2463
|
+
}
|
|
2464
|
+
const stageResults = report.results.filter((r) => r.category === "stage-simulation");
|
|
2465
|
+
const workingStages = stageResults.filter((r) => r.status === "pass").map((r) => r.name.replace("_stage", ""));
|
|
2466
|
+
const failingStages = stageResults.filter((r) => r.status !== "pass").map((r) => r.name.replace("_stage", ""));
|
|
2467
|
+
if (workingStages.length > 0) {
|
|
2468
|
+
lines.push(`[+] Suitable for: ${workingStages.join(", ")} stages`);
|
|
2469
|
+
}
|
|
2470
|
+
if (failingStages.length > 0) {
|
|
2471
|
+
lines.push(`[x] Not recommended for: ${failingStages.join(", ")} stages`);
|
|
2472
|
+
}
|
|
2473
|
+
if (failedTests.length > 0) {
|
|
2474
|
+
lines.push("");
|
|
2475
|
+
lines.push("Failed tests:");
|
|
2476
|
+
for (const t of failedTests) {
|
|
2477
|
+
lines.push(` - ${t.name}: ${t.detail.slice(0, 60)}`);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
return lines;
|
|
2481
|
+
}
|
|
2482
|
+
var CATEGORY_ORDER, CATEGORY_LABELS;
|
|
2483
|
+
var init_test_model_report = __esm({
|
|
2484
|
+
"src/cli/test-model-report.ts"() {
|
|
2485
|
+
"use strict";
|
|
2486
|
+
CATEGORY_ORDER = ["infrastructure", "basic", "tool-use", "stage-simulation", "advanced"];
|
|
2487
|
+
CATEGORY_LABELS = {
|
|
2488
|
+
infrastructure: "INFRASTRUCTURE",
|
|
2489
|
+
basic: "BASIC CAPABILITIES",
|
|
2490
|
+
"tool-use": "TOOL USE",
|
|
2491
|
+
"stage-simulation": "STAGE SIMULATION",
|
|
2492
|
+
advanced: "ADVANCED"
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
|
|
2497
|
+
// src/cli/test-model-command.ts
|
|
2498
|
+
var test_model_command_exports = {};
|
|
2499
|
+
__export(test_model_command_exports, {
|
|
2500
|
+
runTestModelCommand: () => runTestModelCommand
|
|
2501
|
+
});
|
|
2502
|
+
import * as fs13 from "fs";
|
|
2503
|
+
import * as os3 from "os";
|
|
2504
|
+
import * as path12 from "path";
|
|
2505
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
2506
|
+
function parseTestModelArgs() {
|
|
2507
|
+
const args2 = process.argv.slice(3);
|
|
2508
|
+
function getArg3(flag) {
|
|
2509
|
+
const idx = args2.indexOf(flag);
|
|
2510
|
+
if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) return args2[idx + 1];
|
|
2511
|
+
return void 0;
|
|
2512
|
+
}
|
|
2513
|
+
const hasFlag3 = (f) => args2.includes(f);
|
|
2514
|
+
if (hasFlag3("--help") || hasFlag3("-h")) {
|
|
2515
|
+
logger.info([
|
|
2516
|
+
"Usage: kody test-model --provider <provider> --model <model> --key <api-key> [options]",
|
|
2517
|
+
"",
|
|
2518
|
+
"Options:",
|
|
2519
|
+
" --provider LLM provider name (e.g. gemini, openai, claude)",
|
|
2520
|
+
" --model Model identifier (e.g. gemini-2.5-flash, claude-sonnet-4-6)",
|
|
2521
|
+
" --key API key (optional for claude/anthropic \u2014 uses CLI auth)",
|
|
2522
|
+
" --key-env Read API key from this environment variable",
|
|
2523
|
+
" --skip-proxy Use an already-running LiteLLM proxy (don't start one)",
|
|
2524
|
+
" --litellm-url LiteLLM proxy URL (default: http://localhost:4099)",
|
|
2525
|
+
" --filter Comma-separated test names to run (default: all)",
|
|
2526
|
+
" --list List all available tests and exit"
|
|
2527
|
+
].join("\n"));
|
|
2528
|
+
process.exit(0);
|
|
2529
|
+
}
|
|
2530
|
+
if (hasFlag3("--list")) {
|
|
2531
|
+
for (const t of ALL_TESTS) {
|
|
2532
|
+
logger.info(` ${t.name.padEnd(24)} [${t.category}] ${t.description}`);
|
|
2533
|
+
}
|
|
2534
|
+
process.exit(0);
|
|
2535
|
+
}
|
|
2536
|
+
const provider = getArg3("--provider");
|
|
2537
|
+
const model = getArg3("--model");
|
|
2538
|
+
const key = getArg3("--key");
|
|
2539
|
+
const keyEnv = getArg3("--key-env");
|
|
2540
|
+
if (!provider || !model) {
|
|
2541
|
+
logger.error("Required: --provider <provider> --model <model> --key <key>");
|
|
2542
|
+
logger.error("Run with --help for usage.");
|
|
2543
|
+
process.exit(1);
|
|
2544
|
+
}
|
|
2545
|
+
const isDirectAnthropic = provider === "claude" || provider === "anthropic";
|
|
2546
|
+
let apiKey = key;
|
|
2547
|
+
if (!apiKey && keyEnv) apiKey = process.env[keyEnv];
|
|
2548
|
+
if (!apiKey && !isDirectAnthropic) {
|
|
2549
|
+
logger.error("API key required: use --key <value> or --key-env <ENV_VAR>");
|
|
2550
|
+
logger.error("(For claude/anthropic provider, --key is optional \u2014 uses Claude Code auth)");
|
|
2551
|
+
process.exit(1);
|
|
2552
|
+
}
|
|
2553
|
+
return {
|
|
2554
|
+
provider,
|
|
2555
|
+
model,
|
|
2556
|
+
apiKey: apiKey ?? "",
|
|
2557
|
+
proxyUrl: isDirectAnthropic ? "https://api.anthropic.com" : getArg3("--litellm-url") ?? TEST_URL,
|
|
2558
|
+
skipProxy: isDirectAnthropic || hasFlag3("--skip-proxy"),
|
|
2559
|
+
filter: getArg3("--filter")?.split(",")
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
function generateConfig(provider, model, dropParams) {
|
|
2563
|
+
const lines = [];
|
|
2564
|
+
if (dropParams) {
|
|
2565
|
+
lines.push("litellm_settings:");
|
|
2566
|
+
lines.push(" drop_params: true");
|
|
2567
|
+
lines.push("");
|
|
2568
|
+
}
|
|
2569
|
+
lines.push("model_list:");
|
|
2570
|
+
lines.push(` - model_name: ${model}`);
|
|
2571
|
+
lines.push(" litellm_params:");
|
|
2572
|
+
lines.push(` model: ${provider}/${model}`);
|
|
2573
|
+
lines.push(" api_key: os.environ/ANTHROPIC_COMPATIBLE_API_KEY");
|
|
2574
|
+
return lines.join("\n") + "\n";
|
|
2575
|
+
}
|
|
2576
|
+
async function startProxy(config, url) {
|
|
2577
|
+
try {
|
|
2578
|
+
execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
2579
|
+
} catch {
|
|
2580
|
+
try {
|
|
2581
|
+
execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
2582
|
+
} catch {
|
|
2583
|
+
logger.error("litellm not installed. Install: pip install 'litellm[proxy]'");
|
|
2584
|
+
return null;
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
fs13.writeFileSync(CONFIG_PATH, config);
|
|
2588
|
+
const portMatch = url.match(/:(\d+)/);
|
|
2589
|
+
const port = portMatch ? portMatch[1] : "4099";
|
|
2590
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
2591
|
+
const child = spawn2("litellm", ["--config", CONFIG_PATH, "--port", port], {
|
|
2592
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2593
|
+
detached: true,
|
|
2594
|
+
env: process.env
|
|
2595
|
+
});
|
|
2596
|
+
for (let i = 0; i < 30; i++) {
|
|
2597
|
+
await delay(2e3);
|
|
2598
|
+
if (await checkLitellmHealth(url)) {
|
|
2599
|
+
logger.info(`LiteLLM proxy ready at ${url}`);
|
|
2600
|
+
return child;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
child.kill();
|
|
2604
|
+
return null;
|
|
2605
|
+
}
|
|
2606
|
+
async function quickApiTest(url, model, apiKey) {
|
|
2607
|
+
try {
|
|
2608
|
+
const res = await fetch(`${url}/v1/messages`, {
|
|
2609
|
+
method: "POST",
|
|
2610
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
2611
|
+
body: JSON.stringify({
|
|
2612
|
+
model,
|
|
2613
|
+
max_tokens: 32,
|
|
2614
|
+
messages: [{ role: "user", content: "Say ok" }],
|
|
2615
|
+
context_management: { policy: "smart" }
|
|
2616
|
+
}),
|
|
2617
|
+
signal: AbortSignal.timeout(3e4)
|
|
2618
|
+
});
|
|
2619
|
+
if (!res.ok) {
|
|
2620
|
+
const body = await res.text();
|
|
2621
|
+
return { ok: false, error: body.slice(0, 200) };
|
|
2622
|
+
}
|
|
2623
|
+
return { ok: true };
|
|
2624
|
+
} catch (err) {
|
|
2625
|
+
return { ok: false, error: String(err) };
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
function delay(ms) {
|
|
2629
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2630
|
+
}
|
|
2631
|
+
async function runTestModelCommand() {
|
|
2632
|
+
const opts = parseTestModelArgs();
|
|
2633
|
+
const startTime = Date.now();
|
|
2634
|
+
logger.info(`Testing model compatibility: ${opts.provider}/${opts.model}`);
|
|
2635
|
+
logger.info("");
|
|
2636
|
+
let proxyProcess = null;
|
|
2637
|
+
let dropParamsRequired = false;
|
|
2638
|
+
const cleanup = () => {
|
|
2639
|
+
if (proxyProcess) {
|
|
2640
|
+
proxyProcess.kill();
|
|
2641
|
+
proxyProcess = null;
|
|
2642
|
+
}
|
|
2643
|
+
fs13.rmSync(CONFIG_PATH, { force: true });
|
|
2644
|
+
};
|
|
2645
|
+
process.on("SIGINT", () => {
|
|
2646
|
+
cleanup();
|
|
2647
|
+
process.exit(1);
|
|
2648
|
+
});
|
|
2649
|
+
process.on("SIGTERM", () => {
|
|
2650
|
+
cleanup();
|
|
2651
|
+
process.exit(1);
|
|
2652
|
+
});
|
|
2653
|
+
try {
|
|
2654
|
+
if (!opts.skipProxy) {
|
|
2655
|
+
process.env.ANTHROPIC_COMPATIBLE_API_KEY = opts.apiKey;
|
|
2656
|
+
logger.info("Starting LiteLLM proxy (without drop_params)...");
|
|
2657
|
+
proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, false), opts.proxyUrl);
|
|
2658
|
+
if (!proxyProcess) {
|
|
2659
|
+
logger.error("Failed to start LiteLLM proxy");
|
|
2660
|
+
process.exit(1);
|
|
2661
|
+
}
|
|
2662
|
+
const quickRes = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
|
|
2663
|
+
if (!quickRes.ok) {
|
|
2664
|
+
logger.info("Model needs drop_params: true -- restarting proxy...");
|
|
2665
|
+
proxyProcess.kill();
|
|
2666
|
+
proxyProcess = null;
|
|
2667
|
+
await delay(2e3);
|
|
2668
|
+
proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, true), opts.proxyUrl);
|
|
2669
|
+
dropParamsRequired = true;
|
|
2670
|
+
if (!proxyProcess) {
|
|
2671
|
+
logger.error("Failed to start LiteLLM proxy with drop_params");
|
|
2672
|
+
process.exit(1);
|
|
2673
|
+
}
|
|
2674
|
+
const retry = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
|
|
2675
|
+
if (!retry.ok) {
|
|
2676
|
+
logger.error(`Model not accessible: ${retry.error}`);
|
|
2677
|
+
process.exit(1);
|
|
2678
|
+
}
|
|
2679
|
+
logger.info("Proxy restarted with drop_params: true");
|
|
2680
|
+
} else {
|
|
2681
|
+
logger.info("drop_params not required");
|
|
2682
|
+
}
|
|
2683
|
+
} else {
|
|
2684
|
+
logger.info(`Using existing proxy at ${opts.proxyUrl}`);
|
|
2685
|
+
}
|
|
2686
|
+
const tests = opts.filter ? ALL_TESTS.filter((t) => opts.filter.includes(t.name)) : ALL_TESTS;
|
|
2687
|
+
logger.info(`Running ${tests.length} compatibility tests...`);
|
|
2688
|
+
logger.info("");
|
|
2689
|
+
const ctx = { proxyUrl: opts.proxyUrl, model: opts.model, apiKey: opts.apiKey, projectDir: process.cwd() };
|
|
2690
|
+
const results = [];
|
|
2691
|
+
for (const test of tests) {
|
|
2692
|
+
process.stdout.write(` ${test.name.padEnd(28)} `);
|
|
2693
|
+
try {
|
|
2694
|
+
const r = await test.run(ctx);
|
|
2695
|
+
results.push(r);
|
|
2696
|
+
const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
|
|
2697
|
+
logger.info(`[${icon}] ${r.status.toUpperCase()} ${r.accuracy}% (${(r.durationMs / 1e3).toFixed(1)}s)`);
|
|
2698
|
+
} catch (err) {
|
|
2699
|
+
const r = {
|
|
2700
|
+
name: test.name,
|
|
2701
|
+
category: test.category,
|
|
2702
|
+
status: "fail",
|
|
2703
|
+
accuracy: 0,
|
|
2704
|
+
durationMs: 0,
|
|
2705
|
+
detail: `Crash: ${err instanceof Error ? err.message : String(err)}`
|
|
2706
|
+
};
|
|
2707
|
+
results.push(r);
|
|
2708
|
+
logger.info("[x] CRASH");
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
const report = {
|
|
2712
|
+
provider: opts.provider,
|
|
2713
|
+
model: opts.model,
|
|
2714
|
+
results,
|
|
2715
|
+
totalDurationMs: Date.now() - startTime,
|
|
2716
|
+
dropParamsRequired,
|
|
2717
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
|
|
2718
|
+
};
|
|
2719
|
+
console.log("");
|
|
2720
|
+
console.log(formatReport(report));
|
|
2721
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
2722
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
2723
|
+
} finally {
|
|
2724
|
+
cleanup();
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
var TEST_PORT, TEST_URL, CONFIG_PATH;
|
|
2728
|
+
var init_test_model_command = __esm({
|
|
2729
|
+
"src/cli/test-model-command.ts"() {
|
|
2730
|
+
"use strict";
|
|
2731
|
+
init_logger();
|
|
2732
|
+
init_litellm();
|
|
2733
|
+
init_test_model_tests();
|
|
2734
|
+
init_test_model_report();
|
|
2735
|
+
TEST_PORT = 4099;
|
|
2736
|
+
TEST_URL = `http://localhost:${TEST_PORT}`;
|
|
2737
|
+
CONFIG_PATH = path12.join(os3.tmpdir(), "kody-test-model-config.yaml");
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
|
|
1559
2741
|
// src/ci/parse-inputs.ts
|
|
1560
2742
|
var parse_inputs_exports = {};
|
|
1561
2743
|
__export(parse_inputs_exports, {
|
|
@@ -1563,16 +2745,16 @@ __export(parse_inputs_exports, {
|
|
|
1563
2745
|
runCiParse: () => runCiParse,
|
|
1564
2746
|
writeOutputs: () => writeOutputs
|
|
1565
2747
|
});
|
|
1566
|
-
import * as
|
|
2748
|
+
import * as fs14 from "fs";
|
|
1567
2749
|
function generateTimestamp() {
|
|
1568
2750
|
const now = /* @__PURE__ */ new Date();
|
|
1569
|
-
const
|
|
2751
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
1570
2752
|
const y = String(now.getFullYear()).slice(2);
|
|
1571
|
-
const m =
|
|
1572
|
-
const d =
|
|
1573
|
-
const H =
|
|
1574
|
-
const M =
|
|
1575
|
-
const S =
|
|
2753
|
+
const m = pad2(now.getMonth() + 1);
|
|
2754
|
+
const d = pad2(now.getDate());
|
|
2755
|
+
const H = pad2(now.getHours());
|
|
2756
|
+
const M = pad2(now.getMinutes());
|
|
2757
|
+
const S = pad2(now.getSeconds());
|
|
1576
2758
|
return `${y}${m}${d}-${H}${M}${S}`;
|
|
1577
2759
|
}
|
|
1578
2760
|
function parseCommentInputs() {
|
|
@@ -1691,23 +2873,6 @@ function parseCommentInputs() {
|
|
|
1691
2873
|
}
|
|
1692
2874
|
const modesWithoutTaskId = ["fix", "fix-ci", "status", "review", "resolve", "rerun"];
|
|
1693
2875
|
const valid = !!taskId || modesWithoutTaskId.includes(mode);
|
|
1694
|
-
if (mode === "taskify" && !ticketId && !prdFile) {
|
|
1695
|
-
return {
|
|
1696
|
-
task_id: taskId,
|
|
1697
|
-
mode,
|
|
1698
|
-
from_stage: fromStage,
|
|
1699
|
-
issue_number: issueNumber,
|
|
1700
|
-
pr_number: "",
|
|
1701
|
-
feedback,
|
|
1702
|
-
complexity,
|
|
1703
|
-
ci_run_id: ciRunId,
|
|
1704
|
-
ticket_id: "",
|
|
1705
|
-
prd_file: "",
|
|
1706
|
-
dry_run: dryRun,
|
|
1707
|
-
valid: false,
|
|
1708
|
-
trigger_type: "comment"
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
2876
|
return {
|
|
1712
2877
|
task_id: taskId,
|
|
1713
2878
|
mode,
|
|
@@ -1724,40 +2889,40 @@ function parseCommentInputs() {
|
|
|
1724
2889
|
trigger_type: "comment"
|
|
1725
2890
|
};
|
|
1726
2891
|
}
|
|
1727
|
-
function writeOutputs(
|
|
2892
|
+
function writeOutputs(result2) {
|
|
1728
2893
|
const outputFile = process.env.GITHUB_OUTPUT;
|
|
1729
2894
|
function output(key, value) {
|
|
1730
2895
|
if (outputFile) {
|
|
1731
2896
|
if (value.includes("\n")) {
|
|
1732
|
-
|
|
2897
|
+
fs14.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
1733
2898
|
${value}
|
|
1734
2899
|
KODY_EOF
|
|
1735
2900
|
`);
|
|
1736
2901
|
} else {
|
|
1737
|
-
|
|
2902
|
+
fs14.appendFileSync(outputFile, `${key}=${value}
|
|
1738
2903
|
`);
|
|
1739
2904
|
}
|
|
1740
2905
|
}
|
|
1741
2906
|
const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
|
|
1742
2907
|
console.log(`${key}=${display}`);
|
|
1743
2908
|
}
|
|
1744
|
-
output("task_id",
|
|
1745
|
-
output("mode",
|
|
1746
|
-
output("from_stage",
|
|
1747
|
-
output("issue_number",
|
|
1748
|
-
output("pr_number",
|
|
1749
|
-
output("feedback",
|
|
1750
|
-
output("complexity",
|
|
1751
|
-
output("ci_run_id",
|
|
1752
|
-
output("ticket_id",
|
|
1753
|
-
output("prd_file",
|
|
1754
|
-
output("dry_run",
|
|
1755
|
-
output("valid",
|
|
1756
|
-
output("trigger_type",
|
|
2909
|
+
output("task_id", result2.task_id);
|
|
2910
|
+
output("mode", result2.mode);
|
|
2911
|
+
output("from_stage", result2.from_stage);
|
|
2912
|
+
output("issue_number", result2.issue_number);
|
|
2913
|
+
output("pr_number", result2.pr_number);
|
|
2914
|
+
output("feedback", result2.feedback);
|
|
2915
|
+
output("complexity", result2.complexity);
|
|
2916
|
+
output("ci_run_id", result2.ci_run_id);
|
|
2917
|
+
output("ticket_id", result2.ticket_id);
|
|
2918
|
+
output("prd_file", result2.prd_file);
|
|
2919
|
+
output("dry_run", result2.dry_run ? "true" : "false");
|
|
2920
|
+
output("valid", result2.valid ? "true" : "false");
|
|
2921
|
+
output("trigger_type", result2.trigger_type);
|
|
1757
2922
|
}
|
|
1758
2923
|
function runCiParse() {
|
|
1759
|
-
const
|
|
1760
|
-
writeOutputs(
|
|
2924
|
+
const result2 = parseCommentInputs();
|
|
2925
|
+
writeOutputs(result2);
|
|
1761
2926
|
}
|
|
1762
2927
|
var VALID_MODES;
|
|
1763
2928
|
var init_parse_inputs = __esm({
|
|
@@ -1859,7 +3024,7 @@ var init_definitions = __esm({
|
|
|
1859
3024
|
});
|
|
1860
3025
|
|
|
1861
3026
|
// src/git-utils.ts
|
|
1862
|
-
import { execFileSync as
|
|
3027
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
1863
3028
|
function getHookSafeEnv() {
|
|
1864
3029
|
if (!_hookSafeEnv) {
|
|
1865
3030
|
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
@@ -1867,7 +3032,7 @@ function getHookSafeEnv() {
|
|
|
1867
3032
|
return _hookSafeEnv;
|
|
1868
3033
|
}
|
|
1869
3034
|
function git(args2, options) {
|
|
1870
|
-
return
|
|
3035
|
+
return execFileSync11("git", args2, {
|
|
1871
3036
|
encoding: "utf-8",
|
|
1872
3037
|
timeout: options?.timeout ?? 3e4,
|
|
1873
3038
|
cwd: options?.cwd,
|
|
@@ -2053,22 +3218,22 @@ var init_git_utils = __esm({
|
|
|
2053
3218
|
});
|
|
2054
3219
|
|
|
2055
3220
|
// src/pipeline/state.ts
|
|
2056
|
-
import * as
|
|
2057
|
-
import * as
|
|
3221
|
+
import * as fs15 from "fs";
|
|
3222
|
+
import * as path13 from "path";
|
|
2058
3223
|
function loadState(taskId, taskDir) {
|
|
2059
|
-
const p =
|
|
2060
|
-
if (!
|
|
3224
|
+
const p = path13.join(taskDir, "status.json");
|
|
3225
|
+
if (!fs15.existsSync(p)) return null;
|
|
2061
3226
|
try {
|
|
2062
|
-
const
|
|
2063
|
-
|
|
3227
|
+
const result2 = parseJsonSafe(
|
|
3228
|
+
fs15.readFileSync(p, "utf-8"),
|
|
2064
3229
|
["taskId", "state", "stages", "createdAt", "updatedAt"]
|
|
2065
3230
|
);
|
|
2066
|
-
if (!
|
|
2067
|
-
logger.warn(` Corrupt status.json: ${
|
|
3231
|
+
if (!result2.ok) {
|
|
3232
|
+
logger.warn(` Corrupt status.json: ${result2.error}`);
|
|
2068
3233
|
return null;
|
|
2069
3234
|
}
|
|
2070
|
-
if (
|
|
2071
|
-
return
|
|
3235
|
+
if (result2.data.taskId !== taskId) return null;
|
|
3236
|
+
return result2.data;
|
|
2072
3237
|
} catch {
|
|
2073
3238
|
return null;
|
|
2074
3239
|
}
|
|
@@ -2078,10 +3243,10 @@ function writeState(state, taskDir) {
|
|
|
2078
3243
|
...state,
|
|
2079
3244
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2080
3245
|
};
|
|
2081
|
-
const target =
|
|
3246
|
+
const target = path13.join(taskDir, "status.json");
|
|
2082
3247
|
const tmp = target + ".tmp";
|
|
2083
|
-
|
|
2084
|
-
|
|
3248
|
+
fs15.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
3249
|
+
fs15.renameSync(tmp, target);
|
|
2085
3250
|
return updated;
|
|
2086
3251
|
}
|
|
2087
3252
|
function initState(taskId) {
|
|
@@ -2122,16 +3287,16 @@ var init_complexity = __esm({
|
|
|
2122
3287
|
});
|
|
2123
3288
|
|
|
2124
3289
|
// src/memory.ts
|
|
2125
|
-
import * as
|
|
2126
|
-
import * as
|
|
3290
|
+
import * as fs16 from "fs";
|
|
3291
|
+
import * as path14 from "path";
|
|
2127
3292
|
function readProjectMemory(projectDir) {
|
|
2128
|
-
const memoryDir =
|
|
2129
|
-
if (!
|
|
2130
|
-
const files =
|
|
3293
|
+
const memoryDir = path14.join(projectDir, ".kody", "memory");
|
|
3294
|
+
if (!fs16.existsSync(memoryDir)) return "";
|
|
3295
|
+
const files = fs16.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
2131
3296
|
if (files.length === 0) return "";
|
|
2132
3297
|
const sections = [];
|
|
2133
3298
|
for (const file of files) {
|
|
2134
|
-
const content =
|
|
3299
|
+
const content = fs16.readFileSync(path14.join(memoryDir, file), "utf-8").trim();
|
|
2135
3300
|
if (content) {
|
|
2136
3301
|
sections.push(`## ${file.replace(".md", "")}
|
|
2137
3302
|
${content}`);
|
|
@@ -2150,8 +3315,8 @@ var init_memory = __esm({
|
|
|
2150
3315
|
});
|
|
2151
3316
|
|
|
2152
3317
|
// src/context-tiers.ts
|
|
2153
|
-
import * as
|
|
2154
|
-
import * as
|
|
3318
|
+
import * as fs17 from "fs";
|
|
3319
|
+
import * as path15 from "path";
|
|
2155
3320
|
function estimateTokens(text) {
|
|
2156
3321
|
return Math.ceil(text.length / 4);
|
|
2157
3322
|
}
|
|
@@ -2178,8 +3343,8 @@ function generateL0(content, filename) {
|
|
|
2178
3343
|
break;
|
|
2179
3344
|
}
|
|
2180
3345
|
}
|
|
2181
|
-
const
|
|
2182
|
-
return
|
|
3346
|
+
const result2 = parts.join("\n");
|
|
3347
|
+
return result2.slice(0, L0_MAX_CHARS);
|
|
2183
3348
|
}
|
|
2184
3349
|
function generateL0Json(content) {
|
|
2185
3350
|
try {
|
|
@@ -2221,8 +3386,8 @@ function generateL1(content, filename) {
|
|
|
2221
3386
|
inSection = false;
|
|
2222
3387
|
}
|
|
2223
3388
|
}
|
|
2224
|
-
const
|
|
2225
|
-
return
|
|
3389
|
+
const result2 = parts.join("\n");
|
|
3390
|
+
return result2.slice(0, L1_MAX_CHARS);
|
|
2226
3391
|
}
|
|
2227
3392
|
function generateL1Json(content) {
|
|
2228
3393
|
try {
|
|
@@ -2242,7 +3407,7 @@ function generateL1Json(content) {
|
|
|
2242
3407
|
}
|
|
2243
3408
|
}
|
|
2244
3409
|
function getTieredContent(filePath, content) {
|
|
2245
|
-
const key =
|
|
3410
|
+
const key = path15.basename(filePath);
|
|
2246
3411
|
return {
|
|
2247
3412
|
source: filePath,
|
|
2248
3413
|
L0: generateL0(content, key),
|
|
@@ -2254,15 +3419,15 @@ function selectTier(tiered, tier) {
|
|
|
2254
3419
|
return tiered[tier];
|
|
2255
3420
|
}
|
|
2256
3421
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
2257
|
-
const memoryDir =
|
|
2258
|
-
if (!
|
|
2259
|
-
const files =
|
|
3422
|
+
const memoryDir = path15.join(projectDir, ".kody", "memory");
|
|
3423
|
+
if (!fs17.existsSync(memoryDir)) return "";
|
|
3424
|
+
const files = fs17.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
2260
3425
|
if (files.length === 0) return "";
|
|
2261
3426
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
2262
3427
|
const sections = [];
|
|
2263
3428
|
for (const file of files) {
|
|
2264
|
-
const filePath =
|
|
2265
|
-
const content =
|
|
3429
|
+
const filePath = path15.join(memoryDir, file);
|
|
3430
|
+
const content = fs17.readFileSync(filePath, "utf-8").trim();
|
|
2266
3431
|
if (!content) continue;
|
|
2267
3432
|
const tiered = getTieredContent(filePath, content);
|
|
2268
3433
|
const selected = selectTier(tiered, tier);
|
|
@@ -2285,9 +3450,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
2285
3450
|
`;
|
|
2286
3451
|
context += `Task Directory: ${taskDir}
|
|
2287
3452
|
`;
|
|
2288
|
-
const taskMdPath =
|
|
2289
|
-
if (
|
|
2290
|
-
const content =
|
|
3453
|
+
const taskMdPath = path15.join(taskDir, "task.md");
|
|
3454
|
+
if (fs17.existsSync(taskMdPath)) {
|
|
3455
|
+
const content = fs17.readFileSync(taskMdPath, "utf-8");
|
|
2291
3456
|
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
2292
3457
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
2293
3458
|
context += `
|
|
@@ -2295,9 +3460,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
2295
3460
|
${selected}
|
|
2296
3461
|
`;
|
|
2297
3462
|
}
|
|
2298
|
-
const taskJsonPath =
|
|
2299
|
-
if (
|
|
2300
|
-
const content =
|
|
3463
|
+
const taskJsonPath = path15.join(taskDir, "task.json");
|
|
3464
|
+
if (fs17.existsSync(taskJsonPath)) {
|
|
3465
|
+
const content = fs17.readFileSync(taskJsonPath, "utf-8");
|
|
2301
3466
|
if (policy.taskClassification === "L2") {
|
|
2302
3467
|
try {
|
|
2303
3468
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -2323,9 +3488,9 @@ ${selected}
|
|
|
2323
3488
|
}
|
|
2324
3489
|
}
|
|
2325
3490
|
}
|
|
2326
|
-
const specPath =
|
|
2327
|
-
if (
|
|
2328
|
-
const content =
|
|
3491
|
+
const specPath = path15.join(taskDir, "spec.md");
|
|
3492
|
+
if (fs17.existsSync(specPath)) {
|
|
3493
|
+
const content = fs17.readFileSync(specPath, "utf-8");
|
|
2329
3494
|
const selected = selectContent(specPath, content, policy.spec);
|
|
2330
3495
|
const label = tierLabel("Spec", policy.spec);
|
|
2331
3496
|
context += `
|
|
@@ -2333,9 +3498,9 @@ ${selected}
|
|
|
2333
3498
|
${selected}
|
|
2334
3499
|
`;
|
|
2335
3500
|
}
|
|
2336
|
-
const planPath =
|
|
2337
|
-
if (
|
|
2338
|
-
const content =
|
|
3501
|
+
const planPath = path15.join(taskDir, "plan.md");
|
|
3502
|
+
if (fs17.existsSync(planPath)) {
|
|
3503
|
+
const content = fs17.readFileSync(planPath, "utf-8");
|
|
2339
3504
|
const selected = selectContent(planPath, content, policy.plan);
|
|
2340
3505
|
const label = tierLabel("Plan", policy.plan);
|
|
2341
3506
|
context += `
|
|
@@ -2343,9 +3508,9 @@ ${selected}
|
|
|
2343
3508
|
${selected}
|
|
2344
3509
|
`;
|
|
2345
3510
|
}
|
|
2346
|
-
const contextMdPath =
|
|
2347
|
-
if (
|
|
2348
|
-
const content =
|
|
3511
|
+
const contextMdPath = path15.join(taskDir, "context.md");
|
|
3512
|
+
if (fs17.existsSync(contextMdPath)) {
|
|
3513
|
+
const content = fs17.readFileSync(contextMdPath, "utf-8");
|
|
2349
3514
|
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
2350
3515
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
2351
3516
|
context += `
|
|
@@ -2431,24 +3596,24 @@ var init_context_tiers = __esm({
|
|
|
2431
3596
|
});
|
|
2432
3597
|
|
|
2433
3598
|
// src/context.ts
|
|
2434
|
-
import * as
|
|
2435
|
-
import * as
|
|
3599
|
+
import * as fs18 from "fs";
|
|
3600
|
+
import * as path16 from "path";
|
|
2436
3601
|
function readPromptFile(stageName, projectDir) {
|
|
2437
3602
|
if (projectDir) {
|
|
2438
|
-
const stepFile =
|
|
2439
|
-
if (
|
|
2440
|
-
return
|
|
3603
|
+
const stepFile = path16.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
3604
|
+
if (fs18.existsSync(stepFile)) {
|
|
3605
|
+
return fs18.readFileSync(stepFile, "utf-8");
|
|
2441
3606
|
}
|
|
2442
3607
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
2443
3608
|
}
|
|
2444
3609
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
2445
3610
|
const candidates = [
|
|
2446
|
-
|
|
2447
|
-
|
|
3611
|
+
path16.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
3612
|
+
path16.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
2448
3613
|
];
|
|
2449
3614
|
for (const candidate of candidates) {
|
|
2450
|
-
if (
|
|
2451
|
-
return
|
|
3615
|
+
if (fs18.existsSync(candidate)) {
|
|
3616
|
+
return fs18.readFileSync(candidate, "utf-8");
|
|
2452
3617
|
}
|
|
2453
3618
|
}
|
|
2454
3619
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -2460,18 +3625,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
2460
3625
|
`;
|
|
2461
3626
|
context += `Task Directory: ${taskDir}
|
|
2462
3627
|
`;
|
|
2463
|
-
const taskMdPath =
|
|
2464
|
-
if (
|
|
2465
|
-
const taskMd =
|
|
3628
|
+
const taskMdPath = path16.join(taskDir, "task.md");
|
|
3629
|
+
if (fs18.existsSync(taskMdPath)) {
|
|
3630
|
+
const taskMd = fs18.readFileSync(taskMdPath, "utf-8");
|
|
2466
3631
|
context += `
|
|
2467
3632
|
## Task Description
|
|
2468
3633
|
${taskMd}
|
|
2469
3634
|
`;
|
|
2470
3635
|
}
|
|
2471
|
-
const taskJsonPath =
|
|
2472
|
-
if (
|
|
3636
|
+
const taskJsonPath = path16.join(taskDir, "task.json");
|
|
3637
|
+
if (fs18.existsSync(taskJsonPath)) {
|
|
2473
3638
|
try {
|
|
2474
|
-
const taskDef = JSON.parse(
|
|
3639
|
+
const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
|
|
2475
3640
|
context += `
|
|
2476
3641
|
## Task Classification
|
|
2477
3642
|
`;
|
|
@@ -2484,27 +3649,27 @@ ${taskMd}
|
|
|
2484
3649
|
} catch {
|
|
2485
3650
|
}
|
|
2486
3651
|
}
|
|
2487
|
-
const specPath =
|
|
2488
|
-
if (
|
|
2489
|
-
const spec =
|
|
3652
|
+
const specPath = path16.join(taskDir, "spec.md");
|
|
3653
|
+
if (fs18.existsSync(specPath)) {
|
|
3654
|
+
const spec = fs18.readFileSync(specPath, "utf-8");
|
|
2490
3655
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
2491
3656
|
context += `
|
|
2492
3657
|
## Spec Summary
|
|
2493
3658
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
2494
3659
|
`;
|
|
2495
3660
|
}
|
|
2496
|
-
const planPath =
|
|
2497
|
-
if (
|
|
2498
|
-
const plan =
|
|
3661
|
+
const planPath = path16.join(taskDir, "plan.md");
|
|
3662
|
+
if (fs18.existsSync(planPath)) {
|
|
3663
|
+
const plan = fs18.readFileSync(planPath, "utf-8");
|
|
2499
3664
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
2500
3665
|
context += `
|
|
2501
3666
|
## Plan Summary
|
|
2502
3667
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
2503
3668
|
`;
|
|
2504
3669
|
}
|
|
2505
|
-
const contextMdPath =
|
|
2506
|
-
if (
|
|
2507
|
-
const accumulated =
|
|
3670
|
+
const contextMdPath = path16.join(taskDir, "context.md");
|
|
3671
|
+
if (fs18.existsSync(contextMdPath)) {
|
|
3672
|
+
const accumulated = fs18.readFileSync(contextMdPath, "utf-8");
|
|
2508
3673
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
2509
3674
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
2510
3675
|
context += `
|
|
@@ -2522,17 +3687,17 @@ ${feedback}
|
|
|
2522
3687
|
}
|
|
2523
3688
|
function inferHasUIFromScope(scope) {
|
|
2524
3689
|
return scope.some((filePath) => {
|
|
2525
|
-
const ext =
|
|
3690
|
+
const ext = path16.extname(filePath).toLowerCase();
|
|
2526
3691
|
if (UI_EXTENSIONS.has(ext)) return true;
|
|
2527
3692
|
const normalized = filePath.replace(/\\/g, "/");
|
|
2528
3693
|
return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
|
|
2529
3694
|
});
|
|
2530
3695
|
}
|
|
2531
3696
|
function taskHasUI(taskDir) {
|
|
2532
|
-
const taskJsonPath =
|
|
2533
|
-
if (!
|
|
3697
|
+
const taskJsonPath = path16.join(taskDir, "task.json");
|
|
3698
|
+
if (!fs18.existsSync(taskJsonPath)) return true;
|
|
2534
3699
|
try {
|
|
2535
|
-
const taskDef = JSON.parse(
|
|
3700
|
+
const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
|
|
2536
3701
|
const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
|
|
2537
3702
|
if (scope.length === 0) return true;
|
|
2538
3703
|
return inferHasUIFromScope(scope);
|
|
@@ -2654,9 +3819,9 @@ ${prompt}` : prompt;
|
|
|
2654
3819
|
}
|
|
2655
3820
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
2656
3821
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
2657
|
-
const qaGuidePath =
|
|
2658
|
-
if (
|
|
2659
|
-
const qaGuide =
|
|
3822
|
+
const qaGuidePath = path16.join(projectDir, ".kody", "qa-guide.md");
|
|
3823
|
+
if (fs18.existsSync(qaGuidePath)) {
|
|
3824
|
+
const qaGuide = fs18.readFileSync(qaGuidePath, "utf-8").trim();
|
|
2660
3825
|
assembled = assembled + "\n\n" + qaGuide;
|
|
2661
3826
|
}
|
|
2662
3827
|
}
|
|
@@ -2686,10 +3851,12 @@ function escalateModelTier(currentTier) {
|
|
|
2686
3851
|
function resolveModel(modelTier, stageName) {
|
|
2687
3852
|
const config = getProjectConfig();
|
|
2688
3853
|
const mapped = config.agent.modelMap[modelTier];
|
|
2689
|
-
if (mapped)
|
|
2690
|
-
|
|
3854
|
+
if (!mapped) {
|
|
3855
|
+
throw new Error(`No model configured for tier '${modelTier}'. Set agent.modelMap.${modelTier} in kody.config.json`);
|
|
3856
|
+
}
|
|
3857
|
+
return mapped;
|
|
2691
3858
|
}
|
|
2692
|
-
var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION
|
|
3859
|
+
var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
|
|
2693
3860
|
var init_context = __esm({
|
|
2694
3861
|
"src/context.ts"() {
|
|
2695
3862
|
"use strict";
|
|
@@ -2723,11 +3890,6 @@ var init_context = __esm({
|
|
|
2723
3890
|
mid: "strong",
|
|
2724
3891
|
strong: "strong"
|
|
2725
3892
|
};
|
|
2726
|
-
DEFAULT_MODEL_MAP = {
|
|
2727
|
-
cheap: "haiku",
|
|
2728
|
-
mid: "sonnet",
|
|
2729
|
-
strong: "opus"
|
|
2730
|
-
};
|
|
2731
3893
|
}
|
|
2732
3894
|
});
|
|
2733
3895
|
|
|
@@ -2751,8 +3913,8 @@ var init_runner_selection = __esm({
|
|
|
2751
3913
|
});
|
|
2752
3914
|
|
|
2753
3915
|
// src/stages/agent.ts
|
|
2754
|
-
import * as
|
|
2755
|
-
import * as
|
|
3916
|
+
import * as fs19 from "fs";
|
|
3917
|
+
import * as path17 from "path";
|
|
2756
3918
|
function getSessionInfo(stageName, sessions) {
|
|
2757
3919
|
const group = SESSION_GROUP[stageName];
|
|
2758
3920
|
if (!group) return void 0;
|
|
@@ -2837,29 +3999,29 @@ async function executeAgentStage(ctx, def) {
|
|
|
2837
3999
|
if (lastResult.outcome !== "completed") {
|
|
2838
4000
|
return { outcome: lastResult.outcome, error: lastResult.error, retries };
|
|
2839
4001
|
}
|
|
2840
|
-
const
|
|
2841
|
-
if (def.outputFile &&
|
|
2842
|
-
|
|
4002
|
+
const result2 = lastResult;
|
|
4003
|
+
if (def.outputFile && result2.output) {
|
|
4004
|
+
fs19.writeFileSync(path17.join(ctx.taskDir, def.outputFile), result2.output);
|
|
2843
4005
|
}
|
|
2844
4006
|
if (def.outputFile) {
|
|
2845
|
-
const outputPath =
|
|
2846
|
-
if (!
|
|
2847
|
-
const ext =
|
|
2848
|
-
const base =
|
|
2849
|
-
const files =
|
|
4007
|
+
const outputPath = path17.join(ctx.taskDir, def.outputFile);
|
|
4008
|
+
if (!fs19.existsSync(outputPath)) {
|
|
4009
|
+
const ext = path17.extname(def.outputFile);
|
|
4010
|
+
const base = path17.basename(def.outputFile, ext);
|
|
4011
|
+
const files = fs19.readdirSync(ctx.taskDir);
|
|
2850
4012
|
const variant = files.find(
|
|
2851
4013
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
2852
4014
|
);
|
|
2853
4015
|
if (variant) {
|
|
2854
|
-
|
|
4016
|
+
fs19.renameSync(path17.join(ctx.taskDir, variant), outputPath);
|
|
2855
4017
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
2856
4018
|
}
|
|
2857
4019
|
}
|
|
2858
4020
|
}
|
|
2859
4021
|
if (def.outputFile) {
|
|
2860
|
-
const outputPath =
|
|
2861
|
-
if (
|
|
2862
|
-
const content =
|
|
4022
|
+
const outputPath = path17.join(ctx.taskDir, def.outputFile);
|
|
4023
|
+
if (fs19.existsSync(outputPath)) {
|
|
4024
|
+
const content = fs19.readFileSync(outputPath, "utf-8");
|
|
2863
4025
|
const validation = validateStageOutput(def.name, content);
|
|
2864
4026
|
if (!validation.valid) {
|
|
2865
4027
|
if (def.name === "taskify") {
|
|
@@ -2873,7 +4035,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2873
4035
|
const stripped = stripFences(retryResult.output);
|
|
2874
4036
|
const retryValidation = validateTaskJson(stripped);
|
|
2875
4037
|
if (retryValidation.valid) {
|
|
2876
|
-
|
|
4038
|
+
fs19.writeFileSync(outputPath, retryResult.output);
|
|
2877
4039
|
logger.info(` taskify retry produced valid JSON`);
|
|
2878
4040
|
} else {
|
|
2879
4041
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -2886,7 +4048,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2886
4048
|
risk_level: "low",
|
|
2887
4049
|
questions: []
|
|
2888
4050
|
}, null, 2);
|
|
2889
|
-
|
|
4051
|
+
fs19.writeFileSync(outputPath, fallback);
|
|
2890
4052
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
2891
4053
|
}
|
|
2892
4054
|
}
|
|
@@ -2896,11 +4058,11 @@ async function executeAgentStage(ctx, def) {
|
|
|
2896
4058
|
}
|
|
2897
4059
|
}
|
|
2898
4060
|
}
|
|
2899
|
-
appendStageContext(ctx.taskDir, def.name,
|
|
4061
|
+
appendStageContext(ctx.taskDir, def.name, result2.output);
|
|
2900
4062
|
return { outcome: "completed", outputFile: def.outputFile, retries };
|
|
2901
4063
|
}
|
|
2902
4064
|
function appendStageContext(taskDir, stageName, output) {
|
|
2903
|
-
const contextPath =
|
|
4065
|
+
const contextPath = path17.join(taskDir, "context.md");
|
|
2904
4066
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
2905
4067
|
let summary;
|
|
2906
4068
|
if (output && output.trim()) {
|
|
@@ -2913,7 +4075,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
2913
4075
|
### ${stageName} (${timestamp2})
|
|
2914
4076
|
${summary}
|
|
2915
4077
|
`;
|
|
2916
|
-
|
|
4078
|
+
fs19.appendFileSync(contextPath, entry);
|
|
2917
4079
|
}
|
|
2918
4080
|
var SESSION_GROUP;
|
|
2919
4081
|
var init_agent = __esm({
|
|
@@ -2936,7 +4098,7 @@ var init_agent = __esm({
|
|
|
2936
4098
|
});
|
|
2937
4099
|
|
|
2938
4100
|
// src/verify-runner.ts
|
|
2939
|
-
import { execFileSync as
|
|
4101
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
2940
4102
|
function isExecError(err) {
|
|
2941
4103
|
return typeof err === "object" && err !== null;
|
|
2942
4104
|
}
|
|
@@ -2972,7 +4134,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
2972
4134
|
return { success: true, output: "", timedOut: false };
|
|
2973
4135
|
}
|
|
2974
4136
|
try {
|
|
2975
|
-
const output =
|
|
4137
|
+
const output = execFileSync12(parts[0], parts.slice(1), {
|
|
2976
4138
|
cwd,
|
|
2977
4139
|
timeout,
|
|
2978
4140
|
encoding: "utf-8",
|
|
@@ -3018,19 +4180,19 @@ function runQualityGates(taskDir, projectRoot) {
|
|
|
3018
4180
|
for (const { name, cmd } of commands) {
|
|
3019
4181
|
if (!cmd) continue;
|
|
3020
4182
|
logger.info(` Running ${name}: ${cmd}`);
|
|
3021
|
-
const
|
|
3022
|
-
if (
|
|
4183
|
+
const result2 = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
|
|
4184
|
+
if (result2.timedOut) {
|
|
3023
4185
|
allErrors.push(`${name}: timed out after ${VERIFY_COMMAND_TIMEOUT_MS / 1e3}s`);
|
|
3024
4186
|
allPass = false;
|
|
3025
4187
|
continue;
|
|
3026
4188
|
}
|
|
3027
|
-
if (!
|
|
4189
|
+
if (!result2.success) {
|
|
3028
4190
|
allPass = false;
|
|
3029
|
-
const errors = parseErrors(
|
|
4191
|
+
const errors = parseErrors(result2.output);
|
|
3030
4192
|
allErrors.push(...errors.map((e) => `[${name}] ${e}`));
|
|
3031
|
-
rawOutputs.push({ name, output:
|
|
4193
|
+
rawOutputs.push({ name, output: result2.output.slice(-3e3) });
|
|
3032
4194
|
}
|
|
3033
|
-
allSummary.push(...extractSummary(
|
|
4195
|
+
allSummary.push(...extractSummary(result2.output, name));
|
|
3034
4196
|
}
|
|
3035
4197
|
return { pass: allPass, errors: allErrors, summary: allSummary, rawOutputs };
|
|
3036
4198
|
}
|
|
@@ -3043,7 +4205,7 @@ var init_verify_runner = __esm({
|
|
|
3043
4205
|
});
|
|
3044
4206
|
|
|
3045
4207
|
// src/observer.ts
|
|
3046
|
-
import { execFileSync as
|
|
4208
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
3047
4209
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
3048
4210
|
const context = [
|
|
3049
4211
|
`Stage: ${stageName}`,
|
|
@@ -3057,7 +4219,7 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
3057
4219
|
].join("\n");
|
|
3058
4220
|
const prompt = DIAGNOSIS_PROMPT + context;
|
|
3059
4221
|
try {
|
|
3060
|
-
const
|
|
4222
|
+
const result2 = await runner.run(
|
|
3061
4223
|
"diagnosis",
|
|
3062
4224
|
prompt,
|
|
3063
4225
|
model,
|
|
@@ -3066,8 +4228,8 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
3066
4228
|
"",
|
|
3067
4229
|
options
|
|
3068
4230
|
);
|
|
3069
|
-
if (
|
|
3070
|
-
const cleaned =
|
|
4231
|
+
if (result2.outcome === "completed" && result2.output) {
|
|
4232
|
+
const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
3071
4233
|
const parseResult = parseJsonSafe(cleaned, ["classification"]);
|
|
3072
4234
|
if (parseResult.ok) {
|
|
3073
4235
|
const { data } = parseResult;
|
|
@@ -3126,13 +4288,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
3126
4288
|
}
|
|
3127
4289
|
function getModifiedFiles(projectDir) {
|
|
3128
4290
|
try {
|
|
3129
|
-
const staged =
|
|
4291
|
+
const staged = execFileSync13("git", ["diff", "--name-only", "--cached"], {
|
|
3130
4292
|
encoding: "utf-8",
|
|
3131
4293
|
cwd: projectDir,
|
|
3132
4294
|
timeout: 5e3,
|
|
3133
4295
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3134
4296
|
}).trim();
|
|
3135
|
-
const unstaged =
|
|
4297
|
+
const unstaged = execFileSync13("git", ["diff", "--name-only"], {
|
|
3136
4298
|
encoding: "utf-8",
|
|
3137
4299
|
cwd: projectDir,
|
|
3138
4300
|
timeout: 5e3,
|
|
@@ -3175,8 +4337,8 @@ Error context:
|
|
|
3175
4337
|
});
|
|
3176
4338
|
|
|
3177
4339
|
// src/stages/gate.ts
|
|
3178
|
-
import * as
|
|
3179
|
-
import * as
|
|
4340
|
+
import * as fs20 from "fs";
|
|
4341
|
+
import * as path18 from "path";
|
|
3180
4342
|
function executeGateStage(ctx, def) {
|
|
3181
4343
|
if (ctx.input.dryRun) {
|
|
3182
4344
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -3219,7 +4381,7 @@ ${output}
|
|
|
3219
4381
|
`);
|
|
3220
4382
|
}
|
|
3221
4383
|
}
|
|
3222
|
-
|
|
4384
|
+
fs20.writeFileSync(path18.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
3223
4385
|
return {
|
|
3224
4386
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
3225
4387
|
retries: 0
|
|
@@ -3234,9 +4396,9 @@ var init_gate = __esm({
|
|
|
3234
4396
|
});
|
|
3235
4397
|
|
|
3236
4398
|
// src/stages/verify.ts
|
|
3237
|
-
import * as
|
|
3238
|
-
import * as
|
|
3239
|
-
import { execFileSync as
|
|
4399
|
+
import * as fs21 from "fs";
|
|
4400
|
+
import * as path19 from "path";
|
|
4401
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
3240
4402
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
3241
4403
|
const maxAttempts = def.maxRetries ?? 2;
|
|
3242
4404
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -3246,8 +4408,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
3246
4408
|
return { ...gateResult, retries: attempt };
|
|
3247
4409
|
}
|
|
3248
4410
|
if (attempt < maxAttempts) {
|
|
3249
|
-
const verifyPath =
|
|
3250
|
-
const errorOutput =
|
|
4411
|
+
const verifyPath = path19.join(ctx.taskDir, "verify.md");
|
|
4412
|
+
const errorOutput = fs21.existsSync(verifyPath) ? fs21.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
3251
4413
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
3252
4414
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
3253
4415
|
const diagConfig = getProjectConfig();
|
|
@@ -3290,7 +4452,7 @@ ${diagnosis.resolution}`);
|
|
|
3290
4452
|
const parts = parseCommand(cmd);
|
|
3291
4453
|
if (parts.length === 0) return;
|
|
3292
4454
|
try {
|
|
3293
|
-
|
|
4455
|
+
execFileSync14(parts[0], parts.slice(1), {
|
|
3294
4456
|
stdio: "pipe",
|
|
3295
4457
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
3296
4458
|
});
|
|
@@ -3343,8 +4505,8 @@ var init_verify = __esm({
|
|
|
3343
4505
|
});
|
|
3344
4506
|
|
|
3345
4507
|
// src/review-standalone.ts
|
|
3346
|
-
import * as
|
|
3347
|
-
import * as
|
|
4508
|
+
import * as fs22 from "fs";
|
|
4509
|
+
import * as path20 from "path";
|
|
3348
4510
|
function resolveReviewTarget(input) {
|
|
3349
4511
|
if (input.prs.length === 0) {
|
|
3350
4512
|
return {
|
|
@@ -3368,8 +4530,8 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
3368
4530
|
}
|
|
3369
4531
|
async function runStandaloneReview(input) {
|
|
3370
4532
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
3371
|
-
const taskDir =
|
|
3372
|
-
|
|
4533
|
+
const taskDir = path20.join(input.projectDir, ".kody", "tasks", taskId);
|
|
4534
|
+
fs22.mkdirSync(taskDir, { recursive: true });
|
|
3373
4535
|
let diffInstruction = "";
|
|
3374
4536
|
let filesChangedSection = "";
|
|
3375
4537
|
if (input.baseBranch) {
|
|
@@ -3396,7 +4558,7 @@ ${fileList}`;
|
|
|
3396
4558
|
const taskContent = `# ${input.prTitle}
|
|
3397
4559
|
|
|
3398
4560
|
${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
3399
|
-
|
|
4561
|
+
fs22.writeFileSync(path20.join(taskDir, "task.md"), taskContent);
|
|
3400
4562
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
3401
4563
|
const ctx = {
|
|
3402
4564
|
taskId,
|
|
@@ -3410,18 +4572,18 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
|
3410
4572
|
}
|
|
3411
4573
|
};
|
|
3412
4574
|
logger.info(`[review] standalone review for: ${input.prTitle}`);
|
|
3413
|
-
const
|
|
3414
|
-
if (
|
|
4575
|
+
const result2 = await executeAgentStage(ctx, reviewDef);
|
|
4576
|
+
if (result2.outcome !== "completed") {
|
|
3415
4577
|
return {
|
|
3416
4578
|
outcome: "failed",
|
|
3417
4579
|
taskDir,
|
|
3418
|
-
error:
|
|
4580
|
+
error: result2.error ?? "Review stage failed"
|
|
3419
4581
|
};
|
|
3420
4582
|
}
|
|
3421
|
-
const reviewPath =
|
|
4583
|
+
const reviewPath = path20.join(taskDir, "review.md");
|
|
3422
4584
|
let reviewContent;
|
|
3423
|
-
if (
|
|
3424
|
-
reviewContent =
|
|
4585
|
+
if (fs22.existsSync(reviewPath)) {
|
|
4586
|
+
reviewContent = fs22.readFileSync(reviewPath, "utf-8");
|
|
3425
4587
|
}
|
|
3426
4588
|
return {
|
|
3427
4589
|
outcome: "completed",
|
|
@@ -3461,8 +4623,8 @@ var init_review_standalone = __esm({
|
|
|
3461
4623
|
});
|
|
3462
4624
|
|
|
3463
4625
|
// src/stages/review.ts
|
|
3464
|
-
import * as
|
|
3465
|
-
import * as
|
|
4626
|
+
import * as fs23 from "fs";
|
|
4627
|
+
import * as path21 from "path";
|
|
3466
4628
|
async function executeReviewWithFix(ctx, def) {
|
|
3467
4629
|
if (ctx.input.dryRun) {
|
|
3468
4630
|
return { outcome: "completed", retries: 0 };
|
|
@@ -3476,11 +4638,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
3476
4638
|
if (reviewResult.outcome !== "completed") {
|
|
3477
4639
|
return reviewResult;
|
|
3478
4640
|
}
|
|
3479
|
-
const reviewFile =
|
|
3480
|
-
if (!
|
|
4641
|
+
const reviewFile = path21.join(ctx.taskDir, "review.md");
|
|
4642
|
+
if (!fs23.existsSync(reviewFile)) {
|
|
3481
4643
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
3482
4644
|
}
|
|
3483
|
-
const content =
|
|
4645
|
+
const content = fs23.readFileSync(reviewFile, "utf-8");
|
|
3484
4646
|
if (detectReviewVerdict(content) !== "fail") {
|
|
3485
4647
|
return { ...reviewResult, retries: iteration };
|
|
3486
4648
|
}
|
|
@@ -3509,15 +4671,15 @@ var init_review = __esm({
|
|
|
3509
4671
|
});
|
|
3510
4672
|
|
|
3511
4673
|
// src/stages/ship.ts
|
|
3512
|
-
import * as
|
|
3513
|
-
import * as
|
|
3514
|
-
import { execFileSync as
|
|
4674
|
+
import * as fs24 from "fs";
|
|
4675
|
+
import * as path22 from "path";
|
|
4676
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
3515
4677
|
function buildPrBody(ctx) {
|
|
3516
4678
|
const sections = [];
|
|
3517
|
-
const taskJsonPath =
|
|
3518
|
-
if (
|
|
4679
|
+
const taskJsonPath = path22.join(ctx.taskDir, "task.json");
|
|
4680
|
+
if (fs24.existsSync(taskJsonPath)) {
|
|
3519
4681
|
try {
|
|
3520
|
-
const raw =
|
|
4682
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3521
4683
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3522
4684
|
const task = JSON.parse(cleaned);
|
|
3523
4685
|
if (task.description) {
|
|
@@ -3536,9 +4698,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
3536
4698
|
} catch {
|
|
3537
4699
|
}
|
|
3538
4700
|
}
|
|
3539
|
-
const reviewPath =
|
|
3540
|
-
if (
|
|
3541
|
-
const review =
|
|
4701
|
+
const reviewPath = path22.join(ctx.taskDir, "review.md");
|
|
4702
|
+
if (fs24.existsSync(reviewPath)) {
|
|
4703
|
+
const review = fs24.readFileSync(reviewPath, "utf-8");
|
|
3542
4704
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
3543
4705
|
if (summaryMatch) {
|
|
3544
4706
|
const summary = summaryMatch[1].trim();
|
|
@@ -3555,14 +4717,14 @@ ${summary}`);
|
|
|
3555
4717
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
3556
4718
|
}
|
|
3557
4719
|
}
|
|
3558
|
-
const verifyPath =
|
|
3559
|
-
if (
|
|
3560
|
-
const verify =
|
|
4720
|
+
const verifyPath = path22.join(ctx.taskDir, "verify.md");
|
|
4721
|
+
if (fs24.existsSync(verifyPath)) {
|
|
4722
|
+
const verify = fs24.readFileSync(verifyPath, "utf-8");
|
|
3561
4723
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
3562
4724
|
}
|
|
3563
|
-
const planPath =
|
|
3564
|
-
if (
|
|
3565
|
-
const plan =
|
|
4725
|
+
const planPath = path22.join(ctx.taskDir, "plan.md");
|
|
4726
|
+
if (fs24.existsSync(planPath)) {
|
|
4727
|
+
const plan = fs24.readFileSync(planPath, "utf-8").trim();
|
|
3566
4728
|
if (plan) {
|
|
3567
4729
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
3568
4730
|
sections.push(`
|
|
@@ -3582,25 +4744,25 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
3582
4744
|
return sections.join("\n");
|
|
3583
4745
|
}
|
|
3584
4746
|
function executeShipStage(ctx, _def) {
|
|
3585
|
-
const shipPath =
|
|
4747
|
+
const shipPath = path22.join(ctx.taskDir, "ship.md");
|
|
3586
4748
|
if (ctx.input.dryRun) {
|
|
3587
|
-
|
|
4749
|
+
fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
3588
4750
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3589
4751
|
}
|
|
3590
4752
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
3591
|
-
|
|
4753
|
+
fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
3592
4754
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3593
4755
|
}
|
|
3594
4756
|
try {
|
|
3595
4757
|
const head = getCurrentBranch(ctx.projectDir);
|
|
3596
4758
|
const base = getDefaultBranch(ctx.projectDir);
|
|
3597
4759
|
try {
|
|
3598
|
-
|
|
4760
|
+
execFileSync15("git", ["add", ctx.taskDir], {
|
|
3599
4761
|
cwd: ctx.projectDir,
|
|
3600
4762
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
3601
4763
|
stdio: "pipe"
|
|
3602
4764
|
});
|
|
3603
|
-
|
|
4765
|
+
execFileSync15("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
3604
4766
|
cwd: ctx.projectDir,
|
|
3605
4767
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
3606
4768
|
stdio: "pipe"
|
|
@@ -3614,7 +4776,7 @@ function executeShipStage(ctx, _def) {
|
|
|
3614
4776
|
let repo = config.github?.repo;
|
|
3615
4777
|
if (!owner || !repo) {
|
|
3616
4778
|
try {
|
|
3617
|
-
const remoteUrl =
|
|
4779
|
+
const remoteUrl = execFileSync15("git", ["remote", "get-url", "origin"], {
|
|
3618
4780
|
encoding: "utf-8",
|
|
3619
4781
|
cwd: ctx.projectDir
|
|
3620
4782
|
}).trim();
|
|
@@ -3635,28 +4797,28 @@ function executeShipStage(ctx, _def) {
|
|
|
3635
4797
|
chore: "chore"
|
|
3636
4798
|
};
|
|
3637
4799
|
let prefix = "chore";
|
|
3638
|
-
const taskJsonPath =
|
|
3639
|
-
if (
|
|
4800
|
+
const taskJsonPath = path22.join(ctx.taskDir, "task.json");
|
|
4801
|
+
if (fs24.existsSync(taskJsonPath)) {
|
|
3640
4802
|
try {
|
|
3641
|
-
const raw =
|
|
4803
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3642
4804
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3643
4805
|
const task = JSON.parse(cleaned);
|
|
3644
4806
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
3645
4807
|
} catch {
|
|
3646
4808
|
}
|
|
3647
4809
|
}
|
|
3648
|
-
const taskMdPath =
|
|
3649
|
-
if (
|
|
3650
|
-
const content =
|
|
4810
|
+
const taskMdPath = path22.join(ctx.taskDir, "task.md");
|
|
4811
|
+
if (fs24.existsSync(taskMdPath)) {
|
|
4812
|
+
const content = fs24.readFileSync(taskMdPath, "utf-8");
|
|
3651
4813
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
3652
4814
|
if (heading) {
|
|
3653
4815
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
3654
4816
|
}
|
|
3655
4817
|
}
|
|
3656
4818
|
if (title === "Update") {
|
|
3657
|
-
if (
|
|
4819
|
+
if (fs24.existsSync(taskJsonPath)) {
|
|
3658
4820
|
try {
|
|
3659
|
-
const raw =
|
|
4821
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3660
4822
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3661
4823
|
const task = JSON.parse(cleaned);
|
|
3662
4824
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -3679,7 +4841,7 @@ function executeShipStage(ctx, _def) {
|
|
|
3679
4841
|
} catch {
|
|
3680
4842
|
}
|
|
3681
4843
|
}
|
|
3682
|
-
|
|
4844
|
+
fs24.writeFileSync(shipPath, `# Ship
|
|
3683
4845
|
|
|
3684
4846
|
Updated existing PR: ${existingPr.url}
|
|
3685
4847
|
PR #${existingPr.number}
|
|
@@ -3700,20 +4862,20 @@ PR #${existingPr.number}
|
|
|
3700
4862
|
} catch {
|
|
3701
4863
|
}
|
|
3702
4864
|
}
|
|
3703
|
-
|
|
4865
|
+
fs24.writeFileSync(shipPath, `# Ship
|
|
3704
4866
|
|
|
3705
4867
|
PR created: ${pr.url}
|
|
3706
4868
|
PR #${pr.number}
|
|
3707
4869
|
`);
|
|
3708
4870
|
} else {
|
|
3709
|
-
|
|
4871
|
+
fs24.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
3710
4872
|
}
|
|
3711
4873
|
}
|
|
3712
4874
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3713
4875
|
} catch (err) {
|
|
3714
4876
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3715
4877
|
try {
|
|
3716
|
-
|
|
4878
|
+
fs24.writeFileSync(shipPath, `# Ship
|
|
3717
4879
|
|
|
3718
4880
|
Failed: ${msg}
|
|
3719
4881
|
`);
|
|
@@ -3762,15 +4924,15 @@ var init_executor_registry = __esm({
|
|
|
3762
4924
|
});
|
|
3763
4925
|
|
|
3764
4926
|
// src/pipeline/questions.ts
|
|
3765
|
-
import * as
|
|
3766
|
-
import * as
|
|
4927
|
+
import * as fs25 from "fs";
|
|
4928
|
+
import * as path23 from "path";
|
|
3767
4929
|
function checkForQuestions(ctx, stageName) {
|
|
3768
4930
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
3769
4931
|
try {
|
|
3770
4932
|
if (stageName === "taskify") {
|
|
3771
|
-
const taskJsonPath =
|
|
3772
|
-
if (!
|
|
3773
|
-
const raw =
|
|
4933
|
+
const taskJsonPath = path23.join(ctx.taskDir, "task.json");
|
|
4934
|
+
if (!fs25.existsSync(taskJsonPath)) return false;
|
|
4935
|
+
const raw = fs25.readFileSync(taskJsonPath, "utf-8");
|
|
3774
4936
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3775
4937
|
const taskJson = JSON.parse(cleaned);
|
|
3776
4938
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -3785,9 +4947,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
3785
4947
|
}
|
|
3786
4948
|
}
|
|
3787
4949
|
if (stageName === "plan") {
|
|
3788
|
-
const planPath =
|
|
3789
|
-
if (!
|
|
3790
|
-
const plan =
|
|
4950
|
+
const planPath = path23.join(ctx.taskDir, "plan.md");
|
|
4951
|
+
if (!fs25.existsSync(planPath)) return false;
|
|
4952
|
+
const plan = fs25.readFileSync(planPath, "utf-8");
|
|
3791
4953
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
3792
4954
|
if (questionsMatch) {
|
|
3793
4955
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -3816,8 +4978,8 @@ var init_questions = __esm({
|
|
|
3816
4978
|
});
|
|
3817
4979
|
|
|
3818
4980
|
// src/pipeline/hooks.ts
|
|
3819
|
-
import * as
|
|
3820
|
-
import * as
|
|
4981
|
+
import * as fs26 from "fs";
|
|
4982
|
+
import * as path24 from "path";
|
|
3821
4983
|
function applyPreStageLabel(ctx, def) {
|
|
3822
4984
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
3823
4985
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
@@ -3855,9 +5017,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
3855
5017
|
return { complexity, activeStages };
|
|
3856
5018
|
}
|
|
3857
5019
|
try {
|
|
3858
|
-
const taskJsonPath =
|
|
3859
|
-
if (!
|
|
3860
|
-
const raw =
|
|
5020
|
+
const taskJsonPath = path24.join(ctx.taskDir, "task.json");
|
|
5021
|
+
if (!fs26.existsSync(taskJsonPath)) return null;
|
|
5022
|
+
const raw = fs26.readFileSync(taskJsonPath, "utf-8");
|
|
3861
5023
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3862
5024
|
const taskJson = JSON.parse(cleaned);
|
|
3863
5025
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -3887,8 +5049,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
3887
5049
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
3888
5050
|
if (ctx.input.mode === "rerun") return null;
|
|
3889
5051
|
if (!ctx.input.issueNumber) return null;
|
|
3890
|
-
const planPath =
|
|
3891
|
-
const plan =
|
|
5052
|
+
const planPath = path24.join(ctx.taskDir, "plan.md");
|
|
5053
|
+
const plan = fs26.existsSync(planPath) ? fs26.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
3892
5054
|
try {
|
|
3893
5055
|
postComment(
|
|
3894
5056
|
ctx.input.issueNumber,
|
|
@@ -3955,22 +5117,22 @@ var init_hooks = __esm({
|
|
|
3955
5117
|
});
|
|
3956
5118
|
|
|
3957
5119
|
// src/learning/auto-learn.ts
|
|
3958
|
-
import * as
|
|
3959
|
-
import * as
|
|
5120
|
+
import * as fs27 from "fs";
|
|
5121
|
+
import * as path25 from "path";
|
|
3960
5122
|
function stripAnsi(str) {
|
|
3961
5123
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
3962
5124
|
}
|
|
3963
5125
|
function autoLearn(ctx) {
|
|
3964
5126
|
try {
|
|
3965
|
-
const memoryDir =
|
|
3966
|
-
if (!
|
|
3967
|
-
|
|
5127
|
+
const memoryDir = path25.join(ctx.projectDir, ".kody", "memory");
|
|
5128
|
+
if (!fs27.existsSync(memoryDir)) {
|
|
5129
|
+
fs27.mkdirSync(memoryDir, { recursive: true });
|
|
3968
5130
|
}
|
|
3969
5131
|
const learnings = [];
|
|
3970
5132
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3971
|
-
const verifyPath =
|
|
3972
|
-
if (
|
|
3973
|
-
const verify = stripAnsi(
|
|
5133
|
+
const verifyPath = path25.join(ctx.taskDir, "verify.md");
|
|
5134
|
+
if (fs27.existsSync(verifyPath)) {
|
|
5135
|
+
const verify = stripAnsi(fs27.readFileSync(verifyPath, "utf-8"));
|
|
3974
5136
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
3975
5137
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
3976
5138
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -3979,18 +5141,18 @@ function autoLearn(ctx) {
|
|
|
3979
5141
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
3980
5142
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
3981
5143
|
}
|
|
3982
|
-
const reviewPath =
|
|
3983
|
-
if (
|
|
3984
|
-
const review =
|
|
5144
|
+
const reviewPath = path25.join(ctx.taskDir, "review.md");
|
|
5145
|
+
if (fs27.existsSync(reviewPath)) {
|
|
5146
|
+
const review = fs27.readFileSync(reviewPath, "utf-8");
|
|
3985
5147
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
3986
5148
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
3987
5149
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
3988
5150
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
3989
5151
|
}
|
|
3990
|
-
const taskJsonPath =
|
|
3991
|
-
if (
|
|
5152
|
+
const taskJsonPath = path25.join(ctx.taskDir, "task.json");
|
|
5153
|
+
if (fs27.existsSync(taskJsonPath)) {
|
|
3992
5154
|
try {
|
|
3993
|
-
const raw = stripAnsi(
|
|
5155
|
+
const raw = stripAnsi(fs27.readFileSync(taskJsonPath, "utf-8"));
|
|
3994
5156
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3995
5157
|
const task = JSON.parse(cleaned);
|
|
3996
5158
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -4001,12 +5163,12 @@ function autoLearn(ctx) {
|
|
|
4001
5163
|
}
|
|
4002
5164
|
}
|
|
4003
5165
|
if (learnings.length > 0) {
|
|
4004
|
-
const conventionsPath =
|
|
5166
|
+
const conventionsPath = path25.join(memoryDir, "conventions.md");
|
|
4005
5167
|
const entry = `
|
|
4006
5168
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
4007
5169
|
${learnings.join("\n")}
|
|
4008
5170
|
`;
|
|
4009
|
-
|
|
5171
|
+
fs27.appendFileSync(conventionsPath, entry);
|
|
4010
5172
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
4011
5173
|
}
|
|
4012
5174
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
@@ -4014,8 +5176,8 @@ ${learnings.join("\n")}
|
|
|
4014
5176
|
}
|
|
4015
5177
|
}
|
|
4016
5178
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
4017
|
-
const archPath =
|
|
4018
|
-
if (
|
|
5179
|
+
const archPath = path25.join(memoryDir, "architecture.md");
|
|
5180
|
+
if (fs27.existsSync(archPath)) return;
|
|
4019
5181
|
const detected = detectArchitectureBasic(projectDir);
|
|
4020
5182
|
if (detected.length > 0) {
|
|
4021
5183
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
@@ -4023,7 +5185,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
4023
5185
|
## Overview
|
|
4024
5186
|
${detected.join("\n")}
|
|
4025
5187
|
`;
|
|
4026
|
-
|
|
5188
|
+
fs27.writeFileSync(archPath, content);
|
|
4027
5189
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
4028
5190
|
}
|
|
4029
5191
|
}
|
|
@@ -4036,13 +5198,13 @@ var init_auto_learn = __esm({
|
|
|
4036
5198
|
});
|
|
4037
5199
|
|
|
4038
5200
|
// src/retrospective.ts
|
|
4039
|
-
import * as
|
|
4040
|
-
import * as
|
|
5201
|
+
import * as fs28 from "fs";
|
|
5202
|
+
import * as path26 from "path";
|
|
4041
5203
|
function readArtifact(taskDir, filename, maxChars) {
|
|
4042
|
-
const p =
|
|
4043
|
-
if (!
|
|
5204
|
+
const p = path26.join(taskDir, filename);
|
|
5205
|
+
if (!fs28.existsSync(p)) return null;
|
|
4044
5206
|
try {
|
|
4045
|
-
const content =
|
|
5207
|
+
const content = fs28.readFileSync(p, "utf-8");
|
|
4046
5208
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
4047
5209
|
} catch {
|
|
4048
5210
|
return null;
|
|
@@ -4095,13 +5257,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
4095
5257
|
return lines.join("\n");
|
|
4096
5258
|
}
|
|
4097
5259
|
function getLogPath(projectDir) {
|
|
4098
|
-
return
|
|
5260
|
+
return path26.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
4099
5261
|
}
|
|
4100
5262
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
4101
5263
|
const logPath = getLogPath(projectDir);
|
|
4102
|
-
if (!
|
|
5264
|
+
if (!fs28.existsSync(logPath)) return [];
|
|
4103
5265
|
try {
|
|
4104
|
-
const content =
|
|
5266
|
+
const content = fs28.readFileSync(logPath, "utf-8");
|
|
4105
5267
|
const lines = content.split("\n").filter(Boolean);
|
|
4106
5268
|
const entries = [];
|
|
4107
5269
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -4128,11 +5290,11 @@ function formatPreviousEntries(entries) {
|
|
|
4128
5290
|
}
|
|
4129
5291
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
4130
5292
|
const logPath = getLogPath(projectDir);
|
|
4131
|
-
const dir =
|
|
4132
|
-
if (!
|
|
4133
|
-
|
|
5293
|
+
const dir = path26.dirname(logPath);
|
|
5294
|
+
if (!fs28.existsSync(dir)) {
|
|
5295
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
4134
5296
|
}
|
|
4135
|
-
|
|
5297
|
+
fs28.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4136
5298
|
}
|
|
4137
5299
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
4138
5300
|
if (ctx.input.dryRun) return;
|
|
@@ -4154,7 +5316,7 @@ ${previousText}
|
|
|
4154
5316
|
if (needsLitellmProxy(config)) {
|
|
4155
5317
|
extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
|
|
4156
5318
|
}
|
|
4157
|
-
const
|
|
5319
|
+
const result2 = await runner.run("retrospective", prompt, model, 3e4, "", {
|
|
4158
5320
|
cwd: ctx.projectDir,
|
|
4159
5321
|
env: extraEnv
|
|
4160
5322
|
});
|
|
@@ -4162,8 +5324,8 @@ ${previousText}
|
|
|
4162
5324
|
let patternMatch = null;
|
|
4163
5325
|
let suggestion = "No suggestion";
|
|
4164
5326
|
let pipelineFlaw = null;
|
|
4165
|
-
if (
|
|
4166
|
-
const cleaned =
|
|
5327
|
+
if (result2.outcome === "completed" && result2.output) {
|
|
5328
|
+
const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
4167
5329
|
try {
|
|
4168
5330
|
const parsed = JSON.parse(cleaned);
|
|
4169
5331
|
observation = parsed.observation ?? observation;
|
|
@@ -4300,8 +5462,8 @@ var init_summary = __esm({
|
|
|
4300
5462
|
});
|
|
4301
5463
|
|
|
4302
5464
|
// src/pipeline.ts
|
|
4303
|
-
import * as
|
|
4304
|
-
import * as
|
|
5465
|
+
import * as fs29 from "fs";
|
|
5466
|
+
import * as path27 from "path";
|
|
4305
5467
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
4306
5468
|
if (ctx.input.dryRun) return;
|
|
4307
5469
|
if (ctx.input.prNumber) {
|
|
@@ -4314,8 +5476,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
4314
5476
|
}
|
|
4315
5477
|
if (!ctx.input.issueNumber) return;
|
|
4316
5478
|
try {
|
|
4317
|
-
const taskMdPath =
|
|
4318
|
-
const title =
|
|
5479
|
+
const taskMdPath = path27.join(ctx.taskDir, "task.md");
|
|
5480
|
+
const title = fs29.existsSync(taskMdPath) ? fs29.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
4319
5481
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
4320
5482
|
syncWithDefault(ctx.projectDir);
|
|
4321
5483
|
} catch (err) {
|
|
@@ -4329,10 +5491,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
4329
5491
|
}
|
|
4330
5492
|
}
|
|
4331
5493
|
function acquireLock(taskDir) {
|
|
4332
|
-
const lockPath =
|
|
4333
|
-
if (
|
|
5494
|
+
const lockPath = path27.join(taskDir, ".lock");
|
|
5495
|
+
if (fs29.existsSync(lockPath)) {
|
|
4334
5496
|
try {
|
|
4335
|
-
const pid = parseInt(
|
|
5497
|
+
const pid = parseInt(fs29.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
4336
5498
|
if (!isNaN(pid)) {
|
|
4337
5499
|
try {
|
|
4338
5500
|
process.kill(pid, 0);
|
|
@@ -4349,14 +5511,14 @@ function acquireLock(taskDir) {
|
|
|
4349
5511
|
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
4350
5512
|
}
|
|
4351
5513
|
try {
|
|
4352
|
-
|
|
5514
|
+
fs29.unlinkSync(lockPath);
|
|
4353
5515
|
} catch {
|
|
4354
5516
|
}
|
|
4355
5517
|
}
|
|
4356
5518
|
try {
|
|
4357
|
-
const fd =
|
|
4358
|
-
|
|
4359
|
-
|
|
5519
|
+
const fd = fs29.openSync(lockPath, fs29.constants.O_WRONLY | fs29.constants.O_CREAT | fs29.constants.O_EXCL);
|
|
5520
|
+
fs29.writeSync(fd, String(process.pid));
|
|
5521
|
+
fs29.closeSync(fd);
|
|
4360
5522
|
} catch (err) {
|
|
4361
5523
|
if (err.code === "EEXIST") {
|
|
4362
5524
|
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
@@ -4366,7 +5528,7 @@ function acquireLock(taskDir) {
|
|
|
4366
5528
|
}
|
|
4367
5529
|
function releaseLock(taskDir) {
|
|
4368
5530
|
try {
|
|
4369
|
-
|
|
5531
|
+
fs29.unlinkSync(path27.join(taskDir, ".lock"));
|
|
4370
5532
|
} catch {
|
|
4371
5533
|
}
|
|
4372
5534
|
}
|
|
@@ -4455,23 +5617,23 @@ async function runPipelineInner(ctx) {
|
|
|
4455
5617
|
writeState(state, ctx.taskDir);
|
|
4456
5618
|
logger.info(`[${def.name}] starting...`);
|
|
4457
5619
|
applyPreStageLabel(ctx, def);
|
|
4458
|
-
let
|
|
5620
|
+
let result2;
|
|
4459
5621
|
try {
|
|
4460
|
-
|
|
5622
|
+
result2 = await getExecutor(def.name)(ctx, def);
|
|
4461
5623
|
} catch (error) {
|
|
4462
|
-
|
|
5624
|
+
result2 = {
|
|
4463
5625
|
outcome: "failed",
|
|
4464
5626
|
retries: 0,
|
|
4465
5627
|
error: error instanceof Error ? error.message : String(error)
|
|
4466
5628
|
};
|
|
4467
5629
|
}
|
|
4468
5630
|
ciGroupEnd();
|
|
4469
|
-
if (
|
|
5631
|
+
if (result2.outcome === "completed") {
|
|
4470
5632
|
state.stages[def.name] = {
|
|
4471
5633
|
state: "completed",
|
|
4472
5634
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4473
|
-
retries:
|
|
4474
|
-
outputFile:
|
|
5635
|
+
retries: result2.retries,
|
|
5636
|
+
outputFile: result2.outputFile
|
|
4475
5637
|
};
|
|
4476
5638
|
logger.info(`[${def.name}] \u2713 completed`);
|
|
4477
5639
|
const detected = autoDetectComplexity(ctx, def);
|
|
@@ -4485,16 +5647,16 @@ async function runPipelineInner(ctx) {
|
|
|
4485
5647
|
if (gated) return gated;
|
|
4486
5648
|
commitAfterStage(ctx, def);
|
|
4487
5649
|
} else {
|
|
4488
|
-
const isTimeout =
|
|
5650
|
+
const isTimeout = result2.outcome === "timed_out";
|
|
4489
5651
|
state.stages[def.name] = {
|
|
4490
5652
|
state: isTimeout ? "timeout" : "failed",
|
|
4491
|
-
retries:
|
|
4492
|
-
error: isTimeout ? "Stage timed out" :
|
|
5653
|
+
retries: result2.retries,
|
|
5654
|
+
error: isTimeout ? "Stage timed out" : result2.error ?? "Stage failed"
|
|
4493
5655
|
};
|
|
4494
5656
|
state.state = "failed";
|
|
4495
5657
|
state.sessions = ctx.sessions;
|
|
4496
5658
|
writeState(state, ctx.taskDir);
|
|
4497
|
-
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${
|
|
5659
|
+
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result2.error}`}`);
|
|
4498
5660
|
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
4499
5661
|
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
4500
5662
|
}
|
|
@@ -4574,8 +5736,8 @@ var init_pipeline = __esm({
|
|
|
4574
5736
|
});
|
|
4575
5737
|
|
|
4576
5738
|
// src/preflight.ts
|
|
4577
|
-
import { execFileSync as
|
|
4578
|
-
import * as
|
|
5739
|
+
import { execFileSync as execFileSync16 } from "child_process";
|
|
5740
|
+
import * as fs30 from "fs";
|
|
4579
5741
|
function check(name, fn) {
|
|
4580
5742
|
try {
|
|
4581
5743
|
const detail = fn() ?? void 0;
|
|
@@ -4587,7 +5749,7 @@ function check(name, fn) {
|
|
|
4587
5749
|
function runPreflight() {
|
|
4588
5750
|
const checks = [
|
|
4589
5751
|
check("claude CLI", () => {
|
|
4590
|
-
const v =
|
|
5752
|
+
const v = execFileSync16("claude", ["--version"], {
|
|
4591
5753
|
encoding: "utf-8",
|
|
4592
5754
|
timeout: 1e4,
|
|
4593
5755
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4595,14 +5757,14 @@ function runPreflight() {
|
|
|
4595
5757
|
return v;
|
|
4596
5758
|
}),
|
|
4597
5759
|
check("git repo", () => {
|
|
4598
|
-
|
|
5760
|
+
execFileSync16("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
4599
5761
|
encoding: "utf-8",
|
|
4600
5762
|
timeout: 5e3,
|
|
4601
5763
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4602
5764
|
});
|
|
4603
5765
|
}),
|
|
4604
5766
|
check("pnpm", () => {
|
|
4605
|
-
const v =
|
|
5767
|
+
const v = execFileSync16("pnpm", ["--version"], {
|
|
4606
5768
|
encoding: "utf-8",
|
|
4607
5769
|
timeout: 5e3,
|
|
4608
5770
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4610,7 +5772,7 @@ function runPreflight() {
|
|
|
4610
5772
|
return v;
|
|
4611
5773
|
}),
|
|
4612
5774
|
check("node >= 18", () => {
|
|
4613
|
-
const v =
|
|
5775
|
+
const v = execFileSync16("node", ["--version"], {
|
|
4614
5776
|
encoding: "utf-8",
|
|
4615
5777
|
timeout: 5e3,
|
|
4616
5778
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4620,7 +5782,7 @@ function runPreflight() {
|
|
|
4620
5782
|
return v;
|
|
4621
5783
|
}),
|
|
4622
5784
|
check("gh CLI", () => {
|
|
4623
|
-
const v =
|
|
5785
|
+
const v = execFileSync16("gh", ["--version"], {
|
|
4624
5786
|
encoding: "utf-8",
|
|
4625
5787
|
timeout: 5e3,
|
|
4626
5788
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4628,7 +5790,7 @@ function runPreflight() {
|
|
|
4628
5790
|
return v;
|
|
4629
5791
|
}),
|
|
4630
5792
|
check("package.json", () => {
|
|
4631
|
-
if (!
|
|
5793
|
+
if (!fs30.existsSync("package.json")) throw new Error("not found");
|
|
4632
5794
|
})
|
|
4633
5795
|
];
|
|
4634
5796
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -4705,8 +5867,8 @@ var init_args = __esm({
|
|
|
4705
5867
|
});
|
|
4706
5868
|
|
|
4707
5869
|
// src/cli/task-state.ts
|
|
4708
|
-
import * as
|
|
4709
|
-
import * as
|
|
5870
|
+
import * as fs31 from "fs";
|
|
5871
|
+
import * as path28 from "path";
|
|
4710
5872
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
4711
5873
|
if (!existingTaskId || !existingState) {
|
|
4712
5874
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -4738,11 +5900,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
4738
5900
|
function resolveForIssue(issueNumber, projectDir) {
|
|
4739
5901
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
4740
5902
|
if (existingTaskId) {
|
|
4741
|
-
const statusPath =
|
|
5903
|
+
const statusPath = path28.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
4742
5904
|
let existingState = null;
|
|
4743
|
-
if (
|
|
5905
|
+
if (fs31.existsSync(statusPath)) {
|
|
4744
5906
|
try {
|
|
4745
|
-
existingState = JSON.parse(
|
|
5907
|
+
existingState = JSON.parse(fs31.readFileSync(statusPath, "utf-8"));
|
|
4746
5908
|
} catch {
|
|
4747
5909
|
}
|
|
4748
5910
|
}
|
|
@@ -4775,12 +5937,12 @@ var resolve_exports = {};
|
|
|
4775
5937
|
__export(resolve_exports, {
|
|
4776
5938
|
runResolve: () => runResolve
|
|
4777
5939
|
});
|
|
4778
|
-
import { execFileSync as
|
|
5940
|
+
import { execFileSync as execFileSync17 } from "child_process";
|
|
4779
5941
|
function getConflictContext(cwd, files) {
|
|
4780
5942
|
const parts = [];
|
|
4781
5943
|
for (const file of files.slice(0, 10)) {
|
|
4782
5944
|
try {
|
|
4783
|
-
const content =
|
|
5945
|
+
const content = execFileSync17("git", ["diff", file], {
|
|
4784
5946
|
cwd,
|
|
4785
5947
|
encoding: "utf-8",
|
|
4786
5948
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4830,12 +5992,12 @@ async function runResolve(options) {
|
|
|
4830
5992
|
extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
|
|
4831
5993
|
}
|
|
4832
5994
|
logger.info(` Running agent to resolve conflicts (model=${model})...`);
|
|
4833
|
-
const
|
|
5995
|
+
const result2 = await runner.run("resolve", prompt, model, 3e5, projectDir, {
|
|
4834
5996
|
cwd: projectDir,
|
|
4835
5997
|
env: extraEnv
|
|
4836
5998
|
});
|
|
4837
|
-
if (
|
|
4838
|
-
return { outcome: "failed", error: `Agent failed: ${
|
|
5999
|
+
if (result2.outcome !== "completed") {
|
|
6000
|
+
return { outcome: "failed", error: `Agent failed: ${result2.error}` };
|
|
4839
6001
|
}
|
|
4840
6002
|
logger.info(" Verifying resolution...");
|
|
4841
6003
|
const verify = runQualityGates(projectDir, projectDir);
|
|
@@ -4899,8 +6061,8 @@ var init_resolve = __esm({
|
|
|
4899
6061
|
|
|
4900
6062
|
// src/entry.ts
|
|
4901
6063
|
var entry_exports = {};
|
|
4902
|
-
import * as
|
|
4903
|
-
import * as
|
|
6064
|
+
import * as fs32 from "fs";
|
|
6065
|
+
import * as path29 from "path";
|
|
4904
6066
|
async function ensureLitellmProxy(config, projectDir) {
|
|
4905
6067
|
if (!anyStageNeedsProxy(config)) return null;
|
|
4906
6068
|
const litellmUrl = getLitellmUrl();
|
|
@@ -4935,7 +6097,7 @@ async function ensureLitellmProxy(config, projectDir) {
|
|
|
4935
6097
|
return litellmProcess;
|
|
4936
6098
|
}
|
|
4937
6099
|
async function runModelHealthCheck(config) {
|
|
4938
|
-
const usesProxy =
|
|
6100
|
+
const usesProxy = anyStageNeedsProxy(config);
|
|
4939
6101
|
const baseUrl = usesProxy ? getLitellmUrl() : "https://api.anthropic.com";
|
|
4940
6102
|
const apiKey = usesProxy ? process.env.ANTHROPIC_COMPATIBLE_API_KEY : process.env.ANTHROPIC_API_KEY;
|
|
4941
6103
|
if (!apiKey) {
|
|
@@ -4945,19 +6107,19 @@ async function runModelHealthCheck(config) {
|
|
|
4945
6107
|
}
|
|
4946
6108
|
const model = config.agent.modelMap.cheap;
|
|
4947
6109
|
logger.info(`Model health check (${model} via ${usesProxy ? "LiteLLM" : "Anthropic"})...`);
|
|
4948
|
-
const
|
|
4949
|
-
if (
|
|
6110
|
+
const result2 = await checkModelHealth(baseUrl, apiKey, model);
|
|
6111
|
+
if (result2.ok) {
|
|
4950
6112
|
logger.info(" \u2713 Model responded");
|
|
4951
6113
|
} else {
|
|
4952
|
-
logger.error(` \u2717 Model health check failed: ${
|
|
6114
|
+
logger.error(` \u2717 Model health check failed: ${result2.error}`);
|
|
4953
6115
|
process.exit(1);
|
|
4954
6116
|
}
|
|
4955
6117
|
}
|
|
4956
6118
|
async function main() {
|
|
4957
6119
|
const input = parseArgs();
|
|
4958
|
-
const projectDir = input.cwd ?
|
|
6120
|
+
const projectDir = input.cwd ? path29.resolve(input.cwd) : process.cwd();
|
|
4959
6121
|
if (input.cwd) {
|
|
4960
|
-
if (!
|
|
6122
|
+
if (!fs32.existsSync(projectDir)) {
|
|
4961
6123
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
4962
6124
|
process.exit(1);
|
|
4963
6125
|
}
|
|
@@ -4966,7 +6128,7 @@ async function main() {
|
|
|
4966
6128
|
logger.info(`Working directory: ${projectDir}`);
|
|
4967
6129
|
}
|
|
4968
6130
|
const isPRFix = (input.command === "fix" || input.command === "fix-ci") && !!input.prNumber;
|
|
4969
|
-
const skipStateCheck = input.command === "review" || input.command === "resolve" || input.command === "rerun";
|
|
6131
|
+
const skipStateCheck = input.command === "review" || input.command === "resolve" || input.command === "rerun" || input.command === "status";
|
|
4970
6132
|
if (input.issueNumber && !skipStateCheck && !isPRFix) {
|
|
4971
6133
|
const taskAction = resolveForIssue(input.issueNumber, projectDir);
|
|
4972
6134
|
logger.info(`Task action: ${taskAction.action}`);
|
|
@@ -5023,8 +6185,8 @@ async function main() {
|
|
|
5023
6185
|
process.exit(1);
|
|
5024
6186
|
}
|
|
5025
6187
|
}
|
|
5026
|
-
const taskDir =
|
|
5027
|
-
|
|
6188
|
+
const taskDir = path29.join(projectDir, ".kody", "tasks", taskId);
|
|
6189
|
+
fs32.mkdirSync(taskDir, { recursive: true });
|
|
5028
6190
|
if (input.command === "rerun" && isTaskifyRun(taskDir)) {
|
|
5029
6191
|
const marker = readTaskifyMarker(taskDir);
|
|
5030
6192
|
if (marker) {
|
|
@@ -5089,7 +6251,7 @@ async function main() {
|
|
|
5089
6251
|
console.error(`Runner "${defaultRunnerName2}" health check failed`);
|
|
5090
6252
|
process.exit(1);
|
|
5091
6253
|
}
|
|
5092
|
-
const
|
|
6254
|
+
const result2 = await runStandaloneReview({
|
|
5093
6255
|
projectDir,
|
|
5094
6256
|
runners: runners2,
|
|
5095
6257
|
prTitle,
|
|
@@ -5099,15 +6261,15 @@ async function main() {
|
|
|
5099
6261
|
taskId
|
|
5100
6262
|
});
|
|
5101
6263
|
if (litellmProcess2) litellmProcess2.kill();
|
|
5102
|
-
if (
|
|
5103
|
-
console.error(`Review failed: ${
|
|
6264
|
+
if (result2.outcome === "failed") {
|
|
6265
|
+
console.error(`Review failed: ${result2.error}`);
|
|
5104
6266
|
process.exit(1);
|
|
5105
6267
|
}
|
|
5106
|
-
if (
|
|
5107
|
-
console.log(
|
|
6268
|
+
if (result2.reviewContent) {
|
|
6269
|
+
console.log(result2.reviewContent);
|
|
5108
6270
|
if (!input.local && prNumber) {
|
|
5109
|
-
const comment = formatReviewComment(
|
|
5110
|
-
const verdict = detectReviewVerdict(
|
|
6271
|
+
const comment = formatReviewComment(result2.reviewContent, taskId);
|
|
6272
|
+
const verdict = detectReviewVerdict(result2.reviewContent);
|
|
5111
6273
|
const event = verdict === "fail" ? "request-changes" : "approve";
|
|
5112
6274
|
const posted = submitPRReview(prNumber, comment, event);
|
|
5113
6275
|
if (!posted) {
|
|
@@ -5139,48 +6301,48 @@ async function main() {
|
|
|
5139
6301
|
process.exit(1);
|
|
5140
6302
|
}
|
|
5141
6303
|
const { runResolve: runResolve2 } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
|
|
5142
|
-
const
|
|
6304
|
+
const result2 = await runResolve2({
|
|
5143
6305
|
prNumber: input.prNumber,
|
|
5144
6306
|
projectDir,
|
|
5145
6307
|
runners: runners2,
|
|
5146
6308
|
local: input.local ?? true
|
|
5147
6309
|
});
|
|
5148
6310
|
if (litellmProcess2) litellmProcess2.kill();
|
|
5149
|
-
if (
|
|
5150
|
-
console.error(`Resolve failed: ${
|
|
6311
|
+
if (result2.outcome === "failed") {
|
|
6312
|
+
console.error(`Resolve failed: ${result2.error}`);
|
|
5151
6313
|
process.exit(1);
|
|
5152
6314
|
}
|
|
5153
|
-
console.log(`Resolve: ${
|
|
6315
|
+
console.log(`Resolve: ${result2.outcome}`);
|
|
5154
6316
|
process.exit(0);
|
|
5155
6317
|
}
|
|
5156
6318
|
logger.info("Preflight checks:");
|
|
5157
6319
|
runPreflight();
|
|
5158
6320
|
if (input.task) {
|
|
5159
|
-
|
|
6321
|
+
fs32.writeFileSync(path29.join(taskDir, "task.md"), input.task);
|
|
5160
6322
|
}
|
|
5161
|
-
const taskMdPath =
|
|
5162
|
-
if (!
|
|
6323
|
+
const taskMdPath = path29.join(taskDir, "task.md");
|
|
6324
|
+
if (!fs32.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
5163
6325
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
5164
6326
|
const prDetails = getPRDetails(input.prNumber);
|
|
5165
6327
|
if (prDetails) {
|
|
5166
6328
|
const taskContent = `# ${prDetails.title}
|
|
5167
6329
|
|
|
5168
6330
|
${prDetails.body ?? ""}`;
|
|
5169
|
-
|
|
6331
|
+
fs32.writeFileSync(taskMdPath, taskContent);
|
|
5170
6332
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
5171
6333
|
}
|
|
5172
|
-
} else if (!
|
|
6334
|
+
} else if (!fs32.existsSync(taskMdPath) && input.issueNumber) {
|
|
5173
6335
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
5174
6336
|
const issue = getIssue(input.issueNumber);
|
|
5175
6337
|
if (issue) {
|
|
5176
6338
|
const taskContent = `# ${issue.title}
|
|
5177
6339
|
|
|
5178
6340
|
${issue.body ?? ""}`;
|
|
5179
|
-
|
|
6341
|
+
fs32.writeFileSync(taskMdPath, taskContent);
|
|
5180
6342
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
5181
6343
|
}
|
|
5182
6344
|
}
|
|
5183
|
-
if (!
|
|
6345
|
+
if (!fs32.existsSync(taskMdPath)) {
|
|
5184
6346
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
5185
6347
|
process.exit(1);
|
|
5186
6348
|
}
|
|
@@ -5318,7 +6480,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
5318
6480
|
}
|
|
5319
6481
|
}
|
|
5320
6482
|
const state = await runPipeline(ctx);
|
|
5321
|
-
const files =
|
|
6483
|
+
const files = fs32.readdirSync(taskDir);
|
|
5322
6484
|
console.log(`
|
|
5323
6485
|
Artifacts in ${taskDir}:`);
|
|
5324
6486
|
for (const f of files) {
|
|
@@ -5383,8 +6545,8 @@ var init_entry = __esm({
|
|
|
5383
6545
|
});
|
|
5384
6546
|
|
|
5385
6547
|
// src/bin/cli.ts
|
|
5386
|
-
import * as
|
|
5387
|
-
import * as
|
|
6548
|
+
import * as fs33 from "fs";
|
|
6549
|
+
import * as path30 from "path";
|
|
5388
6550
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5389
6551
|
|
|
5390
6552
|
// src/bin/commands/init.ts
|
|
@@ -5556,7 +6718,7 @@ function buildConfig(cwd, basic) {
|
|
|
5556
6718
|
github: { owner: basic.owner, repo: basic.repo },
|
|
5557
6719
|
agent: {
|
|
5558
6720
|
provider: "anthropic",
|
|
5559
|
-
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
6721
|
+
modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
|
|
5560
6722
|
}
|
|
5561
6723
|
};
|
|
5562
6724
|
const mcp = detectMcpConfig(cwd, basic.pm, pkg);
|
|
@@ -5756,15 +6918,15 @@ function initCommand(opts, pkgRoot) {
|
|
|
5756
6918
|
|
|
5757
6919
|
// src/bin/commands/bootstrap.ts
|
|
5758
6920
|
init_architecture_detection();
|
|
5759
|
-
import * as
|
|
5760
|
-
import * as
|
|
6921
|
+
import * as fs8 from "fs";
|
|
6922
|
+
import * as path7 from "path";
|
|
5761
6923
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
5762
6924
|
|
|
5763
6925
|
// src/bin/qa-guide.ts
|
|
5764
6926
|
import * as fs5 from "fs";
|
|
5765
6927
|
import * as path4 from "path";
|
|
5766
6928
|
function discoverQaContext(cwd) {
|
|
5767
|
-
const
|
|
6929
|
+
const result2 = {
|
|
5768
6930
|
routes: [],
|
|
5769
6931
|
authFiles: [],
|
|
5770
6932
|
loginPage: null,
|
|
@@ -5777,21 +6939,21 @@ function discoverQaContext(cwd) {
|
|
|
5777
6939
|
const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
|
|
5778
6940
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5779
6941
|
const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
5780
|
-
if (pkg.scripts?.dev)
|
|
5781
|
-
if (allDeps.next || allDeps.nuxt)
|
|
5782
|
-
else if (allDeps.vite)
|
|
6942
|
+
if (pkg.scripts?.dev) result2.devCommand = `${pm} dev`;
|
|
6943
|
+
if (allDeps.next || allDeps.nuxt) result2.devPort = 3e3;
|
|
6944
|
+
else if (allDeps.vite) result2.devPort = 5173;
|
|
5783
6945
|
} catch {
|
|
5784
6946
|
}
|
|
5785
6947
|
const appDirs = ["src/app", "app"];
|
|
5786
6948
|
for (const appDir of appDirs) {
|
|
5787
6949
|
const fullAppDir = path4.join(cwd, appDir);
|
|
5788
6950
|
if (!fs5.existsSync(fullAppDir)) continue;
|
|
5789
|
-
scanRoutes(fullAppDir, appDir, "",
|
|
6951
|
+
scanRoutes(fullAppDir, appDir, "", result2);
|
|
5790
6952
|
break;
|
|
5791
6953
|
}
|
|
5792
6954
|
const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
5793
6955
|
for (const p of authPatterns) {
|
|
5794
|
-
if (fs5.existsSync(path4.join(cwd, p)))
|
|
6956
|
+
if (fs5.existsSync(path4.join(cwd, p))) result2.authFiles.push(p);
|
|
5795
6957
|
}
|
|
5796
6958
|
const authConfigGlobs = [
|
|
5797
6959
|
"src/app/api/auth",
|
|
@@ -5802,7 +6964,7 @@ function discoverQaContext(cwd) {
|
|
|
5802
6964
|
"src/app/api/oauth"
|
|
5803
6965
|
];
|
|
5804
6966
|
for (const g of authConfigGlobs) {
|
|
5805
|
-
if (fs5.existsSync(path4.join(cwd, g)))
|
|
6967
|
+
if (fs5.existsSync(path4.join(cwd, g))) result2.authFiles.push(g);
|
|
5806
6968
|
}
|
|
5807
6969
|
try {
|
|
5808
6970
|
const rolePaths = [
|
|
@@ -5824,7 +6986,7 @@ function discoverQaContext(cwd) {
|
|
|
5824
6986
|
if (roleMatches) {
|
|
5825
6987
|
for (const m of roleMatches) {
|
|
5826
6988
|
const val = m.match(/['"](\w+)['"]/);
|
|
5827
|
-
if (val && !
|
|
6989
|
+
if (val && !result2.roles.includes(val[1])) result2.roles.push(val[1]);
|
|
5828
6990
|
}
|
|
5829
6991
|
}
|
|
5830
6992
|
const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
|
|
@@ -5833,7 +6995,7 @@ function discoverQaContext(cwd) {
|
|
|
5833
6995
|
if (vals) {
|
|
5834
6996
|
for (const v of vals) {
|
|
5835
6997
|
const clean = v.replace(/['"]/g, "");
|
|
5836
|
-
if (!
|
|
6998
|
+
if (!result2.roles.includes(clean)) result2.roles.push(clean);
|
|
5837
6999
|
}
|
|
5838
7000
|
}
|
|
5839
7001
|
}
|
|
@@ -5843,9 +7005,9 @@ function discoverQaContext(cwd) {
|
|
|
5843
7005
|
}
|
|
5844
7006
|
} catch {
|
|
5845
7007
|
}
|
|
5846
|
-
return
|
|
7008
|
+
return result2;
|
|
5847
7009
|
}
|
|
5848
|
-
function scanRoutes(dir, baseDir, prefix,
|
|
7010
|
+
function scanRoutes(dir, baseDir, prefix, result2) {
|
|
5849
7011
|
let entries;
|
|
5850
7012
|
try {
|
|
5851
7013
|
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
@@ -5856,16 +7018,16 @@ function scanRoutes(dir, baseDir, prefix, result) {
|
|
|
5856
7018
|
if (hasPage) {
|
|
5857
7019
|
const routePath = prefix || "/";
|
|
5858
7020
|
const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
|
|
5859
|
-
|
|
5860
|
-
if (prefix.includes("/login"))
|
|
5861
|
-
if (prefix.startsWith("/admin") && !
|
|
7021
|
+
result2.routes.push({ path: routePath, group });
|
|
7022
|
+
if (prefix.includes("/login")) result2.loginPage = routePath;
|
|
7023
|
+
if (prefix.startsWith("/admin") && !result2.adminPath) result2.adminPath = prefix;
|
|
5862
7024
|
}
|
|
5863
7025
|
for (const entry of entries) {
|
|
5864
7026
|
if (!entry.isDirectory()) continue;
|
|
5865
7027
|
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
5866
7028
|
let segment = entry.name;
|
|
5867
7029
|
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
5868
|
-
scanRoutes(path4.join(dir, entry.name), baseDir, prefix,
|
|
7030
|
+
scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result2);
|
|
5869
7031
|
continue;
|
|
5870
7032
|
}
|
|
5871
7033
|
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
@@ -5874,7 +7036,7 @@ function scanRoutes(dir, baseDir, prefix, result) {
|
|
|
5874
7036
|
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
5875
7037
|
segment = `:${segment.slice(2, -2)}?`;
|
|
5876
7038
|
}
|
|
5877
|
-
scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`,
|
|
7039
|
+
scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result2);
|
|
5878
7040
|
}
|
|
5879
7041
|
}
|
|
5880
7042
|
function generateQaGuide(discovery) {
|
|
@@ -6031,22 +7193,23 @@ function installSkillsForProject(cwd) {
|
|
|
6031
7193
|
}
|
|
6032
7194
|
|
|
6033
7195
|
// src/bin/commands/bootstrap.ts
|
|
7196
|
+
init_config();
|
|
6034
7197
|
var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
|
|
6035
7198
|
function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
6036
|
-
const srcDir =
|
|
6037
|
-
const baseDir =
|
|
7199
|
+
const srcDir = path7.join(cwd, "src");
|
|
7200
|
+
const baseDir = fs8.existsSync(srcDir) ? srcDir : cwd;
|
|
6038
7201
|
const results = [];
|
|
6039
7202
|
function walk(dir) {
|
|
6040
7203
|
const entries = [];
|
|
6041
7204
|
try {
|
|
6042
|
-
for (const entry of
|
|
7205
|
+
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
6043
7206
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
6044
|
-
const full =
|
|
7207
|
+
const full = path7.join(dir, entry.name);
|
|
6045
7208
|
if (entry.isDirectory()) {
|
|
6046
7209
|
entries.push(...walk(full));
|
|
6047
7210
|
} else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
|
|
6048
7211
|
try {
|
|
6049
|
-
const stat =
|
|
7212
|
+
const stat = fs8.statSync(full);
|
|
6050
7213
|
if (stat.size >= 200 && stat.size <= 5e3) {
|
|
6051
7214
|
entries.push({ filePath: full, size: stat.size });
|
|
6052
7215
|
}
|
|
@@ -6060,8 +7223,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
|
6060
7223
|
}
|
|
6061
7224
|
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
6062
7225
|
for (const { filePath } of files) {
|
|
6063
|
-
const rel =
|
|
6064
|
-
const content =
|
|
7226
|
+
const rel = path7.relative(cwd, filePath);
|
|
7227
|
+
const content = fs8.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
6065
7228
|
results.push(`### File: ${rel}
|
|
6066
7229
|
\`\`\`typescript
|
|
6067
7230
|
${content}
|
|
@@ -6073,9 +7236,9 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
6073
7236
|
try {
|
|
6074
7237
|
let repoSlug = "";
|
|
6075
7238
|
try {
|
|
6076
|
-
const configPath =
|
|
6077
|
-
if (
|
|
6078
|
-
const config = JSON.parse(
|
|
7239
|
+
const configPath = path7.join(cwd, "kody.config.json");
|
|
7240
|
+
if (fs8.existsSync(configPath)) {
|
|
7241
|
+
const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
6079
7242
|
if (config.github?.owner && config.github?.repo) {
|
|
6080
7243
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
6081
7244
|
}
|
|
@@ -6102,7 +7265,9 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
6102
7265
|
}
|
|
6103
7266
|
function bootstrapCommand(opts, pkgRoot) {
|
|
6104
7267
|
const cwd = process.cwd();
|
|
7268
|
+
setConfigDir(cwd);
|
|
6105
7269
|
const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
|
|
7270
|
+
const bootstrapModel = resolveStageConfig(getProjectConfig(), "bootstrap", "cheap").model;
|
|
6106
7271
|
console.log(`
|
|
6107
7272
|
\u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
|
|
6108
7273
|
`);
|
|
@@ -6110,8 +7275,8 @@ function bootstrapCommand(opts, pkgRoot) {
|
|
|
6110
7275
|
ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
|
|
6111
7276
|
}
|
|
6112
7277
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
6113
|
-
const p =
|
|
6114
|
-
if (
|
|
7278
|
+
const p = path7.join(cwd, rel);
|
|
7279
|
+
if (fs8.existsSync(p)) return fs8.readFileSync(p, "utf-8").slice(0, maxChars);
|
|
6115
7280
|
return null;
|
|
6116
7281
|
};
|
|
6117
7282
|
let repoContext = "";
|
|
@@ -6146,14 +7311,14 @@ ${sampleFiles}
|
|
|
6146
7311
|
|
|
6147
7312
|
`;
|
|
6148
7313
|
try {
|
|
6149
|
-
const topDirs =
|
|
7314
|
+
const topDirs = fs8.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
6150
7315
|
repoContext += `## Top-level directories
|
|
6151
7316
|
${topDirs.join(", ")}
|
|
6152
7317
|
|
|
6153
7318
|
`;
|
|
6154
|
-
const srcDir =
|
|
6155
|
-
if (
|
|
6156
|
-
const srcDirs =
|
|
7319
|
+
const srcDir = path7.join(cwd, "src");
|
|
7320
|
+
if (fs8.existsSync(srcDir)) {
|
|
7321
|
+
const srcDirs = fs8.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6157
7322
|
if (srcDirs.length > 0) repoContext += `## src/ subdirectories
|
|
6158
7323
|
${srcDirs.join(", ")}
|
|
6159
7324
|
|
|
@@ -6163,19 +7328,19 @@ ${srcDirs.join(", ")}
|
|
|
6163
7328
|
}
|
|
6164
7329
|
const existingFiles = [];
|
|
6165
7330
|
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"]) {
|
|
6166
|
-
if (
|
|
7331
|
+
if (fs8.existsSync(path7.join(cwd, f))) existingFiles.push(f);
|
|
6167
7332
|
}
|
|
6168
7333
|
if (existingFiles.length) repoContext += `## Config files present
|
|
6169
7334
|
${existingFiles.join(", ")}
|
|
6170
7335
|
|
|
6171
7336
|
`;
|
|
6172
7337
|
console.log("\u2500\u2500 Project Memory \u2500\u2500");
|
|
6173
|
-
const memoryDir =
|
|
6174
|
-
|
|
6175
|
-
const archPath =
|
|
6176
|
-
const conventionsPath =
|
|
6177
|
-
const existingArch =
|
|
6178
|
-
const existingConv =
|
|
7338
|
+
const memoryDir = path7.join(cwd, ".kody", "memory");
|
|
7339
|
+
fs8.mkdirSync(memoryDir, { recursive: true });
|
|
7340
|
+
const archPath = path7.join(memoryDir, "architecture.md");
|
|
7341
|
+
const conventionsPath = path7.join(memoryDir, "conventions.md");
|
|
7342
|
+
const existingArch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
|
|
7343
|
+
const existingConv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
|
|
6179
7344
|
const hasExisting = !!(existingArch || existingConv);
|
|
6180
7345
|
const extendInstruction = hasExisting && !opts.force ? `
|
|
6181
7346
|
## Existing Documentation (EXTEND, do not replace)
|
|
@@ -6219,7 +7384,7 @@ ${repoContext}`;
|
|
|
6219
7384
|
const output = execFileSync5("claude", [
|
|
6220
7385
|
"--print",
|
|
6221
7386
|
"--model",
|
|
6222
|
-
|
|
7387
|
+
bootstrapModel,
|
|
6223
7388
|
"--dangerously-skip-permissions",
|
|
6224
7389
|
memoryPrompt
|
|
6225
7390
|
], {
|
|
@@ -6231,12 +7396,12 @@ ${repoContext}`;
|
|
|
6231
7396
|
const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
6232
7397
|
const parsed = JSON.parse(cleaned);
|
|
6233
7398
|
if (parsed.architecture) {
|
|
6234
|
-
|
|
7399
|
+
fs8.writeFileSync(archPath, parsed.architecture);
|
|
6235
7400
|
const lineCount = parsed.architecture.split("\n").length;
|
|
6236
7401
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
|
|
6237
7402
|
}
|
|
6238
7403
|
if (parsed.conventions) {
|
|
6239
|
-
|
|
7404
|
+
fs8.writeFileSync(conventionsPath, parsed.conventions);
|
|
6240
7405
|
const lineCount = parsed.conventions.split("\n").length;
|
|
6241
7406
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
|
|
6242
7407
|
}
|
|
@@ -6245,39 +7410,39 @@ ${repoContext}`;
|
|
|
6245
7410
|
const detected = detectArchitectureBasic(cwd);
|
|
6246
7411
|
if (detected.length > 0) {
|
|
6247
7412
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6248
|
-
|
|
7413
|
+
fs8.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
6249
7414
|
|
|
6250
7415
|
## Overview
|
|
6251
7416
|
${detected.join("\n")}
|
|
6252
7417
|
`);
|
|
6253
7418
|
console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
|
|
6254
7419
|
}
|
|
6255
|
-
|
|
7420
|
+
fs8.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
6256
7421
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
6257
7422
|
}
|
|
6258
7423
|
console.log("\n\u2500\u2500 Step Files \u2500\u2500");
|
|
6259
|
-
const stepsDir =
|
|
6260
|
-
|
|
6261
|
-
const arch =
|
|
6262
|
-
const conv =
|
|
7424
|
+
const stepsDir = path7.join(cwd, ".kody", "steps");
|
|
7425
|
+
fs8.mkdirSync(stepsDir, { recursive: true });
|
|
7426
|
+
const arch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
|
|
7427
|
+
const conv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
|
|
6263
7428
|
console.log(" \u23F3 Customizing step files...");
|
|
6264
7429
|
let stepCount = 0;
|
|
6265
7430
|
for (const stage of STEP_STAGES) {
|
|
6266
|
-
const templatePath =
|
|
6267
|
-
if (!
|
|
7431
|
+
const templatePath = path7.join(pkgRoot, "prompts", `${stage}.md`);
|
|
7432
|
+
if (!fs8.existsSync(templatePath)) {
|
|
6268
7433
|
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
6269
7434
|
continue;
|
|
6270
7435
|
}
|
|
6271
|
-
const stepOutputPath =
|
|
6272
|
-
if (
|
|
7436
|
+
const stepOutputPath = path7.join(stepsDir, `${stage}.md`);
|
|
7437
|
+
if (fs8.existsSync(stepOutputPath) && !opts.force) {
|
|
6273
7438
|
console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
|
|
6274
7439
|
continue;
|
|
6275
7440
|
}
|
|
6276
|
-
const defaultPrompt =
|
|
7441
|
+
const defaultPrompt = fs8.readFileSync(templatePath, "utf-8");
|
|
6277
7442
|
const contextPlaceholder = "{{TASK_CONTEXT}}";
|
|
6278
7443
|
const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
|
|
6279
7444
|
if (placeholderIdx === -1) {
|
|
6280
|
-
|
|
7445
|
+
fs8.copyFileSync(templatePath, stepOutputPath);
|
|
6281
7446
|
stepCount++;
|
|
6282
7447
|
console.log(` \u2713 ${stage}.md`);
|
|
6283
7448
|
continue;
|
|
@@ -6322,7 +7487,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6322
7487
|
const output = execFileSync5("claude", [
|
|
6323
7488
|
"--print",
|
|
6324
7489
|
"--model",
|
|
6325
|
-
|
|
7490
|
+
bootstrapModel,
|
|
6326
7491
|
"--dangerously-skip-permissions",
|
|
6327
7492
|
customizationPrompt
|
|
6328
7493
|
], {
|
|
@@ -6334,23 +7499,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6334
7499
|
let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
6335
7500
|
cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
|
|
6336
7501
|
const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
|
|
6337
|
-
|
|
7502
|
+
fs8.writeFileSync(stepOutputPath, finalPrompt);
|
|
6338
7503
|
stepCount++;
|
|
6339
7504
|
console.log(` \u2713 ${stage}.md`);
|
|
6340
7505
|
} catch {
|
|
6341
7506
|
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
6342
|
-
|
|
7507
|
+
fs8.copyFileSync(templatePath, stepOutputPath);
|
|
6343
7508
|
stepCount++;
|
|
6344
7509
|
}
|
|
6345
7510
|
}
|
|
6346
7511
|
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
6347
7512
|
console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
|
|
6348
|
-
const qaGuidePath =
|
|
6349
|
-
if (!
|
|
7513
|
+
const qaGuidePath = path7.join(cwd, ".kody", "qa-guide.md");
|
|
7514
|
+
if (!fs8.existsSync(qaGuidePath) || opts.force) {
|
|
6350
7515
|
const discovery = discoverQaContext(cwd);
|
|
6351
7516
|
if (discovery.routes.length > 0) {
|
|
6352
7517
|
const qaGuide = generateQaGuide(discovery);
|
|
6353
|
-
|
|
7518
|
+
fs8.writeFileSync(qaGuidePath, qaGuide);
|
|
6354
7519
|
console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
|
|
6355
7520
|
if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
|
|
6356
7521
|
if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
|
|
@@ -6365,9 +7530,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6365
7530
|
try {
|
|
6366
7531
|
let repoSlug = "";
|
|
6367
7532
|
try {
|
|
6368
|
-
const configPath =
|
|
6369
|
-
if (
|
|
6370
|
-
const config = JSON.parse(
|
|
7533
|
+
const configPath = path7.join(cwd, "kody.config.json");
|
|
7534
|
+
if (fs8.existsSync(configPath)) {
|
|
7535
|
+
const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
6371
7536
|
if (config.github?.owner && config.github?.repo) {
|
|
6372
7537
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
6373
7538
|
}
|
|
@@ -6440,19 +7605,19 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6440
7605
|
".kody/memory/conventions.md",
|
|
6441
7606
|
".kody/qa-guide.md",
|
|
6442
7607
|
...installedSkillPaths
|
|
6443
|
-
].filter((f) =>
|
|
6444
|
-
if (
|
|
7608
|
+
].filter((f) => fs8.existsSync(path7.join(cwd, f)));
|
|
7609
|
+
if (fs8.existsSync(path7.join(cwd, "skills-lock.json"))) {
|
|
6445
7610
|
filesToCommit.push("skills-lock.json");
|
|
6446
7611
|
}
|
|
6447
7612
|
for (const stage of STEP_STAGES) {
|
|
6448
7613
|
const stepFile = `.kody/steps/${stage}.md`;
|
|
6449
|
-
if (
|
|
7614
|
+
if (fs8.existsSync(path7.join(cwd, stepFile))) {
|
|
6450
7615
|
filesToCommit.push(stepFile);
|
|
6451
7616
|
}
|
|
6452
7617
|
}
|
|
6453
7618
|
if (filesToCommit.length > 0) {
|
|
6454
7619
|
try {
|
|
6455
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
7620
|
+
const fullPaths = filesToCommit.map((f) => path7.join(cwd, f));
|
|
6456
7621
|
for (let pass = 0; pass < 2; pass++) {
|
|
6457
7622
|
execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
|
|
6458
7623
|
cwd,
|
|
@@ -6479,9 +7644,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6479
7644
|
console.log(` \u2713 Pushed branch: ${branchName}`);
|
|
6480
7645
|
let baseBranch = "main";
|
|
6481
7646
|
try {
|
|
6482
|
-
const configPath =
|
|
6483
|
-
if (
|
|
6484
|
-
const config = JSON.parse(
|
|
7647
|
+
const configPath = path7.join(cwd, "kody.config.json");
|
|
7648
|
+
if (fs8.existsSync(configPath)) {
|
|
7649
|
+
const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
6485
7650
|
baseBranch = config.git?.defaultBranch ?? "main";
|
|
6486
7651
|
}
|
|
6487
7652
|
} catch {
|
|
@@ -6555,11 +7720,11 @@ Create it manually.`, cwd);
|
|
|
6555
7720
|
|
|
6556
7721
|
// src/bin/cli.ts
|
|
6557
7722
|
init_architecture_detection();
|
|
6558
|
-
var __dirname2 =
|
|
6559
|
-
var PKG_ROOT =
|
|
7723
|
+
var __dirname2 = path30.dirname(fileURLToPath2(import.meta.url));
|
|
7724
|
+
var PKG_ROOT = path30.resolve(__dirname2, "..", "..");
|
|
6560
7725
|
function getVersion() {
|
|
6561
|
-
const pkgPath =
|
|
6562
|
-
const pkg = JSON.parse(
|
|
7726
|
+
const pkgPath = path30.join(PKG_ROOT, "package.json");
|
|
7727
|
+
const pkg = JSON.parse(fs33.readFileSync(pkgPath, "utf-8"));
|
|
6563
7728
|
return pkg.version;
|
|
6564
7729
|
}
|
|
6565
7730
|
var args = process.argv.slice(2);
|
|
@@ -6570,6 +7735,8 @@ if (command === "init") {
|
|
|
6570
7735
|
bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
6571
7736
|
} else if (command === "taskify") {
|
|
6572
7737
|
Promise.resolve().then(() => (init_taskify_command(), taskify_command_exports)).then(({ runTaskifyCommand: runTaskifyCommand2 }) => runTaskifyCommand2());
|
|
7738
|
+
} else if (command === "test-model") {
|
|
7739
|
+
Promise.resolve().then(() => (init_test_model_command(), test_model_command_exports)).then(({ runTestModelCommand: runTestModelCommand2 }) => runTestModelCommand2());
|
|
6573
7740
|
} else if (command === "ci-parse") {
|
|
6574
7741
|
Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
|
|
6575
7742
|
} else if (command === "version" || command === "--version" || command === "-v") {
|