@kody-ade/kody-engine-lite 0.1.63 → 0.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-runner.d.ts +4 -0
- package/dist/agent-runner.js +122 -0
- package/dist/bin/cli.js +162 -8
- package/dist/ci/parse-inputs.d.ts +6 -0
- package/dist/ci/parse-inputs.js +76 -0
- package/dist/ci/parse-safety.d.ts +6 -0
- package/dist/ci/parse-safety.js +22 -0
- package/dist/cli/args.d.ts +13 -0
- package/dist/cli/args.js +42 -0
- package/dist/cli/litellm.d.ts +2 -0
- package/dist/cli/litellm.js +85 -0
- package/dist/cli/task-resolution.d.ts +2 -0
- package/dist/cli/task-resolution.js +41 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +72 -0
- package/dist/context.d.ts +4 -0
- package/dist/context.js +83 -0
- package/dist/definitions.d.ts +3 -0
- package/dist/definitions.js +59 -0
- package/dist/entry.d.ts +1 -0
- package/dist/entry.js +236 -0
- package/dist/git-utils.d.ts +13 -0
- package/dist/git-utils.js +174 -0
- package/dist/github-api.d.ts +14 -0
- package/dist/github-api.js +114 -0
- package/dist/kody-utils.d.ts +1 -0
- package/dist/kody-utils.js +9 -0
- package/dist/learning/auto-learn.d.ts +2 -0
- package/dist/learning/auto-learn.js +169 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.js +51 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.js +20 -0
- package/dist/observer.d.ts +9 -0
- package/dist/observer.js +80 -0
- package/dist/pipeline/complexity.d.ts +3 -0
- package/dist/pipeline/complexity.js +12 -0
- package/dist/pipeline/executor-registry.d.ts +3 -0
- package/dist/pipeline/executor-registry.js +20 -0
- package/dist/pipeline/hooks.d.ts +17 -0
- package/dist/pipeline/hooks.js +110 -0
- package/dist/pipeline/questions.d.ts +2 -0
- package/dist/pipeline/questions.js +44 -0
- package/dist/pipeline/runner-selection.d.ts +2 -0
- package/dist/pipeline/runner-selection.js +13 -0
- package/dist/pipeline/state.d.ts +4 -0
- package/dist/pipeline/state.js +37 -0
- package/dist/pipeline.d.ts +3 -0
- package/dist/pipeline.js +213 -0
- package/dist/preflight.d.ts +1 -0
- package/dist/preflight.js +69 -0
- package/dist/retrospective.d.ts +26 -0
- package/dist/retrospective.js +211 -0
- package/dist/stages/agent.d.ts +2 -0
- package/dist/stages/agent.js +94 -0
- package/dist/stages/gate.d.ts +2 -0
- package/dist/stages/gate.js +32 -0
- package/dist/stages/review.d.ts +2 -0
- package/dist/stages/review.js +32 -0
- package/dist/stages/ship.d.ts +3 -0
- package/dist/stages/ship.js +154 -0
- package/dist/stages/verify.d.ts +2 -0
- package/dist/stages/verify.js +94 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +1 -0
- package/dist/validators.d.ts +8 -0
- package/dist/validators.js +42 -0
- package/dist/verify-runner.d.ts +11 -0
- package/dist/verify-runner.js +110 -0
- package/kody.config.schema.json +66 -0
- package/package.json +8 -9
- package/prompts/taskify.md +5 -0
- package/templates/kody.yml +6 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawn, execFileSync } from "child_process";
|
|
2
|
+
const SIGKILL_GRACE_MS = 5000;
|
|
3
|
+
const STDERR_TAIL_CHARS = 500;
|
|
4
|
+
function writeStdin(child, prompt) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
if (!child.stdin) {
|
|
7
|
+
resolve();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
child.stdin.write(prompt, (err) => {
|
|
11
|
+
if (err)
|
|
12
|
+
reject(err);
|
|
13
|
+
else {
|
|
14
|
+
child.stdin.end();
|
|
15
|
+
resolve();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function waitForProcess(child, timeout) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const stdoutChunks = [];
|
|
23
|
+
const stderrChunks = [];
|
|
24
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
25
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
child.kill("SIGTERM");
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
if (!child.killed)
|
|
30
|
+
child.kill("SIGKILL");
|
|
31
|
+
}, SIGKILL_GRACE_MS);
|
|
32
|
+
}, timeout);
|
|
33
|
+
child.on("exit", (code) => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
resolve({
|
|
36
|
+
code,
|
|
37
|
+
stdout: Buffer.concat(stdoutChunks).toString(),
|
|
38
|
+
stderr: Buffer.concat(stderrChunks).toString(),
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
child.on("error", (err) => {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
resolve({ code: -1, stdout: "", stderr: err.message });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function runSubprocess(command, args, prompt, timeout, options) {
|
|
48
|
+
const child = spawn(command, args, {
|
|
49
|
+
cwd: options?.cwd ?? process.cwd(),
|
|
50
|
+
env: {
|
|
51
|
+
...process.env,
|
|
52
|
+
SKIP_BUILD: "1",
|
|
53
|
+
SKIP_HOOKS: "1",
|
|
54
|
+
...options?.env,
|
|
55
|
+
},
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
await writeStdin(child, prompt);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return {
|
|
63
|
+
outcome: "failed",
|
|
64
|
+
error: `Failed to send prompt: ${err instanceof Error ? err.message : String(err)}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const { code, stdout, stderr } = await waitForProcess(child, timeout);
|
|
68
|
+
if (code === 0) {
|
|
69
|
+
return { outcome: "completed", output: stdout };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
outcome: code === null ? "timed_out" : "failed",
|
|
73
|
+
error: `Exit code ${code}\n${stderr.slice(-STDERR_TAIL_CHARS)}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function checkCommand(command, args) {
|
|
77
|
+
try {
|
|
78
|
+
execFileSync(command, args, { timeout: 10_000, stdio: "pipe" });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ─── Claude Code Runner ──────────────────────────────────────────────────────
|
|
86
|
+
export function createClaudeCodeRunner() {
|
|
87
|
+
return {
|
|
88
|
+
async run(_stageName, prompt, model, timeout, _taskDir, options) {
|
|
89
|
+
return runSubprocess("claude", [
|
|
90
|
+
"--print",
|
|
91
|
+
"--model", model,
|
|
92
|
+
"--dangerously-skip-permissions",
|
|
93
|
+
"--allowedTools", "Bash,Edit,Read,Write,Glob,Grep",
|
|
94
|
+
], prompt, timeout, options);
|
|
95
|
+
},
|
|
96
|
+
async healthCheck() {
|
|
97
|
+
return checkCommand("claude", ["--version"]);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// ─── Runner Factory ──────────────────────────────────────────────────────────
|
|
102
|
+
const RUNNER_FACTORIES = {
|
|
103
|
+
"claude-code": createClaudeCodeRunner,
|
|
104
|
+
};
|
|
105
|
+
export function createRunners(config) {
|
|
106
|
+
// New multi-runner config
|
|
107
|
+
if (config.agent.runners && Object.keys(config.agent.runners).length > 0) {
|
|
108
|
+
const runners = {};
|
|
109
|
+
for (const [name, runnerConfig] of Object.entries(config.agent.runners)) {
|
|
110
|
+
const factory = RUNNER_FACTORIES[runnerConfig.type];
|
|
111
|
+
if (factory) {
|
|
112
|
+
runners[name] = factory();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return runners;
|
|
116
|
+
}
|
|
117
|
+
// Legacy single-runner fallback
|
|
118
|
+
const runnerType = config.agent.runner ?? "claude-code";
|
|
119
|
+
const factory = RUNNER_FACTORIES[runnerType];
|
|
120
|
+
const defaultName = config.agent.defaultRunner ?? "claude";
|
|
121
|
+
return { [defaultName]: factory ? factory() : createClaudeCodeRunner() };
|
|
122
|
+
}
|
package/dist/bin/cli.js
CHANGED
|
@@ -96,6 +96,9 @@ function createClaudeCodeRunner() {
|
|
|
96
96
|
"--allowedTools",
|
|
97
97
|
"Bash,Edit,Read,Write,Glob,Grep"
|
|
98
98
|
];
|
|
99
|
+
if (options?.mcpConfigJson) {
|
|
100
|
+
args2.push("--mcp-config", options.mcpConfigJson);
|
|
101
|
+
}
|
|
99
102
|
if (options?.sessionId) {
|
|
100
103
|
if (options.resumeSession) {
|
|
101
104
|
args2.push("--resume", options.sessionId);
|
|
@@ -274,7 +277,8 @@ function getProjectConfig() {
|
|
|
274
277
|
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
275
278
|
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
276
279
|
agent: { ...DEFAULT_CONFIG.agent, ...raw.agent },
|
|
277
|
-
contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers
|
|
280
|
+
contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
|
|
281
|
+
mcp: raw.mcp ? { enabled: false, servers: {}, stages: ["build", "verify", "review", "review-fix"], ...raw.mcp } : void 0
|
|
278
282
|
};
|
|
279
283
|
} catch {
|
|
280
284
|
logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
|
|
@@ -1104,6 +1108,35 @@ var init_context_tiers = __esm({
|
|
|
1104
1108
|
}
|
|
1105
1109
|
});
|
|
1106
1110
|
|
|
1111
|
+
// src/mcp-config.ts
|
|
1112
|
+
function buildMcpConfigJson(mcpConfig) {
|
|
1113
|
+
if (!mcpConfig?.enabled) return void 0;
|
|
1114
|
+
if (Object.keys(mcpConfig.servers).length === 0) return void 0;
|
|
1115
|
+
const config = { mcpServers: {} };
|
|
1116
|
+
const mcpServers = config.mcpServers;
|
|
1117
|
+
for (const [name, server] of Object.entries(mcpConfig.servers)) {
|
|
1118
|
+
mcpServers[name] = {
|
|
1119
|
+
command: server.command,
|
|
1120
|
+
args: server.args ?? [],
|
|
1121
|
+
...server.env ? { env: server.env } : {}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
return JSON.stringify(config);
|
|
1125
|
+
}
|
|
1126
|
+
function isMcpEnabledForStage(stageName, mcpConfig) {
|
|
1127
|
+
if (!mcpConfig?.enabled) return false;
|
|
1128
|
+
if (Object.keys(mcpConfig.servers).length === 0) return false;
|
|
1129
|
+
const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
|
|
1130
|
+
return allowedStages.includes(stageName);
|
|
1131
|
+
}
|
|
1132
|
+
var DEFAULT_MCP_STAGES;
|
|
1133
|
+
var init_mcp_config = __esm({
|
|
1134
|
+
"src/mcp-config.ts"() {
|
|
1135
|
+
"use strict";
|
|
1136
|
+
DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1107
1140
|
// src/context.ts
|
|
1108
1141
|
import * as fs5 from "fs";
|
|
1109
1142
|
import * as path5 from "path";
|
|
@@ -1194,18 +1227,132 @@ ${feedback}
|
|
|
1194
1227
|
}
|
|
1195
1228
|
return prompt.replace("{{TASK_CONTEXT}}", context);
|
|
1196
1229
|
}
|
|
1230
|
+
function taskHasUI(taskDir) {
|
|
1231
|
+
const taskJsonPath = path5.join(taskDir, "task.json");
|
|
1232
|
+
if (!fs5.existsSync(taskJsonPath)) return true;
|
|
1233
|
+
try {
|
|
1234
|
+
const taskDef = JSON.parse(fs5.readFileSync(taskJsonPath, "utf-8"));
|
|
1235
|
+
return taskDef.hasUI !== false;
|
|
1236
|
+
} catch {
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
function getDevServerInfo(taskDir) {
|
|
1241
|
+
const config = getProjectConfig();
|
|
1242
|
+
const ds = config.mcp?.devServer;
|
|
1243
|
+
if (!ds) return void 0;
|
|
1244
|
+
return {
|
|
1245
|
+
command: ds.command,
|
|
1246
|
+
url: ds.url,
|
|
1247
|
+
readyPattern: ds.readyPattern ?? "Ready in|compiled|started server|Local:",
|
|
1248
|
+
readyTimeout: ds.readyTimeout ?? 30
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function getBrowserToolGuidance(stageName, taskDir) {
|
|
1252
|
+
const devServer = getDevServerInfo(taskDir);
|
|
1253
|
+
const devServerBlock = devServer ? `
|
|
1254
|
+
### Dev Server Setup (REQUIRED before browsing)
|
|
1255
|
+
You MUST start the dev server before using any browser navigation tools:
|
|
1256
|
+
\`\`\`bash
|
|
1257
|
+
# Start the dev server in the background
|
|
1258
|
+
${devServer.command} &
|
|
1259
|
+
# Wait for it to be ready (look for "${devServer.readyPattern}" in output)
|
|
1260
|
+
sleep 5
|
|
1261
|
+
\`\`\`
|
|
1262
|
+
The dev server URL is: ${devServer.url}
|
|
1263
|
+
After you are done browsing, kill the dev server: \`kill %1 2>/dev/null || true\`` : `
|
|
1264
|
+
### Dev Server Setup (REQUIRED before browsing)
|
|
1265
|
+
You MUST start the project's dev server before using any browser navigation tools.
|
|
1266
|
+
Check package.json for the dev command (usually \`pnpm dev\` or \`npm run dev\`).
|
|
1267
|
+
\`\`\`bash
|
|
1268
|
+
# Start the dev server in the background
|
|
1269
|
+
pnpm dev &
|
|
1270
|
+
# Wait for it to be ready
|
|
1271
|
+
sleep 5
|
|
1272
|
+
\`\`\`
|
|
1273
|
+
After you are done browsing, kill the dev server: \`kill %1 2>/dev/null || true\``;
|
|
1274
|
+
if (stageName === "build" || stageName === "review-fix") {
|
|
1275
|
+
return `## Browser Visual Verification (MANDATORY for UI tasks)
|
|
1276
|
+
|
|
1277
|
+
This task involves UI changes. You MUST visually verify your implementation using the browser tools.
|
|
1278
|
+
${devServerBlock}
|
|
1279
|
+
|
|
1280
|
+
### Available Browser Tools
|
|
1281
|
+
- \`mcp__playwright__browser_navigate\` \u2014 go to a URL
|
|
1282
|
+
- \`mcp__playwright__browser_snapshot\` \u2014 capture accessibility tree (shows all elements, text, roles)
|
|
1283
|
+
- \`mcp__playwright__browser_take_screenshot\` \u2014 take a visual screenshot
|
|
1284
|
+
- \`mcp__playwright__browser_click\` \u2014 click an element (by text, role, or ref from snapshot)
|
|
1285
|
+
- \`mcp__playwright__browser_type\` \u2014 type text into an input field
|
|
1286
|
+
- \`mcp__playwright__browser_fill_form\` \u2014 fill multiple form fields at once
|
|
1287
|
+
- \`mcp__playwright__browser_hover\` \u2014 hover over an element (test hover states)
|
|
1288
|
+
- \`mcp__playwright__browser_select_option\` \u2014 select a dropdown option
|
|
1289
|
+
- \`mcp__playwright__browser_press_key\` \u2014 press keyboard keys (Enter, Escape, Tab, etc.)
|
|
1290
|
+
- \`mcp__playwright__browser_resize\` \u2014 resize viewport (test responsive layouts)
|
|
1291
|
+
- \`mcp__playwright__browser_wait_for\` \u2014 wait for text to appear/disappear
|
|
1292
|
+
- \`mcp__playwright__browser_evaluate\` \u2014 run JavaScript on the page
|
|
1293
|
+
|
|
1294
|
+
### Verification Steps (DO ALL OF THESE)
|
|
1295
|
+
1. Start the dev server (see above)
|
|
1296
|
+
2. Use \`browser_navigate\` to go to the affected page(s)
|
|
1297
|
+
3. Use \`browser_snapshot\` to capture the page and verify elements are present
|
|
1298
|
+
4. **Test interactions**: if the task involves buttons, forms, search, toggles, or any interactive elements \u2014 click them, type into them, and verify the result with another snapshot
|
|
1299
|
+
5. If the task mentions responsive behavior, use \`browser_resize\` to test at different widths (e.g., 1200px, 768px, 480px) and take snapshots at each
|
|
1300
|
+
6. Kill the dev server when done
|
|
1301
|
+
|
|
1302
|
+
Do NOT skip the browser verification. The visual check AND interaction testing are required parts of implementing UI changes.`;
|
|
1303
|
+
}
|
|
1304
|
+
if (stageName === "review") {
|
|
1305
|
+
return `## Browser Visual Verification (MANDATORY for UI review)
|
|
1306
|
+
|
|
1307
|
+
This task involves UI changes. You MUST visually verify the implementation using the browser tools before giving your verdict.
|
|
1308
|
+
${devServerBlock}
|
|
1309
|
+
|
|
1310
|
+
### Available Browser Tools
|
|
1311
|
+
- \`mcp__playwright__browser_navigate\` \u2014 go to a URL
|
|
1312
|
+
- \`mcp__playwright__browser_snapshot\` \u2014 capture accessibility tree (shows all elements, text, roles)
|
|
1313
|
+
- \`mcp__playwright__browser_take_screenshot\` \u2014 take a visual screenshot
|
|
1314
|
+
- \`mcp__playwright__browser_click\` \u2014 click an element
|
|
1315
|
+
- \`mcp__playwright__browser_type\` \u2014 type text into an input
|
|
1316
|
+
- \`mcp__playwright__browser_hover\` \u2014 hover over an element
|
|
1317
|
+
- \`mcp__playwright__browser_resize\` \u2014 resize viewport
|
|
1318
|
+
- \`mcp__playwright__browser_wait_for\` \u2014 wait for text to appear/disappear
|
|
1319
|
+
|
|
1320
|
+
### Review Verification Steps (DO ALL OF THESE)
|
|
1321
|
+
1. Start the dev server (see above)
|
|
1322
|
+
2. Use \`browser_navigate\` to go to the affected page(s)
|
|
1323
|
+
3. Use \`browser_snapshot\` to verify elements, layout, and text content
|
|
1324
|
+
4. **Test interactions**: click buttons, fill forms, test search \u2014 verify the UI responds correctly
|
|
1325
|
+
5. If the task mentions responsive behavior, use \`browser_resize\` to test at different widths
|
|
1326
|
+
6. Include your browser verification findings in the review (what you saw, what you interacted with, what worked/failed)
|
|
1327
|
+
7. Kill the dev server when done
|
|
1328
|
+
|
|
1329
|
+
Do NOT skip the browser verification. A review of UI changes without visual AND interaction verification is incomplete.`;
|
|
1330
|
+
}
|
|
1331
|
+
return `## Browser Tools Available
|
|
1332
|
+
|
|
1333
|
+
You have access to Playwright MCP browser tools for visual verification.
|
|
1334
|
+
${devServerBlock}
|
|
1335
|
+
|
|
1336
|
+
Use browser tools to navigate to pages and take snapshots to verify UI output.`;
|
|
1337
|
+
}
|
|
1197
1338
|
function buildFullPrompt(stageName, taskId, taskDir, projectDir, feedback) {
|
|
1198
1339
|
const config = getProjectConfig();
|
|
1340
|
+
let assembled;
|
|
1199
1341
|
if (config.contextTiers?.enabled) {
|
|
1200
|
-
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1342
|
+
assembled = buildFullPromptTiered(stageName, taskId, taskDir, projectDir, feedback);
|
|
1343
|
+
} else {
|
|
1344
|
+
const memory = readProjectMemory(projectDir);
|
|
1345
|
+
const promptTemplate = readPromptFile(stageName, projectDir);
|
|
1346
|
+
const prompt = injectTaskContext(promptTemplate, taskId, taskDir, feedback);
|
|
1347
|
+
assembled = memory ? `${memory}
|
|
1206
1348
|
---
|
|
1207
1349
|
|
|
1208
1350
|
${prompt}` : prompt;
|
|
1351
|
+
}
|
|
1352
|
+
if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
|
|
1353
|
+
assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
|
|
1354
|
+
}
|
|
1355
|
+
return assembled;
|
|
1209
1356
|
}
|
|
1210
1357
|
function buildFullPromptTiered(stageName, taskId, taskDir, projectDir, feedback) {
|
|
1211
1358
|
const config = getProjectConfig();
|
|
@@ -1241,6 +1388,7 @@ var init_context = __esm({
|
|
|
1241
1388
|
init_memory();
|
|
1242
1389
|
init_config();
|
|
1243
1390
|
init_context_tiers();
|
|
1391
|
+
init_mcp_config();
|
|
1244
1392
|
DEFAULT_MODEL_MAP = {
|
|
1245
1393
|
cheap: "haiku",
|
|
1246
1394
|
mid: "sonnet",
|
|
@@ -1365,11 +1513,16 @@ async function executeAgentStage(ctx, def) {
|
|
|
1365
1513
|
if (sessionInfo) {
|
|
1366
1514
|
logger.info(` session: ${SESSION_GROUP[def.name]} (${sessionInfo.resumeSession ? "resume" : "new"})`);
|
|
1367
1515
|
}
|
|
1516
|
+
const mcpConfigJson = isMcpEnabledForStage(def.name, config.mcp) ? buildMcpConfigJson(config.mcp) : void 0;
|
|
1517
|
+
if (mcpConfigJson) {
|
|
1518
|
+
logger.info(` MCP servers enabled for ${def.name}`);
|
|
1519
|
+
}
|
|
1368
1520
|
const runner = getRunnerForStage(ctx, def.name);
|
|
1369
1521
|
const result = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
|
|
1370
1522
|
cwd: ctx.projectDir,
|
|
1371
1523
|
env: extraEnv,
|
|
1372
|
-
...sessionInfo
|
|
1524
|
+
...sessionInfo,
|
|
1525
|
+
mcpConfigJson
|
|
1373
1526
|
});
|
|
1374
1527
|
if (result.outcome !== "completed") {
|
|
1375
1528
|
return { outcome: result.outcome, error: result.error, retries: 0 };
|
|
@@ -1447,6 +1600,7 @@ var init_agent = __esm({
|
|
|
1447
1600
|
init_context();
|
|
1448
1601
|
init_validators();
|
|
1449
1602
|
init_config();
|
|
1603
|
+
init_mcp_config();
|
|
1450
1604
|
init_runner_selection();
|
|
1451
1605
|
init_logger();
|
|
1452
1606
|
SESSION_GROUP = {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses @kody / /kody comment body into structured inputs.
|
|
3
|
+
* Run by the parse job in GitHub Actions.
|
|
4
|
+
* Reads from env, writes to $GITHUB_OUTPUT.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
8
|
+
const triggerType = process.env.TRIGGER_TYPE ?? "dispatch";
|
|
9
|
+
function output(key, value) {
|
|
10
|
+
if (outputFile) {
|
|
11
|
+
fs.appendFileSync(outputFile, `${key}=${value}\n`);
|
|
12
|
+
}
|
|
13
|
+
console.log(`${key}=${value}`);
|
|
14
|
+
}
|
|
15
|
+
// For workflow_dispatch, pass through inputs
|
|
16
|
+
if (triggerType === "dispatch") {
|
|
17
|
+
output("task_id", process.env.INPUT_TASK_ID ?? "");
|
|
18
|
+
output("mode", process.env.INPUT_MODE ?? "full");
|
|
19
|
+
output("from_stage", process.env.INPUT_FROM_STAGE ?? "");
|
|
20
|
+
output("issue_number", process.env.INPUT_ISSUE_NUMBER ?? "");
|
|
21
|
+
output("feedback", process.env.INPUT_FEEDBACK ?? "");
|
|
22
|
+
output("valid", process.env.INPUT_TASK_ID ? "true" : "false");
|
|
23
|
+
output("trigger_type", "dispatch");
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
// For issue_comment, parse the comment body
|
|
27
|
+
const commentBody = process.env.COMMENT_BODY ?? "";
|
|
28
|
+
const issueNumber = process.env.ISSUE_NUMBER ?? "";
|
|
29
|
+
// Match: @kody [mode] [task-id] [--from stage] [--feedback "text"]
|
|
30
|
+
const kodyMatch = commentBody.match(/(?:@kody|\/kody)\s*(.*)/i);
|
|
31
|
+
if (!kodyMatch) {
|
|
32
|
+
output("valid", "false");
|
|
33
|
+
output("trigger_type", "comment");
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
const parts = kodyMatch[1].trim().split(/\s+/);
|
|
37
|
+
const validModes = ["full", "rerun", "status"];
|
|
38
|
+
let mode = "full";
|
|
39
|
+
let taskId = "";
|
|
40
|
+
let fromStage = "";
|
|
41
|
+
let feedback = "";
|
|
42
|
+
let i = 0;
|
|
43
|
+
// First arg: mode or task-id
|
|
44
|
+
if (parts[i] && validModes.includes(parts[i])) {
|
|
45
|
+
mode = parts[i];
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
// Second arg: task-id
|
|
49
|
+
if (parts[i] && !parts[i].startsWith("--")) {
|
|
50
|
+
taskId = parts[i];
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
// Named args
|
|
54
|
+
while (i < parts.length) {
|
|
55
|
+
if (parts[i] === "--from" && parts[i + 1]) {
|
|
56
|
+
fromStage = parts[i + 1];
|
|
57
|
+
i += 2;
|
|
58
|
+
}
|
|
59
|
+
else if (parts[i] === "--feedback" && parts[i + 1]) {
|
|
60
|
+
// Collect quoted feedback
|
|
61
|
+
const rest = parts.slice(i + 1).join(" ");
|
|
62
|
+
const quoted = rest.match(/^"([^"]*)"/);
|
|
63
|
+
feedback = quoted ? quoted[1] : parts[i + 1];
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
output("task_id", taskId);
|
|
71
|
+
output("mode", mode);
|
|
72
|
+
output("from_stage", fromStage);
|
|
73
|
+
output("issue_number", issueNumber);
|
|
74
|
+
output("feedback", feedback);
|
|
75
|
+
output("valid", taskId ? "true" : "false");
|
|
76
|
+
output("trigger_type", "comment");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates that a comment trigger is safe to execute.
|
|
3
|
+
* Run by the parse job in GitHub Actions.
|
|
4
|
+
* Reads from env, writes to $GITHUB_OUTPUT.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
const ALLOWED_ASSOCIATIONS = ["COLLABORATOR", "MEMBER", "OWNER"];
|
|
8
|
+
const association = process.env.COMMENT_AUTHOR_ASSOCIATION ?? "";
|
|
9
|
+
const outputFile = process.env.GITHUB_OUTPUT;
|
|
10
|
+
function output(key, value) {
|
|
11
|
+
if (outputFile) {
|
|
12
|
+
fs.appendFileSync(outputFile, `${key}=${value}\n`);
|
|
13
|
+
}
|
|
14
|
+
console.log(`${key}=${value}`);
|
|
15
|
+
}
|
|
16
|
+
if (!ALLOWED_ASSOCIATIONS.includes(association)) {
|
|
17
|
+
output("valid", "false");
|
|
18
|
+
output("reason", `Author association '${association}' not in allowlist: ${ALLOWED_ASSOCIATIONS.join(", ")}`);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
output("valid", "true");
|
|
22
|
+
output("reason", "");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CliInput {
|
|
2
|
+
command: "run" | "rerun" | "fix" | "status";
|
|
3
|
+
taskId?: string;
|
|
4
|
+
task?: string;
|
|
5
|
+
fromStage?: string;
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
issueNumber?: number;
|
|
9
|
+
feedback?: string;
|
|
10
|
+
local?: boolean;
|
|
11
|
+
complexity?: "low" | "medium" | "high";
|
|
12
|
+
}
|
|
13
|
+
export declare function parseArgs(): CliInput;
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const isCI = !!process.env.GITHUB_ACTIONS;
|
|
2
|
+
function getArg(args, flag) {
|
|
3
|
+
const idx = args.indexOf(flag);
|
|
4
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith("--")) {
|
|
5
|
+
return args[idx + 1];
|
|
6
|
+
}
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
function hasFlag(args, flag) {
|
|
10
|
+
return args.includes(flag);
|
|
11
|
+
}
|
|
12
|
+
export function parseArgs() {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
if (hasFlag(args, "--help") || hasFlag(args, "-h") || args.length === 0) {
|
|
15
|
+
console.log(`Usage:
|
|
16
|
+
kody run --task-id <id> [--task "<desc>"] [--cwd <path>] [--issue-number <n>] [--complexity low|medium|high] [--feedback "<text>"] [--local] [--dry-run]
|
|
17
|
+
kody rerun --task-id <id> --from <stage> [--cwd <path>] [--issue-number <n>]
|
|
18
|
+
kody fix --task-id <id> [--cwd <path>] [--issue-number <n>] [--feedback "<text>"]
|
|
19
|
+
kody status --task-id <id> [--cwd <path>]
|
|
20
|
+
kody --help`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const command = args[0];
|
|
24
|
+
if (!["run", "rerun", "fix", "status"].includes(command)) {
|
|
25
|
+
console.error(`Unknown command: ${command}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const issueStr = getArg(args, "--issue-number") ?? process.env.ISSUE_NUMBER;
|
|
29
|
+
const localFlag = hasFlag(args, "--local");
|
|
30
|
+
return {
|
|
31
|
+
command,
|
|
32
|
+
taskId: getArg(args, "--task-id") ?? process.env.TASK_ID,
|
|
33
|
+
task: getArg(args, "--task"),
|
|
34
|
+
fromStage: getArg(args, "--from") ?? process.env.FROM_STAGE,
|
|
35
|
+
dryRun: hasFlag(args, "--dry-run") || process.env.DRY_RUN === "true",
|
|
36
|
+
cwd: getArg(args, "--cwd"),
|
|
37
|
+
issueNumber: issueStr ? parseInt(issueStr, 10) : undefined,
|
|
38
|
+
feedback: getArg(args, "--feedback") ?? process.env.FEEDBACK,
|
|
39
|
+
local: localFlag || (!isCI && !hasFlag(args, "--no-local")),
|
|
40
|
+
complexity: (getArg(args, "--complexity") ?? process.env.COMPLEXITY),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { logger } from "../logger.js";
|
|
5
|
+
export async function checkLitellmHealth(url) {
|
|
6
|
+
try {
|
|
7
|
+
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3000) });
|
|
8
|
+
return response.ok;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function tryStartLitellm(url, projectDir) {
|
|
15
|
+
const configPath = path.join(projectDir, "litellm-config.yaml");
|
|
16
|
+
if (!fs.existsSync(configPath)) {
|
|
17
|
+
logger.warn("litellm-config.yaml not found — cannot start proxy");
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// Extract port from URL
|
|
21
|
+
const portMatch = url.match(/:(\d+)/);
|
|
22
|
+
const port = portMatch ? portMatch[1] : "4000";
|
|
23
|
+
// Check if litellm is installed
|
|
24
|
+
try {
|
|
25
|
+
execFileSync("litellm", ["--version"], { timeout: 5000, stdio: "pipe" });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
try {
|
|
29
|
+
execFileSync("python3", ["-m", "litellm", "--version"], { timeout: 5000, stdio: "pipe" });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
logger.warn("litellm not installed (pip install 'litellm[proxy]')");
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
logger.info(`Starting LiteLLM proxy on port ${port}...`);
|
|
37
|
+
// Determine command
|
|
38
|
+
let cmd;
|
|
39
|
+
let args;
|
|
40
|
+
try {
|
|
41
|
+
execFileSync("litellm", ["--version"], { timeout: 5000, stdio: "pipe" });
|
|
42
|
+
cmd = "litellm";
|
|
43
|
+
args = ["--config", configPath, "--port", port];
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
cmd = "python3";
|
|
47
|
+
args = ["-m", "litellm", "--config", configPath, "--port", port];
|
|
48
|
+
}
|
|
49
|
+
// Load API key env vars from project .env (only *_API_KEY patterns)
|
|
50
|
+
const dotenvPath = path.join(projectDir, ".env");
|
|
51
|
+
const dotenvVars = {};
|
|
52
|
+
if (fs.existsSync(dotenvPath)) {
|
|
53
|
+
for (const line of fs.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
54
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
55
|
+
if (match)
|
|
56
|
+
dotenvVars[match[1]] = match[2];
|
|
57
|
+
}
|
|
58
|
+
if (Object.keys(dotenvVars).length > 0) {
|
|
59
|
+
logger.info(` Loaded API keys: ${Object.keys(dotenvVars).join(", ")}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const { spawn } = await import("child_process");
|
|
63
|
+
const child = spawn(cmd, args, {
|
|
64
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
65
|
+
detached: true,
|
|
66
|
+
env: { ...process.env, ...dotenvVars },
|
|
67
|
+
});
|
|
68
|
+
// Capture stderr for debugging
|
|
69
|
+
let proxyStderr = "";
|
|
70
|
+
child.stderr?.on("data", (chunk) => { proxyStderr += chunk.toString(); });
|
|
71
|
+
// Wait for health
|
|
72
|
+
for (let i = 0; i < 30; i++) {
|
|
73
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
74
|
+
if (await checkLitellmHealth(url)) {
|
|
75
|
+
logger.info(`LiteLLM proxy ready at ${url}`);
|
|
76
|
+
return child;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (proxyStderr) {
|
|
80
|
+
logger.warn(`LiteLLM stderr: ${proxyStderr.slice(-1000)}`);
|
|
81
|
+
}
|
|
82
|
+
logger.warn("LiteLLM proxy failed to start within 60s");
|
|
83
|
+
child.kill();
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
export function findLatestTaskForIssue(issueNumber, projectDir) {
|
|
5
|
+
const tasksDir = path.join(projectDir, ".tasks");
|
|
6
|
+
if (!fs.existsSync(tasksDir))
|
|
7
|
+
return null;
|
|
8
|
+
// Only consider directories (not files)
|
|
9
|
+
const allDirs = fs.readdirSync(tasksDir, { withFileTypes: true })
|
|
10
|
+
.filter((d) => d.isDirectory())
|
|
11
|
+
.map((d) => d.name)
|
|
12
|
+
.sort()
|
|
13
|
+
.reverse();
|
|
14
|
+
// Direct match: tasks starting with issue number
|
|
15
|
+
const prefix = `${issueNumber}-`;
|
|
16
|
+
const direct = allDirs.find((d) => d.startsWith(prefix));
|
|
17
|
+
if (direct)
|
|
18
|
+
return direct;
|
|
19
|
+
// Fallback for PR comments: extract issue number from current git branch
|
|
20
|
+
// Branch format: <issueNum>--<slug> (e.g., 1031--security-8x-route)
|
|
21
|
+
try {
|
|
22
|
+
const branch = execFileSync("git", ["branch", "--show-current"], {
|
|
23
|
+
encoding: "utf-8", cwd: projectDir, timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
24
|
+
}).trim();
|
|
25
|
+
const branchIssueMatch = branch.match(/^(\d+)-/);
|
|
26
|
+
if (branchIssueMatch) {
|
|
27
|
+
const branchIssueNum = branchIssueMatch[1];
|
|
28
|
+
const branchPrefix = `${branchIssueNum}-`;
|
|
29
|
+
const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
|
|
30
|
+
if (fromBranch)
|
|
31
|
+
return fromBranch;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch { /* ignore */ }
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function generateTaskId() {
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
40
|
+
return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
41
|
+
}
|