@kody-ade/kody-engine-lite 0.1.60 → 0.1.62

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 (72) hide show
  1. package/README.md +12 -14
  2. package/dist/agent-runner.d.ts +4 -0
  3. package/dist/agent-runner.js +122 -0
  4. package/dist/bin/cli.js +93 -80
  5. package/dist/ci/parse-inputs.d.ts +6 -0
  6. package/dist/ci/parse-inputs.js +76 -0
  7. package/dist/ci/parse-safety.d.ts +6 -0
  8. package/dist/ci/parse-safety.js +22 -0
  9. package/dist/cli/args.d.ts +13 -0
  10. package/dist/cli/args.js +42 -0
  11. package/dist/cli/litellm.d.ts +2 -0
  12. package/dist/cli/litellm.js +85 -0
  13. package/dist/cli/task-resolution.d.ts +2 -0
  14. package/dist/cli/task-resolution.js +41 -0
  15. package/dist/config.d.ts +49 -0
  16. package/dist/config.js +72 -0
  17. package/dist/context.d.ts +4 -0
  18. package/dist/context.js +83 -0
  19. package/dist/definitions.d.ts +3 -0
  20. package/dist/definitions.js +59 -0
  21. package/dist/entry.d.ts +1 -0
  22. package/dist/entry.js +236 -0
  23. package/dist/git-utils.d.ts +13 -0
  24. package/dist/git-utils.js +174 -0
  25. package/dist/github-api.d.ts +14 -0
  26. package/dist/github-api.js +114 -0
  27. package/dist/kody-utils.d.ts +1 -0
  28. package/dist/kody-utils.js +9 -0
  29. package/dist/learning/auto-learn.d.ts +2 -0
  30. package/dist/learning/auto-learn.js +169 -0
  31. package/dist/logger.d.ts +14 -0
  32. package/dist/logger.js +51 -0
  33. package/dist/memory.d.ts +1 -0
  34. package/dist/memory.js +20 -0
  35. package/dist/observer.d.ts +9 -0
  36. package/dist/observer.js +80 -0
  37. package/dist/pipeline/complexity.d.ts +3 -0
  38. package/dist/pipeline/complexity.js +12 -0
  39. package/dist/pipeline/executor-registry.d.ts +3 -0
  40. package/dist/pipeline/executor-registry.js +20 -0
  41. package/dist/pipeline/hooks.d.ts +17 -0
  42. package/dist/pipeline/hooks.js +110 -0
  43. package/dist/pipeline/questions.d.ts +2 -0
  44. package/dist/pipeline/questions.js +44 -0
  45. package/dist/pipeline/runner-selection.d.ts +2 -0
  46. package/dist/pipeline/runner-selection.js +13 -0
  47. package/dist/pipeline/state.d.ts +4 -0
  48. package/dist/pipeline/state.js +37 -0
  49. package/dist/pipeline.d.ts +3 -0
  50. package/dist/pipeline.js +213 -0
  51. package/dist/preflight.d.ts +1 -0
  52. package/dist/preflight.js +69 -0
  53. package/dist/retrospective.d.ts +26 -0
  54. package/dist/retrospective.js +211 -0
  55. package/dist/stages/agent.d.ts +2 -0
  56. package/dist/stages/agent.js +94 -0
  57. package/dist/stages/gate.d.ts +2 -0
  58. package/dist/stages/gate.js +32 -0
  59. package/dist/stages/review.d.ts +2 -0
  60. package/dist/stages/review.js +32 -0
  61. package/dist/stages/ship.d.ts +3 -0
  62. package/dist/stages/ship.js +154 -0
  63. package/dist/stages/verify.d.ts +2 -0
  64. package/dist/stages/verify.js +94 -0
  65. package/dist/types.d.ts +61 -0
  66. package/dist/types.js +1 -0
  67. package/dist/validators.d.ts +8 -0
  68. package/dist/validators.js +42 -0
  69. package/dist/verify-runner.d.ts +11 -0
  70. package/dist/verify-runner.js +110 -0
  71. package/kody.config.schema.json +7 -48
  72. package/package.json +1 -1
package/README.md CHANGED
@@ -138,23 +138,19 @@ Comment on any GitHub issue:
138
138
 
139
139
  ### Switch to a different model (optional)
140
140
 
141
- Add `litellm-config.yaml` to route all tiers through MiniMax (or any LLM):
142
-
143
- ```yaml
144
- # litellm-config.yaml
145
- model_list:
146
- - model_name: claude-haiku-4-5-20251001
147
- litellm_params:
148
- model: minimax/MiniMax-M2.7-highspeed
149
- api_key: os.environ/MINIMAX_API_KEY
150
- ```
141
+ Set the `provider` field in `kody.config.json` Kody auto-generates the LiteLLM config, starts the proxy, and routes all stages through your provider:
151
142
 
152
143
  ```json
153
- // kody.config.json — add litellmUrl
154
- { "agent": { "litellmUrl": "http://localhost:4000" } }
144
+ // kody.config.json — use MiniMax (or any LLM)
145
+ { "agent": { "provider": "minimax" } }
146
+ ```
147
+
148
+ Add the provider's API key to `.env`:
149
+ ```
150
+ MINIMAX_API_KEY=your-key-here
155
151
  ```
156
152
 
157
- Kody auto-starts the proxy and loads API keys from `.env`. [Full LiteLLM guide →](docs/LITELLM.md)
153
+ That's it. Kody auto-starts the LiteLLM proxy and loads API keys from `.env`. For advanced routing (different models per tier, custom config), add a `litellm-config.yaml`. [Full LiteLLM guide →](docs/LITELLM.md)
158
154
 
159
155
  ## Commands
160
156
 
@@ -167,16 +163,18 @@ Kody auto-starts the proxy and loads API keys from `.env`. [Full LiteLLM guide
167
163
  | `@kody fix` | Re-run from build stage. Write feedback in the comment body — it gets injected into the build prompt |
168
164
  | `@kody rerun` | Resume from the failed or paused stage |
169
165
  | `@kody rerun --from <stage>` | Resume from a specific stage |
166
+ | `@kody bootstrap` | Regenerate project memory and step files |
170
167
 
171
168
  ### CLI
172
169
 
173
170
  ```bash
171
+ kody-engine-lite init [--force] # Setup repo: workflow, config, memory, step files
172
+ kody-engine-lite bootstrap # Regenerate memory + step files (runs in GH Actions)
174
173
  kody-engine-lite run --issue-number 42 --local --cwd ./project
175
174
  kody-engine-lite run --task "Add retry utility" --local
176
175
  kody-engine-lite fix --issue-number 42 --feedback "Use middleware pattern"
177
176
  kody-engine-lite rerun --issue-number 42 --from verify
178
177
  kody-engine-lite status --task-id 42-260327-102254
179
- kody-engine-lite init [--force]
180
178
  ```
181
179
 
182
180
  ## Key Features
@@ -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
@@ -114,17 +114,15 @@ function createRunners(config) {
114
114
  if (config.agent.runners && Object.keys(config.agent.runners).length > 0) {
115
115
  const runners = {};
116
116
  for (const [name, runnerConfig] of Object.entries(config.agent.runners)) {
117
- const factory2 = RUNNER_FACTORIES[runnerConfig.type];
118
- if (factory2) {
119
- runners[name] = factory2();
117
+ const factory = RUNNER_FACTORIES[runnerConfig.type];
118
+ if (factory) {
119
+ runners[name] = factory();
120
120
  }
121
121
  }
122
122
  return runners;
123
123
  }
124
- const runnerType = config.agent.runner ?? "claude-code";
125
- const factory = RUNNER_FACTORIES[runnerType];
126
124
  const defaultName = config.agent.defaultRunner ?? "claude";
127
- return { [defaultName]: factory ? factory() : createClaudeCodeRunner() };
125
+ return { [defaultName]: createClaudeCodeRunner() };
128
126
  }
129
127
  var SIGKILL_GRACE_MS, STDERR_TAIL_CHARS, RUNNER_FACTORIES;
130
128
  var init_agent_runner = __esm({
@@ -253,12 +251,10 @@ var init_logger = __esm({
253
251
  import * as fs from "fs";
254
252
  import * as path from "path";
255
253
  function needsLitellmProxy(config) {
256
- if (config.agent.litellmUrl) return true;
257
- if (config.agent.provider && config.agent.provider !== "anthropic") return true;
258
- return false;
254
+ return !!(config.agent.provider && config.agent.provider !== "anthropic");
259
255
  }
260
- function getLitellmUrl(config) {
261
- return config.agent.litellmUrl ?? LITELLM_DEFAULT_URL;
256
+ function getLitellmUrl() {
257
+ return LITELLM_DEFAULT_URL;
262
258
  }
263
259
  function providerApiKeyEnvVar(provider) {
264
260
  return `${provider.toUpperCase()}_API_KEY`;
@@ -277,7 +273,6 @@ function getProjectConfig() {
277
273
  quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
278
274
  git: { ...DEFAULT_CONFIG.git, ...raw.git },
279
275
  github: { ...DEFAULT_CONFIG.github, ...raw.github },
280
- paths: { ...DEFAULT_CONFIG.paths, ...raw.paths },
281
276
  agent: { ...DEFAULT_CONFIG.agent, ...raw.agent },
282
277
  contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers
283
278
  };
@@ -300,7 +295,6 @@ var init_config = __esm({
300
295
  typecheck: "pnpm -s tsc --noEmit",
301
296
  lint: "pnpm -s lint",
302
297
  lintFix: "pnpm lint:fix",
303
- format: "pnpm -s format:check",
304
298
  formatFix: "pnpm format:fix",
305
299
  testUnit: "pnpm -s test"
306
300
  },
@@ -311,12 +305,7 @@ var init_config = __esm({
311
305
  owner: "",
312
306
  repo: ""
313
307
  },
314
- paths: {
315
- taskDir: ".kody/tasks"
316
- },
317
308
  agent: {
318
- runner: "claude-code",
319
- defaultRunner: "claude",
320
309
  modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
321
310
  },
322
311
  contextTiers: {
@@ -1238,9 +1227,6 @@ ${prompt}` : prompt;
1238
1227
  }
1239
1228
  function resolveModel(modelTier, stageName) {
1240
1229
  const config = getProjectConfig();
1241
- if (config.agent.usePerStageRouting && stageName) {
1242
- return stageName;
1243
- }
1244
1230
  if (config.agent.provider && config.agent.provider !== "anthropic") {
1245
1231
  return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
1246
1232
  }
@@ -1372,7 +1358,7 @@ async function executeAgentStage(ctx, def) {
1372
1358
  logger.info(` runner=${runnerName} model=${model} timeout=${def.timeout / 1e3}s`);
1373
1359
  const extraEnv = {};
1374
1360
  if (needsLitellmProxy(config)) {
1375
- extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl(config);
1361
+ extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
1376
1362
  }
1377
1363
  const sessions = ctx.sessions ?? {};
1378
1364
  const sessionInfo = getSessionInfo(def.name, sessions);
@@ -1751,8 +1737,8 @@ async function executeVerifyWithAutofix(ctx, def) {
1751
1737
  const defaultRunner = getRunnerForStage(ctx, "taskify");
1752
1738
  const diagConfig = getProjectConfig();
1753
1739
  const diagEnv = {};
1754
- if (diagConfig.agent.litellmUrl) {
1755
- diagEnv.ANTHROPIC_BASE_URL = diagConfig.agent.litellmUrl;
1740
+ if (needsLitellmProxy(diagConfig)) {
1741
+ diagEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
1756
1742
  }
1757
1743
  const diagnosis = await diagnoseFailure(
1758
1744
  "verify",
@@ -2047,7 +2033,14 @@ Updated existing PR: ${existingPr.url}
2047
2033
  PR #${existingPr.number}
2048
2034
  `);
2049
2035
  } else {
2050
- const pr = createPR(head, base, title, body);
2036
+ let pr = createPR(head, base, title, body);
2037
+ if (!pr) {
2038
+ const recovered = getPRForBranch(head);
2039
+ if (recovered) {
2040
+ logger.info(` PR recovered after create error: ${recovered.url}`);
2041
+ pr = recovered;
2042
+ }
2043
+ }
2051
2044
  if (pr) {
2052
2045
  if (ctx.input.issueNumber && !ctx.input.local) {
2053
2046
  try {
@@ -2537,8 +2530,8 @@ ${previousText}
2537
2530
  const model = resolveModel("cheap");
2538
2531
  const config = getProjectConfig();
2539
2532
  const extraEnv = {};
2540
- if (config.agent.litellmUrl) {
2541
- extraEnv.ANTHROPIC_BASE_URL = config.agent.litellmUrl;
2533
+ if (needsLitellmProxy(config)) {
2534
+ extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
2542
2535
  }
2543
2536
  const result = await runner.run("retrospective", prompt, model, 3e4, "", {
2544
2537
  cwd: ctx.projectDir,
@@ -3274,7 +3267,7 @@ import * as fs21 from "fs";
3274
3267
  import * as path20 from "path";
3275
3268
  async function ensureLitellmProxy(config, projectDir) {
3276
3269
  if (!needsLitellmProxy(config)) return null;
3277
- const litellmUrl = getLitellmUrl(config);
3270
+ const litellmUrl = getLitellmUrl();
3278
3271
  const proxyRunning = await checkLitellmHealth(litellmUrl);
3279
3272
  let litellmProcess = null;
3280
3273
  if (!proxyRunning) {
@@ -3770,16 +3763,13 @@ function buildConfig(cwd, basic) {
3770
3763
  typecheck: find("typecheck", "type-check") || (pkg.devDependencies?.typescript ? `${basic.pm} tsc --noEmit` : ""),
3771
3764
  lint: find("lint"),
3772
3765
  lintFix: find("lint:fix", "lint-fix"),
3773
- format: find("format:check"),
3774
3766
  formatFix: find("format", "format:fix"),
3775
3767
  testUnit: find("test:unit", "test", "test:ci")
3776
3768
  },
3777
3769
  git: { defaultBranch: basic.defaultBranch },
3778
3770
  github: { owner: basic.owner, repo: basic.repo },
3779
- paths: { taskDir: ".kody/tasks" },
3780
3771
  agent: {
3781
- runner: "claude-code",
3782
- defaultRunner: "claude",
3772
+ provider: "anthropic",
3783
3773
  modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
3784
3774
  }
3785
3775
  };
@@ -3858,55 +3848,8 @@ function initCommand(opts) {
3858
3848
  console.log(` \u2717 ${c.name} \u2014 ${c.fix}`);
3859
3849
  }
3860
3850
  }
3861
- const labels = [
3862
- { name: "kody:planning", color: "c5def5", description: "Kody is analyzing and planning" },
3863
- { name: "kody:building", color: "0e8a16", description: "Kody is building code" },
3864
- { name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
3865
- { name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
3866
- { name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
3867
- { name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
3868
- { name: "kody:low", color: "bfdadc", description: "Low complexity \u2014 skip plan/review" },
3869
- { name: "kody:medium", color: "c5def5", description: "Medium complexity \u2014 skip review-fix" },
3870
- { name: "kody:high", color: "d4c5f9", description: "High complexity \u2014 full pipeline" },
3871
- { name: "kody:feature", color: "0e8a16", description: "New feature" },
3872
- { name: "kody:bugfix", color: "d93f0b", description: "Bug fix" },
3873
- { name: "kody:refactor", color: "fbca04", description: "Code refactoring" },
3874
- { name: "kody:docs", color: "0075ca", description: "Documentation" },
3875
- { name: "kody:chore", color: "e4e669", description: "Maintenance task" }
3876
- ];
3877
3851
  console.log("\n\u2500\u2500 Labels \u2500\u2500");
3878
- for (const label of labels) {
3879
- try {
3880
- execFileSync11("gh", [
3881
- "label",
3882
- "create",
3883
- label.name,
3884
- "--repo",
3885
- repoSlug,
3886
- "--color",
3887
- label.color,
3888
- "--description",
3889
- label.description,
3890
- "--force"
3891
- ], {
3892
- encoding: "utf-8",
3893
- timeout: 1e4,
3894
- stdio: ["pipe", "pipe", "pipe"]
3895
- });
3896
- console.log(` \u2713 ${label.name}`);
3897
- } catch {
3898
- try {
3899
- execFileSync11("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
3900
- encoding: "utf-8",
3901
- timeout: 1e4,
3902
- stdio: ["pipe", "pipe", "pipe"]
3903
- });
3904
- console.log(` \u25CB ${label.name} (exists)`);
3905
- } catch {
3906
- console.log(` \u2717 ${label.name} \u2014 failed to create`);
3907
- }
3908
- }
3909
- }
3852
+ console.log(" \u25CB Labels will be created automatically during bootstrap");
3910
3853
  }
3911
3854
  console.log("\n\u2500\u2500 Config \u2500\u2500");
3912
3855
  if (fs22.existsSync(configDest)) {
@@ -4295,6 +4238,76 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
4295
4238
  }
4296
4239
  }
4297
4240
  console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
4241
+ console.log("\n\u2500\u2500 Labels \u2500\u2500");
4242
+ try {
4243
+ let repoSlug = "";
4244
+ try {
4245
+ const configPath = path21.join(cwd, "kody.config.json");
4246
+ if (fs22.existsSync(configPath)) {
4247
+ const config = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
4248
+ if (config.github?.owner && config.github?.repo) {
4249
+ repoSlug = `${config.github.owner}/${config.github.repo}`;
4250
+ }
4251
+ }
4252
+ } catch {
4253
+ }
4254
+ if (repoSlug) {
4255
+ const labels = [
4256
+ { name: "kody:planning", color: "c5def5", description: "Kody is analyzing and planning" },
4257
+ { name: "kody:building", color: "0e8a16", description: "Kody is building code" },
4258
+ { name: "kody:review", color: "fbca04", description: "Kody is reviewing code" },
4259
+ { name: "kody:done", color: "0e8a16", description: "Kody completed successfully" },
4260
+ { name: "kody:failed", color: "d93f0b", description: "Kody pipeline failed" },
4261
+ { name: "kody:waiting", color: "fef2c0", description: "Kody is waiting for answers" },
4262
+ { name: "kody:low", color: "bfdadc", description: "Low complexity \u2014 skip plan/review" },
4263
+ { name: "kody:medium", color: "c5def5", description: "Medium complexity \u2014 skip review-fix" },
4264
+ { name: "kody:high", color: "d4c5f9", description: "High complexity \u2014 full pipeline" },
4265
+ { name: "kody:feature", color: "0e8a16", description: "New feature" },
4266
+ { name: "kody:bugfix", color: "d93f0b", description: "Bug fix" },
4267
+ { name: "kody:refactor", color: "fbca04", description: "Code refactoring" },
4268
+ { name: "kody:docs", color: "0075ca", description: "Documentation" },
4269
+ { name: "kody:chore", color: "e4e669", description: "Maintenance task" }
4270
+ ];
4271
+ for (const label of labels) {
4272
+ try {
4273
+ execFileSync11("gh", [
4274
+ "label",
4275
+ "create",
4276
+ label.name,
4277
+ "--repo",
4278
+ repoSlug,
4279
+ "--color",
4280
+ label.color,
4281
+ "--description",
4282
+ label.description,
4283
+ "--force"
4284
+ ], {
4285
+ cwd,
4286
+ encoding: "utf-8",
4287
+ timeout: 1e4,
4288
+ stdio: ["pipe", "pipe", "pipe"]
4289
+ });
4290
+ console.log(` \u2713 ${label.name}`);
4291
+ } catch {
4292
+ try {
4293
+ execFileSync11("gh", ["label", "list", "--repo", repoSlug, "--search", label.name], {
4294
+ cwd,
4295
+ encoding: "utf-8",
4296
+ timeout: 1e4,
4297
+ stdio: ["pipe", "pipe", "pipe"]
4298
+ });
4299
+ console.log(` \u25CB ${label.name} (exists)`);
4300
+ } catch {
4301
+ console.log(` \u2717 ${label.name} \u2014 failed to create`);
4302
+ }
4303
+ }
4304
+ }
4305
+ } else {
4306
+ console.log(" \u25CB Skipped \u2014 could not determine repo from kody.config.json");
4307
+ }
4308
+ } catch {
4309
+ console.log(" \u25CB Label creation skipped");
4310
+ }
4298
4311
  console.log("\n\u2500\u2500 Git \u2500\u2500");
4299
4312
  const filesToCommit = [
4300
4313
  ".kody/memory/architecture.md",
@@ -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>;