@kody-ade/kody-engine-lite 0.1.105 → 0.1.106
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/README.md +1 -1
- package/dist/bin/cli.js +1523 -790
- package/kody.config.schema.json +5 -0
- package/package.json +1 -1
- package/prompts/autofix.md +33 -20
- package/prompts/review-fix.md +12 -8
- package/prompts/taskify.md +19 -7
- package/templates/kody.yml +34 -116
package/dist/bin/cli.js
CHANGED
|
@@ -9,8 +9,250 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/bin/architecture-detection.ts
|
|
13
|
+
import * as fs4 from "fs";
|
|
14
|
+
import * as path3 from "path";
|
|
15
|
+
function detectArchitectureBasic(cwd) {
|
|
16
|
+
const detected = [];
|
|
17
|
+
const pkgPath = path3.join(cwd, "package.json");
|
|
18
|
+
if (fs4.existsSync(pkgPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
21
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
22
|
+
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
23
|
+
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
24
|
+
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
25
|
+
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
26
|
+
else if (allDeps.hono) detected.push(`- Framework: Hono ${allDeps.hono}`);
|
|
27
|
+
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
28
|
+
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
29
|
+
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
30
|
+
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
31
|
+
if (allDeps.prettier) detected.push(`- Formatting: prettier ${allDeps.prettier}`);
|
|
32
|
+
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- ORM: Prisma");
|
|
33
|
+
if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
|
|
34
|
+
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
|
|
35
|
+
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
36
|
+
if (fs4.existsSync(path3.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
37
|
+
else if (fs4.existsSync(path3.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
38
|
+
else if (fs4.existsSync(path3.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
39
|
+
else if (fs4.existsSync(path3.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
40
|
+
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
41
|
+
else detected.push("- Module system: CommonJS");
|
|
42
|
+
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const topDirs = fs4.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
48
|
+
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
const srcDir = path3.join(cwd, "src");
|
|
52
|
+
if (fs4.existsSync(srcDir)) {
|
|
53
|
+
try {
|
|
54
|
+
const srcDirs = fs4.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
55
|
+
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return detected;
|
|
60
|
+
}
|
|
61
|
+
var init_architecture_detection = __esm({
|
|
62
|
+
"src/bin/architecture-detection.ts"() {
|
|
63
|
+
"use strict";
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// src/ci/parse-inputs.ts
|
|
68
|
+
var parse_inputs_exports = {};
|
|
69
|
+
__export(parse_inputs_exports, {
|
|
70
|
+
parseCommentInputs: () => parseCommentInputs,
|
|
71
|
+
runCiParse: () => runCiParse,
|
|
72
|
+
writeOutputs: () => writeOutputs
|
|
73
|
+
});
|
|
74
|
+
import * as fs8 from "fs";
|
|
75
|
+
function generateTimestamp() {
|
|
76
|
+
const now = /* @__PURE__ */ new Date();
|
|
77
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
78
|
+
const y = String(now.getFullYear()).slice(2);
|
|
79
|
+
const m = pad(now.getMonth() + 1);
|
|
80
|
+
const d = pad(now.getDate());
|
|
81
|
+
const H = pad(now.getHours());
|
|
82
|
+
const M = pad(now.getMinutes());
|
|
83
|
+
const S = pad(now.getSeconds());
|
|
84
|
+
return `${y}${m}${d}-${H}${M}${S}`;
|
|
85
|
+
}
|
|
86
|
+
function parseCommentInputs() {
|
|
87
|
+
const triggerType = process.env.TRIGGER_TYPE ?? "dispatch";
|
|
88
|
+
if (triggerType === "dispatch") {
|
|
89
|
+
const taskId2 = process.env.INPUT_TASK_ID ?? "";
|
|
90
|
+
return {
|
|
91
|
+
task_id: taskId2,
|
|
92
|
+
mode: process.env.INPUT_MODE ?? "full",
|
|
93
|
+
from_stage: process.env.INPUT_FROM_STAGE ?? "",
|
|
94
|
+
issue_number: process.env.INPUT_ISSUE_NUMBER ?? "",
|
|
95
|
+
pr_number: "",
|
|
96
|
+
feedback: process.env.INPUT_FEEDBACK ?? "",
|
|
97
|
+
complexity: "",
|
|
98
|
+
ci_run_id: "",
|
|
99
|
+
dry_run: false,
|
|
100
|
+
valid: !!taskId2,
|
|
101
|
+
trigger_type: "dispatch"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const commentBody = (process.env.COMMENT_BODY ?? "").replace(/\r/g, "");
|
|
105
|
+
const issueNumber = process.env.ISSUE_NUMBER ?? "";
|
|
106
|
+
const isPR = !!process.env.ISSUE_IS_PR;
|
|
107
|
+
const kodyMatch = commentBody.match(/(?:@kody|\/kody)\s*(.*)/i);
|
|
108
|
+
if (!kodyMatch) {
|
|
109
|
+
return {
|
|
110
|
+
task_id: "",
|
|
111
|
+
mode: "full",
|
|
112
|
+
from_stage: "",
|
|
113
|
+
issue_number: issueNumber,
|
|
114
|
+
pr_number: "",
|
|
115
|
+
feedback: "",
|
|
116
|
+
complexity: "",
|
|
117
|
+
ci_run_id: "",
|
|
118
|
+
dry_run: false,
|
|
119
|
+
valid: false,
|
|
120
|
+
trigger_type: "comment"
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const argsLine = kodyMatch[1].trim();
|
|
124
|
+
let fromStage = "";
|
|
125
|
+
let feedback = "";
|
|
126
|
+
let complexity = "";
|
|
127
|
+
let dryRun = false;
|
|
128
|
+
let ciRunId = "";
|
|
129
|
+
const fromMatch = argsLine.match(/--from\s+(\S+)/);
|
|
130
|
+
if (fromMatch) fromStage = fromMatch[1];
|
|
131
|
+
const feedbackMatch = argsLine.match(/--feedback\s+"([^"]*)"/);
|
|
132
|
+
if (feedbackMatch) feedback = feedbackMatch[1];
|
|
133
|
+
const complexityMatch = argsLine.match(/--complexity\s+(\S+)/);
|
|
134
|
+
if (complexityMatch) complexity = complexityMatch[1];
|
|
135
|
+
if (/--dry-run/.test(argsLine)) dryRun = true;
|
|
136
|
+
const ciRunIdMatch = argsLine.match(/--ci-run-id\s+(\S+)/);
|
|
137
|
+
if (ciRunIdMatch) ciRunId = ciRunIdMatch[1];
|
|
138
|
+
const positional = argsLine.replace(/--from\s+\S+/g, "").replace(/--feedback\s+"[^"]*"/g, "").replace(/--complexity\s+\S+/g, "").replace(/--dry-run/g, "").replace(/--ci-run-id\s+\S+/g, "").replace(/\s+/g, " ").trim();
|
|
139
|
+
const parts = positional ? positional.split(/\s+/) : [];
|
|
140
|
+
let mode = "full";
|
|
141
|
+
let taskId = "";
|
|
142
|
+
let idx = 0;
|
|
143
|
+
if (parts[idx] && VALID_MODES.includes(parts[idx])) {
|
|
144
|
+
mode = parts[idx];
|
|
145
|
+
idx++;
|
|
146
|
+
}
|
|
147
|
+
if (parts[idx] && !parts[idx].startsWith("--")) {
|
|
148
|
+
taskId = parts[idx];
|
|
149
|
+
idx++;
|
|
150
|
+
} else if (parts[0] && !VALID_MODES.includes(parts[0]) && !parts[0].startsWith("--")) {
|
|
151
|
+
taskId = parts[0];
|
|
152
|
+
}
|
|
153
|
+
const kodyLineIdx = commentBody.search(/(?:@kody|\/kody)/i);
|
|
154
|
+
const afterKodyLine = commentBody.slice(kodyLineIdx);
|
|
155
|
+
const newlineIdx = afterKodyLine.indexOf("\n");
|
|
156
|
+
const bodyAfterCommand = newlineIdx !== -1 ? afterKodyLine.slice(newlineIdx + 1) : "";
|
|
157
|
+
if (mode === "approve") {
|
|
158
|
+
mode = "rerun";
|
|
159
|
+
if (bodyAfterCommand) {
|
|
160
|
+
feedback = bodyAfterCommand;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (mode === "fix") {
|
|
164
|
+
if (bodyAfterCommand) {
|
|
165
|
+
feedback = bodyAfterCommand;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (mode === "fix-ci") {
|
|
169
|
+
if (bodyAfterCommand) {
|
|
170
|
+
feedback = bodyAfterCommand;
|
|
171
|
+
const runIdFromBody = bodyAfterCommand.match(/Run ID:\s*(\d+)/);
|
|
172
|
+
if (runIdFromBody) {
|
|
173
|
+
ciRunId = runIdFromBody[1];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (mode === "bootstrap") {
|
|
178
|
+
taskId = `bootstrap-${generateTimestamp()}`;
|
|
179
|
+
}
|
|
180
|
+
const prNumber = isPR ? issueNumber : "";
|
|
181
|
+
if (mode === "review" && prNumber) {
|
|
182
|
+
taskId = `review-pr-${prNumber}-${generateTimestamp()}`;
|
|
183
|
+
}
|
|
184
|
+
if (!taskId && mode === "full") {
|
|
185
|
+
taskId = `${issueNumber}-${generateTimestamp()}`;
|
|
186
|
+
}
|
|
187
|
+
const modesWithoutTaskId = ["fix", "fix-ci", "status", "review", "resolve", "rerun"];
|
|
188
|
+
const valid = !!taskId || modesWithoutTaskId.includes(mode);
|
|
189
|
+
return {
|
|
190
|
+
task_id: taskId,
|
|
191
|
+
mode,
|
|
192
|
+
from_stage: fromStage,
|
|
193
|
+
issue_number: issueNumber,
|
|
194
|
+
pr_number: prNumber,
|
|
195
|
+
feedback,
|
|
196
|
+
complexity,
|
|
197
|
+
ci_run_id: ciRunId,
|
|
198
|
+
dry_run: dryRun,
|
|
199
|
+
valid,
|
|
200
|
+
trigger_type: "comment"
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function writeOutputs(result) {
|
|
204
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
205
|
+
function output(key, value) {
|
|
206
|
+
if (outputFile) {
|
|
207
|
+
if (value.includes("\n")) {
|
|
208
|
+
fs8.appendFileSync(outputFile, `${key}<<KODY_EOF
|
|
209
|
+
${value}
|
|
210
|
+
KODY_EOF
|
|
211
|
+
`);
|
|
212
|
+
} else {
|
|
213
|
+
fs8.appendFileSync(outputFile, `${key}=${value}
|
|
214
|
+
`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
|
|
218
|
+
console.log(`${key}=${display}`);
|
|
219
|
+
}
|
|
220
|
+
output("task_id", result.task_id);
|
|
221
|
+
output("mode", result.mode);
|
|
222
|
+
output("from_stage", result.from_stage);
|
|
223
|
+
output("issue_number", result.issue_number);
|
|
224
|
+
output("pr_number", result.pr_number);
|
|
225
|
+
output("feedback", result.feedback);
|
|
226
|
+
output("complexity", result.complexity);
|
|
227
|
+
output("ci_run_id", result.ci_run_id);
|
|
228
|
+
output("dry_run", result.dry_run ? "true" : "false");
|
|
229
|
+
output("valid", result.valid ? "true" : "false");
|
|
230
|
+
output("trigger_type", result.trigger_type);
|
|
231
|
+
}
|
|
232
|
+
function runCiParse() {
|
|
233
|
+
const result = parseCommentInputs();
|
|
234
|
+
writeOutputs(result);
|
|
235
|
+
}
|
|
236
|
+
var VALID_MODES;
|
|
237
|
+
var init_parse_inputs = __esm({
|
|
238
|
+
"src/ci/parse-inputs.ts"() {
|
|
239
|
+
"use strict";
|
|
240
|
+
VALID_MODES = [
|
|
241
|
+
"full",
|
|
242
|
+
"rerun",
|
|
243
|
+
"fix",
|
|
244
|
+
"fix-ci",
|
|
245
|
+
"status",
|
|
246
|
+
"approve",
|
|
247
|
+
"review",
|
|
248
|
+
"resolve",
|
|
249
|
+
"bootstrap"
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
12
254
|
// src/agent-runner.ts
|
|
13
|
-
import { spawn, execFileSync } from "child_process";
|
|
255
|
+
import { spawn, execFileSync as execFileSync6 } from "child_process";
|
|
14
256
|
function writeStdin(child, prompt) {
|
|
15
257
|
return new Promise((resolve4, reject) => {
|
|
16
258
|
if (!child.stdin) {
|
|
@@ -82,9 +324,9 @@ async function runSubprocess(command2, args2, prompt, timeout, options) {
|
|
|
82
324
|
${errDetail}`
|
|
83
325
|
};
|
|
84
326
|
}
|
|
85
|
-
function
|
|
327
|
+
function checkCommand2(command2, args2) {
|
|
86
328
|
try {
|
|
87
|
-
|
|
329
|
+
execFileSync6(command2, args2, { timeout: 1e4, stdio: "pipe" });
|
|
88
330
|
return true;
|
|
89
331
|
} catch {
|
|
90
332
|
return false;
|
|
@@ -114,7 +356,7 @@ function createClaudeCodeRunner() {
|
|
|
114
356
|
return runSubprocess("claude", args2, prompt, timeout, options);
|
|
115
357
|
},
|
|
116
358
|
async healthCheck() {
|
|
117
|
-
return
|
|
359
|
+
return checkCommand2("claude", ["--version"]);
|
|
118
360
|
}
|
|
119
361
|
};
|
|
120
362
|
}
|
|
@@ -271,12 +513,105 @@ var init_logger = __esm({
|
|
|
271
513
|
}
|
|
272
514
|
});
|
|
273
515
|
|
|
516
|
+
// src/validators.ts
|
|
517
|
+
function stripFences(content) {
|
|
518
|
+
return content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
519
|
+
}
|
|
520
|
+
function parseJsonSafe(raw, requiredFields) {
|
|
521
|
+
let parsed;
|
|
522
|
+
try {
|
|
523
|
+
parsed = JSON.parse(raw);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
return { ok: false, error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
|
|
526
|
+
}
|
|
527
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
528
|
+
return { ok: false, error: `Expected JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}` };
|
|
529
|
+
}
|
|
530
|
+
if (requiredFields) {
|
|
531
|
+
for (const field of requiredFields) {
|
|
532
|
+
if (!(field in parsed)) {
|
|
533
|
+
return { ok: false, error: `Missing required field: ${field}` };
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return { ok: true, data: parsed };
|
|
538
|
+
}
|
|
539
|
+
function validateTaskJson(content) {
|
|
540
|
+
try {
|
|
541
|
+
const parsed = JSON.parse(stripFences(content));
|
|
542
|
+
for (const field of REQUIRED_TASK_FIELDS) {
|
|
543
|
+
if (!(field in parsed)) {
|
|
544
|
+
return { valid: false, error: `Missing field: ${field}` };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return { valid: true };
|
|
548
|
+
} catch (err) {
|
|
549
|
+
return {
|
|
550
|
+
valid: false,
|
|
551
|
+
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function validatePlanMd(content) {
|
|
556
|
+
if (content.length < 10) {
|
|
557
|
+
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
558
|
+
}
|
|
559
|
+
if (!/^##\s+\w+/m.test(content)) {
|
|
560
|
+
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
561
|
+
}
|
|
562
|
+
return { valid: true };
|
|
563
|
+
}
|
|
564
|
+
function validateReviewMd(content) {
|
|
565
|
+
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
566
|
+
return { valid: true };
|
|
567
|
+
}
|
|
568
|
+
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
569
|
+
}
|
|
570
|
+
var REQUIRED_TASK_FIELDS;
|
|
571
|
+
var init_validators = __esm({
|
|
572
|
+
"src/validators.ts"() {
|
|
573
|
+
"use strict";
|
|
574
|
+
REQUIRED_TASK_FIELDS = [
|
|
575
|
+
"task_type",
|
|
576
|
+
"title",
|
|
577
|
+
"description",
|
|
578
|
+
"scope",
|
|
579
|
+
"risk_level"
|
|
580
|
+
];
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
274
584
|
// src/config.ts
|
|
275
|
-
import * as
|
|
276
|
-
import * as
|
|
585
|
+
import * as fs9 from "fs";
|
|
586
|
+
import * as path7 from "path";
|
|
587
|
+
function resolveStageConfig(config, stageName, modelTier) {
|
|
588
|
+
const stageOverride = config.agent.stages?.[stageName];
|
|
589
|
+
if (stageOverride) return stageOverride;
|
|
590
|
+
if (config.agent.default) return config.agent.default;
|
|
591
|
+
const model = config.agent.modelMap[modelTier];
|
|
592
|
+
if (!model) {
|
|
593
|
+
throw new Error(`No model configured for stage '${stageName}' (tier: ${modelTier}). Set agent.stages.${stageName} or agent.default in kody.config.json`);
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
provider: config.agent.provider ?? "claude",
|
|
597
|
+
model
|
|
598
|
+
};
|
|
599
|
+
}
|
|
277
600
|
function needsLitellmProxy(config) {
|
|
278
601
|
return !!(config.agent.provider && config.agent.provider !== "anthropic");
|
|
279
602
|
}
|
|
603
|
+
function stageNeedsProxy(stageConfig) {
|
|
604
|
+
return stageConfig.provider !== "claude" && stageConfig.provider !== "anthropic";
|
|
605
|
+
}
|
|
606
|
+
function anyStageNeedsProxy(config) {
|
|
607
|
+
if (config.agent.stages) {
|
|
608
|
+
for (const sc of Object.values(config.agent.stages)) {
|
|
609
|
+
if (stageNeedsProxy(sc)) return true;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (config.agent.default && stageNeedsProxy(config.agent.default)) return true;
|
|
613
|
+
return needsLitellmProxy(config);
|
|
614
|
+
}
|
|
280
615
|
function getLitellmUrl() {
|
|
281
616
|
return LITELLM_DEFAULT_URL;
|
|
282
617
|
}
|
|
@@ -290,10 +625,16 @@ function setConfigDir(dir) {
|
|
|
290
625
|
}
|
|
291
626
|
function getProjectConfig() {
|
|
292
627
|
if (_config) return _config;
|
|
293
|
-
const configPath =
|
|
294
|
-
if (
|
|
628
|
+
const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
629
|
+
if (fs9.existsSync(configPath)) {
|
|
295
630
|
try {
|
|
296
|
-
const
|
|
631
|
+
const result = parseJsonSafe(fs9.readFileSync(configPath, "utf-8"));
|
|
632
|
+
if (!result.ok) {
|
|
633
|
+
logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
|
|
634
|
+
_config = { ...DEFAULT_CONFIG };
|
|
635
|
+
return _config;
|
|
636
|
+
}
|
|
637
|
+
const raw = result.data;
|
|
297
638
|
_config = {
|
|
298
639
|
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
299
640
|
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
@@ -305,7 +646,13 @@ function getProjectConfig() {
|
|
|
305
646
|
},
|
|
306
647
|
timeouts: raw.timeouts ?? void 0,
|
|
307
648
|
contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
|
|
308
|
-
mcp: raw.mcp ? {
|
|
649
|
+
mcp: raw.mcp ? {
|
|
650
|
+
servers: {},
|
|
651
|
+
stages: ["build", "verify", "review", "review-fix"],
|
|
652
|
+
...raw.mcp,
|
|
653
|
+
// Auto-enable when devServer is configured (user can still set enabled: false to override)
|
|
654
|
+
enabled: raw.mcp.enabled ?? !!raw.mcp.devServer
|
|
655
|
+
} : void 0
|
|
309
656
|
};
|
|
310
657
|
} catch {
|
|
311
658
|
logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
|
|
@@ -321,6 +668,7 @@ var init_config = __esm({
|
|
|
321
668
|
"src/config.ts"() {
|
|
322
669
|
"use strict";
|
|
323
670
|
init_logger();
|
|
671
|
+
init_validators();
|
|
324
672
|
DEFAULT_CONFIG = {
|
|
325
673
|
quality: {
|
|
326
674
|
typecheck: "pnpm -s tsc --noEmit",
|
|
@@ -354,7 +702,7 @@ var init_config = __esm({
|
|
|
354
702
|
});
|
|
355
703
|
|
|
356
704
|
// src/git-utils.ts
|
|
357
|
-
import { execFileSync as
|
|
705
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
358
706
|
function getHookSafeEnv() {
|
|
359
707
|
if (!_hookSafeEnv) {
|
|
360
708
|
_hookSafeEnv = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
|
|
@@ -362,7 +710,7 @@ function getHookSafeEnv() {
|
|
|
362
710
|
return _hookSafeEnv;
|
|
363
711
|
}
|
|
364
712
|
function git(args2, options) {
|
|
365
|
-
return
|
|
713
|
+
return execFileSync7("git", args2, {
|
|
366
714
|
encoding: "utf-8",
|
|
367
715
|
timeout: options?.timeout ?? 3e4,
|
|
368
716
|
cwd: options?.cwd,
|
|
@@ -417,8 +765,9 @@ function ensureFeatureBranch(issueNumber, title, cwd) {
|
|
|
417
765
|
}
|
|
418
766
|
try {
|
|
419
767
|
git(["fetch", "origin"], { cwd, timeout: 3e4 });
|
|
420
|
-
} catch {
|
|
421
|
-
|
|
768
|
+
} catch (err) {
|
|
769
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
770
|
+
logger.warn(` Failed to fetch origin: ${msg}`);
|
|
422
771
|
}
|
|
423
772
|
try {
|
|
424
773
|
git(["rev-parse", "--verify", `origin/${branchName}`], { cwd });
|
|
@@ -450,8 +799,9 @@ function syncWithDefault(cwd, branch) {
|
|
|
450
799
|
if (current === defaultBranch) return;
|
|
451
800
|
try {
|
|
452
801
|
git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
|
|
453
|
-
} catch {
|
|
454
|
-
|
|
802
|
+
} catch (err) {
|
|
803
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
804
|
+
logger.warn(` Failed to fetch latest from origin: ${msg}`);
|
|
455
805
|
return;
|
|
456
806
|
}
|
|
457
807
|
try {
|
|
@@ -460,7 +810,8 @@ function syncWithDefault(cwd, branch) {
|
|
|
460
810
|
} catch {
|
|
461
811
|
try {
|
|
462
812
|
git(["merge", "--abort"], { cwd });
|
|
463
|
-
} catch {
|
|
813
|
+
} catch (abortErr) {
|
|
814
|
+
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
464
815
|
}
|
|
465
816
|
logger.warn(` Merge conflict with origin/${defaultBranch} \u2014 skipping sync`);
|
|
466
817
|
}
|
|
@@ -471,8 +822,9 @@ function mergeDefault(cwd) {
|
|
|
471
822
|
if (current === defaultBranch) return "clean";
|
|
472
823
|
try {
|
|
473
824
|
git(["fetch", "origin", defaultBranch], { cwd, timeout: 3e4 });
|
|
474
|
-
} catch {
|
|
475
|
-
|
|
825
|
+
} catch (err) {
|
|
826
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
827
|
+
logger.warn(` Failed to fetch latest from origin: ${msg}`);
|
|
476
828
|
return "error";
|
|
477
829
|
}
|
|
478
830
|
try {
|
|
@@ -487,7 +839,8 @@ function mergeDefault(cwd) {
|
|
|
487
839
|
}
|
|
488
840
|
try {
|
|
489
841
|
git(["merge", "--abort"], { cwd });
|
|
490
|
-
} catch {
|
|
842
|
+
} catch (abortErr) {
|
|
843
|
+
logger.warn(` Failed to abort merge: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
491
844
|
}
|
|
492
845
|
return "error";
|
|
493
846
|
}
|
|
@@ -511,6 +864,17 @@ function commitAll(message, cwd) {
|
|
|
511
864
|
logger.info(` Committed: ${hash} ${message}`);
|
|
512
865
|
return { success: true, hash, message };
|
|
513
866
|
}
|
|
867
|
+
function getDiffFiles(baseBranch, cwd) {
|
|
868
|
+
try {
|
|
869
|
+
const output = git(["diff", "--name-only", `origin/${baseBranch}...HEAD`], { cwd });
|
|
870
|
+
if (!output) return [];
|
|
871
|
+
return output.split("\n").filter((f) => f && !f.startsWith(".kody/"));
|
|
872
|
+
} catch (err) {
|
|
873
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
874
|
+
logger.warn(` Failed to get diff files: ${msg}`);
|
|
875
|
+
return [];
|
|
876
|
+
}
|
|
877
|
+
}
|
|
514
878
|
function pushBranch(cwd) {
|
|
515
879
|
try {
|
|
516
880
|
git(["push", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
|
|
@@ -532,7 +896,21 @@ var init_git_utils = __esm({
|
|
|
532
896
|
});
|
|
533
897
|
|
|
534
898
|
// src/github-api.ts
|
|
535
|
-
import { execFileSync as
|
|
899
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
900
|
+
function isGhExecError(err) {
|
|
901
|
+
return typeof err === "object" && err !== null;
|
|
902
|
+
}
|
|
903
|
+
function ghErrorMessage(err) {
|
|
904
|
+
if (isGhExecError(err)) {
|
|
905
|
+
const stderr = err.stderr?.toString().trim();
|
|
906
|
+
if (stderr) return stderr;
|
|
907
|
+
}
|
|
908
|
+
return err instanceof Error ? err.message : String(err);
|
|
909
|
+
}
|
|
910
|
+
function isNotFoundError(err) {
|
|
911
|
+
const msg = ghErrorMessage(err).toLowerCase();
|
|
912
|
+
return msg.includes("not found") || msg.includes("no pull requests") || msg.includes("could not resolve");
|
|
913
|
+
}
|
|
536
914
|
function setGhCwd(cwd) {
|
|
537
915
|
_ghCwd = cwd;
|
|
538
916
|
}
|
|
@@ -542,7 +920,7 @@ function ghToken() {
|
|
|
542
920
|
function gh(args2, options) {
|
|
543
921
|
const token = ghToken();
|
|
544
922
|
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
545
|
-
return
|
|
923
|
+
return execFileSync8("gh", args2, {
|
|
546
924
|
encoding: "utf-8",
|
|
547
925
|
timeout: API_TIMEOUT_MS,
|
|
548
926
|
cwd: _ghCwd,
|
|
@@ -560,12 +938,34 @@ function getIssue(issueNumber) {
|
|
|
560
938
|
"--json",
|
|
561
939
|
"body,title"
|
|
562
940
|
]);
|
|
563
|
-
|
|
941
|
+
const parsed = JSON.parse(output);
|
|
942
|
+
if (!parsed || typeof parsed.title !== "string") {
|
|
943
|
+
logger.warn(` Issue #${issueNumber}: unexpected response shape`);
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
return { body: parsed.body ?? "", title: parsed.title };
|
|
564
947
|
} catch (err) {
|
|
565
|
-
|
|
948
|
+
if (isNotFoundError(err)) {
|
|
949
|
+
logger.info(` Issue #${issueNumber} not found`);
|
|
950
|
+
} else {
|
|
951
|
+
logger.error(` Failed to get issue #${issueNumber}: ${ghErrorMessage(err)}`);
|
|
952
|
+
}
|
|
566
953
|
return null;
|
|
567
954
|
}
|
|
568
955
|
}
|
|
956
|
+
function getIssueComments(issueNumber) {
|
|
957
|
+
try {
|
|
958
|
+
const output = gh([
|
|
959
|
+
"api",
|
|
960
|
+
`repos/{owner}/{repo}/issues/${issueNumber}/comments`,
|
|
961
|
+
"--jq",
|
|
962
|
+
"[.[] | {body, created_at}]"
|
|
963
|
+
]);
|
|
964
|
+
return output ? JSON.parse(output) : [];
|
|
965
|
+
} catch {
|
|
966
|
+
return [];
|
|
967
|
+
}
|
|
968
|
+
}
|
|
569
969
|
function getIssueLabels(issueNumber) {
|
|
570
970
|
try {
|
|
571
971
|
const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"]);
|
|
@@ -603,8 +1003,15 @@ function getPRForBranch(branch) {
|
|
|
603
1003
|
"number,url"
|
|
604
1004
|
]);
|
|
605
1005
|
const data = JSON.parse(output);
|
|
1006
|
+
if (typeof data.number !== "number" || typeof data.url !== "string") {
|
|
1007
|
+
logger.warn(` PR for branch ${branch}: unexpected response shape`);
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
606
1010
|
return { number: data.number, url: data.url };
|
|
607
|
-
} catch {
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
if (!isNotFoundError(err)) {
|
|
1013
|
+
logger.warn(` Failed to check PR for branch ${branch}: ${ghErrorMessage(err)}`);
|
|
1014
|
+
}
|
|
608
1015
|
return null;
|
|
609
1016
|
}
|
|
610
1017
|
}
|
|
@@ -642,8 +1049,7 @@ function createPR(head, base, title, body) {
|
|
|
642
1049
|
logger.info(` PR created: ${url}`);
|
|
643
1050
|
return { number, url };
|
|
644
1051
|
} catch (err) {
|
|
645
|
-
const
|
|
646
|
-
const reason = stderr || (err instanceof Error ? err.message : String(err));
|
|
1052
|
+
const reason = ghErrorMessage(err);
|
|
647
1053
|
logger.error(` Failed to create PR: ${reason}`);
|
|
648
1054
|
return null;
|
|
649
1055
|
}
|
|
@@ -714,9 +1120,22 @@ function getPRDetails(prNumber) {
|
|
|
714
1120
|
"title,body,headRefName,baseRefName"
|
|
715
1121
|
]);
|
|
716
1122
|
const data = JSON.parse(output);
|
|
717
|
-
|
|
1123
|
+
if (typeof data.title !== "string" || typeof data.headRefName !== "string") {
|
|
1124
|
+
logger.warn(` PR #${prNumber}: unexpected response shape`);
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
title: data.title,
|
|
1129
|
+
body: data.body ?? "",
|
|
1130
|
+
headBranch: data.headRefName,
|
|
1131
|
+
baseBranch: data.baseRefName ?? "main"
|
|
1132
|
+
};
|
|
718
1133
|
} catch (err) {
|
|
719
|
-
|
|
1134
|
+
if (isNotFoundError(err)) {
|
|
1135
|
+
logger.info(` PR #${prNumber} not found`);
|
|
1136
|
+
} else {
|
|
1137
|
+
logger.error(` Failed to get PR #${prNumber}: ${ghErrorMessage(err)}`);
|
|
1138
|
+
}
|
|
720
1139
|
return null;
|
|
721
1140
|
}
|
|
722
1141
|
}
|
|
@@ -758,7 +1177,7 @@ function getCIFailureLogs(runId, maxLength = 8e3) {
|
|
|
758
1177
|
const prefix = logsOutput.length > maxLength ? "...(earlier output truncated)\n" : "";
|
|
759
1178
|
return `${prefix}${truncated}`;
|
|
760
1179
|
} catch (err) {
|
|
761
|
-
logger.warn(` Failed to get CI failure logs for run ${runId}: ${err}`);
|
|
1180
|
+
logger.warn(` Failed to get CI failure logs for run ${runId}: ${ghErrorMessage(err)}`);
|
|
762
1181
|
return null;
|
|
763
1182
|
}
|
|
764
1183
|
}
|
|
@@ -780,7 +1199,7 @@ function getLatestFailedRunForBranch(branch) {
|
|
|
780
1199
|
]);
|
|
781
1200
|
return output.trim() || null;
|
|
782
1201
|
} catch (err) {
|
|
783
|
-
logger.warn(` Failed to get latest failed run for branch ${branch}: ${err}`);
|
|
1202
|
+
logger.warn(` Failed to get latest failed run for branch ${branch}: ${ghErrorMessage(err)}`);
|
|
784
1203
|
return null;
|
|
785
1204
|
}
|
|
786
1205
|
}
|
|
@@ -861,7 +1280,7 @@ var init_github_api = __esm({
|
|
|
861
1280
|
"use strict";
|
|
862
1281
|
init_logger();
|
|
863
1282
|
API_TIMEOUT_MS = 3e4;
|
|
864
|
-
LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
|
|
1283
|
+
LIFECYCLE_LABELS = ["planning", "building", "review", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
|
|
865
1284
|
KODY_MARKERS = [
|
|
866
1285
|
"Kody Review",
|
|
867
1286
|
"\u{1F916} Generated by Kody",
|
|
@@ -876,15 +1295,22 @@ var init_github_api = __esm({
|
|
|
876
1295
|
});
|
|
877
1296
|
|
|
878
1297
|
// src/pipeline/state.ts
|
|
879
|
-
import * as
|
|
880
|
-
import * as
|
|
1298
|
+
import * as fs10 from "fs";
|
|
1299
|
+
import * as path8 from "path";
|
|
881
1300
|
function loadState(taskId, taskDir) {
|
|
882
|
-
const p =
|
|
883
|
-
if (!
|
|
1301
|
+
const p = path8.join(taskDir, "status.json");
|
|
1302
|
+
if (!fs10.existsSync(p)) return null;
|
|
884
1303
|
try {
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
1304
|
+
const result = parseJsonSafe(
|
|
1305
|
+
fs10.readFileSync(p, "utf-8"),
|
|
1306
|
+
["taskId", "state", "stages", "createdAt", "updatedAt"]
|
|
1307
|
+
);
|
|
1308
|
+
if (!result.ok) {
|
|
1309
|
+
logger.warn(` Corrupt status.json: ${result.error}`);
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
if (result.data.taskId !== taskId) return null;
|
|
1313
|
+
return result.data;
|
|
888
1314
|
} catch {
|
|
889
1315
|
return null;
|
|
890
1316
|
}
|
|
@@ -894,11 +1320,11 @@ function writeState(state, taskDir) {
|
|
|
894
1320
|
...state,
|
|
895
1321
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
896
1322
|
};
|
|
897
|
-
const target =
|
|
1323
|
+
const target = path8.join(taskDir, "status.json");
|
|
898
1324
|
const tmp = target + ".tmp";
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1325
|
+
fs10.writeFileSync(tmp, JSON.stringify(updated, null, 2));
|
|
1326
|
+
fs10.renameSync(tmp, target);
|
|
1327
|
+
return updated;
|
|
902
1328
|
}
|
|
903
1329
|
function initState(taskId) {
|
|
904
1330
|
const stages = {};
|
|
@@ -912,6 +1338,8 @@ var init_state = __esm({
|
|
|
912
1338
|
"src/pipeline/state.ts"() {
|
|
913
1339
|
"use strict";
|
|
914
1340
|
init_definitions();
|
|
1341
|
+
init_validators();
|
|
1342
|
+
init_logger();
|
|
915
1343
|
}
|
|
916
1344
|
});
|
|
917
1345
|
|
|
@@ -936,16 +1364,16 @@ var init_complexity = __esm({
|
|
|
936
1364
|
});
|
|
937
1365
|
|
|
938
1366
|
// src/memory.ts
|
|
939
|
-
import * as
|
|
940
|
-
import * as
|
|
1367
|
+
import * as fs11 from "fs";
|
|
1368
|
+
import * as path9 from "path";
|
|
941
1369
|
function readProjectMemory(projectDir) {
|
|
942
|
-
const memoryDir =
|
|
943
|
-
if (!
|
|
944
|
-
const files =
|
|
1370
|
+
const memoryDir = path9.join(projectDir, ".kody", "memory");
|
|
1371
|
+
if (!fs11.existsSync(memoryDir)) return "";
|
|
1372
|
+
const files = fs11.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
945
1373
|
if (files.length === 0) return "";
|
|
946
1374
|
const sections = [];
|
|
947
1375
|
for (const file of files) {
|
|
948
|
-
const content =
|
|
1376
|
+
const content = fs11.readFileSync(path9.join(memoryDir, file), "utf-8").trim();
|
|
949
1377
|
if (content) {
|
|
950
1378
|
sections.push(`## ${file.replace(".md", "")}
|
|
951
1379
|
${content}`);
|
|
@@ -964,15 +1392,11 @@ var init_memory = __esm({
|
|
|
964
1392
|
});
|
|
965
1393
|
|
|
966
1394
|
// src/context-tiers.ts
|
|
967
|
-
import * as
|
|
968
|
-
import * as
|
|
969
|
-
import * as crypto2 from "crypto";
|
|
1395
|
+
import * as fs12 from "fs";
|
|
1396
|
+
import * as path10 from "path";
|
|
970
1397
|
function estimateTokens(text) {
|
|
971
1398
|
return Math.ceil(text.length / 4);
|
|
972
1399
|
}
|
|
973
|
-
function contentHash(content) {
|
|
974
|
-
return crypto2.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
975
|
-
}
|
|
976
1400
|
function resolveStagePolicy(stageName, stageOverrides) {
|
|
977
1401
|
const defaults = DEFAULT_STAGE_POLICIES[stageName] ?? DEFAULT_STAGE_POLICIES.build;
|
|
978
1402
|
const overrides = stageOverrides?.[stageName];
|
|
@@ -1059,61 +1483,30 @@ function generateL1Json(content) {
|
|
|
1059
1483
|
return content.slice(0, L1_MAX_CHARS);
|
|
1060
1484
|
}
|
|
1061
1485
|
}
|
|
1062
|
-
function
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
try {
|
|
1066
|
-
return JSON.parse(fs4.readFileSync(cachePath, "utf-8"));
|
|
1067
|
-
} catch {
|
|
1068
|
-
return { version: 1, entries: {} };
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
function writeCache(cacheDir, cache) {
|
|
1072
|
-
fs4.mkdirSync(cacheDir, { recursive: true });
|
|
1073
|
-
fs4.writeFileSync(path4.join(cacheDir, "tier-cache.json"), JSON.stringify(cache, null, 2));
|
|
1074
|
-
}
|
|
1075
|
-
function getTieredContent(filePath, content, cacheDir) {
|
|
1076
|
-
const hash = contentHash(content);
|
|
1077
|
-
const key = path4.basename(filePath);
|
|
1078
|
-
const cache = readCache(cacheDir);
|
|
1079
|
-
if (cache.entries[key] && cache.entries[key].hash === hash) {
|
|
1080
|
-
return cache.entries[key];
|
|
1081
|
-
}
|
|
1082
|
-
const tiered = {
|
|
1486
|
+
function getTieredContent(filePath, content) {
|
|
1487
|
+
const key = path10.basename(filePath);
|
|
1488
|
+
return {
|
|
1083
1489
|
source: filePath,
|
|
1084
|
-
hash,
|
|
1085
1490
|
L0: generateL0(content, key),
|
|
1086
1491
|
L1: generateL1(content, key),
|
|
1087
1492
|
L2: content
|
|
1088
1493
|
};
|
|
1089
|
-
cache.entries[key] = tiered;
|
|
1090
|
-
writeCache(cacheDir, cache);
|
|
1091
|
-
return tiered;
|
|
1092
|
-
}
|
|
1093
|
-
function invalidateCache(filePath, cacheDir) {
|
|
1094
|
-
const key = path4.basename(filePath);
|
|
1095
|
-
const cache = readCache(cacheDir);
|
|
1096
|
-
if (cache.entries[key]) {
|
|
1097
|
-
delete cache.entries[key];
|
|
1098
|
-
writeCache(cacheDir, cache);
|
|
1099
|
-
}
|
|
1100
1494
|
}
|
|
1101
1495
|
function selectTier(tiered, tier) {
|
|
1102
1496
|
return tiered[tier];
|
|
1103
1497
|
}
|
|
1104
1498
|
function readProjectMemoryTiered(projectDir, tier) {
|
|
1105
|
-
const memoryDir =
|
|
1106
|
-
if (!
|
|
1107
|
-
const files =
|
|
1499
|
+
const memoryDir = path10.join(projectDir, ".kody", "memory");
|
|
1500
|
+
if (!fs12.existsSync(memoryDir)) return "";
|
|
1501
|
+
const files = fs12.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
|
|
1108
1502
|
if (files.length === 0) return "";
|
|
1109
|
-
const cacheDir = path4.join(memoryDir, ".tiers");
|
|
1110
1503
|
const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
|
|
1111
1504
|
const sections = [];
|
|
1112
1505
|
for (const file of files) {
|
|
1113
|
-
const filePath =
|
|
1114
|
-
const content =
|
|
1506
|
+
const filePath = path10.join(memoryDir, file);
|
|
1507
|
+
const content = fs12.readFileSync(filePath, "utf-8").trim();
|
|
1115
1508
|
if (!content) continue;
|
|
1116
|
-
const tiered = getTieredContent(filePath, content
|
|
1509
|
+
const tiered = getTieredContent(filePath, content);
|
|
1117
1510
|
const selected = selectTier(tiered, tier);
|
|
1118
1511
|
if (selected) {
|
|
1119
1512
|
sections.push(`## ${file.replace(".md", "")}
|
|
@@ -1128,26 +1521,25 @@ ${sections.join("\n\n")}
|
|
|
1128
1521
|
`;
|
|
1129
1522
|
}
|
|
1130
1523
|
function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
|
|
1131
|
-
const cacheDir = path4.join(taskDir, ".tiers");
|
|
1132
1524
|
let context = `## Task Context
|
|
1133
1525
|
`;
|
|
1134
1526
|
context += `Task ID: ${taskId}
|
|
1135
1527
|
`;
|
|
1136
1528
|
context += `Task Directory: ${taskDir}
|
|
1137
1529
|
`;
|
|
1138
|
-
const taskMdPath =
|
|
1139
|
-
if (
|
|
1140
|
-
const content =
|
|
1141
|
-
const selected = selectContent(taskMdPath, content,
|
|
1530
|
+
const taskMdPath = path10.join(taskDir, "task.md");
|
|
1531
|
+
if (fs12.existsSync(taskMdPath)) {
|
|
1532
|
+
const content = fs12.readFileSync(taskMdPath, "utf-8");
|
|
1533
|
+
const selected = selectContent(taskMdPath, content, policy.taskDescription);
|
|
1142
1534
|
const label = tierLabel("Task Description", policy.taskDescription);
|
|
1143
1535
|
context += `
|
|
1144
1536
|
## ${label}
|
|
1145
1537
|
${selected}
|
|
1146
1538
|
`;
|
|
1147
1539
|
}
|
|
1148
|
-
const taskJsonPath =
|
|
1149
|
-
if (
|
|
1150
|
-
const content =
|
|
1540
|
+
const taskJsonPath = path10.join(taskDir, "task.json");
|
|
1541
|
+
if (fs12.existsSync(taskJsonPath)) {
|
|
1542
|
+
const content = fs12.readFileSync(taskJsonPath, "utf-8");
|
|
1151
1543
|
if (policy.taskClassification === "L2") {
|
|
1152
1544
|
try {
|
|
1153
1545
|
const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
|
|
@@ -1163,7 +1555,7 @@ ${selected}
|
|
|
1163
1555
|
} catch {
|
|
1164
1556
|
}
|
|
1165
1557
|
} else {
|
|
1166
|
-
const selected = selectContent(taskJsonPath, content,
|
|
1558
|
+
const selected = selectContent(taskJsonPath, content, policy.taskClassification);
|
|
1167
1559
|
if (selected) {
|
|
1168
1560
|
const label = tierLabel("Task Classification", policy.taskClassification);
|
|
1169
1561
|
context += `
|
|
@@ -1173,30 +1565,30 @@ ${selected}
|
|
|
1173
1565
|
}
|
|
1174
1566
|
}
|
|
1175
1567
|
}
|
|
1176
|
-
const specPath =
|
|
1177
|
-
if (
|
|
1178
|
-
const content =
|
|
1179
|
-
const selected = selectContent(specPath, content,
|
|
1568
|
+
const specPath = path10.join(taskDir, "spec.md");
|
|
1569
|
+
if (fs12.existsSync(specPath)) {
|
|
1570
|
+
const content = fs12.readFileSync(specPath, "utf-8");
|
|
1571
|
+
const selected = selectContent(specPath, content, policy.spec);
|
|
1180
1572
|
const label = tierLabel("Spec", policy.spec);
|
|
1181
1573
|
context += `
|
|
1182
1574
|
## ${label}
|
|
1183
1575
|
${selected}
|
|
1184
1576
|
`;
|
|
1185
1577
|
}
|
|
1186
|
-
const planPath =
|
|
1187
|
-
if (
|
|
1188
|
-
const content =
|
|
1189
|
-
const selected = selectContent(planPath, content,
|
|
1578
|
+
const planPath = path10.join(taskDir, "plan.md");
|
|
1579
|
+
if (fs12.existsSync(planPath)) {
|
|
1580
|
+
const content = fs12.readFileSync(planPath, "utf-8");
|
|
1581
|
+
const selected = selectContent(planPath, content, policy.plan);
|
|
1190
1582
|
const label = tierLabel("Plan", policy.plan);
|
|
1191
1583
|
context += `
|
|
1192
1584
|
## ${label}
|
|
1193
1585
|
${selected}
|
|
1194
1586
|
`;
|
|
1195
1587
|
}
|
|
1196
|
-
const contextMdPath =
|
|
1197
|
-
if (
|
|
1198
|
-
const content =
|
|
1199
|
-
const selected = selectContent(contextMdPath, content,
|
|
1588
|
+
const contextMdPath = path10.join(taskDir, "context.md");
|
|
1589
|
+
if (fs12.existsSync(contextMdPath)) {
|
|
1590
|
+
const content = fs12.readFileSync(contextMdPath, "utf-8");
|
|
1591
|
+
const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
|
|
1200
1592
|
const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
|
|
1201
1593
|
context += `
|
|
1202
1594
|
## ${label}
|
|
@@ -1211,9 +1603,9 @@ ${feedback}
|
|
|
1211
1603
|
}
|
|
1212
1604
|
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
1213
1605
|
}
|
|
1214
|
-
function selectContent(filePath, content,
|
|
1606
|
+
function selectContent(filePath, content, tier) {
|
|
1215
1607
|
if (tier === "L2") return content;
|
|
1216
|
-
const tiered = getTieredContent(filePath, content
|
|
1608
|
+
const tiered = getTieredContent(filePath, content);
|
|
1217
1609
|
return selectTier(tiered, tier);
|
|
1218
1610
|
}
|
|
1219
1611
|
function tierLabel(sectionName, tier) {
|
|
@@ -1281,6 +1673,20 @@ var init_context_tiers = __esm({
|
|
|
1281
1673
|
});
|
|
1282
1674
|
|
|
1283
1675
|
// src/mcp-config.ts
|
|
1676
|
+
function withPlaywrightIfNeeded(mcpConfig, hasUI) {
|
|
1677
|
+
if (!mcpConfig?.enabled || !hasUI) return mcpConfig;
|
|
1678
|
+
const hasPlaywright = Object.keys(mcpConfig.servers).some(
|
|
1679
|
+
(name) => name.toLowerCase().includes("playwright")
|
|
1680
|
+
);
|
|
1681
|
+
if (hasPlaywright) return mcpConfig;
|
|
1682
|
+
return {
|
|
1683
|
+
...mcpConfig,
|
|
1684
|
+
servers: {
|
|
1685
|
+
...mcpConfig.servers,
|
|
1686
|
+
playwright: PLAYWRIGHT_SERVER
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1284
1690
|
function buildMcpConfigJson(mcpConfig) {
|
|
1285
1691
|
if (!mcpConfig?.enabled) return void 0;
|
|
1286
1692
|
if (Object.keys(mcpConfig.servers).length === 0) return void 0;
|
|
@@ -1297,37 +1703,40 @@ function buildMcpConfigJson(mcpConfig) {
|
|
|
1297
1703
|
}
|
|
1298
1704
|
function isMcpEnabledForStage(stageName, mcpConfig) {
|
|
1299
1705
|
if (!mcpConfig?.enabled) return false;
|
|
1300
|
-
if (Object.keys(mcpConfig.servers).length === 0) return false;
|
|
1301
1706
|
const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
|
|
1302
1707
|
return allowedStages.includes(stageName);
|
|
1303
1708
|
}
|
|
1304
|
-
var DEFAULT_MCP_STAGES;
|
|
1709
|
+
var DEFAULT_MCP_STAGES, PLAYWRIGHT_SERVER;
|
|
1305
1710
|
var init_mcp_config = __esm({
|
|
1306
1711
|
"src/mcp-config.ts"() {
|
|
1307
1712
|
"use strict";
|
|
1308
1713
|
DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
|
|
1714
|
+
PLAYWRIGHT_SERVER = {
|
|
1715
|
+
command: "npx",
|
|
1716
|
+
args: ["-y", "@anthropic-ai/mcp-playwright"]
|
|
1717
|
+
};
|
|
1309
1718
|
}
|
|
1310
1719
|
});
|
|
1311
1720
|
|
|
1312
1721
|
// src/context.ts
|
|
1313
|
-
import * as
|
|
1314
|
-
import * as
|
|
1722
|
+
import * as fs13 from "fs";
|
|
1723
|
+
import * as path11 from "path";
|
|
1315
1724
|
function readPromptFile(stageName, projectDir) {
|
|
1316
1725
|
if (projectDir) {
|
|
1317
|
-
const stepFile =
|
|
1318
|
-
if (
|
|
1319
|
-
return
|
|
1726
|
+
const stepFile = path11.join(projectDir, ".kody", "steps", `${stageName}.md`);
|
|
1727
|
+
if (fs13.existsSync(stepFile)) {
|
|
1728
|
+
return fs13.readFileSync(stepFile, "utf-8");
|
|
1320
1729
|
}
|
|
1321
1730
|
console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
|
|
1322
1731
|
}
|
|
1323
1732
|
const scriptDir = new URL(".", import.meta.url).pathname;
|
|
1324
1733
|
const candidates = [
|
|
1325
|
-
|
|
1326
|
-
|
|
1734
|
+
path11.resolve(scriptDir, "..", "prompts", `${stageName}.md`),
|
|
1735
|
+
path11.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
|
|
1327
1736
|
];
|
|
1328
1737
|
for (const candidate of candidates) {
|
|
1329
|
-
if (
|
|
1330
|
-
return
|
|
1738
|
+
if (fs13.existsSync(candidate)) {
|
|
1739
|
+
return fs13.readFileSync(candidate, "utf-8");
|
|
1331
1740
|
}
|
|
1332
1741
|
}
|
|
1333
1742
|
throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
|
|
@@ -1339,18 +1748,18 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
|
|
|
1339
1748
|
`;
|
|
1340
1749
|
context += `Task Directory: ${taskDir}
|
|
1341
1750
|
`;
|
|
1342
|
-
const taskMdPath =
|
|
1343
|
-
if (
|
|
1344
|
-
const taskMd =
|
|
1751
|
+
const taskMdPath = path11.join(taskDir, "task.md");
|
|
1752
|
+
if (fs13.existsSync(taskMdPath)) {
|
|
1753
|
+
const taskMd = fs13.readFileSync(taskMdPath, "utf-8");
|
|
1345
1754
|
context += `
|
|
1346
1755
|
## Task Description
|
|
1347
1756
|
${taskMd}
|
|
1348
1757
|
`;
|
|
1349
1758
|
}
|
|
1350
|
-
const taskJsonPath =
|
|
1351
|
-
if (
|
|
1759
|
+
const taskJsonPath = path11.join(taskDir, "task.json");
|
|
1760
|
+
if (fs13.existsSync(taskJsonPath)) {
|
|
1352
1761
|
try {
|
|
1353
|
-
const taskDef = JSON.parse(
|
|
1762
|
+
const taskDef = JSON.parse(fs13.readFileSync(taskJsonPath, "utf-8"));
|
|
1354
1763
|
context += `
|
|
1355
1764
|
## Task Classification
|
|
1356
1765
|
`;
|
|
@@ -1363,27 +1772,27 @@ ${taskMd}
|
|
|
1363
1772
|
} catch {
|
|
1364
1773
|
}
|
|
1365
1774
|
}
|
|
1366
|
-
const specPath =
|
|
1367
|
-
if (
|
|
1368
|
-
const spec =
|
|
1775
|
+
const specPath = path11.join(taskDir, "spec.md");
|
|
1776
|
+
if (fs13.existsSync(specPath)) {
|
|
1777
|
+
const spec = fs13.readFileSync(specPath, "utf-8");
|
|
1369
1778
|
const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
|
|
1370
1779
|
context += `
|
|
1371
1780
|
## Spec Summary
|
|
1372
1781
|
${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
|
|
1373
1782
|
`;
|
|
1374
1783
|
}
|
|
1375
|
-
const planPath =
|
|
1376
|
-
if (
|
|
1377
|
-
const plan =
|
|
1784
|
+
const planPath = path11.join(taskDir, "plan.md");
|
|
1785
|
+
if (fs13.existsSync(planPath)) {
|
|
1786
|
+
const plan = fs13.readFileSync(planPath, "utf-8");
|
|
1378
1787
|
const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
|
|
1379
1788
|
context += `
|
|
1380
1789
|
## Plan Summary
|
|
1381
1790
|
${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
|
|
1382
1791
|
`;
|
|
1383
1792
|
}
|
|
1384
|
-
const contextMdPath =
|
|
1385
|
-
if (
|
|
1386
|
-
const accumulated =
|
|
1793
|
+
const contextMdPath = path11.join(taskDir, "context.md");
|
|
1794
|
+
if (fs13.existsSync(contextMdPath)) {
|
|
1795
|
+
const accumulated = fs13.readFileSync(contextMdPath, "utf-8");
|
|
1387
1796
|
const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
|
|
1388
1797
|
const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
|
|
1389
1798
|
context += `
|
|
@@ -1399,12 +1808,22 @@ ${feedback}
|
|
|
1399
1808
|
}
|
|
1400
1809
|
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
1401
1810
|
}
|
|
1811
|
+
function inferHasUIFromScope(scope) {
|
|
1812
|
+
return scope.some((filePath) => {
|
|
1813
|
+
const ext = path11.extname(filePath).toLowerCase();
|
|
1814
|
+
if (UI_EXTENSIONS.has(ext)) return true;
|
|
1815
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
1816
|
+
return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1402
1819
|
function taskHasUI(taskDir) {
|
|
1403
|
-
const taskJsonPath =
|
|
1404
|
-
if (!
|
|
1820
|
+
const taskJsonPath = path11.join(taskDir, "task.json");
|
|
1821
|
+
if (!fs13.existsSync(taskJsonPath)) return true;
|
|
1405
1822
|
try {
|
|
1406
|
-
const taskDef = JSON.parse(
|
|
1407
|
-
|
|
1823
|
+
const taskDef = JSON.parse(fs13.readFileSync(taskJsonPath, "utf-8"));
|
|
1824
|
+
const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
|
|
1825
|
+
if (scope.length === 0) return true;
|
|
1826
|
+
return inferHasUIFromScope(scope);
|
|
1408
1827
|
} catch {
|
|
1409
1828
|
return true;
|
|
1410
1829
|
}
|
|
@@ -1523,6 +1942,11 @@ ${prompt}` : prompt;
|
|
|
1523
1942
|
}
|
|
1524
1943
|
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
1525
1944
|
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
1945
|
+
const qaGuidePath = path11.join(projectDir, ".kody", "qa-guide.md");
|
|
1946
|
+
if (fs13.existsSync(qaGuidePath)) {
|
|
1947
|
+
const qaGuide = fs13.readFileSync(qaGuidePath, "utf-8").trim();
|
|
1948
|
+
assembled = assembled + "\n\n" + qaGuide;
|
|
1949
|
+
}
|
|
1526
1950
|
}
|
|
1527
1951
|
return assembled;
|
|
1528
1952
|
}
|
|
@@ -1544,13 +1968,16 @@ ${prompt}` : prompt;
|
|
|
1544
1968
|
}
|
|
1545
1969
|
return assembled;
|
|
1546
1970
|
}
|
|
1971
|
+
function escalateModelTier(currentTier) {
|
|
1972
|
+
return TIER_ESCALATION[currentTier] ?? "strong";
|
|
1973
|
+
}
|
|
1547
1974
|
function resolveModel(modelTier, stageName) {
|
|
1548
1975
|
const config = getProjectConfig();
|
|
1549
1976
|
const mapped = config.agent.modelMap[modelTier];
|
|
1550
1977
|
if (mapped) return mapped;
|
|
1551
1978
|
return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
|
|
1552
1979
|
}
|
|
1553
|
-
var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT;
|
|
1980
|
+
var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
|
|
1554
1981
|
var init_context = __esm({
|
|
1555
1982
|
"src/context.ts"() {
|
|
1556
1983
|
"use strict";
|
|
@@ -1566,69 +1993,43 @@ var init_context = __esm({
|
|
|
1566
1993
|
MAX_TASK_CONTEXT_PLAN = 1500;
|
|
1567
1994
|
MAX_TASK_CONTEXT_SPEC = 2e3;
|
|
1568
1995
|
MAX_ACCUMULATED_CONTEXT = 4e3;
|
|
1996
|
+
UI_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1997
|
+
".tsx",
|
|
1998
|
+
".jsx",
|
|
1999
|
+
".vue",
|
|
2000
|
+
".svelte",
|
|
2001
|
+
".css",
|
|
2002
|
+
".scss",
|
|
2003
|
+
".sass",
|
|
2004
|
+
".less",
|
|
2005
|
+
".html"
|
|
2006
|
+
]);
|
|
2007
|
+
UI_PATH_SEGMENTS = [
|
|
2008
|
+
"/components/",
|
|
2009
|
+
"/pages/",
|
|
2010
|
+
"/layouts/",
|
|
2011
|
+
"/styles/",
|
|
2012
|
+
"/views/"
|
|
2013
|
+
];
|
|
2014
|
+
TIER_ESCALATION = {
|
|
2015
|
+
cheap: "mid",
|
|
2016
|
+
mid: "strong",
|
|
2017
|
+
strong: "strong"
|
|
2018
|
+
};
|
|
1569
2019
|
}
|
|
1570
2020
|
});
|
|
1571
2021
|
|
|
1572
|
-
// src/
|
|
1573
|
-
function
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
}
|
|
1584
|
-
return { valid: true };
|
|
1585
|
-
} catch (err) {
|
|
1586
|
-
return {
|
|
1587
|
-
valid: false,
|
|
1588
|
-
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
1589
|
-
};
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
function validatePlanMd(content) {
|
|
1593
|
-
if (content.length < 10) {
|
|
1594
|
-
return { valid: false, error: "Plan is too short (< 10 chars)" };
|
|
1595
|
-
}
|
|
1596
|
-
if (!/^##\s+\w+/m.test(content)) {
|
|
1597
|
-
return { valid: false, error: "Plan has no markdown h2 sections" };
|
|
1598
|
-
}
|
|
1599
|
-
return { valid: true };
|
|
1600
|
-
}
|
|
1601
|
-
function validateReviewMd(content) {
|
|
1602
|
-
if (/pass/i.test(content) || /fail/i.test(content)) {
|
|
1603
|
-
return { valid: true };
|
|
1604
|
-
}
|
|
1605
|
-
return { valid: false, error: "Review must contain 'pass' or 'fail'" };
|
|
1606
|
-
}
|
|
1607
|
-
var REQUIRED_TASK_FIELDS;
|
|
1608
|
-
var init_validators = __esm({
|
|
1609
|
-
"src/validators.ts"() {
|
|
1610
|
-
"use strict";
|
|
1611
|
-
REQUIRED_TASK_FIELDS = [
|
|
1612
|
-
"task_type",
|
|
1613
|
-
"title",
|
|
1614
|
-
"description",
|
|
1615
|
-
"scope",
|
|
1616
|
-
"risk_level"
|
|
1617
|
-
];
|
|
1618
|
-
}
|
|
1619
|
-
});
|
|
1620
|
-
|
|
1621
|
-
// src/pipeline/runner-selection.ts
|
|
1622
|
-
function getRunnerForStage(ctx, stageName) {
|
|
1623
|
-
const config = getProjectConfig();
|
|
1624
|
-
const runnerName = config.agent.stageRunners?.[stageName] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
1625
|
-
const runner = ctx.runners[runnerName];
|
|
1626
|
-
if (!runner) {
|
|
1627
|
-
throw new Error(
|
|
1628
|
-
`Runner "${runnerName}" not found for stage ${stageName}. Available: ${Object.keys(ctx.runners).join(", ")}`
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
return runner;
|
|
2022
|
+
// src/pipeline/runner-selection.ts
|
|
2023
|
+
function getRunnerForStage(ctx, stageName) {
|
|
2024
|
+
const config = getProjectConfig();
|
|
2025
|
+
const runnerName = config.agent.stageRunners?.[stageName] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
2026
|
+
const runner = ctx.runners[runnerName];
|
|
2027
|
+
if (!runner) {
|
|
2028
|
+
throw new Error(
|
|
2029
|
+
`Runner "${runnerName}" not found for stage ${stageName}. Available: ${Object.keys(ctx.runners).join(", ")}`
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
return runner;
|
|
1632
2033
|
}
|
|
1633
2034
|
var init_runner_selection = __esm({
|
|
1634
2035
|
"src/pipeline/runner-selection.ts"() {
|
|
@@ -1638,8 +2039,8 @@ var init_runner_selection = __esm({
|
|
|
1638
2039
|
});
|
|
1639
2040
|
|
|
1640
2041
|
// src/stages/agent.ts
|
|
1641
|
-
import * as
|
|
1642
|
-
import * as
|
|
2042
|
+
import * as fs14 from "fs";
|
|
2043
|
+
import * as path12 from "path";
|
|
1643
2044
|
function getSessionInfo(stageName, sessions) {
|
|
1644
2045
|
const group = SESSION_GROUP[stageName];
|
|
1645
2046
|
if (!group) return void 0;
|
|
@@ -1669,15 +2070,19 @@ async function executeAgentStage(ctx, def) {
|
|
|
1669
2070
|
return { outcome: "completed", retries: 0 };
|
|
1670
2071
|
}
|
|
1671
2072
|
const prompt = buildFullPrompt(def.name, ctx.taskId, ctx.taskDir, ctx.projectDir, ctx.input.feedback);
|
|
1672
|
-
|
|
2073
|
+
let currentModelTier = def.modelTier;
|
|
1673
2074
|
if (ctx.input.feedback && def.name === "build") {
|
|
1674
2075
|
logger.info(` feedback: ${ctx.input.feedback.slice(0, 200)}${ctx.input.feedback.length > 200 ? "..." : ""}`);
|
|
1675
2076
|
}
|
|
1676
2077
|
const config = getProjectConfig();
|
|
2078
|
+
const sc = resolveStageConfig(config, def.name, def.modelTier);
|
|
2079
|
+
let model = sc.model;
|
|
2080
|
+
const useProxy = stageNeedsProxy(sc);
|
|
2081
|
+
const escalateEnabled = config.agent.escalateOnTimeout !== false;
|
|
1677
2082
|
const runnerName = config.agent.stageRunners?.[def.name] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
|
|
1678
|
-
logger.info(` runner=${runnerName} model=${model} timeout=${def.timeout / 1e3}s`);
|
|
2083
|
+
logger.info(` runner=${runnerName} provider=${sc.provider} model=${model} timeout=${def.timeout / 1e3}s`);
|
|
1679
2084
|
const extraEnv = {};
|
|
1680
|
-
if (
|
|
2085
|
+
if (useProxy) {
|
|
1681
2086
|
extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
|
|
1682
2087
|
}
|
|
1683
2088
|
const sessions = ctx.sessions ?? {};
|
|
@@ -1685,42 +2090,64 @@ async function executeAgentStage(ctx, def) {
|
|
|
1685
2090
|
if (sessionInfo) {
|
|
1686
2091
|
logger.info(` session: ${SESSION_GROUP[def.name]} (${sessionInfo.resumeSession ? "resume" : "new"})`);
|
|
1687
2092
|
}
|
|
1688
|
-
const
|
|
2093
|
+
const mcpForStage = isMcpEnabledForStage(def.name, config.mcp) ? withPlaywrightIfNeeded(config.mcp, taskHasUI(ctx.taskDir)) : void 0;
|
|
2094
|
+
const mcpConfigJson = buildMcpConfigJson(mcpForStage);
|
|
1689
2095
|
if (mcpConfigJson) {
|
|
1690
2096
|
logger.info(` MCP servers enabled for ${def.name}`);
|
|
1691
2097
|
}
|
|
1692
2098
|
const runner = getRunnerForStage(ctx, def.name);
|
|
1693
|
-
const
|
|
2099
|
+
const maxRetries = def.maxRetries ?? 0;
|
|
2100
|
+
let lastResult = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
|
|
1694
2101
|
cwd: ctx.projectDir,
|
|
1695
2102
|
env: extraEnv,
|
|
1696
2103
|
...sessionInfo,
|
|
1697
2104
|
mcpConfigJson
|
|
1698
2105
|
});
|
|
1699
|
-
|
|
1700
|
-
|
|
2106
|
+
let retries = 0;
|
|
2107
|
+
while (lastResult.outcome !== "completed" && retries < maxRetries) {
|
|
2108
|
+
retries++;
|
|
2109
|
+
const isTimeout = lastResult.outcome === "timed_out";
|
|
2110
|
+
if (isTimeout && escalateEnabled) {
|
|
2111
|
+
const nextTier = escalateModelTier(currentModelTier);
|
|
2112
|
+
if (nextTier !== currentModelTier) {
|
|
2113
|
+
logger.info(` Escalating model from ${currentModelTier} to ${nextTier} after timeout`);
|
|
2114
|
+
currentModelTier = nextTier;
|
|
2115
|
+
model = resolveModel(currentModelTier, def.name);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
logger.info(` retry ${retries}/${maxRetries} with model=${model}`);
|
|
2119
|
+
lastResult = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
|
|
2120
|
+
cwd: ctx.projectDir,
|
|
2121
|
+
env: extraEnv,
|
|
2122
|
+
mcpConfigJson
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
if (lastResult.outcome !== "completed") {
|
|
2126
|
+
return { outcome: lastResult.outcome, error: lastResult.error, retries };
|
|
1701
2127
|
}
|
|
2128
|
+
const result = lastResult;
|
|
1702
2129
|
if (def.outputFile && result.output) {
|
|
1703
|
-
|
|
2130
|
+
fs14.writeFileSync(path12.join(ctx.taskDir, def.outputFile), result.output);
|
|
1704
2131
|
}
|
|
1705
2132
|
if (def.outputFile) {
|
|
1706
|
-
const outputPath =
|
|
1707
|
-
if (!
|
|
1708
|
-
const ext =
|
|
1709
|
-
const base =
|
|
1710
|
-
const files =
|
|
2133
|
+
const outputPath = path12.join(ctx.taskDir, def.outputFile);
|
|
2134
|
+
if (!fs14.existsSync(outputPath)) {
|
|
2135
|
+
const ext = path12.extname(def.outputFile);
|
|
2136
|
+
const base = path12.basename(def.outputFile, ext);
|
|
2137
|
+
const files = fs14.readdirSync(ctx.taskDir);
|
|
1711
2138
|
const variant = files.find(
|
|
1712
2139
|
(f) => f.startsWith(base + "-") && f.endsWith(ext)
|
|
1713
2140
|
);
|
|
1714
2141
|
if (variant) {
|
|
1715
|
-
|
|
2142
|
+
fs14.renameSync(path12.join(ctx.taskDir, variant), outputPath);
|
|
1716
2143
|
logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
|
|
1717
2144
|
}
|
|
1718
2145
|
}
|
|
1719
2146
|
}
|
|
1720
2147
|
if (def.outputFile) {
|
|
1721
|
-
const outputPath =
|
|
1722
|
-
if (
|
|
1723
|
-
const content =
|
|
2148
|
+
const outputPath = path12.join(ctx.taskDir, def.outputFile);
|
|
2149
|
+
if (fs14.existsSync(outputPath)) {
|
|
2150
|
+
const content = fs14.readFileSync(outputPath, "utf-8");
|
|
1724
2151
|
const validation = validateStageOutput(def.name, content);
|
|
1725
2152
|
if (!validation.valid) {
|
|
1726
2153
|
if (def.name === "taskify") {
|
|
@@ -1734,7 +2161,7 @@ async function executeAgentStage(ctx, def) {
|
|
|
1734
2161
|
const stripped = stripFences(retryResult.output);
|
|
1735
2162
|
const retryValidation = validateTaskJson(stripped);
|
|
1736
2163
|
if (retryValidation.valid) {
|
|
1737
|
-
|
|
2164
|
+
fs14.writeFileSync(outputPath, retryResult.output);
|
|
1738
2165
|
logger.info(` taskify retry produced valid JSON`);
|
|
1739
2166
|
} else {
|
|
1740
2167
|
logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
|
|
@@ -1745,10 +2172,9 @@ async function executeAgentStage(ctx, def) {
|
|
|
1745
2172
|
description: plainText.slice(0, 500),
|
|
1746
2173
|
scope: [],
|
|
1747
2174
|
risk_level: "low",
|
|
1748
|
-
hasUI: false,
|
|
1749
2175
|
questions: []
|
|
1750
2176
|
}, null, 2);
|
|
1751
|
-
|
|
2177
|
+
fs14.writeFileSync(outputPath, fallback);
|
|
1752
2178
|
logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
|
|
1753
2179
|
}
|
|
1754
2180
|
}
|
|
@@ -1759,10 +2185,10 @@ async function executeAgentStage(ctx, def) {
|
|
|
1759
2185
|
}
|
|
1760
2186
|
}
|
|
1761
2187
|
appendStageContext(ctx.taskDir, def.name, result.output);
|
|
1762
|
-
return { outcome: "completed", outputFile: def.outputFile, retries
|
|
2188
|
+
return { outcome: "completed", outputFile: def.outputFile, retries };
|
|
1763
2189
|
}
|
|
1764
2190
|
function appendStageContext(taskDir, stageName, output) {
|
|
1765
|
-
const contextPath =
|
|
2191
|
+
const contextPath = path12.join(taskDir, "context.md");
|
|
1766
2192
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19);
|
|
1767
2193
|
let summary;
|
|
1768
2194
|
if (output && output.trim()) {
|
|
@@ -1775,7 +2201,7 @@ function appendStageContext(taskDir, stageName, output) {
|
|
|
1775
2201
|
### ${stageName} (${timestamp2})
|
|
1776
2202
|
${summary}
|
|
1777
2203
|
`;
|
|
1778
|
-
|
|
2204
|
+
fs14.appendFileSync(contextPath, entry);
|
|
1779
2205
|
}
|
|
1780
2206
|
var SESSION_GROUP;
|
|
1781
2207
|
var init_agent = __esm({
|
|
@@ -1798,7 +2224,7 @@ var init_agent = __esm({
|
|
|
1798
2224
|
});
|
|
1799
2225
|
|
|
1800
2226
|
// src/verify-runner.ts
|
|
1801
|
-
import { execFileSync as
|
|
2227
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
1802
2228
|
function isExecError(err) {
|
|
1803
2229
|
return typeof err === "object" && err !== null;
|
|
1804
2230
|
}
|
|
@@ -1834,7 +2260,7 @@ function runCommand(cmd, cwd, timeout) {
|
|
|
1834
2260
|
return { success: true, output: "", timedOut: false };
|
|
1835
2261
|
}
|
|
1836
2262
|
try {
|
|
1837
|
-
const output =
|
|
2263
|
+
const output = execFileSync9(parts[0], parts.slice(1), {
|
|
1838
2264
|
cwd,
|
|
1839
2265
|
timeout,
|
|
1840
2266
|
encoding: "utf-8",
|
|
@@ -1905,7 +2331,7 @@ var init_verify_runner = __esm({
|
|
|
1905
2331
|
});
|
|
1906
2332
|
|
|
1907
2333
|
// src/observer.ts
|
|
1908
|
-
import { execFileSync as
|
|
2334
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
1909
2335
|
async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, model, options) {
|
|
1910
2336
|
const context = [
|
|
1911
2337
|
`Stage: ${stageName}`,
|
|
@@ -1930,42 +2356,48 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
|
|
|
1930
2356
|
);
|
|
1931
2357
|
if (result.outcome === "completed" && result.output) {
|
|
1932
2358
|
const cleaned = result.output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
1933
|
-
const
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2359
|
+
const parseResult = parseJsonSafe(cleaned, ["classification"]);
|
|
2360
|
+
if (parseResult.ok) {
|
|
2361
|
+
const { data } = parseResult;
|
|
2362
|
+
const validClassifications = [
|
|
2363
|
+
"fixable",
|
|
2364
|
+
"infrastructure",
|
|
2365
|
+
"pre-existing",
|
|
2366
|
+
"retry",
|
|
2367
|
+
"abort"
|
|
2368
|
+
];
|
|
2369
|
+
if (validClassifications.includes(data.classification)) {
|
|
2370
|
+
logger.info(` Diagnosis: ${data.classification} \u2014 ${data.reason}`);
|
|
2371
|
+
return {
|
|
2372
|
+
classification: data.classification,
|
|
2373
|
+
reason: data.reason ?? "Unknown reason",
|
|
2374
|
+
resolution: data.resolution ?? ""
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
logger.warn(` Diagnosis returned invalid classification: ${data.classification}`);
|
|
2378
|
+
} else {
|
|
2379
|
+
logger.warn(` Diagnosis JSON invalid: ${parseResult.error}`);
|
|
1948
2380
|
}
|
|
1949
2381
|
}
|
|
1950
2382
|
} catch (err) {
|
|
1951
2383
|
logger.warn(` Diagnosis error: ${err instanceof Error ? err.message : err}`);
|
|
1952
2384
|
}
|
|
1953
|
-
logger.warn(" Diagnosis failed \u2014 defaulting to
|
|
2385
|
+
logger.warn(" Diagnosis failed \u2014 defaulting to retry");
|
|
1954
2386
|
return {
|
|
1955
|
-
classification: "
|
|
1956
|
-
reason: "Could not diagnose failure",
|
|
2387
|
+
classification: "retry",
|
|
2388
|
+
reason: "Could not diagnose failure \u2014 retrying gate",
|
|
1957
2389
|
resolution: errorOutput.slice(-500)
|
|
1958
2390
|
};
|
|
1959
2391
|
}
|
|
1960
2392
|
function getModifiedFiles(projectDir) {
|
|
1961
2393
|
try {
|
|
1962
|
-
const staged =
|
|
2394
|
+
const staged = execFileSync10("git", ["diff", "--name-only", "--cached"], {
|
|
1963
2395
|
encoding: "utf-8",
|
|
1964
2396
|
cwd: projectDir,
|
|
1965
2397
|
timeout: 5e3,
|
|
1966
2398
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1967
2399
|
}).trim();
|
|
1968
|
-
const unstaged =
|
|
2400
|
+
const unstaged = execFileSync10("git", ["diff", "--name-only"], {
|
|
1969
2401
|
encoding: "utf-8",
|
|
1970
2402
|
cwd: projectDir,
|
|
1971
2403
|
timeout: 5e3,
|
|
@@ -1974,7 +2406,8 @@ function getModifiedFiles(projectDir) {
|
|
|
1974
2406
|
const all = `${staged}
|
|
1975
2407
|
${unstaged}`.split("\n").filter(Boolean);
|
|
1976
2408
|
return [...new Set(all)];
|
|
1977
|
-
} catch {
|
|
2409
|
+
} catch (err) {
|
|
2410
|
+
logger.warn(` Failed to get modified files: ${err instanceof Error ? err.message : String(err)}`);
|
|
1978
2411
|
return [];
|
|
1979
2412
|
}
|
|
1980
2413
|
}
|
|
@@ -1983,6 +2416,7 @@ var init_observer = __esm({
|
|
|
1983
2416
|
"src/observer.ts"() {
|
|
1984
2417
|
"use strict";
|
|
1985
2418
|
init_logger();
|
|
2419
|
+
init_validators();
|
|
1986
2420
|
DIAGNOSIS_PROMPT = `You are a pipeline failure diagnosis agent. Analyze the error and classify it.
|
|
1987
2421
|
|
|
1988
2422
|
Output ONLY valid JSON. No markdown fences. No explanation.
|
|
@@ -2006,8 +2440,8 @@ Error context:
|
|
|
2006
2440
|
});
|
|
2007
2441
|
|
|
2008
2442
|
// src/stages/gate.ts
|
|
2009
|
-
import * as
|
|
2010
|
-
import * as
|
|
2443
|
+
import * as fs15 from "fs";
|
|
2444
|
+
import * as path13 from "path";
|
|
2011
2445
|
function executeGateStage(ctx, def) {
|
|
2012
2446
|
if (ctx.input.dryRun) {
|
|
2013
2447
|
logger.info(` [dry-run] skipping ${def.name}`);
|
|
@@ -2050,7 +2484,7 @@ ${output}
|
|
|
2050
2484
|
`);
|
|
2051
2485
|
}
|
|
2052
2486
|
}
|
|
2053
|
-
|
|
2487
|
+
fs15.writeFileSync(path13.join(ctx.taskDir, "verify.md"), lines.join(""));
|
|
2054
2488
|
return {
|
|
2055
2489
|
outcome: verifyResult.pass ? "completed" : "failed",
|
|
2056
2490
|
retries: 0
|
|
@@ -2065,9 +2499,9 @@ var init_gate = __esm({
|
|
|
2065
2499
|
});
|
|
2066
2500
|
|
|
2067
2501
|
// src/stages/verify.ts
|
|
2068
|
-
import * as
|
|
2069
|
-
import * as
|
|
2070
|
-
import { execFileSync as
|
|
2502
|
+
import * as fs16 from "fs";
|
|
2503
|
+
import * as path14 from "path";
|
|
2504
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
2071
2505
|
async function executeVerifyWithAutofix(ctx, def) {
|
|
2072
2506
|
const maxAttempts = def.maxRetries ?? 2;
|
|
2073
2507
|
for (let attempt = 0; attempt <= maxAttempts; attempt++) {
|
|
@@ -2077,8 +2511,8 @@ async function executeVerifyWithAutofix(ctx, def) {
|
|
|
2077
2511
|
return { ...gateResult, retries: attempt };
|
|
2078
2512
|
}
|
|
2079
2513
|
if (attempt < maxAttempts) {
|
|
2080
|
-
const verifyPath =
|
|
2081
|
-
const errorOutput =
|
|
2514
|
+
const verifyPath = path14.join(ctx.taskDir, "verify.md");
|
|
2515
|
+
const errorOutput = fs16.existsSync(verifyPath) ? fs16.readFileSync(verifyPath, "utf-8") : "Unknown error";
|
|
2082
2516
|
const modifiedFiles = getModifiedFiles(ctx.projectDir);
|
|
2083
2517
|
const defaultRunner = getRunnerForStage(ctx, "taskify");
|
|
2084
2518
|
const diagConfig = getProjectConfig();
|
|
@@ -2121,7 +2555,7 @@ ${diagnosis.resolution}`);
|
|
|
2121
2555
|
const parts = parseCommand(cmd);
|
|
2122
2556
|
if (parts.length === 0) return;
|
|
2123
2557
|
try {
|
|
2124
|
-
|
|
2558
|
+
execFileSync11(parts[0], parts.slice(1), {
|
|
2125
2559
|
stdio: "pipe",
|
|
2126
2560
|
timeout: FIX_COMMAND_TIMEOUT_MS
|
|
2127
2561
|
});
|
|
@@ -2174,18 +2608,18 @@ var init_verify = __esm({
|
|
|
2174
2608
|
});
|
|
2175
2609
|
|
|
2176
2610
|
// src/cli/task-resolution.ts
|
|
2177
|
-
import * as
|
|
2178
|
-
import * as
|
|
2179
|
-
import { execFileSync as
|
|
2611
|
+
import * as fs17 from "fs";
|
|
2612
|
+
import * as path15 from "path";
|
|
2613
|
+
import { execFileSync as execFileSync12 } from "child_process";
|
|
2180
2614
|
function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
2181
|
-
const tasksDir =
|
|
2182
|
-
if (!
|
|
2183
|
-
const allDirs =
|
|
2615
|
+
const tasksDir = path15.join(projectDir, ".kody", "tasks");
|
|
2616
|
+
if (!fs17.existsSync(tasksDir)) return null;
|
|
2617
|
+
const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
2184
2618
|
const prefix = `${issueNumber}-`;
|
|
2185
2619
|
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
2186
2620
|
if (direct) return direct;
|
|
2187
2621
|
try {
|
|
2188
|
-
const branch =
|
|
2622
|
+
const branch = execFileSync12("git", ["branch", "--show-current"], {
|
|
2189
2623
|
encoding: "utf-8",
|
|
2190
2624
|
cwd: projectDir,
|
|
2191
2625
|
timeout: 5e3,
|
|
@@ -2207,15 +2641,39 @@ function generateTaskId() {
|
|
|
2207
2641
|
const pad = (n) => String(n).padStart(2, "0");
|
|
2208
2642
|
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
2209
2643
|
}
|
|
2644
|
+
function resolveTaskIdFromComments(issueNumber) {
|
|
2645
|
+
try {
|
|
2646
|
+
const comments = getIssueComments(issueNumber);
|
|
2647
|
+
const pattern = /pipeline started: `([^`]+)`/;
|
|
2648
|
+
let latestTaskId = null;
|
|
2649
|
+
for (const comment of comments) {
|
|
2650
|
+
const match = comment.body.match(pattern);
|
|
2651
|
+
if (match) {
|
|
2652
|
+
latestTaskId = match[1];
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
return latestTaskId;
|
|
2656
|
+
} catch {
|
|
2657
|
+
return null;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
function resolveTaskIdForCommand(issueNumber, projectDir) {
|
|
2661
|
+
const fromTasks = findLatestTaskForIssue(issueNumber, projectDir);
|
|
2662
|
+
if (fromTasks) return fromTasks;
|
|
2663
|
+
const fromComments = resolveTaskIdFromComments(issueNumber);
|
|
2664
|
+
if (fromComments) return fromComments;
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2210
2667
|
var init_task_resolution = __esm({
|
|
2211
2668
|
"src/cli/task-resolution.ts"() {
|
|
2212
2669
|
"use strict";
|
|
2670
|
+
init_github_api();
|
|
2213
2671
|
}
|
|
2214
2672
|
});
|
|
2215
2673
|
|
|
2216
2674
|
// src/review-standalone.ts
|
|
2217
|
-
import * as
|
|
2218
|
-
import * as
|
|
2675
|
+
import * as fs18 from "fs";
|
|
2676
|
+
import * as path16 from "path";
|
|
2219
2677
|
function resolveReviewTarget(input) {
|
|
2220
2678
|
if (input.prs.length === 0) {
|
|
2221
2679
|
return {
|
|
@@ -2239,17 +2697,35 @@ Or comment on the specific PR: \`@kody review\``
|
|
|
2239
2697
|
}
|
|
2240
2698
|
async function runStandaloneReview(input) {
|
|
2241
2699
|
const taskId = input.taskId ?? `review-${generateTaskId()}`;
|
|
2242
|
-
const taskDir =
|
|
2243
|
-
|
|
2244
|
-
|
|
2700
|
+
const taskDir = path16.join(input.projectDir, ".kody", "tasks", taskId);
|
|
2701
|
+
fs18.mkdirSync(taskDir, { recursive: true });
|
|
2702
|
+
let diffInstruction = "";
|
|
2703
|
+
let filesChangedSection = "";
|
|
2704
|
+
if (input.baseBranch) {
|
|
2705
|
+
diffInstruction = `
|
|
2245
2706
|
|
|
2246
2707
|
## Diff Command
|
|
2247
2708
|
Run: \`git diff origin/${input.baseBranch}...HEAD\` to see the PR changes.
|
|
2248
|
-
Do NOT use bare \`git diff\` \u2014 it shows only uncommitted working tree changes, not the PR diff
|
|
2709
|
+
Do NOT use bare \`git diff\` \u2014 it shows only uncommitted working tree changes, not the PR diff.`;
|
|
2710
|
+
const diffFiles = getDiffFiles(input.baseBranch, input.projectDir);
|
|
2711
|
+
if (diffFiles.length > 0) {
|
|
2712
|
+
logger.info(`[review] Review scope: git diff origin/${input.baseBranch}...HEAD (${diffFiles.length} files)`);
|
|
2713
|
+
const fileList = diffFiles.map((f) => `- ${f}`).join("\n");
|
|
2714
|
+
filesChangedSection = `
|
|
2715
|
+
|
|
2716
|
+
## Files Changed
|
|
2717
|
+
Only review the following ${diffFiles.length} files (these are the files changed in this PR):
|
|
2718
|
+
${fileList}`;
|
|
2719
|
+
} else {
|
|
2720
|
+
logger.info(`[review] Review scope: git diff origin/${input.baseBranch}...HEAD (0 files)`);
|
|
2721
|
+
}
|
|
2722
|
+
} else {
|
|
2723
|
+
logger.warn(`[review] No baseBranch provided \u2014 reviewing all files (no diff scope)`);
|
|
2724
|
+
}
|
|
2249
2725
|
const taskContent = `# ${input.prTitle}
|
|
2250
2726
|
|
|
2251
|
-
${input.prBody ?? ""}${diffInstruction}`;
|
|
2252
|
-
|
|
2727
|
+
${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
|
|
2728
|
+
fs18.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
|
|
2253
2729
|
const reviewDef = STAGES.find((s) => s.name === "review");
|
|
2254
2730
|
const ctx = {
|
|
2255
2731
|
taskId,
|
|
@@ -2271,10 +2747,10 @@ ${input.prBody ?? ""}${diffInstruction}`;
|
|
|
2271
2747
|
error: result.error ?? "Review stage failed"
|
|
2272
2748
|
};
|
|
2273
2749
|
}
|
|
2274
|
-
const reviewPath =
|
|
2750
|
+
const reviewPath = path16.join(taskDir, "review.md");
|
|
2275
2751
|
let reviewContent;
|
|
2276
|
-
if (
|
|
2277
|
-
reviewContent =
|
|
2752
|
+
if (fs18.existsSync(reviewPath)) {
|
|
2753
|
+
reviewContent = fs18.readFileSync(reviewPath, "utf-8");
|
|
2278
2754
|
}
|
|
2279
2755
|
return {
|
|
2280
2756
|
outcome: "completed",
|
|
@@ -2309,12 +2785,13 @@ var init_review_standalone = __esm({
|
|
|
2309
2785
|
init_agent();
|
|
2310
2786
|
init_task_resolution();
|
|
2311
2787
|
init_logger();
|
|
2788
|
+
init_git_utils();
|
|
2312
2789
|
}
|
|
2313
2790
|
});
|
|
2314
2791
|
|
|
2315
2792
|
// src/stages/review.ts
|
|
2316
|
-
import * as
|
|
2317
|
-
import * as
|
|
2793
|
+
import * as fs19 from "fs";
|
|
2794
|
+
import * as path17 from "path";
|
|
2318
2795
|
async function executeReviewWithFix(ctx, def) {
|
|
2319
2796
|
if (ctx.input.dryRun) {
|
|
2320
2797
|
return { outcome: "completed", retries: 0 };
|
|
@@ -2328,11 +2805,11 @@ async function executeReviewWithFix(ctx, def) {
|
|
|
2328
2805
|
if (reviewResult.outcome !== "completed") {
|
|
2329
2806
|
return reviewResult;
|
|
2330
2807
|
}
|
|
2331
|
-
const reviewFile =
|
|
2332
|
-
if (!
|
|
2808
|
+
const reviewFile = path17.join(ctx.taskDir, "review.md");
|
|
2809
|
+
if (!fs19.existsSync(reviewFile)) {
|
|
2333
2810
|
return { outcome: "failed", retries: iteration, error: "review.md not found" };
|
|
2334
2811
|
}
|
|
2335
|
-
const content =
|
|
2812
|
+
const content = fs19.readFileSync(reviewFile, "utf-8");
|
|
2336
2813
|
if (detectReviewVerdict(content) !== "fail") {
|
|
2337
2814
|
return { ...reviewResult, retries: iteration };
|
|
2338
2815
|
}
|
|
@@ -2361,15 +2838,15 @@ var init_review = __esm({
|
|
|
2361
2838
|
});
|
|
2362
2839
|
|
|
2363
2840
|
// src/stages/ship.ts
|
|
2364
|
-
import * as
|
|
2365
|
-
import * as
|
|
2366
|
-
import { execFileSync as
|
|
2841
|
+
import * as fs20 from "fs";
|
|
2842
|
+
import * as path18 from "path";
|
|
2843
|
+
import { execFileSync as execFileSync13 } from "child_process";
|
|
2367
2844
|
function buildPrBody(ctx) {
|
|
2368
2845
|
const sections = [];
|
|
2369
|
-
const taskJsonPath =
|
|
2370
|
-
if (
|
|
2846
|
+
const taskJsonPath = path18.join(ctx.taskDir, "task.json");
|
|
2847
|
+
if (fs20.existsSync(taskJsonPath)) {
|
|
2371
2848
|
try {
|
|
2372
|
-
const raw =
|
|
2849
|
+
const raw = fs20.readFileSync(taskJsonPath, "utf-8");
|
|
2373
2850
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2374
2851
|
const task = JSON.parse(cleaned);
|
|
2375
2852
|
if (task.description) {
|
|
@@ -2388,9 +2865,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
|
|
|
2388
2865
|
} catch {
|
|
2389
2866
|
}
|
|
2390
2867
|
}
|
|
2391
|
-
const reviewPath =
|
|
2392
|
-
if (
|
|
2393
|
-
const review =
|
|
2868
|
+
const reviewPath = path18.join(ctx.taskDir, "review.md");
|
|
2869
|
+
if (fs20.existsSync(reviewPath)) {
|
|
2870
|
+
const review = fs20.readFileSync(reviewPath, "utf-8");
|
|
2394
2871
|
const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
2395
2872
|
if (summaryMatch) {
|
|
2396
2873
|
const summary = summaryMatch[1].trim();
|
|
@@ -2407,14 +2884,14 @@ ${summary}`);
|
|
|
2407
2884
|
**Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
|
|
2408
2885
|
}
|
|
2409
2886
|
}
|
|
2410
|
-
const verifyPath =
|
|
2411
|
-
if (
|
|
2412
|
-
const verify =
|
|
2887
|
+
const verifyPath = path18.join(ctx.taskDir, "verify.md");
|
|
2888
|
+
if (fs20.existsSync(verifyPath)) {
|
|
2889
|
+
const verify = fs20.readFileSync(verifyPath, "utf-8");
|
|
2413
2890
|
if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
|
|
2414
2891
|
}
|
|
2415
|
-
const planPath =
|
|
2416
|
-
if (
|
|
2417
|
-
const plan =
|
|
2892
|
+
const planPath = path18.join(ctx.taskDir, "plan.md");
|
|
2893
|
+
if (fs20.existsSync(planPath)) {
|
|
2894
|
+
const plan = fs20.readFileSync(planPath, "utf-8").trim();
|
|
2418
2895
|
if (plan) {
|
|
2419
2896
|
const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
|
|
2420
2897
|
sections.push(`
|
|
@@ -2434,25 +2911,25 @@ Closes #${ctx.input.issueNumber}`);
|
|
|
2434
2911
|
return sections.join("\n");
|
|
2435
2912
|
}
|
|
2436
2913
|
function executeShipStage(ctx, _def) {
|
|
2437
|
-
const shipPath =
|
|
2914
|
+
const shipPath = path18.join(ctx.taskDir, "ship.md");
|
|
2438
2915
|
if (ctx.input.dryRun) {
|
|
2439
|
-
|
|
2916
|
+
fs20.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
|
|
2440
2917
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2441
2918
|
}
|
|
2442
2919
|
if (ctx.input.local && !ctx.input.issueNumber) {
|
|
2443
|
-
|
|
2920
|
+
fs20.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
|
|
2444
2921
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2445
2922
|
}
|
|
2446
2923
|
try {
|
|
2447
2924
|
const head = getCurrentBranch(ctx.projectDir);
|
|
2448
2925
|
const base = getDefaultBranch(ctx.projectDir);
|
|
2449
2926
|
try {
|
|
2450
|
-
|
|
2927
|
+
execFileSync13("git", ["add", ctx.taskDir], {
|
|
2451
2928
|
cwd: ctx.projectDir,
|
|
2452
2929
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2453
2930
|
stdio: "pipe"
|
|
2454
2931
|
});
|
|
2455
|
-
|
|
2932
|
+
execFileSync13("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
|
|
2456
2933
|
cwd: ctx.projectDir,
|
|
2457
2934
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
2458
2935
|
stdio: "pipe"
|
|
@@ -2466,7 +2943,7 @@ function executeShipStage(ctx, _def) {
|
|
|
2466
2943
|
let repo = config.github?.repo;
|
|
2467
2944
|
if (!owner || !repo) {
|
|
2468
2945
|
try {
|
|
2469
|
-
const remoteUrl =
|
|
2946
|
+
const remoteUrl = execFileSync13("git", ["remote", "get-url", "origin"], {
|
|
2470
2947
|
encoding: "utf-8",
|
|
2471
2948
|
cwd: ctx.projectDir
|
|
2472
2949
|
}).trim();
|
|
@@ -2487,28 +2964,28 @@ function executeShipStage(ctx, _def) {
|
|
|
2487
2964
|
chore: "chore"
|
|
2488
2965
|
};
|
|
2489
2966
|
let prefix = "chore";
|
|
2490
|
-
const taskJsonPath =
|
|
2491
|
-
if (
|
|
2967
|
+
const taskJsonPath = path18.join(ctx.taskDir, "task.json");
|
|
2968
|
+
if (fs20.existsSync(taskJsonPath)) {
|
|
2492
2969
|
try {
|
|
2493
|
-
const raw =
|
|
2970
|
+
const raw = fs20.readFileSync(taskJsonPath, "utf-8");
|
|
2494
2971
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2495
2972
|
const task = JSON.parse(cleaned);
|
|
2496
2973
|
prefix = TYPE_PREFIX[task.task_type] ?? "chore";
|
|
2497
2974
|
} catch {
|
|
2498
2975
|
}
|
|
2499
2976
|
}
|
|
2500
|
-
const taskMdPath =
|
|
2501
|
-
if (
|
|
2502
|
-
const content =
|
|
2977
|
+
const taskMdPath = path18.join(ctx.taskDir, "task.md");
|
|
2978
|
+
if (fs20.existsSync(taskMdPath)) {
|
|
2979
|
+
const content = fs20.readFileSync(taskMdPath, "utf-8");
|
|
2503
2980
|
const heading = content.split("\n").find((l) => l.startsWith("# "));
|
|
2504
2981
|
if (heading) {
|
|
2505
2982
|
title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
|
|
2506
2983
|
}
|
|
2507
2984
|
}
|
|
2508
2985
|
if (title === "Update") {
|
|
2509
|
-
if (
|
|
2986
|
+
if (fs20.existsSync(taskJsonPath)) {
|
|
2510
2987
|
try {
|
|
2511
|
-
const raw =
|
|
2988
|
+
const raw = fs20.readFileSync(taskJsonPath, "utf-8");
|
|
2512
2989
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2513
2990
|
const task = JSON.parse(cleaned);
|
|
2514
2991
|
if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
|
|
@@ -2531,7 +3008,7 @@ function executeShipStage(ctx, _def) {
|
|
|
2531
3008
|
} catch {
|
|
2532
3009
|
}
|
|
2533
3010
|
}
|
|
2534
|
-
|
|
3011
|
+
fs20.writeFileSync(shipPath, `# Ship
|
|
2535
3012
|
|
|
2536
3013
|
Updated existing PR: ${existingPr.url}
|
|
2537
3014
|
PR #${existingPr.number}
|
|
@@ -2552,22 +3029,26 @@ PR #${existingPr.number}
|
|
|
2552
3029
|
} catch {
|
|
2553
3030
|
}
|
|
2554
3031
|
}
|
|
2555
|
-
|
|
3032
|
+
fs20.writeFileSync(shipPath, `# Ship
|
|
2556
3033
|
|
|
2557
3034
|
PR created: ${pr.url}
|
|
2558
3035
|
PR #${pr.number}
|
|
2559
3036
|
`);
|
|
2560
3037
|
} else {
|
|
2561
|
-
|
|
3038
|
+
fs20.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
|
|
2562
3039
|
}
|
|
2563
3040
|
}
|
|
2564
3041
|
return { outcome: "completed", outputFile: "ship.md", retries: 0 };
|
|
2565
3042
|
} catch (err) {
|
|
2566
3043
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2567
|
-
|
|
3044
|
+
try {
|
|
3045
|
+
fs20.writeFileSync(shipPath, `# Ship
|
|
2568
3046
|
|
|
2569
3047
|
Failed: ${msg}
|
|
2570
3048
|
`);
|
|
3049
|
+
} catch {
|
|
3050
|
+
logger.warn(` Failed to write ship.md artifact`);
|
|
3051
|
+
}
|
|
2571
3052
|
return { outcome: "failed", retries: 0, error: msg };
|
|
2572
3053
|
}
|
|
2573
3054
|
}
|
|
@@ -2610,15 +3091,15 @@ var init_executor_registry = __esm({
|
|
|
2610
3091
|
});
|
|
2611
3092
|
|
|
2612
3093
|
// src/pipeline/questions.ts
|
|
2613
|
-
import * as
|
|
2614
|
-
import * as
|
|
3094
|
+
import * as fs21 from "fs";
|
|
3095
|
+
import * as path19 from "path";
|
|
2615
3096
|
function checkForQuestions(ctx, stageName) {
|
|
2616
3097
|
if (ctx.input.local || !ctx.input.issueNumber) return false;
|
|
2617
3098
|
try {
|
|
2618
3099
|
if (stageName === "taskify") {
|
|
2619
|
-
const taskJsonPath =
|
|
2620
|
-
if (!
|
|
2621
|
-
const raw =
|
|
3100
|
+
const taskJsonPath = path19.join(ctx.taskDir, "task.json");
|
|
3101
|
+
if (!fs21.existsSync(taskJsonPath)) return false;
|
|
3102
|
+
const raw = fs21.readFileSync(taskJsonPath, "utf-8");
|
|
2622
3103
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2623
3104
|
const taskJson = JSON.parse(cleaned);
|
|
2624
3105
|
if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
|
|
@@ -2633,9 +3114,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
|
|
|
2633
3114
|
}
|
|
2634
3115
|
}
|
|
2635
3116
|
if (stageName === "plan") {
|
|
2636
|
-
const planPath =
|
|
2637
|
-
if (!
|
|
2638
|
-
const plan =
|
|
3117
|
+
const planPath = path19.join(ctx.taskDir, "plan.md");
|
|
3118
|
+
if (!fs21.existsSync(planPath)) return false;
|
|
3119
|
+
const plan = fs21.readFileSync(planPath, "utf-8");
|
|
2639
3120
|
const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
|
2640
3121
|
if (questionsMatch) {
|
|
2641
3122
|
const questionsText = questionsMatch[1].trim();
|
|
@@ -2664,12 +3145,13 @@ var init_questions = __esm({
|
|
|
2664
3145
|
});
|
|
2665
3146
|
|
|
2666
3147
|
// src/pipeline/hooks.ts
|
|
2667
|
-
import * as
|
|
2668
|
-
import * as
|
|
3148
|
+
import * as fs22 from "fs";
|
|
3149
|
+
import * as path20 from "path";
|
|
2669
3150
|
function applyPreStageLabel(ctx, def) {
|
|
2670
3151
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
2671
3152
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
2672
3153
|
if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
|
|
3154
|
+
if (def.name === "ship") setLifecycleLabel(ctx.input.issueNumber, "shipping");
|
|
2673
3155
|
}
|
|
2674
3156
|
function checkQuestionsAfterStage(ctx, def, state) {
|
|
2675
3157
|
if (def.name !== "taskify" && def.name !== "plan") return null;
|
|
@@ -2702,9 +3184,9 @@ function autoDetectComplexity(ctx, def) {
|
|
|
2702
3184
|
return { complexity, activeStages };
|
|
2703
3185
|
}
|
|
2704
3186
|
try {
|
|
2705
|
-
const taskJsonPath =
|
|
2706
|
-
if (!
|
|
2707
|
-
const raw =
|
|
3187
|
+
const taskJsonPath = path20.join(ctx.taskDir, "task.json");
|
|
3188
|
+
if (!fs22.existsSync(taskJsonPath)) return null;
|
|
3189
|
+
const raw = fs22.readFileSync(taskJsonPath, "utf-8");
|
|
2708
3190
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2709
3191
|
const taskJson = JSON.parse(cleaned);
|
|
2710
3192
|
if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
|
|
@@ -2734,8 +3216,8 @@ function checkRiskGate(ctx, def, state, complexity) {
|
|
|
2734
3216
|
if (ctx.input.dryRun || ctx.input.local) return null;
|
|
2735
3217
|
if (ctx.input.mode === "rerun") return null;
|
|
2736
3218
|
if (!ctx.input.issueNumber) return null;
|
|
2737
|
-
const planPath =
|
|
2738
|
-
const plan =
|
|
3219
|
+
const planPath = path20.join(ctx.taskDir, "plan.md");
|
|
3220
|
+
const plan = fs22.existsSync(planPath) ? fs22.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
|
|
2739
3221
|
try {
|
|
2740
3222
|
postComment(
|
|
2741
3223
|
ctx.input.issueNumber,
|
|
@@ -2802,22 +3284,22 @@ var init_hooks = __esm({
|
|
|
2802
3284
|
});
|
|
2803
3285
|
|
|
2804
3286
|
// src/learning/auto-learn.ts
|
|
2805
|
-
import * as
|
|
2806
|
-
import * as
|
|
3287
|
+
import * as fs23 from "fs";
|
|
3288
|
+
import * as path21 from "path";
|
|
2807
3289
|
function stripAnsi(str) {
|
|
2808
3290
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
2809
3291
|
}
|
|
2810
3292
|
function autoLearn(ctx) {
|
|
2811
3293
|
try {
|
|
2812
|
-
const memoryDir =
|
|
2813
|
-
if (!
|
|
2814
|
-
|
|
3294
|
+
const memoryDir = path21.join(ctx.projectDir, ".kody", "memory");
|
|
3295
|
+
if (!fs23.existsSync(memoryDir)) {
|
|
3296
|
+
fs23.mkdirSync(memoryDir, { recursive: true });
|
|
2815
3297
|
}
|
|
2816
3298
|
const learnings = [];
|
|
2817
3299
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2818
|
-
const verifyPath =
|
|
2819
|
-
if (
|
|
2820
|
-
const verify = stripAnsi(
|
|
3300
|
+
const verifyPath = path21.join(ctx.taskDir, "verify.md");
|
|
3301
|
+
if (fs23.existsSync(verifyPath)) {
|
|
3302
|
+
const verify = stripAnsi(fs23.readFileSync(verifyPath, "utf-8"));
|
|
2821
3303
|
if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
|
|
2822
3304
|
if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
|
|
2823
3305
|
if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
|
|
@@ -2826,18 +3308,18 @@ function autoLearn(ctx) {
|
|
|
2826
3308
|
if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
|
|
2827
3309
|
if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
|
|
2828
3310
|
}
|
|
2829
|
-
const reviewPath =
|
|
2830
|
-
if (
|
|
2831
|
-
const review =
|
|
3311
|
+
const reviewPath = path21.join(ctx.taskDir, "review.md");
|
|
3312
|
+
if (fs23.existsSync(reviewPath)) {
|
|
3313
|
+
const review = fs23.readFileSync(reviewPath, "utf-8");
|
|
2832
3314
|
if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
|
|
2833
3315
|
if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
|
|
2834
3316
|
if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
|
|
2835
3317
|
if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
|
|
2836
3318
|
}
|
|
2837
|
-
const taskJsonPath =
|
|
2838
|
-
if (
|
|
3319
|
+
const taskJsonPath = path21.join(ctx.taskDir, "task.json");
|
|
3320
|
+
if (fs23.existsSync(taskJsonPath)) {
|
|
2839
3321
|
try {
|
|
2840
|
-
const raw = stripAnsi(
|
|
3322
|
+
const raw = stripAnsi(fs23.readFileSync(taskJsonPath, "utf-8"));
|
|
2841
3323
|
const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
2842
3324
|
const task = JSON.parse(cleaned);
|
|
2843
3325
|
if (task.scope && Array.isArray(task.scope)) {
|
|
@@ -2848,135 +3330,48 @@ function autoLearn(ctx) {
|
|
|
2848
3330
|
}
|
|
2849
3331
|
}
|
|
2850
3332
|
if (learnings.length > 0) {
|
|
2851
|
-
const conventionsPath =
|
|
3333
|
+
const conventionsPath = path21.join(memoryDir, "conventions.md");
|
|
2852
3334
|
const entry = `
|
|
2853
3335
|
## Learned ${timestamp2} (task: ${ctx.taskId})
|
|
2854
3336
|
${learnings.join("\n")}
|
|
2855
3337
|
`;
|
|
2856
|
-
|
|
2857
|
-
invalidateCache(conventionsPath, path15.join(memoryDir, ".tiers"));
|
|
3338
|
+
fs23.appendFileSync(conventionsPath, entry);
|
|
2858
3339
|
logger.info(`Auto-learned ${learnings.length} convention(s)`);
|
|
2859
3340
|
}
|
|
2860
|
-
autoLearnDecisions(ctx.taskDir, memoryDir, ctx.taskId, timestamp2);
|
|
2861
3341
|
autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
|
|
2862
3342
|
} catch {
|
|
2863
3343
|
}
|
|
2864
3344
|
}
|
|
2865
3345
|
function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
|
|
2866
|
-
const archPath =
|
|
2867
|
-
if (
|
|
2868
|
-
const detected =
|
|
2869
|
-
const pkgPath = path15.join(projectDir, "package.json");
|
|
2870
|
-
if (fs15.existsSync(pkgPath)) {
|
|
2871
|
-
try {
|
|
2872
|
-
const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
|
|
2873
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
2874
|
-
if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
|
|
2875
|
-
else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
|
|
2876
|
-
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
2877
|
-
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
2878
|
-
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
2879
|
-
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
2880
|
-
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
2881
|
-
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
2882
|
-
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- Database: Prisma ORM");
|
|
2883
|
-
if (allDeps.drizzle || allDeps["drizzle-orm"]) detected.push("- Database: Drizzle ORM");
|
|
2884
|
-
if (allDeps.pg || allDeps.postgres) detected.push("- Database: PostgreSQL");
|
|
2885
|
-
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
|
|
2886
|
-
if (pkg.type === "module") detected.push("- Module system: ESM");
|
|
2887
|
-
else detected.push("- Module system: CommonJS");
|
|
2888
|
-
if (fs15.existsSync(path15.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
2889
|
-
else if (fs15.existsSync(path15.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
2890
|
-
else if (fs15.existsSync(path15.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
2891
|
-
} catch {
|
|
2892
|
-
}
|
|
2893
|
-
}
|
|
2894
|
-
const topDirs = [];
|
|
2895
|
-
try {
|
|
2896
|
-
const entries = fs15.readdirSync(projectDir, { withFileTypes: true });
|
|
2897
|
-
for (const entry of entries) {
|
|
2898
|
-
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2899
|
-
topDirs.push(entry.name);
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
2902
|
-
if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
|
|
2903
|
-
} catch {
|
|
2904
|
-
}
|
|
2905
|
-
const srcDir = path15.join(projectDir, "src");
|
|
2906
|
-
if (fs15.existsSync(srcDir)) {
|
|
2907
|
-
try {
|
|
2908
|
-
const srcEntries = fs15.readdirSync(srcDir, { withFileTypes: true });
|
|
2909
|
-
const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
2910
|
-
if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
|
|
2911
|
-
} catch {
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
3346
|
+
const archPath = path21.join(memoryDir, "architecture.md");
|
|
3347
|
+
if (fs23.existsSync(archPath)) return;
|
|
3348
|
+
const detected = detectArchitectureBasic(projectDir);
|
|
2914
3349
|
if (detected.length > 0) {
|
|
2915
3350
|
const content = `# Architecture (auto-detected ${timestamp2})
|
|
2916
3351
|
|
|
2917
3352
|
## Overview
|
|
2918
3353
|
${detected.join("\n")}
|
|
2919
3354
|
`;
|
|
2920
|
-
|
|
2921
|
-
invalidateCache(archPath, path15.join(memoryDir, ".tiers"));
|
|
3355
|
+
fs23.writeFileSync(archPath, content);
|
|
2922
3356
|
logger.info(`Auto-detected architecture (${detected.length} items)`);
|
|
2923
3357
|
}
|
|
2924
3358
|
}
|
|
2925
|
-
function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
|
|
2926
|
-
const reviewPath = path15.join(taskDir, "review.md");
|
|
2927
|
-
if (!fs15.existsSync(reviewPath)) return;
|
|
2928
|
-
const review = fs15.readFileSync(reviewPath, "utf-8");
|
|
2929
|
-
const decisions = [];
|
|
2930
|
-
const existingPatternRe = /(?:use|follow|reuse|match|adopt)\s+(?:the\s+)?existing\s+(.+?)(?:\.|$)/gim;
|
|
2931
|
-
for (const match of review.matchAll(existingPatternRe)) {
|
|
2932
|
-
decisions.push(`- Use existing ${match[1].trim()}`);
|
|
2933
|
-
}
|
|
2934
|
-
const insteadOfRe = /instead\s+of\s+(.+?),?\s+(?:use|prefer|adopt)\s+(.+?)(?:\.|$)/gim;
|
|
2935
|
-
for (const match of review.matchAll(insteadOfRe)) {
|
|
2936
|
-
decisions.push(`- Prefer ${match[2].trim()} over ${match[1].trim()}`);
|
|
2937
|
-
}
|
|
2938
|
-
const consistentRe = /(?:consistent\s+with|same\s+pattern\s+as|follow\s+the\s+pattern\s+(?:in|from))\s+(.+?)(?:\.|$)/gim;
|
|
2939
|
-
for (const match of review.matchAll(consistentRe)) {
|
|
2940
|
-
decisions.push(`- Follow pattern from ${match[1].trim()}`);
|
|
2941
|
-
}
|
|
2942
|
-
const avoidRe = /(?:don't|do\s+not|never|avoid)\s+(?:use\s+)?(.+?)\s+(?:for|when|in)\s+(.+?)(?:\.|$)/gim;
|
|
2943
|
-
for (const match of review.matchAll(avoidRe)) {
|
|
2944
|
-
decisions.push(`- Avoid ${match[1].trim()} for ${match[2].trim()}`);
|
|
2945
|
-
}
|
|
2946
|
-
if (decisions.length === 0) return;
|
|
2947
|
-
const decisionsPath = path15.join(memoryDir, "decisions.md");
|
|
2948
|
-
let existing = "";
|
|
2949
|
-
if (fs15.existsSync(decisionsPath)) {
|
|
2950
|
-
existing = fs15.readFileSync(decisionsPath, "utf-8");
|
|
2951
|
-
} else {
|
|
2952
|
-
existing = "# Architectural Decisions\n\nDecisions extracted from code reviews. The planning agent MUST follow these.\n";
|
|
2953
|
-
}
|
|
2954
|
-
const newDecisions = decisions.filter((d) => !existing.includes(d));
|
|
2955
|
-
if (newDecisions.length === 0) return;
|
|
2956
|
-
const entry = `
|
|
2957
|
-
## From task ${taskId} (${timestamp2})
|
|
2958
|
-
${newDecisions.join("\n")}
|
|
2959
|
-
`;
|
|
2960
|
-
fs15.appendFileSync(decisionsPath, existing ? entry : existing + entry);
|
|
2961
|
-
invalidateCache(decisionsPath, path15.join(memoryDir, ".tiers"));
|
|
2962
|
-
logger.info(`Auto-learned ${newDecisions.length} architectural decision(s)`);
|
|
2963
|
-
}
|
|
2964
3359
|
var init_auto_learn = __esm({
|
|
2965
3360
|
"src/learning/auto-learn.ts"() {
|
|
2966
3361
|
"use strict";
|
|
2967
3362
|
init_logger();
|
|
2968
|
-
|
|
3363
|
+
init_architecture_detection();
|
|
2969
3364
|
}
|
|
2970
3365
|
});
|
|
2971
3366
|
|
|
2972
3367
|
// src/retrospective.ts
|
|
2973
|
-
import * as
|
|
2974
|
-
import * as
|
|
3368
|
+
import * as fs24 from "fs";
|
|
3369
|
+
import * as path22 from "path";
|
|
2975
3370
|
function readArtifact(taskDir, filename, maxChars) {
|
|
2976
|
-
const p =
|
|
2977
|
-
if (!
|
|
3371
|
+
const p = path22.join(taskDir, filename);
|
|
3372
|
+
if (!fs24.existsSync(p)) return null;
|
|
2978
3373
|
try {
|
|
2979
|
-
const content =
|
|
3374
|
+
const content = fs24.readFileSync(p, "utf-8");
|
|
2980
3375
|
return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
|
|
2981
3376
|
} catch {
|
|
2982
3377
|
return null;
|
|
@@ -3029,13 +3424,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
|
|
|
3029
3424
|
return lines.join("\n");
|
|
3030
3425
|
}
|
|
3031
3426
|
function getLogPath(projectDir) {
|
|
3032
|
-
return
|
|
3427
|
+
return path22.join(projectDir, ".kody", "memory", "observer-log.jsonl");
|
|
3033
3428
|
}
|
|
3034
3429
|
function readPreviousRetrospectives(projectDir, limit = 10) {
|
|
3035
3430
|
const logPath = getLogPath(projectDir);
|
|
3036
|
-
if (!
|
|
3431
|
+
if (!fs24.existsSync(logPath)) return [];
|
|
3037
3432
|
try {
|
|
3038
|
-
const content =
|
|
3433
|
+
const content = fs24.readFileSync(logPath, "utf-8");
|
|
3039
3434
|
const lines = content.split("\n").filter(Boolean);
|
|
3040
3435
|
const entries = [];
|
|
3041
3436
|
const start = Math.max(0, lines.length - limit);
|
|
@@ -3062,11 +3457,11 @@ function formatPreviousEntries(entries) {
|
|
|
3062
3457
|
}
|
|
3063
3458
|
function appendRetrospectiveEntry(projectDir, entry) {
|
|
3064
3459
|
const logPath = getLogPath(projectDir);
|
|
3065
|
-
const dir =
|
|
3066
|
-
if (!
|
|
3067
|
-
|
|
3460
|
+
const dir = path22.dirname(logPath);
|
|
3461
|
+
if (!fs24.existsSync(dir)) {
|
|
3462
|
+
fs24.mkdirSync(dir, { recursive: true });
|
|
3068
3463
|
}
|
|
3069
|
-
|
|
3464
|
+
fs24.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
3070
3465
|
}
|
|
3071
3466
|
async function runRetrospective(ctx, state, pipelineStartTime) {
|
|
3072
3467
|
if (ctx.input.dryRun) return;
|
|
@@ -3176,9 +3571,66 @@ If no pipeline flaw is detected, set "pipelineFlaw" to null.
|
|
|
3176
3571
|
}
|
|
3177
3572
|
});
|
|
3178
3573
|
|
|
3574
|
+
// src/pipeline/summary.ts
|
|
3575
|
+
function formatDuration(ms) {
|
|
3576
|
+
const totalSec = Math.round(ms / 1e3);
|
|
3577
|
+
if (totalSec < 60) return `${totalSec}s`;
|
|
3578
|
+
const min = Math.floor(totalSec / 60);
|
|
3579
|
+
const sec = totalSec % 60;
|
|
3580
|
+
return `${min}m ${sec}s`;
|
|
3581
|
+
}
|
|
3582
|
+
function stageDuration(stage) {
|
|
3583
|
+
if (!stage.startedAt) return "-";
|
|
3584
|
+
const start = new Date(stage.startedAt).getTime();
|
|
3585
|
+
const end = stage.completedAt ? new Date(stage.completedAt).getTime() : Date.now();
|
|
3586
|
+
if (isNaN(start) || isNaN(end)) return "-";
|
|
3587
|
+
return formatDuration(end - start);
|
|
3588
|
+
}
|
|
3589
|
+
function formatPipelineSummary(state, options) {
|
|
3590
|
+
const lines = [];
|
|
3591
|
+
lines.push(`## Pipeline Summary: \`${state.taskId}\``);
|
|
3592
|
+
lines.push("");
|
|
3593
|
+
lines.push("| Stage | Status | Duration | Retries |");
|
|
3594
|
+
lines.push("|-------|--------|----------|---------|");
|
|
3595
|
+
for (const def of STAGES) {
|
|
3596
|
+
const s = state.stages[def.name];
|
|
3597
|
+
if (!s) continue;
|
|
3598
|
+
const status = STATUS_ICONS[s.state] ?? s.state;
|
|
3599
|
+
const duration = stageDuration(s);
|
|
3600
|
+
const retries = s.retries ?? 0;
|
|
3601
|
+
lines.push(`| ${def.name} | ${status} | ${duration} | ${retries} |`);
|
|
3602
|
+
}
|
|
3603
|
+
const totalMs = new Date(state.updatedAt).getTime() - new Date(state.createdAt).getTime();
|
|
3604
|
+
const totalStr = isNaN(totalMs) || totalMs < 0 ? "-" : formatDuration(totalMs);
|
|
3605
|
+
lines.push("");
|
|
3606
|
+
const footerParts = [`**Total:** ${totalStr}`];
|
|
3607
|
+
if (options?.complexity) {
|
|
3608
|
+
footerParts.push(`**Complexity:** ${options.complexity}`);
|
|
3609
|
+
}
|
|
3610
|
+
if (options?.model) {
|
|
3611
|
+
footerParts.push(`**Model:** ${options.model}`);
|
|
3612
|
+
}
|
|
3613
|
+
lines.push(footerParts.join(" | "));
|
|
3614
|
+
return lines.join("\n");
|
|
3615
|
+
}
|
|
3616
|
+
var STATUS_ICONS;
|
|
3617
|
+
var init_summary = __esm({
|
|
3618
|
+
"src/pipeline/summary.ts"() {
|
|
3619
|
+
"use strict";
|
|
3620
|
+
init_definitions();
|
|
3621
|
+
STATUS_ICONS = {
|
|
3622
|
+
completed: "completed",
|
|
3623
|
+
failed: "failed",
|
|
3624
|
+
timeout: "timeout",
|
|
3625
|
+
running: "running",
|
|
3626
|
+
pending: "pending"
|
|
3627
|
+
};
|
|
3628
|
+
}
|
|
3629
|
+
});
|
|
3630
|
+
|
|
3179
3631
|
// src/pipeline.ts
|
|
3180
|
-
import * as
|
|
3181
|
-
import * as
|
|
3632
|
+
import * as fs25 from "fs";
|
|
3633
|
+
import * as path23 from "path";
|
|
3182
3634
|
function ensureFeatureBranchIfNeeded(ctx) {
|
|
3183
3635
|
if (ctx.input.dryRun) return;
|
|
3184
3636
|
if (ctx.input.prNumber) {
|
|
@@ -3191,34 +3643,59 @@ function ensureFeatureBranchIfNeeded(ctx) {
|
|
|
3191
3643
|
}
|
|
3192
3644
|
if (!ctx.input.issueNumber) return;
|
|
3193
3645
|
try {
|
|
3194
|
-
const taskMdPath =
|
|
3195
|
-
const title =
|
|
3646
|
+
const taskMdPath = path23.join(ctx.taskDir, "task.md");
|
|
3647
|
+
const title = fs25.existsSync(taskMdPath) ? fs25.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
|
|
3196
3648
|
ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
|
|
3197
3649
|
syncWithDefault(ctx.projectDir);
|
|
3198
3650
|
} catch (err) {
|
|
3199
|
-
|
|
3651
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3652
|
+
if (msg.includes("not a git repository")) {
|
|
3653
|
+
logger.warn(` Not a git repository \u2014 skipping feature branch setup`);
|
|
3654
|
+
} else {
|
|
3655
|
+
logger.error(` Failed to create/sync feature branch: ${msg}`);
|
|
3656
|
+
throw new Error(`Feature branch setup failed: ${msg}`);
|
|
3657
|
+
}
|
|
3200
3658
|
}
|
|
3201
3659
|
}
|
|
3202
3660
|
function acquireLock(taskDir) {
|
|
3203
|
-
const lockPath =
|
|
3204
|
-
if (
|
|
3661
|
+
const lockPath = path23.join(taskDir, ".lock");
|
|
3662
|
+
if (fs25.existsSync(lockPath)) {
|
|
3205
3663
|
try {
|
|
3206
|
-
const pid = parseInt(
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3664
|
+
const pid = parseInt(fs25.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
3665
|
+
if (!isNaN(pid)) {
|
|
3666
|
+
try {
|
|
3667
|
+
process.kill(pid, 0);
|
|
3668
|
+
throw new Error(`Pipeline already running (PID ${pid})`);
|
|
3669
|
+
} catch (e) {
|
|
3670
|
+
if (e.code !== "ESRCH") throw e;
|
|
3671
|
+
logger.info(` Removing stale lock (PID ${pid} no longer running)`);
|
|
3672
|
+
}
|
|
3673
|
+
} else {
|
|
3674
|
+
logger.warn(` Corrupt lock file (non-numeric PID) \u2014 overwriting`);
|
|
3212
3675
|
}
|
|
3213
3676
|
} catch (e) {
|
|
3214
3677
|
if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
|
|
3678
|
+
logger.warn(` Corrupt lock file \u2014 overwriting`);
|
|
3679
|
+
}
|
|
3680
|
+
try {
|
|
3681
|
+
fs25.unlinkSync(lockPath);
|
|
3682
|
+
} catch {
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
try {
|
|
3686
|
+
const fd = fs25.openSync(lockPath, fs25.constants.O_WRONLY | fs25.constants.O_CREAT | fs25.constants.O_EXCL);
|
|
3687
|
+
fs25.writeSync(fd, String(process.pid));
|
|
3688
|
+
fs25.closeSync(fd);
|
|
3689
|
+
} catch (err) {
|
|
3690
|
+
if (err.code === "EEXIST") {
|
|
3691
|
+
throw new Error("Pipeline already running (lock acquired by another process)");
|
|
3215
3692
|
}
|
|
3693
|
+
throw err;
|
|
3216
3694
|
}
|
|
3217
|
-
fs17.writeFileSync(lockPath, String(process.pid));
|
|
3218
3695
|
}
|
|
3219
3696
|
function releaseLock(taskDir) {
|
|
3220
3697
|
try {
|
|
3221
|
-
|
|
3698
|
+
fs25.unlinkSync(path23.join(taskDir, ".lock"));
|
|
3222
3699
|
} catch {
|
|
3223
3700
|
}
|
|
3224
3701
|
}
|
|
@@ -3230,17 +3707,17 @@ async function runPipeline(ctx) {
|
|
|
3230
3707
|
try {
|
|
3231
3708
|
const state = loadState(ctx.taskId, ctx.taskDir);
|
|
3232
3709
|
if (state && state.state === "running") {
|
|
3233
|
-
state.
|
|
3710
|
+
const updatedStages = { ...state.stages };
|
|
3234
3711
|
for (const stage of STAGES) {
|
|
3235
|
-
if (
|
|
3236
|
-
|
|
3237
|
-
...
|
|
3712
|
+
if (updatedStages[stage.name]?.state === "running") {
|
|
3713
|
+
updatedStages[stage.name] = {
|
|
3714
|
+
...updatedStages[stage.name],
|
|
3238
3715
|
state: "failed",
|
|
3239
3716
|
error: "Pipeline crashed unexpectedly"
|
|
3240
3717
|
};
|
|
3241
3718
|
}
|
|
3242
3719
|
}
|
|
3243
|
-
writeState(state, ctx.taskDir);
|
|
3720
|
+
writeState({ ...state, state: "failed", stages: updatedStages }, ctx.taskDir);
|
|
3244
3721
|
}
|
|
3245
3722
|
} catch {
|
|
3246
3723
|
}
|
|
@@ -3365,8 +3842,27 @@ async function runPipelineInner(ctx) {
|
|
|
3365
3842
|
}
|
|
3366
3843
|
autoLearn(ctx);
|
|
3367
3844
|
}
|
|
3368
|
-
await runRetrospective(ctx, state, pipelineStartTime).catch(() => {
|
|
3845
|
+
await runRetrospective(ctx, state, pipelineStartTime).catch((err) => {
|
|
3846
|
+
logger.warn(` Retrospective failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3369
3847
|
});
|
|
3848
|
+
if (ctx.input.issueNumber && !ctx.input.dryRun) {
|
|
3849
|
+
const config = getProjectConfig();
|
|
3850
|
+
const isCI3 = !!process.env.GITHUB_ACTIONS;
|
|
3851
|
+
const shouldPost = config.github.postSummary ?? (isCI3 ? true : false);
|
|
3852
|
+
if (shouldPost) {
|
|
3853
|
+
try {
|
|
3854
|
+
const summaryOpts = {};
|
|
3855
|
+
if (complexity) summaryOpts.complexity = complexity;
|
|
3856
|
+
const modelMap = config.agent?.modelMap;
|
|
3857
|
+
if (modelMap?.mid) summaryOpts.model = modelMap.mid;
|
|
3858
|
+
const summary = formatPipelineSummary(state, summaryOpts);
|
|
3859
|
+
postComment(ctx.input.issueNumber, summary);
|
|
3860
|
+
logger.info("Pipeline summary posted on issue");
|
|
3861
|
+
} catch (err) {
|
|
3862
|
+
logger.warn(` Failed to post pipeline summary: ${err instanceof Error ? err.message : String(err)}`);
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3370
3866
|
return state;
|
|
3371
3867
|
}
|
|
3372
3868
|
function printStatus(taskId, taskDir) {
|
|
@@ -3401,12 +3897,14 @@ var init_pipeline = __esm({
|
|
|
3401
3897
|
init_hooks();
|
|
3402
3898
|
init_auto_learn();
|
|
3403
3899
|
init_retrospective();
|
|
3900
|
+
init_summary();
|
|
3901
|
+
init_config();
|
|
3404
3902
|
}
|
|
3405
3903
|
});
|
|
3406
3904
|
|
|
3407
3905
|
// src/preflight.ts
|
|
3408
|
-
import { execFileSync as
|
|
3409
|
-
import * as
|
|
3906
|
+
import { execFileSync as execFileSync14 } from "child_process";
|
|
3907
|
+
import * as fs26 from "fs";
|
|
3410
3908
|
function check(name, fn) {
|
|
3411
3909
|
try {
|
|
3412
3910
|
const detail = fn() ?? void 0;
|
|
@@ -3418,7 +3916,7 @@ function check(name, fn) {
|
|
|
3418
3916
|
function runPreflight() {
|
|
3419
3917
|
const checks = [
|
|
3420
3918
|
check("claude CLI", () => {
|
|
3421
|
-
const v =
|
|
3919
|
+
const v = execFileSync14("claude", ["--version"], {
|
|
3422
3920
|
encoding: "utf-8",
|
|
3423
3921
|
timeout: 1e4,
|
|
3424
3922
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3426,14 +3924,14 @@ function runPreflight() {
|
|
|
3426
3924
|
return v;
|
|
3427
3925
|
}),
|
|
3428
3926
|
check("git repo", () => {
|
|
3429
|
-
|
|
3927
|
+
execFileSync14("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
3430
3928
|
encoding: "utf-8",
|
|
3431
3929
|
timeout: 5e3,
|
|
3432
3930
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3433
3931
|
});
|
|
3434
3932
|
}),
|
|
3435
3933
|
check("pnpm", () => {
|
|
3436
|
-
const v =
|
|
3934
|
+
const v = execFileSync14("pnpm", ["--version"], {
|
|
3437
3935
|
encoding: "utf-8",
|
|
3438
3936
|
timeout: 5e3,
|
|
3439
3937
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3441,7 +3939,7 @@ function runPreflight() {
|
|
|
3441
3939
|
return v;
|
|
3442
3940
|
}),
|
|
3443
3941
|
check("node >= 18", () => {
|
|
3444
|
-
const v =
|
|
3942
|
+
const v = execFileSync14("node", ["--version"], {
|
|
3445
3943
|
encoding: "utf-8",
|
|
3446
3944
|
timeout: 5e3,
|
|
3447
3945
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3451,7 +3949,7 @@ function runPreflight() {
|
|
|
3451
3949
|
return v;
|
|
3452
3950
|
}),
|
|
3453
3951
|
check("gh CLI", () => {
|
|
3454
|
-
const v =
|
|
3952
|
+
const v = execFileSync14("gh", ["--version"], {
|
|
3455
3953
|
encoding: "utf-8",
|
|
3456
3954
|
timeout: 5e3,
|
|
3457
3955
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3459,7 +3957,7 @@ function runPreflight() {
|
|
|
3459
3957
|
return v;
|
|
3460
3958
|
}),
|
|
3461
3959
|
check("package.json", () => {
|
|
3462
|
-
if (!
|
|
3960
|
+
if (!fs26.existsSync("package.json")) throw new Error("not found");
|
|
3463
3961
|
})
|
|
3464
3962
|
];
|
|
3465
3963
|
const failed = checks.filter((c) => !c.ok);
|
|
@@ -3536,10 +4034,10 @@ var init_args = __esm({
|
|
|
3536
4034
|
});
|
|
3537
4035
|
|
|
3538
4036
|
// src/cli/litellm.ts
|
|
3539
|
-
import * as
|
|
4037
|
+
import * as fs27 from "fs";
|
|
3540
4038
|
import * as os from "os";
|
|
3541
|
-
import * as
|
|
3542
|
-
import { execFileSync as
|
|
4039
|
+
import * as path24 from "path";
|
|
4040
|
+
import { execFileSync as execFileSync15 } from "child_process";
|
|
3543
4041
|
async function checkLitellmHealth(url) {
|
|
3544
4042
|
try {
|
|
3545
4043
|
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
@@ -3594,22 +4092,49 @@ function generateLitellmConfig(provider, modelMap) {
|
|
|
3594
4092
|
}
|
|
3595
4093
|
return entries.join("\n") + "\n";
|
|
3596
4094
|
}
|
|
4095
|
+
function generateLitellmConfigFromStages(defaultConfig, stages) {
|
|
4096
|
+
const proxyModels = [];
|
|
4097
|
+
if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
|
|
4098
|
+
proxyModels.push(defaultConfig);
|
|
4099
|
+
}
|
|
4100
|
+
if (stages) {
|
|
4101
|
+
for (const sc of Object.values(stages)) {
|
|
4102
|
+
if (sc.provider !== "claude" && sc.provider !== "anthropic") {
|
|
4103
|
+
proxyModels.push(sc);
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
if (proxyModels.length === 0) return void 0;
|
|
4108
|
+
const entries = ["model_list:"];
|
|
4109
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4110
|
+
for (const { provider, model } of proxyModels) {
|
|
4111
|
+
const key = `${provider}/${model}`;
|
|
4112
|
+
if (seen.has(key)) continue;
|
|
4113
|
+
seen.add(key);
|
|
4114
|
+
const apiKeyVar = providerApiKeyEnvVar(provider);
|
|
4115
|
+
entries.push(` - model_name: ${model}`);
|
|
4116
|
+
entries.push(` litellm_params:`);
|
|
4117
|
+
entries.push(` model: ${provider}/${model}`);
|
|
4118
|
+
entries.push(` api_key: os.environ/${apiKeyVar}`);
|
|
4119
|
+
}
|
|
4120
|
+
return entries.join("\n") + "\n";
|
|
4121
|
+
}
|
|
3597
4122
|
async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
3598
4123
|
if (!generatedConfig) {
|
|
3599
4124
|
logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
|
|
3600
4125
|
return null;
|
|
3601
4126
|
}
|
|
3602
|
-
const configPath =
|
|
3603
|
-
|
|
4127
|
+
const configPath = path24.join(os.tmpdir(), "kody-litellm-config.yaml");
|
|
4128
|
+
fs27.writeFileSync(configPath, generatedConfig);
|
|
3604
4129
|
const portMatch = url.match(/:(\d+)/);
|
|
3605
4130
|
const port = portMatch ? portMatch[1] : "4000";
|
|
3606
4131
|
let litellmFound = false;
|
|
3607
4132
|
try {
|
|
3608
|
-
|
|
4133
|
+
execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
3609
4134
|
litellmFound = true;
|
|
3610
4135
|
} catch {
|
|
3611
4136
|
try {
|
|
3612
|
-
|
|
4137
|
+
execFileSync15("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
3613
4138
|
litellmFound = true;
|
|
3614
4139
|
} catch {
|
|
3615
4140
|
}
|
|
@@ -3622,19 +4147,29 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
|
|
|
3622
4147
|
let cmd;
|
|
3623
4148
|
let args2;
|
|
3624
4149
|
try {
|
|
3625
|
-
|
|
4150
|
+
execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
3626
4151
|
cmd = "litellm";
|
|
3627
|
-
args2 = ["--config", configPath, "--port", port];
|
|
4152
|
+
args2 = ["--config", configPath, "--port", port, "--no_db"];
|
|
3628
4153
|
} catch {
|
|
3629
4154
|
cmd = "python3";
|
|
3630
|
-
args2 = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
4155
|
+
args2 = ["-m", "litellm", "--config", configPath, "--port", port, "--no_db"];
|
|
3631
4156
|
}
|
|
3632
|
-
const dotenvPath =
|
|
4157
|
+
const dotenvPath = path24.join(projectDir, ".env");
|
|
3633
4158
|
const dotenvVars = {};
|
|
3634
|
-
if (
|
|
3635
|
-
for (const
|
|
4159
|
+
if (fs27.existsSync(dotenvPath)) {
|
|
4160
|
+
for (const rawLine of fs27.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
4161
|
+
const line = rawLine.trim();
|
|
4162
|
+
if (!line || line.startsWith("#")) continue;
|
|
3636
4163
|
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
3637
|
-
if (match)
|
|
4164
|
+
if (match) {
|
|
4165
|
+
let value = match[2].trim();
|
|
4166
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
4167
|
+
value = value.slice(1, -1);
|
|
4168
|
+
}
|
|
4169
|
+
const commentIdx = value.indexOf(" #");
|
|
4170
|
+
if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
|
|
4171
|
+
if (value) dotenvVars[match[1]] = value;
|
|
4172
|
+
}
|
|
3638
4173
|
}
|
|
3639
4174
|
if (Object.keys(dotenvVars).length > 0) {
|
|
3640
4175
|
logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
|
|
@@ -3673,8 +4208,8 @@ var init_litellm = __esm({
|
|
|
3673
4208
|
});
|
|
3674
4209
|
|
|
3675
4210
|
// src/cli/task-state.ts
|
|
3676
|
-
import * as
|
|
3677
|
-
import * as
|
|
4211
|
+
import * as fs28 from "fs";
|
|
4212
|
+
import * as path25 from "path";
|
|
3678
4213
|
function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
3679
4214
|
if (!existingTaskId || !existingState) {
|
|
3680
4215
|
return { action: "start-fresh", taskId: `${issueNumber}-${generateTaskId()}` };
|
|
@@ -3706,11 +4241,11 @@ function resolveTaskAction(issueNumber, existingTaskId, existingState) {
|
|
|
3706
4241
|
function resolveForIssue(issueNumber, projectDir) {
|
|
3707
4242
|
const existingTaskId = findLatestTaskForIssue(issueNumber, projectDir);
|
|
3708
4243
|
if (existingTaskId) {
|
|
3709
|
-
const statusPath =
|
|
4244
|
+
const statusPath = path25.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
|
|
3710
4245
|
let existingState = null;
|
|
3711
|
-
if (
|
|
4246
|
+
if (fs28.existsSync(statusPath)) {
|
|
3712
4247
|
try {
|
|
3713
|
-
existingState = JSON.parse(
|
|
4248
|
+
existingState = JSON.parse(fs28.readFileSync(statusPath, "utf-8"));
|
|
3714
4249
|
} catch {
|
|
3715
4250
|
}
|
|
3716
4251
|
}
|
|
@@ -3743,12 +4278,12 @@ var resolve_exports = {};
|
|
|
3743
4278
|
__export(resolve_exports, {
|
|
3744
4279
|
runResolve: () => runResolve
|
|
3745
4280
|
});
|
|
3746
|
-
import { execFileSync as
|
|
4281
|
+
import { execFileSync as execFileSync16 } from "child_process";
|
|
3747
4282
|
function getConflictContext(cwd, files) {
|
|
3748
4283
|
const parts = [];
|
|
3749
4284
|
for (const file of files.slice(0, 10)) {
|
|
3750
4285
|
try {
|
|
3751
|
-
const content =
|
|
4286
|
+
const content = execFileSync16("git", ["diff", file], {
|
|
3752
4287
|
cwd,
|
|
3753
4288
|
encoding: "utf-8",
|
|
3754
4289
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -3867,10 +4402,10 @@ var init_resolve = __esm({
|
|
|
3867
4402
|
|
|
3868
4403
|
// src/entry.ts
|
|
3869
4404
|
var entry_exports = {};
|
|
3870
|
-
import * as
|
|
3871
|
-
import * as
|
|
4405
|
+
import * as fs29 from "fs";
|
|
4406
|
+
import * as path26 from "path";
|
|
3872
4407
|
async function ensureLitellmProxy(config, projectDir) {
|
|
3873
|
-
if (!
|
|
4408
|
+
if (!anyStageNeedsProxy(config)) return null;
|
|
3874
4409
|
const litellmUrl = getLitellmUrl();
|
|
3875
4410
|
const proxyRunning = await checkLitellmHealth(litellmUrl);
|
|
3876
4411
|
let litellmProcess = null;
|
|
@@ -3883,7 +4418,9 @@ async function ensureLitellmProxy(config, projectDir) {
|
|
|
3883
4418
|
}
|
|
3884
4419
|
}
|
|
3885
4420
|
let generatedConfig;
|
|
3886
|
-
if (config.agent.
|
|
4421
|
+
if (config.agent.stages || config.agent.default) {
|
|
4422
|
+
generatedConfig = generateLitellmConfigFromStages(config.agent.default, config.agent.stages);
|
|
4423
|
+
} else if (config.agent.provider && config.agent.provider !== "anthropic") {
|
|
3887
4424
|
generatedConfig = generateLitellmConfig(config.agent.provider, config.agent.modelMap);
|
|
3888
4425
|
}
|
|
3889
4426
|
litellmProcess = await tryStartLitellm(litellmUrl, projectDir, generatedConfig);
|
|
@@ -3894,10 +4431,9 @@ async function ensureLitellmProxy(config, projectDir) {
|
|
|
3894
4431
|
} else {
|
|
3895
4432
|
logger.info(`LiteLLM proxy already running at ${litellmUrl}`);
|
|
3896
4433
|
}
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
process.env.ANTHROPIC_API_KEY = "sk-ant-api03-litellm-proxy-key-00000000000000000000000000000000000000000000000000000000000000000000";
|
|
4434
|
+
logger.info(`LiteLLM proxy available at ${litellmUrl}`);
|
|
4435
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
4436
|
+
process.env.ANTHROPIC_API_KEY = `sk-ant-api03-${"0".repeat(64)}`;
|
|
3901
4437
|
}
|
|
3902
4438
|
return litellmProcess;
|
|
3903
4439
|
}
|
|
@@ -3922,9 +4458,9 @@ async function runModelHealthCheck(config) {
|
|
|
3922
4458
|
}
|
|
3923
4459
|
async function main() {
|
|
3924
4460
|
const input = parseArgs();
|
|
3925
|
-
const projectDir = input.cwd ?
|
|
4461
|
+
const projectDir = input.cwd ? path26.resolve(input.cwd) : process.cwd();
|
|
3926
4462
|
if (input.cwd) {
|
|
3927
|
-
if (!
|
|
4463
|
+
if (!fs29.existsSync(projectDir)) {
|
|
3928
4464
|
console.error(`--cwd path does not exist: ${projectDir}`);
|
|
3929
4465
|
process.exit(1);
|
|
3930
4466
|
}
|
|
@@ -3958,15 +4494,26 @@ async function main() {
|
|
|
3958
4494
|
process.exit(0);
|
|
3959
4495
|
}
|
|
3960
4496
|
if (taskAction.action === "resume") {
|
|
3961
|
-
input
|
|
3962
|
-
|
|
3963
|
-
|
|
4497
|
+
Object.assign(input, {
|
|
4498
|
+
taskId: taskAction.taskId,
|
|
4499
|
+
fromStage: taskAction.fromStage,
|
|
4500
|
+
command: "rerun"
|
|
4501
|
+
});
|
|
3964
4502
|
logger.info(`Resuming task ${taskAction.taskId} from ${taskAction.fromStage}`);
|
|
3965
4503
|
}
|
|
3966
4504
|
}
|
|
3967
4505
|
let taskId = input.taskId;
|
|
3968
4506
|
if (!taskId) {
|
|
3969
|
-
if (
|
|
4507
|
+
if ((input.command === "rerun" || input.command === "status") && input.issueNumber) {
|
|
4508
|
+
const resolved = resolveTaskIdForCommand(input.issueNumber, projectDir);
|
|
4509
|
+
if (resolved) {
|
|
4510
|
+
taskId = resolved;
|
|
4511
|
+
logger.info(`Auto-resolved task-id: ${taskId} (from issue #${input.issueNumber})`);
|
|
4512
|
+
} else {
|
|
4513
|
+
console.error(`No task found for issue #${input.issueNumber}. Provide --task-id explicitly.`);
|
|
4514
|
+
process.exit(1);
|
|
4515
|
+
}
|
|
4516
|
+
} else if (isPRFix) {
|
|
3970
4517
|
taskId = `${input.command === "fix-ci" ? "fixci" : "fix"}-pr-${input.prNumber}-${generateTaskId()}`;
|
|
3971
4518
|
} else if (input.issueNumber) {
|
|
3972
4519
|
taskId = `${input.issueNumber}-${generateTaskId()}`;
|
|
@@ -3979,8 +4526,8 @@ async function main() {
|
|
|
3979
4526
|
process.exit(1);
|
|
3980
4527
|
}
|
|
3981
4528
|
}
|
|
3982
|
-
const taskDir =
|
|
3983
|
-
|
|
4529
|
+
const taskDir = path26.join(projectDir, ".kody", "tasks", taskId);
|
|
4530
|
+
fs29.mkdirSync(taskDir, { recursive: true });
|
|
3984
4531
|
if (input.command === "status") {
|
|
3985
4532
|
printStatus(taskId, taskDir);
|
|
3986
4533
|
return;
|
|
@@ -4085,7 +4632,7 @@ async function main() {
|
|
|
4085
4632
|
runners: runners2,
|
|
4086
4633
|
local: input.local ?? true
|
|
4087
4634
|
});
|
|
4088
|
-
if (litellmProcess2) litellmProcess2.kill
|
|
4635
|
+
if (litellmProcess2) litellmProcess2.kill();
|
|
4089
4636
|
if (result.outcome === "failed") {
|
|
4090
4637
|
console.error(`Resolve failed: ${result.error}`);
|
|
4091
4638
|
process.exit(1);
|
|
@@ -4096,31 +4643,31 @@ async function main() {
|
|
|
4096
4643
|
logger.info("Preflight checks:");
|
|
4097
4644
|
runPreflight();
|
|
4098
4645
|
if (input.task) {
|
|
4099
|
-
|
|
4646
|
+
fs29.writeFileSync(path26.join(taskDir, "task.md"), input.task);
|
|
4100
4647
|
}
|
|
4101
|
-
const taskMdPath =
|
|
4102
|
-
if (!
|
|
4648
|
+
const taskMdPath = path26.join(taskDir, "task.md");
|
|
4649
|
+
if (!fs29.existsSync(taskMdPath) && isPRFix && input.prNumber) {
|
|
4103
4650
|
logger.info(`Fetching PR #${input.prNumber} details as task context...`);
|
|
4104
4651
|
const prDetails = getPRDetails(input.prNumber);
|
|
4105
4652
|
if (prDetails) {
|
|
4106
4653
|
const taskContent = `# ${prDetails.title}
|
|
4107
4654
|
|
|
4108
4655
|
${prDetails.body ?? ""}`;
|
|
4109
|
-
|
|
4656
|
+
fs29.writeFileSync(taskMdPath, taskContent);
|
|
4110
4657
|
logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
|
|
4111
4658
|
}
|
|
4112
|
-
} else if (!
|
|
4659
|
+
} else if (!fs29.existsSync(taskMdPath) && input.issueNumber) {
|
|
4113
4660
|
logger.info(`Fetching issue #${input.issueNumber} body as task...`);
|
|
4114
4661
|
const issue = getIssue(input.issueNumber);
|
|
4115
4662
|
if (issue) {
|
|
4116
4663
|
const taskContent = `# ${issue.title}
|
|
4117
4664
|
|
|
4118
4665
|
${issue.body ?? ""}`;
|
|
4119
|
-
|
|
4666
|
+
fs29.writeFileSync(taskMdPath, taskContent);
|
|
4120
4667
|
logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
|
|
4121
4668
|
}
|
|
4122
4669
|
}
|
|
4123
|
-
if (!
|
|
4670
|
+
if (!fs29.existsSync(taskMdPath)) {
|
|
4124
4671
|
console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
|
|
4125
4672
|
process.exit(1);
|
|
4126
4673
|
}
|
|
@@ -4192,7 +4739,7 @@ ${input.feedback}`);
|
|
|
4192
4739
|
await runModelHealthCheck(config);
|
|
4193
4740
|
const cleanupLitellm = () => {
|
|
4194
4741
|
if (litellmProcess) {
|
|
4195
|
-
litellmProcess.kill
|
|
4742
|
+
litellmProcess.kill();
|
|
4196
4743
|
litellmProcess = null;
|
|
4197
4744
|
}
|
|
4198
4745
|
};
|
|
@@ -4258,7 +4805,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
|
|
|
4258
4805
|
}
|
|
4259
4806
|
}
|
|
4260
4807
|
const state = await runPipeline(ctx);
|
|
4261
|
-
const files =
|
|
4808
|
+
const files = fs29.readdirSync(taskDir);
|
|
4262
4809
|
console.log(`
|
|
4263
4810
|
Artifacts in ${taskDir}:`);
|
|
4264
4811
|
for (const f of files) {
|
|
@@ -4322,20 +4869,21 @@ var init_entry = __esm({
|
|
|
4322
4869
|
});
|
|
4323
4870
|
|
|
4324
4871
|
// src/bin/cli.ts
|
|
4325
|
-
import * as
|
|
4326
|
-
import * as
|
|
4327
|
-
import { execFileSync as execFileSync12 } from "child_process";
|
|
4872
|
+
import * as fs30 from "fs";
|
|
4873
|
+
import * as path27 from "path";
|
|
4328
4874
|
import { fileURLToPath } from "url";
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4875
|
+
|
|
4876
|
+
// src/bin/commands/init.ts
|
|
4877
|
+
import * as fs3 from "fs";
|
|
4878
|
+
import * as path2 from "path";
|
|
4879
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
4880
|
+
|
|
4881
|
+
// src/bin/health-checks.ts
|
|
4882
|
+
import * as fs from "fs";
|
|
4883
|
+
import { execFileSync } from "child_process";
|
|
4884
|
+
function checkCommand(name, args2, fix) {
|
|
4337
4885
|
try {
|
|
4338
|
-
const output =
|
|
4886
|
+
const output = execFileSync(name, args2, {
|
|
4339
4887
|
encoding: "utf-8",
|
|
4340
4888
|
timeout: 1e4,
|
|
4341
4889
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4346,14 +4894,14 @@ function checkCommand2(name, args2, fix) {
|
|
|
4346
4894
|
}
|
|
4347
4895
|
}
|
|
4348
4896
|
function checkFile(filePath, description, fix) {
|
|
4349
|
-
if (
|
|
4897
|
+
if (fs.existsSync(filePath)) {
|
|
4350
4898
|
return { name: description, ok: true, detail: filePath };
|
|
4351
4899
|
}
|
|
4352
4900
|
return { name: description, ok: false, fix };
|
|
4353
4901
|
}
|
|
4354
4902
|
function checkGhAuth(cwd) {
|
|
4355
4903
|
try {
|
|
4356
|
-
const output =
|
|
4904
|
+
const output = execFileSync("gh", ["auth", "status"], {
|
|
4357
4905
|
encoding: "utf-8",
|
|
4358
4906
|
timeout: 1e4,
|
|
4359
4907
|
cwd,
|
|
@@ -4371,7 +4919,7 @@ function checkGhAuth(cwd) {
|
|
|
4371
4919
|
}
|
|
4372
4920
|
function checkGhRepoAccess(cwd) {
|
|
4373
4921
|
try {
|
|
4374
|
-
const remote =
|
|
4922
|
+
const remote = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
4375
4923
|
encoding: "utf-8",
|
|
4376
4924
|
timeout: 5e3,
|
|
4377
4925
|
cwd,
|
|
@@ -4382,7 +4930,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
4382
4930
|
return { name: "GitHub repo", ok: false, fix: "Set git remote origin to a GitHub URL" };
|
|
4383
4931
|
}
|
|
4384
4932
|
const repoSlug = `${match[1]}/${match[2]}`;
|
|
4385
|
-
|
|
4933
|
+
execFileSync("gh", ["repo", "view", repoSlug, "--json", "name"], {
|
|
4386
4934
|
encoding: "utf-8",
|
|
4387
4935
|
timeout: 1e4,
|
|
4388
4936
|
cwd,
|
|
@@ -4395,7 +4943,7 @@ function checkGhRepoAccess(cwd) {
|
|
|
4395
4943
|
}
|
|
4396
4944
|
function checkGhSecret(repoSlug, secretName) {
|
|
4397
4945
|
try {
|
|
4398
|
-
const output =
|
|
4946
|
+
const output = execFileSync("gh", ["secret", "list", "--repo", repoSlug], {
|
|
4399
4947
|
encoding: "utf-8",
|
|
4400
4948
|
timeout: 1e4,
|
|
4401
4949
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -4416,14 +4964,20 @@ function checkGhSecret(repoSlug, secretName) {
|
|
|
4416
4964
|
};
|
|
4417
4965
|
}
|
|
4418
4966
|
}
|
|
4967
|
+
|
|
4968
|
+
// src/bin/config-detection.ts
|
|
4969
|
+
import * as fs2 from "fs";
|
|
4970
|
+
import * as path from "path";
|
|
4971
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
4972
|
+
var FRONTEND_DEPS = ["next", "react", "vue", "svelte", "nuxt", "astro", "solid-js", "angular", "@angular/core"];
|
|
4419
4973
|
function detectBasicConfig(cwd) {
|
|
4420
4974
|
let pm = "pnpm";
|
|
4421
|
-
if (
|
|
4422
|
-
else if (
|
|
4423
|
-
else if (!
|
|
4975
|
+
if (fs2.existsSync(path.join(cwd, "yarn.lock"))) pm = "yarn";
|
|
4976
|
+
else if (fs2.existsSync(path.join(cwd, "bun.lockb"))) pm = "bun";
|
|
4977
|
+
else if (!fs2.existsSync(path.join(cwd, "pnpm-lock.yaml")) && fs2.existsSync(path.join(cwd, "package-lock.json"))) pm = "npm";
|
|
4424
4978
|
let defaultBranch = "main";
|
|
4425
4979
|
try {
|
|
4426
|
-
const ref =
|
|
4980
|
+
const ref = execFileSync2("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
4427
4981
|
encoding: "utf-8",
|
|
4428
4982
|
timeout: 5e3,
|
|
4429
4983
|
cwd,
|
|
@@ -4432,7 +4986,7 @@ function detectBasicConfig(cwd) {
|
|
|
4432
4986
|
defaultBranch = ref.replace("refs/remotes/origin/", "");
|
|
4433
4987
|
} catch {
|
|
4434
4988
|
try {
|
|
4435
|
-
|
|
4989
|
+
execFileSync2("git", ["rev-parse", "--verify", "origin/dev"], {
|
|
4436
4990
|
encoding: "utf-8",
|
|
4437
4991
|
timeout: 5e3,
|
|
4438
4992
|
cwd,
|
|
@@ -4445,7 +4999,7 @@ function detectBasicConfig(cwd) {
|
|
|
4445
4999
|
let owner = "";
|
|
4446
5000
|
let repo = "";
|
|
4447
5001
|
try {
|
|
4448
|
-
const remote =
|
|
5002
|
+
const remote = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
4449
5003
|
encoding: "utf-8",
|
|
4450
5004
|
timeout: 5e3,
|
|
4451
5005
|
cwd,
|
|
@@ -4463,7 +5017,7 @@ function detectBasicConfig(cwd) {
|
|
|
4463
5017
|
function buildConfig(cwd, basic) {
|
|
4464
5018
|
const pkg = (() => {
|
|
4465
5019
|
try {
|
|
4466
|
-
return JSON.parse(
|
|
5020
|
+
return JSON.parse(fs2.readFileSync(path.join(cwd, "package.json"), "utf-8"));
|
|
4467
5021
|
} catch {
|
|
4468
5022
|
return {};
|
|
4469
5023
|
}
|
|
@@ -4495,7 +5049,6 @@ function buildConfig(cwd, basic) {
|
|
|
4495
5049
|
if (mcp) config.mcp = mcp;
|
|
4496
5050
|
return config;
|
|
4497
5051
|
}
|
|
4498
|
-
var FRONTEND_DEPS = ["next", "react", "vue", "svelte", "nuxt", "astro", "solid-js", "angular", "@angular/core"];
|
|
4499
5052
|
function detectMcpConfig(cwd, pm, pkg) {
|
|
4500
5053
|
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
4501
5054
|
const hasFrontend = FRONTEND_DEPS.some((dep) => dep in allDeps);
|
|
@@ -4507,12 +5060,7 @@ function detectMcpConfig(cwd, pm, pkg) {
|
|
|
4507
5060
|
const defaultPort = isNext ? 3e3 : isVite ? 5173 : 3e3;
|
|
4508
5061
|
const mcp = {
|
|
4509
5062
|
enabled: true,
|
|
4510
|
-
servers: {
|
|
4511
|
-
playwright: {
|
|
4512
|
-
command: "npx",
|
|
4513
|
-
args: ["@playwright/mcp@latest"]
|
|
4514
|
-
}
|
|
4515
|
-
},
|
|
5063
|
+
servers: {},
|
|
4516
5064
|
stages: ["build", "review"]
|
|
4517
5065
|
};
|
|
4518
5066
|
if (hasDevScript) {
|
|
@@ -4523,7 +5071,9 @@ function detectMcpConfig(cwd, pm, pkg) {
|
|
|
4523
5071
|
}
|
|
4524
5072
|
return mcp;
|
|
4525
5073
|
}
|
|
4526
|
-
|
|
5074
|
+
|
|
5075
|
+
// src/bin/commands/init.ts
|
|
5076
|
+
function initCommand(opts, pkgRoot) {
|
|
4527
5077
|
const cwd = process.cwd();
|
|
4528
5078
|
console.log(`
|
|
4529
5079
|
\u{1F527} Kody Engine Lite \u2014 Init
|
|
@@ -4531,35 +5081,35 @@ function initCommand(opts) {
|
|
|
4531
5081
|
console.log(`Project: ${cwd}
|
|
4532
5082
|
`);
|
|
4533
5083
|
console.log("\u2500\u2500 Files \u2500\u2500");
|
|
4534
|
-
const templatesDir =
|
|
5084
|
+
const templatesDir = path2.join(pkgRoot, "templates");
|
|
4535
5085
|
const basic = detectBasicConfig(cwd);
|
|
4536
|
-
const workflowSrc =
|
|
4537
|
-
const workflowDest =
|
|
4538
|
-
if (!
|
|
5086
|
+
const workflowSrc = path2.join(templatesDir, "kody.yml");
|
|
5087
|
+
const workflowDest = path2.join(cwd, ".github", "workflows", "kody.yml");
|
|
5088
|
+
if (!fs3.existsSync(workflowSrc)) {
|
|
4539
5089
|
console.error(" \u2717 Template kody.yml not found in package");
|
|
4540
5090
|
process.exit(1);
|
|
4541
5091
|
}
|
|
4542
|
-
if (
|
|
5092
|
+
if (fs3.existsSync(workflowDest) && !opts.force) {
|
|
4543
5093
|
console.log(" \u25CB .github/workflows/kody.yml (exists, use --force to overwrite)");
|
|
4544
5094
|
} else {
|
|
4545
|
-
|
|
4546
|
-
|
|
5095
|
+
fs3.mkdirSync(path2.dirname(workflowDest), { recursive: true });
|
|
5096
|
+
fs3.copyFileSync(workflowSrc, workflowDest);
|
|
4547
5097
|
console.log(" \u2713 .github/workflows/kody.yml");
|
|
4548
5098
|
}
|
|
4549
|
-
const configDest =
|
|
4550
|
-
if (!
|
|
5099
|
+
const configDest = path2.join(cwd, "kody.config.json");
|
|
5100
|
+
if (!fs3.existsSync(configDest) || opts.force) {
|
|
4551
5101
|
const config = buildConfig(cwd, basic);
|
|
4552
|
-
|
|
5102
|
+
fs3.writeFileSync(configDest, JSON.stringify(config, null, 2) + "\n");
|
|
4553
5103
|
console.log(" \u2713 kody.config.json (auto-configured)");
|
|
4554
5104
|
} else {
|
|
4555
5105
|
console.log(" \u25CB kody.config.json (exists)");
|
|
4556
5106
|
}
|
|
4557
|
-
const gitignorePath =
|
|
4558
|
-
if (
|
|
4559
|
-
const content =
|
|
5107
|
+
const gitignorePath = path2.join(cwd, ".gitignore");
|
|
5108
|
+
if (fs3.existsSync(gitignorePath)) {
|
|
5109
|
+
const content = fs3.readFileSync(gitignorePath, "utf-8");
|
|
4560
5110
|
if (content.includes(".tasks/")) {
|
|
4561
5111
|
const updated = content.replace(/\n?\.tasks\/\n?/g, "\n");
|
|
4562
|
-
|
|
5112
|
+
fs3.writeFileSync(gitignorePath, updated);
|
|
4563
5113
|
console.log(" \u2713 .gitignore (removed legacy .tasks/ \u2014 tasks now committed in .kody/tasks/)");
|
|
4564
5114
|
} else {
|
|
4565
5115
|
console.log(" \u25CB .gitignore (ok)");
|
|
@@ -4567,10 +5117,10 @@ function initCommand(opts) {
|
|
|
4567
5117
|
}
|
|
4568
5118
|
console.log("\n\u2500\u2500 Prerequisites \u2500\u2500");
|
|
4569
5119
|
const checks = [
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
checkFile(
|
|
5120
|
+
checkCommand("gh", ["--version"], "Install: https://cli.github.com"),
|
|
5121
|
+
checkCommand("git", ["--version"], "Install git"),
|
|
5122
|
+
checkCommand("node", ["--version"], "Install Node.js >= 22"),
|
|
5123
|
+
checkFile(path2.join(cwd, "package.json"), "package.json", `Run: ${basic.pm} init`)
|
|
4574
5124
|
];
|
|
4575
5125
|
for (const c of checks) {
|
|
4576
5126
|
if (c.ok) {
|
|
@@ -4584,26 +5134,19 @@ function initCommand(opts) {
|
|
|
4584
5134
|
console.log(ghAuth.ok ? ` \u2713 ${ghAuth.name} (${ghAuth.detail})` : ` \u2717 ${ghAuth.name} \u2014 ${ghAuth.fix}`);
|
|
4585
5135
|
const ghRepo = checkGhRepoAccess(cwd);
|
|
4586
5136
|
console.log(ghRepo.ok ? ` \u2713 ${ghRepo.name} (${ghRepo.detail})` : ` \u2717 ${ghRepo.name} \u2014 ${ghRepo.fix}`);
|
|
4587
|
-
let repoSlug = "";
|
|
4588
5137
|
if (ghRepo.ok && ghRepo.detail) {
|
|
4589
|
-
repoSlug = ghRepo.detail;
|
|
4590
|
-
const secretChecks = [
|
|
4591
|
-
checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")
|
|
4592
|
-
];
|
|
5138
|
+
const repoSlug = ghRepo.detail;
|
|
5139
|
+
const secretChecks = [checkGhSecret(repoSlug, "ANTHROPIC_API_KEY")];
|
|
4593
5140
|
for (const c of secretChecks) {
|
|
4594
|
-
|
|
4595
|
-
console.log(` \u2713 ${c.name}`);
|
|
4596
|
-
} else {
|
|
4597
|
-
console.log(` \u2717 ${c.name} \u2014 ${c.fix}`);
|
|
4598
|
-
}
|
|
5141
|
+
console.log(c.ok ? ` \u2713 ${c.name}` : ` \u2717 ${c.name} \u2014 ${c.fix}`);
|
|
4599
5142
|
}
|
|
4600
5143
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
4601
5144
|
console.log(" \u25CB Labels will be created automatically during bootstrap");
|
|
4602
5145
|
}
|
|
4603
5146
|
console.log("\n\u2500\u2500 Config \u2500\u2500");
|
|
4604
|
-
if (
|
|
5147
|
+
if (fs3.existsSync(configDest)) {
|
|
4605
5148
|
try {
|
|
4606
|
-
const config = JSON.parse(
|
|
5149
|
+
const config = JSON.parse(fs3.readFileSync(configDest, "utf-8"));
|
|
4607
5150
|
const configChecks = [];
|
|
4608
5151
|
if (config.github?.owner && config.github?.repo) {
|
|
4609
5152
|
configChecks.push({ name: "github.owner/repo", ok: true, detail: `${config.github.owner}/${config.github.repo}` });
|
|
@@ -4633,11 +5176,11 @@ function initCommand(opts) {
|
|
|
4633
5176
|
const filesToCommit = [
|
|
4634
5177
|
".github/workflows/kody.yml",
|
|
4635
5178
|
"kody.config.json"
|
|
4636
|
-
].filter((f) =>
|
|
5179
|
+
].filter((f) => fs3.existsSync(path2.join(cwd, f)));
|
|
4637
5180
|
if (filesToCommit.length > 0) {
|
|
4638
5181
|
try {
|
|
4639
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
4640
|
-
|
|
5182
|
+
const fullPaths = filesToCommit.map((f) => path2.join(cwd, f));
|
|
5183
|
+
execFileSync3("npx", ["prettier", "--write", ...fullPaths], {
|
|
4641
5184
|
cwd,
|
|
4642
5185
|
encoding: "utf-8",
|
|
4643
5186
|
timeout: 3e4,
|
|
@@ -4648,13 +5191,13 @@ function initCommand(opts) {
|
|
|
4648
5191
|
}
|
|
4649
5192
|
if (filesToCommit.length > 0) {
|
|
4650
5193
|
try {
|
|
4651
|
-
|
|
4652
|
-
const staged =
|
|
5194
|
+
execFileSync3("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
5195
|
+
const staged = execFileSync3("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
4653
5196
|
if (staged) {
|
|
4654
|
-
|
|
5197
|
+
execFileSync3("git", ["commit", "-m", "chore: Add Kody Engine workflow and config\n\nAdd GitHub Actions workflow and auto-detected configuration for Kody Engine Lite."], { cwd, stdio: "pipe" });
|
|
4655
5198
|
console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
|
|
4656
5199
|
try {
|
|
4657
|
-
|
|
5200
|
+
execFileSync3("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
4658
5201
|
console.log(" \u2713 Pushed to origin");
|
|
4659
5202
|
} catch {
|
|
4660
5203
|
console.log(" \u25CB Push failed \u2014 run 'git push' manually");
|
|
@@ -4696,22 +5239,300 @@ function initCommand(opts) {
|
|
|
4696
5239
|
console.log("");
|
|
4697
5240
|
}
|
|
4698
5241
|
}
|
|
5242
|
+
|
|
5243
|
+
// src/bin/commands/bootstrap.ts
|
|
5244
|
+
init_architecture_detection();
|
|
5245
|
+
import * as fs7 from "fs";
|
|
5246
|
+
import * as path6 from "path";
|
|
5247
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
5248
|
+
|
|
5249
|
+
// src/bin/qa-guide.ts
|
|
5250
|
+
import * as fs5 from "fs";
|
|
5251
|
+
import * as path4 from "path";
|
|
5252
|
+
function discoverQaContext(cwd) {
|
|
5253
|
+
const result = {
|
|
5254
|
+
routes: [],
|
|
5255
|
+
authFiles: [],
|
|
5256
|
+
loginPage: null,
|
|
5257
|
+
adminPath: null,
|
|
5258
|
+
roles: [],
|
|
5259
|
+
devCommand: "",
|
|
5260
|
+
devPort: 3e3
|
|
5261
|
+
};
|
|
5262
|
+
try {
|
|
5263
|
+
const pkg = JSON.parse(fs5.readFileSync(path4.join(cwd, "package.json"), "utf-8"));
|
|
5264
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5265
|
+
const pm = fs5.existsSync(path4.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs5.existsSync(path4.join(cwd, "yarn.lock")) ? "yarn" : "npm";
|
|
5266
|
+
if (pkg.scripts?.dev) result.devCommand = `${pm} dev`;
|
|
5267
|
+
if (allDeps.next || allDeps.nuxt) result.devPort = 3e3;
|
|
5268
|
+
else if (allDeps.vite) result.devPort = 5173;
|
|
5269
|
+
} catch {
|
|
5270
|
+
}
|
|
5271
|
+
const appDirs = ["src/app", "app"];
|
|
5272
|
+
for (const appDir of appDirs) {
|
|
5273
|
+
const fullAppDir = path4.join(cwd, appDir);
|
|
5274
|
+
if (!fs5.existsSync(fullAppDir)) continue;
|
|
5275
|
+
scanRoutes(fullAppDir, appDir, "", result);
|
|
5276
|
+
break;
|
|
5277
|
+
}
|
|
5278
|
+
const authPatterns = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
5279
|
+
for (const p of authPatterns) {
|
|
5280
|
+
if (fs5.existsSync(path4.join(cwd, p))) result.authFiles.push(p);
|
|
5281
|
+
}
|
|
5282
|
+
const authConfigGlobs = [
|
|
5283
|
+
"src/app/api/auth",
|
|
5284
|
+
"src/auth",
|
|
5285
|
+
"src/lib/auth",
|
|
5286
|
+
"auth.config.ts",
|
|
5287
|
+
"auth.ts",
|
|
5288
|
+
"src/app/api/oauth"
|
|
5289
|
+
];
|
|
5290
|
+
for (const g of authConfigGlobs) {
|
|
5291
|
+
if (fs5.existsSync(path4.join(cwd, g))) result.authFiles.push(g);
|
|
5292
|
+
}
|
|
5293
|
+
try {
|
|
5294
|
+
const rolePaths = [
|
|
5295
|
+
"src/types",
|
|
5296
|
+
"src/lib",
|
|
5297
|
+
"src/utils",
|
|
5298
|
+
"src/constants",
|
|
5299
|
+
"src/access",
|
|
5300
|
+
"src/collections"
|
|
5301
|
+
];
|
|
5302
|
+
for (const rp of rolePaths) {
|
|
5303
|
+
const dir = path4.join(cwd, rp);
|
|
5304
|
+
if (!fs5.existsSync(dir)) continue;
|
|
5305
|
+
const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
5306
|
+
for (const f of files) {
|
|
5307
|
+
try {
|
|
5308
|
+
const content = fs5.readFileSync(path4.join(dir, f), "utf-8").slice(0, 5e3);
|
|
5309
|
+
const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
|
|
5310
|
+
if (roleMatches) {
|
|
5311
|
+
for (const m of roleMatches) {
|
|
5312
|
+
const val = m.match(/['"](\w+)['"]/);
|
|
5313
|
+
if (val && !result.roles.includes(val[1])) result.roles.push(val[1]);
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
const enumMatch = content.match(/(?:enum|type)\s+\w*[Rr]ole\w*\s*[={]([^}]+)/s);
|
|
5317
|
+
if (enumMatch) {
|
|
5318
|
+
const vals = enumMatch[1].match(/['"](\w+)['"]/g);
|
|
5319
|
+
if (vals) {
|
|
5320
|
+
for (const v of vals) {
|
|
5321
|
+
const clean = v.replace(/['"]/g, "");
|
|
5322
|
+
if (!result.roles.includes(clean)) result.roles.push(clean);
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
} catch {
|
|
5327
|
+
}
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
} catch {
|
|
5331
|
+
}
|
|
5332
|
+
return result;
|
|
5333
|
+
}
|
|
5334
|
+
function scanRoutes(dir, baseDir, prefix, result) {
|
|
5335
|
+
let entries;
|
|
5336
|
+
try {
|
|
5337
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
5338
|
+
} catch {
|
|
5339
|
+
return;
|
|
5340
|
+
}
|
|
5341
|
+
const hasPage = entries.some((e) => e.isFile() && /^page\.(tsx?|jsx?)$/.test(e.name));
|
|
5342
|
+
if (hasPage) {
|
|
5343
|
+
const routePath = prefix || "/";
|
|
5344
|
+
const group = prefix.startsWith("/admin") ? "admin" : prefix.includes("/login") ? "auth" : prefix.includes("/signup") ? "auth" : prefix.includes("/api") ? "api" : "frontend";
|
|
5345
|
+
result.routes.push({ path: routePath, group });
|
|
5346
|
+
if (prefix.includes("/login")) result.loginPage = routePath;
|
|
5347
|
+
if (prefix.startsWith("/admin") && !result.adminPath) result.adminPath = prefix;
|
|
5348
|
+
}
|
|
5349
|
+
for (const entry of entries) {
|
|
5350
|
+
if (!entry.isDirectory()) continue;
|
|
5351
|
+
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
5352
|
+
let segment = entry.name;
|
|
5353
|
+
if (segment.startsWith("(") && segment.endsWith(")")) {
|
|
5354
|
+
scanRoutes(path4.join(dir, entry.name), baseDir, prefix, result);
|
|
5355
|
+
continue;
|
|
5356
|
+
}
|
|
5357
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
5358
|
+
segment = `:${segment.slice(1, -1)}`;
|
|
5359
|
+
}
|
|
5360
|
+
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
|
5361
|
+
segment = `:${segment.slice(2, -2)}?`;
|
|
5362
|
+
}
|
|
5363
|
+
scanRoutes(path4.join(dir, entry.name), baseDir, `${prefix}/${segment}`, result);
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
function generateQaGuide(discovery) {
|
|
5367
|
+
const lines = ["# QA Guide", "", "## Authentication", ""];
|
|
5368
|
+
if (discovery.loginPage) {
|
|
5369
|
+
lines.push(`- Login page: \`${discovery.loginPage}\``);
|
|
5370
|
+
}
|
|
5371
|
+
lines.push(
|
|
5372
|
+
"",
|
|
5373
|
+
"### Test Accounts",
|
|
5374
|
+
"<!-- Fill in your test/preview environment credentials below -->",
|
|
5375
|
+
"| Role | Email | Password |",
|
|
5376
|
+
"|------|-------|----------|",
|
|
5377
|
+
"| Admin | admin@example.com | CHANGE_ME |",
|
|
5378
|
+
"| User | user@example.com | CHANGE_ME |",
|
|
5379
|
+
"",
|
|
5380
|
+
"### Login Steps",
|
|
5381
|
+
`1. Navigate to \`${discovery.loginPage ?? "/login"}\``,
|
|
5382
|
+
"2. Enter credentials from the test accounts table above",
|
|
5383
|
+
"3. Submit the login form",
|
|
5384
|
+
"4. Verify redirect to dashboard or home page"
|
|
5385
|
+
);
|
|
5386
|
+
if (discovery.authFiles.length > 0) {
|
|
5387
|
+
lines.push("", "### Auth Files");
|
|
5388
|
+
for (const f of discovery.authFiles) {
|
|
5389
|
+
lines.push(`- \`${f}\``);
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5392
|
+
if (discovery.roles.length > 0) {
|
|
5393
|
+
lines.push("", "## Roles", "");
|
|
5394
|
+
for (const role of discovery.roles) {
|
|
5395
|
+
lines.push(`- \`${role}\``);
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
lines.push("", "## Key Pages", "");
|
|
5399
|
+
const groups = {};
|
|
5400
|
+
for (const route of discovery.routes) {
|
|
5401
|
+
if (!groups[route.group]) groups[route.group] = [];
|
|
5402
|
+
groups[route.group].push(route.path);
|
|
5403
|
+
}
|
|
5404
|
+
for (const [group, routes] of Object.entries(groups)) {
|
|
5405
|
+
lines.push(`### ${group.charAt(0).toUpperCase() + group.slice(1)}`);
|
|
5406
|
+
const sorted = routes.sort();
|
|
5407
|
+
for (const r of sorted.slice(0, 20)) {
|
|
5408
|
+
lines.push(`- \`${r}\``);
|
|
5409
|
+
}
|
|
5410
|
+
if (sorted.length > 20) {
|
|
5411
|
+
lines.push(`- ... and ${sorted.length - 20} more`);
|
|
5412
|
+
}
|
|
5413
|
+
lines.push("");
|
|
5414
|
+
}
|
|
5415
|
+
lines.push(
|
|
5416
|
+
"## Dev Server",
|
|
5417
|
+
"",
|
|
5418
|
+
`- Command: \`${discovery.devCommand || "pnpm dev"}\``,
|
|
5419
|
+
`- URL: \`http://localhost:${discovery.devPort}\``,
|
|
5420
|
+
""
|
|
5421
|
+
);
|
|
5422
|
+
return lines.join("\n");
|
|
5423
|
+
}
|
|
5424
|
+
|
|
5425
|
+
// src/bin/skills.ts
|
|
5426
|
+
import * as fs6 from "fs";
|
|
5427
|
+
import * as path5 from "path";
|
|
5428
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
5429
|
+
var SKILL_MAPPINGS = [
|
|
5430
|
+
{
|
|
5431
|
+
detect: (deps) => "next" in deps,
|
|
5432
|
+
skills: [
|
|
5433
|
+
{ package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
|
|
5434
|
+
]
|
|
5435
|
+
},
|
|
5436
|
+
{
|
|
5437
|
+
detect: (deps) => "react" in deps && !("next" in deps),
|
|
5438
|
+
skills: [
|
|
5439
|
+
{ package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
|
|
5440
|
+
]
|
|
5441
|
+
},
|
|
5442
|
+
{
|
|
5443
|
+
detect: (deps) => FRONTEND_DEPS.some((d) => d in deps),
|
|
5444
|
+
skills: [
|
|
5445
|
+
{ package: "microsoft/playwright-cli@playwright-cli", label: "Playwright browser automation" }
|
|
5446
|
+
]
|
|
5447
|
+
}
|
|
5448
|
+
];
|
|
5449
|
+
function detectSkillsForProject(cwd) {
|
|
5450
|
+
const pkgPath = path5.join(cwd, "package.json");
|
|
5451
|
+
if (!fs6.existsSync(pkgPath)) return [];
|
|
5452
|
+
try {
|
|
5453
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
5454
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5455
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5456
|
+
const skills = [];
|
|
5457
|
+
for (const mapping of SKILL_MAPPINGS) {
|
|
5458
|
+
if (mapping.detect(allDeps)) {
|
|
5459
|
+
for (const skill of mapping.skills) {
|
|
5460
|
+
if (!seen.has(skill.package)) {
|
|
5461
|
+
seen.add(skill.package);
|
|
5462
|
+
skills.push(skill);
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5465
|
+
}
|
|
5466
|
+
}
|
|
5467
|
+
return skills;
|
|
5468
|
+
} catch {
|
|
5469
|
+
return [];
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
function installSkillsForProject(cwd) {
|
|
5473
|
+
const skills = detectSkillsForProject(cwd);
|
|
5474
|
+
if (skills.length === 0) {
|
|
5475
|
+
console.log(" \u25CB No skills to install (no frontend framework detected)");
|
|
5476
|
+
return [];
|
|
5477
|
+
}
|
|
5478
|
+
let installedSkills = {};
|
|
5479
|
+
const lockPath = path5.join(cwd, "skills-lock.json");
|
|
5480
|
+
if (fs6.existsSync(lockPath)) {
|
|
5481
|
+
try {
|
|
5482
|
+
const lock = JSON.parse(fs6.readFileSync(lockPath, "utf-8"));
|
|
5483
|
+
installedSkills = lock.skills ?? {};
|
|
5484
|
+
} catch {
|
|
5485
|
+
}
|
|
5486
|
+
}
|
|
5487
|
+
const installedPaths = [];
|
|
5488
|
+
for (const skill of skills) {
|
|
5489
|
+
const skillName = skill.package.split("@").pop() ?? "";
|
|
5490
|
+
if (skillName in installedSkills) {
|
|
5491
|
+
console.log(` \u25CB ${skill.label} \u2014 already installed`);
|
|
5492
|
+
const agentPath = `.agents/skills/${skillName}`;
|
|
5493
|
+
const claudePath = `.claude/skills/${skillName}`;
|
|
5494
|
+
if (fs6.existsSync(path5.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
5495
|
+
if (fs6.existsSync(path5.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
5496
|
+
continue;
|
|
5497
|
+
}
|
|
5498
|
+
try {
|
|
5499
|
+
console.log(` Installing: ${skill.label} (${skill.package})`);
|
|
5500
|
+
execFileSync4("npx", ["skills", "add", skill.package, "--yes"], {
|
|
5501
|
+
cwd,
|
|
5502
|
+
encoding: "utf-8",
|
|
5503
|
+
timeout: 6e4,
|
|
5504
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5505
|
+
});
|
|
5506
|
+
const installedName = skill.package.split("@").pop() ?? "";
|
|
5507
|
+
const agentPath = `.agents/skills/${installedName}`;
|
|
5508
|
+
const claudePath = `.claude/skills/${installedName}`;
|
|
5509
|
+
if (fs6.existsSync(path5.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
5510
|
+
if (fs6.existsSync(path5.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
5511
|
+
console.log(` \u2713 ${skill.label}`);
|
|
5512
|
+
} catch {
|
|
5513
|
+
console.log(` \u2717 ${skill.label} \u2014 failed to install`);
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
return installedPaths;
|
|
5517
|
+
}
|
|
5518
|
+
|
|
5519
|
+
// src/bin/commands/bootstrap.ts
|
|
4699
5520
|
var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
|
|
4700
5521
|
function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
4701
|
-
const srcDir =
|
|
4702
|
-
const baseDir =
|
|
5522
|
+
const srcDir = path6.join(cwd, "src");
|
|
5523
|
+
const baseDir = fs7.existsSync(srcDir) ? srcDir : cwd;
|
|
4703
5524
|
const results = [];
|
|
4704
5525
|
function walk(dir) {
|
|
4705
5526
|
const entries = [];
|
|
4706
5527
|
try {
|
|
4707
|
-
for (const entry of
|
|
5528
|
+
for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
|
|
4708
5529
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
4709
|
-
const full =
|
|
5530
|
+
const full = path6.join(dir, entry.name);
|
|
4710
5531
|
if (entry.isDirectory()) {
|
|
4711
5532
|
entries.push(...walk(full));
|
|
4712
5533
|
} else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
|
|
4713
5534
|
try {
|
|
4714
|
-
const stat =
|
|
5535
|
+
const stat = fs7.statSync(full);
|
|
4715
5536
|
if (stat.size >= 200 && stat.size <= 5e3) {
|
|
4716
5537
|
entries.push({ filePath: full, size: stat.size });
|
|
4717
5538
|
}
|
|
@@ -4725,8 +5546,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
|
|
|
4725
5546
|
}
|
|
4726
5547
|
const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
|
|
4727
5548
|
for (const { filePath } of files) {
|
|
4728
|
-
const rel =
|
|
4729
|
-
const content =
|
|
5549
|
+
const rel = path6.relative(cwd, filePath);
|
|
5550
|
+
const content = fs7.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
|
|
4730
5551
|
results.push(`### File: ${rel}
|
|
4731
5552
|
\`\`\`typescript
|
|
4732
5553
|
${content}
|
|
@@ -4738,9 +5559,9 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
4738
5559
|
try {
|
|
4739
5560
|
let repoSlug = "";
|
|
4740
5561
|
try {
|
|
4741
|
-
const configPath =
|
|
4742
|
-
if (
|
|
4743
|
-
const config = JSON.parse(
|
|
5562
|
+
const configPath = path6.join(cwd, "kody.config.json");
|
|
5563
|
+
if (fs7.existsSync(configPath)) {
|
|
5564
|
+
const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
4744
5565
|
if (config.github?.owner && config.github?.repo) {
|
|
4745
5566
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
4746
5567
|
}
|
|
@@ -4748,7 +5569,7 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
4748
5569
|
} catch {
|
|
4749
5570
|
}
|
|
4750
5571
|
if (!repoSlug) return;
|
|
4751
|
-
|
|
5572
|
+
execFileSync5("gh", [
|
|
4752
5573
|
"issue",
|
|
4753
5574
|
"comment",
|
|
4754
5575
|
String(issueNumber),
|
|
@@ -4765,7 +5586,7 @@ function ghComment(issueNumber, body, cwd) {
|
|
|
4765
5586
|
} catch {
|
|
4766
5587
|
}
|
|
4767
5588
|
}
|
|
4768
|
-
function bootstrapCommand(opts
|
|
5589
|
+
function bootstrapCommand(opts, pkgRoot) {
|
|
4769
5590
|
const cwd = process.cwd();
|
|
4770
5591
|
const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
|
|
4771
5592
|
console.log(`
|
|
@@ -4775,8 +5596,8 @@ function bootstrapCommand(opts = { force: false }) {
|
|
|
4775
5596
|
ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
|
|
4776
5597
|
}
|
|
4777
5598
|
const readIfExists = (rel, maxChars = 3e3) => {
|
|
4778
|
-
const p =
|
|
4779
|
-
if (
|
|
5599
|
+
const p = path6.join(cwd, rel);
|
|
5600
|
+
if (fs7.existsSync(p)) return fs7.readFileSync(p, "utf-8").slice(0, maxChars);
|
|
4780
5601
|
return null;
|
|
4781
5602
|
};
|
|
4782
5603
|
let repoContext = "";
|
|
@@ -4811,14 +5632,14 @@ ${sampleFiles}
|
|
|
4811
5632
|
|
|
4812
5633
|
`;
|
|
4813
5634
|
try {
|
|
4814
|
-
const topDirs =
|
|
5635
|
+
const topDirs = fs7.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
|
|
4815
5636
|
repoContext += `## Top-level directories
|
|
4816
5637
|
${topDirs.join(", ")}
|
|
4817
5638
|
|
|
4818
5639
|
`;
|
|
4819
|
-
const srcDir =
|
|
4820
|
-
if (
|
|
4821
|
-
const srcDirs =
|
|
5640
|
+
const srcDir = path6.join(cwd, "src");
|
|
5641
|
+
if (fs7.existsSync(srcDir)) {
|
|
5642
|
+
const srcDirs = fs7.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4822
5643
|
if (srcDirs.length > 0) repoContext += `## src/ subdirectories
|
|
4823
5644
|
${srcDirs.join(", ")}
|
|
4824
5645
|
|
|
@@ -4828,19 +5649,19 @@ ${srcDirs.join(", ")}
|
|
|
4828
5649
|
}
|
|
4829
5650
|
const existingFiles = [];
|
|
4830
5651
|
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"]) {
|
|
4831
|
-
if (
|
|
5652
|
+
if (fs7.existsSync(path6.join(cwd, f))) existingFiles.push(f);
|
|
4832
5653
|
}
|
|
4833
5654
|
if (existingFiles.length) repoContext += `## Config files present
|
|
4834
5655
|
${existingFiles.join(", ")}
|
|
4835
5656
|
|
|
4836
5657
|
`;
|
|
4837
5658
|
console.log("\u2500\u2500 Project Memory \u2500\u2500");
|
|
4838
|
-
const memoryDir =
|
|
4839
|
-
|
|
4840
|
-
const archPath =
|
|
4841
|
-
const conventionsPath =
|
|
4842
|
-
const existingArch =
|
|
4843
|
-
const existingConv =
|
|
5659
|
+
const memoryDir = path6.join(cwd, ".kody", "memory");
|
|
5660
|
+
fs7.mkdirSync(memoryDir, { recursive: true });
|
|
5661
|
+
const archPath = path6.join(memoryDir, "architecture.md");
|
|
5662
|
+
const conventionsPath = path6.join(memoryDir, "conventions.md");
|
|
5663
|
+
const existingArch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
|
|
5664
|
+
const existingConv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
|
|
4844
5665
|
const hasExisting = !!(existingArch || existingConv);
|
|
4845
5666
|
const extendInstruction = hasExisting && !opts.force ? `
|
|
4846
5667
|
## Existing Documentation (EXTEND, do not replace)
|
|
@@ -4881,7 +5702,7 @@ Output ONLY valid JSON. No markdown fences. No explanation.
|
|
|
4881
5702
|
${repoContext}`;
|
|
4882
5703
|
console.log(" \u23F3 Analyzing project...");
|
|
4883
5704
|
try {
|
|
4884
|
-
const output =
|
|
5705
|
+
const output = execFileSync5("claude", [
|
|
4885
5706
|
"--print",
|
|
4886
5707
|
"--model",
|
|
4887
5708
|
"haiku",
|
|
@@ -4896,12 +5717,12 @@ ${repoContext}`;
|
|
|
4896
5717
|
const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
|
|
4897
5718
|
const parsed = JSON.parse(cleaned);
|
|
4898
5719
|
if (parsed.architecture) {
|
|
4899
|
-
|
|
5720
|
+
fs7.writeFileSync(archPath, parsed.architecture);
|
|
4900
5721
|
const lineCount = parsed.architecture.split("\n").length;
|
|
4901
5722
|
console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
|
|
4902
5723
|
}
|
|
4903
5724
|
if (parsed.conventions) {
|
|
4904
|
-
|
|
5725
|
+
fs7.writeFileSync(conventionsPath, parsed.conventions);
|
|
4905
5726
|
const lineCount = parsed.conventions.split("\n").length;
|
|
4906
5727
|
console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
|
|
4907
5728
|
}
|
|
@@ -4910,39 +5731,39 @@ ${repoContext}`;
|
|
|
4910
5731
|
const detected = detectArchitectureBasic(cwd);
|
|
4911
5732
|
if (detected.length > 0) {
|
|
4912
5733
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4913
|
-
|
|
5734
|
+
fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
|
|
4914
5735
|
|
|
4915
5736
|
## Overview
|
|
4916
5737
|
${detected.join("\n")}
|
|
4917
5738
|
`);
|
|
4918
5739
|
console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
|
|
4919
5740
|
}
|
|
4920
|
-
|
|
5741
|
+
fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
|
|
4921
5742
|
console.log(" \u2713 .kody/memory/conventions.md (seed)");
|
|
4922
5743
|
}
|
|
4923
5744
|
console.log("\n\u2500\u2500 Step Files \u2500\u2500");
|
|
4924
|
-
const stepsDir =
|
|
4925
|
-
|
|
4926
|
-
const arch =
|
|
4927
|
-
const conv =
|
|
5745
|
+
const stepsDir = path6.join(cwd, ".kody", "steps");
|
|
5746
|
+
fs7.mkdirSync(stepsDir, { recursive: true });
|
|
5747
|
+
const arch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
|
|
5748
|
+
const conv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
|
|
4928
5749
|
console.log(" \u23F3 Customizing step files...");
|
|
4929
5750
|
let stepCount = 0;
|
|
4930
5751
|
for (const stage of STEP_STAGES) {
|
|
4931
|
-
const templatePath =
|
|
4932
|
-
if (!
|
|
5752
|
+
const templatePath = path6.join(pkgRoot, "prompts", `${stage}.md`);
|
|
5753
|
+
if (!fs7.existsSync(templatePath)) {
|
|
4933
5754
|
console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
|
|
4934
5755
|
continue;
|
|
4935
5756
|
}
|
|
4936
|
-
const stepOutputPath =
|
|
4937
|
-
if (
|
|
5757
|
+
const stepOutputPath = path6.join(stepsDir, `${stage}.md`);
|
|
5758
|
+
if (fs7.existsSync(stepOutputPath) && !opts.force) {
|
|
4938
5759
|
console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
|
|
4939
5760
|
continue;
|
|
4940
5761
|
}
|
|
4941
|
-
const defaultPrompt =
|
|
5762
|
+
const defaultPrompt = fs7.readFileSync(templatePath, "utf-8");
|
|
4942
5763
|
const contextPlaceholder = "{{TASK_CONTEXT}}";
|
|
4943
5764
|
const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
|
|
4944
5765
|
if (placeholderIdx === -1) {
|
|
4945
|
-
|
|
5766
|
+
fs7.copyFileSync(templatePath, stepOutputPath);
|
|
4946
5767
|
stepCount++;
|
|
4947
5768
|
console.log(` \u2713 ${stage}.md`);
|
|
4948
5769
|
continue;
|
|
@@ -4984,7 +5805,7 @@ ${repoContext}
|
|
|
4984
5805
|
|
|
4985
5806
|
REMINDER: Output the full prompt template first (unchanged), then your three appended sections. Do NOT include "${contextPlaceholder}".`;
|
|
4986
5807
|
try {
|
|
4987
|
-
const output =
|
|
5808
|
+
const output = execFileSync5("claude", [
|
|
4988
5809
|
"--print",
|
|
4989
5810
|
"--model",
|
|
4990
5811
|
"haiku",
|
|
@@ -4999,23 +5820,40 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
4999
5820
|
let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
|
|
5000
5821
|
cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
|
|
5001
5822
|
const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
|
|
5002
|
-
|
|
5823
|
+
fs7.writeFileSync(stepOutputPath, finalPrompt);
|
|
5003
5824
|
stepCount++;
|
|
5004
5825
|
console.log(` \u2713 ${stage}.md`);
|
|
5005
5826
|
} catch {
|
|
5006
5827
|
console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
|
|
5007
|
-
|
|
5828
|
+
fs7.copyFileSync(templatePath, stepOutputPath);
|
|
5008
5829
|
stepCount++;
|
|
5009
5830
|
}
|
|
5010
5831
|
}
|
|
5011
5832
|
console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
|
|
5833
|
+
console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
|
|
5834
|
+
const qaGuidePath = path6.join(cwd, ".kody", "qa-guide.md");
|
|
5835
|
+
if (!fs7.existsSync(qaGuidePath) || opts.force) {
|
|
5836
|
+
const discovery = discoverQaContext(cwd);
|
|
5837
|
+
if (discovery.routes.length > 0) {
|
|
5838
|
+
const qaGuide = generateQaGuide(discovery);
|
|
5839
|
+
fs7.writeFileSync(qaGuidePath, qaGuide);
|
|
5840
|
+
console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
|
|
5841
|
+
if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
|
|
5842
|
+
if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
|
|
5843
|
+
console.log(" \u2139 Add QA_ADMIN_EMAIL, QA_ADMIN_PASSWORD, QA_USER_EMAIL, QA_USER_PASSWORD as GitHub secrets");
|
|
5844
|
+
} else {
|
|
5845
|
+
console.log(" \u25CB No routes detected \u2014 skipping QA guide");
|
|
5846
|
+
}
|
|
5847
|
+
} else {
|
|
5848
|
+
console.log(" \u25CB .kody/qa-guide.md already exists (use --force to regenerate)");
|
|
5849
|
+
}
|
|
5012
5850
|
console.log("\n\u2500\u2500 Labels \u2500\u2500");
|
|
5013
5851
|
try {
|
|
5014
5852
|
let repoSlug = "";
|
|
5015
5853
|
try {
|
|
5016
|
-
const configPath =
|
|
5017
|
-
if (
|
|
5018
|
-
const config = JSON.parse(
|
|
5854
|
+
const configPath = path6.join(cwd, "kody.config.json");
|
|
5855
|
+
if (fs7.existsSync(configPath)) {
|
|
5856
|
+
const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
5019
5857
|
if (config.github?.owner && config.github?.repo) {
|
|
5020
5858
|
repoSlug = `${config.github.owner}/${config.github.repo}`;
|
|
5021
5859
|
}
|
|
@@ -5027,6 +5865,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5027
5865
|
{ name: "kody:planning", color: "c5def5", description: "Kody is analyzing and planning" },
|
|
5028
5866
|
{ name: "kody:building", color: "0e8a16", description: "Kody is building code" },
|
|
5029
5867
|
{ name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
|
|
5868
|
+
{ name: "kody:shipping", color: "1d76db", description: "Kody is creating the pull request" },
|
|
5030
5869
|
{ name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
|
|
5031
5870
|
{ name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
|
|
5032
5871
|
{ name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
|
|
@@ -5041,7 +5880,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5041
5880
|
];
|
|
5042
5881
|
for (const label of labels) {
|
|
5043
5882
|
try {
|
|
5044
|
-
|
|
5883
|
+
execFileSync5("gh", [
|
|
5045
5884
|
"label",
|
|
5046
5885
|
"create",
|
|
5047
5886
|
label.name,
|
|
@@ -5061,7 +5900,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5061
5900
|
console.log(` \u2713 ${label.name}`);
|
|
5062
5901
|
} catch {
|
|
5063
5902
|
try {
|
|
5064
|
-
|
|
5903
|
+
execFileSync5("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
|
|
5065
5904
|
cwd,
|
|
5066
5905
|
encoding: "utf-8",
|
|
5067
5906
|
timeout: 1e4,
|
|
@@ -5085,22 +5924,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5085
5924
|
const filesToCommit = [
|
|
5086
5925
|
".kody/memory/architecture.md",
|
|
5087
5926
|
".kody/memory/conventions.md",
|
|
5927
|
+
".kody/qa-guide.md",
|
|
5088
5928
|
...installedSkillPaths
|
|
5089
|
-
].filter((f) =>
|
|
5090
|
-
if (
|
|
5929
|
+
].filter((f) => fs7.existsSync(path6.join(cwd, f)));
|
|
5930
|
+
if (fs7.existsSync(path6.join(cwd, "skills-lock.json"))) {
|
|
5091
5931
|
filesToCommit.push("skills-lock.json");
|
|
5092
5932
|
}
|
|
5093
5933
|
for (const stage of STEP_STAGES) {
|
|
5094
5934
|
const stepFile = `.kody/steps/${stage}.md`;
|
|
5095
|
-
if (
|
|
5935
|
+
if (fs7.existsSync(path6.join(cwd, stepFile))) {
|
|
5096
5936
|
filesToCommit.push(stepFile);
|
|
5097
5937
|
}
|
|
5098
5938
|
}
|
|
5099
5939
|
if (filesToCommit.length > 0) {
|
|
5100
5940
|
try {
|
|
5101
|
-
const fullPaths = filesToCommit.map((f) =>
|
|
5941
|
+
const fullPaths = filesToCommit.map((f) => path6.join(cwd, f));
|
|
5102
5942
|
for (let pass = 0; pass < 2; pass++) {
|
|
5103
|
-
|
|
5943
|
+
execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
|
|
5104
5944
|
cwd,
|
|
5105
5945
|
encoding: "utf-8",
|
|
5106
5946
|
timeout: 3e4,
|
|
@@ -5116,24 +5956,24 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5116
5956
|
try {
|
|
5117
5957
|
if (isCI3) {
|
|
5118
5958
|
const branchName = `kody-bootstrap-${Date.now()}`;
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
const staged =
|
|
5959
|
+
execFileSync5("git", ["checkout", "-b", branchName], { cwd, stdio: "pipe" });
|
|
5960
|
+
execFileSync5("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
5961
|
+
const staged = execFileSync5("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
5122
5962
|
if (staged) {
|
|
5123
|
-
|
|
5124
|
-
|
|
5963
|
+
execFileSync5("git", ["commit", "-m", "chore: Add Kody project memory and step files\n\nBootstrap Kody Engine with project-specific architecture, conventions, and pipeline step files."], { cwd, stdio: "pipe" });
|
|
5964
|
+
execFileSync5("git", ["push", "-u", "origin", branchName], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
5125
5965
|
console.log(` \u2713 Pushed branch: ${branchName}`);
|
|
5126
5966
|
let baseBranch = "main";
|
|
5127
5967
|
try {
|
|
5128
|
-
const configPath =
|
|
5129
|
-
if (
|
|
5130
|
-
const config = JSON.parse(
|
|
5968
|
+
const configPath = path6.join(cwd, "kody.config.json");
|
|
5969
|
+
if (fs7.existsSync(configPath)) {
|
|
5970
|
+
const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
5131
5971
|
baseBranch = config.git?.defaultBranch ?? "main";
|
|
5132
5972
|
}
|
|
5133
5973
|
} catch {
|
|
5134
5974
|
}
|
|
5135
5975
|
try {
|
|
5136
|
-
const prUrl =
|
|
5976
|
+
const prUrl = execFileSync5("gh", [
|
|
5137
5977
|
"pr",
|
|
5138
5978
|
"create",
|
|
5139
5979
|
"--title",
|
|
@@ -5172,13 +6012,13 @@ Create it manually.`, cwd);
|
|
|
5172
6012
|
console.log(" \u25CB No new changes to commit");
|
|
5173
6013
|
}
|
|
5174
6014
|
} else {
|
|
5175
|
-
|
|
5176
|
-
const staged =
|
|
6015
|
+
execFileSync5("git", ["add", ...filesToCommit], { cwd, stdio: "pipe" });
|
|
6016
|
+
const staged = execFileSync5("git", ["diff", "--cached", "--name-only"], { cwd, encoding: "utf-8" }).trim();
|
|
5177
6017
|
if (staged) {
|
|
5178
|
-
|
|
6018
|
+
execFileSync5("git", ["commit", "-m", "chore: Add Kody project memory and step files\n\nBootstrap Kody Engine with project-specific architecture, conventions, and pipeline step files."], { cwd, stdio: "pipe" });
|
|
5179
6019
|
console.log(` \u2713 Committed: ${filesToCommit.join(", ")}`);
|
|
5180
6020
|
try {
|
|
5181
|
-
|
|
6021
|
+
execFileSync5("git", ["push"], { cwd, stdio: "pipe", timeout: 6e4 });
|
|
5182
6022
|
console.log(" \u2713 Pushed to origin");
|
|
5183
6023
|
} catch {
|
|
5184
6024
|
console.log(" \u25CB Push failed \u2014 run 'git push' manually");
|
|
@@ -5198,131 +6038,24 @@ Create it manually.`, cwd);
|
|
|
5198
6038
|
console.log(" \u2713 Project bootstrap complete!");
|
|
5199
6039
|
console.log(" Kody now has project-specific memory and customized step files.\n");
|
|
5200
6040
|
}
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
else if (allDeps.express) detected.push(`- Framework: Express ${allDeps.express}`);
|
|
5211
|
-
else if (allDeps.fastify) detected.push(`- Framework: Fastify ${allDeps.fastify}`);
|
|
5212
|
-
else if (allDeps.hono) detected.push(`- Framework: Hono ${allDeps.hono}`);
|
|
5213
|
-
if (allDeps.typescript) detected.push(`- Language: TypeScript ${allDeps.typescript}`);
|
|
5214
|
-
if (allDeps.vitest) detected.push(`- Testing: vitest ${allDeps.vitest}`);
|
|
5215
|
-
else if (allDeps.jest) detected.push(`- Testing: jest ${allDeps.jest}`);
|
|
5216
|
-
if (allDeps.eslint) detected.push(`- Linting: eslint ${allDeps.eslint}`);
|
|
5217
|
-
if (allDeps.prettier) detected.push(`- Formatting: prettier ${allDeps.prettier}`);
|
|
5218
|
-
if (allDeps.prisma || allDeps["@prisma/client"]) detected.push("- ORM: Prisma");
|
|
5219
|
-
if (allDeps["drizzle-orm"]) detected.push("- ORM: Drizzle");
|
|
5220
|
-
if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push("- CMS: Payload CMS");
|
|
5221
|
-
if (allDeps.tailwindcss) detected.push(`- CSS: Tailwind CSS ${allDeps.tailwindcss}`);
|
|
5222
|
-
if (fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
|
|
5223
|
-
else if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) detected.push("- Package manager: yarn");
|
|
5224
|
-
else if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) detected.push("- Package manager: bun");
|
|
5225
|
-
else if (fs22.existsSync(path21.join(cwd, "package-lock.json"))) detected.push("- Package manager: npm");
|
|
5226
|
-
} catch {
|
|
5227
|
-
}
|
|
5228
|
-
}
|
|
5229
|
-
return detected;
|
|
5230
|
-
}
|
|
5231
|
-
var SKILL_MAPPINGS = [
|
|
5232
|
-
{
|
|
5233
|
-
detect: (deps) => "next" in deps,
|
|
5234
|
-
skills: [
|
|
5235
|
-
{ package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
|
|
5236
|
-
]
|
|
5237
|
-
},
|
|
5238
|
-
{
|
|
5239
|
-
detect: (deps) => "react" in deps && !("next" in deps),
|
|
5240
|
-
skills: [
|
|
5241
|
-
{ package: "vercel-labs/agent-skills@vercel-react-best-practices", label: "React best practices (Vercel)" }
|
|
5242
|
-
]
|
|
5243
|
-
},
|
|
5244
|
-
{
|
|
5245
|
-
detect: (deps) => FRONTEND_DEPS.some((d) => d in deps),
|
|
5246
|
-
skills: [
|
|
5247
|
-
{ package: "microsoft/playwright-cli@playwright-cli", label: "Playwright browser automation" }
|
|
5248
|
-
]
|
|
5249
|
-
}
|
|
5250
|
-
];
|
|
5251
|
-
function detectSkillsForProject(cwd) {
|
|
5252
|
-
const pkgPath = path21.join(cwd, "package.json");
|
|
5253
|
-
if (!fs22.existsSync(pkgPath)) return [];
|
|
5254
|
-
try {
|
|
5255
|
-
const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
|
|
5256
|
-
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
5257
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5258
|
-
const skills = [];
|
|
5259
|
-
for (const mapping of SKILL_MAPPINGS) {
|
|
5260
|
-
if (mapping.detect(allDeps)) {
|
|
5261
|
-
for (const skill of mapping.skills) {
|
|
5262
|
-
if (!seen.has(skill.package)) {
|
|
5263
|
-
seen.add(skill.package);
|
|
5264
|
-
skills.push(skill);
|
|
5265
|
-
}
|
|
5266
|
-
}
|
|
5267
|
-
}
|
|
5268
|
-
}
|
|
5269
|
-
return skills;
|
|
5270
|
-
} catch {
|
|
5271
|
-
return [];
|
|
5272
|
-
}
|
|
5273
|
-
}
|
|
5274
|
-
function installSkillsForProject(cwd) {
|
|
5275
|
-
const skills = detectSkillsForProject(cwd);
|
|
5276
|
-
if (skills.length === 0) {
|
|
5277
|
-
console.log(" \u25CB No skills to install (no frontend framework detected)");
|
|
5278
|
-
return [];
|
|
5279
|
-
}
|
|
5280
|
-
let installedSkills = {};
|
|
5281
|
-
const lockPath = path21.join(cwd, "skills-lock.json");
|
|
5282
|
-
if (fs22.existsSync(lockPath)) {
|
|
5283
|
-
try {
|
|
5284
|
-
const lock = JSON.parse(fs22.readFileSync(lockPath, "utf-8"));
|
|
5285
|
-
installedSkills = lock.skills ?? {};
|
|
5286
|
-
} catch {
|
|
5287
|
-
}
|
|
5288
|
-
}
|
|
5289
|
-
const installedPaths = [];
|
|
5290
|
-
for (const skill of skills) {
|
|
5291
|
-
const skillName = skill.package.split("@").pop() ?? "";
|
|
5292
|
-
if (skillName in installedSkills) {
|
|
5293
|
-
console.log(` \u25CB ${skill.label} \u2014 already installed`);
|
|
5294
|
-
const agentPath = `.agents/skills/${skillName}`;
|
|
5295
|
-
const claudePath = `.claude/skills/${skillName}`;
|
|
5296
|
-
if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
5297
|
-
if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
5298
|
-
continue;
|
|
5299
|
-
}
|
|
5300
|
-
try {
|
|
5301
|
-
console.log(` Installing: ${skill.label} (${skill.package})`);
|
|
5302
|
-
execFileSync12("npx", ["skills", "add", skill.package, "--yes"], {
|
|
5303
|
-
cwd,
|
|
5304
|
-
encoding: "utf-8",
|
|
5305
|
-
timeout: 6e4,
|
|
5306
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
5307
|
-
});
|
|
5308
|
-
const skillName2 = skill.package.split("@").pop() ?? "";
|
|
5309
|
-
const agentPath = `.agents/skills/${skillName2}`;
|
|
5310
|
-
const claudePath = `.claude/skills/${skillName2}`;
|
|
5311
|
-
if (fs22.existsSync(path21.join(cwd, agentPath))) installedPaths.push(agentPath);
|
|
5312
|
-
if (fs22.existsSync(path21.join(cwd, claudePath))) installedPaths.push(claudePath);
|
|
5313
|
-
console.log(` \u2713 ${skill.label}`);
|
|
5314
|
-
} catch (err) {
|
|
5315
|
-
console.log(` \u2717 ${skill.label} \u2014 failed to install`);
|
|
5316
|
-
}
|
|
5317
|
-
}
|
|
5318
|
-
return installedPaths;
|
|
6041
|
+
|
|
6042
|
+
// src/bin/cli.ts
|
|
6043
|
+
init_architecture_detection();
|
|
6044
|
+
var __dirname = path27.dirname(fileURLToPath(import.meta.url));
|
|
6045
|
+
var PKG_ROOT = path27.resolve(__dirname, "..", "..");
|
|
6046
|
+
function getVersion() {
|
|
6047
|
+
const pkgPath = path27.join(PKG_ROOT, "package.json");
|
|
6048
|
+
const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf-8"));
|
|
6049
|
+
return pkg.version;
|
|
5319
6050
|
}
|
|
5320
6051
|
var args = process.argv.slice(2);
|
|
5321
6052
|
var command = args[0];
|
|
5322
6053
|
if (command === "init") {
|
|
5323
|
-
initCommand({ force: args.includes("--force") });
|
|
6054
|
+
initCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
5324
6055
|
} else if (command === "bootstrap") {
|
|
5325
|
-
bootstrapCommand({ force: args.includes("--force") });
|
|
6056
|
+
bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
|
|
6057
|
+
} else if (command === "ci-parse") {
|
|
6058
|
+
Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
|
|
5326
6059
|
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
5327
6060
|
console.log(getVersion());
|
|
5328
6061
|
} else {
|
|
@@ -5330,7 +6063,7 @@ if (command === "init") {
|
|
|
5330
6063
|
}
|
|
5331
6064
|
export {
|
|
5332
6065
|
buildConfig,
|
|
5333
|
-
|
|
6066
|
+
checkCommand,
|
|
5334
6067
|
checkFile,
|
|
5335
6068
|
checkGhAuth,
|
|
5336
6069
|
checkGhRepoAccess,
|