@kody-ade/kody-engine-lite 0.1.112 → 0.1.113

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 +98 -86
  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/kody.config.schema.json +31 -0
  71. package/package.json +1 -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
@@ -180,8 +180,8 @@ var init_validators = __esm({
180
180
  });
181
181
 
182
182
  // src/config.ts
183
- import * as fs8 from "fs";
184
- import * as path7 from "path";
183
+ import * as fs7 from "fs";
184
+ import * as path6 from "path";
185
185
  function resolveStageConfig(config, stageName, modelTier) {
186
186
  const stageOverride = config.agent.stages?.[stageName];
187
187
  if (stageOverride) return stageOverride;
@@ -223,10 +223,10 @@ function setConfigDir(dir) {
223
223
  }
224
224
  function getProjectConfig() {
225
225
  if (_config) return _config;
226
- const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
227
- if (fs8.existsSync(configPath)) {
226
+ const configPath = path6.join(_configDir ?? process.cwd(), "kody.config.json");
227
+ if (fs7.existsSync(configPath)) {
228
228
  try {
229
- const result = parseJsonSafe(fs8.readFileSync(configPath, "utf-8"));
229
+ const result = parseJsonSafe(fs7.readFileSync(configPath, "utf-8"));
230
230
  if (!result.ok) {
231
231
  logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
232
232
  _config = { ...DEFAULT_CONFIG };
@@ -283,7 +283,7 @@ var init_config = __esm({
283
283
  repo: ""
284
284
  },
285
285
  agent: {
286
- modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
286
+ modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
287
287
  },
288
288
  contextTiers: {
289
289
  enabled: true,
@@ -389,10 +389,11 @@ function createClaudeCodeRunner() {
389
389
  model,
390
390
  "--dangerously-skip-permissions"
391
391
  ];
392
+ const baseTools = "Bash,Edit,Read,Write,Glob,Grep";
392
393
  if (options?.mcpConfigJson) {
393
394
  args2.push("--mcp-config", options.mcpConfigJson);
394
395
  } else {
395
- args2.push("--allowedTools", "Bash,Edit,Read,Write,Glob,Grep");
396
+ args2.push("--allowedTools", baseTools);
396
397
  }
397
398
  if (options?.sessionId) {
398
399
  if (options.resumeSession) {
@@ -1192,6 +1193,7 @@ var init_litellm = __esm({
1192
1193
  // src/cli/taskify-command.ts
1193
1194
  var taskify_command_exports = {};
1194
1195
  __export(taskify_command_exports, {
1196
+ TaskifyError: () => TaskifyError,
1195
1197
  isTaskifyRun: () => isTaskifyRun,
1196
1198
  readTaskifyMarker: () => readTaskifyMarker,
1197
1199
  runTaskifyCommand: () => runTaskifyCommand,
@@ -1283,8 +1285,17 @@ async function runTaskifyCommand() {
1283
1285
  ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "dummy"
1284
1286
  };
1285
1287
  }
1286
- await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1287
- litellmProcess?.kill();
1288
+ try {
1289
+ await taskifyCommand({ ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId, runnerEnv });
1290
+ } catch (err) {
1291
+ if (err instanceof TaskifyError) {
1292
+ logger.error(`[taskify] ${err.message}`);
1293
+ process.exit(1);
1294
+ }
1295
+ throw err;
1296
+ } finally {
1297
+ litellmProcess?.kill();
1298
+ }
1288
1299
  }
1289
1300
  async function taskifyCommand(opts) {
1290
1301
  const { ticketId, prdFile, issueNumber, feedback, local, projectDir, taskId } = opts;
@@ -1299,7 +1310,6 @@ async function taskifyCommand(opts) {
1299
1310
  mcpConfigJson = buildTaskifyMcpConfigJson(config);
1300
1311
  } catch (err) {
1301
1312
  const msg = err instanceof Error ? err.message : String(err);
1302
- logger.error(`[taskify] MCP config error: ${msg}`);
1303
1313
  if (issueNumber && !local) {
1304
1314
  postComment(
1305
1315
  issueNumber,
@@ -1310,7 +1320,7 @@ async function taskifyCommand(opts) {
1310
1320
  Add the required MCP server config to \`kody.config.json\` and try again.`
1311
1321
  );
1312
1322
  }
1313
- process.exit(1);
1323
+ throw new TaskifyError(`MCP config error: ${msg}`);
1314
1324
  }
1315
1325
  }
1316
1326
  const sc = resolveStageConfig(config, "taskify", "strong");
@@ -1358,40 +1368,37 @@ Kody is decomposing ${src} into tasks...`);
1358
1368
  });
1359
1369
  if (result.outcome !== "completed") {
1360
1370
  const errMsg = result.outcome === "timed_out" ? "Taskify timed out after 5 minutes." : `Taskify failed: ${result.error}`;
1361
- logger.error(`[taskify] ${errMsg}`);
1362
1371
  if (issueNumber && !local) {
1363
1372
  postComment(issueNumber, `Kody taskify failed:
1364
1373
 
1365
1374
  > ${errMsg}`);
1366
1375
  setLifecycleLabel(issueNumber, "failed");
1367
1376
  }
1368
- process.exit(1);
1377
+ throw new TaskifyError(errMsg);
1369
1378
  }
1370
1379
  const resultPath = path10.join(taskDir, RESULT_FILE);
1371
1380
  if (!fs11.existsSync(resultPath)) {
1372
1381
  const errMsg = `Claude did not write ${RESULT_FILE}. Output:
1373
1382
 
1374
1383
  ${result.output?.slice(0, 500) ?? "(none)"}`;
1375
- logger.error(`[taskify] ${errMsg}`);
1376
1384
  if (issueNumber && !local) {
1377
1385
  postComment(issueNumber, `Kody taskify failed: result file not found.
1378
1386
 
1379
1387
  ${errMsg}`);
1380
1388
  setLifecycleLabel(issueNumber, "failed");
1381
1389
  }
1382
- process.exit(1);
1390
+ throw new TaskifyError(errMsg);
1383
1391
  }
1384
1392
  let parsed;
1385
1393
  try {
1386
1394
  parsed = JSON.parse(fs11.readFileSync(resultPath, "utf-8"));
1387
1395
  } catch {
1388
1396
  const errMsg = `Could not parse ${RESULT_FILE} as JSON.`;
1389
- logger.error(`[taskify] ${errMsg}`);
1390
1397
  if (issueNumber && !local) {
1391
1398
  postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1392
1399
  setLifecycleLabel(issueNumber, "failed");
1393
1400
  }
1394
- process.exit(1);
1401
+ throw new TaskifyError(errMsg);
1395
1402
  }
1396
1403
  const sourceLabel = ticketId ?? (prdFile ? path10.basename(prdFile) : "spec");
1397
1404
  if (parsed.status === "questions") {
@@ -1400,12 +1407,11 @@ ${errMsg}`);
1400
1407
  await handleTasks(parsed, sourceLabel, issueNumber, local ?? false);
1401
1408
  } else {
1402
1409
  const errMsg = `Unexpected status in ${RESULT_FILE}: ${JSON.stringify(parsed)}`;
1403
- logger.error(`[taskify] ${errMsg}`);
1404
1410
  if (issueNumber && !local) {
1405
1411
  postComment(issueNumber, `Kody taskify failed: ${errMsg}`);
1406
1412
  setLifecycleLabel(issueNumber, "failed");
1407
1413
  }
1408
- process.exit(1);
1414
+ throw new TaskifyError(errMsg);
1409
1415
  }
1410
1416
  }
1411
1417
  function handleQuestions(parsed, ticketId, issueNumber, local) {
@@ -1536,7 +1542,7 @@ function readTaskifyMarker(taskDir) {
1536
1542
  return null;
1537
1543
  }
1538
1544
  }
1539
- var __dirname, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
1545
+ var __dirname, TaskifyError, AUTO_TRIGGER_THRESHOLD, MAX_TASKS_GUARD, TASKIFY_TIMEOUT_MS, MARKER_FILE, RESULT_FILE;
1540
1546
  var init_taskify_command = __esm({
1541
1547
  "src/cli/taskify-command.ts"() {
1542
1548
  "use strict";
@@ -1548,6 +1554,12 @@ var init_taskify_command = __esm({
1548
1554
  init_task_resolution();
1549
1555
  init_litellm();
1550
1556
  __dirname = path10.dirname(fileURLToPath(import.meta.url));
1557
+ TaskifyError = class extends Error {
1558
+ constructor(message) {
1559
+ super(message);
1560
+ this.name = "TaskifyError";
1561
+ }
1562
+ };
1551
1563
  AUTO_TRIGGER_THRESHOLD = 5;
1552
1564
  MAX_TASKS_GUARD = 20;
1553
1565
  TASKIFY_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -2686,10 +2698,12 @@ function escalateModelTier(currentTier) {
2686
2698
  function resolveModel(modelTier, stageName) {
2687
2699
  const config = getProjectConfig();
2688
2700
  const mapped = config.agent.modelMap[modelTier];
2689
- if (mapped) return mapped;
2690
- return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
2701
+ if (!mapped) {
2702
+ throw new Error(`No model configured for tier '${modelTier}'. Set agent.modelMap.${modelTier} in kody.config.json`);
2703
+ }
2704
+ return mapped;
2691
2705
  }
2692
- var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION, DEFAULT_MODEL_MAP;
2706
+ var MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
2693
2707
  var init_context = __esm({
2694
2708
  "src/context.ts"() {
2695
2709
  "use strict";
@@ -2723,11 +2737,6 @@ var init_context = __esm({
2723
2737
  mid: "strong",
2724
2738
  strong: "strong"
2725
2739
  };
2726
- DEFAULT_MODEL_MAP = {
2727
- cheap: "haiku",
2728
- mid: "sonnet",
2729
- strong: "opus"
2730
- };
2731
2740
  }
2732
2741
  });
2733
2742
 
@@ -4912,7 +4921,7 @@ async function ensureLitellmProxy(config, projectDir) {
4912
4921
  return litellmProcess;
4913
4922
  }
4914
4923
  async function runModelHealthCheck(config) {
4915
- const usesProxy = needsLitellmProxy(config);
4924
+ const usesProxy = anyStageNeedsProxy(config);
4916
4925
  const baseUrl = usesProxy ? getLitellmUrl() : "https://api.anthropic.com";
4917
4926
  const apiKey = usesProxy ? process.env.ANTHROPIC_COMPATIBLE_API_KEY : process.env.ANTHROPIC_API_KEY;
4918
4927
  if (!apiKey) {
@@ -5533,7 +5542,7 @@ function buildConfig(cwd, basic) {
5533
5542
  github: { owner: basic.owner, repo: basic.repo },
5534
5543
  agent: {
5535
5544
  provider: "anthropic",
5536
- modelMap: { cheap: "haiku", mid: "sonnet", strong: "opus" }
5545
+ modelMap: { cheap: "claude-haiku-4-5-20251001", mid: "claude-sonnet-4-6", strong: "claude-opus-4-6" }
5537
5546
  }
5538
5547
  };
5539
5548
  const mcp = detectMcpConfig(cwd, basic.pm, pkg);
@@ -5733,8 +5742,8 @@ function initCommand(opts, pkgRoot) {
5733
5742
 
5734
5743
  // src/bin/commands/bootstrap.ts
5735
5744
  init_architecture_detection();
5736
- import * as fs7 from "fs";
5737
- import * as path6 from "path";
5745
+ import * as fs8 from "fs";
5746
+ import * as path7 from "path";
5738
5747
  import { execFileSync as execFileSync5 } from "child_process";
5739
5748
 
5740
5749
  // src/bin/qa-guide.ts
@@ -6008,22 +6017,23 @@ function installSkillsForProject(cwd) {
6008
6017
  }
6009
6018
 
6010
6019
  // src/bin/commands/bootstrap.ts
6020
+ init_config();
6011
6021
  var STEP_STAGES = ["taskify", "plan", "build", "autofix", "review", "review-fix"];
6012
6022
  function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
6013
- const srcDir = path6.join(cwd, "src");
6014
- const baseDir = fs7.existsSync(srcDir) ? srcDir : cwd;
6023
+ const srcDir = path7.join(cwd, "src");
6024
+ const baseDir = fs8.existsSync(srcDir) ? srcDir : cwd;
6015
6025
  const results = [];
6016
6026
  function walk(dir) {
6017
6027
  const entries = [];
6018
6028
  try {
6019
- for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
6029
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
6020
6030
  if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
6021
- const full = path6.join(dir, entry.name);
6031
+ const full = path7.join(dir, entry.name);
6022
6032
  if (entry.isDirectory()) {
6023
6033
  entries.push(...walk(full));
6024
6034
  } else if (/\.(ts|js)$/.test(entry.name) && !/\.(test|spec|config|d)\.(ts|js)$/.test(entry.name)) {
6025
6035
  try {
6026
- const stat = fs7.statSync(full);
6036
+ const stat = fs8.statSync(full);
6027
6037
  if (stat.size >= 200 && stat.size <= 5e3) {
6028
6038
  entries.push({ filePath: full, size: stat.size });
6029
6039
  }
@@ -6037,8 +6047,8 @@ function gatherSampleSourceFiles(cwd, maxFiles = 3, maxCharsEach = 2e3) {
6037
6047
  }
6038
6048
  const files = walk(baseDir).sort((a, b) => b.size - a.size).slice(0, maxFiles);
6039
6049
  for (const { filePath } of files) {
6040
- const rel = path6.relative(cwd, filePath);
6041
- const content = fs7.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
6050
+ const rel = path7.relative(cwd, filePath);
6051
+ const content = fs8.readFileSync(filePath, "utf-8").slice(0, maxCharsEach);
6042
6052
  results.push(`### File: ${rel}
6043
6053
  \`\`\`typescript
6044
6054
  ${content}
@@ -6050,9 +6060,9 @@ function ghComment(issueNumber, body, cwd) {
6050
6060
  try {
6051
6061
  let repoSlug = "";
6052
6062
  try {
6053
- const configPath = path6.join(cwd, "kody.config.json");
6054
- if (fs7.existsSync(configPath)) {
6055
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
6063
+ const configPath = path7.join(cwd, "kody.config.json");
6064
+ if (fs8.existsSync(configPath)) {
6065
+ const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6056
6066
  if (config.github?.owner && config.github?.repo) {
6057
6067
  repoSlug = `${config.github.owner}/${config.github.repo}`;
6058
6068
  }
@@ -6079,7 +6089,9 @@ function ghComment(issueNumber, body, cwd) {
6079
6089
  }
6080
6090
  function bootstrapCommand(opts, pkgRoot) {
6081
6091
  const cwd = process.cwd();
6092
+ setConfigDir(cwd);
6082
6093
  const issueNumber = parseInt(process.env.ISSUE_NUMBER ?? "", 10) || 0;
6094
+ const bootstrapModel = resolveStageConfig(getProjectConfig(), "bootstrap", "cheap").model;
6083
6095
  console.log(`
6084
6096
  \u{1F527} Kody Bootstrap \u2014 Generating project memory + step files
6085
6097
  `);
@@ -6087,8 +6099,8 @@ function bootstrapCommand(opts, pkgRoot) {
6087
6099
  ghComment(issueNumber, "\u{1F527} **Bootstrap started** \u2014 analyzing project and generating configuration...", cwd);
6088
6100
  }
6089
6101
  const readIfExists = (rel, maxChars = 3e3) => {
6090
- const p = path6.join(cwd, rel);
6091
- if (fs7.existsSync(p)) return fs7.readFileSync(p, "utf-8").slice(0, maxChars);
6102
+ const p = path7.join(cwd, rel);
6103
+ if (fs8.existsSync(p)) return fs8.readFileSync(p, "utf-8").slice(0, maxChars);
6092
6104
  return null;
6093
6105
  };
6094
6106
  let repoContext = "";
@@ -6123,14 +6135,14 @@ ${sampleFiles}
6123
6135
 
6124
6136
  `;
6125
6137
  try {
6126
- const topDirs = fs7.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
6138
+ const topDirs = fs8.readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name);
6127
6139
  repoContext += `## Top-level directories
6128
6140
  ${topDirs.join(", ")}
6129
6141
 
6130
6142
  `;
6131
- const srcDir = path6.join(cwd, "src");
6132
- if (fs7.existsSync(srcDir)) {
6133
- const srcDirs = fs7.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6143
+ const srcDir = path7.join(cwd, "src");
6144
+ if (fs8.existsSync(srcDir)) {
6145
+ const srcDirs = fs8.readdirSync(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
6134
6146
  if (srcDirs.length > 0) repoContext += `## src/ subdirectories
6135
6147
  ${srcDirs.join(", ")}
6136
6148
 
@@ -6140,19 +6152,19 @@ ${srcDirs.join(", ")}
6140
6152
  }
6141
6153
  const existingFiles = [];
6142
6154
  for (const f of [".env.example", "CLAUDE.md", ".ai-docs", "vitest.config.ts", "vitest.config.mts", "jest.config.ts", "playwright.config.ts", ".eslintrc.js", "eslint.config.mjs", ".prettierrc"]) {
6143
- if (fs7.existsSync(path6.join(cwd, f))) existingFiles.push(f);
6155
+ if (fs8.existsSync(path7.join(cwd, f))) existingFiles.push(f);
6144
6156
  }
6145
6157
  if (existingFiles.length) repoContext += `## Config files present
6146
6158
  ${existingFiles.join(", ")}
6147
6159
 
6148
6160
  `;
6149
6161
  console.log("\u2500\u2500 Project Memory \u2500\u2500");
6150
- const memoryDir = path6.join(cwd, ".kody", "memory");
6151
- fs7.mkdirSync(memoryDir, { recursive: true });
6152
- const archPath = path6.join(memoryDir, "architecture.md");
6153
- const conventionsPath = path6.join(memoryDir, "conventions.md");
6154
- const existingArch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
6155
- const existingConv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
6162
+ const memoryDir = path7.join(cwd, ".kody", "memory");
6163
+ fs8.mkdirSync(memoryDir, { recursive: true });
6164
+ const archPath = path7.join(memoryDir, "architecture.md");
6165
+ const conventionsPath = path7.join(memoryDir, "conventions.md");
6166
+ const existingArch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
6167
+ const existingConv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
6156
6168
  const hasExisting = !!(existingArch || existingConv);
6157
6169
  const extendInstruction = hasExisting && !opts.force ? `
6158
6170
  ## Existing Documentation (EXTEND, do not replace)
@@ -6196,7 +6208,7 @@ ${repoContext}`;
6196
6208
  const output = execFileSync5("claude", [
6197
6209
  "--print",
6198
6210
  "--model",
6199
- "claude-haiku-4-5-20251001",
6211
+ bootstrapModel,
6200
6212
  "--dangerously-skip-permissions",
6201
6213
  memoryPrompt
6202
6214
  ], {
@@ -6208,12 +6220,12 @@ ${repoContext}`;
6208
6220
  const cleaned = output.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
6209
6221
  const parsed = JSON.parse(cleaned);
6210
6222
  if (parsed.architecture) {
6211
- fs7.writeFileSync(archPath, parsed.architecture);
6223
+ fs8.writeFileSync(archPath, parsed.architecture);
6212
6224
  const lineCount = parsed.architecture.split("\n").length;
6213
6225
  console.log(` \u2713 .kody/memory/architecture.md (${lineCount} lines)`);
6214
6226
  }
6215
6227
  if (parsed.conventions) {
6216
- fs7.writeFileSync(conventionsPath, parsed.conventions);
6228
+ fs8.writeFileSync(conventionsPath, parsed.conventions);
6217
6229
  const lineCount = parsed.conventions.split("\n").length;
6218
6230
  console.log(` \u2713 .kody/memory/conventions.md (${lineCount} lines)`);
6219
6231
  }
@@ -6222,39 +6234,39 @@ ${repoContext}`;
6222
6234
  const detected = detectArchitectureBasic(cwd);
6223
6235
  if (detected.length > 0) {
6224
6236
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6225
- fs7.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
6237
+ fs8.writeFileSync(archPath, `# Architecture (auto-detected ${timestamp2})
6226
6238
 
6227
6239
  ## Overview
6228
6240
  ${detected.join("\n")}
6229
6241
  `);
6230
6242
  console.log(` \u2713 .kody/memory/architecture.md (${detected.length} items, basic detection)`);
6231
6243
  }
6232
- fs7.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
6244
+ fs8.writeFileSync(conventionsPath, "# Conventions\n\n<!-- Auto-learned conventions will be appended here -->\n");
6233
6245
  console.log(" \u2713 .kody/memory/conventions.md (seed)");
6234
6246
  }
6235
6247
  console.log("\n\u2500\u2500 Step Files \u2500\u2500");
6236
- const stepsDir = path6.join(cwd, ".kody", "steps");
6237
- fs7.mkdirSync(stepsDir, { recursive: true });
6238
- const arch = fs7.existsSync(archPath) ? fs7.readFileSync(archPath, "utf-8") : "";
6239
- const conv = fs7.existsSync(conventionsPath) ? fs7.readFileSync(conventionsPath, "utf-8") : "";
6248
+ const stepsDir = path7.join(cwd, ".kody", "steps");
6249
+ fs8.mkdirSync(stepsDir, { recursive: true });
6250
+ const arch = fs8.existsSync(archPath) ? fs8.readFileSync(archPath, "utf-8") : "";
6251
+ const conv = fs8.existsSync(conventionsPath) ? fs8.readFileSync(conventionsPath, "utf-8") : "";
6240
6252
  console.log(" \u23F3 Customizing step files...");
6241
6253
  let stepCount = 0;
6242
6254
  for (const stage of STEP_STAGES) {
6243
- const templatePath = path6.join(pkgRoot, "prompts", `${stage}.md`);
6244
- if (!fs7.existsSync(templatePath)) {
6255
+ const templatePath = path7.join(pkgRoot, "prompts", `${stage}.md`);
6256
+ if (!fs8.existsSync(templatePath)) {
6245
6257
  console.log(` \u2717 ${stage}.md \u2014 template not found in engine`);
6246
6258
  continue;
6247
6259
  }
6248
- const stepOutputPath = path6.join(stepsDir, `${stage}.md`);
6249
- if (fs7.existsSync(stepOutputPath) && !opts.force) {
6260
+ const stepOutputPath = path7.join(stepsDir, `${stage}.md`);
6261
+ if (fs8.existsSync(stepOutputPath) && !opts.force) {
6250
6262
  console.log(` \u25CB ${stage}.md \u2014 already exists (use --force to regenerate)`);
6251
6263
  continue;
6252
6264
  }
6253
- const defaultPrompt = fs7.readFileSync(templatePath, "utf-8");
6265
+ const defaultPrompt = fs8.readFileSync(templatePath, "utf-8");
6254
6266
  const contextPlaceholder = "{{TASK_CONTEXT}}";
6255
6267
  const placeholderIdx = defaultPrompt.indexOf(contextPlaceholder);
6256
6268
  if (placeholderIdx === -1) {
6257
- fs7.copyFileSync(templatePath, stepOutputPath);
6269
+ fs8.copyFileSync(templatePath, stepOutputPath);
6258
6270
  stepCount++;
6259
6271
  console.log(` \u2713 ${stage}.md`);
6260
6272
  continue;
@@ -6299,7 +6311,7 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6299
6311
  const output = execFileSync5("claude", [
6300
6312
  "--print",
6301
6313
  "--model",
6302
- "claude-haiku-4-5-20251001",
6314
+ bootstrapModel,
6303
6315
  "--dangerously-skip-permissions",
6304
6316
  customizationPrompt
6305
6317
  ], {
@@ -6311,23 +6323,23 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6311
6323
  let cleaned = output.replace(/^```(?:markdown|md)?\s*\n?/, "").replace(/\n?```\s*$/, "");
6312
6324
  cleaned = cleaned.replace(/\n*\{\{TASK_CONTEXT\}\}\s*$/, "").trimEnd();
6313
6325
  const finalPrompt = cleaned + "\n\n" + afterPlaceholder;
6314
- fs7.writeFileSync(stepOutputPath, finalPrompt);
6326
+ fs8.writeFileSync(stepOutputPath, finalPrompt);
6315
6327
  stepCount++;
6316
6328
  console.log(` \u2713 ${stage}.md`);
6317
6329
  } catch {
6318
6330
  console.log(` \u26A0 ${stage}.md \u2014 customization failed, using default template`);
6319
- fs7.copyFileSync(templatePath, stepOutputPath);
6331
+ fs8.copyFileSync(templatePath, stepOutputPath);
6320
6332
  stepCount++;
6321
6333
  }
6322
6334
  }
6323
6335
  console.log(` \u2713 Generated ${stepCount} step files in .kody/steps/`);
6324
6336
  console.log("\n\u2500\u2500 QA Guide \u2500\u2500");
6325
- const qaGuidePath = path6.join(cwd, ".kody", "qa-guide.md");
6326
- if (!fs7.existsSync(qaGuidePath) || opts.force) {
6337
+ const qaGuidePath = path7.join(cwd, ".kody", "qa-guide.md");
6338
+ if (!fs8.existsSync(qaGuidePath) || opts.force) {
6327
6339
  const discovery = discoverQaContext(cwd);
6328
6340
  if (discovery.routes.length > 0) {
6329
6341
  const qaGuide = generateQaGuide(discovery);
6330
- fs7.writeFileSync(qaGuidePath, qaGuide);
6342
+ fs8.writeFileSync(qaGuidePath, qaGuide);
6331
6343
  console.log(` \u2713 .kody/qa-guide.md (${discovery.routes.length} routes, ${discovery.roles.length} roles)`);
6332
6344
  if (discovery.loginPage) console.log(` \u2713 Login page detected: ${discovery.loginPage}`);
6333
6345
  if (discovery.adminPath) console.log(` \u2713 Admin panel detected: ${discovery.adminPath}`);
@@ -6342,9 +6354,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6342
6354
  try {
6343
6355
  let repoSlug = "";
6344
6356
  try {
6345
- const configPath = path6.join(cwd, "kody.config.json");
6346
- if (fs7.existsSync(configPath)) {
6347
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
6357
+ const configPath = path7.join(cwd, "kody.config.json");
6358
+ if (fs8.existsSync(configPath)) {
6359
+ const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6348
6360
  if (config.github?.owner && config.github?.repo) {
6349
6361
  repoSlug = `${config.github.owner}/${config.github.repo}`;
6350
6362
  }
@@ -6417,19 +6429,19 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6417
6429
  ".kody/memory/conventions.md",
6418
6430
  ".kody/qa-guide.md",
6419
6431
  ...installedSkillPaths
6420
- ].filter((f) => fs7.existsSync(path6.join(cwd, f)));
6421
- if (fs7.existsSync(path6.join(cwd, "skills-lock.json"))) {
6432
+ ].filter((f) => fs8.existsSync(path7.join(cwd, f)));
6433
+ if (fs8.existsSync(path7.join(cwd, "skills-lock.json"))) {
6422
6434
  filesToCommit.push("skills-lock.json");
6423
6435
  }
6424
6436
  for (const stage of STEP_STAGES) {
6425
6437
  const stepFile = `.kody/steps/${stage}.md`;
6426
- if (fs7.existsSync(path6.join(cwd, stepFile))) {
6438
+ if (fs8.existsSync(path7.join(cwd, stepFile))) {
6427
6439
  filesToCommit.push(stepFile);
6428
6440
  }
6429
6441
  }
6430
6442
  if (filesToCommit.length > 0) {
6431
6443
  try {
6432
- const fullPaths = filesToCommit.map((f) => path6.join(cwd, f));
6444
+ const fullPaths = filesToCommit.map((f) => path7.join(cwd, f));
6433
6445
  for (let pass = 0; pass < 2; pass++) {
6434
6446
  execFileSync5("npx", ["prettier", "--write", ...fullPaths], {
6435
6447
  cwd,
@@ -6456,9 +6468,9 @@ REMINDER: Output the full prompt template first (unchanged), then your three app
6456
6468
  console.log(` \u2713 Pushed branch: ${branchName}`);
6457
6469
  let baseBranch = "main";
6458
6470
  try {
6459
- const configPath = path6.join(cwd, "kody.config.json");
6460
- if (fs7.existsSync(configPath)) {
6461
- const config = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
6471
+ const configPath = path7.join(cwd, "kody.config.json");
6472
+ if (fs8.existsSync(configPath)) {
6473
+ const config = JSON.parse(fs8.readFileSync(configPath, "utf-8"));
6462
6474
  baseBranch = config.git?.defaultBranch ?? "main";
6463
6475
  }
6464
6476
  } catch {
@@ -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 {};