@kody-ade/kody-engine-lite 0.1.109 → 0.1.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +463 -420
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -984,7 +984,24 @@ function resolveTaskIdFromComments(issueNumber) {
|
|
|
984
984
|
return null;
|
|
985
985
|
}
|
|
986
986
|
}
|
|
987
|
+
function findPausedTaskifyForIssue(issueNumber, projectDir) {
|
|
988
|
+
const tasksDir = path8.join(projectDir, ".kody", "tasks");
|
|
989
|
+
if (!fs9.existsSync(tasksDir)) return null;
|
|
990
|
+
const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
991
|
+
for (const dir of allDirs) {
|
|
992
|
+
const markerPath = path8.join(tasksDir, dir, "taskify.marker");
|
|
993
|
+
if (!fs9.existsSync(markerPath)) continue;
|
|
994
|
+
try {
|
|
995
|
+
const marker = JSON.parse(fs9.readFileSync(markerPath, "utf-8"));
|
|
996
|
+
if (marker.issueNumber === issueNumber) return dir;
|
|
997
|
+
} catch {
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
987
1002
|
function resolveTaskIdForCommand(issueNumber, projectDir) {
|
|
1003
|
+
const fromTaskify = findPausedTaskifyForIssue(issueNumber, projectDir);
|
|
1004
|
+
if (fromTaskify) return fromTaskify;
|
|
988
1005
|
const fromTasks = findLatestTaskForIssue(issueNumber, projectDir);
|
|
989
1006
|
if (fromTasks) return fromTasks;
|
|
990
1007
|
const fromComments = resolveTaskIdFromComments(issueNumber);
|
|
@@ -998,6 +1015,180 @@ var init_task_resolution = __esm({
|
|
|
998
1015
|
}
|
|
999
1016
|
});
|
|
1000
1017
|
|
|
1018
|
+
// src/cli/litellm.ts
|
|
1019
|
+
import * as fs10 from "fs";
|
|
1020
|
+
import * as os from "os";
|
|
1021
|
+
import * as path9 from "path";
|
|
1022
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
1023
|
+
async function checkLitellmHealth(url) {
|
|
1024
|
+
try {
|
|
1025
|
+
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
1026
|
+
return response.ok;
|
|
1027
|
+
} catch {
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
async function checkModelHealth(baseUrl, apiKey, model) {
|
|
1032
|
+
try {
|
|
1033
|
+
const res = await fetch(`${baseUrl}/v1/messages`, {
|
|
1034
|
+
method: "POST",
|
|
1035
|
+
headers: {
|
|
1036
|
+
"Content-Type": "application/json",
|
|
1037
|
+
"x-api-key": apiKey,
|
|
1038
|
+
"anthropic-version": "2023-06-01"
|
|
1039
|
+
},
|
|
1040
|
+
body: JSON.stringify({
|
|
1041
|
+
model,
|
|
1042
|
+
max_tokens: 4,
|
|
1043
|
+
messages: [{ role: "user", content: "Reply with: ok" }]
|
|
1044
|
+
}),
|
|
1045
|
+
signal: AbortSignal.timeout(3e4)
|
|
1046
|
+
});
|
|
1047
|
+
if (!res.ok) {
|
|
1048
|
+
const body2 = await res.text().catch(() => "");
|
|
1049
|
+
return { ok: false, error: `HTTP ${res.status}: ${body2.slice(0, 200)}` };
|
|
1050
|
+
}
|
|
1051
|
+
const body = await res.json();
|
|
1052
|
+
const hasAnthropicContent = Array.isArray(body.content) && body.content.some((b) => b.type === "text");
|
|
1053
|
+
const hasThinkingContent = Array.isArray(body.content) && body.content.some((b) => b.type === "thinking");
|
|
1054
|
+
const hasOpenAIContent = !!body.choices?.[0]?.message?.content;
|
|
1055
|
+
if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent) {
|
|
1056
|
+
return { ok: false, error: `Unexpected response format: ${JSON.stringify(body).slice(0, 200)}` };
|
|
1057
|
+
}
|
|
1058
|
+
return { ok: true };
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function generateLitellmConfig(provider, modelMap) {
|
|
1064
|
+
const apiKeyVar = providerApiKeyEnvVar(provider);
|
|
1065
|
+
const entries = ["model_list:"];
|
|
1066
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1067
|
+
for (const providerModel of Object.values(modelMap)) {
|
|
1068
|
+
if (seen.has(providerModel)) continue;
|
|
1069
|
+
seen.add(providerModel);
|
|
1070
|
+
entries.push(` - model_name: ${providerModel}`);
|
|
1071
|
+
entries.push(` litellm_params:`);
|
|
1072
|
+
entries.push(` model: ${provider}/${providerModel}`);
|
|
1073
|
+
entries.push(` api_key: os.environ/${apiKeyVar}`);
|
|
1074
|
+
}
|
|
1075
|
+
return entries.join("\n") + "\n";
|
|
1076
|
+
}
|
|
1077
|
+
function generateLitellmConfigFromStages(defaultConfig, stages) {
|
|
1078
|
+
const proxyModels = [];
|
|
1079
|
+
if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
|
|
1080
|
+
proxyModels.push(defaultConfig);
|
|
1081
|
+
}
|
|
1082
|
+
if (stages) {
|
|
1083
|
+
for (const sc of Object.values(stages)) {
|
|
1084
|
+
if (sc.provider !== "claude" && sc.provider !== "anthropic") {
|
|
1085
|
+
proxyModels.push(sc);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (proxyModels.length === 0) return void 0;
|
|
1090
|
+
const entries = ["model_list:"];
|
|
1091
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1092
|
+
for (const { provider, model } of proxyModels) {
|
|
1093
|
+
const key = `${provider}/${model}`;
|
|
1094
|
+
if (seen.has(key)) continue;
|
|
1095
|
+
seen.add(key);
|
|
1096
|
+
const apiKeyVar = providerApiKeyEnvVar(provider);
|
|
1097
|
+
entries.push(` - model_name: ${model}`);
|
|
1098
|
+
entries.push(` litellm_params:`);
|
|
1099
|
+
entries.push(` model: ${provider}/${model}`);
|
|
1100
|
+
entries.push(` api_key: os.environ/${apiKeyVar}`);
|
|
1101
|
+
}
|
|
1102
|
+
return entries.join("\n") + "\n";
|
|
1103
|
+
}
|
|
1104
|
+
async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
1105
|
+
if (!generatedConfig) {
|
|
1106
|
+
logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
const configPath = path9.join(os.tmpdir(), "kody-litellm-config.yaml");
|
|
1110
|
+
fs10.writeFileSync(configPath, generatedConfig);
|
|
1111
|
+
const portMatch = url.match(/:(\d+)/);
|
|
1112
|
+
const port = portMatch ? portMatch[1] : "4000";
|
|
1113
|
+
let litellmFound = false;
|
|
1114
|
+
try {
|
|
1115
|
+
execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
1116
|
+
litellmFound = true;
|
|
1117
|
+
} catch {
|
|
1118
|
+
try {
|
|
1119
|
+
execFileSync9("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
1120
|
+
litellmFound = true;
|
|
1121
|
+
} catch {
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (!litellmFound) {
|
|
1125
|
+
logger.warn("litellm not installed (pip install 'litellm[proxy]')");
|
|
1126
|
+
return null;
|
|
1127
|
+
}
|
|
1128
|
+
logger.info(`Starting LiteLLM proxy on port ${port}...`);
|
|
1129
|
+
let cmd;
|
|
1130
|
+
let args2;
|
|
1131
|
+
try {
|
|
1132
|
+
execFileSync9("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
1133
|
+
cmd = "litellm";
|
|
1134
|
+
args2 = ["--config", configPath, "--port", port];
|
|
1135
|
+
} catch {
|
|
1136
|
+
cmd = "python3";
|
|
1137
|
+
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
1138
|
+
}
|
|
1139
|
+
const dotenvPath = path9.join(projectDir, ".env");
|
|
1140
|
+
const dotenvVars = {};
|
|
1141
|
+
if (fs10.existsSync(dotenvPath)) {
|
|
1142
|
+
for (const rawLine of fs10.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
1143
|
+
const line = rawLine.trim();
|
|
1144
|
+
if (!line || line.startsWith("#")) continue;
|
|
1145
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
1146
|
+
if (match) {
|
|
1147
|
+
let value = match[2].trim();
|
|
1148
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1149
|
+
value = value.slice(1, -1);
|
|
1150
|
+
}
|
|
1151
|
+
const commentIdx = value.indexOf(" #");
|
|
1152
|
+
if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
|
|
1153
|
+
if (value) dotenvVars[match[1]] = value;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (Object.keys(dotenvVars).length > 0) {
|
|
1157
|
+
logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
1161
|
+
const child = spawn2(cmd, args2, {
|
|
1162
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1163
|
+
detached: true,
|
|
1164
|
+
env: { ...process.env, ...dotenvVars }
|
|
1165
|
+
});
|
|
1166
|
+
let proxyStderr = "";
|
|
1167
|
+
child.stderr?.on("data", (chunk) => {
|
|
1168
|
+
proxyStderr += chunk.toString();
|
|
1169
|
+
});
|
|
1170
|
+
for (let i = 0; i < 30; i++) {
|
|
1171
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1172
|
+
if (await checkLitellmHealth(url)) {
|
|
1173
|
+
logger.info(`LiteLLM proxy ready at ${url}`);
|
|
1174
|
+
return child;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (proxyStderr) {
|
|
1178
|
+
logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1e3)}`);
|
|
1179
|
+
}
|
|
1180
|
+
logger.warn("LiteLLM proxy failed to start within 60s");
|
|
1181
|
+
child.kill();
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
var init_litellm = __esm({
|
|
1185
|
+
"src/cli/litellm.ts"() {
|
|
1186
|
+
"use strict";
|
|
1187
|
+
init_logger();
|
|
1188
|
+
init_config();
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1001
1192
|
// src/cli/taskify-command.ts
|
|
1002
1193
|
var taskify_command_exports = {};
|
|
1003
1194
|
__export(taskify_command_exports, {
|
|
@@ -1007,8 +1198,8 @@ __export(taskify_command_exports, {
|
|
|
1007
1198
|
taskifyCommand: () => taskifyCommand,
|
|
1008
1199
|
topoSort: () => topoSort
|
|
1009
1200
|
});
|
|
1010
|
-
import * as
|
|
1011
|
-
import * as
|
|
1201
|
+
import * as fs11 from "fs";
|
|
1202
|
+
import * as path10 from "path";
|
|
1012
1203
|
import { fileURLToPath } from "url";
|
|
1013
1204
|
import { execSync } from "child_process";
|
|
1014
1205
|
function topoSort(tasks) {
|
|
@@ -1052,10 +1243,10 @@ function hasFlag(args2, flag) {
|
|
|
1052
1243
|
async function runTaskifyCommand() {
|
|
1053
1244
|
const args2 = process.argv.slice(3);
|
|
1054
1245
|
const cwdArg = getArg(args2, "--cwd") ?? process.cwd();
|
|
1055
|
-
const projectDir =
|
|
1246
|
+
const projectDir = path10.resolve(cwdArg);
|
|
1056
1247
|
const ticketId = getArg(args2, "--ticket") ?? process.env.TICKET_ID;
|
|
1057
1248
|
const prdFileArg = getArg(args2, "--file") ?? process.env.PRD_FILE;
|
|
1058
|
-
const prdFile = prdFileArg ?
|
|
1249
|
+
const prdFile = prdFileArg ? path10.resolve(projectDir, prdFileArg) : void 0;
|
|
1059
1250
|
const issueNumberStr = getArg(args2, "--issue-number") ?? process.env.ISSUE_NUMBER ?? "";
|
|
1060
1251
|
const issueNumber = issueNumberStr ? parseInt(issueNumberStr, 10) : void 0;
|
|
1061
1252
|
const feedback = getArg(args2, "--feedback") ?? process.env.FEEDBACK;
|
|
@@ -1066,19 +1257,40 @@ async function runTaskifyCommand() {
|
|
|
1066
1257
|
logger.error("Usage: kody taskify --ticket <ticket-id> OR kody taskify --file <prd.md>");
|
|
1067
1258
|
process.exit(1);
|
|
1068
1259
|
}
|
|
1069
|
-
if (prdFile && !
|
|
1260
|
+
if (prdFile && !fs11.existsSync(prdFile)) {
|
|
1070
1261
|
logger.error(`File not found: ${prdFile}`);
|
|
1071
1262
|
process.exit(1);
|
|
1072
1263
|
}
|
|
1073
1264
|
setConfigDir(projectDir);
|
|
1074
1265
|
setGhCwd(projectDir);
|
|
1075
|
-
|
|
1266
|
+
const config = getProjectConfig();
|
|
1267
|
+
let litellmProcess = null;
|
|
1268
|
+
let runnerEnv;
|
|
1269
|
+
if (anyStageNeedsProxy(config)) {
|
|
1270
|
+
const litellmUrl = getLitellmUrl();
|
|
1271
|
+
const proxyRunning = await checkLitellmHealth(litellmUrl);
|
|
1272
|
+
if (!proxyRunning) {
|
|
1273
|
+
let generatedConfig;
|
|
1274
|
+
if (config.agent.stages || config.agent.default) {
|
|
1275
|
+
generatedConfig = generateLitellmConfigFromStages(config.agent.default, config.agent.stages);
|
|
1276
|
+
} else if (config.agent.provider && config.agent.provider !== "anthropic") {
|
|
1277
|
+
generatedConfig = generateLitellmConfig(config.agent.provider, config.agent.modelMap);
|
|
1278
|
+
}
|
|
1279
|
+
litellmProcess = await tryStartLitellm(litellmUrl, projectDir, generatedConfig);
|
|
1280
|
+
}
|
|
1281
|
+
runnerEnv = {
|
|
1282
|
+
ANTHROPIC_BASE_URL: litellmUrl,
|
|
1283
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
|
|
1287
|
+
litellmProcess?.kill();
|
|
1076
1288
|
}
|
|
1077
1289
|
async function taskifyCommand(opts) {
|
|
1078
1290
|
const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
|
|
1079
1291
|
const config = getProjectConfig();
|
|
1080
|
-
const taskDir =
|
|
1081
|
-
|
|
1292
|
+
const taskDir = path10.join(projectDir, ".kody", "tasks", taskId);
|
|
1293
|
+
fs11.mkdirSync(taskDir, { recursive: true });
|
|
1082
1294
|
const mode = prdFile ? "file" : "ticket";
|
|
1083
1295
|
logger.info(`[taskify] mode=${mode} source=${ticketId ?? prdFile} issue=${issueNumber ?? "none"} task=${taskId}`);
|
|
1084
1296
|
let mcpConfigJson;
|
|
@@ -1103,14 +1315,14 @@ Add the required MCP server config to \`kody.config.json\` and try again.`
|
|
|
1103
1315
|
}
|
|
1104
1316
|
const sc = resolveStageConfig(config, "taskify", "strong");
|
|
1105
1317
|
const model = sc.model;
|
|
1106
|
-
const fileContent = prdFile ?
|
|
1318
|
+
const fileContent = prdFile ? fs11.readFileSync(prdFile, "utf-8") : void 0;
|
|
1107
1319
|
let projectContext;
|
|
1108
1320
|
{
|
|
1109
1321
|
const parts = [];
|
|
1110
|
-
const memoryPath =
|
|
1111
|
-
if (
|
|
1322
|
+
const memoryPath = path10.join(projectDir, ".kody", "memory.md");
|
|
1323
|
+
if (fs11.existsSync(memoryPath)) {
|
|
1112
1324
|
try {
|
|
1113
|
-
const content =
|
|
1325
|
+
const content = fs11.readFileSync(memoryPath, "utf-8").slice(0, 2e3);
|
|
1114
1326
|
if (content.trim()) parts.push(`### Project Memory
|
|
1115
1327
|
${content}`);
|
|
1116
1328
|
} catch {
|
|
@@ -1129,16 +1341,20 @@ ${lines.join("\n")}
|
|
|
1129
1341
|
}
|
|
1130
1342
|
const prompt = buildPrompt({ ticketId, fileContent, taskDir, feedback, projectContext });
|
|
1131
1343
|
if (issueNumber && !local) {
|
|
1132
|
-
const src = mode === "file" ? `file \`${
|
|
1133
|
-
|
|
1344
|
+
const src = mode === "file" ? `file \`${path10.basename(prdFile)}\`` : `ticket **${ticketId}**`;
|
|
1345
|
+
const runUrl = process.env.RUN_URL ? ` ([logs](${process.env.RUN_URL}))` : "";
|
|
1346
|
+
postComment(issueNumber, `\u{1F680} Kody pipeline started: \`${taskId}\`${runUrl}
|
|
1347
|
+
|
|
1348
|
+
Kody is decomposing ${src} into tasks...`);
|
|
1134
1349
|
setLifecycleLabel(issueNumber, "planning");
|
|
1135
1350
|
}
|
|
1136
|
-
|
|
1351
|
+
fs11.writeFileSync(path10.join(taskDir, MARKER_FILE), JSON.stringify({ ticketId, prdFile, issueNumber }));
|
|
1137
1352
|
const runner = opts.runner ?? createClaudeCodeRunner();
|
|
1138
1353
|
logger.info(` model=${model} timeout=${TASKIFY_TIMEOUT_MS / 1e3}s`);
|
|
1139
1354
|
const result = await runner.run("taskify", prompt, model, TASKIFY_TIMEOUT_MS, taskDir, {
|
|
1140
1355
|
cwd: projectDir,
|
|
1141
|
-
mcpConfigJson
|
|
1356
|
+
mcpConfigJson,
|
|
1357
|
+
env: opts.runnerEnv
|
|
1142
1358
|
});
|
|
1143
1359
|
if (result.outcome !== "completed") {
|
|
1144
1360
|
const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
|
|
@@ -1151,8 +1367,8 @@ ${lines.join("\n")}
|
|
|
1151
1367
|
}
|
|
1152
1368
|
process.exit(1);
|
|
1153
1369
|
}
|
|
1154
|
-
const resultPath =
|
|
1155
|
-
if (!
|
|
1370
|
+
const resultPath = path10.join(taskDir, RESULT_FILE);
|
|
1371
|
+
if (!fs11.existsSync(resultPath)) {
|
|
1156
1372
|
const errMsg = `Claude did not write ${RESULT_FILE}. Output:
|
|
1157
1373
|
|
|
1158
1374
|
${result.output?.slice(0, 500) ?? "(none)"}`;
|
|
@@ -1167,7 +1383,7 @@ ${errMsg}`);
|
|
|
1167
1383
|
}
|
|
1168
1384
|
let parsed;
|
|
1169
1385
|
try {
|
|
1170
|
-
parsed = JSON.parse(
|
|
1386
|
+
parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
|
|
1171
1387
|
} catch {
|
|
1172
1388
|
const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
|
|
1173
1389
|
logger.error(`[taskify] ${errMsg}`);
|
|
@@ -1177,7 +1393,7 @@ ${errMsg}`);
|
|
|
1177
1393
|
}
|
|
1178
1394
|
process.exit(1);
|
|
1179
1395
|
}
|
|
1180
|
-
const sourceLabel = ticketId ?? (prdFile ?
|
|
1396
|
+
const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
|
|
1181
1397
|
if (parsed.status === "questions") {
|
|
1182
1398
|
handleQuestions(parsed, sourceLabel, issueNumber, local ?? false);
|
|
1183
1399
|
} else if (parsed.status === "ready") {
|
|
@@ -1272,15 +1488,15 @@ function buildPrompt(opts) {
|
|
|
1272
1488
|
const { ticketId, fileContent, taskDir, feedback, projectContext } = opts;
|
|
1273
1489
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
1274
1490
|
const candidates = [
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1491
|
+
path10.resolve(scriptDir, "..", "prompts", "taskify-ticket.md"),
|
|
1492
|
+
path10.resolve(scriptDir, "..", "..", "prompts", "taskify-ticket.md"),
|
|
1493
|
+
path10.resolve(__dirname, "..", "..", "prompts", "taskify-ticket.md"),
|
|
1494
|
+
path10.resolve(__dirname, "..", "prompts", "taskify-ticket.md")
|
|
1279
1495
|
];
|
|
1280
1496
|
let template = "";
|
|
1281
1497
|
for (const candidate of candidates) {
|
|
1282
|
-
if (
|
|
1283
|
-
template =
|
|
1498
|
+
if (fs11.existsSync(candidate)) {
|
|
1499
|
+
template = fs11.readFileSync(candidate, "utf-8");
|
|
1284
1500
|
break;
|
|
1285
1501
|
}
|
|
1286
1502
|
}
|
|
@@ -1303,13 +1519,13 @@ function buildPrompt(opts) {
|
|
|
1303
1519
|
return template;
|
|
1304
1520
|
}
|
|
1305
1521
|
function isTaskifyRun(taskDir) {
|
|
1306
|
-
return
|
|
1522
|
+
return fs11.existsSync(path10.join(taskDir, MARKER_FILE));
|
|
1307
1523
|
}
|
|
1308
1524
|
function readTaskifyMarker(taskDir) {
|
|
1309
|
-
const markerPath =
|
|
1310
|
-
if (!
|
|
1525
|
+
const markerPath = path10.join(taskDir, MARKER_FILE);
|
|
1526
|
+
if (!fs11.existsSync(markerPath)) return null;
|
|
1311
1527
|
try {
|
|
1312
|
-
return JSON.parse(
|
|
1528
|
+
return JSON.parse(fs11.readFileSync(markerPath, "utf-8"));
|
|
1313
1529
|
} catch {
|
|
1314
1530
|
return null;
|
|
1315
1531
|
}
|
|
@@ -1324,7 +1540,8 @@ var init_taskify_command = __esm({
|
|
|
1324
1540
|
init_github_api();
|
|
1325
1541
|
init_logger();
|
|
1326
1542
|
init_task_resolution();
|
|
1327
|
-
|
|
1543
|
+
init_litellm();
|
|
1544
|
+
__dirname = path10.dirname(fileURLToPath(import.meta.url));
|
|
1328
1545
|
AUTO_TRIGGER_THRESHOLD = 5;
|
|
1329
1546
|
MAX_TASKS_GUARD = 20;
|
|
1330
1547
|
TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -1340,7 +1557,7 @@ __export(parse_inputs_exports, {
|
|
|
1340
1557
|
runCiParse: () => runCiParse,
|
|
1341
1558
|
writeOutputs: () => writeOutputs
|
|
1342
1559
|
});
|
|
1343
|
-
import * as
|
|
1560
|
+
import * as fs12 from "fs";
|
|
1344
1561
|
function generateTimestamp() {
|
|
1345
1562
|
const now = /* @__PURE__ */ new Date();
|
|
1346
1563
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -1506,12 +1723,12 @@ function writeOutputs(result) {
|
|
|
1506
1723
|
function output(key, value) {
|
|
1507
1724
|
if (outputFile) {
|
|
1508
1725
|
if (value.includes("\n")) {
|
|
1509
|
-
|
|
1726
|
+
fs12.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
1510
1727
|
${value}
|
|
1511
1728
|
KODY_EOF
|
|
1512
1729
|
`);
|
|
1513
1730
|
} else {
|
|
1514
|
-
|
|
1731
|
+
fs12.appendFileSync(outputFile, `${key}=${value}
|
|
1515
1732
|
`);
|
|
1516
1733
|
}
|
|
1517
1734
|
}
|
|
@@ -1636,7 +1853,7 @@ var init_definitions = __esm({
|
|
|
1636
1853
|
});
|
|
1637
1854
|
|
|
1638
1855
|
// src/git-utils.ts
|
|
1639
|
-
import { execFileSync as
|
|
1856
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
1640
1857
|
function getHookSafeEnv() {
|
|
1641
1858
|
if (!_hookSafeEnv) {
|
|
1642
1859
|
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
@@ -1644,7 +1861,7 @@ function getHookSafeEnv() {
|
|
|
1644
1861
|
return _hookSafeEnv;
|
|
1645
1862
|
}
|
|
1646
1863
|
function git(args2, options) {
|
|
1647
|
-
return
|
|
1864
|
+
return execFileSync10("git", args2, {
|
|
1648
1865
|
encoding: "utf-8",
|
|
1649
1866
|
timeout: options?.timeout ?? 3e4,
|
|
1650
1867
|
cwd: options?.cwd,
|
|
@@ -1830,14 +2047,14 @@ var init_git_utils = __esm({
|
|
|
1830
2047
|
});
|
|
1831
2048
|
|
|
1832
2049
|
// src/pipeline/state.ts
|
|
1833
|
-
import * as
|
|
1834
|
-
import * as
|
|
2050
|
+
import * as fs13 from "fs";
|
|
2051
|
+
import * as path11 from "path";
|
|
1835
2052
|
function loadState(taskId, taskDir) {
|
|
1836
|
-
const p =
|
|
1837
|
-
if (!
|
|
2053
|
+
const p = path11.join(taskDir, "status.json");
|
|
2054
|
+
if (!fs13.existsSync(p)) return null;
|
|
1838
2055
|
try {
|
|
1839
2056
|
const result = parseJsonSafe(
|
|
1840
|
-
|
|
2057
|
+
fs13.readFileSync(p, "utf-8"),
|
|
1841
2058
|
["taskId", "state", "stages", "createdAt", "updatedAt"]
|
|
1842
2059
|
);
|
|
1843
2060
|
if (!result.ok) {
|
|
@@ -1855,10 +2072,10 @@ function writeState(state, taskDir) {
|
|
|
1855
2072
|
...state,
|
|
1856
2073
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1857
2074
|
};
|
|
1858
|
-
const target =
|
|
2075
|
+
const target = path11.join(taskDir, "status.json");
|
|
1859
2076
|
const tmp = target + ".tmp";
|
|
1860
|
-
|
|
1861
|
-
|
|
2077
|
+
fs13.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
2078
|
+
fs13.renameSync(tmp, target);
|
|
1862
2079
|
return updated;
|
|
1863
2080
|
}
|
|
1864
2081
|
function initState(taskId) {
|
|
@@ -1899,16 +2116,16 @@ var init_complexity = __esm({
|
|
|
1899
2116
|
});
|
|
1900
2117
|
|
|
1901
2118
|
// src/memory.ts
|
|
1902
|
-
import * as
|
|
1903
|
-
import * as
|
|
2119
|
+
import * as fs14 from "fs";
|
|
2120
|
+
import * as path12 from "path";
|
|
1904
2121
|
function readProjectMemory(projectDir) {
|
|
1905
|
-
const memoryDir =
|
|
1906
|
-
if (!
|
|
1907
|
-
const files =
|
|
2122
|
+
const memoryDir = path12.join(projectDir, ".kody", "memory");
|
|
2123
|
+
if (!fs14.existsSync(memoryDir)) return "";
|
|
2124
|
+
const files = fs14.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
1908
2125
|
if (files.length === 0) return "";
|
|
1909
2126
|
const sections = [];
|
|
1910
2127
|
for (const file of files) {
|
|
1911
|
-
const content =
|
|
2128
|
+
const content = fs14.readFileSync(path12.join(memoryDir, file), "utf-8").trim();
|
|
1912
2129
|
if (content) {
|
|
1913
2130
|
sections.push(`## ${file.replace(".md", "")}
|
|
1914
2131
|
${content}`);
|
|
@@ -1927,8 +2144,8 @@ var init_memory = __esm({
|
|
|
1927
2144
|
});
|
|
1928
2145
|
|
|
1929
2146
|
// src/context-tiers.ts
|
|
1930
|
-
import * as
|
|
1931
|
-
import * as
|
|
2147
|
+
import * as fs15 from "fs";
|
|
2148
|
+
import * as path13 from "path";
|
|
1932
2149
|
function estimateTokens(text) {
|
|
1933
2150
|
return Math.ceil(text.length / 4);
|
|
1934
2151
|
}
|
|
@@ -2019,7 +2236,7 @@ function generateL1Json(content) {
|
|
|
2019
2236
|
}
|
|
2020
2237
|
}
|
|
2021
2238
|
function getTieredContent(filePath, content) {
|
|
2022
|
-
const key =
|
|
2239
|
+
const key = path13.basename(filePath);
|
|
2023
2240
|
return {
|
|
2024
2241
|
source: filePath,
|
|
2025
2242
|
L0: generateL0(content, key),
|
|
@@ -2031,15 +2248,15 @@ function selectTier(tiered, tier) {
|
|
|
2031
2248
|
return tiered[tier];
|
|
2032
2249
|
}
|
|
2033
2250
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
2034
|
-
const memoryDir =
|
|
2035
|
-
if (!
|
|
2036
|
-
const files =
|
|
2251
|
+
const memoryDir = path13.join(projectDir, ".kody", "memory");
|
|
2252
|
+
if (!fs15.existsSync(memoryDir)) return "";
|
|
2253
|
+
const files = fs15.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
2037
2254
|
if (files.length === 0) return "";
|
|
2038
2255
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
2039
2256
|
const sections = [];
|
|
2040
2257
|
for (const file of files) {
|
|
2041
|
-
const filePath =
|
|
2042
|
-
const content =
|
|
2258
|
+
const filePath = path13.join(memoryDir, file);
|
|
2259
|
+
const content = fs15.readFileSync(filePath, "utf-8").trim();
|
|
2043
2260
|
if (!content) continue;
|
|
2044
2261
|
const tiered = getTieredContent(filePath, content);
|
|
2045
2262
|
const selected = selectTier(tiered, tier);
|
|
@@ -2062,9 +2279,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
2062
2279
|
`;
|
|
2063
2280
|
context += `Task Directory: ${taskDir}
|
|
2064
2281
|
`;
|
|
2065
|
-
const taskMdPath =
|
|
2066
|
-
if (
|
|
2067
|
-
const content =
|
|
2282
|
+
const taskMdPath = path13.join(taskDir, "task.md");
|
|
2283
|
+
if (fs15.existsSync(taskMdPath)) {
|
|
2284
|
+
const content = fs15.readFileSync(taskMdPath, "utf-8");
|
|
2068
2285
|
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
2069
2286
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
2070
2287
|
context += `
|
|
@@ -2072,9 +2289,9 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
|
2072
2289
|
${selected}
|
|
2073
2290
|
`;
|
|
2074
2291
|
}
|
|
2075
|
-
const taskJsonPath =
|
|
2076
|
-
if (
|
|
2077
|
-
const content =
|
|
2292
|
+
const taskJsonPath = path13.join(taskDir, "task.json");
|
|
2293
|
+
if (fs15.existsSync(taskJsonPath)) {
|
|
2294
|
+
const content = fs15.readFileSync(taskJsonPath, "utf-8");
|
|
2078
2295
|
if (policy.taskClassification === "L2") {
|
|
2079
2296
|
try {
|
|
2080
2297
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -2100,9 +2317,9 @@ ${selected}
|
|
|
2100
2317
|
}
|
|
2101
2318
|
}
|
|
2102
2319
|
}
|
|
2103
|
-
const specPath =
|
|
2104
|
-
if (
|
|
2105
|
-
const content =
|
|
2320
|
+
const specPath = path13.join(taskDir, "spec.md");
|
|
2321
|
+
if (fs15.existsSync(specPath)) {
|
|
2322
|
+
const content = fs15.readFileSync(specPath, "utf-8");
|
|
2106
2323
|
const selected = selectContent(specPath, content, policy.spec);
|
|
2107
2324
|
const label = tierLabel("Spec", policy.spec);
|
|
2108
2325
|
context += `
|
|
@@ -2110,9 +2327,9 @@ ${selected}
|
|
|
2110
2327
|
${selected}
|
|
2111
2328
|
`;
|
|
2112
2329
|
}
|
|
2113
|
-
const planPath =
|
|
2114
|
-
if (
|
|
2115
|
-
const content =
|
|
2330
|
+
const planPath = path13.join(taskDir, "plan.md");
|
|
2331
|
+
if (fs15.existsSync(planPath)) {
|
|
2332
|
+
const content = fs15.readFileSync(planPath, "utf-8");
|
|
2116
2333
|
const selected = selectContent(planPath, content, policy.plan);
|
|
2117
2334
|
const label = tierLabel("Plan", policy.plan);
|
|
2118
2335
|
context += `
|
|
@@ -2120,9 +2337,9 @@ ${selected}
|
|
|
2120
2337
|
${selected}
|
|
2121
2338
|
`;
|
|
2122
2339
|
}
|
|
2123
|
-
const contextMdPath =
|
|
2124
|
-
if (
|
|
2125
|
-
const content =
|
|
2340
|
+
const contextMdPath = path13.join(taskDir, "context.md");
|
|
2341
|
+
if (fs15.existsSync(contextMdPath)) {
|
|
2342
|
+
const content = fs15.readFileSync(contextMdPath, "utf-8");
|
|
2126
2343
|
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
2127
2344
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
2128
2345
|
context += `
|
|
@@ -2208,24 +2425,24 @@ var init_context_tiers = __esm({
|
|
|
2208
2425
|
});
|
|
2209
2426
|
|
|
2210
2427
|
// src/context.ts
|
|
2211
|
-
import * as
|
|
2212
|
-
import * as
|
|
2428
|
+
import * as fs16 from "fs";
|
|
2429
|
+
import * as path14 from "path";
|
|
2213
2430
|
function readPromptFile(stageName, projectDir) {
|
|
2214
2431
|
if (projectDir) {
|
|
2215
|
-
const stepFile =
|
|
2216
|
-
if (
|
|
2217
|
-
return
|
|
2432
|
+
const stepFile = path14.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
2433
|
+
if (fs16.existsSync(stepFile)) {
|
|
2434
|
+
return fs16.readFileSync(stepFile, "utf-8");
|
|
2218
2435
|
}
|
|
2219
2436
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
2220
2437
|
}
|
|
2221
2438
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
2222
2439
|
const candidates = [
|
|
2223
|
-
|
|
2224
|
-
|
|
2440
|
+
path14.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
2441
|
+
path14.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
2225
2442
|
];
|
|
2226
2443
|
for (const candidate of candidates) {
|
|
2227
|
-
if (
|
|
2228
|
-
return
|
|
2444
|
+
if (fs16.existsSync(candidate)) {
|
|
2445
|
+
return fs16.readFileSync(candidate, "utf-8");
|
|
2229
2446
|
}
|
|
2230
2447
|
}
|
|
2231
2448
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -2237,18 +2454,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
2237
2454
|
`;
|
|
2238
2455
|
context += `Task Directory: ${taskDir}
|
|
2239
2456
|
`;
|
|
2240
|
-
const taskMdPath =
|
|
2241
|
-
if (
|
|
2242
|
-
const taskMd =
|
|
2457
|
+
const taskMdPath = path14.join(taskDir, "task.md");
|
|
2458
|
+
if (fs16.existsSync(taskMdPath)) {
|
|
2459
|
+
const taskMd = fs16.readFileSync(taskMdPath, "utf-8");
|
|
2243
2460
|
context += `
|
|
2244
2461
|
## Task Description
|
|
2245
2462
|
${taskMd}
|
|
2246
2463
|
`;
|
|
2247
2464
|
}
|
|
2248
|
-
const taskJsonPath =
|
|
2249
|
-
if (
|
|
2465
|
+
const taskJsonPath = path14.join(taskDir, "task.json");
|
|
2466
|
+
if (fs16.existsSync(taskJsonPath)) {
|
|
2250
2467
|
try {
|
|
2251
|
-
const taskDef = JSON.parse(
|
|
2468
|
+
const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
|
|
2252
2469
|
context += `
|
|
2253
2470
|
## Task Classification
|
|
2254
2471
|
`;
|
|
@@ -2261,27 +2478,27 @@ ${taskMd}
|
|
|
2261
2478
|
} catch {
|
|
2262
2479
|
}
|
|
2263
2480
|
}
|
|
2264
|
-
const specPath =
|
|
2265
|
-
if (
|
|
2266
|
-
const spec =
|
|
2481
|
+
const specPath = path14.join(taskDir, "spec.md");
|
|
2482
|
+
if (fs16.existsSync(specPath)) {
|
|
2483
|
+
const spec = fs16.readFileSync(specPath, "utf-8");
|
|
2267
2484
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
2268
2485
|
context += `
|
|
2269
2486
|
## Spec Summary
|
|
2270
2487
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
2271
2488
|
`;
|
|
2272
2489
|
}
|
|
2273
|
-
const planPath =
|
|
2274
|
-
if (
|
|
2275
|
-
const plan =
|
|
2490
|
+
const planPath = path14.join(taskDir, "plan.md");
|
|
2491
|
+
if (fs16.existsSync(planPath)) {
|
|
2492
|
+
const plan = fs16.readFileSync(planPath, "utf-8");
|
|
2276
2493
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
2277
2494
|
context += `
|
|
2278
2495
|
## Plan Summary
|
|
2279
2496
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
2280
2497
|
`;
|
|
2281
2498
|
}
|
|
2282
|
-
const contextMdPath =
|
|
2283
|
-
if (
|
|
2284
|
-
const accumulated =
|
|
2499
|
+
const contextMdPath = path14.join(taskDir, "context.md");
|
|
2500
|
+
if (fs16.existsSync(contextMdPath)) {
|
|
2501
|
+
const accumulated = fs16.readFileSync(contextMdPath, "utf-8");
|
|
2285
2502
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
2286
2503
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
2287
2504
|
context += `
|
|
@@ -2299,17 +2516,17 @@ ${feedback}
|
|
|
2299
2516
|
}
|
|
2300
2517
|
function inferHasUIFromScope(scope) {
|
|
2301
2518
|
return scope.some((filePath) => {
|
|
2302
|
-
const ext =
|
|
2519
|
+
const ext = path14.extname(filePath).toLowerCase();
|
|
2303
2520
|
if (UI_EXTENSIONS.has(ext)) return true;
|
|
2304
2521
|
const normalized = filePath.replace(/\\/g, "/");
|
|
2305
2522
|
return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
|
|
2306
2523
|
});
|
|
2307
2524
|
}
|
|
2308
2525
|
function taskHasUI(taskDir) {
|
|
2309
|
-
const taskJsonPath =
|
|
2310
|
-
if (!
|
|
2526
|
+
const taskJsonPath = path14.join(taskDir, "task.json");
|
|
2527
|
+
if (!fs16.existsSync(taskJsonPath)) return true;
|
|
2311
2528
|
try {
|
|
2312
|
-
const taskDef = JSON.parse(
|
|
2529
|
+
const taskDef = JSON.parse(fs16.readFileSync(taskJsonPath, "utf-8"));
|
|
2313
2530
|
const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
|
|
2314
2531
|
if (scope.length === 0) return true;
|
|
2315
2532
|
return inferHasUIFromScope(scope);
|
|
@@ -2431,9 +2648,9 @@ ${prompt}` : prompt;
|
|
|
2431
2648
|
}
|
|
2432
2649
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
2433
2650
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
2434
|
-
const qaGuidePath =
|
|
2435
|
-
if (
|
|
2436
|
-
const qaGuide =
|
|
2651
|
+
const qaGuidePath = path14.join(projectDir, ".kody", "qa-guide.md");
|
|
2652
|
+
if (fs16.existsSync(qaGuidePath)) {
|
|
2653
|
+
const qaGuide = fs16.readFileSync(qaGuidePath, "utf-8").trim();
|
|
2437
2654
|
assembled = assembled + "\n\n" + qaGuide;
|
|
2438
2655
|
}
|
|
2439
2656
|
}
|
|
@@ -2528,8 +2745,8 @@ var init_runner_selection = __esm({
|
|
|
2528
2745
|
});
|
|
2529
2746
|
|
|
2530
2747
|
// src/stages/agent.ts
|
|
2531
|
-
import * as
|
|
2532
|
-
import * as
|
|
2748
|
+
import * as fs17 from "fs";
|
|
2749
|
+
import * as path15 from "path";
|
|
2533
2750
|
function getSessionInfo(stageName, sessions) {
|
|
2534
2751
|
const group = SESSION_GROUP[stageName];
|
|
2535
2752
|
if (!group) return void 0;
|
|
@@ -2616,27 +2833,27 @@ async function executeAgentStage(ctx, def) {
|
|
|
2616
2833
|
}
|
|
2617
2834
|
const result = lastResult;
|
|
2618
2835
|
if (def.outputFile && result.output) {
|
|
2619
|
-
|
|
2836
|
+
fs17.writeFileSync(path15.join(ctx.taskDir, def.outputFile), result.output);
|
|
2620
2837
|
}
|
|
2621
2838
|
if (def.outputFile) {
|
|
2622
|
-
const outputPath =
|
|
2623
|
-
if (!
|
|
2624
|
-
const ext =
|
|
2625
|
-
const base =
|
|
2626
|
-
const files =
|
|
2839
|
+
const outputPath = path15.join(ctx.taskDir, def.outputFile);
|
|
2840
|
+
if (!fs17.existsSync(outputPath)) {
|
|
2841
|
+
const ext = path15.extname(def.outputFile);
|
|
2842
|
+
const base = path15.basename(def.outputFile, ext);
|
|
2843
|
+
const files = fs17.readdirSync(ctx.taskDir);
|
|
2627
2844
|
const variant = files.find(
|
|
2628
2845
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
2629
2846
|
);
|
|
2630
2847
|
if (variant) {
|
|
2631
|
-
|
|
2848
|
+
fs17.renameSync(path15.join(ctx.taskDir, variant), outputPath);
|
|
2632
2849
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
2633
2850
|
}
|
|
2634
2851
|
}
|
|
2635
2852
|
}
|
|
2636
2853
|
if (def.outputFile) {
|
|
2637
|
-
const outputPath =
|
|
2638
|
-
if (
|
|
2639
|
-
const content =
|
|
2854
|
+
const outputPath = path15.join(ctx.taskDir, def.outputFile);
|
|
2855
|
+
if (fs17.existsSync(outputPath)) {
|
|
2856
|
+
const content = fs17.readFileSync(outputPath, "utf-8");
|
|
2640
2857
|
const validation = validateStageOutput(def.name, content);
|
|
2641
2858
|
if (!validation.valid) {
|
|
2642
2859
|
if (def.name === "taskify") {
|
|
@@ -2650,7 +2867,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2650
2867
|
const stripped = stripFences(retryResult.output);
|
|
2651
2868
|
const retryValidation = validateTaskJson(stripped);
|
|
2652
2869
|
if (retryValidation.valid) {
|
|
2653
|
-
|
|
2870
|
+
fs17.writeFileSync(outputPath, retryResult.output);
|
|
2654
2871
|
logger.info(` taskify retry produced valid JSON`);
|
|
2655
2872
|
} else {
|
|
2656
2873
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -2663,7 +2880,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2663
2880
|
risk_level: "low",
|
|
2664
2881
|
questions: []
|
|
2665
2882
|
}, null, 2);
|
|
2666
|
-
|
|
2883
|
+
fs17.writeFileSync(outputPath, fallback);
|
|
2667
2884
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
2668
2885
|
}
|
|
2669
2886
|
}
|
|
@@ -2677,7 +2894,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
2677
2894
|
return { outcome: "completed", outputFile: def.outputFile, retries };
|
|
2678
2895
|
}
|
|
2679
2896
|
function appendStageContext(taskDir, stageName, output) {
|
|
2680
|
-
const contextPath =
|
|
2897
|
+
const contextPath = path15.join(taskDir, "context.md");
|
|
2681
2898
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
2682
2899
|
let summary;
|
|
2683
2900
|
if (output && output.trim()) {
|
|
@@ -2690,7 +2907,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
2690
2907
|
### ${stageName} (${timestamp2})
|
|
2691
2908
|
${summary}
|
|
2692
2909
|
`;
|
|
2693
|
-
|
|
2910
|
+
fs17.appendFileSync(contextPath, entry);
|
|
2694
2911
|
}
|
|
2695
2912
|
var SESSION_GROUP;
|
|
2696
2913
|
var init_agent = __esm({
|
|
@@ -2713,7 +2930,7 @@ var init_agent = __esm({
|
|
|
2713
2930
|
});
|
|
2714
2931
|
|
|
2715
2932
|
// src/verify-runner.ts
|
|
2716
|
-
import { execFileSync as
|
|
2933
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
2717
2934
|
function isExecError(err) {
|
|
2718
2935
|
return typeof err === "object" && err !== null;
|
|
2719
2936
|
}
|
|
@@ -2749,7 +2966,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
2749
2966
|
return { success: true, output: "", timedOut: false };
|
|
2750
2967
|
}
|
|
2751
2968
|
try {
|
|
2752
|
-
const output =
|
|
2969
|
+
const output = execFileSync11(parts[0], parts.slice(1), {
|
|
2753
2970
|
cwd,
|
|
2754
2971
|
timeout,
|
|
2755
2972
|
encoding: "utf-8",
|
|
@@ -2820,7 +3037,7 @@ var init_verify_runner = __esm({
|
|
|
2820
3037
|
});
|
|
2821
3038
|
|
|
2822
3039
|
// src/observer.ts
|
|
2823
|
-
import { execFileSync as
|
|
3040
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
2824
3041
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
2825
3042
|
const context = [
|
|
2826
3043
|
`Stage: ${stageName}`,
|
|
@@ -2880,13 +3097,13 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
2880
3097
|
}
|
|
2881
3098
|
function getModifiedFiles(projectDir) {
|
|
2882
3099
|
try {
|
|
2883
|
-
const staged =
|
|
3100
|
+
const staged = execFileSync12("git", ["diff", "--name-only", "--cached"], {
|
|
2884
3101
|
encoding: "utf-8",
|
|
2885
3102
|
cwd: projectDir,
|
|
2886
3103
|
timeout: 5e3,
|
|
2887
3104
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2888
3105
|
}).trim();
|
|
2889
|
-
const unstaged =
|
|
3106
|
+
const unstaged = execFileSync12("git", ["diff", "--name-only"], {
|
|
2890
3107
|
encoding: "utf-8",
|
|
2891
3108
|
cwd: projectDir,
|
|
2892
3109
|
timeout: 5e3,
|
|
@@ -2929,8 +3146,8 @@ Error context:
|
|
|
2929
3146
|
});
|
|
2930
3147
|
|
|
2931
3148
|
// src/stages/gate.ts
|
|
2932
|
-
import * as
|
|
2933
|
-
import * as
|
|
3149
|
+
import * as fs18 from "fs";
|
|
3150
|
+
import * as path16 from "path";
|
|
2934
3151
|
function executeGateStage(ctx, def) {
|
|
2935
3152
|
if (ctx.input.dryRun) {
|
|
2936
3153
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -2973,7 +3190,7 @@ ${output}
|
|
|
2973
3190
|
`);
|
|
2974
3191
|
}
|
|
2975
3192
|
}
|
|
2976
|
-
|
|
3193
|
+
fs18.writeFileSync(path16.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
2977
3194
|
return {
|
|
2978
3195
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
2979
3196
|
retries: 0
|
|
@@ -2988,9 +3205,9 @@ var init_gate = __esm({
|
|
|
2988
3205
|
});
|
|
2989
3206
|
|
|
2990
3207
|
// src/stages/verify.ts
|
|
2991
|
-
import * as
|
|
2992
|
-
import * as
|
|
2993
|
-
import { execFileSync as
|
|
3208
|
+
import * as fs19 from "fs";
|
|
3209
|
+
import * as path17 from "path";
|
|
3210
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
2994
3211
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
2995
3212
|
const maxAttempts = def.maxRetries ?? 2;
|
|
2996
3213
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -3000,8 +3217,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
3000
3217
|
return { ...gateResult, retries: attempt };
|
|
3001
3218
|
}
|
|
3002
3219
|
if (attempt < maxAttempts) {
|
|
3003
|
-
const verifyPath =
|
|
3004
|
-
const errorOutput =
|
|
3220
|
+
const verifyPath = path17.join(ctx.taskDir, "verify.md");
|
|
3221
|
+
const errorOutput = fs19.existsSync(verifyPath) ? fs19.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
3005
3222
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
3006
3223
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
3007
3224
|
const diagConfig = getProjectConfig();
|
|
@@ -3044,7 +3261,7 @@ ${diagnosis.resolution}`);
|
|
|
3044
3261
|
const parts = parseCommand(cmd);
|
|
3045
3262
|
if (parts.length === 0) return;
|
|
3046
3263
|
try {
|
|
3047
|
-
|
|
3264
|
+
execFileSync13(parts[0], parts.slice(1), {
|
|
3048
3265
|
stdio: "pipe",
|
|
3049
3266
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
3050
3267
|
});
|
|
@@ -3097,8 +3314,8 @@ var init_verify = __esm({
|
|
|
3097
3314
|
});
|
|
3098
3315
|
|
|
3099
3316
|
// src/review-standalone.ts
|
|
3100
|
-
import * as
|
|
3101
|
-
import * as
|
|
3317
|
+
import * as fs20 from "fs";
|
|
3318
|
+
import * as path18 from "path";
|
|
3102
3319
|
function resolveReviewTarget(input) {
|
|
3103
3320
|
if (input.prs.length === 0) {
|
|
3104
3321
|
return {
|
|
@@ -3122,8 +3339,8 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
3122
3339
|
}
|
|
3123
3340
|
async function runStandaloneReview(input) {
|
|
3124
3341
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
3125
|
-
const taskDir =
|
|
3126
|
-
|
|
3342
|
+
const taskDir = path18.join(input.projectDir, ".kody", "tasks", taskId);
|
|
3343
|
+
fs20.mkdirSync(taskDir, { recursive: true });
|
|
3127
3344
|
let diffInstruction = "";
|
|
3128
3345
|
let filesChangedSection = "";
|
|
3129
3346
|
if (input.baseBranch) {
|
|
@@ -3150,7 +3367,7 @@ ${fileList}`;
|
|
|
3150
3367
|
const taskContent = `# ${input.prTitle}
|
|
3151
3368
|
|
|
3152
3369
|
${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
3153
|
-
|
|
3370
|
+
fs20.writeFileSync(path18.join(taskDir, "task.md"), taskContent);
|
|
3154
3371
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
3155
3372
|
const ctx = {
|
|
3156
3373
|
taskId,
|
|
@@ -3172,10 +3389,10 @@ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
|
3172
3389
|
error: result.error ?? "Review stage failed"
|
|
3173
3390
|
};
|
|
3174
3391
|
}
|
|
3175
|
-
const reviewPath =
|
|
3392
|
+
const reviewPath = path18.join(taskDir, "review.md");
|
|
3176
3393
|
let reviewContent;
|
|
3177
|
-
if (
|
|
3178
|
-
reviewContent =
|
|
3394
|
+
if (fs20.existsSync(reviewPath)) {
|
|
3395
|
+
reviewContent = fs20.readFileSync(reviewPath, "utf-8");
|
|
3179
3396
|
}
|
|
3180
3397
|
return {
|
|
3181
3398
|
outcome: "completed",
|
|
@@ -3215,8 +3432,8 @@ var init_review_standalone = __esm({
|
|
|
3215
3432
|
});
|
|
3216
3433
|
|
|
3217
3434
|
// src/stages/review.ts
|
|
3218
|
-
import * as
|
|
3219
|
-
import * as
|
|
3435
|
+
import * as fs21 from "fs";
|
|
3436
|
+
import * as path19 from "path";
|
|
3220
3437
|
async function executeReviewWithFix(ctx, def) {
|
|
3221
3438
|
if (ctx.input.dryRun) {
|
|
3222
3439
|
return { outcome: "completed", retries: 0 };
|
|
@@ -3230,11 +3447,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
3230
3447
|
if (reviewResult.outcome !== "completed") {
|
|
3231
3448
|
return reviewResult;
|
|
3232
3449
|
}
|
|
3233
|
-
const reviewFile =
|
|
3234
|
-
if (!
|
|
3450
|
+
const reviewFile = path19.join(ctx.taskDir, "review.md");
|
|
3451
|
+
if (!fs21.existsSync(reviewFile)) {
|
|
3235
3452
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
3236
3453
|
}
|
|
3237
|
-
const content =
|
|
3454
|
+
const content = fs21.readFileSync(reviewFile, "utf-8");
|
|
3238
3455
|
if (detectReviewVerdict(content) !== "fail") {
|
|
3239
3456
|
return { ...reviewResult, retries: iteration };
|
|
3240
3457
|
}
|
|
@@ -3263,15 +3480,15 @@ var init_review = __esm({
|
|
|
3263
3480
|
});
|
|
3264
3481
|
|
|
3265
3482
|
// src/stages/ship.ts
|
|
3266
|
-
import * as
|
|
3267
|
-
import * as
|
|
3268
|
-
import { execFileSync as
|
|
3483
|
+
import * as fs22 from "fs";
|
|
3484
|
+
import * as path20 from "path";
|
|
3485
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
3269
3486
|
function buildPrBody(ctx) {
|
|
3270
3487
|
const sections = [];
|
|
3271
|
-
const taskJsonPath =
|
|
3272
|
-
if (
|
|
3488
|
+
const taskJsonPath = path20.join(ctx.taskDir, "task.json");
|
|
3489
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
3273
3490
|
try {
|
|
3274
|
-
const raw =
|
|
3491
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
3275
3492
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3276
3493
|
const task = JSON.parse(cleaned);
|
|
3277
3494
|
if (task.description) {
|
|
@@ -3290,9 +3507,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
3290
3507
|
} catch {
|
|
3291
3508
|
}
|
|
3292
3509
|
}
|
|
3293
|
-
const reviewPath =
|
|
3294
|
-
if (
|
|
3295
|
-
const review =
|
|
3510
|
+
const reviewPath = path20.join(ctx.taskDir, "review.md");
|
|
3511
|
+
if (fs22.existsSync(reviewPath)) {
|
|
3512
|
+
const review = fs22.readFileSync(reviewPath, "utf-8");
|
|
3296
3513
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
3297
3514
|
if (summaryMatch) {
|
|
3298
3515
|
const summary = summaryMatch[1].trim();
|
|
@@ -3309,14 +3526,14 @@ ${summary}`);
|
|
|
3309
3526
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
3310
3527
|
}
|
|
3311
3528
|
}
|
|
3312
|
-
const verifyPath =
|
|
3313
|
-
if (
|
|
3314
|
-
const verify =
|
|
3529
|
+
const verifyPath = path20.join(ctx.taskDir, "verify.md");
|
|
3530
|
+
if (fs22.existsSync(verifyPath)) {
|
|
3531
|
+
const verify = fs22.readFileSync(verifyPath, "utf-8");
|
|
3315
3532
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
3316
3533
|
}
|
|
3317
|
-
const planPath =
|
|
3318
|
-
if (
|
|
3319
|
-
const plan =
|
|
3534
|
+
const planPath = path20.join(ctx.taskDir, "plan.md");
|
|
3535
|
+
if (fs22.existsSync(planPath)) {
|
|
3536
|
+
const plan = fs22.readFileSync(planPath, "utf-8").trim();
|
|
3320
3537
|
if (plan) {
|
|
3321
3538
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
3322
3539
|
sections.push(`
|
|
@@ -3336,25 +3553,25 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
3336
3553
|
return sections.join("\n");
|
|
3337
3554
|
}
|
|
3338
3555
|
function executeShipStage(ctx, _def) {
|
|
3339
|
-
const shipPath =
|
|
3556
|
+
const shipPath = path20.join(ctx.taskDir, "ship.md");
|
|
3340
3557
|
if (ctx.input.dryRun) {
|
|
3341
|
-
|
|
3558
|
+
fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
3342
3559
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3343
3560
|
}
|
|
3344
3561
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
3345
|
-
|
|
3562
|
+
fs22.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
3346
3563
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3347
3564
|
}
|
|
3348
3565
|
try {
|
|
3349
3566
|
const head = getCurrentBranch(ctx.projectDir);
|
|
3350
3567
|
const base = getDefaultBranch(ctx.projectDir);
|
|
3351
3568
|
try {
|
|
3352
|
-
|
|
3569
|
+
execFileSync14("git", ["add", ctx.taskDir], {
|
|
3353
3570
|
cwd: ctx.projectDir,
|
|
3354
3571
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
3355
3572
|
stdio: "pipe"
|
|
3356
3573
|
});
|
|
3357
|
-
|
|
3574
|
+
execFileSync14("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
3358
3575
|
cwd: ctx.projectDir,
|
|
3359
3576
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
3360
3577
|
stdio: "pipe"
|
|
@@ -3368,7 +3585,7 @@ function executeShipStage(ctx, _def) {
|
|
|
3368
3585
|
let repo = config.github?.repo;
|
|
3369
3586
|
if (!owner || !repo) {
|
|
3370
3587
|
try {
|
|
3371
|
-
const remoteUrl =
|
|
3588
|
+
const remoteUrl = execFileSync14("git", ["remote", "get-url", "origin"], {
|
|
3372
3589
|
encoding: "utf-8",
|
|
3373
3590
|
cwd: ctx.projectDir
|
|
3374
3591
|
}).trim();
|
|
@@ -3389,28 +3606,28 @@ function executeShipStage(ctx, _def) {
|
|
|
3389
3606
|
chore: "chore"
|
|
3390
3607
|
};
|
|
3391
3608
|
let prefix = "chore";
|
|
3392
|
-
const taskJsonPath =
|
|
3393
|
-
if (
|
|
3609
|
+
const taskJsonPath = path20.join(ctx.taskDir, "task.json");
|
|
3610
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
3394
3611
|
try {
|
|
3395
|
-
const raw =
|
|
3612
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
3396
3613
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3397
3614
|
const task = JSON.parse(cleaned);
|
|
3398
3615
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
3399
3616
|
} catch {
|
|
3400
3617
|
}
|
|
3401
3618
|
}
|
|
3402
|
-
const taskMdPath =
|
|
3403
|
-
if (
|
|
3404
|
-
const content =
|
|
3619
|
+
const taskMdPath = path20.join(ctx.taskDir, "task.md");
|
|
3620
|
+
if (fs22.existsSync(taskMdPath)) {
|
|
3621
|
+
const content = fs22.readFileSync(taskMdPath, "utf-8");
|
|
3405
3622
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
3406
3623
|
if (heading) {
|
|
3407
3624
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
3408
3625
|
}
|
|
3409
3626
|
}
|
|
3410
3627
|
if (title === "Update") {
|
|
3411
|
-
if (
|
|
3628
|
+
if (fs22.existsSync(taskJsonPath)) {
|
|
3412
3629
|
try {
|
|
3413
|
-
const raw =
|
|
3630
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
3414
3631
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3415
3632
|
const task = JSON.parse(cleaned);
|
|
3416
3633
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -3433,7 +3650,7 @@ function executeShipStage(ctx, _def) {
|
|
|
3433
3650
|
} catch {
|
|
3434
3651
|
}
|
|
3435
3652
|
}
|
|
3436
|
-
|
|
3653
|
+
fs22.writeFileSync(shipPath, `# Ship
|
|
3437
3654
|
|
|
3438
3655
|
Updated existing PR: ${existingPr.url}
|
|
3439
3656
|
PR #${existingPr.number}
|
|
@@ -3454,20 +3671,20 @@ PR #${existingPr.number}
|
|
|
3454
3671
|
} catch {
|
|
3455
3672
|
}
|
|
3456
3673
|
}
|
|
3457
|
-
|
|
3674
|
+
fs22.writeFileSync(shipPath, `# Ship
|
|
3458
3675
|
|
|
3459
3676
|
PR created: ${pr.url}
|
|
3460
3677
|
PR #${pr.number}
|
|
3461
3678
|
`);
|
|
3462
3679
|
} else {
|
|
3463
|
-
|
|
3680
|
+
fs22.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
3464
3681
|
}
|
|
3465
3682
|
}
|
|
3466
3683
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
3467
3684
|
} catch (err) {
|
|
3468
3685
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3469
3686
|
try {
|
|
3470
|
-
|
|
3687
|
+
fs22.writeFileSync(shipPath, `# Ship
|
|
3471
3688
|
|
|
3472
3689
|
Failed: ${msg}
|
|
3473
3690
|
`);
|
|
@@ -3516,15 +3733,15 @@ var init_executor_registry = __esm({
|
|
|
3516
3733
|
});
|
|
3517
3734
|
|
|
3518
3735
|
// src/pipeline/questions.ts
|
|
3519
|
-
import * as
|
|
3520
|
-
import * as
|
|
3736
|
+
import * as fs23 from "fs";
|
|
3737
|
+
import * as path21 from "path";
|
|
3521
3738
|
function checkForQuestions(ctx, stageName) {
|
|
3522
3739
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
3523
3740
|
try {
|
|
3524
3741
|
if (stageName === "taskify") {
|
|
3525
|
-
const taskJsonPath =
|
|
3526
|
-
if (!
|
|
3527
|
-
const raw =
|
|
3742
|
+
const taskJsonPath = path21.join(ctx.taskDir, "task.json");
|
|
3743
|
+
if (!fs23.existsSync(taskJsonPath)) return false;
|
|
3744
|
+
const raw = fs23.readFileSync(taskJsonPath, "utf-8");
|
|
3528
3745
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3529
3746
|
const taskJson = JSON.parse(cleaned);
|
|
3530
3747
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -3539,9 +3756,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
3539
3756
|
}
|
|
3540
3757
|
}
|
|
3541
3758
|
if (stageName === "plan") {
|
|
3542
|
-
const planPath =
|
|
3543
|
-
if (!
|
|
3544
|
-
const plan =
|
|
3759
|
+
const planPath = path21.join(ctx.taskDir, "plan.md");
|
|
3760
|
+
if (!fs23.existsSync(planPath)) return false;
|
|
3761
|
+
const plan = fs23.readFileSync(planPath, "utf-8");
|
|
3545
3762
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
3546
3763
|
if (questionsMatch) {
|
|
3547
3764
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -3570,8 +3787,8 @@ var init_questions = __esm({
|
|
|
3570
3787
|
});
|
|
3571
3788
|
|
|
3572
3789
|
// src/pipeline/hooks.ts
|
|
3573
|
-
import * as
|
|
3574
|
-
import * as
|
|
3790
|
+
import * as fs24 from "fs";
|
|
3791
|
+
import * as path22 from "path";
|
|
3575
3792
|
function applyPreStageLabel(ctx, def) {
|
|
3576
3793
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
3577
3794
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
@@ -3609,9 +3826,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
3609
3826
|
return { complexity, activeStages };
|
|
3610
3827
|
}
|
|
3611
3828
|
try {
|
|
3612
|
-
const taskJsonPath =
|
|
3613
|
-
if (!
|
|
3614
|
-
const raw =
|
|
3829
|
+
const taskJsonPath = path22.join(ctx.taskDir, "task.json");
|
|
3830
|
+
if (!fs24.existsSync(taskJsonPath)) return null;
|
|
3831
|
+
const raw = fs24.readFileSync(taskJsonPath, "utf-8");
|
|
3615
3832
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3616
3833
|
const taskJson = JSON.parse(cleaned);
|
|
3617
3834
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -3641,8 +3858,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
3641
3858
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
3642
3859
|
if (ctx.input.mode === "rerun") return null;
|
|
3643
3860
|
if (!ctx.input.issueNumber) return null;
|
|
3644
|
-
const planPath =
|
|
3645
|
-
const plan =
|
|
3861
|
+
const planPath = path22.join(ctx.taskDir, "plan.md");
|
|
3862
|
+
const plan = fs24.existsSync(planPath) ? fs24.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
3646
3863
|
try {
|
|
3647
3864
|
postComment(
|
|
3648
3865
|
ctx.input.issueNumber,
|
|
@@ -3709,22 +3926,22 @@ var init_hooks = __esm({
|
|
|
3709
3926
|
});
|
|
3710
3927
|
|
|
3711
3928
|
// src/learning/auto-learn.ts
|
|
3712
|
-
import * as
|
|
3713
|
-
import * as
|
|
3929
|
+
import * as fs25 from "fs";
|
|
3930
|
+
import * as path23 from "path";
|
|
3714
3931
|
function stripAnsi(str) {
|
|
3715
3932
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
3716
3933
|
}
|
|
3717
3934
|
function autoLearn(ctx) {
|
|
3718
3935
|
try {
|
|
3719
|
-
const memoryDir =
|
|
3720
|
-
if (!
|
|
3721
|
-
|
|
3936
|
+
const memoryDir = path23.join(ctx.projectDir, ".kody", "memory");
|
|
3937
|
+
if (!fs25.existsSync(memoryDir)) {
|
|
3938
|
+
fs25.mkdirSync(memoryDir, { recursive: true });
|
|
3722
3939
|
}
|
|
3723
3940
|
const learnings = [];
|
|
3724
3941
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3725
|
-
const verifyPath =
|
|
3726
|
-
if (
|
|
3727
|
-
const verify = stripAnsi(
|
|
3942
|
+
const verifyPath = path23.join(ctx.taskDir, "verify.md");
|
|
3943
|
+
if (fs25.existsSync(verifyPath)) {
|
|
3944
|
+
const verify = stripAnsi(fs25.readFileSync(verifyPath, "utf-8"));
|
|
3728
3945
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
3729
3946
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
3730
3947
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -3733,18 +3950,18 @@ function autoLearn(ctx) {
|
|
|
3733
3950
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
3734
3951
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
3735
3952
|
}
|
|
3736
|
-
const reviewPath =
|
|
3737
|
-
if (
|
|
3738
|
-
const review =
|
|
3953
|
+
const reviewPath = path23.join(ctx.taskDir, "review.md");
|
|
3954
|
+
if (fs25.existsSync(reviewPath)) {
|
|
3955
|
+
const review = fs25.readFileSync(reviewPath, "utf-8");
|
|
3739
3956
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
3740
3957
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
3741
3958
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
3742
3959
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
3743
3960
|
}
|
|
3744
|
-
const taskJsonPath =
|
|
3745
|
-
if (
|
|
3961
|
+
const taskJsonPath = path23.join(ctx.taskDir, "task.json");
|
|
3962
|
+
if (fs25.existsSync(taskJsonPath)) {
|
|
3746
3963
|
try {
|
|
3747
|
-
const raw = stripAnsi(
|
|
3964
|
+
const raw = stripAnsi(fs25.readFileSync(taskJsonPath, "utf-8"));
|
|
3748
3965
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3749
3966
|
const task = JSON.parse(cleaned);
|
|
3750
3967
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -3755,12 +3972,12 @@ function autoLearn(ctx) {
|
|
|
3755
3972
|
}
|
|
3756
3973
|
}
|
|
3757
3974
|
if (learnings.length > 0) {
|
|
3758
|
-
const conventionsPath =
|
|
3975
|
+
const conventionsPath = path23.join(memoryDir, "conventions.md");
|
|
3759
3976
|
const entry = `
|
|
3760
3977
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
3761
3978
|
${learnings.join("\n")}
|
|
3762
3979
|
`;
|
|
3763
|
-
|
|
3980
|
+
fs25.appendFileSync(conventionsPath, entry);
|
|
3764
3981
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
3765
3982
|
}
|
|
3766
3983
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
@@ -3768,8 +3985,8 @@ ${learnings.join("\n")}
|
|
|
3768
3985
|
}
|
|
3769
3986
|
}
|
|
3770
3987
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
3771
|
-
const archPath =
|
|
3772
|
-
if (
|
|
3988
|
+
const archPath = path23.join(memoryDir, "architecture.md");
|
|
3989
|
+
if (fs25.existsSync(archPath)) return;
|
|
3773
3990
|
const detected = detectArchitectureBasic(projectDir);
|
|
3774
3991
|
if (detected.length > 0) {
|
|
3775
3992
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
@@ -3777,7 +3994,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
3777
3994
|
## Overview
|
|
3778
3995
|
${detected.join("\n")}
|
|
3779
3996
|
`;
|
|
3780
|
-
|
|
3997
|
+
fs25.writeFileSync(archPath, content);
|
|
3781
3998
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
3782
3999
|
}
|
|
3783
4000
|
}
|
|
@@ -3790,13 +4007,13 @@ var init_auto_learn = __esm({
|
|
|
3790
4007
|
});
|
|
3791
4008
|
|
|
3792
4009
|
// src/retrospective.ts
|
|
3793
|
-
import * as
|
|
3794
|
-
import * as
|
|
4010
|
+
import * as fs26 from "fs";
|
|
4011
|
+
import * as path24 from "path";
|
|
3795
4012
|
function readArtifact(taskDir, filename, maxChars) {
|
|
3796
|
-
const p =
|
|
3797
|
-
if (!
|
|
4013
|
+
const p = path24.join(taskDir, filename);
|
|
4014
|
+
if (!fs26.existsSync(p)) return null;
|
|
3798
4015
|
try {
|
|
3799
|
-
const content =
|
|
4016
|
+
const content = fs26.readFileSync(p, "utf-8");
|
|
3800
4017
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
3801
4018
|
} catch {
|
|
3802
4019
|
return null;
|
|
@@ -3849,13 +4066,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
3849
4066
|
return lines.join("\n");
|
|
3850
4067
|
}
|
|
3851
4068
|
function getLogPath(projectDir) {
|
|
3852
|
-
return
|
|
4069
|
+
return path24.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
3853
4070
|
}
|
|
3854
4071
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
3855
4072
|
const logPath = getLogPath(projectDir);
|
|
3856
|
-
if (!
|
|
4073
|
+
if (!fs26.existsSync(logPath)) return [];
|
|
3857
4074
|
try {
|
|
3858
|
-
const content =
|
|
4075
|
+
const content = fs26.readFileSync(logPath, "utf-8");
|
|
3859
4076
|
const lines = content.split("\n").filter(Boolean);
|
|
3860
4077
|
const entries = [];
|
|
3861
4078
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -3882,11 +4099,11 @@ function formatPreviousEntries(entries) {
|
|
|
3882
4099
|
}
|
|
3883
4100
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
3884
4101
|
const logPath = getLogPath(projectDir);
|
|
3885
|
-
const dir =
|
|
3886
|
-
if (!
|
|
3887
|
-
|
|
4102
|
+
const dir = path24.dirname(logPath);
|
|
4103
|
+
if (!fs26.existsSync(dir)) {
|
|
4104
|
+
fs26.mkdirSync(dir, { recursive: true });
|
|
3888
4105
|
}
|
|
3889
|
-
|
|
4106
|
+
fs26.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
3890
4107
|
}
|
|
3891
4108
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
3892
4109
|
if (ctx.input.dryRun) return;
|
|
@@ -4054,8 +4271,8 @@ var init_summary = __esm({
|
|
|
4054
4271
|
});
|
|
4055
4272
|
|
|
4056
4273
|
// src/pipeline.ts
|
|
4057
|
-
import * as
|
|
4058
|
-
import * as
|
|
4274
|
+
import * as fs27 from "fs";
|
|
4275
|
+
import * as path25 from "path";
|
|
4059
4276
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
4060
4277
|
if (ctx.input.dryRun) return;
|
|
4061
4278
|
if (ctx.input.prNumber) {
|
|
@@ -4068,8 +4285,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
4068
4285
|
}
|
|
4069
4286
|
if (!ctx.input.issueNumber) return;
|
|
4070
4287
|
try {
|
|
4071
|
-
const taskMdPath =
|
|
4072
|
-
const title =
|
|
4288
|
+
const taskMdPath = path25.join(ctx.taskDir, "task.md");
|
|
4289
|
+
const title = fs27.existsSync(taskMdPath) ? fs27.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
4073
4290
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
4074
4291
|
syncWithDefault(ctx.projectDir);
|
|
4075
4292
|
} catch (err) {
|
|
@@ -4083,10 +4300,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
4083
4300
|
}
|
|
4084
4301
|
}
|
|
4085
4302
|
function acquireLock(taskDir) {
|
|
4086
|
-
const lockPath =
|
|
4087
|
-
if (
|
|
4303
|
+
const lockPath = path25.join(taskDir, ".lock");
|
|
4304
|
+
if (fs27.existsSync(lockPath)) {
|
|
4088
4305
|
try {
|
|
4089
|
-
const pid = parseInt(
|
|
4306
|
+
const pid = parseInt(fs27.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
4090
4307
|
if (!isNaN(pid)) {
|
|
4091
4308
|
try {
|
|
4092
4309
|
process.kill(pid, 0);
|
|
@@ -4103,14 +4320,14 @@ function acquireLock(taskDir) {
|
|
|
4103
4320
|
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
4104
4321
|
}
|
|
4105
4322
|
try {
|
|
4106
|
-
|
|
4323
|
+
fs27.unlinkSync(lockPath);
|
|
4107
4324
|
} catch {
|
|
4108
4325
|
}
|
|
4109
4326
|
}
|
|
4110
4327
|
try {
|
|
4111
|
-
const fd =
|
|
4112
|
-
|
|
4113
|
-
|
|
4328
|
+
const fd = fs27.openSync(lockPath, fs27.constants.O_WRONLY | fs27.constants.O_CREAT | fs27.constants.O_EXCL);
|
|
4329
|
+
fs27.writeSync(fd, String(process.pid));
|
|
4330
|
+
fs27.closeSync(fd);
|
|
4114
4331
|
} catch (err) {
|
|
4115
4332
|
if (err.code === "EEXIST") {
|
|
4116
4333
|
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
@@ -4120,7 +4337,7 @@ function acquireLock(taskDir) {
|
|
|
4120
4337
|
}
|
|
4121
4338
|
function releaseLock(taskDir) {
|
|
4122
4339
|
try {
|
|
4123
|
-
|
|
4340
|
+
fs27.unlinkSync(path25.join(taskDir, ".lock"));
|
|
4124
4341
|
} catch {
|
|
4125
4342
|
}
|
|
4126
4343
|
}
|
|
@@ -4328,8 +4545,8 @@ var init_pipeline = __esm({
|
|
|
4328
4545
|
});
|
|
4329
4546
|
|
|
4330
4547
|
// src/preflight.ts
|
|
4331
|
-
import { execFileSync as
|
|
4332
|
-
import * as
|
|
4548
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
4549
|
+
import * as fs28 from "fs";
|
|
4333
4550
|
function check(name, fn) {
|
|
4334
4551
|
try {
|
|
4335
4552
|
const detail = fn() ?? void 0;
|
|
@@ -4341,7 +4558,7 @@ function check(name, fn) {
|
|
|
4341
4558
|
function runPreflight() {
|
|
4342
4559
|
const checks = [
|
|
4343
4560
|
check("claude CLI", () => {
|
|
4344
|
-
const v =
|
|
4561
|
+
const v = execFileSync15("claude", ["--version"], {
|
|
4345
4562
|
encoding: "utf-8",
|
|
4346
4563
|
timeout: 1e4,
|
|
4347
4564
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4349,14 +4566,14 @@ function runPreflight() {
|
|
|
4349
4566
|
return v;
|
|
4350
4567
|
}),
|
|
4351
4568
|
check("git repo", () => {
|
|
4352
|
-
|
|
4569
|
+
execFileSync15("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
4353
4570
|
encoding: "utf-8",
|
|
4354
4571
|
timeout: 5e3,
|
|
4355
4572
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4356
4573
|
});
|
|
4357
4574
|
}),
|
|
4358
4575
|
check("pnpm", () => {
|
|
4359
|
-
const v =
|
|
4576
|
+
const v = execFileSync15("pnpm", ["--version"], {
|
|
4360
4577
|
encoding: "utf-8",
|
|
4361
4578
|
timeout: 5e3,
|
|
4362
4579
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4364,7 +4581,7 @@ function runPreflight() {
|
|
|
4364
4581
|
return v;
|
|
4365
4582
|
}),
|
|
4366
4583
|
check("node >= 18", () => {
|
|
4367
|
-
const v =
|
|
4584
|
+
const v = execFileSync15("node", ["--version"], {
|
|
4368
4585
|
encoding: "utf-8",
|
|
4369
4586
|
timeout: 5e3,
|
|
4370
4587
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4374,7 +4591,7 @@ function runPreflight() {
|
|
|
4374
4591
|
return v;
|
|
4375
4592
|
}),
|
|
4376
4593
|
check("gh CLI", () => {
|
|
4377
|
-
const v =
|
|
4594
|
+
const v = execFileSync15("gh", ["--version"], {
|
|
4378
4595
|
encoding: "utf-8",
|
|
4379
4596
|
timeout: 5e3,
|
|
4380
4597
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4382,7 +4599,7 @@ function runPreflight() {
|
|
|
4382
4599
|
return v;
|
|
4383
4600
|
}),
|
|
4384
4601
|
check("package.json", () => {
|
|
4385
|
-
if (!
|
|
4602
|
+
if (!fs28.existsSync("package.json")) throw new Error("not found");
|
|
4386
4603
|
})
|
|
4387
4604
|
];
|
|
4388
4605
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -4458,180 +4675,6 @@ var init_args = __esm({
|
|
|
4458
4675
|
}
|
|
4459
4676
|
});
|
|
4460
4677
|
|
|
4461
|
-
// src/cli/litellm.ts
|
|
4462
|
-
import * as fs28 from "fs";
|
|
4463
|
-
import * as os from "os";
|
|
4464
|
-
import * as path25 from "path";
|
|
4465
|
-
import { execFileSync as execFileSync15 } from "child_process";
|
|
4466
|
-
async function checkLitellmHealth(url) {
|
|
4467
|
-
try {
|
|
4468
|
-
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
4469
|
-
return response.ok;
|
|
4470
|
-
} catch {
|
|
4471
|
-
return false;
|
|
4472
|
-
}
|
|
4473
|
-
}
|
|
4474
|
-
async function checkModelHealth(baseUrl, apiKey, model) {
|
|
4475
|
-
try {
|
|
4476
|
-
const res = await fetch(`${baseUrl}/v1/messages`, {
|
|
4477
|
-
method: "POST",
|
|
4478
|
-
headers: {
|
|
4479
|
-
"Content-Type": "application/json",
|
|
4480
|
-
"x-api-key": apiKey,
|
|
4481
|
-
"anthropic-version": "2023-06-01"
|
|
4482
|
-
},
|
|
4483
|
-
body: JSON.stringify({
|
|
4484
|
-
model,
|
|
4485
|
-
max_tokens: 4,
|
|
4486
|
-
messages: [{ role: "user", content: "Reply with: ok" }]
|
|
4487
|
-
}),
|
|
4488
|
-
signal: AbortSignal.timeout(3e4)
|
|
4489
|
-
});
|
|
4490
|
-
if (!res.ok) {
|
|
4491
|
-
const body2 = await res.text().catch(() => "");
|
|
4492
|
-
return { ok: false, error: `HTTP ${res.status}: ${body2.slice(0, 200)}` };
|
|
4493
|
-
}
|
|
4494
|
-
const body = await res.json();
|
|
4495
|
-
const hasAnthropicContent = Array.isArray(body.content) && body.content.some((b) => b.type === "text");
|
|
4496
|
-
const hasThinkingContent = Array.isArray(body.content) && body.content.some((b) => b.type === "thinking");
|
|
4497
|
-
const hasOpenAIContent = !!body.choices?.[0]?.message?.content;
|
|
4498
|
-
if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent) {
|
|
4499
|
-
return { ok: false, error: `Unexpected response format: ${JSON.stringify(body).slice(0, 200)}` };
|
|
4500
|
-
}
|
|
4501
|
-
return { ok: true };
|
|
4502
|
-
} catch (err) {
|
|
4503
|
-
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
4504
|
-
}
|
|
4505
|
-
}
|
|
4506
|
-
function generateLitellmConfig(provider, modelMap) {
|
|
4507
|
-
const apiKeyVar = providerApiKeyEnvVar(provider);
|
|
4508
|
-
const entries = ["model_list:"];
|
|
4509
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4510
|
-
for (const providerModel of Object.values(modelMap)) {
|
|
4511
|
-
if (seen.has(providerModel)) continue;
|
|
4512
|
-
seen.add(providerModel);
|
|
4513
|
-
entries.push(` - model_name: ${providerModel}`);
|
|
4514
|
-
entries.push(` litellm_params:`);
|
|
4515
|
-
entries.push(` model: ${provider}/${providerModel}`);
|
|
4516
|
-
entries.push(` api_key: os.environ/${apiKeyVar}`);
|
|
4517
|
-
}
|
|
4518
|
-
return entries.join("\n") + "\n";
|
|
4519
|
-
}
|
|
4520
|
-
function generateLitellmConfigFromStages(defaultConfig, stages) {
|
|
4521
|
-
const proxyModels = [];
|
|
4522
|
-
if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
|
|
4523
|
-
proxyModels.push(defaultConfig);
|
|
4524
|
-
}
|
|
4525
|
-
if (stages) {
|
|
4526
|
-
for (const sc of Object.values(stages)) {
|
|
4527
|
-
if (sc.provider !== "claude" && sc.provider !== "anthropic") {
|
|
4528
|
-
proxyModels.push(sc);
|
|
4529
|
-
}
|
|
4530
|
-
}
|
|
4531
|
-
}
|
|
4532
|
-
if (proxyModels.length === 0) return void 0;
|
|
4533
|
-
const entries = ["model_list:"];
|
|
4534
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4535
|
-
for (const { provider, model } of proxyModels) {
|
|
4536
|
-
const key = `${provider}/${model}`;
|
|
4537
|
-
if (seen.has(key)) continue;
|
|
4538
|
-
seen.add(key);
|
|
4539
|
-
const apiKeyVar = providerApiKeyEnvVar(provider);
|
|
4540
|
-
entries.push(` - model_name: ${model}`);
|
|
4541
|
-
entries.push(` litellm_params:`);
|
|
4542
|
-
entries.push(` model: ${provider}/${model}`);
|
|
4543
|
-
entries.push(` api_key: os.environ/${apiKeyVar}`);
|
|
4544
|
-
}
|
|
4545
|
-
return entries.join("\n") + "\n";
|
|
4546
|
-
}
|
|
4547
|
-
async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
4548
|
-
if (!generatedConfig) {
|
|
4549
|
-
logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
|
|
4550
|
-
return null;
|
|
4551
|
-
}
|
|
4552
|
-
const configPath = path25.join(os.tmpdir(), "kody-litellm-config.yaml");
|
|
4553
|
-
fs28.writeFileSync(configPath, generatedConfig);
|
|
4554
|
-
const portMatch = url.match(/:(\d+)/);
|
|
4555
|
-
const port = portMatch ? portMatch[1] : "4000";
|
|
4556
|
-
let litellmFound = false;
|
|
4557
|
-
try {
|
|
4558
|
-
execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
4559
|
-
litellmFound = true;
|
|
4560
|
-
} catch {
|
|
4561
|
-
try {
|
|
4562
|
-
execFileSync15("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
4563
|
-
litellmFound = true;
|
|
4564
|
-
} catch {
|
|
4565
|
-
}
|
|
4566
|
-
}
|
|
4567
|
-
if (!litellmFound) {
|
|
4568
|
-
logger.warn("litellm not installed (pip install 'litellm[proxy]')");
|
|
4569
|
-
return null;
|
|
4570
|
-
}
|
|
4571
|
-
logger.info(`Starting LiteLLM proxy on port ${port}...`);
|
|
4572
|
-
let cmd;
|
|
4573
|
-
let args2;
|
|
4574
|
-
try {
|
|
4575
|
-
execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
4576
|
-
cmd = "litellm";
|
|
4577
|
-
args2 = ["--config", configPath, "--port", port];
|
|
4578
|
-
} catch {
|
|
4579
|
-
cmd = "python3";
|
|
4580
|
-
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
4581
|
-
}
|
|
4582
|
-
const dotenvPath = path25.join(projectDir, ".env");
|
|
4583
|
-
const dotenvVars = {};
|
|
4584
|
-
if (fs28.existsSync(dotenvPath)) {
|
|
4585
|
-
for (const rawLine of fs28.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
4586
|
-
const line = rawLine.trim();
|
|
4587
|
-
if (!line || line.startsWith("#")) continue;
|
|
4588
|
-
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
4589
|
-
if (match) {
|
|
4590
|
-
let value = match[2].trim();
|
|
4591
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
4592
|
-
value = value.slice(1, -1);
|
|
4593
|
-
}
|
|
4594
|
-
const commentIdx = value.indexOf(" #");
|
|
4595
|
-
if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
|
|
4596
|
-
if (value) dotenvVars[match[1]] = value;
|
|
4597
|
-
}
|
|
4598
|
-
}
|
|
4599
|
-
if (Object.keys(dotenvVars).length > 0) {
|
|
4600
|
-
logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
|
|
4601
|
-
}
|
|
4602
|
-
}
|
|
4603
|
-
const { spawn: spawn2 } = await import("child_process");
|
|
4604
|
-
const child = spawn2(cmd, args2, {
|
|
4605
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
4606
|
-
detached: true,
|
|
4607
|
-
env: { ...process.env, ...dotenvVars }
|
|
4608
|
-
});
|
|
4609
|
-
let proxyStderr = "";
|
|
4610
|
-
child.stderr?.on("data", (chunk) => {
|
|
4611
|
-
proxyStderr += chunk.toString();
|
|
4612
|
-
});
|
|
4613
|
-
for (let i = 0; i < 30; i++) {
|
|
4614
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
4615
|
-
if (await checkLitellmHealth(url)) {
|
|
4616
|
-
logger.info(`LiteLLM proxy ready at ${url}`);
|
|
4617
|
-
return child;
|
|
4618
|
-
}
|
|
4619
|
-
}
|
|
4620
|
-
if (proxyStderr) {
|
|
4621
|
-
logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1e3)}`);
|
|
4622
|
-
}
|
|
4623
|
-
logger.warn("LiteLLM proxy failed to start within 60s");
|
|
4624
|
-
child.kill();
|
|
4625
|
-
return null;
|
|
4626
|
-
}
|
|
4627
|
-
var init_litellm = __esm({
|
|
4628
|
-
"src/cli/litellm.ts"() {
|
|
4629
|
-
"use strict";
|
|
4630
|
-
init_logger();
|
|
4631
|
-
init_config();
|
|
4632
|
-
}
|
|
4633
|
-
});
|
|
4634
|
-
|
|
4635
4678
|
// src/cli/task-state.ts
|
|
4636
4679
|
import * as fs29 from "fs";
|
|
4637
4680
|
import * as path26 from "path";
|