@kody-ade/kody-engine-lite 0.1.99 → 0.1.101
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 +31 -5
- 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/package.json +1 -1
- package/templates/kody.yml +3 -0
|
@@ -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
|
@@ -305,7 +305,13 @@ function getProjectConfig() {
|
|
|
305
305
|
},
|
|
306
306
|
timeouts: raw.timeouts ?? void 0,
|
|
307
307
|
contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
|
|
308
|
-
mcp: raw.mcp ? {
|
|
308
|
+
mcp: raw.mcp ? {
|
|
309
|
+
servers: {},
|
|
310
|
+
stages: ["build", "verify", "review", "review-fix"],
|
|
311
|
+
...raw.mcp,
|
|
312
|
+
// Auto-enable when devServer is configured (user can still set enabled: false to override)
|
|
313
|
+
enabled: raw.mcp.enabled ?? !!raw.mcp.devServer
|
|
314
|
+
} : void 0
|
|
309
315
|
};
|
|
310
316
|
} catch {
|
|
311
317
|
logger.warn("kody.config.json is invalid JSON \u2014 using defaults");
|
|
@@ -861,7 +867,7 @@ var init_github_api = __esm({
|
|
|
861
867
|
"use strict";
|
|
862
868
|
init_logger();
|
|
863
869
|
API_TIMEOUT_MS = 3e4;
|
|
864
|
-
LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
|
|
870
|
+
LIFECYCLE_LABELS = ["planning", "building", "review", "shipping", "done", "failed", "waiting", "low", "medium", "high"];
|
|
865
871
|
KODY_MARKERS = [
|
|
866
872
|
"Kody Review",
|
|
867
873
|
"\u{1F916} Generated by Kody",
|
|
@@ -1281,6 +1287,20 @@ var init_context_tiers = __esm({
|
|
|
1281
1287
|
});
|
|
1282
1288
|
|
|
1283
1289
|
// src/mcp-config.ts
|
|
1290
|
+
function withPlaywrightIfNeeded(mcpConfig, hasUI) {
|
|
1291
|
+
if (!mcpConfig?.enabled || !hasUI) return mcpConfig;
|
|
1292
|
+
const hasPlaywright = Object.keys(mcpConfig.servers).some(
|
|
1293
|
+
(name) => name.toLowerCase().includes("playwright")
|
|
1294
|
+
);
|
|
1295
|
+
if (hasPlaywright) return mcpConfig;
|
|
1296
|
+
return {
|
|
1297
|
+
...mcpConfig,
|
|
1298
|
+
servers: {
|
|
1299
|
+
...mcpConfig.servers,
|
|
1300
|
+
playwright: PLAYWRIGHT_SERVER
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1284
1304
|
function buildMcpConfigJson(mcpConfig) {
|
|
1285
1305
|
if (!mcpConfig?.enabled) return void 0;
|
|
1286
1306
|
if (Object.keys(mcpConfig.servers).length === 0) return void 0;
|
|
@@ -1297,15 +1317,18 @@ function buildMcpConfigJson(mcpConfig) {
|
|
|
1297
1317
|
}
|
|
1298
1318
|
function isMcpEnabledForStage(stageName, mcpConfig) {
|
|
1299
1319
|
if (!mcpConfig?.enabled) return false;
|
|
1300
|
-
if (Object.keys(mcpConfig.servers).length === 0) return false;
|
|
1301
1320
|
const allowedStages = mcpConfig.stages ?? DEFAULT_MCP_STAGES;
|
|
1302
1321
|
return allowedStages.includes(stageName);
|
|
1303
1322
|
}
|
|
1304
|
-
var DEFAULT_MCP_STAGES;
|
|
1323
|
+
var DEFAULT_MCP_STAGES, PLAYWRIGHT_SERVER;
|
|
1305
1324
|
var init_mcp_config = __esm({
|
|
1306
1325
|
"src/mcp-config.ts"() {
|
|
1307
1326
|
"use strict";
|
|
1308
1327
|
DEFAULT_MCP_STAGES = ["build", "verify", "review", "review-fix"];
|
|
1328
|
+
PLAYWRIGHT_SERVER = {
|
|
1329
|
+
command: "npx",
|
|
1330
|
+
args: ["-y", "@anthropic-ai/mcp-playwright"]
|
|
1331
|
+
};
|
|
1309
1332
|
}
|
|
1310
1333
|
});
|
|
1311
1334
|
|
|
@@ -1685,7 +1708,8 @@ async function executeAgentStage(ctx, def) {
|
|
|
1685
1708
|
if (sessionInfo) {
|
|
1686
1709
|
logger.info(` session: ${SESSION_GROUP[def.name]} (${sessionInfo.resumeSession ? "resume" : "new"})`);
|
|
1687
1710
|
}
|
|
1688
|
-
const
|
|
1711
|
+
const mcpForStage = isMcpEnabledForStage(def.name, config.mcp) ? withPlaywrightIfNeeded(config.mcp, taskHasUI(ctx.taskDir)) : void 0;
|
|
1712
|
+
const mcpConfigJson = buildMcpConfigJson(mcpForStage);
|
|
1689
1713
|
if (mcpConfigJson) {
|
|
1690
1714
|
logger.info(` MCP servers enabled for ${def.name}`);
|
|
1691
1715
|
}
|
|
@@ -2670,6 +2694,7 @@ function applyPreStageLabel(ctx, def) {
|
|
|
2670
2694
|
if (!ctx.input.issueNumber || ctx.input.local) return;
|
|
2671
2695
|
if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
|
|
2672
2696
|
if (def.name === "review") setLifecycleLabel(ctx.input.issueNumber, "review");
|
|
2697
|
+
if (def.name === "ship") setLifecycleLabel(ctx.input.issueNumber, "shipping");
|
|
2673
2698
|
}
|
|
2674
2699
|
function checkQuestionsAfterStage(ctx, def, state) {
|
|
2675
2700
|
if (def.name !== "taskify" && def.name !== "plan") return null;
|
|
@@ -5027,6 +5052,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
|
|
|
5027
5052
|
{ name: "kody:planning", color: "c5def5", description: "Kody is analyzing and planning" },
|
|
5028
5053
|
{ name: "kody:building", color: "0e8a16", description: "Kody is building code" },
|
|
5029
5054
|
{ name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
|
|
5055
|
+
{ name: "kody:shipping", color: "1d76db", description: "Kody is creating the pull request" },
|
|
5030
5056
|
{ name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
|
|
5031
5057
|
{ name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
|
|
5032
5058
|
{ name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
|
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface RunnerConfig {
|
|
2
|
+
type: "claude-code";
|
|
3
|
+
}
|
|
4
|
+
export interface KodyConfig {
|
|
5
|
+
quality: {
|
|
6
|
+
typecheck: string;
|
|
7
|
+
lint: string;
|
|
8
|
+
lintFix: string;
|
|
9
|
+
format: string;
|
|
10
|
+
formatFix: string;
|
|
11
|
+
testUnit: string;
|
|
12
|
+
};
|
|
13
|
+
git: {
|
|
14
|
+
defaultBranch: string;
|
|
15
|
+
userEmail?: string;
|
|
16
|
+
userName?: string;
|
|
17
|
+
};
|
|
18
|
+
github: {
|
|
19
|
+
owner: string;
|
|
20
|
+
repo: string;
|
|
21
|
+
};
|
|
22
|
+
paths: {
|
|
23
|
+
taskDir: string;
|
|
24
|
+
};
|
|
25
|
+
agent: {
|
|
26
|
+
runner?: string;
|
|
27
|
+
modelMap: {
|
|
28
|
+
cheap: string;
|
|
29
|
+
mid: string;
|
|
30
|
+
strong: string;
|
|
31
|
+
};
|
|
32
|
+
litellmUrl?: string;
|
|
33
|
+
usePerStageRouting?: boolean;
|
|
34
|
+
defaultRunner?: string;
|
|
35
|
+
runners?: Record<string, RunnerConfig>;
|
|
36
|
+
stageRunners?: Record<string, string>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export declare const SIGKILL_GRACE_MS = 5000;
|
|
40
|
+
export declare const MAX_PR_TITLE_LENGTH = 72;
|
|
41
|
+
export declare const STDERR_TAIL_CHARS = 500;
|
|
42
|
+
export declare const API_TIMEOUT_MS = 30000;
|
|
43
|
+
export declare const DEFAULT_MAX_FIX_ATTEMPTS = 2;
|
|
44
|
+
export declare const AGENT_RETRY_DELAY_MS = 2000;
|
|
45
|
+
export declare const VERIFY_COMMAND_TIMEOUT_MS: number;
|
|
46
|
+
export declare const FIX_COMMAND_TIMEOUT_MS: number;
|
|
47
|
+
export declare function setConfigDir(dir: string): void;
|
|
48
|
+
export declare function getProjectConfig(): KodyConfig;
|
|
49
|
+
export declare function resetProjectConfig(): void;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
quality: {
|
|
6
|
+
typecheck: "pnpm -s tsc --noEmit",
|
|
7
|
+
lint: "pnpm -s lint",
|
|
8
|
+
lintFix: "pnpm lint:fix",
|
|
9
|
+
format: "pnpm -s format:check",
|
|
10
|
+
formatFix: "pnpm format:fix",
|
|
11
|
+
testUnit: "pnpm -s test",
|
|
12
|
+
},
|
|
13
|
+
git: {
|
|
14
|
+
defaultBranch: "dev",
|
|
15
|
+
},
|
|
16
|
+
github: {
|
|
17
|
+
owner: "",
|
|
18
|
+
repo: "",
|
|
19
|
+
},
|
|
20
|
+
paths: {
|
|
21
|
+
taskDir: ".tasks",
|
|
22
|
+
},
|
|
23
|
+
agent: {
|
|
24
|
+
runner: "claude-code",
|
|
25
|
+
defaultRunner: "claude",
|
|
26
|
+
modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
// Pipeline constants
|
|
30
|
+
export const SIGKILL_GRACE_MS = 5000;
|
|
31
|
+
export const MAX_PR_TITLE_LENGTH = 72;
|
|
32
|
+
export const STDERR_TAIL_CHARS = 500;
|
|
33
|
+
export const API_TIMEOUT_MS = 30_000;
|
|
34
|
+
export const DEFAULT_MAX_FIX_ATTEMPTS = 2;
|
|
35
|
+
export const AGENT_RETRY_DELAY_MS = 2000;
|
|
36
|
+
export const VERIFY_COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
|
|
37
|
+
export const FIX_COMMAND_TIMEOUT_MS = 2 * 60 * 1000;
|
|
38
|
+
let _config = null;
|
|
39
|
+
let _configDir = null;
|
|
40
|
+
export function setConfigDir(dir) {
|
|
41
|
+
_configDir = dir;
|
|
42
|
+
_config = null;
|
|
43
|
+
}
|
|
44
|
+
export function getProjectConfig() {
|
|
45
|
+
if (_config)
|
|
46
|
+
return _config;
|
|
47
|
+
const configPath = path.join(_configDir ?? process.cwd(), "kody.config.json");
|
|
48
|
+
if (fs.existsSync(configPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
51
|
+
_config = {
|
|
52
|
+
quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
|
|
53
|
+
git: { ...DEFAULT_CONFIG.git, ...raw.git },
|
|
54
|
+
github: { ...DEFAULT_CONFIG.github, ...raw.github },
|
|
55
|
+
paths: { ...DEFAULT_CONFIG.paths, ...raw.paths },
|
|
56
|
+
agent: { ...DEFAULT_CONFIG.agent, ...raw.agent },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
logger.warn("kody.config.json is invalid JSON — using defaults");
|
|
61
|
+
_config = { ...DEFAULT_CONFIG };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
_config = { ...DEFAULT_CONFIG };
|
|
66
|
+
}
|
|
67
|
+
return _config;
|
|
68
|
+
}
|
|
69
|
+
export function resetProjectConfig() {
|
|
70
|
+
_config = null;
|
|
71
|
+
_configDir = null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function readPromptFile(stageName: string): string;
|
|
2
|
+
export declare function injectTaskContext(prompt: string, taskId: string, taskDir: string, feedback?: string): string;
|
|
3
|
+
export declare function buildFullPrompt(stageName: string, taskId: string, taskDir: string, projectDir: string, feedback?: string): string;
|
|
4
|
+
export declare function resolveModel(modelTier: string, stageName?: string): string;
|