@kody-ade/kody-engine-lite 0.1.112 → 0.1.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-runner.d.ts +4 -0
- package/dist/agent-runner.js +122 -0
- package/dist/bin/cli.js +1436 -429
- package/dist/ci/parse-inputs.d.ts +6 -0
- package/dist/ci/parse-inputs.js +76 -0
- package/dist/ci/parse-safety.d.ts +6 -0
- package/dist/ci/parse-safety.js +22 -0
- package/dist/cli/args.d.ts +13 -0
- package/dist/cli/args.js +42 -0
- package/dist/cli/litellm.d.ts +2 -0
- package/dist/cli/litellm.js +85 -0
- package/dist/cli/task-resolution.d.ts +2 -0
- package/dist/cli/task-resolution.js +41 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +72 -0
- package/dist/context.d.ts +4 -0
- package/dist/context.js +83 -0
- package/dist/definitions.d.ts +3 -0
- package/dist/definitions.js +59 -0
- package/dist/entry.d.ts +1 -0
- package/dist/entry.js +236 -0
- package/dist/git-utils.d.ts +13 -0
- package/dist/git-utils.js +174 -0
- package/dist/github-api.d.ts +14 -0
- package/dist/github-api.js +114 -0
- package/dist/kody-utils.d.ts +1 -0
- package/dist/kody-utils.js +9 -0
- package/dist/learning/auto-learn.d.ts +2 -0
- package/dist/learning/auto-learn.js +169 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.js +51 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.js +20 -0
- package/dist/observer.d.ts +9 -0
- package/dist/observer.js +80 -0
- package/dist/pipeline/complexity.d.ts +3 -0
- package/dist/pipeline/complexity.js +12 -0
- package/dist/pipeline/executor-registry.d.ts +3 -0
- package/dist/pipeline/executor-registry.js +20 -0
- package/dist/pipeline/hooks.d.ts +17 -0
- package/dist/pipeline/hooks.js +110 -0
- package/dist/pipeline/questions.d.ts +2 -0
- package/dist/pipeline/questions.js +44 -0
- package/dist/pipeline/runner-selection.d.ts +2 -0
- package/dist/pipeline/runner-selection.js +13 -0
- package/dist/pipeline/state.d.ts +4 -0
- package/dist/pipeline/state.js +37 -0
- package/dist/pipeline.d.ts +3 -0
- package/dist/pipeline.js +213 -0
- package/dist/preflight.d.ts +1 -0
- package/dist/preflight.js +69 -0
- package/dist/retrospective.d.ts +26 -0
- package/dist/retrospective.js +211 -0
- package/dist/stages/agent.d.ts +2 -0
- package/dist/stages/agent.js +94 -0
- package/dist/stages/gate.d.ts +2 -0
- package/dist/stages/gate.js +32 -0
- package/dist/stages/review.d.ts +2 -0
- package/dist/stages/review.js +32 -0
- package/dist/stages/ship.d.ts +3 -0
- package/dist/stages/ship.js +154 -0
- package/dist/stages/verify.d.ts +2 -0
- package/dist/stages/verify.js +94 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +1 -0
- package/dist/validators.d.ts +8 -0
- package/dist/validators.js +42 -0
- package/dist/verify-runner.d.ts +11 -0
- package/dist/verify-runner.js +110 -0
- package/kody.config.schema.json +31 -0
- package/package.json +1 -1
- 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;
|
|
@@ -223,16 +223,16 @@ function setConfigDir(dir) {
|
|
|
223
223
|
}
|
|
224
224
|
function getProjectConfig() {
|
|
225
225
|
if (_config) return _config;
|
|
226
|
-
const configPath =
|
|
227
|
-
if (
|
|
226
|
+
const configPath = path6.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
227
|
+
if (fs7.existsSync(configPath)) {
|
|
228
228
|
try {
|
|
229
|
-
const
|
|
230
|
-
if (!
|
|
231
|
-
logger.warn(`kody.config.json: ${
|
|
229
|
+
const result2 = parseJsonSafe(fs7.readFileSync(configPath, "utf-8"));
|
|
230
|
+
if (!result2.ok) {
|
|
231
|
+
logger.warn(`kody.config.json: ${result2.error} \u2014 using defaults`);
|
|
232
232
|
_config = { ...DEFAULT_CONFIG };
|
|
233
233
|
return _config;
|
|
234
234
|
}
|
|
235
|
-
const raw =
|
|
235
|
+
const raw = result2.data;
|
|
236
236
|
_config = {
|
|
237
237
|
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
238
238
|
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
@@ -283,7 +283,7 @@ var init_config = __esm({
|
|
|
283
283
|
repo: ""
|
|
284
284
|
},
|
|
285
285
|
agent: {
|
|
286
|
-
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
286
|
+
modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
|
|
287
287
|
},
|
|
288
288
|
contextTiers: {
|
|
289
289
|
enabled: true,
|
|
@@ -389,10 +389,11 @@ function createClaudeCodeRunner() {
|
|
|
389
389
|
model,
|
|
390
390
|
"--dangerously-skip-permissions"
|
|
391
391
|
];
|
|
392
|
+
const baseTools = "Bash,Edit,Read,Write,Glob,Grep";
|
|
392
393
|
if (options?.mcpConfigJson) {
|
|
393
394
|
args2.push("--mcp-config", options.mcpConfigJson);
|
|
394
395
|
} else {
|
|
395
|
-
args2.push("--allowedTools",
|
|
396
|
+
args2.push("--allowedTools", baseTools);
|
|
396
397
|
}
|
|
397
398
|
if (options?.sessionId) {
|
|
398
399
|
if (options.resumeSession) {
|
|
@@ -965,8 +966,8 @@ function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
|
965
966
|
}
|
|
966
967
|
function generateTaskId() {
|
|
967
968
|
const now = /* @__PURE__ */ new Date();
|
|
968
|
-
const
|
|
969
|
-
return `${String(now.getFullYear()).slice(2)}${
|
|
969
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
970
|
+
return `${String(now.getFullYear()).slice(2)}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
|
|
970
971
|
}
|
|
971
972
|
function resolveTaskIdFromComments(issueNumber) {
|
|
972
973
|
try {
|
|
@@ -1192,6 +1193,7 @@ var init_litellm = __esm({
|
|
|
1192
1193
|
// src/cli/taskify-command.ts
|
|
1193
1194
|
var taskify_command_exports = {};
|
|
1194
1195
|
__export(taskify_command_exports, {
|
|
1196
|
+
TaskifyError: () => TaskifyError,
|
|
1195
1197
|
isTaskifyRun: () => isTaskifyRun,
|
|
1196
1198
|
readTaskifyMarker: () => readTaskifyMarker,
|
|
1197
1199
|
runTaskifyCommand: () => runTaskifyCommand,
|
|
@@ -1283,8 +1285,17 @@ async function runTaskifyCommand() {
|
|
|
1283
1285
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
|
|
1284
1286
|
};
|
|
1285
1287
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
+
try {
|
|
1289
|
+
await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
if (err instanceof TaskifyError) {
|
|
1292
|
+
logger.error(`[taskify] ${err.message}`);
|
|
1293
|
+
process.exit(1);
|
|
1294
|
+
}
|
|
1295
|
+
throw err;
|
|
1296
|
+
} finally {
|
|
1297
|
+
litellmProcess?.kill();
|
|
1298
|
+
}
|
|
1288
1299
|
}
|
|
1289
1300
|
async function taskifyCommand(opts) {
|
|
1290
1301
|
const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
|
|
@@ -1299,7 +1310,6 @@ async function taskifyCommand(opts) {
|
|
|
1299
1310
|
mcpConfigJson = buildTaskifyMcpConfigJson(config);
|
|
1300
1311
|
} catch (err) {
|
|
1301
1312
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1302
|
-
logger.error(`[taskify] MCP config error: ${msg}`);
|
|
1303
1313
|
if (issueNumber && !local) {
|
|
1304
1314
|
postComment(
|
|
1305
1315
|
issueNumber,
|
|
@@ -1310,7 +1320,7 @@ async function taskifyCommand(opts) {
|
|
|
1310
1320
|
Add the required MCP server config to \`kody.config.json\` and try again.`
|
|
1311
1321
|
);
|
|
1312
1322
|
}
|
|
1313
|
-
|
|
1323
|
+
throw new TaskifyError(`MCP config error: ${msg}`);
|
|
1314
1324
|
}
|
|
1315
1325
|
}
|
|
1316
1326
|
const sc = resolveStageConfig(config, "taskify", "strong");
|
|
@@ -1351,47 +1361,44 @@ Kody is decomposing ${src} into tasks...`);
|
|
|
1351
1361
|
fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
|
|
1352
1362
|
const runner = opts.runner ?? createClaudeCodeRunner();
|
|
1353
1363
|
logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
|
|
1354
|
-
const
|
|
1364
|
+
const result2 = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
|
|
1355
1365
|
cwd: projectDir,
|
|
1356
1366
|
mcpConfigJson,
|
|
1357
1367
|
env: opts.runnerEnv
|
|
1358
1368
|
});
|
|
1359
|
-
if (
|
|
1360
|
-
const errMsg =
|
|
1361
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1369
|
+
if (result2.outcome !== "completed") {
|
|
1370
|
+
const errMsg = result2.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result2.error}`;
|
|
1362
1371
|
if (issueNumber && !local) {
|
|
1363
1372
|
postComment(issueNumber, `Kody taskify failed:
|
|
1364
1373
|
|
|
1365
1374
|
> ${errMsg}`);
|
|
1366
1375
|
setLifecycleLabel(issueNumber, "failed");
|
|
1367
1376
|
}
|
|
1368
|
-
|
|
1377
|
+
throw new TaskifyError(errMsg);
|
|
1369
1378
|
}
|
|
1370
1379
|
const resultPath = path10.join(taskDir, RESULT_FILE);
|
|
1371
1380
|
if (!fs11.existsSync(resultPath)) {
|
|
1372
1381
|
const errMsg = `Claude did not write ${RESULT_FILE}. Output:
|
|
1373
1382
|
|
|
1374
|
-
${
|
|
1375
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1383
|
+
${result2.output?.slice(0, 500) ?? "(none)"}`;
|
|
1376
1384
|
if (issueNumber && !local) {
|
|
1377
1385
|
postComment(issueNumber, `Kody taskify failed: result file not found.
|
|
1378
1386
|
|
|
1379
1387
|
${errMsg}`);
|
|
1380
1388
|
setLifecycleLabel(issueNumber, "failed");
|
|
1381
1389
|
}
|
|
1382
|
-
|
|
1390
|
+
throw new TaskifyError(errMsg);
|
|
1383
1391
|
}
|
|
1384
1392
|
let parsed;
|
|
1385
1393
|
try {
|
|
1386
1394
|
parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
|
|
1387
1395
|
} catch {
|
|
1388
1396
|
const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
|
|
1389
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1390
1397
|
if (issueNumber && !local) {
|
|
1391
1398
|
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1392
1399
|
setLifecycleLabel(issueNumber, "failed");
|
|
1393
1400
|
}
|
|
1394
|
-
|
|
1401
|
+
throw new TaskifyError(errMsg);
|
|
1395
1402
|
}
|
|
1396
1403
|
const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
|
|
1397
1404
|
if (parsed.status === "questions") {
|
|
@@ -1400,12 +1407,11 @@ ${errMsg}`);
|
|
|
1400
1407
|
await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
|
|
1401
1408
|
} else {
|
|
1402
1409
|
const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
|
|
1403
|
-
logger.error(`[taskify] ${errMsg}`);
|
|
1404
1410
|
if (issueNumber && !local) {
|
|
1405
1411
|
postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
|
|
1406
1412
|
setLifecycleLabel(issueNumber, "failed");
|
|
1407
1413
|
}
|
|
1408
|
-
|
|
1414
|
+
throw new TaskifyError(errMsg);
|
|
1409
1415
|
}
|
|
1410
1416
|
}
|
|
1411
1417
|
function handleQuestions(parsed, ticketId, issueNumber, local) {
|
|
@@ -1536,7 +1542,7 @@ function readTaskifyMarker(taskDir) {
|
|
|
1536
1542
|
return null;
|
|
1537
1543
|
}
|
|
1538
1544
|
}
|
|
1539
|
-
var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
|
|
1545
|
+
var __dirname, TaskifyError, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
|
|
1540
1546
|
var init_taskify_command = __esm({
|
|
1541
1547
|
"src/cli/taskify-command.ts"() {
|
|
1542
1548
|
"use strict";
|
|
@@ -1548,6 +1554,12 @@ var init_taskify_command = __esm({
|
|
|
1548
1554
|
init_task_resolution();
|
|
1549
1555
|
init_litellm();
|
|
1550
1556
|
__dirname = path10.dirname(fileURLToPath(import.meta.url));
|
|
1557
|
+
TaskifyError = class extends Error {
|
|
1558
|
+
constructor(message) {
|
|
1559
|
+
super(message);
|
|
1560
|
+
this.name = "TaskifyError";
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1551
1563
|
AUTO_TRIGGER_THRESHOLD = 5;
|
|
1552
1564
|
MAX_TASKS_GUARD = 20;
|
|
1553
1565
|
TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -1556,6 +1568,999 @@ var init_taskify_command = __esm({
|
|
|
1556
1568
|
}
|
|
1557
1569
|
});
|
|
1558
1570
|
|
|
1571
|
+
// src/cli/test-model-tests.ts
|
|
1572
|
+
import * as fs12 from "fs";
|
|
1573
|
+
import * as os2 from "os";
|
|
1574
|
+
import * as path11 from "path";
|
|
1575
|
+
import * as zlib from "zlib";
|
|
1576
|
+
import { spawnSync, execSync as execSync2 } from "child_process";
|
|
1577
|
+
async function apiCall(ctx, body) {
|
|
1578
|
+
try {
|
|
1579
|
+
const res = await fetch(`${ctx.proxyUrl}/v1/messages`, {
|
|
1580
|
+
method: "POST",
|
|
1581
|
+
headers: {
|
|
1582
|
+
"Content-Type": "application/json",
|
|
1583
|
+
"x-api-key": ctx.apiKey,
|
|
1584
|
+
"anthropic-version": "2023-06-01"
|
|
1585
|
+
},
|
|
1586
|
+
body: JSON.stringify({ model: ctx.model, ...body }),
|
|
1587
|
+
signal: AbortSignal.timeout(6e4)
|
|
1588
|
+
});
|
|
1589
|
+
const data = await res.json();
|
|
1590
|
+
if (!res.ok) {
|
|
1591
|
+
return { ok: false, data, status: res.status, errorMsg: data?.error?.message ?? `HTTP ${res.status}` };
|
|
1592
|
+
}
|
|
1593
|
+
return { ok: true, data, status: res.status };
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
return { ok: false, data: null, status: 0, errorMsg: err instanceof Error ? err.message : String(err) };
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
function extractText(data) {
|
|
1599
|
+
if (!data?.content) return "";
|
|
1600
|
+
return data.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
|
|
1601
|
+
}
|
|
1602
|
+
async function runToolConversation(ctx, tools, userPrompt, simulate, opts) {
|
|
1603
|
+
const messages = [{ role: "user", content: userPrompt }];
|
|
1604
|
+
const allCalls = [];
|
|
1605
|
+
for (let turn = 0; turn < (opts?.maxTurns ?? 5); turn++) {
|
|
1606
|
+
const body = {
|
|
1607
|
+
max_tokens: 1024,
|
|
1608
|
+
temperature: 0,
|
|
1609
|
+
messages,
|
|
1610
|
+
tools
|
|
1611
|
+
};
|
|
1612
|
+
if (opts?.system) body.system = opts.system;
|
|
1613
|
+
const res = await apiCall(ctx, body);
|
|
1614
|
+
if (!res.ok) return { finalText: "", toolCalls: allCalls, error: res.errorMsg };
|
|
1615
|
+
const content = res.data.content ?? [];
|
|
1616
|
+
const toolBlocks = content.filter((b) => b.type === "tool_use");
|
|
1617
|
+
const textBlocks = content.filter((b) => b.type === "text");
|
|
1618
|
+
if (toolBlocks.length === 0) {
|
|
1619
|
+
return { finalText: textBlocks.map((b) => b.text ?? "").join(""), toolCalls: allCalls };
|
|
1620
|
+
}
|
|
1621
|
+
for (const tc of toolBlocks) allCalls.push({ name: tc.name, input: tc.input });
|
|
1622
|
+
messages.push({ role: "assistant", content });
|
|
1623
|
+
messages.push({
|
|
1624
|
+
role: "user",
|
|
1625
|
+
content: toolBlocks.map((tc) => ({
|
|
1626
|
+
type: "tool_result",
|
|
1627
|
+
tool_use_id: tc.id,
|
|
1628
|
+
content: simulate(tc.name, tc.input)
|
|
1629
|
+
}))
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
return { finalText: "", toolCalls: allCalls, error: "Max turns reached" };
|
|
1633
|
+
}
|
|
1634
|
+
function filterStderr(stderr) {
|
|
1635
|
+
return stderr.split("\n").filter((l) => !l.includes("CPU lacks AVX") && !l.includes("bun-darwin") && !l.includes("Warning: no stdin data") && l.trim().length > 0).join("\n").trim();
|
|
1636
|
+
}
|
|
1637
|
+
function runClaudeTest(ctx, prompt, extraFlags = [], timeout = 9e4) {
|
|
1638
|
+
try {
|
|
1639
|
+
const result2 = spawnSync("claude", [
|
|
1640
|
+
"--print",
|
|
1641
|
+
"--model",
|
|
1642
|
+
ctx.model,
|
|
1643
|
+
"--dangerously-skip-permissions",
|
|
1644
|
+
...extraFlags,
|
|
1645
|
+
"-p",
|
|
1646
|
+
prompt
|
|
1647
|
+
], {
|
|
1648
|
+
env: { ...process.env, ANTHROPIC_BASE_URL: ctx.proxyUrl, ANTHROPIC_API_KEY: ctx.apiKey },
|
|
1649
|
+
timeout,
|
|
1650
|
+
encoding: "utf-8",
|
|
1651
|
+
cwd: ctx.projectDir
|
|
1652
|
+
});
|
|
1653
|
+
return {
|
|
1654
|
+
stdout: result2.stdout ?? "",
|
|
1655
|
+
stderr: filterStderr(result2.stderr ?? ""),
|
|
1656
|
+
exitCode: result2.status ?? 1
|
|
1657
|
+
};
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
return { stdout: "", stderr: String(err), exitCode: 1 };
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
function isGitClean(dir) {
|
|
1663
|
+
try {
|
|
1664
|
+
const out = execSync2("git diff --name-only", { cwd: dir, encoding: "utf-8", timeout: 5e3 });
|
|
1665
|
+
return out.trim().length === 0;
|
|
1666
|
+
} catch {
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function revertChanges(dir) {
|
|
1671
|
+
try {
|
|
1672
|
+
execSync2("git checkout -- src/logger.ts", { cwd: dir, timeout: 5e3, stdio: "pipe" });
|
|
1673
|
+
} catch {
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function result(name, category, status, accuracy, durationMs, detail, metrics) {
|
|
1677
|
+
return { name, category, status, accuracy, durationMs, detail, metrics };
|
|
1678
|
+
}
|
|
1679
|
+
function crc32(buf) {
|
|
1680
|
+
let c = 4294967295;
|
|
1681
|
+
for (const b of buf) c = CRC_TABLE[(c ^ b) & 255] ^ c >>> 8;
|
|
1682
|
+
return (c ^ 4294967295) >>> 0;
|
|
1683
|
+
}
|
|
1684
|
+
function createRedPng() {
|
|
1685
|
+
const w = 4, h = 4;
|
|
1686
|
+
const scanlines = Buffer.alloc(h * (1 + w * 3));
|
|
1687
|
+
for (let y = 0; y < h; y++) {
|
|
1688
|
+
const off = y * (1 + w * 3);
|
|
1689
|
+
scanlines[off] = 0;
|
|
1690
|
+
for (let x = 0; x < w; x++) {
|
|
1691
|
+
scanlines[off + 1 + x * 3] = 255;
|
|
1692
|
+
scanlines[off + 1 + x * 3 + 1] = 0;
|
|
1693
|
+
scanlines[off + 1 + x * 3 + 2] = 0;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
function chunk(type, data) {
|
|
1697
|
+
const tb = Buffer.from(type, "ascii");
|
|
1698
|
+
const merged = Buffer.concat([tb, data]);
|
|
1699
|
+
const len = Buffer.alloc(4);
|
|
1700
|
+
len.writeUInt32BE(data.length);
|
|
1701
|
+
const crcBuf = Buffer.alloc(4);
|
|
1702
|
+
crcBuf.writeUInt32BE(crc32(merged));
|
|
1703
|
+
return Buffer.concat([len, tb, data, crcBuf]);
|
|
1704
|
+
}
|
|
1705
|
+
const sig = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
1706
|
+
const ihdr = Buffer.alloc(13);
|
|
1707
|
+
ihdr.writeUInt32BE(w, 0);
|
|
1708
|
+
ihdr.writeUInt32BE(h, 4);
|
|
1709
|
+
ihdr[8] = 8;
|
|
1710
|
+
ihdr[9] = 2;
|
|
1711
|
+
return Buffer.concat([sig, chunk("IHDR", ihdr), chunk("IDAT", zlib.deflateSync(scanlines)), chunk("IEND", Buffer.alloc(0))]);
|
|
1712
|
+
}
|
|
1713
|
+
async function testSimplePrompt(ctx) {
|
|
1714
|
+
const t = Date.now();
|
|
1715
|
+
const res = await apiCall(ctx, {
|
|
1716
|
+
max_tokens: 50,
|
|
1717
|
+
temperature: 0,
|
|
1718
|
+
messages: [{ role: "user", content: "Reply with exactly: KODY_TEST_OK" }]
|
|
1719
|
+
});
|
|
1720
|
+
if (!res.ok) return result("simple_prompt", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
|
|
1721
|
+
const text = extractText(res.data);
|
|
1722
|
+
const ok = text.includes("KODY_TEST_OK");
|
|
1723
|
+
return result(
|
|
1724
|
+
"simple_prompt",
|
|
1725
|
+
"basic",
|
|
1726
|
+
ok ? "pass" : "fail",
|
|
1727
|
+
ok ? 100 : 0,
|
|
1728
|
+
Date.now() - t,
|
|
1729
|
+
ok ? "Model responded correctly" : `Expected KODY_TEST_OK, got: ${text.slice(0, 80)}`
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
async function testJsonOutput(ctx) {
|
|
1733
|
+
const t = Date.now();
|
|
1734
|
+
const res = await apiCall(ctx, {
|
|
1735
|
+
max_tokens: 200,
|
|
1736
|
+
temperature: 0,
|
|
1737
|
+
system: "Respond with ONLY valid JSON. No markdown fences, no explanation. Just raw JSON.",
|
|
1738
|
+
messages: [{ role: "user", content: 'Return a JSON object with keys "status" (string "ok") and "model" (string, your model name).' }]
|
|
1739
|
+
});
|
|
1740
|
+
if (!res.ok) return result("json_output", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
|
|
1741
|
+
let text = extractText(res.data).trim();
|
|
1742
|
+
text = text.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/i, "").trim();
|
|
1743
|
+
try {
|
|
1744
|
+
const parsed = JSON.parse(text);
|
|
1745
|
+
const hasKeys = typeof parsed.status === "string" && typeof parsed.model === "string";
|
|
1746
|
+
return result(
|
|
1747
|
+
"json_output",
|
|
1748
|
+
"basic",
|
|
1749
|
+
"pass",
|
|
1750
|
+
hasKeys ? 100 : 70,
|
|
1751
|
+
Date.now() - t,
|
|
1752
|
+
hasKeys ? "Valid JSON with correct keys" : "Valid JSON but missing expected keys"
|
|
1753
|
+
);
|
|
1754
|
+
} catch {
|
|
1755
|
+
return result("json_output", "basic", "fail", 0, Date.now() - t, `Invalid JSON: ${text.slice(0, 80)}`);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
async function testSystemPromptRules(ctx) {
|
|
1759
|
+
const t = Date.now();
|
|
1760
|
+
const res = await apiCall(ctx, {
|
|
1761
|
+
max_tokens: 200,
|
|
1762
|
+
temperature: 0,
|
|
1763
|
+
system: [
|
|
1764
|
+
"STRICT RULES \u2014 violating ANY will crash the system:",
|
|
1765
|
+
"1) Start every response with 'KODY:'",
|
|
1766
|
+
"2) Never use the word 'the'",
|
|
1767
|
+
"3) Keep response under 50 words",
|
|
1768
|
+
"4) End your response with 'END'",
|
|
1769
|
+
"5) Use ONLY lowercase letters (no uppercase anywhere)"
|
|
1770
|
+
].join("\n"),
|
|
1771
|
+
messages: [{ role: "user", content: "Describe what a compiler does." }]
|
|
1772
|
+
});
|
|
1773
|
+
if (!res.ok) return result("system_prompt_rules", "basic", "fail", 0, Date.now() - t, `API error: ${res.errorMsg}`);
|
|
1774
|
+
const text = extractText(res.data).trim();
|
|
1775
|
+
let score = 0;
|
|
1776
|
+
const checks = [];
|
|
1777
|
+
if (text.startsWith("KODY:") || text.startsWith("kody:")) {
|
|
1778
|
+
score += 20;
|
|
1779
|
+
checks.push("starts-with-kody");
|
|
1780
|
+
}
|
|
1781
|
+
if (!text.toLowerCase().split(/\s+/).includes("the")) {
|
|
1782
|
+
score += 20;
|
|
1783
|
+
checks.push("no-the");
|
|
1784
|
+
}
|
|
1785
|
+
if (text.split(/\s+/).length <= 55) {
|
|
1786
|
+
score += 20;
|
|
1787
|
+
checks.push("under-50-words");
|
|
1788
|
+
}
|
|
1789
|
+
if (text.endsWith("END") || text.endsWith("end")) {
|
|
1790
|
+
score += 20;
|
|
1791
|
+
checks.push("ends-with-end");
|
|
1792
|
+
}
|
|
1793
|
+
if (text === text.toLowerCase()) {
|
|
1794
|
+
score += 20;
|
|
1795
|
+
checks.push("all-lowercase");
|
|
1796
|
+
}
|
|
1797
|
+
const status = score >= 80 ? "pass" : score >= 40 ? "warn" : "fail";
|
|
1798
|
+
return result(
|
|
1799
|
+
"system_prompt_rules",
|
|
1800
|
+
"basic",
|
|
1801
|
+
status,
|
|
1802
|
+
score,
|
|
1803
|
+
Date.now() - t,
|
|
1804
|
+
`${score / 20}/5 rules followed: ${checks.join(", ")}`,
|
|
1805
|
+
{ instructionCompliance: score }
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
async function testExtendedThinking(ctx) {
|
|
1809
|
+
const t = Date.now();
|
|
1810
|
+
const res = await apiCall(ctx, {
|
|
1811
|
+
max_tokens: 200,
|
|
1812
|
+
thinking: { type: "enabled", budget_tokens: 2e3 },
|
|
1813
|
+
messages: [{ role: "user", content: "What is 15 * 23?" }]
|
|
1814
|
+
});
|
|
1815
|
+
if (!res.ok) return result(
|
|
1816
|
+
"extended_thinking",
|
|
1817
|
+
"infrastructure",
|
|
1818
|
+
"warn",
|
|
1819
|
+
50,
|
|
1820
|
+
Date.now() - t,
|
|
1821
|
+
`Request failed (model may not support thinking): ${res.errorMsg?.slice(0, 80)}`
|
|
1822
|
+
);
|
|
1823
|
+
const hasThinking = Array.isArray(res.data.content) && res.data.content.some((b) => b.type === "thinking");
|
|
1824
|
+
const hasText = extractText(res.data).length > 0;
|
|
1825
|
+
if (hasThinking) return result("extended_thinking", "infrastructure", "pass", 100, Date.now() - t, "Thinking block present in response");
|
|
1826
|
+
if (hasText) return result("extended_thinking", "infrastructure", "warn", 70, Date.now() - t, "Response OK but no thinking block");
|
|
1827
|
+
return result("extended_thinking", "infrastructure", "fail", 0, Date.now() - t, "No content in response");
|
|
1828
|
+
}
|
|
1829
|
+
async function testToolRead(ctx) {
|
|
1830
|
+
const t = Date.now();
|
|
1831
|
+
const testFile = path11.join(os2.tmpdir(), "kody-test-model-read.txt");
|
|
1832
|
+
fs12.writeFileSync(testFile, "KODY_SECRET_CONTENT_42");
|
|
1833
|
+
try {
|
|
1834
|
+
const conv = await runToolConversation(
|
|
1835
|
+
ctx,
|
|
1836
|
+
[TOOL_READ],
|
|
1837
|
+
`Read the file ${testFile} and tell me what it contains.`,
|
|
1838
|
+
(name, input) => {
|
|
1839
|
+
if (name === "Read" && input.path === testFile) return "KODY_SECRET_CONTENT_42";
|
|
1840
|
+
return "Error: File not found";
|
|
1841
|
+
}
|
|
1842
|
+
);
|
|
1843
|
+
if (conv.error) return result("tool_read", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
1844
|
+
const calledRead = conv.toolCalls.some((tc) => tc.name === "Read");
|
|
1845
|
+
const correctPath = conv.toolCalls.some((tc) => tc.name === "Read" && tc.input.path === testFile);
|
|
1846
|
+
const mentionsContent = conv.finalText.includes("KODY_SECRET_CONTENT_42") || conv.finalText.includes("42");
|
|
1847
|
+
let acc = 0;
|
|
1848
|
+
if (calledRead) acc += 30;
|
|
1849
|
+
if (correctPath) acc += 30;
|
|
1850
|
+
if (mentionsContent) acc += 40;
|
|
1851
|
+
return result(
|
|
1852
|
+
"tool_read",
|
|
1853
|
+
"tool-use",
|
|
1854
|
+
acc >= 60 ? "pass" : "fail",
|
|
1855
|
+
acc,
|
|
1856
|
+
Date.now() - t,
|
|
1857
|
+
`Read called: ${calledRead}, correct path: ${correctPath}, content referenced: ${mentionsContent}`,
|
|
1858
|
+
{ toolSelection: calledRead ? 100 : 0 }
|
|
1859
|
+
);
|
|
1860
|
+
} finally {
|
|
1861
|
+
fs12.rmSync(testFile, { force: true });
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
async function testToolEdit(ctx) {
|
|
1865
|
+
const t = Date.now();
|
|
1866
|
+
const conv = await runToolConversation(
|
|
1867
|
+
ctx,
|
|
1868
|
+
[TOOL_READ, TOOL_EDIT],
|
|
1869
|
+
'Read the file /tmp/kody-edit-test.txt, then use Edit to replace "hello" with "goodbye" in it.',
|
|
1870
|
+
(name, input) => {
|
|
1871
|
+
if (name === "Read") return "hello world";
|
|
1872
|
+
if (name === "Edit") return "File edited successfully";
|
|
1873
|
+
return "Unknown tool";
|
|
1874
|
+
}
|
|
1875
|
+
);
|
|
1876
|
+
if (conv.error) return result("tool_edit", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
1877
|
+
const editCall = conv.toolCalls.find((tc) => tc.name === "Edit");
|
|
1878
|
+
let acc = 0;
|
|
1879
|
+
if (editCall) {
|
|
1880
|
+
acc += 40;
|
|
1881
|
+
if (editCall.input.old_string === "hello") acc += 30;
|
|
1882
|
+
if (editCall.input.new_string === "goodbye") acc += 30;
|
|
1883
|
+
}
|
|
1884
|
+
return result(
|
|
1885
|
+
"tool_edit",
|
|
1886
|
+
"tool-use",
|
|
1887
|
+
acc >= 70 ? "pass" : acc > 0 ? "warn" : "fail",
|
|
1888
|
+
acc,
|
|
1889
|
+
Date.now() - t,
|
|
1890
|
+
editCall ? `Edit called with old="${editCall.input.old_string}" new="${editCall.input.new_string}"` : "Edit tool was not called",
|
|
1891
|
+
{ toolSelection: editCall ? 100 : 0 }
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
async function testToolBash(ctx) {
|
|
1895
|
+
const t = Date.now();
|
|
1896
|
+
const conv = await runToolConversation(
|
|
1897
|
+
ctx,
|
|
1898
|
+
[TOOL_BASH],
|
|
1899
|
+
"Run this exact bash command: echo KODY_BASH_OK",
|
|
1900
|
+
(name, input) => {
|
|
1901
|
+
if (name === "Bash") return "KODY_BASH_OK\n";
|
|
1902
|
+
return "Error";
|
|
1903
|
+
}
|
|
1904
|
+
);
|
|
1905
|
+
if (conv.error) return result("tool_bash", "tool-use", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
1906
|
+
const bashCall = conv.toolCalls.find((tc) => tc.name === "Bash");
|
|
1907
|
+
const correctCmd = bashCall && String(bashCall.input.command).includes("echo KODY_BASH_OK");
|
|
1908
|
+
const acc = bashCall ? correctCmd ? 100 : 50 : 0;
|
|
1909
|
+
return result(
|
|
1910
|
+
"tool_bash",
|
|
1911
|
+
"tool-use",
|
|
1912
|
+
acc >= 50 ? "pass" : "fail",
|
|
1913
|
+
acc,
|
|
1914
|
+
Date.now() - t,
|
|
1915
|
+
bashCall ? `Bash called: ${bashCall.input.command}` : "Bash tool was not called",
|
|
1916
|
+
{ toolSelection: bashCall ? 100 : 0 }
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1919
|
+
async function testImageAttachment(ctx) {
|
|
1920
|
+
const t = Date.now();
|
|
1921
|
+
const pngData = createRedPng().toString("base64");
|
|
1922
|
+
const res = await apiCall(ctx, {
|
|
1923
|
+
max_tokens: 100,
|
|
1924
|
+
temperature: 0,
|
|
1925
|
+
messages: [{
|
|
1926
|
+
role: "user",
|
|
1927
|
+
content: [
|
|
1928
|
+
{ type: "image", source: { type: "base64", media_type: "image/png", data: pngData } },
|
|
1929
|
+
{ type: "text", text: "What color is this image? Reply with just the color name." }
|
|
1930
|
+
]
|
|
1931
|
+
}]
|
|
1932
|
+
});
|
|
1933
|
+
if (!res.ok) return result(
|
|
1934
|
+
"image_attachment",
|
|
1935
|
+
"tool-use",
|
|
1936
|
+
"fail",
|
|
1937
|
+
0,
|
|
1938
|
+
Date.now() - t,
|
|
1939
|
+
`API error (model may not support vision): ${res.errorMsg?.slice(0, 80)}`
|
|
1940
|
+
);
|
|
1941
|
+
const text = extractText(res.data).toLowerCase();
|
|
1942
|
+
const mentionsRed = text.includes("red");
|
|
1943
|
+
const mentionsColor = mentionsRed || text.includes("color") || text.includes("image") || text.includes("pixel");
|
|
1944
|
+
const acc = mentionsRed ? 100 : mentionsColor ? 50 : 20;
|
|
1945
|
+
return result(
|
|
1946
|
+
"image_attachment",
|
|
1947
|
+
"tool-use",
|
|
1948
|
+
mentionsRed ? "pass" : mentionsColor ? "warn" : "fail",
|
|
1949
|
+
acc,
|
|
1950
|
+
Date.now() - t,
|
|
1951
|
+
`Response: ${text.slice(0, 80)}`
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
async function testErrorRecovery(ctx) {
|
|
1955
|
+
const t = Date.now();
|
|
1956
|
+
let errorGiven = false;
|
|
1957
|
+
const conv = await runToolConversation(
|
|
1958
|
+
ctx,
|
|
1959
|
+
[TOOL_READ, TOOL_BASH],
|
|
1960
|
+
"Read the file /tmp/nonexistent-kody-file.txt and tell me what's in it. If the file doesn't exist, say so.",
|
|
1961
|
+
(name, input) => {
|
|
1962
|
+
if (name === "Read" && !errorGiven) {
|
|
1963
|
+
errorGiven = true;
|
|
1964
|
+
return "Error: ENOENT: no such file or directory";
|
|
1965
|
+
}
|
|
1966
|
+
if (name === "Bash") return "ls: /tmp/nonexistent-kody-file.txt: No such file or directory";
|
|
1967
|
+
return "Error: File not found";
|
|
1968
|
+
}
|
|
1969
|
+
);
|
|
1970
|
+
if (conv.error) return result("error_recovery", "advanced", "fail", 0, Date.now() - t, `Error: ${conv.error}`);
|
|
1971
|
+
const reported = conv.finalText.toLowerCase().includes("not found") || conv.finalText.toLowerCase().includes("doesn't exist") || conv.finalText.toLowerCase().includes("does not exist") || conv.finalText.toLowerCase().includes("no such file");
|
|
1972
|
+
const tried = conv.toolCalls.length >= 1;
|
|
1973
|
+
const acc = reported ? tried ? 100 : 70 : 20;
|
|
1974
|
+
return result(
|
|
1975
|
+
"error_recovery",
|
|
1976
|
+
"advanced",
|
|
1977
|
+
reported ? "pass" : "warn",
|
|
1978
|
+
acc,
|
|
1979
|
+
Date.now() - t,
|
|
1980
|
+
reported ? "Gracefully reported missing file" : `Response: ${conv.finalText.slice(0, 80)}`
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
async function testToolMultiStep(ctx) {
|
|
1984
|
+
const t = Date.now();
|
|
1985
|
+
const r = runClaudeTest(
|
|
1986
|
+
ctx,
|
|
1987
|
+
"Do these steps in order: 1) Read kody.config.json 2) Tell me the value of git.defaultBranch. Reply with ONLY the branch name, nothing else."
|
|
1988
|
+
);
|
|
1989
|
+
if (!r.stdout.trim() && r.exitCode !== 0) return result(
|
|
1990
|
+
"tool_multi_step",
|
|
1991
|
+
"tool-use",
|
|
1992
|
+
"fail",
|
|
1993
|
+
0,
|
|
1994
|
+
Date.now() - t,
|
|
1995
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
1996
|
+
);
|
|
1997
|
+
const out = r.stdout.trim().toLowerCase();
|
|
1998
|
+
const correct = out.includes("main");
|
|
1999
|
+
return result(
|
|
2000
|
+
"tool_multi_step",
|
|
2001
|
+
"tool-use",
|
|
2002
|
+
correct ? "pass" : "fail",
|
|
2003
|
+
correct ? 100 : 20,
|
|
2004
|
+
Date.now() - t,
|
|
2005
|
+
correct ? "Correct: main" : `Got: ${out.slice(0, 80)}`
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
async function testPlanStage(ctx) {
|
|
2009
|
+
const t = Date.now();
|
|
2010
|
+
const wasClean = isGitClean(ctx.projectDir);
|
|
2011
|
+
const r = runClaudeTest(ctx, [
|
|
2012
|
+
"You are a planning agent. Your ONLY job is to output a markdown plan.",
|
|
2013
|
+
"CRITICAL: Do NOT use Edit, Write, or Bash tools. Do NOT modify any files. ONLY use Read, Glob, and Grep for research.",
|
|
2014
|
+
"If you modify any files, the system will crash.",
|
|
2015
|
+
"",
|
|
2016
|
+
"Task: Plan adding a /health endpoint to an Express app.",
|
|
2017
|
+
"Output a markdown plan with ## Step N sections. Each step must have File, Change, and Why fields.",
|
|
2018
|
+
"Keep it to 3 steps maximum."
|
|
2019
|
+
].join("\n"), [], 12e4);
|
|
2020
|
+
const filesModified = wasClean && !isGitClean(ctx.projectDir);
|
|
2021
|
+
if (filesModified) revertChanges(ctx.projectDir);
|
|
2022
|
+
if (!r.stdout.trim() && r.exitCode !== 0) return result(
|
|
2023
|
+
"plan_stage",
|
|
2024
|
+
"stage-simulation",
|
|
2025
|
+
"fail",
|
|
2026
|
+
0,
|
|
2027
|
+
Date.now() - t,
|
|
2028
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
2029
|
+
);
|
|
2030
|
+
const out = r.stdout;
|
|
2031
|
+
const hasStepFormat = /##\s*Step/i.test(out);
|
|
2032
|
+
const hasStructure = hasStepFormat || /\*\*File\*\*/i.test(out) && /\*\*Change\*\*/i.test(out);
|
|
2033
|
+
const boundary = filesModified ? 0 : 100;
|
|
2034
|
+
const format = hasStructure ? 100 : hasStepFormat ? 70 : out.length > 50 ? 30 : 0;
|
|
2035
|
+
const acc = Math.round(boundary * 0.6 + format * 0.4);
|
|
2036
|
+
const status = filesModified ? "fail" : hasStructure ? "pass" : "warn";
|
|
2037
|
+
return result(
|
|
2038
|
+
"plan_stage",
|
|
2039
|
+
"stage-simulation",
|
|
2040
|
+
status,
|
|
2041
|
+
acc,
|
|
2042
|
+
Date.now() - t,
|
|
2043
|
+
filesModified ? "FAIL: Model modified files during plan stage (instruction violation)" : hasStructure ? "Plan output with correct structure, no files modified" : "Output lacks expected ## Step structure",
|
|
2044
|
+
{ boundaryRespect: boundary, outputFormat: format, instructionCompliance: boundary }
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
async function testBuildStage(ctx) {
|
|
2048
|
+
const t = Date.now();
|
|
2049
|
+
const r = runClaudeTest(ctx, "Add a comment '// kody-build-test' as the very first line of src/logger.ts. That is your only task.");
|
|
2050
|
+
const diff = (() => {
|
|
2051
|
+
try {
|
|
2052
|
+
return execSync2("git diff src/logger.ts", { cwd: ctx.projectDir, encoding: "utf-8", timeout: 5e3 });
|
|
2053
|
+
} catch {
|
|
2054
|
+
return "";
|
|
2055
|
+
}
|
|
2056
|
+
})();
|
|
2057
|
+
const edited = diff.includes("kody-build-test");
|
|
2058
|
+
revertChanges(ctx.projectDir);
|
|
2059
|
+
if (!r.stdout.trim() && r.exitCode !== 0 && !edited) return result(
|
|
2060
|
+
"build_stage",
|
|
2061
|
+
"stage-simulation",
|
|
2062
|
+
"fail",
|
|
2063
|
+
0,
|
|
2064
|
+
Date.now() - t,
|
|
2065
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
2066
|
+
);
|
|
2067
|
+
return result(
|
|
2068
|
+
"build_stage",
|
|
2069
|
+
"stage-simulation",
|
|
2070
|
+
edited ? "pass" : "fail",
|
|
2071
|
+
edited ? 100 : 0,
|
|
2072
|
+
Date.now() - t,
|
|
2073
|
+
edited ? "File correctly modified with expected comment" : "File was not modified as expected"
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
async function testReviewStage(ctx) {
|
|
2077
|
+
const t = Date.now();
|
|
2078
|
+
const wasClean = isGitClean(ctx.projectDir);
|
|
2079
|
+
const r = runClaudeTest(ctx, [
|
|
2080
|
+
"You are a code review agent. Review the file src/logger.ts.",
|
|
2081
|
+
"CRITICAL: Do NOT modify any files. Only READ and analyze.",
|
|
2082
|
+
"Output your review as markdown with this exact format:",
|
|
2083
|
+
"## Summary",
|
|
2084
|
+
"<1-2 sentence summary>",
|
|
2085
|
+
"## Issues Found",
|
|
2086
|
+
"- <issues>",
|
|
2087
|
+
"## Verdict",
|
|
2088
|
+
"APPROVE or REQUEST_CHANGES"
|
|
2089
|
+
].join("\n"));
|
|
2090
|
+
const filesModified = wasClean && !isGitClean(ctx.projectDir);
|
|
2091
|
+
if (filesModified) revertChanges(ctx.projectDir);
|
|
2092
|
+
if (!r.stdout.trim() && r.exitCode !== 0) return result(
|
|
2093
|
+
"review_stage",
|
|
2094
|
+
"stage-simulation",
|
|
2095
|
+
"fail",
|
|
2096
|
+
0,
|
|
2097
|
+
Date.now() - t,
|
|
2098
|
+
`CLI failed: ${r.stderr.slice(0, 200) || "no output"}`
|
|
2099
|
+
);
|
|
2100
|
+
const out = r.stdout;
|
|
2101
|
+
const hasVerdict = /verdict/i.test(out);
|
|
2102
|
+
const hasSummary = /summary/i.test(out);
|
|
2103
|
+
const boundary = filesModified ? 0 : 100;
|
|
2104
|
+
const format = (hasVerdict ? 50 : 0) + (hasSummary ? 50 : 0);
|
|
2105
|
+
const acc = Math.round(boundary * 0.5 + format * 0.5);
|
|
2106
|
+
const status = filesModified ? "fail" : hasVerdict && hasSummary ? "pass" : "warn";
|
|
2107
|
+
return result(
|
|
2108
|
+
"review_stage",
|
|
2109
|
+
"stage-simulation",
|
|
2110
|
+
status,
|
|
2111
|
+
acc,
|
|
2112
|
+
Date.now() - t,
|
|
2113
|
+
filesModified ? "FAIL: Model modified files during review (instruction violation)" : `Summary: ${hasSummary}, Verdict: ${hasVerdict}, no files modified`,
|
|
2114
|
+
{ boundaryRespect: boundary, outputFormat: format }
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
async function testMcpTools(ctx) {
|
|
2118
|
+
const t = Date.now();
|
|
2119
|
+
const mcpConfig = path11.join(os2.tmpdir(), `kody-test-mcp-${Date.now()}.json`);
|
|
2120
|
+
const testFile = path11.join(ctx.projectDir, "kody-mcp-compat-test.txt");
|
|
2121
|
+
try {
|
|
2122
|
+
fs12.writeFileSync(mcpConfig, JSON.stringify({
|
|
2123
|
+
mcpServers: {
|
|
2124
|
+
filesystem: { command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", ctx.projectDir] }
|
|
2125
|
+
}
|
|
2126
|
+
}));
|
|
2127
|
+
const r = runClaudeTest(
|
|
2128
|
+
ctx,
|
|
2129
|
+
`Use the MCP filesystem write_file tool to create a file at ${testFile} with the content 'mcp-ok'. Do not use the built-in Write tool.`,
|
|
2130
|
+
["--mcp-config", mcpConfig],
|
|
2131
|
+
12e4
|
|
2132
|
+
);
|
|
2133
|
+
const created = fs12.existsSync(testFile);
|
|
2134
|
+
const content = created ? fs12.readFileSync(testFile, "utf-8").trim() : "";
|
|
2135
|
+
const correct = content.includes("mcp-ok");
|
|
2136
|
+
return result(
|
|
2137
|
+
"mcp_tools",
|
|
2138
|
+
"advanced",
|
|
2139
|
+
created ? "pass" : "fail",
|
|
2140
|
+
correct ? 100 : created ? 70 : 0,
|
|
2141
|
+
Date.now() - t,
|
|
2142
|
+
created ? `File created, content: ${content.slice(0, 50)}` : `MCP test failed: ${r.stderr.slice(0, 80)}`
|
|
2143
|
+
);
|
|
2144
|
+
} catch (err) {
|
|
2145
|
+
return result("mcp_tools", "advanced", "warn", 0, Date.now() - t, `MCP test error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2146
|
+
} finally {
|
|
2147
|
+
fs12.rmSync(mcpConfig, { force: true });
|
|
2148
|
+
fs12.rmSync(testFile, { force: true });
|
|
2149
|
+
revertChanges(ctx.projectDir);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
var TOOL_READ, TOOL_EDIT, TOOL_BASH, CRC_TABLE, ALL_TESTS;
|
|
2153
|
+
var init_test_model_tests = __esm({
|
|
2154
|
+
"src/cli/test-model-tests.ts"() {
|
|
2155
|
+
"use strict";
|
|
2156
|
+
TOOL_READ = {
|
|
2157
|
+
name: "Read",
|
|
2158
|
+
description: "Read a file from the filesystem",
|
|
2159
|
+
input_schema: {
|
|
2160
|
+
type: "object",
|
|
2161
|
+
properties: { path: { type: "string", description: "Absolute file path" } },
|
|
2162
|
+
required: ["path"]
|
|
2163
|
+
}
|
|
2164
|
+
};
|
|
2165
|
+
TOOL_EDIT = {
|
|
2166
|
+
name: "Edit",
|
|
2167
|
+
description: "Replace old_string with new_string in a file",
|
|
2168
|
+
input_schema: {
|
|
2169
|
+
type: "object",
|
|
2170
|
+
properties: {
|
|
2171
|
+
file_path: { type: "string" },
|
|
2172
|
+
old_string: { type: "string" },
|
|
2173
|
+
new_string: { type: "string" }
|
|
2174
|
+
},
|
|
2175
|
+
required: ["file_path", "old_string", "new_string"]
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
TOOL_BASH = {
|
|
2179
|
+
name: "Bash",
|
|
2180
|
+
description: "Execute a bash command and return output",
|
|
2181
|
+
input_schema: {
|
|
2182
|
+
type: "object",
|
|
2183
|
+
properties: { command: { type: "string", description: "The command to run" } },
|
|
2184
|
+
required: ["command"]
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
CRC_TABLE = new Uint32Array(256);
|
|
2188
|
+
for (let n = 0; n < 256; n++) {
|
|
2189
|
+
let c = n;
|
|
2190
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
2191
|
+
CRC_TABLE[n] = c >>> 0;
|
|
2192
|
+
}
|
|
2193
|
+
ALL_TESTS = [
|
|
2194
|
+
// Infrastructure
|
|
2195
|
+
{ name: "extended_thinking", category: "infrastructure", description: "Extended thinking parameter support", run: testExtendedThinking },
|
|
2196
|
+
// Basic
|
|
2197
|
+
{ name: "simple_prompt", category: "basic", description: "Basic text prompt and response", run: testSimplePrompt },
|
|
2198
|
+
{ name: "json_output", category: "basic", description: "JSON-only output constraint", run: testJsonOutput },
|
|
2199
|
+
{ name: "system_prompt_rules", category: "basic", description: "Multi-rule system prompt adherence", run: testSystemPromptRules },
|
|
2200
|
+
// Tool use
|
|
2201
|
+
{ name: "tool_read", category: "tool-use", description: "Read tool: file reading", run: testToolRead },
|
|
2202
|
+
{ name: "tool_edit", category: "tool-use", description: "Edit tool: old/new string replacement", run: testToolEdit },
|
|
2203
|
+
{ name: "tool_bash", category: "tool-use", description: "Bash tool: command execution", run: testToolBash },
|
|
2204
|
+
{ name: "tool_multi_step", category: "tool-use", description: "Multi-step tool chain via CLI", run: testToolMultiStep },
|
|
2205
|
+
{ name: "image_attachment", category: "tool-use", description: "Vision: image content processing", run: testImageAttachment },
|
|
2206
|
+
// Stage simulation
|
|
2207
|
+
{ name: "plan_stage", category: "stage-simulation", description: "Plan stage: read-only research + structured output", run: testPlanStage },
|
|
2208
|
+
{ name: "build_stage", category: "stage-simulation", description: "Build stage: code editing", run: testBuildStage },
|
|
2209
|
+
{ name: "review_stage", category: "stage-simulation", description: "Review stage: read-only + structured verdict", run: testReviewStage },
|
|
2210
|
+
// Advanced
|
|
2211
|
+
{ name: "mcp_tools", category: "advanced", description: "MCP server tool integration", run: testMcpTools },
|
|
2212
|
+
{ name: "error_recovery", category: "advanced", description: "Graceful error handling on tool failure", run: testErrorRecovery }
|
|
2213
|
+
];
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
|
|
2217
|
+
// src/cli/test-model-report.ts
|
|
2218
|
+
function pad(str, len) {
|
|
2219
|
+
return str.padEnd(len);
|
|
2220
|
+
}
|
|
2221
|
+
function fmtDuration(ms) {
|
|
2222
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
2223
|
+
}
|
|
2224
|
+
function formatReport(report) {
|
|
2225
|
+
const W = 74;
|
|
2226
|
+
const lines = [];
|
|
2227
|
+
lines.push("=".repeat(W));
|
|
2228
|
+
lines.push("");
|
|
2229
|
+
lines.push(" Model Compatibility Report");
|
|
2230
|
+
lines.push(` Provider: ${report.provider} | Model: ${report.model}`);
|
|
2231
|
+
lines.push(` Date: ${report.timestamp}`);
|
|
2232
|
+
lines.push(` Duration: ${fmtDuration(report.totalDurationMs)}`);
|
|
2233
|
+
lines.push("");
|
|
2234
|
+
lines.push("-".repeat(W));
|
|
2235
|
+
for (const cat of CATEGORY_ORDER) {
|
|
2236
|
+
const catResults = report.results.filter((r) => r.category === cat);
|
|
2237
|
+
if (catResults.length === 0) continue;
|
|
2238
|
+
lines.push("");
|
|
2239
|
+
lines.push(` ${CATEGORY_LABELS[cat]}`);
|
|
2240
|
+
lines.push("");
|
|
2241
|
+
for (const r of catResults) {
|
|
2242
|
+
const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
|
|
2243
|
+
const name = pad(r.name, 28);
|
|
2244
|
+
const status = pad(r.status.toUpperCase(), 6);
|
|
2245
|
+
const acc = pad(`${r.accuracy}%`, 5);
|
|
2246
|
+
const dur = fmtDuration(r.durationMs);
|
|
2247
|
+
lines.push(` [${icon}] ${name} ${status} ${acc} ${dur}`);
|
|
2248
|
+
if (r.status !== "pass" && r.detail) {
|
|
2249
|
+
lines.push(` ${r.detail.slice(0, W - 8)}`);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
const passed = report.results.filter((r) => r.status === "pass").length;
|
|
2254
|
+
const failed = report.results.filter((r) => r.status === "fail").length;
|
|
2255
|
+
const warned = report.results.filter((r) => r.status === "warn").length;
|
|
2256
|
+
const total = report.results.length;
|
|
2257
|
+
const avgAccuracy = total > 0 ? Math.round(report.results.reduce((s, r) => s + r.accuracy, 0) / total) : 0;
|
|
2258
|
+
lines.push("");
|
|
2259
|
+
lines.push("-".repeat(W));
|
|
2260
|
+
lines.push("");
|
|
2261
|
+
lines.push(` RESULTS: ${passed}/${total} PASS | ${failed} FAIL | ${warned} WARN`);
|
|
2262
|
+
lines.push(` OVERALL ACCURACY: ${avgAccuracy}%`);
|
|
2263
|
+
lines.push(` drop_params required: ${report.dropParamsRequired ? "YES" : "NO"}`);
|
|
2264
|
+
lines.push("");
|
|
2265
|
+
lines.push(" ACCURACY BY CATEGORY:");
|
|
2266
|
+
for (const cat of CATEGORY_ORDER) {
|
|
2267
|
+
const cr = report.results.filter((r) => r.category === cat);
|
|
2268
|
+
if (cr.length === 0) continue;
|
|
2269
|
+
const avg = Math.round(cr.reduce((s, r) => s + r.accuracy, 0) / cr.length);
|
|
2270
|
+
lines.push(` ${pad(CATEGORY_LABELS[cat], 22)} ${avg}%`);
|
|
2271
|
+
}
|
|
2272
|
+
lines.push("");
|
|
2273
|
+
lines.push(" RECOMMENDATION:");
|
|
2274
|
+
for (const line of getRecommendation(report)) {
|
|
2275
|
+
lines.push(` ${line}`);
|
|
2276
|
+
}
|
|
2277
|
+
lines.push("");
|
|
2278
|
+
lines.push("=".repeat(W));
|
|
2279
|
+
return lines.join("\n");
|
|
2280
|
+
}
|
|
2281
|
+
function getRecommendation(report) {
|
|
2282
|
+
const lines = [];
|
|
2283
|
+
const failedTests = report.results.filter((r) => r.status === "fail");
|
|
2284
|
+
const avg = report.results.length > 0 ? Math.round(report.results.reduce((s, r) => s + r.accuracy, 0) / report.results.length) : 0;
|
|
2285
|
+
if (avg >= 90 && failedTests.length === 0) {
|
|
2286
|
+
lines.push("[+] Fully compatible -- suitable for all pipeline stages");
|
|
2287
|
+
return lines;
|
|
2288
|
+
}
|
|
2289
|
+
const stageResults = report.results.filter((r) => r.category === "stage-simulation");
|
|
2290
|
+
const workingStages = stageResults.filter((r) => r.status === "pass").map((r) => r.name.replace("_stage", ""));
|
|
2291
|
+
const failingStages = stageResults.filter((r) => r.status !== "pass").map((r) => r.name.replace("_stage", ""));
|
|
2292
|
+
if (workingStages.length > 0) {
|
|
2293
|
+
lines.push(`[+] Suitable for: ${workingStages.join(", ")} stages`);
|
|
2294
|
+
}
|
|
2295
|
+
if (failingStages.length > 0) {
|
|
2296
|
+
lines.push(`[x] Not recommended for: ${failingStages.join(", ")} stages`);
|
|
2297
|
+
}
|
|
2298
|
+
if (failedTests.length > 0) {
|
|
2299
|
+
lines.push("");
|
|
2300
|
+
lines.push("Failed tests:");
|
|
2301
|
+
for (const t of failedTests) {
|
|
2302
|
+
lines.push(` - ${t.name}: ${t.detail.slice(0, 60)}`);
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
return lines;
|
|
2306
|
+
}
|
|
2307
|
+
var CATEGORY_ORDER, CATEGORY_LABELS;
|
|
2308
|
+
var init_test_model_report = __esm({
|
|
2309
|
+
"src/cli/test-model-report.ts"() {
|
|
2310
|
+
"use strict";
|
|
2311
|
+
CATEGORY_ORDER = ["infrastructure", "basic", "tool-use", "stage-simulation", "advanced"];
|
|
2312
|
+
CATEGORY_LABELS = {
|
|
2313
|
+
infrastructure: "INFRASTRUCTURE",
|
|
2314
|
+
basic: "BASIC CAPABILITIES",
|
|
2315
|
+
"tool-use": "TOOL USE",
|
|
2316
|
+
"stage-simulation": "STAGE SIMULATION",
|
|
2317
|
+
advanced: "ADVANCED"
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
// src/cli/test-model-command.ts
|
|
2323
|
+
var test_model_command_exports = {};
|
|
2324
|
+
__export(test_model_command_exports, {
|
|
2325
|
+
runTestModelCommand: () => runTestModelCommand
|
|
2326
|
+
});
|
|
2327
|
+
import * as fs13 from "fs";
|
|
2328
|
+
import * as os3 from "os";
|
|
2329
|
+
import * as path12 from "path";
|
|
2330
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
2331
|
+
function parseTestModelArgs() {
|
|
2332
|
+
const args2 = process.argv.slice(3);
|
|
2333
|
+
function getArg3(flag) {
|
|
2334
|
+
const idx = args2.indexOf(flag);
|
|
2335
|
+
if (idx !== -1 && args2[idx + 1] && !args2[idx + 1].startsWith("--")) return args2[idx + 1];
|
|
2336
|
+
return void 0;
|
|
2337
|
+
}
|
|
2338
|
+
const hasFlag3 = (f) => args2.includes(f);
|
|
2339
|
+
if (hasFlag3("--help") || hasFlag3("-h")) {
|
|
2340
|
+
logger.info([
|
|
2341
|
+
"Usage: kody test-model --provider <provider> --model <model> --key <api-key> [options]",
|
|
2342
|
+
"",
|
|
2343
|
+
"Options:",
|
|
2344
|
+
" --provider LLM provider name (e.g. gemini, openai, mistral)",
|
|
2345
|
+
" --model Model identifier (e.g. gemini-2.5-flash)",
|
|
2346
|
+
" --key API key for the provider",
|
|
2347
|
+
" --key-env Read API key from this environment variable",
|
|
2348
|
+
" --skip-proxy Use an already-running LiteLLM proxy (don't start one)",
|
|
2349
|
+
" --litellm-url LiteLLM proxy URL (default: http://localhost:4099)",
|
|
2350
|
+
" --filter Comma-separated test names to run (default: all)",
|
|
2351
|
+
" --list List all available tests and exit"
|
|
2352
|
+
].join("\n"));
|
|
2353
|
+
process.exit(0);
|
|
2354
|
+
}
|
|
2355
|
+
if (hasFlag3("--list")) {
|
|
2356
|
+
for (const t of ALL_TESTS) {
|
|
2357
|
+
logger.info(` ${t.name.padEnd(24)} [${t.category}] ${t.description}`);
|
|
2358
|
+
}
|
|
2359
|
+
process.exit(0);
|
|
2360
|
+
}
|
|
2361
|
+
const provider = getArg3("--provider");
|
|
2362
|
+
const model = getArg3("--model");
|
|
2363
|
+
const key = getArg3("--key");
|
|
2364
|
+
const keyEnv = getArg3("--key-env");
|
|
2365
|
+
if (!provider || !model) {
|
|
2366
|
+
logger.error("Required: --provider <provider> --model <model> --key <key>");
|
|
2367
|
+
logger.error("Run with --help for usage.");
|
|
2368
|
+
process.exit(1);
|
|
2369
|
+
}
|
|
2370
|
+
let apiKey = key;
|
|
2371
|
+
if (!apiKey && keyEnv) apiKey = process.env[keyEnv];
|
|
2372
|
+
if (!apiKey) {
|
|
2373
|
+
logger.error("API key required: use --key <value> or --key-env <ENV_VAR>");
|
|
2374
|
+
process.exit(1);
|
|
2375
|
+
}
|
|
2376
|
+
return {
|
|
2377
|
+
provider,
|
|
2378
|
+
model,
|
|
2379
|
+
apiKey,
|
|
2380
|
+
proxyUrl: getArg3("--litellm-url") ?? TEST_URL,
|
|
2381
|
+
skipProxy: hasFlag3("--skip-proxy"),
|
|
2382
|
+
filter: getArg3("--filter")?.split(",")
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
function generateConfig(provider, model, dropParams) {
|
|
2386
|
+
const lines = [];
|
|
2387
|
+
if (dropParams) {
|
|
2388
|
+
lines.push("litellm_settings:");
|
|
2389
|
+
lines.push(" drop_params: true");
|
|
2390
|
+
lines.push("");
|
|
2391
|
+
}
|
|
2392
|
+
lines.push("model_list:");
|
|
2393
|
+
lines.push(` - model_name: ${model}`);
|
|
2394
|
+
lines.push(" litellm_params:");
|
|
2395
|
+
lines.push(` model: ${provider}/${model}`);
|
|
2396
|
+
lines.push(" api_key: os.environ/ANTHROPIC_COMPATIBLE_API_KEY");
|
|
2397
|
+
return lines.join("\n") + "\n";
|
|
2398
|
+
}
|
|
2399
|
+
async function startProxy(config, url) {
|
|
2400
|
+
try {
|
|
2401
|
+
execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
2402
|
+
} catch {
|
|
2403
|
+
try {
|
|
2404
|
+
execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
2405
|
+
} catch {
|
|
2406
|
+
logger.error("litellm not installed. Install: pip install 'litellm[proxy]'");
|
|
2407
|
+
return null;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
fs13.writeFileSync(CONFIG_PATH, config);
|
|
2411
|
+
const portMatch = url.match(/:(\d+)/);
|
|
2412
|
+
const port = portMatch ? portMatch[1] : "4099";
|
|
2413
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
2414
|
+
const child = spawn2("litellm", ["--config", CONFIG_PATH, "--port", port], {
|
|
2415
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2416
|
+
detached: true,
|
|
2417
|
+
env: process.env
|
|
2418
|
+
});
|
|
2419
|
+
for (let i = 0; i < 30; i++) {
|
|
2420
|
+
await delay(2e3);
|
|
2421
|
+
if (await checkLitellmHealth(url)) {
|
|
2422
|
+
logger.info(`LiteLLM proxy ready at ${url}`);
|
|
2423
|
+
return child;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
child.kill();
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
async function quickApiTest(url, model, apiKey) {
|
|
2430
|
+
try {
|
|
2431
|
+
const res = await fetch(`${url}/v1/messages`, {
|
|
2432
|
+
method: "POST",
|
|
2433
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
2434
|
+
body: JSON.stringify({
|
|
2435
|
+
model,
|
|
2436
|
+
max_tokens: 10,
|
|
2437
|
+
messages: [{ role: "user", content: "Say ok" }],
|
|
2438
|
+
context_management: { policy: "smart" }
|
|
2439
|
+
}),
|
|
2440
|
+
signal: AbortSignal.timeout(3e4)
|
|
2441
|
+
});
|
|
2442
|
+
if (!res.ok) {
|
|
2443
|
+
const body = await res.text();
|
|
2444
|
+
return { ok: false, error: body.slice(0, 200) };
|
|
2445
|
+
}
|
|
2446
|
+
return { ok: true };
|
|
2447
|
+
} catch (err) {
|
|
2448
|
+
return { ok: false, error: String(err) };
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function delay(ms) {
|
|
2452
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2453
|
+
}
|
|
2454
|
+
async function runTestModelCommand() {
|
|
2455
|
+
const opts = parseTestModelArgs();
|
|
2456
|
+
const startTime = Date.now();
|
|
2457
|
+
logger.info(`Testing model compatibility: ${opts.provider}/${opts.model}`);
|
|
2458
|
+
logger.info("");
|
|
2459
|
+
let proxyProcess = null;
|
|
2460
|
+
let dropParamsRequired = false;
|
|
2461
|
+
const cleanup = () => {
|
|
2462
|
+
if (proxyProcess) {
|
|
2463
|
+
proxyProcess.kill();
|
|
2464
|
+
proxyProcess = null;
|
|
2465
|
+
}
|
|
2466
|
+
fs13.rmSync(CONFIG_PATH, { force: true });
|
|
2467
|
+
};
|
|
2468
|
+
process.on("SIGINT", () => {
|
|
2469
|
+
cleanup();
|
|
2470
|
+
process.exit(1);
|
|
2471
|
+
});
|
|
2472
|
+
process.on("SIGTERM", () => {
|
|
2473
|
+
cleanup();
|
|
2474
|
+
process.exit(1);
|
|
2475
|
+
});
|
|
2476
|
+
try {
|
|
2477
|
+
if (!opts.skipProxy) {
|
|
2478
|
+
process.env.ANTHROPIC_COMPATIBLE_API_KEY = opts.apiKey;
|
|
2479
|
+
logger.info("Starting LiteLLM proxy (without drop_params)...");
|
|
2480
|
+
proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, false), opts.proxyUrl);
|
|
2481
|
+
if (!proxyProcess) {
|
|
2482
|
+
logger.error("Failed to start LiteLLM proxy");
|
|
2483
|
+
process.exit(1);
|
|
2484
|
+
}
|
|
2485
|
+
const quickRes = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
|
|
2486
|
+
if (!quickRes.ok) {
|
|
2487
|
+
logger.info("Model needs drop_params: true -- restarting proxy...");
|
|
2488
|
+
proxyProcess.kill();
|
|
2489
|
+
proxyProcess = null;
|
|
2490
|
+
await delay(2e3);
|
|
2491
|
+
proxyProcess = await startProxy(generateConfig(opts.provider, opts.model, true), opts.proxyUrl);
|
|
2492
|
+
dropParamsRequired = true;
|
|
2493
|
+
if (!proxyProcess) {
|
|
2494
|
+
logger.error("Failed to start LiteLLM proxy with drop_params");
|
|
2495
|
+
process.exit(1);
|
|
2496
|
+
}
|
|
2497
|
+
const retry = await quickApiTest(opts.proxyUrl, opts.model, opts.apiKey);
|
|
2498
|
+
if (!retry.ok) {
|
|
2499
|
+
logger.error(`Model not accessible: ${retry.error}`);
|
|
2500
|
+
process.exit(1);
|
|
2501
|
+
}
|
|
2502
|
+
logger.info("Proxy restarted with drop_params: true");
|
|
2503
|
+
} else {
|
|
2504
|
+
logger.info("drop_params not required");
|
|
2505
|
+
}
|
|
2506
|
+
} else {
|
|
2507
|
+
logger.info(`Using existing proxy at ${opts.proxyUrl}`);
|
|
2508
|
+
}
|
|
2509
|
+
const tests = opts.filter ? ALL_TESTS.filter((t) => opts.filter.includes(t.name)) : ALL_TESTS;
|
|
2510
|
+
logger.info(`Running ${tests.length} compatibility tests...`);
|
|
2511
|
+
logger.info("");
|
|
2512
|
+
const ctx = { proxyUrl: opts.proxyUrl, model: opts.model, apiKey: opts.apiKey, projectDir: process.cwd() };
|
|
2513
|
+
const results = [];
|
|
2514
|
+
for (const test of tests) {
|
|
2515
|
+
process.stdout.write(` ${test.name.padEnd(28)} `);
|
|
2516
|
+
try {
|
|
2517
|
+
const r = await test.run(ctx);
|
|
2518
|
+
results.push(r);
|
|
2519
|
+
const icon = r.status === "pass" ? "+" : r.status === "fail" ? "x" : "!";
|
|
2520
|
+
logger.info(`[${icon}] ${r.status.toUpperCase()} ${r.accuracy}% (${(r.durationMs / 1e3).toFixed(1)}s)`);
|
|
2521
|
+
} catch (err) {
|
|
2522
|
+
const r = {
|
|
2523
|
+
name: test.name,
|
|
2524
|
+
category: test.category,
|
|
2525
|
+
status: "fail",
|
|
2526
|
+
accuracy: 0,
|
|
2527
|
+
durationMs: 0,
|
|
2528
|
+
detail: `Crash: ${err instanceof Error ? err.message : String(err)}`
|
|
2529
|
+
};
|
|
2530
|
+
results.push(r);
|
|
2531
|
+
logger.info("[x] CRASH");
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
const report = {
|
|
2535
|
+
provider: opts.provider,
|
|
2536
|
+
model: opts.model,
|
|
2537
|
+
results,
|
|
2538
|
+
totalDurationMs: Date.now() - startTime,
|
|
2539
|
+
dropParamsRequired,
|
|
2540
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19)
|
|
2541
|
+
};
|
|
2542
|
+
console.log("");
|
|
2543
|
+
console.log(formatReport(report));
|
|
2544
|
+
const failed = results.filter((r) => r.status === "fail").length;
|
|
2545
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
2546
|
+
} finally {
|
|
2547
|
+
cleanup();
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
var TEST_PORT, TEST_URL, CONFIG_PATH;
|
|
2551
|
+
var init_test_model_command = __esm({
|
|
2552
|
+
"src/cli/test-model-command.ts"() {
|
|
2553
|
+
"use strict";
|
|
2554
|
+
init_logger();
|
|
2555
|
+
init_litellm();
|
|
2556
|
+
init_test_model_tests();
|
|
2557
|
+
init_test_model_report();
|
|
2558
|
+
TEST_PORT = 4099;
|
|
2559
|
+
TEST_URL = `http://localhost:${TEST_PORT}`;
|
|
2560
|
+
CONFIG_PATH = path12.join(os3.tmpdir(), "kody-test-model-config.yaml");
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2563
|
+
|
|
1559
2564
|
// src/ci/parse-inputs.ts
|
|
1560
2565
|
var parse_inputs_exports = {};
|
|
1561
2566
|
__export(parse_inputs_exports, {
|
|
@@ -1563,16 +2568,16 @@ __export(parse_inputs_exports, {
|
|
|
1563
2568
|
runCiParse: () => runCiParse,
|
|
1564
2569
|
writeOutputs: () => writeOutputs
|
|
1565
2570
|
});
|
|
1566
|
-
import * as
|
|
2571
|
+
import * as fs14 from "fs";
|
|
1567
2572
|
function generateTimestamp() {
|
|
1568
2573
|
const now = /* @__PURE__ */ new Date();
|
|
1569
|
-
const
|
|
2574
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
1570
2575
|
const y = String(now.getFullYear()).slice(2);
|
|
1571
|
-
const m =
|
|
1572
|
-
const d =
|
|
1573
|
-
const H =
|
|
1574
|
-
const M =
|
|
1575
|
-
const S =
|
|
2576
|
+
const m = pad2(now.getMonth() + 1);
|
|
2577
|
+
const d = pad2(now.getDate());
|
|
2578
|
+
const H = pad2(now.getHours());
|
|
2579
|
+
const M = pad2(now.getMinutes());
|
|
2580
|
+
const S = pad2(now.getSeconds());
|
|
1576
2581
|
return `${y}${m}${d}-${H}${M}${S}`;
|
|
1577
2582
|
}
|
|
1578
2583
|
function parseCommentInputs() {
|
|
@@ -1724,40 +2729,40 @@ function parseCommentInputs() {
|
|
|
1724
2729
|
trigger_type: "comment"
|
|
1725
2730
|
};
|
|
1726
2731
|
}
|
|
1727
|
-
function writeOutputs(
|
|
2732
|
+
function writeOutputs(result2) {
|
|
1728
2733
|
const outputFile = process.env.GITHUB_OUTPUT;
|
|
1729
2734
|
function output(key, value) {
|
|
1730
2735
|
if (outputFile) {
|
|
1731
2736
|
if (value.includes("\n")) {
|
|
1732
|
-
|
|
2737
|
+
fs14.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
1733
2738
|
${value}
|
|
1734
2739
|
KODY_EOF
|
|
1735
2740
|
`);
|
|
1736
2741
|
} else {
|
|
1737
|
-
|
|
2742
|
+
fs14.appendFileSync(outputFile, `${key}=${value}
|
|
1738
2743
|
`);
|
|
1739
2744
|
}
|
|
1740
2745
|
}
|
|
1741
2746
|
const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
|
|
1742
2747
|
console.log(`${key}=${display}`);
|
|
1743
2748
|
}
|
|
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",
|
|
2749
|
+
output("task_id", result2.task_id);
|
|
2750
|
+
output("mode", result2.mode);
|
|
2751
|
+
output("from_stage", result2.from_stage);
|
|
2752
|
+
output("issue_number", result2.issue_number);
|
|
2753
|
+
output("pr_number", result2.pr_number);
|
|
2754
|
+
output("feedback", result2.feedback);
|
|
2755
|
+
output("complexity", result2.complexity);
|
|
2756
|
+
output("ci_run_id", result2.ci_run_id);
|
|
2757
|
+
output("ticket_id", result2.ticket_id);
|
|
2758
|
+
output("prd_file", result2.prd_file);
|
|
2759
|
+
output("dry_run", result2.dry_run ? "true" : "false");
|
|
2760
|
+
output("valid", result2.valid ? "true" : "false");
|
|
2761
|
+
output("trigger_type", result2.trigger_type);
|
|
1757
2762
|
}
|
|
1758
2763
|
function runCiParse() {
|
|
1759
|
-
const
|
|
1760
|
-
writeOutputs(
|
|
2764
|
+
const result2 = parseCommentInputs();
|
|
2765
|
+
writeOutputs(result2);
|
|
1761
2766
|
}
|
|
1762
2767
|
var VALID_MODES;
|
|
1763
2768
|
var init_parse_inputs = __esm({
|
|
@@ -1859,7 +2864,7 @@ var init_definitions = __esm({
|
|
|
1859
2864
|
});
|
|
1860
2865
|
|
|
1861
2866
|
// src/git-utils.ts
|
|
1862
|
-
import { execFileSync as
|
|
2867
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
1863
2868
|
function getHookSafeEnv() {
|
|
1864
2869
|
if (!_hookSafeEnv) {
|
|
1865
2870
|
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
@@ -1867,7 +2872,7 @@ function getHookSafeEnv() {
|
|
|
1867
2872
|
return _hookSafeEnv;
|
|
1868
2873
|
}
|
|
1869
2874
|
function git(args2, options) {
|
|
1870
|
-
return
|
|
2875
|
+
return execFileSync11("git", args2, {
|
|
1871
2876
|
encoding: "utf-8",
|
|
1872
2877
|
timeout: options?.timeout ?? 3e4,
|
|
1873
2878
|
cwd: options?.cwd,
|
|
@@ -2053,22 +3058,22 @@ var init_git_utils = __esm({
|
|
|
2053
3058
|
});
|
|
2054
3059
|
|
|
2055
3060
|
// src/pipeline/state.ts
|
|
2056
|
-
import * as
|
|
2057
|
-
import * as
|
|
3061
|
+
import * as fs15 from "fs";
|
|
3062
|
+
import * as path13 from "path";
|
|
2058
3063
|
function loadState(taskId, taskDir) {
|
|
2059
|
-
const p =
|
|
2060
|
-
if (!
|
|
3064
|
+
const p = path13.join(taskDir, "status.json");
|
|
3065
|
+
if (!fs15.existsSync(p)) return null;
|
|
2061
3066
|
try {
|
|
2062
|
-
const
|
|
2063
|
-
|
|
3067
|
+
const result2 = parseJsonSafe(
|
|
3068
|
+
fs15.readFileSync(p, "utf-8"),
|
|
2064
3069
|
["taskId", "state", "stages", "createdAt", "updatedAt"]
|
|
2065
3070
|
);
|
|
2066
|
-
if (!
|
|
2067
|
-
logger.warn(` Corrupt status.json: ${
|
|
3071
|
+
if (!result2.ok) {
|
|
3072
|
+
logger.warn(` Corrupt status.json: ${result2.error}`);
|
|
2068
3073
|
return null;
|
|
2069
3074
|
}
|
|
2070
|
-
if (
|
|
2071
|
-
return
|
|
3075
|
+
if (result2.data.taskId !== taskId) return null;
|
|
3076
|
+
return result2.data;
|
|
2072
3077
|
} catch {
|
|
2073
3078
|
return null;
|
|
2074
3079
|
}
|
|
@@ -2078,10 +3083,10 @@ function writeState(state, taskDir) {
|
|
|
2078
3083
|
...state,
|
|
2079
3084
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2080
3085
|
};
|
|
2081
|
-
const target =
|
|
3086
|
+
const target = path13.join(taskDir, "status.json");
|
|
2082
3087
|
const tmp = target + ".tmp";
|
|
2083
|
-
|
|
2084
|
-
|
|
3088
|
+
fs15.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
3089
|
+
fs15.renameSync(tmp, target);
|
|
2085
3090
|
return updated;
|
|
2086
3091
|
}
|
|
2087
3092
|
function initState(taskId) {
|
|
@@ -2122,16 +3127,16 @@ var init_complexity = __esm({
|
|
|
2122
3127
|
});
|
|
2123
3128
|
|
|
2124
3129
|
// src/memory.ts
|
|
2125
|
-
import * as
|
|
2126
|
-
import * as
|
|
3130
|
+
import * as fs16 from "fs";
|
|
3131
|
+
import * as path14 from "path";
|
|
2127
3132
|
function readProjectMemory(projectDir) {
|
|
2128
|
-
const memoryDir =
|
|
2129
|
-
if (!
|
|
2130
|
-
const files =
|
|
3133
|
+
const memoryDir = path14.join(projectDir, ".kody", "memory");
|
|
3134
|
+
if (!fs16.existsSync(memoryDir)) return "";
|
|
3135
|
+
const files = fs16.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
2131
3136
|
if (files.length === 0) return "";
|
|
2132
3137
|
const sections = [];
|
|
2133
3138
|
for (const file of files) {
|
|
2134
|
-
const content =
|
|
3139
|
+
const content = fs16.readFileSync(path14.join(memoryDir, file), "utf-8").trim();
|
|
2135
3140
|
if (content) {
|
|
2136
3141
|
sections.push(`## ${file.replace(".md", "")}
|
|
2137
3142
|
${content}`);
|
|
@@ -2150,8 +3155,8 @@ var init_memory = __esm({
|
|
|
2150
3155
|
});
|
|
2151
3156
|
|
|
2152
3157
|
// src/context-tiers.ts
|
|
2153
|
-
import * as
|
|
2154
|
-
import * as
|
|
3158
|
+
import * as fs17 from "fs";
|
|
3159
|
+
import * as path15 from "path";
|
|
2155
3160
|
function estimateTokens(text) {
|
|
2156
3161
|
return Math.ceil(text.length / 4);
|
|
2157
3162
|
}
|
|
@@ -2178,8 +3183,8 @@ function generateL0(content, filename) {
|
|
|
2178
3183
|
break;
|
|
2179
3184
|
}
|
|
2180
3185
|
}
|
|
2181
|
-
const
|
|
2182
|
-
return
|
|
3186
|
+
const result2 = parts.join("\n");
|
|
3187
|
+
return result2.slice(0, L0_MAX_CHARS);
|
|
2183
3188
|
}
|
|
2184
3189
|
function generateL0Json(content) {
|
|
2185
3190
|
try {
|
|
@@ -2221,8 +3226,8 @@ function generateL1(content, filename) {
|
|
|
2221
3226
|
inSection = false;
|
|
2222
3227
|
}
|
|
2223
3228
|
}
|
|
2224
|
-
const
|
|
2225
|
-
return
|
|
3229
|
+
const result2 = parts.join("\n");
|
|
3230
|
+
return result2.slice(0, L1_MAX_CHARS);
|
|
2226
3231
|
}
|
|
2227
3232
|
function generateL1Json(content) {
|
|
2228
3233
|
try {
|
|
@@ -2242,7 +3247,7 @@ function generateL1Json(content) {
|
|
|
2242
3247
|
}
|
|
2243
3248
|
}
|
|
2244
3249
|
function getTieredContent(filePath, content) {
|
|
2245
|
-
const key =
|
|
3250
|
+
const key = path15.basename(filePath);
|
|
2246
3251
|
return {
|
|
2247
3252
|
source: filePath,
|
|
2248
3253
|
L0: generateL0(content, key),
|
|
@@ -2254,15 +3259,15 @@ function selectTier(tiered, tier) {
|
|
|
2254
3259
|
return tiered[tier];
|
|
2255
3260
|
}
|
|
2256
3261
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
2257
|
-
const memoryDir =
|
|
2258
|
-
if (!
|
|
2259
|
-
const files =
|
|
3262
|
+
const memoryDir = path15.join(projectDir, ".kody", "memory");
|
|
3263
|
+
if (!fs17.existsSync(memoryDir)) return "";
|
|
3264
|
+
const files = fs17.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
2260
3265
|
if (files.length === 0) return "";
|
|
2261
3266
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
2262
3267
|
const sections = [];
|
|
2263
3268
|
for (const file of files) {
|
|
2264
|
-
const filePath =
|
|
2265
|
-
const content =
|
|
3269
|
+
const filePath = path15.join(memoryDir, file);
|
|
3270
|
+
const content = fs17.readFileSync(filePath, "utf-8").trim();
|
|
2266
3271
|
if (!content) continue;
|
|
2267
3272
|
const tiered = getTieredContent(filePath, content);
|
|
2268
3273
|
const selected = selectTier(tiered, tier);
|
|
@@ -2285,9 +3290,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
2285
3290
|
`;
|
|
2286
3291
|
context += `Task Directory: ${taskDir}
|
|
2287
3292
|
`;
|
|
2288
|
-
const taskMdPath =
|
|
2289
|
-
if (
|
|
2290
|
-
const content =
|
|
3293
|
+
const taskMdPath = path15.join(taskDir, "task.md");
|
|
3294
|
+
if (fs17.existsSync(taskMdPath)) {
|
|
3295
|
+
const content = fs17.readFileSync(taskMdPath, "utf-8");
|
|
2291
3296
|
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
2292
3297
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
2293
3298
|
context += `
|
|
@@ -2295,9 +3300,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
2295
3300
|
${selected}
|
|
2296
3301
|
`;
|
|
2297
3302
|
}
|
|
2298
|
-
const taskJsonPath =
|
|
2299
|
-
if (
|
|
2300
|
-
const content =
|
|
3303
|
+
const taskJsonPath = path15.join(taskDir, "task.json");
|
|
3304
|
+
if (fs17.existsSync(taskJsonPath)) {
|
|
3305
|
+
const content = fs17.readFileSync(taskJsonPath, "utf-8");
|
|
2301
3306
|
if (policy.taskClassification === "L2") {
|
|
2302
3307
|
try {
|
|
2303
3308
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -2323,9 +3328,9 @@ ${selected}
|
|
|
2323
3328
|
}
|
|
2324
3329
|
}
|
|
2325
3330
|
}
|
|
2326
|
-
const specPath =
|
|
2327
|
-
if (
|
|
2328
|
-
const content =
|
|
3331
|
+
const specPath = path15.join(taskDir, "spec.md");
|
|
3332
|
+
if (fs17.existsSync(specPath)) {
|
|
3333
|
+
const content = fs17.readFileSync(specPath, "utf-8");
|
|
2329
3334
|
const selected = selectContent(specPath, content, policy.spec);
|
|
2330
3335
|
const label = tierLabel("Spec", policy.spec);
|
|
2331
3336
|
context += `
|
|
@@ -2333,9 +3338,9 @@ ${selected}
|
|
|
2333
3338
|
${selected}
|
|
2334
3339
|
`;
|
|
2335
3340
|
}
|
|
2336
|
-
const planPath =
|
|
2337
|
-
if (
|
|
2338
|
-
const content =
|
|
3341
|
+
const planPath = path15.join(taskDir, "plan.md");
|
|
3342
|
+
if (fs17.existsSync(planPath)) {
|
|
3343
|
+
const content = fs17.readFileSync(planPath, "utf-8");
|
|
2339
3344
|
const selected = selectContent(planPath, content, policy.plan);
|
|
2340
3345
|
const label = tierLabel("Plan", policy.plan);
|
|
2341
3346
|
context += `
|
|
@@ -2343,9 +3348,9 @@ ${selected}
|
|
|
2343
3348
|
${selected}
|
|
2344
3349
|
`;
|
|
2345
3350
|
}
|
|
2346
|
-
const contextMdPath =
|
|
2347
|
-
if (
|
|
2348
|
-
const content =
|
|
3351
|
+
const contextMdPath = path15.join(taskDir, "context.md");
|
|
3352
|
+
if (fs17.existsSync(contextMdPath)) {
|
|
3353
|
+
const content = fs17.readFileSync(contextMdPath, "utf-8");
|
|
2349
3354
|
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
2350
3355
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
2351
3356
|
context += `
|
|
@@ -2431,24 +3436,24 @@ var init_context_tiers = __esm({
|
|
|
2431
3436
|
});
|
|
2432
3437
|
|
|
2433
3438
|
// src/context.ts
|
|
2434
|
-
import * as
|
|
2435
|
-
import * as
|
|
3439
|
+
import * as fs18 from "fs";
|
|
3440
|
+
import * as path16 from "path";
|
|
2436
3441
|
function readPromptFile(stageName, projectDir) {
|
|
2437
3442
|
if (projectDir) {
|
|
2438
|
-
const stepFile =
|
|
2439
|
-
if (
|
|
2440
|
-
return
|
|
3443
|
+
const stepFile = path16.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
3444
|
+
if (fs18.existsSync(stepFile)) {
|
|
3445
|
+
return fs18.readFileSync(stepFile, "utf-8");
|
|
2441
3446
|
}
|
|
2442
3447
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
2443
3448
|
}
|
|
2444
3449
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
2445
3450
|
const candidates = [
|
|
2446
|
-
|
|
2447
|
-
|
|
3451
|
+
path16.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
3452
|
+
path16.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
2448
3453
|
];
|
|
2449
3454
|
for (const candidate of candidates) {
|
|
2450
|
-
if (
|
|
2451
|
-
return
|
|
3455
|
+
if (fs18.existsSync(candidate)) {
|
|
3456
|
+
return fs18.readFileSync(candidate, "utf-8");
|
|
2452
3457
|
}
|
|
2453
3458
|
}
|
|
2454
3459
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -2460,18 +3465,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
2460
3465
|
`;
|
|
2461
3466
|
context += `Task Directory: ${taskDir}
|
|
2462
3467
|
`;
|
|
2463
|
-
const taskMdPath =
|
|
2464
|
-
if (
|
|
2465
|
-
const taskMd =
|
|
3468
|
+
const taskMdPath = path16.join(taskDir, "task.md");
|
|
3469
|
+
if (fs18.existsSync(taskMdPath)) {
|
|
3470
|
+
const taskMd = fs18.readFileSync(taskMdPath, "utf-8");
|
|
2466
3471
|
context += `
|
|
2467
3472
|
## Task Description
|
|
2468
3473
|
${taskMd}
|
|
2469
3474
|
`;
|
|
2470
3475
|
}
|
|
2471
|
-
const taskJsonPath =
|
|
2472
|
-
if (
|
|
3476
|
+
const taskJsonPath = path16.join(taskDir, "task.json");
|
|
3477
|
+
if (fs18.existsSync(taskJsonPath)) {
|
|
2473
3478
|
try {
|
|
2474
|
-
const taskDef = JSON.parse(
|
|
3479
|
+
const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
|
|
2475
3480
|
context += `
|
|
2476
3481
|
## Task Classification
|
|
2477
3482
|
`;
|
|
@@ -2484,27 +3489,27 @@ ${taskMd}
|
|
|
2484
3489
|
} catch {
|
|
2485
3490
|
}
|
|
2486
3491
|
}
|
|
2487
|
-
const specPath =
|
|
2488
|
-
if (
|
|
2489
|
-
const spec =
|
|
3492
|
+
const specPath = path16.join(taskDir, "spec.md");
|
|
3493
|
+
if (fs18.existsSync(specPath)) {
|
|
3494
|
+
const spec = fs18.readFileSync(specPath, "utf-8");
|
|
2490
3495
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
2491
3496
|
context += `
|
|
2492
3497
|
## Spec Summary
|
|
2493
3498
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
2494
3499
|
`;
|
|
2495
3500
|
}
|
|
2496
|
-
const planPath =
|
|
2497
|
-
if (
|
|
2498
|
-
const plan =
|
|
3501
|
+
const planPath = path16.join(taskDir, "plan.md");
|
|
3502
|
+
if (fs18.existsSync(planPath)) {
|
|
3503
|
+
const plan = fs18.readFileSync(planPath, "utf-8");
|
|
2499
3504
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
2500
3505
|
context += `
|
|
2501
3506
|
## Plan Summary
|
|
2502
3507
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
2503
3508
|
`;
|
|
2504
3509
|
}
|
|
2505
|
-
const contextMdPath =
|
|
2506
|
-
if (
|
|
2507
|
-
const accumulated =
|
|
3510
|
+
const contextMdPath = path16.join(taskDir, "context.md");
|
|
3511
|
+
if (fs18.existsSync(contextMdPath)) {
|
|
3512
|
+
const accumulated = fs18.readFileSync(contextMdPath, "utf-8");
|
|
2508
3513
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
2509
3514
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
2510
3515
|
context += `
|
|
@@ -2522,17 +3527,17 @@ ${feedback}
|
|
|
2522
3527
|
}
|
|
2523
3528
|
function inferHasUIFromScope(scope) {
|
|
2524
3529
|
return scope.some((filePath) => {
|
|
2525
|
-
const ext =
|
|
3530
|
+
const ext = path16.extname(filePath).toLowerCase();
|
|
2526
3531
|
if (UI_EXTENSIONS.has(ext)) return true;
|
|
2527
3532
|
const normalized = filePath.replace(/\\/g, "/");
|
|
2528
3533
|
return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
|
|
2529
3534
|
});
|
|
2530
3535
|
}
|
|
2531
3536
|
function taskHasUI(taskDir) {
|
|
2532
|
-
const taskJsonPath =
|
|
2533
|
-
if (!
|
|
3537
|
+
const taskJsonPath = path16.join(taskDir, "task.json");
|
|
3538
|
+
if (!fs18.existsSync(taskJsonPath)) return true;
|
|
2534
3539
|
try {
|
|
2535
|
-
const taskDef = JSON.parse(
|
|
3540
|
+
const taskDef = JSON.parse(fs18.readFileSync(taskJsonPath, "utf-8"));
|
|
2536
3541
|
const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
|
|
2537
3542
|
if (scope.length === 0) return true;
|
|
2538
3543
|
return inferHasUIFromScope(scope);
|
|
@@ -2654,9 +3659,9 @@ ${prompt}` : prompt;
|
|
|
2654
3659
|
}
|
|
2655
3660
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
2656
3661
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
2657
|
-
const qaGuidePath =
|
|
2658
|
-
if (
|
|
2659
|
-
const qaGuide =
|
|
3662
|
+
const qaGuidePath = path16.join(projectDir, ".kody", "qa-guide.md");
|
|
3663
|
+
if (fs18.existsSync(qaGuidePath)) {
|
|
3664
|
+
const qaGuide = fs18.readFileSync(qaGuidePath, "utf-8").trim();
|
|
2660
3665
|
assembled = assembled + "\n\n" + qaGuide;
|
|
2661
3666
|
}
|
|
2662
3667
|
}
|
|
@@ -2686,10 +3691,12 @@ function escalateModelTier(currentTier) {
|
|
|
2686
3691
|
function resolveModel(modelTier, stageName) {
|
|
2687
3692
|
const config = getProjectConfig();
|
|
2688
3693
|
const mapped = config.agent.modelMap[modelTier];
|
|
2689
|
-
if (mapped)
|
|
2690
|
-
|
|
3694
|
+
if (!mapped) {
|
|
3695
|
+
throw new Error(`No model configured for tier '${modelTier}'. Set agent.modelMap.${modelTier} in kody.config.json`);
|
|
3696
|
+
}
|
|
3697
|
+
return mapped;
|
|
2691
3698
|
}
|
|
2692
|
-
var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION
|
|
3699
|
+
var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
|
|
2693
3700
|
var init_context = __esm({
|
|
2694
3701
|
"src/context.ts"() {
|
|
2695
3702
|
"use strict";
|
|
@@ -2723,11 +3730,6 @@ var init_context = __esm({
|
|
|
2723
3730
|
mid: "strong",
|
|
2724
3731
|
strong: "strong"
|
|
2725
3732
|
};
|
|
2726
|
-
DEFAULT_MODEL_MAP = {
|
|
2727
|
-
cheap: "haiku",
|
|
2728
|
-
mid: "sonnet",
|
|
2729
|
-
strong: "opus"
|
|
2730
|
-
};
|
|
2731
3733
|
}
|
|
2732
3734
|
});
|
|
2733
3735
|
|
|
@@ -2751,8 +3753,8 @@ var init_runner_selection = __esm({
|
|
|
2751
3753
|
});
|
|
2752
3754
|
|
|
2753
3755
|
// src/stages/agent.ts
|
|
2754
|
-
import * as
|
|
2755
|
-
import * as
|
|
3756
|
+
import * as fs19 from "fs";
|
|
3757
|
+
import * as path17 from "path";
|
|
2756
3758
|
function getSessionInfo(stageName, sessions) {
|
|
2757
3759
|
const group = SESSION_GROUP[stageName];
|
|
2758
3760
|
if (!group) return void 0;
|
|
@@ -2837,29 +3839,29 @@ async function executeAgentStage(ctx, def) {
|
|
|
2837
3839
|
if (lastResult.outcome !== "completed") {
|
|
2838
3840
|
return { outcome: lastResult.outcome, error: lastResult.error, retries };
|
|
2839
3841
|
}
|
|
2840
|
-
const
|
|
2841
|
-
if (def.outputFile &&
|
|
2842
|
-
|
|
3842
|
+
const result2 = lastResult;
|
|
3843
|
+
if (def.outputFile && result2.output) {
|
|
3844
|
+
fs19.writeFileSync(path17.join(ctx.taskDir, def.outputFile), result2.output);
|
|
2843
3845
|
}
|
|
2844
3846
|
if (def.outputFile) {
|
|
2845
|
-
const outputPath =
|
|
2846
|
-
if (!
|
|
2847
|
-
const ext =
|
|
2848
|
-
const base =
|
|
2849
|
-
const files =
|
|
3847
|
+
const outputPath = path17.join(ctx.taskDir, def.outputFile);
|
|
3848
|
+
if (!fs19.existsSync(outputPath)) {
|
|
3849
|
+
const ext = path17.extname(def.outputFile);
|
|
3850
|
+
const base = path17.basename(def.outputFile, ext);
|
|
3851
|
+
const files = fs19.readdirSync(ctx.taskDir);
|
|
2850
3852
|
const variant = files.find(
|
|
2851
3853
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
2852
3854
|
);
|
|
2853
3855
|
if (variant) {
|
|
2854
|
-
|
|
3856
|
+
fs19.renameSync(path17.join(ctx.taskDir, variant), outputPath);
|
|
2855
3857
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
2856
3858
|
}
|
|
2857
3859
|
}
|
|
2858
3860
|
}
|
|
2859
3861
|
if (def.outputFile) {
|
|
2860
|
-
const outputPath =
|
|
2861
|
-
if (
|
|
2862
|
-
const content =
|
|
3862
|
+
const outputPath = path17.join(ctx.taskDir, def.outputFile);
|
|
3863
|
+
if (fs19.existsSync(outputPath)) {
|
|
3864
|
+
const content = fs19.readFileSync(outputPath, "utf-8");
|
|
2863
3865
|
const validation = validateStageOutput(def.name, content);
|
|
2864
3866
|
if (!validation.valid) {
|
|
2865
3867
|
if (def.name === "taskify") {
|
|
@@ -2873,7 +3875,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2873
3875
|
const stripped = stripFences(retryResult.output);
|
|
2874
3876
|
const retryValidation = validateTaskJson(stripped);
|
|
2875
3877
|
if (retryValidation.valid) {
|
|
2876
|
-
|
|
3878
|
+
fs19.writeFileSync(outputPath, retryResult.output);
|
|
2877
3879
|
logger.info(` taskify retry produced valid JSON`);
|
|
2878
3880
|
} else {
|
|
2879
3881
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -2886,7 +3888,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2886
3888
|
risk_level: "low",
|
|
2887
3889
|
questions: []
|
|
2888
3890
|
}, null, 2);
|
|
2889
|
-
|
|
3891
|
+
fs19.writeFileSync(outputPath, fallback);
|
|
2890
3892
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
2891
3893
|
}
|
|
2892
3894
|
}
|
|
@@ -2896,11 +3898,11 @@ async function executeAgentStage(ctx, def) {
|
|
|
2896
3898
|
}
|
|
2897
3899
|
}
|
|
2898
3900
|
}
|
|
2899
|
-
appendStageContext(ctx.taskDir, def.name,
|
|
3901
|
+
appendStageContext(ctx.taskDir, def.name, result2.output);
|
|
2900
3902
|
return { outcome: "completed", outputFile: def.outputFile, retries };
|
|
2901
3903
|
}
|
|
2902
3904
|
function appendStageContext(taskDir, stageName, output) {
|
|
2903
|
-
const contextPath =
|
|
3905
|
+
const contextPath = path17.join(taskDir, "context.md");
|
|
2904
3906
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
2905
3907
|
let summary;
|
|
2906
3908
|
if (output && output.trim()) {
|
|
@@ -2913,7 +3915,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
2913
3915
|
### ${stageName} (${timestamp2})
|
|
2914
3916
|
${summary}
|
|
2915
3917
|
`;
|
|
2916
|
-
|
|
3918
|
+
fs19.appendFileSync(contextPath, entry);
|
|
2917
3919
|
}
|
|
2918
3920
|
var SESSION_GROUP;
|
|
2919
3921
|
var init_agent = __esm({
|
|
@@ -2936,7 +3938,7 @@ var init_agent = __esm({
|
|
|
2936
3938
|
});
|
|
2937
3939
|
|
|
2938
3940
|
// src/verify-runner.ts
|
|
2939
|
-
import { execFileSync as
|
|
3941
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
2940
3942
|
function isExecError(err) {
|
|
2941
3943
|
return typeof err === "object" && err !== null;
|
|
2942
3944
|
}
|
|
@@ -2972,7 +3974,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
2972
3974
|
return { success: true, output: "", timedOut: false };
|
|
2973
3975
|
}
|
|
2974
3976
|
try {
|
|
2975
|
-
const output =
|
|
3977
|
+
const output = execFileSync12(parts[0], parts.slice(1), {
|
|
2976
3978
|
cwd,
|
|
2977
3979
|
timeout,
|
|
2978
3980
|
encoding: "utf-8",
|
|
@@ -3018,19 +4020,19 @@ function runQualityGates(taskDir, projectRoot) {
|
|
|
3018
4020
|
for (const { name, cmd } of commands) {
|
|
3019
4021
|
if (!cmd) continue;
|
|
3020
4022
|
logger.info(` Running ${name}: ${cmd}`);
|
|
3021
|
-
const
|
|
3022
|
-
if (
|
|
4023
|
+
const result2 = runCommand(cmd, cwd, VERIFY_COMMAND_TIMEOUT_MS);
|
|
4024
|
+
if (result2.timedOut) {
|
|
3023
4025
|
allErrors.push(`${name}: timed out after ${VERIFY_COMMAND_TIMEOUT_MS / 1e3}s`);
|
|
3024
4026
|
allPass = false;
|
|
3025
4027
|
continue;
|
|
3026
4028
|
}
|
|
3027
|
-
if (!
|
|
4029
|
+
if (!result2.success) {
|
|
3028
4030
|
allPass = false;
|
|
3029
|
-
const errors = parseErrors(
|
|
4031
|
+
const errors = parseErrors(result2.output);
|
|
3030
4032
|
allErrors.push(...errors.map((e) => `[${name}] ${e}`));
|
|
3031
|
-
rawOutputs.push({ name, output:
|
|
4033
|
+
rawOutputs.push({ name, output: result2.output.slice(-3e3) });
|
|
3032
4034
|
}
|
|
3033
|
-
allSummary.push(...extractSummary(
|
|
4035
|
+
allSummary.push(...extractSummary(result2.output, name));
|
|
3034
4036
|
}
|
|
3035
4037
|
return { pass: allPass, errors: allErrors, summary: allSummary, rawOutputs };
|
|
3036
4038
|
}
|
|
@@ -3043,7 +4045,7 @@ var init_verify_runner = __esm({
|
|
|
3043
4045
|
});
|
|
3044
4046
|
|
|
3045
4047
|
// src/observer.ts
|
|
3046
|
-
import { execFileSync as
|
|
4048
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
3047
4049
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
3048
4050
|
const context = [
|
|
3049
4051
|
`Stage: ${stageName}`,
|
|
@@ -3057,7 +4059,7 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
3057
4059
|
].join("\n");
|
|
3058
4060
|
const prompt = DIAGNOSIS_PROMPT + context;
|
|
3059
4061
|
try {
|
|
3060
|
-
const
|
|
4062
|
+
const result2 = await runner.run(
|
|
3061
4063
|
"diagnosis",
|
|
3062
4064
|
prompt,
|
|
3063
4065
|
model,
|
|
@@ -3066,8 +4068,8 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
3066
4068
|
"",
|
|
3067
4069
|
options
|
|
3068
4070
|
);
|
|
3069
|
-
if (
|
|
3070
|
-
const cleaned =
|
|
4071
|
+
if (result2.outcome === "completed" && result2.output) {
|
|
4072
|
+
const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
3071
4073
|
const parseResult = parseJsonSafe(cleaned, ["classification"]);
|
|
3072
4074
|
if (parseResult.ok) {
|
|
3073
4075
|
const { data } = parseResult;
|
|
@@ -3103,13 +4105,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
3103
4105
|
}
|
|
3104
4106
|
function getModifiedFiles(projectDir) {
|
|
3105
4107
|
try {
|
|
3106
|
-
const staged =
|
|
4108
|
+
const staged = execFileSync13("git", ["diff", "--name-only", "--cached"], {
|
|
3107
4109
|
encoding: "utf-8",
|
|
3108
4110
|
cwd: projectDir,
|
|
3109
4111
|
timeout: 5e3,
|
|
3110
4112
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3111
4113
|
}).trim();
|
|
3112
|
-
const unstaged =
|
|
4114
|
+
const unstaged = execFileSync13("git", ["diff", "--name-only"], {
|
|
3113
4115
|
encoding: "utf-8",
|
|
3114
4116
|
cwd: projectDir,
|
|
3115
4117
|
timeout: 5e3,
|
|
@@ -3152,8 +4154,8 @@ Error context:
|
|
|
3152
4154
|
});
|
|
3153
4155
|
|
|
3154
4156
|
// src/stages/gate.ts
|
|
3155
|
-
import * as
|
|
3156
|
-
import * as
|
|
4157
|
+
import * as fs20 from "fs";
|
|
4158
|
+
import * as path18 from "path";
|
|
3157
4159
|
function executeGateStage(ctx, def) {
|
|
3158
4160
|
if (ctx.input.dryRun) {
|
|
3159
4161
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -3196,7 +4198,7 @@ ${output}
|
|
|
3196
4198
|
`);
|
|
3197
4199
|
}
|
|
3198
4200
|
}
|
|
3199
|
-
|
|
4201
|
+
fs20.writeFileSync(path18.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
3200
4202
|
return {
|
|
3201
4203
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
3202
4204
|
retries: 0
|
|
@@ -3211,9 +4213,9 @@ var init_gate = __esm({
|
|
|
3211
4213
|
});
|
|
3212
4214
|
|
|
3213
4215
|
// src/stages/verify.ts
|
|
3214
|
-
import * as
|
|
3215
|
-
import * as
|
|
3216
|
-
import { execFileSync as
|
|
4216
|
+
import * as fs21 from "fs";
|
|
4217
|
+
import * as path19 from "path";
|
|
4218
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
3217
4219
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
3218
4220
|
const maxAttempts = def.maxRetries ?? 2;
|
|
3219
4221
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -3223,8 +4225,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
3223
4225
|
return { ...gateResult, retries: attempt };
|
|
3224
4226
|
}
|
|
3225
4227
|
if (attempt < maxAttempts) {
|
|
3226
|
-
const verifyPath =
|
|
3227
|
-
const errorOutput =
|
|
4228
|
+
const verifyPath = path19.join(ctx.taskDir, "verify.md");
|
|
4229
|
+
const errorOutput = fs21.existsSync(verifyPath) ? fs21.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
3228
4230
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
3229
4231
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
3230
4232
|
const diagConfig = getProjectConfig();
|
|
@@ -3267,7 +4269,7 @@ ${diagnosis.resolution}`);
|
|
|
3267
4269
|
const parts = parseCommand(cmd);
|
|
3268
4270
|
if (parts.length === 0) return;
|
|
3269
4271
|
try {
|
|
3270
|
-
|
|
4272
|
+
execFileSync14(parts[0], parts.slice(1), {
|
|
3271
4273
|
stdio: "pipe",
|
|
3272
4274
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
3273
4275
|
});
|
|
@@ -3320,8 +4322,8 @@ var init_verify = __esm({
|
|
|
3320
4322
|
});
|
|
3321
4323
|
|
|
3322
4324
|
// src/review-standalone.ts
|
|
3323
|
-
import * as
|
|
3324
|
-
import * as
|
|
4325
|
+
import * as fs22 from "fs";
|
|
4326
|
+
import * as path20 from "path";
|
|
3325
4327
|
function resolveReviewTarget(input) {
|
|
3326
4328
|
if (input.prs.length === 0) {
|
|
3327
4329
|
return {
|
|
@@ -3345,8 +4347,8 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
3345
4347
|
}
|
|
3346
4348
|
async function runStandaloneReview(input) {
|
|
3347
4349
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
3348
|
-
const taskDir =
|
|
3349
|
-
|
|
4350
|
+
const taskDir = path20.join(input.projectDir, ".kody", "tasks", taskId);
|
|
4351
|
+
fs22.mkdirSync(taskDir, { recursive: true });
|
|
3350
4352
|
let diffInstruction = "";
|
|
3351
4353
|
let filesChangedSection = "";
|
|
3352
4354
|
if (input.baseBranch) {
|
|
@@ -3373,7 +4375,7 @@ ${fileList}`;
|
|
|
3373
4375
|
const taskContent = `# ${input.prTitle}
|
|
3374
4376
|
|
|
3375
4377
|
${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
3376
|
-
|
|
4378
|
+
fs22.writeFileSync(path20.join(taskDir, "task.md"), taskContent);
|
|
3377
4379
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
3378
4380
|
const ctx = {
|
|
3379
4381
|
taskId,
|
|
@@ -3387,18 +4389,18 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
|
3387
4389
|
}
|
|
3388
4390
|
};
|
|
3389
4391
|
logger.info(`[review] standalone review for: ${input.prTitle}`);
|
|
3390
|
-
const
|
|
3391
|
-
if (
|
|
4392
|
+
const result2 = await executeAgentStage(ctx, reviewDef);
|
|
4393
|
+
if (result2.outcome !== "completed") {
|
|
3392
4394
|
return {
|
|
3393
4395
|
outcome: "failed",
|
|
3394
4396
|
taskDir,
|
|
3395
|
-
error:
|
|
4397
|
+
error: result2.error ?? "Review stage failed"
|
|
3396
4398
|
};
|
|
3397
4399
|
}
|
|
3398
|
-
const reviewPath =
|
|
4400
|
+
const reviewPath = path20.join(taskDir, "review.md");
|
|
3399
4401
|
let reviewContent;
|
|
3400
|
-
if (
|
|
3401
|
-
reviewContent =
|
|
4402
|
+
if (fs22.existsSync(reviewPath)) {
|
|
4403
|
+
reviewContent = fs22.readFileSync(reviewPath, "utf-8");
|
|
3402
4404
|
}
|
|
3403
4405
|
return {
|
|
3404
4406
|
outcome: "completed",
|
|
@@ -3438,8 +4440,8 @@ var init_review_standalone = __esm({
|
|
|
3438
4440
|
});
|
|
3439
4441
|
|
|
3440
4442
|
// src/stages/review.ts
|
|
3441
|
-
import * as
|
|
3442
|
-
import * as
|
|
4443
|
+
import * as fs23 from "fs";
|
|
4444
|
+
import * as path21 from "path";
|
|
3443
4445
|
async function executeReviewWithFix(ctx, def) {
|
|
3444
4446
|
if (ctx.input.dryRun) {
|
|
3445
4447
|
return { outcome: "completed", retries: 0 };
|
|
@@ -3453,11 +4455,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
3453
4455
|
if (reviewResult.outcome !== "completed") {
|
|
3454
4456
|
return reviewResult;
|
|
3455
4457
|
}
|
|
3456
|
-
const reviewFile =
|
|
3457
|
-
if (!
|
|
4458
|
+
const reviewFile = path21.join(ctx.taskDir, "review.md");
|
|
4459
|
+
if (!fs23.existsSync(reviewFile)) {
|
|
3458
4460
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
3459
4461
|
}
|
|
3460
|
-
const content =
|
|
4462
|
+
const content = fs23.readFileSync(reviewFile, "utf-8");
|
|
3461
4463
|
if (detectReviewVerdict(content) !== "fail") {
|
|
3462
4464
|
return { ...reviewResult, retries: iteration };
|
|
3463
4465
|
}
|
|
@@ -3486,15 +4488,15 @@ var init_review = __esm({
|
|
|
3486
4488
|
});
|
|
3487
4489
|
|
|
3488
4490
|
// src/stages/ship.ts
|
|
3489
|
-
import * as
|
|
3490
|
-
import * as
|
|
3491
|
-
import { execFileSync as
|
|
4491
|
+
import * as fs24 from "fs";
|
|
4492
|
+
import * as path22 from "path";
|
|
4493
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
3492
4494
|
function buildPrBody(ctx) {
|
|
3493
4495
|
const sections = [];
|
|
3494
|
-
const taskJsonPath =
|
|
3495
|
-
if (
|
|
4496
|
+
const taskJsonPath = path22.join(ctx.taskDir, "task.json");
|
|
4497
|
+
if (fs24.existsSync(taskJsonPath)) {
|
|
3496
4498
|
try {
|
|
3497
|
-
const raw =
|
|
4499
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3498
4500
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3499
4501
|
const task = JSON.parse(cleaned);
|
|
3500
4502
|
if (task.description) {
|
|
@@ -3513,9 +4515,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
3513
4515
|
} catch {
|
|
3514
4516
|
}
|
|
3515
4517
|
}
|
|
3516
|
-
const reviewPath =
|
|
3517
|
-
if (
|
|
3518
|
-
const review =
|
|
4518
|
+
const reviewPath = path22.join(ctx.taskDir, "review.md");
|
|
4519
|
+
if (fs24.existsSync(reviewPath)) {
|
|
4520
|
+
const review = fs24.readFileSync(reviewPath, "utf-8");
|
|
3519
4521
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
3520
4522
|
if (summaryMatch) {
|
|
3521
4523
|
const summary = summaryMatch[1].trim();
|
|
@@ -3532,14 +4534,14 @@ ${summary}`);
|
|
|
3532
4534
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
3533
4535
|
}
|
|
3534
4536
|
}
|
|
3535
|
-
const verifyPath =
|
|
3536
|
-
if (
|
|
3537
|
-
const verify =
|
|
4537
|
+
const verifyPath = path22.join(ctx.taskDir, "verify.md");
|
|
4538
|
+
if (fs24.existsSync(verifyPath)) {
|
|
4539
|
+
const verify = fs24.readFileSync(verifyPath, "utf-8");
|
|
3538
4540
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
3539
4541
|
}
|
|
3540
|
-
const planPath =
|
|
3541
|
-
if (
|
|
3542
|
-
const plan =
|
|
4542
|
+
const planPath = path22.join(ctx.taskDir, "plan.md");
|
|
4543
|
+
if (fs24.existsSync(planPath)) {
|
|
4544
|
+
const plan = fs24.readFileSync(planPath, "utf-8").trim();
|
|
3543
4545
|
if (plan) {
|
|
3544
4546
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
3545
4547
|
sections.push(`
|
|
@@ -3559,25 +4561,25 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
3559
4561
|
return sections.join("\n");
|
|
3560
4562
|
}
|
|
3561
4563
|
function executeShipStage(ctx, _def) {
|
|
3562
|
-
const shipPath =
|
|
4564
|
+
const shipPath = path22.join(ctx.taskDir, "ship.md");
|
|
3563
4565
|
if (ctx.input.dryRun) {
|
|
3564
|
-
|
|
4566
|
+
fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
3565
4567
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3566
4568
|
}
|
|
3567
4569
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
3568
|
-
|
|
4570
|
+
fs24.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
3569
4571
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3570
4572
|
}
|
|
3571
4573
|
try {
|
|
3572
4574
|
const head = getCurrentBranch(ctx.projectDir);
|
|
3573
4575
|
const base = getDefaultBranch(ctx.projectDir);
|
|
3574
4576
|
try {
|
|
3575
|
-
|
|
4577
|
+
execFileSync15("git", ["add", ctx.taskDir], {
|
|
3576
4578
|
cwd: ctx.projectDir,
|
|
3577
4579
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
3578
4580
|
stdio: "pipe"
|
|
3579
4581
|
});
|
|
3580
|
-
|
|
4582
|
+
execFileSync15("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
3581
4583
|
cwd: ctx.projectDir,
|
|
3582
4584
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
3583
4585
|
stdio: "pipe"
|
|
@@ -3591,7 +4593,7 @@ function executeShipStage(ctx, _def) {
|
|
|
3591
4593
|
let repo = config.github?.repo;
|
|
3592
4594
|
if (!owner || !repo) {
|
|
3593
4595
|
try {
|
|
3594
|
-
const remoteUrl =
|
|
4596
|
+
const remoteUrl = execFileSync15("git", ["remote", "get-url", "origin"], {
|
|
3595
4597
|
encoding: "utf-8",
|
|
3596
4598
|
cwd: ctx.projectDir
|
|
3597
4599
|
}).trim();
|
|
@@ -3612,28 +4614,28 @@ function executeShipStage(ctx, _def) {
|
|
|
3612
4614
|
chore: "chore"
|
|
3613
4615
|
};
|
|
3614
4616
|
let prefix = "chore";
|
|
3615
|
-
const taskJsonPath =
|
|
3616
|
-
if (
|
|
4617
|
+
const taskJsonPath = path22.join(ctx.taskDir, "task.json");
|
|
4618
|
+
if (fs24.existsSync(taskJsonPath)) {
|
|
3617
4619
|
try {
|
|
3618
|
-
const raw =
|
|
4620
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3619
4621
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3620
4622
|
const task = JSON.parse(cleaned);
|
|
3621
4623
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
3622
4624
|
} catch {
|
|
3623
4625
|
}
|
|
3624
4626
|
}
|
|
3625
|
-
const taskMdPath =
|
|
3626
|
-
if (
|
|
3627
|
-
const content =
|
|
4627
|
+
const taskMdPath = path22.join(ctx.taskDir, "task.md");
|
|
4628
|
+
if (fs24.existsSync(taskMdPath)) {
|
|
4629
|
+
const content = fs24.readFileSync(taskMdPath, "utf-8");
|
|
3628
4630
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
3629
4631
|
if (heading) {
|
|
3630
4632
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
3631
4633
|
}
|
|
3632
4634
|
}
|
|
3633
4635
|
if (title === "Update") {
|
|
3634
|
-
if (
|
|
4636
|
+
if (fs24.existsSync(taskJsonPath)) {
|
|
3635
4637
|
try {
|
|
3636
|
-
const raw =
|
|
4638
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3637
4639
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3638
4640
|
const task = JSON.parse(cleaned);
|
|
3639
4641
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -3656,7 +4658,7 @@ function executeShipStage(ctx, _def) {
|
|
|
3656
4658
|
} catch {
|
|
3657
4659
|
}
|
|
3658
4660
|
}
|
|
3659
|
-
|
|
4661
|
+
fs24.writeFileSync(shipPath, `# Ship
|
|
3660
4662
|
|
|
3661
4663
|
Updated existing PR: ${existingPr.url}
|
|
3662
4664
|
PR #${existingPr.number}
|
|
@@ -3677,20 +4679,20 @@ PR #${existingPr.number}
|
|
|
3677
4679
|
} catch {
|
|
3678
4680
|
}
|
|
3679
4681
|
}
|
|
3680
|
-
|
|
4682
|
+
fs24.writeFileSync(shipPath, `# Ship
|
|
3681
4683
|
|
|
3682
4684
|
PR created: ${pr.url}
|
|
3683
4685
|
PR #${pr.number}
|
|
3684
4686
|
`);
|
|
3685
4687
|
} else {
|
|
3686
|
-
|
|
4688
|
+
fs24.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
3687
4689
|
}
|
|
3688
4690
|
}
|
|
3689
4691
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3690
4692
|
} catch (err) {
|
|
3691
4693
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3692
4694
|
try {
|
|
3693
|
-
|
|
4695
|
+
fs24.writeFileSync(shipPath, `# Ship
|
|
3694
4696
|
|
|
3695
4697
|
Failed: ${msg}
|
|
3696
4698
|
`);
|
|
@@ -3739,15 +4741,15 @@ var init_executor_registry = __esm({
|
|
|
3739
4741
|
});
|
|
3740
4742
|
|
|
3741
4743
|
// src/pipeline/questions.ts
|
|
3742
|
-
import * as
|
|
3743
|
-
import * as
|
|
4744
|
+
import * as fs25 from "fs";
|
|
4745
|
+
import * as path23 from "path";
|
|
3744
4746
|
function checkForQuestions(ctx, stageName) {
|
|
3745
4747
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
3746
4748
|
try {
|
|
3747
4749
|
if (stageName === "taskify") {
|
|
3748
|
-
const taskJsonPath =
|
|
3749
|
-
if (!
|
|
3750
|
-
const raw =
|
|
4750
|
+
const taskJsonPath = path23.join(ctx.taskDir, "task.json");
|
|
4751
|
+
if (!fs25.existsSync(taskJsonPath)) return false;
|
|
4752
|
+
const raw = fs25.readFileSync(taskJsonPath, "utf-8");
|
|
3751
4753
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3752
4754
|
const taskJson = JSON.parse(cleaned);
|
|
3753
4755
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -3762,9 +4764,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
3762
4764
|
}
|
|
3763
4765
|
}
|
|
3764
4766
|
if (stageName === "plan") {
|
|
3765
|
-
const planPath =
|
|
3766
|
-
if (!
|
|
3767
|
-
const plan =
|
|
4767
|
+
const planPath = path23.join(ctx.taskDir, "plan.md");
|
|
4768
|
+
if (!fs25.existsSync(planPath)) return false;
|
|
4769
|
+
const plan = fs25.readFileSync(planPath, "utf-8");
|
|
3768
4770
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
3769
4771
|
if (questionsMatch) {
|
|
3770
4772
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -3793,8 +4795,8 @@ var init_questions = __esm({
|
|
|
3793
4795
|
});
|
|
3794
4796
|
|
|
3795
4797
|
// src/pipeline/hooks.ts
|
|
3796
|
-
import * as
|
|
3797
|
-
import * as
|
|
4798
|
+
import * as fs26 from "fs";
|
|
4799
|
+
import * as path24 from "path";
|
|
3798
4800
|
function applyPreStageLabel(ctx, def) {
|
|
3799
4801
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
3800
4802
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
@@ -3832,9 +4834,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
3832
4834
|
return { complexity, activeStages };
|
|
3833
4835
|
}
|
|
3834
4836
|
try {
|
|
3835
|
-
const taskJsonPath =
|
|
3836
|
-
if (!
|
|
3837
|
-
const raw =
|
|
4837
|
+
const taskJsonPath = path24.join(ctx.taskDir, "task.json");
|
|
4838
|
+
if (!fs26.existsSync(taskJsonPath)) return null;
|
|
4839
|
+
const raw = fs26.readFileSync(taskJsonPath, "utf-8");
|
|
3838
4840
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3839
4841
|
const taskJson = JSON.parse(cleaned);
|
|
3840
4842
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -3864,8 +4866,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
3864
4866
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
3865
4867
|
if (ctx.input.mode === "rerun") return null;
|
|
3866
4868
|
if (!ctx.input.issueNumber) return null;
|
|
3867
|
-
const planPath =
|
|
3868
|
-
const plan =
|
|
4869
|
+
const planPath = path24.join(ctx.taskDir, "plan.md");
|
|
4870
|
+
const plan = fs26.existsSync(planPath) ? fs26.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
3869
4871
|
try {
|
|
3870
4872
|
postComment(
|
|
3871
4873
|
ctx.input.issueNumber,
|
|
@@ -3932,22 +4934,22 @@ var init_hooks = __esm({
|
|
|
3932
4934
|
});
|
|
3933
4935
|
|
|
3934
4936
|
// src/learning/auto-learn.ts
|
|
3935
|
-
import * as
|
|
3936
|
-
import * as
|
|
4937
|
+
import * as fs27 from "fs";
|
|
4938
|
+
import * as path25 from "path";
|
|
3937
4939
|
function stripAnsi(str) {
|
|
3938
4940
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
3939
4941
|
}
|
|
3940
4942
|
function autoLearn(ctx) {
|
|
3941
4943
|
try {
|
|
3942
|
-
const memoryDir =
|
|
3943
|
-
if (!
|
|
3944
|
-
|
|
4944
|
+
const memoryDir = path25.join(ctx.projectDir, ".kody", "memory");
|
|
4945
|
+
if (!fs27.existsSync(memoryDir)) {
|
|
4946
|
+
fs27.mkdirSync(memoryDir, { recursive: true });
|
|
3945
4947
|
}
|
|
3946
4948
|
const learnings = [];
|
|
3947
4949
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3948
|
-
const verifyPath =
|
|
3949
|
-
if (
|
|
3950
|
-
const verify = stripAnsi(
|
|
4950
|
+
const verifyPath = path25.join(ctx.taskDir, "verify.md");
|
|
4951
|
+
if (fs27.existsSync(verifyPath)) {
|
|
4952
|
+
const verify = stripAnsi(fs27.readFileSync(verifyPath, "utf-8"));
|
|
3951
4953
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
3952
4954
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
3953
4955
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -3956,18 +4958,18 @@ function autoLearn(ctx) {
|
|
|
3956
4958
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
3957
4959
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
3958
4960
|
}
|
|
3959
|
-
const reviewPath =
|
|
3960
|
-
if (
|
|
3961
|
-
const review =
|
|
4961
|
+
const reviewPath = path25.join(ctx.taskDir, "review.md");
|
|
4962
|
+
if (fs27.existsSync(reviewPath)) {
|
|
4963
|
+
const review = fs27.readFileSync(reviewPath, "utf-8");
|
|
3962
4964
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
3963
4965
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
3964
4966
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
3965
4967
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
3966
4968
|
}
|
|
3967
|
-
const taskJsonPath =
|
|
3968
|
-
if (
|
|
4969
|
+
const taskJsonPath = path25.join(ctx.taskDir, "task.json");
|
|
4970
|
+
if (fs27.existsSync(taskJsonPath)) {
|
|
3969
4971
|
try {
|
|
3970
|
-
const raw = stripAnsi(
|
|
4972
|
+
const raw = stripAnsi(fs27.readFileSync(taskJsonPath, "utf-8"));
|
|
3971
4973
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3972
4974
|
const task = JSON.parse(cleaned);
|
|
3973
4975
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -3978,12 +4980,12 @@ function autoLearn(ctx) {
|
|
|
3978
4980
|
}
|
|
3979
4981
|
}
|
|
3980
4982
|
if (learnings.length > 0) {
|
|
3981
|
-
const conventionsPath =
|
|
4983
|
+
const conventionsPath = path25.join(memoryDir, "conventions.md");
|
|
3982
4984
|
const entry = `
|
|
3983
4985
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
3984
4986
|
${learnings.join("\n")}
|
|
3985
4987
|
`;
|
|
3986
|
-
|
|
4988
|
+
fs27.appendFileSync(conventionsPath, entry);
|
|
3987
4989
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
3988
4990
|
}
|
|
3989
4991
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
@@ -3991,8 +4993,8 @@ ${learnings.join("\n")}
|
|
|
3991
4993
|
}
|
|
3992
4994
|
}
|
|
3993
4995
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
3994
|
-
const archPath =
|
|
3995
|
-
if (
|
|
4996
|
+
const archPath = path25.join(memoryDir, "architecture.md");
|
|
4997
|
+
if (fs27.existsSync(archPath)) return;
|
|
3996
4998
|
const detected = detectArchitectureBasic(projectDir);
|
|
3997
4999
|
if (detected.length > 0) {
|
|
3998
5000
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
@@ -4000,7 +5002,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
4000
5002
|
## Overview
|
|
4001
5003
|
${detected.join("\n")}
|
|
4002
5004
|
`;
|
|
4003
|
-
|
|
5005
|
+
fs27.writeFileSync(archPath, content);
|
|
4004
5006
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
4005
5007
|
}
|
|
4006
5008
|
}
|
|
@@ -4013,13 +5015,13 @@ var init_auto_learn = __esm({
|
|
|
4013
5015
|
});
|
|
4014
5016
|
|
|
4015
5017
|
// src/retrospective.ts
|
|
4016
|
-
import * as
|
|
4017
|
-
import * as
|
|
5018
|
+
import * as fs28 from "fs";
|
|
5019
|
+
import * as path26 from "path";
|
|
4018
5020
|
function readArtifact(taskDir, filename, maxChars) {
|
|
4019
|
-
const p =
|
|
4020
|
-
if (!
|
|
5021
|
+
const p = path26.join(taskDir, filename);
|
|
5022
|
+
if (!fs28.existsSync(p)) return null;
|
|
4021
5023
|
try {
|
|
4022
|
-
const content =
|
|
5024
|
+
const content = fs28.readFileSync(p, "utf-8");
|
|
4023
5025
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
4024
5026
|
} catch {
|
|
4025
5027
|
return null;
|
|
@@ -4072,13 +5074,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
4072
5074
|
return lines.join("\n");
|
|
4073
5075
|
}
|
|
4074
5076
|
function getLogPath(projectDir) {
|
|
4075
|
-
return
|
|
5077
|
+
return path26.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
4076
5078
|
}
|
|
4077
5079
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
4078
5080
|
const logPath = getLogPath(projectDir);
|
|
4079
|
-
if (!
|
|
5081
|
+
if (!fs28.existsSync(logPath)) return [];
|
|
4080
5082
|
try {
|
|
4081
|
-
const content =
|
|
5083
|
+
const content = fs28.readFileSync(logPath, "utf-8");
|
|
4082
5084
|
const lines = content.split("\n").filter(Boolean);
|
|
4083
5085
|
const entries = [];
|
|
4084
5086
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -4105,11 +5107,11 @@ function formatPreviousEntries(entries) {
|
|
|
4105
5107
|
}
|
|
4106
5108
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
4107
5109
|
const logPath = getLogPath(projectDir);
|
|
4108
|
-
const dir =
|
|
4109
|
-
if (!
|
|
4110
|
-
|
|
5110
|
+
const dir = path26.dirname(logPath);
|
|
5111
|
+
if (!fs28.existsSync(dir)) {
|
|
5112
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
4111
5113
|
}
|
|
4112
|
-
|
|
5114
|
+
fs28.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
4113
5115
|
}
|
|
4114
5116
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
4115
5117
|
if (ctx.input.dryRun) return;
|
|
@@ -4131,7 +5133,7 @@ ${previousText}
|
|
|
4131
5133
|
if (needsLitellmProxy(config)) {
|
|
4132
5134
|
extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
|
|
4133
5135
|
}
|
|
4134
|
-
const
|
|
5136
|
+
const result2 = await runner.run("retrospective", prompt, model, 3e4, "", {
|
|
4135
5137
|
cwd: ctx.projectDir,
|
|
4136
5138
|
env: extraEnv
|
|
4137
5139
|
});
|
|
@@ -4139,8 +5141,8 @@ ${previousText}
|
|
|
4139
5141
|
let patternMatch = null;
|
|
4140
5142
|
let suggestion = "No suggestion";
|
|
4141
5143
|
let pipelineFlaw = null;
|
|
4142
|
-
if (
|
|
4143
|
-
const cleaned =
|
|
5144
|
+
if (result2.outcome === "completed" && result2.output) {
|
|
5145
|
+
const cleaned = result2.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
4144
5146
|
try {
|
|
4145
5147
|
const parsed = JSON.parse(cleaned);
|
|
4146
5148
|
observation = parsed.observation ?? observation;
|
|
@@ -4277,8 +5279,8 @@ var init_summary = __esm({
|
|
|
4277
5279
|
});
|
|
4278
5280
|
|
|
4279
5281
|
// src/pipeline.ts
|
|
4280
|
-
import * as
|
|
4281
|
-
import * as
|
|
5282
|
+
import * as fs29 from "fs";
|
|
5283
|
+
import * as path27 from "path";
|
|
4282
5284
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
4283
5285
|
if (ctx.input.dryRun) return;
|
|
4284
5286
|
if (ctx.input.prNumber) {
|
|
@@ -4291,8 +5293,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
4291
5293
|
}
|
|
4292
5294
|
if (!ctx.input.issueNumber) return;
|
|
4293
5295
|
try {
|
|
4294
|
-
const taskMdPath =
|
|
4295
|
-
const title =
|
|
5296
|
+
const taskMdPath = path27.join(ctx.taskDir, "task.md");
|
|
5297
|
+
const title = fs29.existsSync(taskMdPath) ? fs29.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
4296
5298
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
4297
5299
|
syncWithDefault(ctx.projectDir);
|
|
4298
5300
|
} catch (err) {
|
|
@@ -4306,10 +5308,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
4306
5308
|
}
|
|
4307
5309
|
}
|
|
4308
5310
|
function acquireLock(taskDir) {
|
|
4309
|
-
const lockPath =
|
|
4310
|
-
if (
|
|
5311
|
+
const lockPath = path27.join(taskDir, ".lock");
|
|
5312
|
+
if (fs29.existsSync(lockPath)) {
|
|
4311
5313
|
try {
|
|
4312
|
-
const pid = parseInt(
|
|
5314
|
+
const pid = parseInt(fs29.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
4313
5315
|
if (!isNaN(pid)) {
|
|
4314
5316
|
try {
|
|
4315
5317
|
process.kill(pid, 0);
|
|
@@ -4326,14 +5328,14 @@ function acquireLock(taskDir) {
|
|
|
4326
5328
|
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
4327
5329
|
}
|
|
4328
5330
|
try {
|
|
4329
|
-
|
|
5331
|
+
fs29.unlinkSync(lockPath);
|
|
4330
5332
|
} catch {
|
|
4331
5333
|
}
|
|
4332
5334
|
}
|
|
4333
5335
|
try {
|
|
4334
|
-
const fd =
|
|
4335
|
-
|
|
4336
|
-
|
|
5336
|
+
const fd = fs29.openSync(lockPath, fs29.constants.O_WRONLY | fs29.constants.O_CREAT | fs29.constants.O_EXCL);
|
|
5337
|
+
fs29.writeSync(fd, String(process.pid));
|
|
5338
|
+
fs29.closeSync(fd);
|
|
4337
5339
|
} catch (err) {
|
|
4338
5340
|
if (err.code === "EEXIST") {
|
|
4339
5341
|
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
@@ -4343,7 +5345,7 @@ function acquireLock(taskDir) {
|
|
|
4343
5345
|
}
|
|
4344
5346
|
function releaseLock(taskDir) {
|
|
4345
5347
|
try {
|
|
4346
|
-
|
|
5348
|
+
fs29.unlinkSync(path27.join(taskDir, ".lock"));
|
|
4347
5349
|
} catch {
|
|
4348
5350
|
}
|
|
4349
5351
|
}
|
|
@@ -4432,23 +5434,23 @@ async function runPipelineInner(ctx) {
|
|
|
4432
5434
|
writeState(state, ctx.taskDir);
|
|
4433
5435
|
logger.info(`[${def.name}] starting...`);
|
|
4434
5436
|
applyPreStageLabel(ctx, def);
|
|
4435
|
-
let
|
|
5437
|
+
let result2;
|
|
4436
5438
|
try {
|
|
4437
|
-
|
|
5439
|
+
result2 = await getExecutor(def.name)(ctx, def);
|
|
4438
5440
|
} catch (error) {
|
|
4439
|
-
|
|
5441
|
+
result2 = {
|
|
4440
5442
|
outcome: "failed",
|
|
4441
5443
|
retries: 0,
|
|
4442
5444
|
error: error instanceof Error ? error.message : String(error)
|
|
4443
5445
|
};
|
|
4444
5446
|
}
|
|
4445
5447
|
ciGroupEnd();
|
|
4446
|
-
if (
|
|
5448
|
+
if (result2.outcome === "completed") {
|
|
4447
5449
|
state.stages[def.name] = {
|
|
4448
5450
|
state: "completed",
|
|
4449
5451
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4450
|
-
retries:
|
|
4451
|
-
outputFile:
|
|
5452
|
+
retries: result2.retries,
|
|
5453
|
+
outputFile: result2.outputFile
|
|
4452
5454
|
};
|
|
4453
5455
|
logger.info(`[${def.name}] \u2713 completed`);
|
|
4454
5456
|
const detected = autoDetectComplexity(ctx, def);
|
|
@@ -4462,16 +5464,16 @@ async function runPipelineInner(ctx) {
|
|
|
4462
5464
|
if (gated) return gated;
|
|
4463
5465
|
commitAfterStage(ctx, def);
|
|
4464
5466
|
} else {
|
|
4465
|
-
const isTimeout =
|
|
5467
|
+
const isTimeout = result2.outcome === "timed_out";
|
|
4466
5468
|
state.stages[def.name] = {
|
|
4467
5469
|
state: isTimeout ? "timeout" : "failed",
|
|
4468
|
-
retries:
|
|
4469
|
-
error: isTimeout ? "Stage timed out" :
|
|
5470
|
+
retries: result2.retries,
|
|
5471
|
+
error: isTimeout ? "Stage timed out" : result2.error ?? "Stage failed"
|
|
4470
5472
|
};
|
|
4471
5473
|
state.state = "failed";
|
|
4472
5474
|
state.sessions = ctx.sessions;
|
|
4473
5475
|
writeState(state, ctx.taskDir);
|
|
4474
|
-
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${
|
|
5476
|
+
logger.error(`[${def.name}] ${isTimeout ? "\u23F1 timed out" : `\u2717 failed: ${result2.error}`}`);
|
|
4475
5477
|
if (ctx.input.issueNumber && !ctx.input.local) {
|
|
4476
5478
|
setLifecycleLabel(ctx.input.issueNumber, "failed");
|
|
4477
5479
|
}
|
|
@@ -4551,8 +5553,8 @@ var init_pipeline = __esm({
|
|
|
4551
5553
|
});
|
|
4552
5554
|
|
|
4553
5555
|
// src/preflight.ts
|
|
4554
|
-
import { execFileSync as
|
|
4555
|
-
import * as
|
|
5556
|
+
import { execFileSync as execFileSync16 } from "child_process";
|
|
5557
|
+
import * as fs30 from "fs";
|
|
4556
5558
|
function check(name, fn) {
|
|
4557
5559
|
try {
|
|
4558
5560
|
const detail = fn() ?? void 0;
|
|
@@ -4564,7 +5566,7 @@ function check(name, fn) {
|
|
|
4564
5566
|
function runPreflight() {
|
|
4565
5567
|
const checks = [
|
|
4566
5568
|
check("claude CLI", () => {
|
|
4567
|
-
const v =
|
|
5569
|
+
const v = execFileSync16("claude", ["--version"], {
|
|
4568
5570
|
encoding: "utf-8",
|
|
4569
5571
|
timeout: 1e4,
|
|
4570
5572
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4572,14 +5574,14 @@ function runPreflight() {
|
|
|
4572
5574
|
return v;
|
|
4573
5575
|
}),
|
|
4574
5576
|
check("git repo", () => {
|
|
4575
|
-
|
|
5577
|
+
execFileSync16("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
4576
5578
|
encoding: "utf-8",
|
|
4577
5579
|
timeout: 5e3,
|
|
4578
5580
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4579
5581
|
});
|
|
4580
5582
|
}),
|
|
4581
5583
|
check("pnpm", () => {
|
|
4582
|
-
const v =
|
|
5584
|
+
const v = execFileSync16("pnpm", ["--version"], {
|
|
4583
5585
|
encoding: "utf-8",
|
|
4584
5586
|
timeout: 5e3,
|
|
4585
5587
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4587,7 +5589,7 @@ function runPreflight() {
|
|
|
4587
5589
|
return v;
|
|
4588
5590
|
}),
|
|
4589
5591
|
check("node >= 18", () => {
|
|
4590
|
-
const v =
|
|
5592
|
+
const v = execFileSync16("node", ["--version"], {
|
|
4591
5593
|
encoding: "utf-8",
|
|
4592
5594
|
timeout: 5e3,
|
|
4593
5595
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4597,7 +5599,7 @@ function runPreflight() {
|
|
|
4597
5599
|
return v;
|
|
4598
5600
|
}),
|
|
4599
5601
|
check("gh CLI", () => {
|
|
4600
|
-
const v =
|
|
5602
|
+
const v = execFileSync16("gh", ["--version"], {
|
|
4601
5603
|
encoding: "utf-8",
|
|
4602
5604
|
timeout: 5e3,
|
|
4603
5605
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4605,7 +5607,7 @@ function runPreflight() {
|
|
|
4605
5607
|
return v;
|
|
4606
5608
|
}),
|
|
4607
5609
|
check("package.json", () => {
|
|
4608
|
-
if (!
|
|
5610
|
+
if (!fs30.existsSync("package.json")) throw new Error("not found");
|
|
4609
5611
|
})
|
|
4610
5612
|
];
|
|
4611
5613
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -4682,8 +5684,8 @@ var init_args = __esm({
|
|
|
4682
5684
|
});
|
|
4683
5685
|
|
|
4684
5686
|
// src/cli/task-state.ts
|
|
4685
|
-
import * as
|
|
4686
|
-
import * as
|
|
5687
|
+
import * as fs31 from "fs";
|
|
5688
|
+
import * as path28 from "path";
|
|
4687
5689
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
4688
5690
|
if (!existingTaskId || !existingState) {
|
|
4689
5691
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -4715,11 +5717,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
4715
5717
|
function resolveForIssue(issueNumber, projectDir) {
|
|
4716
5718
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
4717
5719
|
if (existingTaskId) {
|
|
4718
|
-
const statusPath =
|
|
5720
|
+
const statusPath = path28.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
4719
5721
|
let existingState = null;
|
|
4720
|
-
if (
|
|
5722
|
+
if (fs31.existsSync(statusPath)) {
|
|
4721
5723
|
try {
|
|
4722
|
-
existingState = JSON.parse(
|
|
5724
|
+
existingState = JSON.parse(fs31.readFileSync(statusPath, "utf-8"));
|
|
4723
5725
|
} catch {
|
|
4724
5726
|
}
|
|
4725
5727
|
}
|
|
@@ -4752,12 +5754,12 @@ var resolve_exports = {};
|
|
|
4752
5754
|
__export(resolve_exports, {
|
|
4753
5755
|
runResolve: () => runResolve
|
|
4754
5756
|
});
|
|
4755
|
-
import { execFileSync as
|
|
5757
|
+
import { execFileSync as execFileSync17 } from "child_process";
|
|
4756
5758
|
function getConflictContext(cwd, files) {
|
|
4757
5759
|
const parts = [];
|
|
4758
5760
|
for (const file of files.slice(0, 10)) {
|
|
4759
5761
|
try {
|
|
4760
|
-
const content =
|
|
5762
|
+
const content = execFileSync17("git", ["diff", file], {
|
|
4761
5763
|
cwd,
|
|
4762
5764
|
encoding: "utf-8",
|
|
4763
5765
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4807,12 +5809,12 @@ async function runResolve(options) {
|
|
|
4807
5809
|
extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
|
|
4808
5810
|
}
|
|
4809
5811
|
logger.info(` Running agent to resolve conflicts (model=${model})...`);
|
|
4810
|
-
const
|
|
5812
|
+
const result2 = await runner.run("resolve", prompt, model, 3e5, projectDir, {
|
|
4811
5813
|
cwd: projectDir,
|
|
4812
5814
|
env: extraEnv
|
|
4813
5815
|
});
|
|
4814
|
-
if (
|
|
4815
|
-
return { outcome: "failed", error: `Agent failed: ${
|
|
5816
|
+
if (result2.outcome !== "completed") {
|
|
5817
|
+
return { outcome: "failed", error: `Agent failed: ${result2.error}` };
|
|
4816
5818
|
}
|
|
4817
5819
|
logger.info(" Verifying resolution...");
|
|
4818
5820
|
const verify = runQualityGates(projectDir, projectDir);
|
|
@@ -4876,8 +5878,8 @@ var init_resolve = __esm({
|
|
|
4876
5878
|
|
|
4877
5879
|
// src/entry.ts
|
|
4878
5880
|
var entry_exports = {};
|
|
4879
|
-
import * as
|
|
4880
|
-
import * as
|
|
5881
|
+
import * as fs32 from "fs";
|
|
5882
|
+
import * as path29 from "path";
|
|
4881
5883
|
async function ensureLitellmProxy(config, projectDir) {
|
|
4882
5884
|
if (!anyStageNeedsProxy(config)) return null;
|
|
4883
5885
|
const litellmUrl = getLitellmUrl();
|
|
@@ -4912,7 +5914,7 @@ async function ensureLitellmProxy(config, projectDir) {
|
|
|
4912
5914
|
return litellmProcess;
|
|
4913
5915
|
}
|
|
4914
5916
|
async function runModelHealthCheck(config) {
|
|
4915
|
-
const usesProxy =
|
|
5917
|
+
const usesProxy = anyStageNeedsProxy(config);
|
|
4916
5918
|
const baseUrl = usesProxy ? getLitellmUrl() : "https://api.anthropic.com";
|
|
4917
5919
|
const apiKey = usesProxy ? process.env.ANTHROPIC_COMPATIBLE_API_KEY : process.env.ANTHROPIC_API_KEY;
|
|
4918
5920
|
if (!apiKey) {
|
|
@@ -4922,19 +5924,19 @@ async function runModelHealthCheck(config) {
|
|
|
4922
5924
|
}
|
|
4923
5925
|
const model = config.agent.modelMap.cheap;
|
|
4924
5926
|
logger.info(`Model health check (${model} via ${usesProxy ? "LiteLLM" : "Anthropic"})...`);
|
|
4925
|
-
const
|
|
4926
|
-
if (
|
|
5927
|
+
const result2 = await checkModelHealth(baseUrl, apiKey, model);
|
|
5928
|
+
if (result2.ok) {
|
|
4927
5929
|
logger.info(" \u2713 Model responded");
|
|
4928
5930
|
} else {
|
|
4929
|
-
logger.error(` \u2717 Model health check failed: ${
|
|
5931
|
+
logger.error(` \u2717 Model health check failed: ${result2.error}`);
|
|
4930
5932
|
process.exit(1);
|
|
4931
5933
|
}
|
|
4932
5934
|
}
|
|
4933
5935
|
async function main() {
|
|
4934
5936
|
const input = parseArgs();
|
|
4935
|
-
const projectDir = input.cwd ?
|
|
5937
|
+
const projectDir = input.cwd ? path29.resolve(input.cwd) : process.cwd();
|
|
4936
5938
|
if (input.cwd) {
|
|
4937
|
-
if (!
|
|
5939
|
+
if (!fs32.existsSync(projectDir)) {
|
|
4938
5940
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
4939
5941
|
process.exit(1);
|
|
4940
5942
|
}
|
|
@@ -5000,8 +6002,8 @@ async function main() {
|
|
|
5000
6002
|
process.exit(1);
|
|
5001
6003
|
}
|
|
5002
6004
|
}
|
|
5003
|
-
const taskDir =
|
|
5004
|
-
|
|
6005
|
+
const taskDir = path29.join(projectDir, ".kody", "tasks", taskId);
|
|
6006
|
+
fs32.mkdirSync(taskDir, { recursive: true });
|
|
5005
6007
|
if (input.command === "rerun" && isTaskifyRun(taskDir)) {
|
|
5006
6008
|
const marker = readTaskifyMarker(taskDir);
|
|
5007
6009
|
if (marker) {
|
|
@@ -5066,7 +6068,7 @@ async function main() {
|
|
|
5066
6068
|
console.error(`Runner "${defaultRunnerName2}" health check failed`);
|
|
5067
6069
|
process.exit(1);
|
|
5068
6070
|
}
|
|
5069
|
-
const
|
|
6071
|
+
const result2 = await runStandaloneReview({
|
|
5070
6072
|
projectDir,
|
|
5071
6073
|
runners: runners2,
|
|
5072
6074
|
prTitle,
|
|
@@ -5076,15 +6078,15 @@ async function main() {
|
|
|
5076
6078
|
taskId
|
|
5077
6079
|
});
|
|
5078
6080
|
if (litellmProcess2) litellmProcess2.kill();
|
|
5079
|
-
if (
|
|
5080
|
-
console.error(`Review failed: ${
|
|
6081
|
+
if (result2.outcome === "failed") {
|
|
6082
|
+
console.error(`Review failed: ${result2.error}`);
|
|
5081
6083
|
process.exit(1);
|
|
5082
6084
|
}
|
|
5083
|
-
if (
|
|
5084
|
-
console.log(
|
|
6085
|
+
if (result2.reviewContent) {
|
|
6086
|
+
console.log(result2.reviewContent);
|
|
5085
6087
|
if (!input.local && prNumber) {
|
|
5086
|
-
const comment = formatReviewComment(
|
|
5087
|
-
const verdict = detectReviewVerdict(
|
|
6088
|
+
const comment = formatReviewComment(result2.reviewContent, taskId);
|
|
6089
|
+
const verdict = detectReviewVerdict(result2.reviewContent);
|
|
5088
6090
|
const event = verdict === "fail" ? "request-changes" : "approve";
|
|
5089
6091
|
const posted = submitPRReview(prNumber, comment, event);
|
|
5090
6092
|
if (!posted) {
|
|
@@ -5116,48 +6118,48 @@ async function main() {
|
|
|
5116
6118
|
process.exit(1);
|
|
5117
6119
|
}
|
|
5118
6120
|
const { runResolve: runResolve2 } = await Promise.resolve().then(() => (init_resolve(), resolve_exports));
|
|
5119
|
-
const
|
|
6121
|
+
const result2 = await runResolve2({
|
|
5120
6122
|
prNumber: input.prNumber,
|
|
5121
6123
|
projectDir,
|
|
5122
6124
|
runners: runners2,
|
|
5123
6125
|
local: input.local ?? true
|
|
5124
6126
|
});
|
|
5125
6127
|
if (litellmProcess2) litellmProcess2.kill();
|
|
5126
|
-
if (
|
|
5127
|
-
console.error(`Resolve failed: ${
|
|
6128
|
+
if (result2.outcome === "failed") {
|
|
6129
|
+
console.error(`Resolve failed: ${result2.error}`);
|
|
5128
6130
|
process.exit(1);
|
|
5129
6131
|
}
|
|
5130
|
-
console.log(`Resolve: ${
|
|
6132
|
+
console.log(`Resolve: ${result2.outcome}`);
|
|
5131
6133
|
process.exit(0);
|
|
5132
6134
|
}
|
|
5133
6135
|
logger.info("Preflight checks:");
|
|
5134
6136
|
runPreflight();
|
|
5135
6137
|
if (input.task) {
|
|
5136
|
-
|
|
6138
|
+
fs32.writeFileSync(path29.join(taskDir, "task.md"), input.task);
|
|
5137
6139
|
}
|
|
5138
|
-
const taskMdPath =
|
|
5139
|
-
if (!
|
|
6140
|
+
const taskMdPath = path29.join(taskDir, "task.md");
|
|
6141
|
+
if (!fs32.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
5140
6142
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
5141
6143
|
const prDetails = getPRDetails(input.prNumber);
|
|
5142
6144
|
if (prDetails) {
|
|
5143
6145
|
const taskContent = `# ${prDetails.title}
|
|
5144
6146
|
|
|
5145
6147
|
${prDetails.body ?? ""}`;
|
|
5146
|
-
|
|
6148
|
+
fs32.writeFileSync(taskMdPath, taskContent);
|
|
5147
6149
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
5148
6150
|
}
|
|
5149
|
-
} else if (!
|
|
6151
|
+
} else if (!fs32.existsSync(taskMdPath) && input.issueNumber) {
|
|
5150
6152
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
5151
6153
|
const issue = getIssue(input.issueNumber);
|
|
5152
6154
|
if (issue) {
|
|
5153
6155
|
const taskContent = `# ${issue.title}
|
|
5154
6156
|
|
|
5155
6157
|
${issue.body ?? ""}`;
|
|
5156
|
-
|
|
6158
|
+
fs32.writeFileSync(taskMdPath, taskContent);
|
|
5157
6159
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
5158
6160
|
}
|
|
5159
6161
|
}
|
|
5160
|
-
if (!
|
|
6162
|
+
if (!fs32.existsSync(taskMdPath)) {
|
|
5161
6163
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
5162
6164
|
process.exit(1);
|
|
5163
6165
|
}
|
|
@@ -5295,7 +6297,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
5295
6297
|
}
|
|
5296
6298
|
}
|
|
5297
6299
|
const state = await runPipeline(ctx);
|
|
5298
|
-
const files =
|
|
6300
|
+
const files = fs32.readdirSync(taskDir);
|
|
5299
6301
|
console.log(`
|
|
5300
6302
|
Artifacts in ${taskDir}:`);
|
|
5301
6303
|
for (const f of files) {
|
|
@@ -5360,8 +6362,8 @@ var init_entry = __esm({
|
|
|
5360
6362
|
});
|
|
5361
6363
|
|
|
5362
6364
|
// src/bin/cli.ts
|
|
5363
|
-
import * as
|
|
5364
|
-
import * as
|
|
6365
|
+
import * as fs33 from "fs";
|
|
6366
|
+
import * as path30 from "path";
|
|
5365
6367
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5366
6368
|
|
|
5367
6369
|
// src/bin/commands/init.ts
|
|
@@ -5533,7 +6535,7 @@ function buildConfig(cwd, basic) {
|
|
|
5533
6535
|
github: { owner: basic.owner, repo: basic.repo },
|
|
5534
6536
|
agent: {
|
|
5535
6537
|
provider: "anthropic",
|
|
5536
|
-
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
6538
|
+
modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
|
|
5537
6539
|
}
|
|
5538
6540
|
};
|
|
5539
6541
|
const mcp = detectMcpConfig(cwd, basic.pm, pkg);
|
|
@@ -5733,15 +6735,15 @@ function initCommand(opts, pkgRoot) {
|
|
|
5733
6735
|
|
|
5734
6736
|
// src/bin/commands/bootstrap.ts
|
|
5735
6737
|
init_architecture_detection();
|
|
5736
|
-
import * as
|
|
5737
|
-
import * as
|
|
6738
|
+
import * as fs8 from "fs";
|
|
6739
|
+
import * as path7 from "path";
|
|
5738
6740
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
5739
6741
|
|
|
5740
6742
|
// src/bin/qa-guide.ts
|
|
5741
6743
|
import * as fs5 from "fs";
|
|
5742
6744
|
import * as path4 from "path";
|
|
5743
6745
|
function discoverQaContext(cwd) {
|
|
5744
|
-
const
|
|
6746
|
+
const result2 = {
|
|
5745
6747
|
routes: [],
|
|
5746
6748
|
authFiles: [],
|
|
5747
6749
|
loginPage: null,
|
|
@@ -5754,21 +6756,21 @@ function discoverQaContext(cwd) {
|
|
|
5754
6756
|
const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
|
|
5755
6757
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5756
6758
|
const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
5757
|
-
if (pkg.scripts?.dev)
|
|
5758
|
-
if (allDeps.next || allDeps.nuxt)
|
|
5759
|
-
else if (allDeps.vite)
|
|
6759
|
+
if (pkg.scripts?.dev) result2.devCommand = `${pm} dev`;
|
|
6760
|
+
if (allDeps.next || allDeps.nuxt) result2.devPort = 3e3;
|
|
6761
|
+
else if (allDeps.vite) result2.devPort = 5173;
|
|
5760
6762
|
} catch {
|
|
5761
6763
|
}
|
|
5762
6764
|
const appDirs = ["src/app", "app"];
|
|
5763
6765
|
for (const appDir of appDirs) {
|
|
5764
6766
|
const fullAppDir = path4.join(cwd, appDir);
|
|
5765
6767
|
if (!fs5.existsSync(fullAppDir)) continue;
|
|
5766
|
-
scanRoutes(fullAppDir, appDir, "",
|
|
6768
|
+
scanRoutes(fullAppDir, appDir, "", result2);
|
|
5767
6769
|
break;
|
|
5768
6770
|
}
|
|
5769
6771
|
const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
5770
6772
|
for (const p of authPatterns) {
|
|
5771
|
-
if (fs5.existsSync(path4.join(cwd, p)))
|
|
6773
|
+
if (fs5.existsSync(path4.join(cwd, p))) result2.authFiles.push(p);
|
|
5772
6774
|
}
|
|
5773
6775
|
const authConfigGlobs = [
|
|
5774
6776
|
"src/app/api/auth",
|
|
@@ -5779,7 +6781,7 @@ function discoverQaContext(cwd) {
|
|
|
5779
6781
|
"src/app/api/oauth"
|
|
5780
6782
|
];
|
|
5781
6783
|
for (const g of authConfigGlobs) {
|
|
5782
|
-
if (fs5.existsSync(path4.join(cwd, g)))
|
|
6784
|
+
if (fs5.existsSync(path4.join(cwd, g))) result2.authFiles.push(g);
|
|
5783
6785
|
}
|
|
5784
6786
|
try {
|
|
5785
6787
|
const rolePaths = [
|
|
@@ -5801,7 +6803,7 @@ function discoverQaContext(cwd) {
|
|
|
5801
6803
|
if (roleMatches) {
|
|
5802
6804
|
for (const m of roleMatches) {
|
|
5803
6805
|
const val = m.match(/['"](\w+)['"]/);
|
|
5804
|
-
if (val && !
|
|
6806
|
+
if (val && !result2.roles.includes(val[1])) result2.roles.push(val[1]);
|
|
5805
6807
|
}
|
|
5806
6808
|
}
|
|
5807
6809
|
const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
|
|
@@ -5810,7 +6812,7 @@ function discoverQaContext(cwd) {
|
|
|
5810
6812
|
if (vals) {
|
|
5811
6813
|
for (const v of vals) {
|
|
5812
6814
|
const clean = v.replace(/['"]/g, "");
|
|
5813
|
-
if (!
|
|
6815
|
+
if (!result2.roles.includes(clean)) result2.roles.push(clean);
|
|
5814
6816
|
}
|
|
5815
6817
|
}
|
|
5816
6818
|
}
|
|
@@ -5820,9 +6822,9 @@ function discoverQaContext(cwd) {
|
|
|
5820
6822
|
}
|
|
5821
6823
|
} catch {
|
|
5822
6824
|
}
|
|
5823
|
-
return
|
|
6825
|
+
return result2;
|
|
5824
6826
|
}
|
|
5825
|
-
function scanRoutes(dir, baseDir, prefix,
|
|
6827
|
+
function scanRoutes(dir, baseDir, prefix, result2) {
|
|
5826
6828
|
let entries;
|
|
5827
6829
|
try {
|
|
5828
6830
|
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
@@ -5833,16 +6835,16 @@ function scanRoutes(dir, baseDir, prefix, result) {
|
|
|
5833
6835
|
if (hasPage) {
|
|
5834
6836
|
const routePath = prefix || "/";
|
|
5835
6837
|
const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
|
|
5836
|
-
|
|
5837
|
-
if (prefix.includes("/login"))
|
|
5838
|
-
if (prefix.startsWith("/admin") && !
|
|
6838
|
+
result2.routes.push({ path: routePath, group });
|
|
6839
|
+
if (prefix.includes("/login")) result2.loginPage = routePath;
|
|
6840
|
+
if (prefix.startsWith("/admin") && !result2.adminPath) result2.adminPath = prefix;
|
|
5839
6841
|
}
|
|
5840
6842
|
for (const entry of entries) {
|
|
5841
6843
|
if (!entry.isDirectory()) continue;
|
|
5842
6844
|
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
5843
6845
|
let segment = entry.name;
|
|
5844
6846
|
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
5845
|
-
scanRoutes(path4.join(dir, entry.name), baseDir, prefix,
|
|
6847
|
+
scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result2);
|
|
5846
6848
|
continue;
|
|
5847
6849
|
}
|
|
5848
6850
|
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
@@ -5851,7 +6853,7 @@ function scanRoutes(dir, baseDir, prefix, result) {
|
|
|
5851
6853
|
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
5852
6854
|
segment = `:${segment.slice(2, -2)}?`;
|
|
5853
6855
|
}
|
|
5854
|
-
scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`,
|
|
6856
|
+
scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result2);
|
|
5855
6857
|
}
|
|
5856
6858
|
}
|
|
5857
6859
|
function generateQaGuide(discovery) {
|
|
@@ -6008,22 +7010,23 @@ function installSkillsForProject(cwd) {
|
|
|
6008
7010
|
}
|
|
6009
7011
|
|
|
6010
7012
|
// src/bin/commands/bootstrap.ts
|
|
7013
|
+
init_config();
|
|
6011
7014
|
var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
|
|
6012
7015
|
function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
6013
|
-
const srcDir =
|
|
6014
|
-
const baseDir =
|
|
7016
|
+
const srcDir = path7.join(cwd, "src");
|
|
7017
|
+
const baseDir = fs8.existsSync(srcDir) ? srcDir : cwd;
|
|
6015
7018
|
const results = [];
|
|
6016
7019
|
function walk(dir) {
|
|
6017
7020
|
const entries = [];
|
|
6018
7021
|
try {
|
|
6019
|
-
for (const entry of
|
|
7022
|
+
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
6020
7023
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
6021
|
-
const full =
|
|
7024
|
+
const full = path7.join(dir, entry.name);
|
|
6022
7025
|
if (entry.isDirectory()) {
|
|
6023
7026
|
entries.push(...walk(full));
|
|
6024
7027
|
} else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
|
|
6025
7028
|
try {
|
|
6026
|
-
const stat =
|
|
7029
|
+
const stat = fs8.statSync(full);
|
|
6027
7030
|
if (stat.size >= 200 && stat.size <= 5e3) {
|
|
6028
7031
|
entries.push({ filePath: full, size: stat.size });
|
|
6029
7032
|
}
|
|
@@ -6037,8 +7040,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
|
6037
7040
|
}
|
|
6038
7041
|
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
6039
7042
|
for (const { filePath } of files) {
|
|
6040
|
-
const rel =
|
|
6041
|
-
const content =
|
|
7043
|
+
const rel = path7.relative(cwd, filePath);
|
|
7044
|
+
const content = fs8.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
6042
7045
|
results.push(`### File: ${rel}
|
|
6043
7046
|
\`\`\`typescript
|
|
6044
7047
|
${content}
|
|
@@ -6050,9 +7053,9 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
6050
7053
|
try {
|
|
6051
7054
|
let repoSlug = "";
|
|
6052
7055
|
try {
|
|
6053
|
-
const configPath =
|
|
6054
|
-
if (
|
|
6055
|
-
const config = JSON.parse(
|
|
7056
|
+
const configPath = path7.join(cwd, "kody.config.json");
|
|
7057
|
+
if (fs8.existsSync(configPath)) {
|
|
7058
|
+
const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
6056
7059
|
if (config.github?.owner && config.github?.repo) {
|
|
6057
7060
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
6058
7061
|
}
|
|
@@ -6079,7 +7082,9 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
6079
7082
|
}
|
|
6080
7083
|
function bootstrapCommand(opts, pkgRoot) {
|
|
6081
7084
|
const cwd = process.cwd();
|
|
7085
|
+
setConfigDir(cwd);
|
|
6082
7086
|
const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
|
|
7087
|
+
const bootstrapModel = resolveStageConfig(getProjectConfig(), "bootstrap", "cheap").model;
|
|
6083
7088
|
console.log(`
|
|
6084
7089
|
\u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
|
|
6085
7090
|
`);
|
|
@@ -6087,8 +7092,8 @@ function bootstrapCommand(opts, pkgRoot) {
|
|
|
6087
7092
|
ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
|
|
6088
7093
|
}
|
|
6089
7094
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
6090
|
-
const p =
|
|
6091
|
-
if (
|
|
7095
|
+
const p = path7.join(cwd, rel);
|
|
7096
|
+
if (fs8.existsSync(p)) return fs8.readFileSync(p, "utf-8").slice(0, maxChars);
|
|
6092
7097
|
return null;
|
|
6093
7098
|
};
|
|
6094
7099
|
let repoContext = "";
|
|
@@ -6123,14 +7128,14 @@ ${sampleFiles}
|
|
|
6123
7128
|
|
|
6124
7129
|
`;
|
|
6125
7130
|
try {
|
|
6126
|
-
const topDirs =
|
|
7131
|
+
const topDirs = fs8.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
6127
7132
|
repoContext += `## Top-level directories
|
|
6128
7133
|
${topDirs.join(", ")}
|
|
6129
7134
|
|
|
6130
7135
|
`;
|
|
6131
|
-
const srcDir =
|
|
6132
|
-
if (
|
|
6133
|
-
const srcDirs =
|
|
7136
|
+
const srcDir = path7.join(cwd, "src");
|
|
7137
|
+
if (fs8.existsSync(srcDir)) {
|
|
7138
|
+
const srcDirs = fs8.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6134
7139
|
if (srcDirs.length > 0) repoContext += `## src/ subdirectories
|
|
6135
7140
|
${srcDirs.join(", ")}
|
|
6136
7141
|
|
|
@@ -6140,19 +7145,19 @@ ${srcDirs.join(", ")}
|
|
|
6140
7145
|
}
|
|
6141
7146
|
const existingFiles = [];
|
|
6142
7147
|
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"]) {
|
|
6143
|
-
if (
|
|
7148
|
+
if (fs8.existsSync(path7.join(cwd, f))) existingFiles.push(f);
|
|
6144
7149
|
}
|
|
6145
7150
|
if (existingFiles.length) repoContext += `## Config files present
|
|
6146
7151
|
${existingFiles.join(", ")}
|
|
6147
7152
|
|
|
6148
7153
|
`;
|
|
6149
7154
|
console.log("\u2500\u2500 Project Memory \u2500\u2500");
|
|
6150
|
-
const memoryDir =
|
|
6151
|
-
|
|
6152
|
-
const archPath =
|
|
6153
|
-
const conventionsPath =
|
|
6154
|
-
const existingArch =
|
|
6155
|
-
const existingConv =
|
|
7155
|
+
const memoryDir = path7.join(cwd, ".kody", "memory");
|
|
7156
|
+
fs8.mkdirSync(memoryDir, { recursive: true });
|
|
7157
|
+
const archPath = path7.join(memoryDir, "architecture.md");
|
|
7158
|
+
const conventionsPath = path7.join(memoryDir, "conventions.md");
|
|
7159
|
+
const existingArch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
|
|
7160
|
+
const existingConv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
|
|
6156
7161
|
const hasExisting = !!(existingArch || existingConv);
|
|
6157
7162
|
const extendInstruction = hasExisting && !opts.force ? `
|
|
6158
7163
|
## Existing Documentation (EXTEND, do not replace)
|
|
@@ -6196,7 +7201,7 @@ ${repoContext}`;
|
|
|
6196
7201
|
const output = execFileSync5("claude", [
|
|
6197
7202
|
"--print",
|
|
6198
7203
|
"--model",
|
|
6199
|
-
|
|
7204
|
+
bootstrapModel,
|
|
6200
7205
|
"--dangerously-skip-permissions",
|
|
6201
7206
|
memoryPrompt
|
|
6202
7207
|
], {
|
|
@@ -6208,12 +7213,12 @@ ${repoContext}`;
|
|
|
6208
7213
|
const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
6209
7214
|
const parsed = JSON.parse(cleaned);
|
|
6210
7215
|
if (parsed.architecture) {
|
|
6211
|
-
|
|
7216
|
+
fs8.writeFileSync(archPath, parsed.architecture);
|
|
6212
7217
|
const lineCount = parsed.architecture.split("\n").length;
|
|
6213
7218
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
|
|
6214
7219
|
}
|
|
6215
7220
|
if (parsed.conventions) {
|
|
6216
|
-
|
|
7221
|
+
fs8.writeFileSync(conventionsPath, parsed.conventions);
|
|
6217
7222
|
const lineCount = parsed.conventions.split("\n").length;
|
|
6218
7223
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
|
|
6219
7224
|
}
|
|
@@ -6222,39 +7227,39 @@ ${repoContext}`;
|
|
|
6222
7227
|
const detected = detectArchitectureBasic(cwd);
|
|
6223
7228
|
if (detected.length > 0) {
|
|
6224
7229
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6225
|
-
|
|
7230
|
+
fs8.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
6226
7231
|
|
|
6227
7232
|
## Overview
|
|
6228
7233
|
${detected.join("\n")}
|
|
6229
7234
|
`);
|
|
6230
7235
|
console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
|
|
6231
7236
|
}
|
|
6232
|
-
|
|
7237
|
+
fs8.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
6233
7238
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
6234
7239
|
}
|
|
6235
7240
|
console.log("\n\u2500\u2500 Step Files \u2500\u2500");
|
|
6236
|
-
const stepsDir =
|
|
6237
|
-
|
|
6238
|
-
const arch =
|
|
6239
|
-
const conv =
|
|
7241
|
+
const stepsDir = path7.join(cwd, ".kody", "steps");
|
|
7242
|
+
fs8.mkdirSync(stepsDir, { recursive: true });
|
|
7243
|
+
const arch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
|
|
7244
|
+
const conv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
|
|
6240
7245
|
console.log(" \u23F3 Customizing step files...");
|
|
6241
7246
|
let stepCount = 0;
|
|
6242
7247
|
for (const stage of STEP_STAGES) {
|
|
6243
|
-
const templatePath =
|
|
6244
|
-
if (!
|
|
7248
|
+
const templatePath = path7.join(pkgRoot, "prompts", `${stage}.md`);
|
|
7249
|
+
if (!fs8.existsSync(templatePath)) {
|
|
6245
7250
|
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
6246
7251
|
continue;
|
|
6247
7252
|
}
|
|
6248
|
-
const stepOutputPath =
|
|
6249
|
-
if (
|
|
7253
|
+
const stepOutputPath = path7.join(stepsDir, `${stage}.md`);
|
|
7254
|
+
if (fs8.existsSync(stepOutputPath) && !opts.force) {
|
|
6250
7255
|
console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
|
|
6251
7256
|
continue;
|
|
6252
7257
|
}
|
|
6253
|
-
const defaultPrompt =
|
|
7258
|
+
const defaultPrompt = fs8.readFileSync(templatePath, "utf-8");
|
|
6254
7259
|
const contextPlaceholder = "{{TASK_CONTEXT}}";
|
|
6255
7260
|
const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
|
|
6256
7261
|
if (placeholderIdx === -1) {
|
|
6257
|
-
|
|
7262
|
+
fs8.copyFileSync(templatePath, stepOutputPath);
|
|
6258
7263
|
stepCount++;
|
|
6259
7264
|
console.log(` \u2713 ${stage}.md`);
|
|
6260
7265
|
continue;
|
|
@@ -6299,7 +7304,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6299
7304
|
const output = execFileSync5("claude", [
|
|
6300
7305
|
"--print",
|
|
6301
7306
|
"--model",
|
|
6302
|
-
|
|
7307
|
+
bootstrapModel,
|
|
6303
7308
|
"--dangerously-skip-permissions",
|
|
6304
7309
|
customizationPrompt
|
|
6305
7310
|
], {
|
|
@@ -6311,23 +7316,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6311
7316
|
let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
6312
7317
|
cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
|
|
6313
7318
|
const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
|
|
6314
|
-
|
|
7319
|
+
fs8.writeFileSync(stepOutputPath, finalPrompt);
|
|
6315
7320
|
stepCount++;
|
|
6316
7321
|
console.log(` \u2713 ${stage}.md`);
|
|
6317
7322
|
} catch {
|
|
6318
7323
|
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
6319
|
-
|
|
7324
|
+
fs8.copyFileSync(templatePath, stepOutputPath);
|
|
6320
7325
|
stepCount++;
|
|
6321
7326
|
}
|
|
6322
7327
|
}
|
|
6323
7328
|
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
6324
7329
|
console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
|
|
6325
|
-
const qaGuidePath =
|
|
6326
|
-
if (!
|
|
7330
|
+
const qaGuidePath = path7.join(cwd, ".kody", "qa-guide.md");
|
|
7331
|
+
if (!fs8.existsSync(qaGuidePath) || opts.force) {
|
|
6327
7332
|
const discovery = discoverQaContext(cwd);
|
|
6328
7333
|
if (discovery.routes.length > 0) {
|
|
6329
7334
|
const qaGuide = generateQaGuide(discovery);
|
|
6330
|
-
|
|
7335
|
+
fs8.writeFileSync(qaGuidePath, qaGuide);
|
|
6331
7336
|
console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
|
|
6332
7337
|
if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
|
|
6333
7338
|
if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
|
|
@@ -6342,9 +7347,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6342
7347
|
try {
|
|
6343
7348
|
let repoSlug = "";
|
|
6344
7349
|
try {
|
|
6345
|
-
const configPath =
|
|
6346
|
-
if (
|
|
6347
|
-
const config = JSON.parse(
|
|
7350
|
+
const configPath = path7.join(cwd, "kody.config.json");
|
|
7351
|
+
if (fs8.existsSync(configPath)) {
|
|
7352
|
+
const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
6348
7353
|
if (config.github?.owner && config.github?.repo) {
|
|
6349
7354
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
6350
7355
|
}
|
|
@@ -6417,19 +7422,19 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6417
7422
|
".kody/memory/conventions.md",
|
|
6418
7423
|
".kody/qa-guide.md",
|
|
6419
7424
|
...installedSkillPaths
|
|
6420
|
-
].filter((f) =>
|
|
6421
|
-
if (
|
|
7425
|
+
].filter((f) => fs8.existsSync(path7.join(cwd, f)));
|
|
7426
|
+
if (fs8.existsSync(path7.join(cwd, "skills-lock.json"))) {
|
|
6422
7427
|
filesToCommit.push("skills-lock.json");
|
|
6423
7428
|
}
|
|
6424
7429
|
for (const stage of STEP_STAGES) {
|
|
6425
7430
|
const stepFile = `.kody/steps/${stage}.md`;
|
|
6426
|
-
if (
|
|
7431
|
+
if (fs8.existsSync(path7.join(cwd, stepFile))) {
|
|
6427
7432
|
filesToCommit.push(stepFile);
|
|
6428
7433
|
}
|
|
6429
7434
|
}
|
|
6430
7435
|
if (filesToCommit.length > 0) {
|
|
6431
7436
|
try {
|
|
6432
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
7437
|
+
const fullPaths = filesToCommit.map((f) => path7.join(cwd, f));
|
|
6433
7438
|
for (let pass = 0; pass < 2; pass++) {
|
|
6434
7439
|
execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
|
|
6435
7440
|
cwd,
|
|
@@ -6456,9 +7461,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
6456
7461
|
console.log(` \u2713 Pushed branch: ${branchName}`);
|
|
6457
7462
|
let baseBranch = "main";
|
|
6458
7463
|
try {
|
|
6459
|
-
const configPath =
|
|
6460
|
-
if (
|
|
6461
|
-
const config = JSON.parse(
|
|
7464
|
+
const configPath = path7.join(cwd, "kody.config.json");
|
|
7465
|
+
if (fs8.existsSync(configPath)) {
|
|
7466
|
+
const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
|
|
6462
7467
|
baseBranch = config.git?.defaultBranch ?? "main";
|
|
6463
7468
|
}
|
|
6464
7469
|
} catch {
|
|
@@ -6532,11 +7537,11 @@ Create it manually.`, cwd);
|
|
|
6532
7537
|
|
|
6533
7538
|
// src/bin/cli.ts
|
|
6534
7539
|
init_architecture_detection();
|
|
6535
|
-
var __dirname2 =
|
|
6536
|
-
var PKG_ROOT =
|
|
7540
|
+
var __dirname2 = path30.dirname(fileURLToPath2(import.meta.url));
|
|
7541
|
+
var PKG_ROOT = path30.resolve(__dirname2, "..", "..");
|
|
6537
7542
|
function getVersion() {
|
|
6538
|
-
const pkgPath =
|
|
6539
|
-
const pkg = JSON.parse(
|
|
7543
|
+
const pkgPath = path30.join(PKG_ROOT, "package.json");
|
|
7544
|
+
const pkg = JSON.parse(fs33.readFileSync(pkgPath, "utf-8"));
|
|
6540
7545
|
return pkg.version;
|
|
6541
7546
|
}
|
|
6542
7547
|
var args = process.argv.slice(2);
|
|
@@ -6547,6 +7552,8 @@ if (command === "init") {
|
|
|
6547
7552
|
bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
6548
7553
|
} else if (command === "taskify") {
|
|
6549
7554
|
Promise.resolve().then(() => (init_taskify_command(), taskify_command_exports)).then(({ runTaskifyCommand: runTaskifyCommand2 }) => runTaskifyCommand2());
|
|
7555
|
+
} else if (command === "test-model") {
|
|
7556
|
+
Promise.resolve().then(() => (init_test_model_command(), test_model_command_exports)).then(({ runTestModelCommand: runTestModelCommand2 }) => runTestModelCommand2());
|
|
6550
7557
|
} else if (command === "ci-parse") {
|
|
6551
7558
|
Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
|
|
6552
7559
|
} else if (command === "version" || command === "--version" || command === "-v") {
|