@kody-ade/kody-engine-lite 0.1.68 → 0.1.69

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 (71) 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 +125 -7
  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 +1 -1
  71. package/prompts/plan.md +18 -1
@@ -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
@@ -732,13 +732,80 @@ function getLatestKodyReviewComment(prNumber) {
732
732
  return null;
733
733
  }
734
734
  }
735
- var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd;
735
+ function getPRFeedbackSinceLastKodyAction(prNumber) {
736
+ try {
737
+ const issueCommentsRaw = gh([
738
+ "api",
739
+ `repos/{owner}/{repo}/issues/${prNumber}/comments`,
740
+ "--jq",
741
+ "[.[] | {body, created_at, user_login: .user.login, user_type: .user.type}]"
742
+ ]);
743
+ const issueComments = issueCommentsRaw ? JSON.parse(issueCommentsRaw) : [];
744
+ const reviewCommentsRaw = gh([
745
+ "api",
746
+ `repos/{owner}/{repo}/pulls/${prNumber}/comments`,
747
+ "--jq",
748
+ "[.[] | {body, created_at, user_login: .user.login, user_type: .user.type, path, line}]"
749
+ ]);
750
+ const reviewComments = reviewCommentsRaw ? JSON.parse(reviewCommentsRaw) : [];
751
+ const kodyTimestamp = findLastKodyActionTimestamp(issueComments);
752
+ const humanIssueComments = issueComments.filter(
753
+ (c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
754
+ );
755
+ const humanReviewComments = reviewComments.filter(
756
+ (c) => !isKodyComment(c) && (!kodyTimestamp || c.created_at > kodyTimestamp)
757
+ );
758
+ if (humanIssueComments.length === 0 && humanReviewComments.length === 0) {
759
+ return null;
760
+ }
761
+ const parts = [];
762
+ if (humanIssueComments.length > 0) {
763
+ parts.push("### PR Comments");
764
+ for (const c of humanIssueComments) {
765
+ parts.push(`**@${c.user_login}:**
766
+ ${c.body}`);
767
+ }
768
+ }
769
+ if (humanReviewComments.length > 0) {
770
+ parts.push("### Code Review Comments");
771
+ for (const c of humanReviewComments) {
772
+ const location = c.path ? `\`${c.path}${c.line ? `:${c.line}` : ""}\`` : "";
773
+ parts.push(`**@${c.user_login}** ${location}:
774
+ ${c.body}`);
775
+ }
776
+ }
777
+ return parts.join("\n\n");
778
+ } catch (err) {
779
+ logger.warn(` Failed to get PR feedback for #${prNumber}: ${err}`);
780
+ return null;
781
+ }
782
+ }
783
+ function isKodyComment(comment) {
784
+ if (comment.user_type === "Bot") return true;
785
+ return KODY_MARKERS.some((marker) => comment.body.includes(marker));
786
+ }
787
+ function findLastKodyActionTimestamp(comments) {
788
+ const kodyComments = comments.filter(isKodyComment);
789
+ if (kodyComments.length === 0) return null;
790
+ return kodyComments[kodyComments.length - 1].created_at;
791
+ }
792
+ var API_TIMEOUT_MS, LIFECYCLE_LABELS, _ghCwd, KODY_MARKERS;
736
793
  var init_github_api = __esm({
737
794
  "src/github-api.ts"() {
738
795
  "use strict";
739
796
  init_logger();
740
797
  API_TIMEOUT_MS = 3e4;
741
798
  LIFECYCLE_LABELS = ["planning", "building", "review", "done", "failed", "waiting", "low", "medium", "high"];
799
+ KODY_MARKERS = [
800
+ "Kody Review",
801
+ "\u{1F916} Generated by Kody",
802
+ "Kody pipeline started",
803
+ "Fix pushed to PR",
804
+ "PR created:",
805
+ "Pipeline failed at",
806
+ "Pipeline already running",
807
+ "already completed"
808
+ ];
742
809
  }
743
810
  });
744
811
 
@@ -2535,6 +2602,7 @@ ${learnings.join("\n")}
2535
2602
  invalidateCache(conventionsPath, path13.join(memoryDir, ".tiers"));
2536
2603
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
2537
2604
  }
2605
+ autoLearnDecisions(ctx.taskDir, memoryDir, ctx.taskId, timestamp2);
2538
2606
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
2539
2607
  } catch {
2540
2608
  }
@@ -2599,6 +2667,45 @@ ${detected.join("\n")}
2599
2667
  logger.info(`Auto-detected architecture (${detected.length} items)`);
2600
2668
  }
2601
2669
  }
2670
+ function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
2671
+ const reviewPath = path13.join(taskDir, "review.md");
2672
+ if (!fs13.existsSync(reviewPath)) return;
2673
+ const review = fs13.readFileSync(reviewPath, "utf-8");
2674
+ const decisions = [];
2675
+ const existingPatternRe = /(?:use|follow|reuse|match|adopt)\s+(?:the\s+)?existing\s+(.+?)(?:\.|$)/gim;
2676
+ for (const match of review.matchAll(existingPatternRe)) {
2677
+ decisions.push(`- Use existing ${match[1].trim()}`);
2678
+ }
2679
+ const insteadOfRe = /instead\s+of\s+(.+?),?\s+(?:use|prefer|adopt)\s+(.+?)(?:\.|$)/gim;
2680
+ for (const match of review.matchAll(insteadOfRe)) {
2681
+ decisions.push(`- Prefer ${match[2].trim()} over ${match[1].trim()}`);
2682
+ }
2683
+ const consistentRe = /(?:consistent\s+with|same\s+pattern\s+as|follow\s+the\s+pattern\s+(?:in|from))\s+(.+?)(?:\.|$)/gim;
2684
+ for (const match of review.matchAll(consistentRe)) {
2685
+ decisions.push(`- Follow pattern from ${match[1].trim()}`);
2686
+ }
2687
+ const avoidRe = /(?:don't|do\s+not|never|avoid)\s+(?:use\s+)?(.+?)\s+(?:for|when|in)\s+(.+?)(?:\.|$)/gim;
2688
+ for (const match of review.matchAll(avoidRe)) {
2689
+ decisions.push(`- Avoid ${match[1].trim()} for ${match[2].trim()}`);
2690
+ }
2691
+ if (decisions.length === 0) return;
2692
+ const decisionsPath = path13.join(memoryDir, "decisions.md");
2693
+ let existing = "";
2694
+ if (fs13.existsSync(decisionsPath)) {
2695
+ existing = fs13.readFileSync(decisionsPath, "utf-8");
2696
+ } else {
2697
+ existing = "# Architectural Decisions\n\nDecisions extracted from code reviews. The planning agent MUST follow these.\n";
2698
+ }
2699
+ const newDecisions = decisions.filter((d) => !existing.includes(d));
2700
+ if (newDecisions.length === 0) return;
2701
+ const entry = `
2702
+ ## From task ${taskId} (${timestamp2})
2703
+ ${newDecisions.join("\n")}
2704
+ `;
2705
+ fs13.appendFileSync(decisionsPath, existing ? entry : existing + entry);
2706
+ invalidateCache(decisionsPath, path13.join(memoryDir, ".tiers"));
2707
+ logger.info(`Auto-learned ${newDecisions.length} architectural decision(s)`);
2708
+ }
2602
2709
  var init_auto_learn = __esm({
2603
2710
  "src/learning/auto-learn.ts"() {
2604
2711
  "use strict";
@@ -3691,17 +3798,28 @@ ${input.feedback}` : ciContext;
3691
3798
  }
3692
3799
  }
3693
3800
  if (input.command === "fix" && input.prNumber) {
3801
+ const feedbackParts = [];
3694
3802
  const reviewComment = getLatestKodyReviewComment(input.prNumber);
3695
3803
  if (reviewComment) {
3696
- logger.info(` Found Kody review comment on PR #${input.prNumber}, injecting as feedback`);
3697
- const reviewContext = `## Review findings from PR #${input.prNumber}
3804
+ logger.info(` Found Kody review comment on PR #${input.prNumber}`);
3805
+ feedbackParts.push(`## Review findings from PR #${input.prNumber}
3698
3806
 
3699
- ${reviewComment}`;
3700
- input.feedback = input.feedback ? `${reviewContext}
3807
+ ${reviewComment}`);
3808
+ }
3809
+ const humanFeedback = getPRFeedbackSinceLastKodyAction(input.prNumber);
3810
+ if (humanFeedback) {
3811
+ logger.info(` Found human feedback on PR #${input.prNumber}`);
3812
+ feedbackParts.push(`## Human review feedback from PR #${input.prNumber}
3701
3813
 
3702
- ## Additional feedback
3814
+ ${humanFeedback}`);
3815
+ }
3816
+ if (input.feedback) {
3817
+ feedbackParts.push(`## Additional feedback
3703
3818
 
3704
- ${input.feedback}` : reviewContext;
3819
+ ${input.feedback}`);
3820
+ }
3821
+ if (feedbackParts.length > 0) {
3822
+ input.feedback = feedbackParts.join("\n\n");
3705
3823
  }
3706
3824
  }
3707
3825
  const config = getProjectConfig();
@@ -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;