@kody-ade/kody-engine-lite 0.1.57 → 0.1.59
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 +692 -295
- package/kody.config.schema.json +6 -1
- package/package.json +1 -1
package/dist/bin/cli.js
CHANGED
|
@@ -252,6 +252,17 @@ var init_logger = __esm({
|
|
|
252
252
|
// src/config.ts
|
|
253
253
|
import * as fs from "fs";
|
|
254
254
|
import * as path from "path";
|
|
255
|
+
function needsLitellmProxy(config) {
|
|
256
|
+
if (config.agent.litellmUrl) return true;
|
|
257
|
+
if (config.agent.provider && config.agent.provider !== "anthropic") return true;
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
function getLitellmUrl(config) {
|
|
261
|
+
return config.agent.litellmUrl ?? LITELLM_DEFAULT_URL;
|
|
262
|
+
}
|
|
263
|
+
function providerApiKeyEnvVar(provider) {
|
|
264
|
+
return `${provider.toUpperCase()}_API_KEY`;
|
|
265
|
+
}
|
|
255
266
|
function setConfigDir(dir) {
|
|
256
267
|
_configDir = dir;
|
|
257
268
|
_config = null;
|
|
@@ -267,7 +278,8 @@ function getProjectConfig() {
|
|
|
267
278
|
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
268
279
|
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
269
280
|
paths: { ...DEFAULT_CONFIG.paths, ...raw.paths },
|
|
270
|
-
agent: { ...DEFAULT_CONFIG.agent, ...raw.agent }
|
|
281
|
+
agent: { ...DEFAULT_CONFIG.agent, ...raw.agent },
|
|
282
|
+
contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers
|
|
271
283
|
};
|
|
272
284
|
} catch {
|
|
273
285
|
logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
|
|
@@ -278,7 +290,7 @@ function getProjectConfig() {
|
|
|
278
290
|
}
|
|
279
291
|
return _config;
|
|
280
292
|
}
|
|
281
|
-
var DEFAULT_CONFIG, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
|
|
293
|
+
var DEFAULT_CONFIG, LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, TIER_TO_ANTHROPIC_IDS, VERIFY_COMMAND_TIMEOUT_MS, FIX_COMMAND_TIMEOUT_MS, _config, _configDir;
|
|
282
294
|
var init_config = __esm({
|
|
283
295
|
"src/config.ts"() {
|
|
284
296
|
"use strict";
|
|
@@ -306,8 +318,19 @@ var init_config = __esm({
|
|
|
306
318
|
runner: "claude-code",
|
|
307
319
|
defaultRunner: "claude",
|
|
308
320
|
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
|
|
321
|
+
},
|
|
322
|
+
contextTiers: {
|
|
323
|
+
enabled: true,
|
|
324
|
+
tokenBudget: 8e3
|
|
309
325
|
}
|
|
310
326
|
};
|
|
327
|
+
LITELLM_DEFAULT_PORT = 4e3;
|
|
328
|
+
LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
|
|
329
|
+
TIER_TO_ANTHROPIC_IDS = {
|
|
330
|
+
cheap: ["claude-haiku-4-5-20251001", "claude-haiku-4-5", "haiku"],
|
|
331
|
+
mid: ["claude-sonnet-4-6-20250514", "claude-sonnet-4-6", "sonnet"],
|
|
332
|
+
strong: ["claude-opus-4-6-20250514", "claude-opus-4-6", "opus"]
|
|
333
|
+
};
|
|
311
334
|
VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
312
335
|
FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
313
336
|
_config = null;
|
|
@@ -775,25 +798,342 @@ var init_memory = __esm({
|
|
|
775
798
|
}
|
|
776
799
|
});
|
|
777
800
|
|
|
778
|
-
// src/context.ts
|
|
801
|
+
// src/context-tiers.ts
|
|
779
802
|
import * as fs4 from "fs";
|
|
780
803
|
import * as path4 from "path";
|
|
804
|
+
import * as crypto2 from "crypto";
|
|
805
|
+
function estimateTokens(text) {
|
|
806
|
+
return Math.ceil(text.length / 4);
|
|
807
|
+
}
|
|
808
|
+
function contentHash(content) {
|
|
809
|
+
return crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
810
|
+
}
|
|
811
|
+
function resolveStagePolicy(stageName, stageOverrides) {
|
|
812
|
+
const defaults = DEFAULT_STAGE_POLICIES[stageName] ?? DEFAULT_STAGE_POLICIES.build;
|
|
813
|
+
const overrides = stageOverrides?.[stageName];
|
|
814
|
+
return overrides ? { ...defaults, ...overrides } : { ...defaults };
|
|
815
|
+
}
|
|
816
|
+
function generateL0(content, filename) {
|
|
817
|
+
if (!content.trim()) return "";
|
|
818
|
+
if (filename.endsWith(".json")) {
|
|
819
|
+
return generateL0Json(content);
|
|
820
|
+
}
|
|
821
|
+
const lines = content.split("\n");
|
|
822
|
+
const parts = [];
|
|
823
|
+
const headingLine = lines.find((l) => l.startsWith("#"));
|
|
824
|
+
if (headingLine) parts.push(headingLine);
|
|
825
|
+
for (const line of lines) {
|
|
826
|
+
if (!line.trim() || line.startsWith("#")) continue;
|
|
827
|
+
if (line.startsWith("-") || line.startsWith("*")) continue;
|
|
828
|
+
const sentence = line.split(/\.\s/)[0];
|
|
829
|
+
if (sentence && sentence.length > 10) {
|
|
830
|
+
parts.push(sentence.endsWith(".") ? sentence : sentence + ".");
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const result = parts.join("\n");
|
|
835
|
+
return result.slice(0, L0_MAX_CHARS);
|
|
836
|
+
}
|
|
837
|
+
function generateL0Json(content) {
|
|
838
|
+
try {
|
|
839
|
+
const cleaned = content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
840
|
+
const obj = JSON.parse(cleaned);
|
|
841
|
+
const fields = [];
|
|
842
|
+
if (obj.title) fields.push(`Title: ${obj.title}`);
|
|
843
|
+
if (obj.task_type) fields.push(`Type: ${obj.task_type}`);
|
|
844
|
+
if (obj.risk_level) fields.push(`Risk: ${obj.risk_level}`);
|
|
845
|
+
if (obj.estimated_complexity) fields.push(`Complexity: ${obj.estimated_complexity}`);
|
|
846
|
+
return fields.join(" | ") || content.slice(0, L0_MAX_CHARS);
|
|
847
|
+
} catch {
|
|
848
|
+
return content.slice(0, L0_MAX_CHARS);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function generateL1(content, filename) {
|
|
852
|
+
if (!content.trim()) return "";
|
|
853
|
+
if (filename.endsWith(".json")) {
|
|
854
|
+
return generateL1Json(content);
|
|
855
|
+
}
|
|
856
|
+
const lines = content.split("\n");
|
|
857
|
+
const parts = [];
|
|
858
|
+
let inSection = false;
|
|
859
|
+
for (const line of lines) {
|
|
860
|
+
if (line.startsWith("#")) {
|
|
861
|
+
parts.push(line);
|
|
862
|
+
inSection = true;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (line.match(/^\s*[-*]\s/)) {
|
|
866
|
+
const recentBullets = parts.slice(-5).filter((p) => p.match(/^\s*[-*]\s/)).length;
|
|
867
|
+
if (recentBullets < 5) {
|
|
868
|
+
parts.push(line);
|
|
869
|
+
}
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
if (inSection && line.trim()) {
|
|
873
|
+
parts.push(line);
|
|
874
|
+
inSection = false;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const result = parts.join("\n");
|
|
878
|
+
return result.slice(0, L1_MAX_CHARS);
|
|
879
|
+
}
|
|
880
|
+
function generateL1Json(content) {
|
|
881
|
+
try {
|
|
882
|
+
const cleaned = content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
883
|
+
const obj = JSON.parse(cleaned);
|
|
884
|
+
const lines = [];
|
|
885
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
886
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
887
|
+
lines.push(`- ${key}: ${value}`);
|
|
888
|
+
} else if (Array.isArray(value)) {
|
|
889
|
+
lines.push(`- ${key}: [${value.length} items] ${value.slice(0, 3).join(", ")}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return lines.join("\n").slice(0, L1_MAX_CHARS);
|
|
893
|
+
} catch {
|
|
894
|
+
return content.slice(0, L1_MAX_CHARS);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function readCache(cacheDir) {
|
|
898
|
+
const cachePath = path4.join(cacheDir, "tier-cache.json");
|
|
899
|
+
if (!fs4.existsSync(cachePath)) return { version: 1, entries: {} };
|
|
900
|
+
try {
|
|
901
|
+
return JSON.parse(fs4.readFileSync(cachePath, "utf-8"));
|
|
902
|
+
} catch {
|
|
903
|
+
return { version: 1, entries: {} };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function writeCache(cacheDir, cache) {
|
|
907
|
+
fs4.mkdirSync(cacheDir, { recursive: true });
|
|
908
|
+
fs4.writeFileSync(path4.join(cacheDir, "tier-cache.json"), JSON.stringify(cache, null, 2));
|
|
909
|
+
}
|
|
910
|
+
function getTieredContent(filePath, content, cacheDir) {
|
|
911
|
+
const hash = contentHash(content);
|
|
912
|
+
const key = path4.basename(filePath);
|
|
913
|
+
const cache = readCache(cacheDir);
|
|
914
|
+
if (cache.entries[key] && cache.entries[key].hash === hash) {
|
|
915
|
+
return cache.entries[key];
|
|
916
|
+
}
|
|
917
|
+
const tiered = {
|
|
918
|
+
source: filePath,
|
|
919
|
+
hash,
|
|
920
|
+
L0: generateL0(content, key),
|
|
921
|
+
L1: generateL1(content, key),
|
|
922
|
+
L2: content
|
|
923
|
+
};
|
|
924
|
+
cache.entries[key] = tiered;
|
|
925
|
+
writeCache(cacheDir, cache);
|
|
926
|
+
return tiered;
|
|
927
|
+
}
|
|
928
|
+
function invalidateCache(filePath, cacheDir) {
|
|
929
|
+
const key = path4.basename(filePath);
|
|
930
|
+
const cache = readCache(cacheDir);
|
|
931
|
+
if (cache.entries[key]) {
|
|
932
|
+
delete cache.entries[key];
|
|
933
|
+
writeCache(cacheDir, cache);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function selectTier(tiered, tier) {
|
|
937
|
+
return tiered[tier];
|
|
938
|
+
}
|
|
939
|
+
function readProjectMemoryTiered(projectDir, tier) {
|
|
940
|
+
const memoryDir = path4.join(projectDir, ".kody", "memory");
|
|
941
|
+
if (!fs4.existsSync(memoryDir)) return "";
|
|
942
|
+
const files = fs4.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
943
|
+
if (files.length === 0) return "";
|
|
944
|
+
const cacheDir = path4.join(memoryDir, ".tiers");
|
|
945
|
+
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
946
|
+
const sections = [];
|
|
947
|
+
for (const file of files) {
|
|
948
|
+
const filePath = path4.join(memoryDir, file);
|
|
949
|
+
const content = fs4.readFileSync(filePath, "utf-8").trim();
|
|
950
|
+
if (!content) continue;
|
|
951
|
+
const tiered = getTieredContent(filePath, content, cacheDir);
|
|
952
|
+
const selected = selectTier(tiered, tier);
|
|
953
|
+
if (selected) {
|
|
954
|
+
sections.push(`## ${file.replace(".md", "")}
|
|
955
|
+
${selected}`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (sections.length === 0) return "";
|
|
959
|
+
const header = tier === "L2" ? "# Project Memory\n" : `# Project Memory (${tierLabel2} \u2014 use Read tool for full details)
|
|
960
|
+
`;
|
|
961
|
+
return `${header}
|
|
962
|
+
${sections.join("\n\n")}
|
|
963
|
+
`;
|
|
964
|
+
}
|
|
965
|
+
function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
966
|
+
const cacheDir = path4.join(taskDir, ".tiers");
|
|
967
|
+
let context = `## Task Context
|
|
968
|
+
`;
|
|
969
|
+
context += `Task ID: ${taskId}
|
|
970
|
+
`;
|
|
971
|
+
context += `Task Directory: ${taskDir}
|
|
972
|
+
`;
|
|
973
|
+
const taskMdPath = path4.join(taskDir, "task.md");
|
|
974
|
+
if (fs4.existsSync(taskMdPath)) {
|
|
975
|
+
const content = fs4.readFileSync(taskMdPath, "utf-8");
|
|
976
|
+
const selected = selectContent(taskMdPath, content, cacheDir, policy.taskDescription);
|
|
977
|
+
const label = tierLabel("Task Description", policy.taskDescription);
|
|
978
|
+
context += `
|
|
979
|
+
## ${label}
|
|
980
|
+
${selected}
|
|
981
|
+
`;
|
|
982
|
+
}
|
|
983
|
+
const taskJsonPath = path4.join(taskDir, "task.json");
|
|
984
|
+
if (fs4.existsSync(taskJsonPath)) {
|
|
985
|
+
const content = fs4.readFileSync(taskJsonPath, "utf-8");
|
|
986
|
+
if (policy.taskClassification === "L2") {
|
|
987
|
+
try {
|
|
988
|
+
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
989
|
+
context += `
|
|
990
|
+
## Task Classification
|
|
991
|
+
`;
|
|
992
|
+
context += `Type: ${taskDef.task_type ?? "unknown"}
|
|
993
|
+
`;
|
|
994
|
+
context += `Title: ${taskDef.title ?? "unknown"}
|
|
995
|
+
`;
|
|
996
|
+
context += `Risk: ${taskDef.risk_level ?? "unknown"}
|
|
997
|
+
`;
|
|
998
|
+
} catch {
|
|
999
|
+
}
|
|
1000
|
+
} else {
|
|
1001
|
+
const selected = selectContent(taskJsonPath, content, cacheDir, policy.taskClassification);
|
|
1002
|
+
if (selected) {
|
|
1003
|
+
const label = tierLabel("Task Classification", policy.taskClassification);
|
|
1004
|
+
context += `
|
|
1005
|
+
## ${label}
|
|
1006
|
+
${selected}
|
|
1007
|
+
`;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const specPath = path4.join(taskDir, "spec.md");
|
|
1012
|
+
if (fs4.existsSync(specPath)) {
|
|
1013
|
+
const content = fs4.readFileSync(specPath, "utf-8");
|
|
1014
|
+
const selected = selectContent(specPath, content, cacheDir, policy.spec);
|
|
1015
|
+
const label = tierLabel("Spec", policy.spec);
|
|
1016
|
+
context += `
|
|
1017
|
+
## ${label}
|
|
1018
|
+
${selected}
|
|
1019
|
+
`;
|
|
1020
|
+
}
|
|
1021
|
+
const planPath = path4.join(taskDir, "plan.md");
|
|
1022
|
+
if (fs4.existsSync(planPath)) {
|
|
1023
|
+
const content = fs4.readFileSync(planPath, "utf-8");
|
|
1024
|
+
const selected = selectContent(planPath, content, cacheDir, policy.plan);
|
|
1025
|
+
const label = tierLabel("Plan", policy.plan);
|
|
1026
|
+
context += `
|
|
1027
|
+
## ${label}
|
|
1028
|
+
${selected}
|
|
1029
|
+
`;
|
|
1030
|
+
}
|
|
1031
|
+
const contextMdPath = path4.join(taskDir, "context.md");
|
|
1032
|
+
if (fs4.existsSync(contextMdPath)) {
|
|
1033
|
+
const content = fs4.readFileSync(contextMdPath, "utf-8");
|
|
1034
|
+
const selected = selectContent(contextMdPath, content, cacheDir, policy.accumulatedContext);
|
|
1035
|
+
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
1036
|
+
context += `
|
|
1037
|
+
## ${label}
|
|
1038
|
+
${selected}
|
|
1039
|
+
`;
|
|
1040
|
+
}
|
|
1041
|
+
if (feedback) {
|
|
1042
|
+
context += `
|
|
1043
|
+
## Human Feedback
|
|
1044
|
+
${feedback}
|
|
1045
|
+
`;
|
|
1046
|
+
}
|
|
1047
|
+
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
1048
|
+
}
|
|
1049
|
+
function selectContent(filePath, content, cacheDir, tier) {
|
|
1050
|
+
if (tier === "L2") return content;
|
|
1051
|
+
const tiered = getTieredContent(filePath, content, cacheDir);
|
|
1052
|
+
return selectTier(tiered, tier);
|
|
1053
|
+
}
|
|
1054
|
+
function tierLabel(sectionName, tier) {
|
|
1055
|
+
if (tier === "L2") return sectionName;
|
|
1056
|
+
if (tier === "L1") return `${sectionName} (overview)`;
|
|
1057
|
+
return `${sectionName} (abstract)`;
|
|
1058
|
+
}
|
|
1059
|
+
var DEFAULT_STAGE_POLICIES, L0_MAX_CHARS, L1_MAX_CHARS;
|
|
1060
|
+
var init_context_tiers = __esm({
|
|
1061
|
+
"src/context-tiers.ts"() {
|
|
1062
|
+
"use strict";
|
|
1063
|
+
DEFAULT_STAGE_POLICIES = {
|
|
1064
|
+
taskify: {
|
|
1065
|
+
memory: "L1",
|
|
1066
|
+
taskDescription: "L2",
|
|
1067
|
+
taskClassification: "L0",
|
|
1068
|
+
spec: "L0",
|
|
1069
|
+
plan: "L0",
|
|
1070
|
+
accumulatedContext: "L0"
|
|
1071
|
+
},
|
|
1072
|
+
plan: {
|
|
1073
|
+
memory: "L1",
|
|
1074
|
+
taskDescription: "L2",
|
|
1075
|
+
taskClassification: "L2",
|
|
1076
|
+
spec: "L0",
|
|
1077
|
+
plan: "L0",
|
|
1078
|
+
accumulatedContext: "L1"
|
|
1079
|
+
},
|
|
1080
|
+
build: {
|
|
1081
|
+
memory: "L1",
|
|
1082
|
+
taskDescription: "L1",
|
|
1083
|
+
taskClassification: "L1",
|
|
1084
|
+
spec: "L1",
|
|
1085
|
+
plan: "L2",
|
|
1086
|
+
accumulatedContext: "L1"
|
|
1087
|
+
},
|
|
1088
|
+
autofix: {
|
|
1089
|
+
memory: "L0",
|
|
1090
|
+
taskDescription: "L0",
|
|
1091
|
+
taskClassification: "L0",
|
|
1092
|
+
spec: "L0",
|
|
1093
|
+
plan: "L1",
|
|
1094
|
+
accumulatedContext: "L2"
|
|
1095
|
+
},
|
|
1096
|
+
review: {
|
|
1097
|
+
memory: "L1",
|
|
1098
|
+
taskDescription: "L1",
|
|
1099
|
+
taskClassification: "L1",
|
|
1100
|
+
spec: "L0",
|
|
1101
|
+
plan: "L2",
|
|
1102
|
+
accumulatedContext: "L1"
|
|
1103
|
+
},
|
|
1104
|
+
"review-fix": {
|
|
1105
|
+
memory: "L0",
|
|
1106
|
+
taskDescription: "L0",
|
|
1107
|
+
taskClassification: "L0",
|
|
1108
|
+
spec: "L0",
|
|
1109
|
+
plan: "L1",
|
|
1110
|
+
accumulatedContext: "L2"
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
L0_MAX_CHARS = 400;
|
|
1114
|
+
L1_MAX_CHARS = 1600;
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// src/context.ts
|
|
1119
|
+
import * as fs5 from "fs";
|
|
1120
|
+
import * as path5 from "path";
|
|
781
1121
|
function readPromptFile(stageName, projectDir) {
|
|
782
1122
|
if (projectDir) {
|
|
783
|
-
const stepFile =
|
|
784
|
-
if (
|
|
785
|
-
return
|
|
1123
|
+
const stepFile = path5.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
1124
|
+
if (fs5.existsSync(stepFile)) {
|
|
1125
|
+
return fs5.readFileSync(stepFile, "utf-8");
|
|
786
1126
|
}
|
|
787
1127
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
788
1128
|
}
|
|
789
1129
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
790
1130
|
const candidates = [
|
|
791
|
-
|
|
792
|
-
|
|
1131
|
+
path5.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
1132
|
+
path5.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
793
1133
|
];
|
|
794
1134
|
for (const candidate of candidates) {
|
|
795
|
-
if (
|
|
796
|
-
return
|
|
1135
|
+
if (fs5.existsSync(candidate)) {
|
|
1136
|
+
return fs5.readFileSync(candidate, "utf-8");
|
|
797
1137
|
}
|
|
798
1138
|
}
|
|
799
1139
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -805,18 +1145,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
805
1145
|
`;
|
|
806
1146
|
context += `Task Directory: ${taskDir}
|
|
807
1147
|
`;
|
|
808
|
-
const taskMdPath =
|
|
809
|
-
if (
|
|
810
|
-
const taskMd =
|
|
1148
|
+
const taskMdPath = path5.join(taskDir, "task.md");
|
|
1149
|
+
if (fs5.existsSync(taskMdPath)) {
|
|
1150
|
+
const taskMd = fs5.readFileSync(taskMdPath, "utf-8");
|
|
811
1151
|
context += `
|
|
812
1152
|
## Task Description
|
|
813
1153
|
${taskMd}
|
|
814
1154
|
`;
|
|
815
1155
|
}
|
|
816
|
-
const taskJsonPath =
|
|
817
|
-
if (
|
|
1156
|
+
const taskJsonPath = path5.join(taskDir, "task.json");
|
|
1157
|
+
if (fs5.existsSync(taskJsonPath)) {
|
|
818
1158
|
try {
|
|
819
|
-
const taskDef = JSON.parse(
|
|
1159
|
+
const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
|
|
820
1160
|
context += `
|
|
821
1161
|
## Task Classification
|
|
822
1162
|
`;
|
|
@@ -829,27 +1169,27 @@ ${taskMd}
|
|
|
829
1169
|
} catch {
|
|
830
1170
|
}
|
|
831
1171
|
}
|
|
832
|
-
const specPath =
|
|
833
|
-
if (
|
|
834
|
-
const spec =
|
|
1172
|
+
const specPath = path5.join(taskDir, "spec.md");
|
|
1173
|
+
if (fs5.existsSync(specPath)) {
|
|
1174
|
+
const spec = fs5.readFileSync(specPath, "utf-8");
|
|
835
1175
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
836
1176
|
context += `
|
|
837
1177
|
## Spec Summary
|
|
838
1178
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
839
1179
|
`;
|
|
840
1180
|
}
|
|
841
|
-
const planPath =
|
|
842
|
-
if (
|
|
843
|
-
const plan =
|
|
1181
|
+
const planPath = path5.join(taskDir, "plan.md");
|
|
1182
|
+
if (fs5.existsSync(planPath)) {
|
|
1183
|
+
const plan = fs5.readFileSync(planPath, "utf-8");
|
|
844
1184
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
845
1185
|
context += `
|
|
846
1186
|
## Plan Summary
|
|
847
1187
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
848
1188
|
`;
|
|
849
1189
|
}
|
|
850
|
-
const contextMdPath =
|
|
851
|
-
if (
|
|
852
|
-
const accumulated =
|
|
1190
|
+
const contextMdPath = path5.join(taskDir, "context.md");
|
|
1191
|
+
if (fs5.existsSync(contextMdPath)) {
|
|
1192
|
+
const accumulated = fs5.readFileSync(contextMdPath, "utf-8");
|
|
853
1193
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
854
1194
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
855
1195
|
context += `
|
|
@@ -866,6 +1206,10 @@ ${feedback}
|
|
|
866
1206
|
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
867
1207
|
}
|
|
868
1208
|
function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
|
|
1209
|
+
const config = getProjectConfig();
|
|
1210
|
+
if (config.contextTiers?.enabled) {
|
|
1211
|
+
return buildFullPromptTiered(stageName, taskId, taskDir, projectDir, feedback);
|
|
1212
|
+
}
|
|
869
1213
|
const memory = readProjectMemory(projectDir);
|
|
870
1214
|
const promptTemplate = readPromptFile(stageName, projectDir);
|
|
871
1215
|
const prompt = injectTaskContext(promptTemplate, taskId, taskDir, feedback);
|
|
@@ -874,6 +1218,24 @@ function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
|
|
|
874
1218
|
|
|
875
1219
|
${prompt}` : prompt;
|
|
876
1220
|
}
|
|
1221
|
+
function buildFullPromptTiered(stageName, taskId, taskDir, projectDir, feedback) {
|
|
1222
|
+
const config = getProjectConfig();
|
|
1223
|
+
const policy = resolveStagePolicy(stageName, config.contextTiers?.stageOverrides);
|
|
1224
|
+
const tokenBudget = config.contextTiers?.tokenBudget ?? 8e3;
|
|
1225
|
+
const memory = readProjectMemoryTiered(projectDir, policy.memory);
|
|
1226
|
+
const promptTemplate = readPromptFile(stageName, projectDir);
|
|
1227
|
+
const prompt = injectTaskContextTiered(promptTemplate, taskId, taskDir, policy, feedback);
|
|
1228
|
+
let assembled = memory ? `${memory}
|
|
1229
|
+
---
|
|
1230
|
+
|
|
1231
|
+
${prompt}` : prompt;
|
|
1232
|
+
const tokens = estimateTokens(assembled);
|
|
1233
|
+
if (tokens > tokenBudget) {
|
|
1234
|
+
const maxChars = tokenBudget * 4;
|
|
1235
|
+
assembled = assembled.slice(0, maxChars) + "\n...(context truncated to fit token budget)";
|
|
1236
|
+
}
|
|
1237
|
+
return assembled;
|
|
1238
|
+
}
|
|
877
1239
|
function resolveModel(modelTier, stageName) {
|
|
878
1240
|
const config = getProjectConfig();
|
|
879
1241
|
if (config.agent.usePerStageRouting && stageName) {
|
|
@@ -889,6 +1251,7 @@ var init_context = __esm({
|
|
|
889
1251
|
"use strict";
|
|
890
1252
|
init_memory();
|
|
891
1253
|
init_config();
|
|
1254
|
+
init_context_tiers();
|
|
892
1255
|
DEFAULT_MODEL_MAP = {
|
|
893
1256
|
cheap: "haiku",
|
|
894
1257
|
mid: "sonnet",
|
|
@@ -969,8 +1332,8 @@ var init_runner_selection = __esm({
|
|
|
969
1332
|
});
|
|
970
1333
|
|
|
971
1334
|
// src/stages/agent.ts
|
|
972
|
-
import * as
|
|
973
|
-
import * as
|
|
1335
|
+
import * as fs6 from "fs";
|
|
1336
|
+
import * as path6 from "path";
|
|
974
1337
|
function getSessionInfo(stageName, sessions) {
|
|
975
1338
|
const group = SESSION_GROUP[stageName];
|
|
976
1339
|
if (!group) return void 0;
|
|
@@ -1005,8 +1368,8 @@ async function executeAgentStage(ctx, def) {
|
|
|
1005
1368
|
const runnerName = config.agent.stageRunners?.[def.name] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
1006
1369
|
logger.info(` runner=${runnerName} model=${model} timeout=${def.timeout / 1e3}s`);
|
|
1007
1370
|
const extraEnv = {};
|
|
1008
|
-
if (config
|
|
1009
|
-
extraEnv.ANTHROPIC_BASE_URL = config
|
|
1371
|
+
if (needsLitellmProxy(config)) {
|
|
1372
|
+
extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl(config);
|
|
1010
1373
|
}
|
|
1011
1374
|
const sessions = ctx.sessions ?? {};
|
|
1012
1375
|
const sessionInfo = getSessionInfo(def.name, sessions);
|
|
@@ -1023,27 +1386,27 @@ async function executeAgentStage(ctx, def) {
|
|
|
1023
1386
|
return { outcome: result.outcome, error: result.error, retries: 0 };
|
|
1024
1387
|
}
|
|
1025
1388
|
if (def.outputFile && result.output) {
|
|
1026
|
-
|
|
1389
|
+
fs6.writeFileSync(path6.join(ctx.taskDir, def.outputFile), result.output);
|
|
1027
1390
|
}
|
|
1028
1391
|
if (def.outputFile) {
|
|
1029
|
-
const outputPath =
|
|
1030
|
-
if (!
|
|
1031
|
-
const ext =
|
|
1032
|
-
const base =
|
|
1033
|
-
const files =
|
|
1392
|
+
const outputPath = path6.join(ctx.taskDir, def.outputFile);
|
|
1393
|
+
if (!fs6.existsSync(outputPath)) {
|
|
1394
|
+
const ext = path6.extname(def.outputFile);
|
|
1395
|
+
const base = path6.basename(def.outputFile, ext);
|
|
1396
|
+
const files = fs6.readdirSync(ctx.taskDir);
|
|
1034
1397
|
const variant = files.find(
|
|
1035
1398
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
1036
1399
|
);
|
|
1037
1400
|
if (variant) {
|
|
1038
|
-
|
|
1401
|
+
fs6.renameSync(path6.join(ctx.taskDir, variant), outputPath);
|
|
1039
1402
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
1040
1403
|
}
|
|
1041
1404
|
}
|
|
1042
1405
|
}
|
|
1043
1406
|
if (def.outputFile) {
|
|
1044
|
-
const outputPath =
|
|
1045
|
-
if (
|
|
1046
|
-
const content =
|
|
1407
|
+
const outputPath = path6.join(ctx.taskDir, def.outputFile);
|
|
1408
|
+
if (fs6.existsSync(outputPath)) {
|
|
1409
|
+
const content = fs6.readFileSync(outputPath, "utf-8");
|
|
1047
1410
|
const validation = validateStageOutput(def.name, content);
|
|
1048
1411
|
if (!validation.valid) {
|
|
1049
1412
|
if (def.name === "taskify") {
|
|
@@ -1057,7 +1420,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
1057
1420
|
const stripped = stripFences(retryResult.output);
|
|
1058
1421
|
const retryValidation = validateTaskJson(stripped);
|
|
1059
1422
|
if (retryValidation.valid) {
|
|
1060
|
-
|
|
1423
|
+
fs6.writeFileSync(outputPath, retryResult.output);
|
|
1061
1424
|
logger.info(` taskify retry produced valid JSON`);
|
|
1062
1425
|
} else {
|
|
1063
1426
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -1073,7 +1436,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
1073
1436
|
return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
|
|
1074
1437
|
}
|
|
1075
1438
|
function appendStageContext(taskDir, stageName, output) {
|
|
1076
|
-
const contextPath =
|
|
1439
|
+
const contextPath = path6.join(taskDir, "context.md");
|
|
1077
1440
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
1078
1441
|
let summary;
|
|
1079
1442
|
if (output && output.trim()) {
|
|
@@ -1086,7 +1449,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
1086
1449
|
### ${stageName} (${timestamp2})
|
|
1087
1450
|
${summary}
|
|
1088
1451
|
`;
|
|
1089
|
-
|
|
1452
|
+
fs6.appendFileSync(contextPath, entry);
|
|
1090
1453
|
}
|
|
1091
1454
|
var SESSION_GROUP;
|
|
1092
1455
|
var init_agent = __esm({
|
|
@@ -1308,8 +1671,8 @@ Error context:
|
|
|
1308
1671
|
});
|
|
1309
1672
|
|
|
1310
1673
|
// src/stages/gate.ts
|
|
1311
|
-
import * as
|
|
1312
|
-
import * as
|
|
1674
|
+
import * as fs7 from "fs";
|
|
1675
|
+
import * as path7 from "path";
|
|
1313
1676
|
function executeGateStage(ctx, def) {
|
|
1314
1677
|
if (ctx.input.dryRun) {
|
|
1315
1678
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -1352,7 +1715,7 @@ ${output}
|
|
|
1352
1715
|
`);
|
|
1353
1716
|
}
|
|
1354
1717
|
}
|
|
1355
|
-
|
|
1718
|
+
fs7.writeFileSync(path7.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
1356
1719
|
return {
|
|
1357
1720
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
1358
1721
|
retries: 0
|
|
@@ -1367,8 +1730,8 @@ var init_gate = __esm({
|
|
|
1367
1730
|
});
|
|
1368
1731
|
|
|
1369
1732
|
// src/stages/verify.ts
|
|
1370
|
-
import * as
|
|
1371
|
-
import * as
|
|
1733
|
+
import * as fs8 from "fs";
|
|
1734
|
+
import * as path8 from "path";
|
|
1372
1735
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
1373
1736
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
1374
1737
|
const maxAttempts = def.maxRetries ?? 2;
|
|
@@ -1379,8 +1742,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
1379
1742
|
return { ...gateResult, retries: attempt };
|
|
1380
1743
|
}
|
|
1381
1744
|
if (attempt < maxAttempts) {
|
|
1382
|
-
const verifyPath =
|
|
1383
|
-
const errorOutput =
|
|
1745
|
+
const verifyPath = path8.join(ctx.taskDir, "verify.md");
|
|
1746
|
+
const errorOutput = fs8.existsSync(verifyPath) ? fs8.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
1384
1747
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
1385
1748
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
1386
1749
|
const diagConfig = getProjectConfig();
|
|
@@ -1476,8 +1839,8 @@ var init_verify = __esm({
|
|
|
1476
1839
|
});
|
|
1477
1840
|
|
|
1478
1841
|
// src/stages/review.ts
|
|
1479
|
-
import * as
|
|
1480
|
-
import * as
|
|
1842
|
+
import * as fs9 from "fs";
|
|
1843
|
+
import * as path9 from "path";
|
|
1481
1844
|
async function executeReviewWithFix(ctx, def) {
|
|
1482
1845
|
if (ctx.input.dryRun) {
|
|
1483
1846
|
return { outcome: "completed", retries: 0 };
|
|
@@ -1488,11 +1851,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
1488
1851
|
if (reviewResult.outcome !== "completed") {
|
|
1489
1852
|
return reviewResult;
|
|
1490
1853
|
}
|
|
1491
|
-
const reviewFile =
|
|
1492
|
-
if (!
|
|
1854
|
+
const reviewFile = path9.join(ctx.taskDir, "review.md");
|
|
1855
|
+
if (!fs9.existsSync(reviewFile)) {
|
|
1493
1856
|
return { outcome: "failed", retries: 0, error: "review.md not found" };
|
|
1494
1857
|
}
|
|
1495
|
-
const content =
|
|
1858
|
+
const content = fs9.readFileSync(reviewFile, "utf-8");
|
|
1496
1859
|
const hasIssues = /\bfail\b/i.test(content) && !/pass/i.test(content);
|
|
1497
1860
|
if (!hasIssues) {
|
|
1498
1861
|
return reviewResult;
|
|
@@ -1515,15 +1878,15 @@ var init_review = __esm({
|
|
|
1515
1878
|
});
|
|
1516
1879
|
|
|
1517
1880
|
// src/stages/ship.ts
|
|
1518
|
-
import * as
|
|
1519
|
-
import * as
|
|
1881
|
+
import * as fs10 from "fs";
|
|
1882
|
+
import * as path10 from "path";
|
|
1520
1883
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
1521
1884
|
function buildPrBody(ctx) {
|
|
1522
1885
|
const sections = [];
|
|
1523
|
-
const taskJsonPath =
|
|
1524
|
-
if (
|
|
1886
|
+
const taskJsonPath = path10.join(ctx.taskDir, "task.json");
|
|
1887
|
+
if (fs10.existsSync(taskJsonPath)) {
|
|
1525
1888
|
try {
|
|
1526
|
-
const raw =
|
|
1889
|
+
const raw = fs10.readFileSync(taskJsonPath, "utf-8");
|
|
1527
1890
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1528
1891
|
const task = JSON.parse(cleaned);
|
|
1529
1892
|
if (task.description) {
|
|
@@ -1542,9 +1905,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
1542
1905
|
} catch {
|
|
1543
1906
|
}
|
|
1544
1907
|
}
|
|
1545
|
-
const reviewPath =
|
|
1546
|
-
if (
|
|
1547
|
-
const review =
|
|
1908
|
+
const reviewPath = path10.join(ctx.taskDir, "review.md");
|
|
1909
|
+
if (fs10.existsSync(reviewPath)) {
|
|
1910
|
+
const review = fs10.readFileSync(reviewPath, "utf-8");
|
|
1548
1911
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
1549
1912
|
if (summaryMatch) {
|
|
1550
1913
|
const summary = summaryMatch[1].trim();
|
|
@@ -1561,14 +1924,14 @@ ${summary}`);
|
|
|
1561
1924
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
1562
1925
|
}
|
|
1563
1926
|
}
|
|
1564
|
-
const verifyPath =
|
|
1565
|
-
if (
|
|
1566
|
-
const verify =
|
|
1927
|
+
const verifyPath = path10.join(ctx.taskDir, "verify.md");
|
|
1928
|
+
if (fs10.existsSync(verifyPath)) {
|
|
1929
|
+
const verify = fs10.readFileSync(verifyPath, "utf-8");
|
|
1567
1930
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
1568
1931
|
}
|
|
1569
|
-
const planPath =
|
|
1570
|
-
if (
|
|
1571
|
-
const plan =
|
|
1932
|
+
const planPath = path10.join(ctx.taskDir, "plan.md");
|
|
1933
|
+
if (fs10.existsSync(planPath)) {
|
|
1934
|
+
const plan = fs10.readFileSync(planPath, "utf-8").trim();
|
|
1572
1935
|
if (plan) {
|
|
1573
1936
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
1574
1937
|
sections.push(`
|
|
@@ -1588,13 +1951,13 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
1588
1951
|
return sections.join("\n");
|
|
1589
1952
|
}
|
|
1590
1953
|
function executeShipStage(ctx, _def) {
|
|
1591
|
-
const shipPath =
|
|
1954
|
+
const shipPath = path10.join(ctx.taskDir, "ship.md");
|
|
1592
1955
|
if (ctx.input.dryRun) {
|
|
1593
|
-
|
|
1956
|
+
fs10.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
1594
1957
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1595
1958
|
}
|
|
1596
1959
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
1597
|
-
|
|
1960
|
+
fs10.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
1598
1961
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1599
1962
|
}
|
|
1600
1963
|
try {
|
|
@@ -1640,10 +2003,10 @@ function executeShipStage(ctx, _def) {
|
|
|
1640
2003
|
docs: "docs",
|
|
1641
2004
|
chore: "chore"
|
|
1642
2005
|
};
|
|
1643
|
-
const taskJsonPath =
|
|
1644
|
-
if (
|
|
2006
|
+
const taskJsonPath = path10.join(ctx.taskDir, "task.json");
|
|
2007
|
+
if (fs10.existsSync(taskJsonPath)) {
|
|
1645
2008
|
try {
|
|
1646
|
-
const raw =
|
|
2009
|
+
const raw = fs10.readFileSync(taskJsonPath, "utf-8");
|
|
1647
2010
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1648
2011
|
const task = JSON.parse(cleaned);
|
|
1649
2012
|
const prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
@@ -1653,9 +2016,9 @@ function executeShipStage(ctx, _def) {
|
|
|
1653
2016
|
}
|
|
1654
2017
|
}
|
|
1655
2018
|
if (title === "Update") {
|
|
1656
|
-
const taskMdPath =
|
|
1657
|
-
if (
|
|
1658
|
-
const content =
|
|
2019
|
+
const taskMdPath = path10.join(ctx.taskDir, "task.md");
|
|
2020
|
+
if (fs10.existsSync(taskMdPath)) {
|
|
2021
|
+
const content = fs10.readFileSync(taskMdPath, "utf-8");
|
|
1659
2022
|
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("*"));
|
|
1660
2023
|
if (firstLine) title = `chore: ${firstLine.trim()}`.slice(0, 72);
|
|
1661
2024
|
}
|
|
@@ -1675,7 +2038,7 @@ function executeShipStage(ctx, _def) {
|
|
|
1675
2038
|
} catch {
|
|
1676
2039
|
}
|
|
1677
2040
|
}
|
|
1678
|
-
|
|
2041
|
+
fs10.writeFileSync(shipPath, `# Ship
|
|
1679
2042
|
|
|
1680
2043
|
Updated existing PR: ${existingPr.url}
|
|
1681
2044
|
PR #${existingPr.number}
|
|
@@ -1689,19 +2052,19 @@ PR #${existingPr.number}
|
|
|
1689
2052
|
} catch {
|
|
1690
2053
|
}
|
|
1691
2054
|
}
|
|
1692
|
-
|
|
2055
|
+
fs10.writeFileSync(shipPath, `# Ship
|
|
1693
2056
|
|
|
1694
2057
|
PR created: ${pr.url}
|
|
1695
2058
|
PR #${pr.number}
|
|
1696
2059
|
`);
|
|
1697
2060
|
} else {
|
|
1698
|
-
|
|
2061
|
+
fs10.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
1699
2062
|
}
|
|
1700
2063
|
}
|
|
1701
2064
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
1702
2065
|
} catch (err) {
|
|
1703
2066
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1704
|
-
|
|
2067
|
+
fs10.writeFileSync(shipPath, `# Ship
|
|
1705
2068
|
|
|
1706
2069
|
Failed: ${msg}
|
|
1707
2070
|
`);
|
|
@@ -1747,15 +2110,15 @@ var init_executor_registry = __esm({
|
|
|
1747
2110
|
});
|
|
1748
2111
|
|
|
1749
2112
|
// src/pipeline/questions.ts
|
|
1750
|
-
import * as
|
|
1751
|
-
import * as
|
|
2113
|
+
import * as fs11 from "fs";
|
|
2114
|
+
import * as path11 from "path";
|
|
1752
2115
|
function checkForQuestions(ctx, stageName) {
|
|
1753
2116
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
1754
2117
|
try {
|
|
1755
2118
|
if (stageName === "taskify") {
|
|
1756
|
-
const taskJsonPath =
|
|
1757
|
-
if (!
|
|
1758
|
-
const raw =
|
|
2119
|
+
const taskJsonPath = path11.join(ctx.taskDir, "task.json");
|
|
2120
|
+
if (!fs11.existsSync(taskJsonPath)) return false;
|
|
2121
|
+
const raw = fs11.readFileSync(taskJsonPath, "utf-8");
|
|
1759
2122
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1760
2123
|
const taskJson = JSON.parse(cleaned);
|
|
1761
2124
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -1770,9 +2133,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
1770
2133
|
}
|
|
1771
2134
|
}
|
|
1772
2135
|
if (stageName === "plan") {
|
|
1773
|
-
const planPath =
|
|
1774
|
-
if (!
|
|
1775
|
-
const plan =
|
|
2136
|
+
const planPath = path11.join(ctx.taskDir, "plan.md");
|
|
2137
|
+
if (!fs11.existsSync(planPath)) return false;
|
|
2138
|
+
const plan = fs11.readFileSync(planPath, "utf-8");
|
|
1776
2139
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
1777
2140
|
if (questionsMatch) {
|
|
1778
2141
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -1801,8 +2164,8 @@ var init_questions = __esm({
|
|
|
1801
2164
|
});
|
|
1802
2165
|
|
|
1803
2166
|
// src/pipeline/hooks.ts
|
|
1804
|
-
import * as
|
|
1805
|
-
import * as
|
|
2167
|
+
import * as fs12 from "fs";
|
|
2168
|
+
import * as path12 from "path";
|
|
1806
2169
|
function applyPreStageLabel(ctx, def) {
|
|
1807
2170
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
1808
2171
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
@@ -1828,9 +2191,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
1828
2191
|
if (def.name !== "taskify") return null;
|
|
1829
2192
|
if (ctx.input.complexity) return null;
|
|
1830
2193
|
try {
|
|
1831
|
-
const taskJsonPath =
|
|
1832
|
-
if (!
|
|
1833
|
-
const raw =
|
|
2194
|
+
const taskJsonPath = path12.join(ctx.taskDir, "task.json");
|
|
2195
|
+
if (!fs12.existsSync(taskJsonPath)) return null;
|
|
2196
|
+
const raw = fs12.readFileSync(taskJsonPath, "utf-8");
|
|
1834
2197
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1835
2198
|
const taskJson = JSON.parse(cleaned);
|
|
1836
2199
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -1860,8 +2223,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
1860
2223
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
1861
2224
|
if (ctx.input.mode === "rerun") return null;
|
|
1862
2225
|
if (!ctx.input.issueNumber) return null;
|
|
1863
|
-
const planPath =
|
|
1864
|
-
const plan =
|
|
2226
|
+
const planPath = path12.join(ctx.taskDir, "plan.md");
|
|
2227
|
+
const plan = fs12.existsSync(planPath) ? fs12.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
1865
2228
|
try {
|
|
1866
2229
|
postComment(
|
|
1867
2230
|
ctx.input.issueNumber,
|
|
@@ -1928,22 +2291,22 @@ var init_hooks = __esm({
|
|
|
1928
2291
|
});
|
|
1929
2292
|
|
|
1930
2293
|
// src/learning/auto-learn.ts
|
|
1931
|
-
import * as
|
|
1932
|
-
import * as
|
|
2294
|
+
import * as fs13 from "fs";
|
|
2295
|
+
import * as path13 from "path";
|
|
1933
2296
|
function stripAnsi(str) {
|
|
1934
2297
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
1935
2298
|
}
|
|
1936
2299
|
function autoLearn(ctx) {
|
|
1937
2300
|
try {
|
|
1938
|
-
const memoryDir =
|
|
1939
|
-
if (!
|
|
1940
|
-
|
|
2301
|
+
const memoryDir = path13.join(ctx.projectDir, ".kody", "memory");
|
|
2302
|
+
if (!fs13.existsSync(memoryDir)) {
|
|
2303
|
+
fs13.mkdirSync(memoryDir, { recursive: true });
|
|
1941
2304
|
}
|
|
1942
2305
|
const learnings = [];
|
|
1943
2306
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1944
|
-
const verifyPath =
|
|
1945
|
-
if (
|
|
1946
|
-
const verify = stripAnsi(
|
|
2307
|
+
const verifyPath = path13.join(ctx.taskDir, "verify.md");
|
|
2308
|
+
if (fs13.existsSync(verifyPath)) {
|
|
2309
|
+
const verify = stripAnsi(fs13.readFileSync(verifyPath, "utf-8"));
|
|
1947
2310
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
1948
2311
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
1949
2312
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -1952,18 +2315,18 @@ function autoLearn(ctx) {
|
|
|
1952
2315
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
1953
2316
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
1954
2317
|
}
|
|
1955
|
-
const reviewPath =
|
|
1956
|
-
if (
|
|
1957
|
-
const review =
|
|
2318
|
+
const reviewPath = path13.join(ctx.taskDir, "review.md");
|
|
2319
|
+
if (fs13.existsSync(reviewPath)) {
|
|
2320
|
+
const review = fs13.readFileSync(reviewPath, "utf-8");
|
|
1958
2321
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
1959
2322
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
1960
2323
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
1961
2324
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
1962
2325
|
}
|
|
1963
|
-
const taskJsonPath =
|
|
1964
|
-
if (
|
|
2326
|
+
const taskJsonPath = path13.join(ctx.taskDir, "task.json");
|
|
2327
|
+
if (fs13.existsSync(taskJsonPath)) {
|
|
1965
2328
|
try {
|
|
1966
|
-
const raw = stripAnsi(
|
|
2329
|
+
const raw = stripAnsi(fs13.readFileSync(taskJsonPath, "utf-8"));
|
|
1967
2330
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
1968
2331
|
const task = JSON.parse(cleaned);
|
|
1969
2332
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -1974,12 +2337,13 @@ function autoLearn(ctx) {
|
|
|
1974
2337
|
}
|
|
1975
2338
|
}
|
|
1976
2339
|
if (learnings.length > 0) {
|
|
1977
|
-
const conventionsPath =
|
|
2340
|
+
const conventionsPath = path13.join(memoryDir, "conventions.md");
|
|
1978
2341
|
const entry = `
|
|
1979
2342
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
1980
2343
|
${learnings.join("\n")}
|
|
1981
2344
|
`;
|
|
1982
|
-
|
|
2345
|
+
fs13.appendFileSync(conventionsPath, entry);
|
|
2346
|
+
invalidateCache(conventionsPath, path13.join(memoryDir, ".tiers"));
|
|
1983
2347
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
1984
2348
|
}
|
|
1985
2349
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
@@ -1987,13 +2351,13 @@ ${learnings.join("\n")}
|
|
|
1987
2351
|
}
|
|
1988
2352
|
}
|
|
1989
2353
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
1990
|
-
const archPath =
|
|
1991
|
-
if (
|
|
2354
|
+
const archPath = path13.join(memoryDir, "architecture.md");
|
|
2355
|
+
if (fs13.existsSync(archPath)) return;
|
|
1992
2356
|
const detected = [];
|
|
1993
|
-
const pkgPath =
|
|
1994
|
-
if (
|
|
2357
|
+
const pkgPath = path13.join(projectDir, "package.json");
|
|
2358
|
+
if (fs13.existsSync(pkgPath)) {
|
|
1995
2359
|
try {
|
|
1996
|
-
const pkg = JSON.parse(
|
|
2360
|
+
const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
|
|
1997
2361
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1998
2362
|
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
1999
2363
|
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
@@ -2009,15 +2373,15 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
2009
2373
|
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
2010
2374
|
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
2011
2375
|
else detected.push("- Module system: CommonJS");
|
|
2012
|
-
if (
|
|
2013
|
-
else if (
|
|
2014
|
-
else if (
|
|
2376
|
+
if (fs13.existsSync(path13.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
2377
|
+
else if (fs13.existsSync(path13.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
2378
|
+
else if (fs13.existsSync(path13.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
2015
2379
|
} catch {
|
|
2016
2380
|
}
|
|
2017
2381
|
}
|
|
2018
2382
|
const topDirs = [];
|
|
2019
2383
|
try {
|
|
2020
|
-
const entries =
|
|
2384
|
+
const entries = fs13.readdirSync(projectDir, { withFileTypes: true });
|
|
2021
2385
|
for (const entry of entries) {
|
|
2022
2386
|
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2023
2387
|
topDirs.push(entry.name);
|
|
@@ -2026,10 +2390,10 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
2026
2390
|
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
2027
2391
|
} catch {
|
|
2028
2392
|
}
|
|
2029
|
-
const srcDir =
|
|
2030
|
-
if (
|
|
2393
|
+
const srcDir = path13.join(projectDir, "src");
|
|
2394
|
+
if (fs13.existsSync(srcDir)) {
|
|
2031
2395
|
try {
|
|
2032
|
-
const srcEntries =
|
|
2396
|
+
const srcEntries = fs13.readdirSync(srcDir, { withFileTypes: true });
|
|
2033
2397
|
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2034
2398
|
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
2035
2399
|
} catch {
|
|
@@ -2041,7 +2405,8 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
|
2041
2405
|
## Overview
|
|
2042
2406
|
${detected.join("\n")}
|
|
2043
2407
|
`;
|
|
2044
|
-
|
|
2408
|
+
fs13.writeFileSync(archPath, content);
|
|
2409
|
+
invalidateCache(archPath, path13.join(memoryDir, ".tiers"));
|
|
2045
2410
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
2046
2411
|
}
|
|
2047
2412
|
}
|
|
@@ -2049,17 +2414,18 @@ var init_auto_learn = __esm({
|
|
|
2049
2414
|
"src/learning/auto-learn.ts"() {
|
|
2050
2415
|
"use strict";
|
|
2051
2416
|
init_logger();
|
|
2417
|
+
init_context_tiers();
|
|
2052
2418
|
}
|
|
2053
2419
|
});
|
|
2054
2420
|
|
|
2055
2421
|
// src/retrospective.ts
|
|
2056
|
-
import * as
|
|
2057
|
-
import * as
|
|
2422
|
+
import * as fs14 from "fs";
|
|
2423
|
+
import * as path14 from "path";
|
|
2058
2424
|
function readArtifact(taskDir, filename, maxChars) {
|
|
2059
|
-
const p =
|
|
2060
|
-
if (!
|
|
2425
|
+
const p = path14.join(taskDir, filename);
|
|
2426
|
+
if (!fs14.existsSync(p)) return null;
|
|
2061
2427
|
try {
|
|
2062
|
-
const content =
|
|
2428
|
+
const content = fs14.readFileSync(p, "utf-8");
|
|
2063
2429
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
2064
2430
|
} catch {
|
|
2065
2431
|
return null;
|
|
@@ -2112,13 +2478,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
2112
2478
|
return lines.join("\n");
|
|
2113
2479
|
}
|
|
2114
2480
|
function getLogPath(projectDir) {
|
|
2115
|
-
return
|
|
2481
|
+
return path14.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
2116
2482
|
}
|
|
2117
2483
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
2118
2484
|
const logPath = getLogPath(projectDir);
|
|
2119
|
-
if (!
|
|
2485
|
+
if (!fs14.existsSync(logPath)) return [];
|
|
2120
2486
|
try {
|
|
2121
|
-
const content =
|
|
2487
|
+
const content = fs14.readFileSync(logPath, "utf-8");
|
|
2122
2488
|
const lines = content.split("\n").filter(Boolean);
|
|
2123
2489
|
const entries = [];
|
|
2124
2490
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -2145,11 +2511,11 @@ function formatPreviousEntries(entries) {
|
|
|
2145
2511
|
}
|
|
2146
2512
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
2147
2513
|
const logPath = getLogPath(projectDir);
|
|
2148
|
-
const dir =
|
|
2149
|
-
if (!
|
|
2150
|
-
|
|
2514
|
+
const dir = path14.dirname(logPath);
|
|
2515
|
+
if (!fs14.existsSync(dir)) {
|
|
2516
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
2151
2517
|
}
|
|
2152
|
-
|
|
2518
|
+
fs14.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
2153
2519
|
}
|
|
2154
2520
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
2155
2521
|
if (ctx.input.dryRun) return;
|
|
@@ -2260,14 +2626,14 @@ If no pipeline flaw is detected, set "pipelineFlaw" to null.
|
|
|
2260
2626
|
});
|
|
2261
2627
|
|
|
2262
2628
|
// src/pipeline.ts
|
|
2263
|
-
import * as
|
|
2264
|
-
import * as
|
|
2629
|
+
import * as fs15 from "fs";
|
|
2630
|
+
import * as path15 from "path";
|
|
2265
2631
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
2266
2632
|
if (ctx.input.prNumber) return;
|
|
2267
2633
|
if (!ctx.input.issueNumber || ctx.input.dryRun) return;
|
|
2268
2634
|
try {
|
|
2269
|
-
const taskMdPath =
|
|
2270
|
-
const title =
|
|
2635
|
+
const taskMdPath = path15.join(ctx.taskDir, "task.md");
|
|
2636
|
+
const title = fs15.existsSync(taskMdPath) ? fs15.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
2271
2637
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
2272
2638
|
syncWithDefault(ctx.projectDir);
|
|
2273
2639
|
} catch (err) {
|
|
@@ -2275,10 +2641,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
2275
2641
|
}
|
|
2276
2642
|
}
|
|
2277
2643
|
function acquireLock(taskDir) {
|
|
2278
|
-
const lockPath =
|
|
2279
|
-
if (
|
|
2644
|
+
const lockPath = path15.join(taskDir, ".lock");
|
|
2645
|
+
if (fs15.existsSync(lockPath)) {
|
|
2280
2646
|
try {
|
|
2281
|
-
const pid = parseInt(
|
|
2647
|
+
const pid = parseInt(fs15.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
2282
2648
|
try {
|
|
2283
2649
|
process.kill(pid, 0);
|
|
2284
2650
|
throw new Error(`Pipeline already running (PID ${pid})`);
|
|
@@ -2289,11 +2655,11 @@ function acquireLock(taskDir) {
|
|
|
2289
2655
|
if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
|
|
2290
2656
|
}
|
|
2291
2657
|
}
|
|
2292
|
-
|
|
2658
|
+
fs15.writeFileSync(lockPath, String(process.pid));
|
|
2293
2659
|
}
|
|
2294
2660
|
function releaseLock(taskDir) {
|
|
2295
2661
|
try {
|
|
2296
|
-
|
|
2662
|
+
fs15.unlinkSync(path15.join(taskDir, ".lock"));
|
|
2297
2663
|
} catch {
|
|
2298
2664
|
}
|
|
2299
2665
|
}
|
|
@@ -2462,7 +2828,7 @@ var init_pipeline = __esm({
|
|
|
2462
2828
|
|
|
2463
2829
|
// src/preflight.ts
|
|
2464
2830
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
2465
|
-
import * as
|
|
2831
|
+
import * as fs16 from "fs";
|
|
2466
2832
|
function check(name, fn) {
|
|
2467
2833
|
try {
|
|
2468
2834
|
const detail = fn() ?? void 0;
|
|
@@ -2515,7 +2881,7 @@ function runPreflight() {
|
|
|
2515
2881
|
return v;
|
|
2516
2882
|
}),
|
|
2517
2883
|
check("package.json", () => {
|
|
2518
|
-
if (!
|
|
2884
|
+
if (!fs16.existsSync("package.json")) throw new Error("not found");
|
|
2519
2885
|
})
|
|
2520
2886
|
];
|
|
2521
2887
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -2536,13 +2902,13 @@ var init_preflight = __esm({
|
|
|
2536
2902
|
});
|
|
2537
2903
|
|
|
2538
2904
|
// src/cli/task-resolution.ts
|
|
2539
|
-
import * as
|
|
2540
|
-
import * as
|
|
2905
|
+
import * as fs17 from "fs";
|
|
2906
|
+
import * as path16 from "path";
|
|
2541
2907
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
2542
2908
|
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
2543
|
-
const tasksDir =
|
|
2544
|
-
if (!
|
|
2545
|
-
const allDirs =
|
|
2909
|
+
const tasksDir = path16.join(projectDir, ".kody", "tasks");
|
|
2910
|
+
if (!fs17.existsSync(tasksDir)) return null;
|
|
2911
|
+
const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
2546
2912
|
const prefix = `${issueNumber}-`;
|
|
2547
2913
|
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
2548
2914
|
if (direct) return direct;
|
|
@@ -2576,8 +2942,8 @@ var init_task_resolution = __esm({
|
|
|
2576
2942
|
});
|
|
2577
2943
|
|
|
2578
2944
|
// src/review-standalone.ts
|
|
2579
|
-
import * as
|
|
2580
|
-
import * as
|
|
2945
|
+
import * as fs18 from "fs";
|
|
2946
|
+
import * as path17 from "path";
|
|
2581
2947
|
function resolveReviewTarget(input) {
|
|
2582
2948
|
if (input.prs.length === 0) {
|
|
2583
2949
|
return {
|
|
@@ -2601,12 +2967,12 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
2601
2967
|
}
|
|
2602
2968
|
async function runStandaloneReview(input) {
|
|
2603
2969
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
2604
|
-
const taskDir =
|
|
2605
|
-
|
|
2970
|
+
const taskDir = path17.join(input.projectDir, ".kody", "tasks", taskId);
|
|
2971
|
+
fs18.mkdirSync(taskDir, { recursive: true });
|
|
2606
2972
|
const taskContent = `# ${input.prTitle}
|
|
2607
2973
|
|
|
2608
2974
|
${input.prBody ?? ""}`;
|
|
2609
|
-
|
|
2975
|
+
fs18.writeFileSync(path17.join(taskDir, "task.md"), taskContent);
|
|
2610
2976
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
2611
2977
|
const ctx = {
|
|
2612
2978
|
taskId,
|
|
@@ -2628,10 +2994,10 @@ ${input.prBody ?? ""}`;
|
|
|
2628
2994
|
error: result.error ?? "Review stage failed"
|
|
2629
2995
|
};
|
|
2630
2996
|
}
|
|
2631
|
-
const reviewPath =
|
|
2997
|
+
const reviewPath = path17.join(taskDir, "review.md");
|
|
2632
2998
|
let reviewContent;
|
|
2633
|
-
if (
|
|
2634
|
-
reviewContent =
|
|
2999
|
+
if (fs18.existsSync(reviewPath)) {
|
|
3000
|
+
reviewContent = fs18.readFileSync(reviewPath, "utf-8");
|
|
2635
3001
|
}
|
|
2636
3002
|
return {
|
|
2637
3003
|
outcome: "completed",
|
|
@@ -2723,8 +3089,9 @@ var init_args = __esm({
|
|
|
2723
3089
|
});
|
|
2724
3090
|
|
|
2725
3091
|
// src/cli/litellm.ts
|
|
2726
|
-
import * as
|
|
2727
|
-
import * as
|
|
3092
|
+
import * as fs19 from "fs";
|
|
3093
|
+
import * as os from "os";
|
|
3094
|
+
import * as path18 from "path";
|
|
2728
3095
|
import { execFileSync as execFileSync10 } from "child_process";
|
|
2729
3096
|
async function checkLitellmHealth(url) {
|
|
2730
3097
|
try {
|
|
@@ -2734,10 +3101,31 @@ async function checkLitellmHealth(url) {
|
|
|
2734
3101
|
return false;
|
|
2735
3102
|
}
|
|
2736
3103
|
}
|
|
2737
|
-
|
|
2738
|
-
const
|
|
2739
|
-
|
|
2740
|
-
|
|
3104
|
+
function generateLitellmConfig(provider, modelMap) {
|
|
3105
|
+
const apiKeyVar = providerApiKeyEnvVar(provider);
|
|
3106
|
+
const entries = ["model_list:"];
|
|
3107
|
+
for (const [tier, providerModel] of Object.entries(modelMap)) {
|
|
3108
|
+
const anthropicIds = TIER_TO_ANTHROPIC_IDS[tier];
|
|
3109
|
+
if (!anthropicIds) continue;
|
|
3110
|
+
for (const modelName of anthropicIds) {
|
|
3111
|
+
entries.push(` - model_name: ${modelName}`);
|
|
3112
|
+
entries.push(` litellm_params:`);
|
|
3113
|
+
entries.push(` model: ${provider}/${providerModel}`);
|
|
3114
|
+
entries.push(` api_key: os.environ/${apiKeyVar}`);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
return entries.join("\n") + "\n";
|
|
3118
|
+
}
|
|
3119
|
+
async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
3120
|
+
const manualConfigPath = path18.join(projectDir, "litellm-config.yaml");
|
|
3121
|
+
let configPath;
|
|
3122
|
+
if (fs19.existsSync(manualConfigPath)) {
|
|
3123
|
+
configPath = manualConfigPath;
|
|
3124
|
+
} else if (generatedConfig) {
|
|
3125
|
+
configPath = path18.join(os.tmpdir(), "kody-litellm-config.yaml");
|
|
3126
|
+
fs19.writeFileSync(configPath, generatedConfig);
|
|
3127
|
+
} else {
|
|
3128
|
+
logger.warn("litellm-config.yaml not found and no provider configured \u2014 cannot start proxy");
|
|
2741
3129
|
return null;
|
|
2742
3130
|
}
|
|
2743
3131
|
const portMatch = url.match(/:(\d+)/);
|
|
@@ -2768,10 +3156,10 @@ async function tryStartLitellm(url, projectDir) {
|
|
|
2768
3156
|
cmd = "python3";
|
|
2769
3157
|
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
2770
3158
|
}
|
|
2771
|
-
const dotenvPath =
|
|
3159
|
+
const dotenvPath = path18.join(projectDir, ".env");
|
|
2772
3160
|
const dotenvVars = {};
|
|
2773
|
-
if (
|
|
2774
|
-
for (const line of
|
|
3161
|
+
if (fs19.existsSync(dotenvPath)) {
|
|
3162
|
+
for (const line of fs19.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
2775
3163
|
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
2776
3164
|
if (match) dotenvVars[match[1]] = match[2];
|
|
2777
3165
|
}
|
|
@@ -2807,12 +3195,13 @@ var init_litellm = __esm({
|
|
|
2807
3195
|
"src/cli/litellm.ts"() {
|
|
2808
3196
|
"use strict";
|
|
2809
3197
|
init_logger();
|
|
3198
|
+
init_config();
|
|
2810
3199
|
}
|
|
2811
3200
|
});
|
|
2812
3201
|
|
|
2813
3202
|
// src/cli/task-state.ts
|
|
2814
|
-
import * as
|
|
2815
|
-
import * as
|
|
3203
|
+
import * as fs20 from "fs";
|
|
3204
|
+
import * as path19 from "path";
|
|
2816
3205
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
2817
3206
|
if (!existingTaskId || !existingState) {
|
|
2818
3207
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -2844,11 +3233,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
2844
3233
|
function resolveForIssue(issueNumber, projectDir) {
|
|
2845
3234
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
2846
3235
|
if (existingTaskId) {
|
|
2847
|
-
const statusPath =
|
|
3236
|
+
const statusPath = path19.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
2848
3237
|
let existingState = null;
|
|
2849
|
-
if (
|
|
3238
|
+
if (fs20.existsSync(statusPath)) {
|
|
2850
3239
|
try {
|
|
2851
|
-
existingState = JSON.parse(
|
|
3240
|
+
existingState = JSON.parse(fs20.readFileSync(statusPath, "utf-8"));
|
|
2852
3241
|
} catch {
|
|
2853
3242
|
}
|
|
2854
3243
|
}
|
|
@@ -2878,13 +3267,45 @@ var init_task_state = __esm({
|
|
|
2878
3267
|
|
|
2879
3268
|
// src/entry.ts
|
|
2880
3269
|
var entry_exports = {};
|
|
2881
|
-
import * as
|
|
2882
|
-
import * as
|
|
3270
|
+
import * as fs21 from "fs";
|
|
3271
|
+
import * as path20 from "path";
|
|
3272
|
+
async function ensureLitellmProxy(config, projectDir) {
|
|
3273
|
+
if (!needsLitellmProxy(config)) return null;
|
|
3274
|
+
const litellmUrl = getLitellmUrl(config);
|
|
3275
|
+
const proxyRunning = await checkLitellmHealth(litellmUrl);
|
|
3276
|
+
let litellmProcess = null;
|
|
3277
|
+
if (!proxyRunning) {
|
|
3278
|
+
if (config.agent.provider && config.agent.provider !== "anthropic") {
|
|
3279
|
+
const keyVar = providerApiKeyEnvVar(config.agent.provider);
|
|
3280
|
+
if (!process.env[keyVar]) {
|
|
3281
|
+
logger.error(`Provider '${config.agent.provider}' requires ${keyVar} environment variable`);
|
|
3282
|
+
process.exit(1);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
let generatedConfig;
|
|
3286
|
+
if (config.agent.provider && config.agent.provider !== "anthropic") {
|
|
3287
|
+
generatedConfig = generateLitellmConfig(config.agent.provider, config.agent.modelMap);
|
|
3288
|
+
}
|
|
3289
|
+
litellmProcess = await tryStartLitellm(litellmUrl, projectDir, generatedConfig);
|
|
3290
|
+
if (!litellmProcess) {
|
|
3291
|
+
logger.error("LiteLLM is configured but could not be started. Install it with: pip install 'litellm[proxy]'");
|
|
3292
|
+
process.exit(1);
|
|
3293
|
+
}
|
|
3294
|
+
} else {
|
|
3295
|
+
logger.info(`LiteLLM proxy already running at ${litellmUrl}`);
|
|
3296
|
+
}
|
|
3297
|
+
process.env.ANTHROPIC_BASE_URL = litellmUrl;
|
|
3298
|
+
logger.info(`ANTHROPIC_BASE_URL set to ${litellmUrl}`);
|
|
3299
|
+
if (!process.env.ANTHROPIC_API_KEY || !process.env.ANTHROPIC_API_KEY.startsWith("sk-ant-")) {
|
|
3300
|
+
process.env.ANTHROPIC_API_KEY = "sk-ant-api03-litellm-proxy-key-00000000000000000000000000000000000000000000000000000000000000000000";
|
|
3301
|
+
}
|
|
3302
|
+
return litellmProcess;
|
|
3303
|
+
}
|
|
2883
3304
|
async function main() {
|
|
2884
3305
|
const input = parseArgs();
|
|
2885
|
-
const projectDir = input.cwd ?
|
|
3306
|
+
const projectDir = input.cwd ? path20.resolve(input.cwd) : process.cwd();
|
|
2886
3307
|
if (input.cwd) {
|
|
2887
|
-
if (!
|
|
3308
|
+
if (!fs21.existsSync(projectDir)) {
|
|
2888
3309
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
2889
3310
|
process.exit(1);
|
|
2890
3311
|
}
|
|
@@ -2938,8 +3359,8 @@ async function main() {
|
|
|
2938
3359
|
process.exit(1);
|
|
2939
3360
|
}
|
|
2940
3361
|
}
|
|
2941
|
-
const taskDir =
|
|
2942
|
-
|
|
3362
|
+
const taskDir = path20.join(projectDir, ".kody", "tasks", taskId);
|
|
3363
|
+
fs21.mkdirSync(taskDir, { recursive: true });
|
|
2943
3364
|
if (input.command === "status") {
|
|
2944
3365
|
printStatus(taskId, taskDir);
|
|
2945
3366
|
return;
|
|
@@ -2972,18 +3393,7 @@ async function main() {
|
|
|
2972
3393
|
}
|
|
2973
3394
|
}
|
|
2974
3395
|
const config2 = getProjectConfig();
|
|
2975
|
-
|
|
2976
|
-
if (config2.agent.litellmUrl) {
|
|
2977
|
-
const proxyRunning = await checkLitellmHealth(config2.agent.litellmUrl);
|
|
2978
|
-
if (!proxyRunning) {
|
|
2979
|
-
litellmProcess2 = await tryStartLitellm(config2.agent.litellmUrl, projectDir);
|
|
2980
|
-
if (!litellmProcess2) {
|
|
2981
|
-
logger.error("LiteLLM is configured (litellmUrl) but could not be started. Install it with: pip install 'litellm[proxy]'");
|
|
2982
|
-
process.exit(1);
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
|
-
process.env.ANTHROPIC_BASE_URL = config2.agent.litellmUrl;
|
|
2986
|
-
}
|
|
3396
|
+
const litellmProcess2 = await ensureLitellmProxy(config2, projectDir);
|
|
2987
3397
|
const runners2 = createRunners(config2);
|
|
2988
3398
|
const defaultRunnerName2 = config2.agent.defaultRunner ?? Object.keys(runners2)[0] ?? "claude";
|
|
2989
3399
|
const defaultRunner2 = runners2[defaultRunnerName2];
|
|
@@ -3027,31 +3437,31 @@ async function main() {
|
|
|
3027
3437
|
logger.info("Preflight checks:");
|
|
3028
3438
|
runPreflight();
|
|
3029
3439
|
if (input.task) {
|
|
3030
|
-
|
|
3440
|
+
fs21.writeFileSync(path20.join(taskDir, "task.md"), input.task);
|
|
3031
3441
|
}
|
|
3032
|
-
const taskMdPath =
|
|
3033
|
-
if (!
|
|
3442
|
+
const taskMdPath = path20.join(taskDir, "task.md");
|
|
3443
|
+
if (!fs21.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
3034
3444
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
3035
3445
|
const prDetails = getPRDetails(input.prNumber);
|
|
3036
3446
|
if (prDetails) {
|
|
3037
3447
|
const taskContent = `# ${prDetails.title}
|
|
3038
3448
|
|
|
3039
3449
|
${prDetails.body ?? ""}`;
|
|
3040
|
-
|
|
3450
|
+
fs21.writeFileSync(taskMdPath, taskContent);
|
|
3041
3451
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
3042
3452
|
}
|
|
3043
|
-
} else if (!
|
|
3453
|
+
} else if (!fs21.existsSync(taskMdPath) && input.issueNumber) {
|
|
3044
3454
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
3045
3455
|
const issue = getIssue(input.issueNumber);
|
|
3046
3456
|
if (issue) {
|
|
3047
3457
|
const taskContent = `# ${issue.title}
|
|
3048
3458
|
|
|
3049
3459
|
${issue.body ?? ""}`;
|
|
3050
|
-
|
|
3460
|
+
fs21.writeFileSync(taskMdPath, taskContent);
|
|
3051
3461
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
3052
3462
|
}
|
|
3053
3463
|
}
|
|
3054
|
-
if (!
|
|
3464
|
+
if (!fs21.existsSync(taskMdPath)) {
|
|
3055
3465
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
3056
3466
|
process.exit(1);
|
|
3057
3467
|
}
|
|
@@ -3073,10 +3483,10 @@ ${input.feedback}` : reviewContext;
|
|
|
3073
3483
|
}
|
|
3074
3484
|
}
|
|
3075
3485
|
const config = getProjectConfig();
|
|
3076
|
-
let litellmProcess =
|
|
3486
|
+
let litellmProcess = await ensureLitellmProxy(config, projectDir);
|
|
3077
3487
|
const cleanupLitellm = () => {
|
|
3078
3488
|
if (litellmProcess) {
|
|
3079
|
-
litellmProcess.kill();
|
|
3489
|
+
litellmProcess.kill?.();
|
|
3080
3490
|
litellmProcess = null;
|
|
3081
3491
|
}
|
|
3082
3492
|
};
|
|
@@ -3089,20 +3499,6 @@ ${input.feedback}` : reviewContext;
|
|
|
3089
3499
|
cleanupLitellm();
|
|
3090
3500
|
process.exit(143);
|
|
3091
3501
|
});
|
|
3092
|
-
if (config.agent.litellmUrl) {
|
|
3093
|
-
const proxyRunning = await checkLitellmHealth(config.agent.litellmUrl);
|
|
3094
|
-
if (!proxyRunning) {
|
|
3095
|
-
litellmProcess = await tryStartLitellm(config.agent.litellmUrl, projectDir);
|
|
3096
|
-
if (!litellmProcess) {
|
|
3097
|
-
logger.error("LiteLLM is configured (litellmUrl) but could not be started. Install it with: pip install 'litellm[proxy]'");
|
|
3098
|
-
process.exit(1);
|
|
3099
|
-
}
|
|
3100
|
-
} else {
|
|
3101
|
-
logger.info(`LiteLLM proxy already running at ${config.agent.litellmUrl}`);
|
|
3102
|
-
}
|
|
3103
|
-
process.env.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
|
|
3104
|
-
logger.info(`ANTHROPIC_BASE_URL set to ${config.agent.litellmUrl}`);
|
|
3105
|
-
}
|
|
3106
3502
|
const runners = createRunners(config);
|
|
3107
3503
|
const defaultRunnerName = config.agent.defaultRunner ?? Object.keys(runners)[0] ?? "claude";
|
|
3108
3504
|
const defaultRunner = runners[defaultRunnerName];
|
|
@@ -3148,7 +3544,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
3148
3544
|
}
|
|
3149
3545
|
}
|
|
3150
3546
|
const state = await runPipeline(ctx);
|
|
3151
|
-
const files =
|
|
3547
|
+
const files = fs21.readdirSync(taskDir);
|
|
3152
3548
|
console.log(`
|
|
3153
3549
|
Artifacts in ${taskDir}:`);
|
|
3154
3550
|
for (const f of files) {
|
|
@@ -3194,6 +3590,7 @@ var init_entry = __esm({
|
|
|
3194
3590
|
init_litellm();
|
|
3195
3591
|
init_task_resolution();
|
|
3196
3592
|
init_task_state();
|
|
3593
|
+
init_config();
|
|
3197
3594
|
main().catch(async (err) => {
|
|
3198
3595
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3199
3596
|
console.error(msg);
|
|
@@ -3211,15 +3608,15 @@ var init_entry = __esm({
|
|
|
3211
3608
|
});
|
|
3212
3609
|
|
|
3213
3610
|
// src/bin/cli.ts
|
|
3214
|
-
import * as
|
|
3215
|
-
import * as
|
|
3611
|
+
import * as fs22 from "fs";
|
|
3612
|
+
import * as path21 from "path";
|
|
3216
3613
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
3217
3614
|
import { fileURLToPath } from "url";
|
|
3218
|
-
var __dirname =
|
|
3219
|
-
var PKG_ROOT =
|
|
3615
|
+
var __dirname = path21.dirname(fileURLToPath(import.meta.url));
|
|
3616
|
+
var PKG_ROOT = path21.resolve(__dirname, "..", "..");
|
|
3220
3617
|
function getVersion() {
|
|
3221
|
-
const pkgPath =
|
|
3222
|
-
const pkg = JSON.parse(
|
|
3618
|
+
const pkgPath = path21.join(PKG_ROOT, "package.json");
|
|
3619
|
+
const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
|
|
3223
3620
|
return pkg.version;
|
|
3224
3621
|
}
|
|
3225
3622
|
function checkCommand2(name, args2, fix) {
|
|
@@ -3235,7 +3632,7 @@ function checkCommand2(name, args2, fix) {
|
|
|
3235
3632
|
}
|
|
3236
3633
|
}
|
|
3237
3634
|
function checkFile(filePath, description, fix) {
|
|
3238
|
-
if (
|
|
3635
|
+
if (fs22.existsSync(filePath)) {
|
|
3239
3636
|
return { name: description, ok: true, detail: filePath };
|
|
3240
3637
|
}
|
|
3241
3638
|
return { name: description, ok: false, fix };
|
|
@@ -3307,9 +3704,9 @@ function checkGhSecret(repoSlug, secretName) {
|
|
|
3307
3704
|
}
|
|
3308
3705
|
function detectBasicConfig(cwd) {
|
|
3309
3706
|
let pm = "pnpm";
|
|
3310
|
-
if (
|
|
3311
|
-
else if (
|
|
3312
|
-
else if (!
|
|
3707
|
+
if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) pm = "yarn";
|
|
3708
|
+
else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) pm = "bun";
|
|
3709
|
+
else if (!fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml")) && fs22.existsSync(path21.join(cwd, "package-lock.json"))) pm = "npm";
|
|
3313
3710
|
let defaultBranch = "main";
|
|
3314
3711
|
try {
|
|
3315
3712
|
const ref = execFileSync11("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
@@ -3352,7 +3749,7 @@ function detectBasicConfig(cwd) {
|
|
|
3352
3749
|
function buildConfig(cwd, basic) {
|
|
3353
3750
|
const pkg = (() => {
|
|
3354
3751
|
try {
|
|
3355
|
-
return JSON.parse(
|
|
3752
|
+
return JSON.parse(fs22.readFileSync(path21.join(cwd, "package.json"), "utf-8"));
|
|
3356
3753
|
} catch {
|
|
3357
3754
|
return {};
|
|
3358
3755
|
}
|
|
@@ -3392,35 +3789,35 @@ function initCommand(opts) {
|
|
|
3392
3789
|
console.log(`Project: ${cwd}
|
|
3393
3790
|
`);
|
|
3394
3791
|
console.log("\u2500\u2500 Files \u2500\u2500");
|
|
3395
|
-
const templatesDir =
|
|
3792
|
+
const templatesDir = path21.join(PKG_ROOT, "templates");
|
|
3396
3793
|
const basic = detectBasicConfig(cwd);
|
|
3397
|
-
const workflowSrc =
|
|
3398
|
-
const workflowDest =
|
|
3399
|
-
if (!
|
|
3794
|
+
const workflowSrc = path21.join(templatesDir, "kody.yml");
|
|
3795
|
+
const workflowDest = path21.join(cwd, ".github", "workflows", "kody.yml");
|
|
3796
|
+
if (!fs22.existsSync(workflowSrc)) {
|
|
3400
3797
|
console.error(" \u2717 Template kody.yml not found in package");
|
|
3401
3798
|
process.exit(1);
|
|
3402
3799
|
}
|
|
3403
|
-
if (
|
|
3800
|
+
if (fs22.existsSync(workflowDest) && !opts.force) {
|
|
3404
3801
|
console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
|
|
3405
3802
|
} else {
|
|
3406
|
-
|
|
3407
|
-
|
|
3803
|
+
fs22.mkdirSync(path21.dirname(workflowDest), { recursive: true });
|
|
3804
|
+
fs22.copyFileSync(workflowSrc, workflowDest);
|
|
3408
3805
|
console.log(" \u2713 .github/workflows/kody.yml");
|
|
3409
3806
|
}
|
|
3410
|
-
const configDest =
|
|
3411
|
-
if (!
|
|
3807
|
+
const configDest = path21.join(cwd, "kody.config.json");
|
|
3808
|
+
if (!fs22.existsSync(configDest) || opts.force) {
|
|
3412
3809
|
const config = buildConfig(cwd, basic);
|
|
3413
|
-
|
|
3810
|
+
fs22.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
|
|
3414
3811
|
console.log(" \u2713 kody.config.json (auto-configured)");
|
|
3415
3812
|
} else {
|
|
3416
3813
|
console.log(" \u25CB kody.config.json (exists)");
|
|
3417
3814
|
}
|
|
3418
|
-
const gitignorePath =
|
|
3419
|
-
if (
|
|
3420
|
-
const content =
|
|
3815
|
+
const gitignorePath = path21.join(cwd, ".gitignore");
|
|
3816
|
+
if (fs22.existsSync(gitignorePath)) {
|
|
3817
|
+
const content = fs22.readFileSync(gitignorePath, "utf-8");
|
|
3421
3818
|
if (content.includes(".tasks/")) {
|
|
3422
3819
|
const updated = content.replace(/\n?\.tasks\/\n?/g, "\n");
|
|
3423
|
-
|
|
3820
|
+
fs22.writeFileSync(gitignorePath, updated);
|
|
3424
3821
|
console.log(" \u2713 .gitignore (removed legacy .tasks/ \u2014 tasks now committed in .kody/tasks/)");
|
|
3425
3822
|
} else {
|
|
3426
3823
|
console.log(" \u25CB .gitignore (ok)");
|
|
@@ -3431,7 +3828,7 @@ function initCommand(opts) {
|
|
|
3431
3828
|
checkCommand2("gh", ["--version"], "Install: https://cli.github.com"),
|
|
3432
3829
|
checkCommand2("git", ["--version"], "Install git"),
|
|
3433
3830
|
checkCommand2("node", ["--version"], "Install Node.js >= 22"),
|
|
3434
|
-
checkFile(
|
|
3831
|
+
checkFile(path21.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
|
|
3435
3832
|
];
|
|
3436
3833
|
for (const c of checks) {
|
|
3437
3834
|
if (c.ok) {
|
|
@@ -3509,9 +3906,9 @@ function initCommand(opts) {
|
|
|
3509
3906
|
}
|
|
3510
3907
|
}
|
|
3511
3908
|
console.log("\n\u2500\u2500 Config \u2500\u2500");
|
|
3512
|
-
if (
|
|
3909
|
+
if (fs22.existsSync(configDest)) {
|
|
3513
3910
|
try {
|
|
3514
|
-
const config = JSON.parse(
|
|
3911
|
+
const config = JSON.parse(fs22.readFileSync(configDest, "utf-8"));
|
|
3515
3912
|
const configChecks = [];
|
|
3516
3913
|
if (config.github?.owner && config.github?.repo) {
|
|
3517
3914
|
configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
|
|
@@ -3541,10 +3938,10 @@ function initCommand(opts) {
|
|
|
3541
3938
|
const filesToCommit = [
|
|
3542
3939
|
".github/workflows/kody.yml",
|
|
3543
3940
|
"kody.config.json"
|
|
3544
|
-
].filter((f) =>
|
|
3941
|
+
].filter((f) => fs22.existsSync(path21.join(cwd, f)));
|
|
3545
3942
|
if (filesToCommit.length > 0) {
|
|
3546
3943
|
try {
|
|
3547
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
3944
|
+
const fullPaths = filesToCommit.map((f) => path21.join(cwd, f));
|
|
3548
3945
|
execFileSync11("npx", ["prettier", "--write", ...fullPaths], {
|
|
3549
3946
|
cwd,
|
|
3550
3947
|
encoding: "utf-8",
|
|
@@ -3606,20 +4003,20 @@ function initCommand(opts) {
|
|
|
3606
4003
|
}
|
|
3607
4004
|
var STEP_STAGES = ["taskify", "plan", "build", "review", "review-fix"];
|
|
3608
4005
|
function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
3609
|
-
const srcDir =
|
|
3610
|
-
const baseDir =
|
|
4006
|
+
const srcDir = path21.join(cwd, "src");
|
|
4007
|
+
const baseDir = fs22.existsSync(srcDir) ? srcDir : cwd;
|
|
3611
4008
|
const results = [];
|
|
3612
4009
|
function walk(dir) {
|
|
3613
4010
|
const entries = [];
|
|
3614
4011
|
try {
|
|
3615
|
-
for (const entry of
|
|
4012
|
+
for (const entry of fs22.readdirSync(dir, { withFileTypes: true })) {
|
|
3616
4013
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
3617
|
-
const full =
|
|
4014
|
+
const full = path21.join(dir, entry.name);
|
|
3618
4015
|
if (entry.isDirectory()) {
|
|
3619
4016
|
entries.push(...walk(full));
|
|
3620
4017
|
} else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
|
|
3621
4018
|
try {
|
|
3622
|
-
const stat =
|
|
4019
|
+
const stat = fs22.statSync(full);
|
|
3623
4020
|
if (stat.size >= 200 && stat.size <= 5e3) {
|
|
3624
4021
|
entries.push({ filePath: full, size: stat.size });
|
|
3625
4022
|
}
|
|
@@ -3633,8 +4030,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
|
3633
4030
|
}
|
|
3634
4031
|
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
3635
4032
|
for (const { filePath } of files) {
|
|
3636
|
-
const rel =
|
|
3637
|
-
const content =
|
|
4033
|
+
const rel = path21.relative(cwd, filePath);
|
|
4034
|
+
const content = fs22.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
3638
4035
|
results.push(`### File: ${rel}
|
|
3639
4036
|
\`\`\`typescript
|
|
3640
4037
|
${content}
|
|
@@ -3646,9 +4043,9 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
3646
4043
|
try {
|
|
3647
4044
|
let repoSlug = "";
|
|
3648
4045
|
try {
|
|
3649
|
-
const configPath =
|
|
3650
|
-
if (
|
|
3651
|
-
const config = JSON.parse(
|
|
4046
|
+
const configPath = path21.join(cwd, "kody.config.json");
|
|
4047
|
+
if (fs22.existsSync(configPath)) {
|
|
4048
|
+
const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
|
|
3652
4049
|
if (config.github?.owner && config.github?.repo) {
|
|
3653
4050
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
3654
4051
|
}
|
|
@@ -3683,8 +4080,8 @@ function bootstrapCommand() {
|
|
|
3683
4080
|
ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
|
|
3684
4081
|
}
|
|
3685
4082
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
3686
|
-
const p =
|
|
3687
|
-
if (
|
|
4083
|
+
const p = path21.join(cwd, rel);
|
|
4084
|
+
if (fs22.existsSync(p)) return fs22.readFileSync(p, "utf-8").slice(0, maxChars);
|
|
3688
4085
|
return null;
|
|
3689
4086
|
};
|
|
3690
4087
|
let repoContext = "";
|
|
@@ -3719,14 +4116,14 @@ ${sampleFiles}
|
|
|
3719
4116
|
|
|
3720
4117
|
`;
|
|
3721
4118
|
try {
|
|
3722
|
-
const topDirs =
|
|
4119
|
+
const topDirs = fs22.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
3723
4120
|
repoContext += `## Top-level directories
|
|
3724
4121
|
${topDirs.join(", ")}
|
|
3725
4122
|
|
|
3726
4123
|
`;
|
|
3727
|
-
const srcDir =
|
|
3728
|
-
if (
|
|
3729
|
-
const srcDirs =
|
|
4124
|
+
const srcDir = path21.join(cwd, "src");
|
|
4125
|
+
if (fs22.existsSync(srcDir)) {
|
|
4126
|
+
const srcDirs = fs22.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
3730
4127
|
if (srcDirs.length > 0) repoContext += `## src/ subdirectories
|
|
3731
4128
|
${srcDirs.join(", ")}
|
|
3732
4129
|
|
|
@@ -3736,17 +4133,17 @@ ${srcDirs.join(", ")}
|
|
|
3736
4133
|
}
|
|
3737
4134
|
const existingFiles = [];
|
|
3738
4135
|
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"]) {
|
|
3739
|
-
if (
|
|
4136
|
+
if (fs22.existsSync(path21.join(cwd, f))) existingFiles.push(f);
|
|
3740
4137
|
}
|
|
3741
4138
|
if (existingFiles.length) repoContext += `## Config files present
|
|
3742
4139
|
${existingFiles.join(", ")}
|
|
3743
4140
|
|
|
3744
4141
|
`;
|
|
3745
4142
|
console.log("\u2500\u2500 Project Memory \u2500\u2500");
|
|
3746
|
-
const memoryDir =
|
|
3747
|
-
|
|
3748
|
-
const archPath =
|
|
3749
|
-
const conventionsPath =
|
|
4143
|
+
const memoryDir = path21.join(cwd, ".kody", "memory");
|
|
4144
|
+
fs22.mkdirSync(memoryDir, { recursive: true });
|
|
4145
|
+
const archPath = path21.join(memoryDir, "architecture.md");
|
|
4146
|
+
const conventionsPath = path21.join(memoryDir, "conventions.md");
|
|
3750
4147
|
const memoryPrompt = `You are analyzing a project to generate documentation for an autonomous SDLC pipeline.
|
|
3751
4148
|
|
|
3752
4149
|
Given this project context, output ONLY a JSON object with EXACTLY this structure:
|
|
@@ -3787,12 +4184,12 @@ ${repoContext}`;
|
|
|
3787
4184
|
const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
3788
4185
|
const parsed = JSON.parse(cleaned);
|
|
3789
4186
|
if (parsed.architecture) {
|
|
3790
|
-
|
|
4187
|
+
fs22.writeFileSync(archPath, parsed.architecture);
|
|
3791
4188
|
const lineCount = parsed.architecture.split("\n").length;
|
|
3792
4189
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
|
|
3793
4190
|
}
|
|
3794
4191
|
if (parsed.conventions) {
|
|
3795
|
-
|
|
4192
|
+
fs22.writeFileSync(conventionsPath, parsed.conventions);
|
|
3796
4193
|
const lineCount = parsed.conventions.split("\n").length;
|
|
3797
4194
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
|
|
3798
4195
|
}
|
|
@@ -3801,34 +4198,34 @@ ${repoContext}`;
|
|
|
3801
4198
|
const detected = detectArchitectureBasic(cwd);
|
|
3802
4199
|
if (detected.length > 0) {
|
|
3803
4200
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3804
|
-
|
|
4201
|
+
fs22.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
3805
4202
|
|
|
3806
4203
|
## Overview
|
|
3807
4204
|
${detected.join("\n")}
|
|
3808
4205
|
`);
|
|
3809
4206
|
console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
|
|
3810
4207
|
}
|
|
3811
|
-
|
|
4208
|
+
fs22.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
3812
4209
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
3813
4210
|
}
|
|
3814
4211
|
console.log("\n\u2500\u2500 Step Files \u2500\u2500");
|
|
3815
|
-
const stepsDir =
|
|
3816
|
-
|
|
3817
|
-
const arch =
|
|
3818
|
-
const conv =
|
|
4212
|
+
const stepsDir = path21.join(cwd, ".kody", "steps");
|
|
4213
|
+
fs22.mkdirSync(stepsDir, { recursive: true });
|
|
4214
|
+
const arch = fs22.existsSync(archPath) ? fs22.readFileSync(archPath, "utf-8") : "";
|
|
4215
|
+
const conv = fs22.existsSync(conventionsPath) ? fs22.readFileSync(conventionsPath, "utf-8") : "";
|
|
3819
4216
|
console.log(" \u23F3 Customizing step files...");
|
|
3820
4217
|
let stepCount = 0;
|
|
3821
4218
|
for (const stage of STEP_STAGES) {
|
|
3822
|
-
const templatePath =
|
|
3823
|
-
if (!
|
|
4219
|
+
const templatePath = path21.join(PKG_ROOT, "prompts", `${stage}.md`);
|
|
4220
|
+
if (!fs22.existsSync(templatePath)) {
|
|
3824
4221
|
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
3825
4222
|
continue;
|
|
3826
4223
|
}
|
|
3827
|
-
const defaultPrompt =
|
|
4224
|
+
const defaultPrompt = fs22.readFileSync(templatePath, "utf-8");
|
|
3828
4225
|
const contextPlaceholder = "{{TASK_CONTEXT}}";
|
|
3829
4226
|
const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
|
|
3830
4227
|
if (placeholderIdx === -1) {
|
|
3831
|
-
|
|
4228
|
+
fs22.copyFileSync(templatePath, path21.join(stepsDir, `${stage}.md`));
|
|
3832
4229
|
stepCount++;
|
|
3833
4230
|
console.log(` \u2713 ${stage}.md`);
|
|
3834
4231
|
continue;
|
|
@@ -3885,12 +4282,12 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
3885
4282
|
let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
3886
4283
|
cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
|
|
3887
4284
|
const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
|
|
3888
|
-
|
|
4285
|
+
fs22.writeFileSync(path21.join(stepsDir, `${stage}.md`), finalPrompt);
|
|
3889
4286
|
stepCount++;
|
|
3890
4287
|
console.log(` \u2713 ${stage}.md`);
|
|
3891
4288
|
} catch {
|
|
3892
4289
|
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
3893
|
-
|
|
4290
|
+
fs22.copyFileSync(templatePath, path21.join(stepsDir, `${stage}.md`));
|
|
3894
4291
|
stepCount++;
|
|
3895
4292
|
}
|
|
3896
4293
|
}
|
|
@@ -3899,16 +4296,16 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
3899
4296
|
const filesToCommit = [
|
|
3900
4297
|
".kody/memory/architecture.md",
|
|
3901
4298
|
".kody/memory/conventions.md"
|
|
3902
|
-
].filter((f) =>
|
|
4299
|
+
].filter((f) => fs22.existsSync(path21.join(cwd, f)));
|
|
3903
4300
|
for (const stage of STEP_STAGES) {
|
|
3904
4301
|
const stepFile = `.kody/steps/${stage}.md`;
|
|
3905
|
-
if (
|
|
4302
|
+
if (fs22.existsSync(path21.join(cwd, stepFile))) {
|
|
3906
4303
|
filesToCommit.push(stepFile);
|
|
3907
4304
|
}
|
|
3908
4305
|
}
|
|
3909
4306
|
if (filesToCommit.length > 0) {
|
|
3910
4307
|
try {
|
|
3911
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
4308
|
+
const fullPaths = filesToCommit.map((f) => path21.join(cwd, f));
|
|
3912
4309
|
for (let pass = 0; pass < 2; pass++) {
|
|
3913
4310
|
execFileSync11("npx", ["prettier", "--write", ...fullPaths], {
|
|
3914
4311
|
cwd,
|
|
@@ -3935,9 +4332,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
3935
4332
|
console.log(` \u2713 Pushed branch: ${branchName}`);
|
|
3936
4333
|
let baseBranch = "main";
|
|
3937
4334
|
try {
|
|
3938
|
-
const configPath =
|
|
3939
|
-
if (
|
|
3940
|
-
const config = JSON.parse(
|
|
4335
|
+
const configPath = path21.join(cwd, "kody.config.json");
|
|
4336
|
+
if (fs22.existsSync(configPath)) {
|
|
4337
|
+
const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
|
|
3941
4338
|
baseBranch = config.git?.defaultBranch ?? "main";
|
|
3942
4339
|
}
|
|
3943
4340
|
} catch {
|
|
@@ -4004,10 +4401,10 @@ Review and merge to activate project-specific pipeline configuration.`, cwd);
|
|
|
4004
4401
|
}
|
|
4005
4402
|
function detectArchitectureBasic(cwd) {
|
|
4006
4403
|
const detected = [];
|
|
4007
|
-
const pkgPath =
|
|
4008
|
-
if (
|
|
4404
|
+
const pkgPath = path21.join(cwd, "package.json");
|
|
4405
|
+
if (fs22.existsSync(pkgPath)) {
|
|
4009
4406
|
try {
|
|
4010
|
-
const pkg = JSON.parse(
|
|
4407
|
+
const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
|
|
4011
4408
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4012
4409
|
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
4013
4410
|
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
@@ -4023,10 +4420,10 @@ function detectArchitectureBasic(cwd) {
|
|
|
4023
4420
|
if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
|
|
4024
4421
|
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
|
|
4025
4422
|
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
4026
|
-
if (
|
|
4027
|
-
else if (
|
|
4028
|
-
else if (
|
|
4029
|
-
else if (
|
|
4423
|
+
if (fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
4424
|
+
else if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
4425
|
+
else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
4426
|
+
else if (fs22.existsSync(path21.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
4030
4427
|
} catch {
|
|
4031
4428
|
}
|
|
4032
4429
|
}
|