@kody-ade/kody-engine-lite 0.1.121 → 0.1.123

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.
Files changed (70) hide show
  1. package/dist/agent-runner.d.ts +4 -0
  2. package/dist/agent-runner.js +122 -0
  3. package/dist/bin/cli.js +43 -24
  4. package/dist/ci/parse-inputs.d.ts +6 -0
  5. package/dist/ci/parse-inputs.js +76 -0
  6. package/dist/ci/parse-safety.d.ts +6 -0
  7. package/dist/ci/parse-safety.js +22 -0
  8. package/dist/cli/args.d.ts +13 -0
  9. package/dist/cli/args.js +42 -0
  10. package/dist/cli/litellm.d.ts +2 -0
  11. package/dist/cli/litellm.js +85 -0
  12. package/dist/cli/task-resolution.d.ts +2 -0
  13. package/dist/cli/task-resolution.js +41 -0
  14. package/dist/config.d.ts +49 -0
  15. package/dist/config.js +72 -0
  16. package/dist/context.d.ts +4 -0
  17. package/dist/context.js +83 -0
  18. package/dist/definitions.d.ts +3 -0
  19. package/dist/definitions.js +59 -0
  20. package/dist/entry.d.ts +1 -0
  21. package/dist/entry.js +236 -0
  22. package/dist/git-utils.d.ts +13 -0
  23. package/dist/git-utils.js +174 -0
  24. package/dist/github-api.d.ts +14 -0
  25. package/dist/github-api.js +114 -0
  26. package/dist/kody-utils.d.ts +1 -0
  27. package/dist/kody-utils.js +9 -0
  28. package/dist/learning/auto-learn.d.ts +2 -0
  29. package/dist/learning/auto-learn.js +169 -0
  30. package/dist/logger.d.ts +14 -0
  31. package/dist/logger.js +51 -0
  32. package/dist/memory.d.ts +1 -0
  33. package/dist/memory.js +20 -0
  34. package/dist/observer.d.ts +9 -0
  35. package/dist/observer.js +80 -0
  36. package/dist/pipeline/complexity.d.ts +3 -0
  37. package/dist/pipeline/complexity.js +12 -0
  38. package/dist/pipeline/executor-registry.d.ts +3 -0
  39. package/dist/pipeline/executor-registry.js +20 -0
  40. package/dist/pipeline/hooks.d.ts +17 -0
  41. package/dist/pipeline/hooks.js +110 -0
  42. package/dist/pipeline/questions.d.ts +2 -0
  43. package/dist/pipeline/questions.js +44 -0
  44. package/dist/pipeline/runner-selection.d.ts +2 -0
  45. package/dist/pipeline/runner-selection.js +13 -0
  46. package/dist/pipeline/state.d.ts +4 -0
  47. package/dist/pipeline/state.js +37 -0
  48. package/dist/pipeline.d.ts +3 -0
  49. package/dist/pipeline.js +213 -0
  50. package/dist/preflight.d.ts +1 -0
  51. package/dist/preflight.js +69 -0
  52. package/dist/retrospective.d.ts +26 -0
  53. package/dist/retrospective.js +211 -0
  54. package/dist/stages/agent.d.ts +2 -0
  55. package/dist/stages/agent.js +94 -0
  56. package/dist/stages/gate.d.ts +2 -0
  57. package/dist/stages/gate.js +32 -0
  58. package/dist/stages/review.d.ts +2 -0
  59. package/dist/stages/review.js +32 -0
  60. package/dist/stages/ship.d.ts +3 -0
  61. package/dist/stages/ship.js +154 -0
  62. package/dist/stages/verify.d.ts +2 -0
  63. package/dist/stages/verify.js +94 -0
  64. package/dist/types.d.ts +61 -0
  65. package/dist/types.js +1 -0
  66. package/dist/validators.d.ts +8 -0
  67. package/dist/validators.js +42 -0
  68. package/dist/verify-runner.d.ts +11 -0
  69. package/dist/verify-runner.js +110 -0
  70. package/package.json +3 -3
@@ -0,0 +1,4 @@
1
+ import type { AgentRunner } from "./types.js";
2
+ import type { KodyConfig } from "./config.js";
3
+ export declare function createClaudeCodeRunner(): AgentRunner;
4
+ export declare function createRunners(config: KodyConfig): Record<string, AgentRunner>;
@@ -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
@@ -561,14 +561,25 @@ function getIssue(issueNumber) {
561
561
  "view",
562
562
  String(issueNumber),
563
563
  "--json",
564
- "body,title"
564
+ "body,title,labels,comments,assignees,milestone"
565
565
  ]);
566
566
  const parsed = JSON.parse(output);
567
567
  if (!parsed || typeof parsed.title !== "string") {
568
568
  logger.warn(` Issue #${issueNumber}: unexpected response shape`);
569
569
  return null;
570
570
  }
571
- return { body: parsed.body ?? "", title: parsed.title };
571
+ return {
572
+ body: parsed.body ?? "",
573
+ title: parsed.title,
574
+ labels: (parsed.labels ?? []).map((l) => l.name),
575
+ comments: (parsed.comments ?? []).map((c) => ({
576
+ body: c.body,
577
+ author: c.author.login,
578
+ createdAt: c.createdAt
579
+ })),
580
+ assignees: (parsed.assignees ?? []).map((a) => a.login),
581
+ milestone: parsed.milestone?.title ?? null
582
+ };
572
583
  } catch (err) {
573
584
  if (isNotFoundError(err)) {
574
585
  logger.info(` Issue #${issueNumber} not found`);
@@ -1055,7 +1066,8 @@ async function checkModelHealth(baseUrl, apiKey, model) {
1055
1066
  const hasAnthropicContent = Array.isArray(body.content) && body.content.some((b) => b.type === "text");
1056
1067
  const hasThinkingContent = Array.isArray(body.content) && body.content.some((b) => b.type === "thinking");
1057
1068
  const hasOpenAIContent = !!body.choices?.[0]?.message?.content;
1058
- if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent) {
1069
+ const hasEmptyContentResponse = Array.isArray(body.content) && body.role === "assistant";
1070
+ if (!hasAnthropicContent && !hasThinkingContent && !hasOpenAIContent && !hasEmptyContentResponse) {
1059
1071
  return { ok: false, error: `Unexpected response format: ${JSON.stringify(body).slice(0, 200)}` };
1060
1072
  }
1061
1073
  return { ok: true };
@@ -4163,7 +4175,7 @@ function extractSummary(output, cmdName) {
4163
4175
  const lines = output.split("\n").filter((l) => summaryPatterns.test(l));
4164
4176
  return lines.slice(-3).map((l) => `[${cmdName}] ${l.trim()}`);
4165
4177
  }
4166
- function runQualityGates(taskDir, projectRoot) {
4178
+ function runQualityGates(taskDir, projectRoot, options) {
4167
4179
  const config = getProjectConfig();
4168
4180
  const cwd = projectRoot ?? process.cwd();
4169
4181
  const allErrors = [];
@@ -4187,10 +4199,28 @@ function runQualityGates(taskDir, projectRoot) {
4187
4199
  continue;
4188
4200
  }
4189
4201
  if (!result2.success) {
4190
- allPass = false;
4191
4202
  const errors = parseErrors(result2.output);
4192
- allErrors.push(...errors.map((e) => `[${name}] ${e}`));
4193
- rawOutputs.push({ name, output: result2.output.slice(-3e3) });
4203
+ if (name === "typecheck" && options?.onlyFailOnFiles && options.onlyFailOnFiles.length > 0) {
4204
+ const scopedFiles = new Set(options.onlyFailOnFiles);
4205
+ const errorLines = errors.filter((e) => {
4206
+ const fileMatch = e.match(/\b(src\/[^\s(:]+\.[a-z]+)/)?.[1];
4207
+ if (!fileMatch) return false;
4208
+ return scopedFiles.has(fileMatch) || options.onlyFailOnFiles.some((f) => f.endsWith(fileMatch) || fileMatch.endsWith(f));
4209
+ });
4210
+ if (errorLines.length === 0) {
4211
+ logger.warn(` [typecheck] errors found but none in scoped files \u2014 treating as pre-existing, skipping`);
4212
+ rawOutputs.push({ name, output: result2.output.slice(-3e3) });
4213
+ allSummary.push(...extractSummary(result2.output, name));
4214
+ continue;
4215
+ }
4216
+ allPass = false;
4217
+ allErrors.push(...errorLines.map((e) => `[${name}] ${e}`));
4218
+ rawOutputs.push({ name, output: result2.output.slice(-3e3) });
4219
+ } else {
4220
+ allPass = false;
4221
+ allErrors.push(...errors.map((e) => `[${name}] ${e}`));
4222
+ rawOutputs.push({ name, output: result2.output.slice(-3e3) });
4223
+ }
4194
4224
  }
4195
4225
  allSummary.push(...extractSummary(result2.output, name));
4196
4226
  }
@@ -5313,7 +5343,7 @@ ${previousText}
5313
5343
  const model = resolveModel("cheap");
5314
5344
  const config = getProjectConfig();
5315
5345
  const extraEnv = {};
5316
- if (needsLitellmProxy(config)) {
5346
+ if (anyStageNeedsProxy(config)) {
5317
5347
  extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
5318
5348
  }
5319
5349
  const result2 = await runner.run("retrospective", prompt, model, 3e4, "", {
@@ -6000,26 +6030,15 @@ async function runResolve(options) {
6000
6030
  return { outcome: "failed", error: `Agent failed: ${result2.error}` };
6001
6031
  }
6002
6032
  logger.info(" Verifying resolution...");
6003
- const verify = runQualityGates(projectDir, projectDir);
6033
+ const verify = runQualityGates(projectDir, projectDir, { onlyFailOnFiles: conflictedFiles });
6004
6034
  if (!verify.pass) {
6005
- const errorText = verify.errors.join("\n");
6006
- const errorFilePaths = errorText.match(/src\/[^\s(:]+\.[a-z]+/g) ?? [];
6007
- const resolvedSet = new Set(conflictedFiles);
6008
- const allPreExisting = errorFilePaths.length > 0 && errorFilePaths.every(
6009
- (f) => !resolvedSet.has(f) && !conflictedFiles.some((c) => c.endsWith(f))
6010
- );
6011
- if (allPreExisting) {
6012
- logger.warn(" Verification: all errors in files not touched by resolution \u2014 treating as pre-existing, proceeding");
6013
- } else {
6014
- const errorSummary = verify.errors.slice(0, 5).join("\n");
6015
- logger.error(` Verification failed:
6035
+ const errorSummary = verify.errors.slice(0, 5).join("\n");
6036
+ logger.error(` Verification failed:
6016
6037
  ${errorSummary}`);
6017
- return { outcome: "failed", error: `Conflict resolution failed verification:
6038
+ return { outcome: "failed", error: `Conflict resolution failed verification:
6018
6039
  ${errorSummary}` };
6019
- }
6020
- } else {
6021
- logger.info(" Verification passed");
6022
6040
  }
6041
+ logger.info(" Verification passed");
6023
6042
  commitAll(`chore: resolve merge conflicts with ${defaultBranch}`, projectDir);
6024
6043
  if (!local) {
6025
6044
  pushBranch(projectDir);
@@ -0,0 +1,6 @@
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
+ export {};
@@ -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,6 @@
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
+ export {};
@@ -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;
@@ -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,2 @@
1
+ export declare function checkLitellmHealth(url: string): Promise<boolean>;
2
+ export declare function tryStartLitellm(url: string, projectDir: string): Promise<ReturnType<typeof import("child_process").spawn> | null>;
@@ -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,2 @@
1
+ export declare function findLatestTaskForIssue(issueNumber: number, projectDir: string): string | null;
2
+ export declare function generateTaskId(): string;
@@ -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
+ }
@@ -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
+ }