@kody-ade/kody-engine-lite 0.1.104 → 0.1.106

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 (73) hide show
  1. package/dist/bin/cli.js +626 -171
  2. package/kody.config.schema.json +5 -0
  3. package/package.json +1 -1
  4. package/prompts/taskify.md +0 -5
  5. package/templates/kody.yml +30 -103
  6. package/dist/agent-runner.d.ts +0 -4
  7. package/dist/agent-runner.js +0 -122
  8. package/dist/ci/parse-inputs.d.ts +0 -6
  9. package/dist/ci/parse-inputs.js +0 -76
  10. package/dist/ci/parse-safety.d.ts +0 -6
  11. package/dist/ci/parse-safety.js +0 -22
  12. package/dist/cli/args.d.ts +0 -13
  13. package/dist/cli/args.js +0 -42
  14. package/dist/cli/litellm.d.ts +0 -2
  15. package/dist/cli/litellm.js +0 -85
  16. package/dist/cli/task-resolution.d.ts +0 -2
  17. package/dist/cli/task-resolution.js +0 -41
  18. package/dist/config.d.ts +0 -49
  19. package/dist/config.js +0 -72
  20. package/dist/context.d.ts +0 -4
  21. package/dist/context.js +0 -83
  22. package/dist/definitions.d.ts +0 -3
  23. package/dist/definitions.js +0 -59
  24. package/dist/entry.d.ts +0 -1
  25. package/dist/entry.js +0 -236
  26. package/dist/git-utils.d.ts +0 -13
  27. package/dist/git-utils.js +0 -174
  28. package/dist/github-api.d.ts +0 -14
  29. package/dist/github-api.js +0 -114
  30. package/dist/kody-utils.d.ts +0 -1
  31. package/dist/kody-utils.js +0 -9
  32. package/dist/learning/auto-learn.d.ts +0 -2
  33. package/dist/learning/auto-learn.js +0 -169
  34. package/dist/logger.d.ts +0 -14
  35. package/dist/logger.js +0 -51
  36. package/dist/memory.d.ts +0 -1
  37. package/dist/memory.js +0 -20
  38. package/dist/observer.d.ts +0 -9
  39. package/dist/observer.js +0 -80
  40. package/dist/pipeline/complexity.d.ts +0 -3
  41. package/dist/pipeline/complexity.js +0 -12
  42. package/dist/pipeline/executor-registry.d.ts +0 -3
  43. package/dist/pipeline/executor-registry.js +0 -20
  44. package/dist/pipeline/hooks.d.ts +0 -17
  45. package/dist/pipeline/hooks.js +0 -110
  46. package/dist/pipeline/questions.d.ts +0 -2
  47. package/dist/pipeline/questions.js +0 -44
  48. package/dist/pipeline/runner-selection.d.ts +0 -2
  49. package/dist/pipeline/runner-selection.js +0 -13
  50. package/dist/pipeline/state.d.ts +0 -4
  51. package/dist/pipeline/state.js +0 -37
  52. package/dist/pipeline.d.ts +0 -3
  53. package/dist/pipeline.js +0 -213
  54. package/dist/preflight.d.ts +0 -1
  55. package/dist/preflight.js +0 -69
  56. package/dist/retrospective.d.ts +0 -26
  57. package/dist/retrospective.js +0 -211
  58. package/dist/stages/agent.d.ts +0 -2
  59. package/dist/stages/agent.js +0 -94
  60. package/dist/stages/gate.d.ts +0 -2
  61. package/dist/stages/gate.js +0 -32
  62. package/dist/stages/review.d.ts +0 -2
  63. package/dist/stages/review.js +0 -32
  64. package/dist/stages/ship.d.ts +0 -3
  65. package/dist/stages/ship.js +0 -154
  66. package/dist/stages/verify.d.ts +0 -2
  67. package/dist/stages/verify.js +0 -94
  68. package/dist/types.d.ts +0 -61
  69. package/dist/types.js +0 -1
  70. package/dist/validators.d.ts +0 -8
  71. package/dist/validators.js +0 -42
  72. package/dist/verify-runner.d.ts +0 -11
  73. package/dist/verify-runner.js +0 -110
package/dist/bin/cli.js CHANGED
@@ -64,6 +64,193 @@ var init_architecture_detection = __esm({
64
64
  }
65
65
  });
66
66
 
67
+ // src/ci/parse-inputs.ts
68
+ var parse_inputs_exports = {};
69
+ __export(parse_inputs_exports, {
70
+ parseCommentInputs: () => parseCommentInputs,
71
+ runCiParse: () => runCiParse,
72
+ writeOutputs: () => writeOutputs
73
+ });
74
+ import * as fs8 from "fs";
75
+ function generateTimestamp() {
76
+ const now = /* @__PURE__ */ new Date();
77
+ const pad = (n) => String(n).padStart(2, "0");
78
+ const y = String(now.getFullYear()).slice(2);
79
+ const m = pad(now.getMonth() + 1);
80
+ const d = pad(now.getDate());
81
+ const H = pad(now.getHours());
82
+ const M = pad(now.getMinutes());
83
+ const S = pad(now.getSeconds());
84
+ return `${y}${m}${d}-${H}${M}${S}`;
85
+ }
86
+ function parseCommentInputs() {
87
+ const triggerType = process.env.TRIGGER_TYPE ?? "dispatch";
88
+ if (triggerType === "dispatch") {
89
+ const taskId2 = process.env.INPUT_TASK_ID ?? "";
90
+ return {
91
+ task_id: taskId2,
92
+ mode: process.env.INPUT_MODE ?? "full",
93
+ from_stage: process.env.INPUT_FROM_STAGE ?? "",
94
+ issue_number: process.env.INPUT_ISSUE_NUMBER ?? "",
95
+ pr_number: "",
96
+ feedback: process.env.INPUT_FEEDBACK ?? "",
97
+ complexity: "",
98
+ ci_run_id: "",
99
+ dry_run: false,
100
+ valid: !!taskId2,
101
+ trigger_type: "dispatch"
102
+ };
103
+ }
104
+ const commentBody = (process.env.COMMENT_BODY ?? "").replace(/\r/g, "");
105
+ const issueNumber = process.env.ISSUE_NUMBER ?? "";
106
+ const isPR = !!process.env.ISSUE_IS_PR;
107
+ const kodyMatch = commentBody.match(/(?:@kody|\/kody)\s*(.*)/i);
108
+ if (!kodyMatch) {
109
+ return {
110
+ task_id: "",
111
+ mode: "full",
112
+ from_stage: "",
113
+ issue_number: issueNumber,
114
+ pr_number: "",
115
+ feedback: "",
116
+ complexity: "",
117
+ ci_run_id: "",
118
+ dry_run: false,
119
+ valid: false,
120
+ trigger_type: "comment"
121
+ };
122
+ }
123
+ const argsLine = kodyMatch[1].trim();
124
+ let fromStage = "";
125
+ let feedback = "";
126
+ let complexity = "";
127
+ let dryRun = false;
128
+ let ciRunId = "";
129
+ const fromMatch = argsLine.match(/--from\s+(\S+)/);
130
+ if (fromMatch) fromStage = fromMatch[1];
131
+ const feedbackMatch = argsLine.match(/--feedback\s+"([^"]*)"/);
132
+ if (feedbackMatch) feedback = feedbackMatch[1];
133
+ const complexityMatch = argsLine.match(/--complexity\s+(\S+)/);
134
+ if (complexityMatch) complexity = complexityMatch[1];
135
+ if (/--dry-run/.test(argsLine)) dryRun = true;
136
+ const ciRunIdMatch = argsLine.match(/--ci-run-id\s+(\S+)/);
137
+ if (ciRunIdMatch) ciRunId = ciRunIdMatch[1];
138
+ const positional = argsLine.replace(/--from\s+\S+/g, "").replace(/--feedback\s+"[^"]*"/g, "").replace(/--complexity\s+\S+/g, "").replace(/--dry-run/g, "").replace(/--ci-run-id\s+\S+/g, "").replace(/\s+/g, " ").trim();
139
+ const parts = positional ? positional.split(/\s+/) : [];
140
+ let mode = "full";
141
+ let taskId = "";
142
+ let idx = 0;
143
+ if (parts[idx] && VALID_MODES.includes(parts[idx])) {
144
+ mode = parts[idx];
145
+ idx++;
146
+ }
147
+ if (parts[idx] && !parts[idx].startsWith("--")) {
148
+ taskId = parts[idx];
149
+ idx++;
150
+ } else if (parts[0] && !VALID_MODES.includes(parts[0]) && !parts[0].startsWith("--")) {
151
+ taskId = parts[0];
152
+ }
153
+ const kodyLineIdx = commentBody.search(/(?:@kody|\/kody)/i);
154
+ const afterKodyLine = commentBody.slice(kodyLineIdx);
155
+ const newlineIdx = afterKodyLine.indexOf("\n");
156
+ const bodyAfterCommand = newlineIdx !== -1 ? afterKodyLine.slice(newlineIdx + 1) : "";
157
+ if (mode === "approve") {
158
+ mode = "rerun";
159
+ if (bodyAfterCommand) {
160
+ feedback = bodyAfterCommand;
161
+ }
162
+ }
163
+ if (mode === "fix") {
164
+ if (bodyAfterCommand) {
165
+ feedback = bodyAfterCommand;
166
+ }
167
+ }
168
+ if (mode === "fix-ci") {
169
+ if (bodyAfterCommand) {
170
+ feedback = bodyAfterCommand;
171
+ const runIdFromBody = bodyAfterCommand.match(/Run ID:\s*(\d+)/);
172
+ if (runIdFromBody) {
173
+ ciRunId = runIdFromBody[1];
174
+ }
175
+ }
176
+ }
177
+ if (mode === "bootstrap") {
178
+ taskId = `bootstrap-${generateTimestamp()}`;
179
+ }
180
+ const prNumber = isPR ? issueNumber : "";
181
+ if (mode === "review" && prNumber) {
182
+ taskId = `review-pr-${prNumber}-${generateTimestamp()}`;
183
+ }
184
+ if (!taskId && mode === "full") {
185
+ taskId = `${issueNumber}-${generateTimestamp()}`;
186
+ }
187
+ const modesWithoutTaskId = ["fix", "fix-ci", "status", "review", "resolve", "rerun"];
188
+ const valid = !!taskId || modesWithoutTaskId.includes(mode);
189
+ return {
190
+ task_id: taskId,
191
+ mode,
192
+ from_stage: fromStage,
193
+ issue_number: issueNumber,
194
+ pr_number: prNumber,
195
+ feedback,
196
+ complexity,
197
+ ci_run_id: ciRunId,
198
+ dry_run: dryRun,
199
+ valid,
200
+ trigger_type: "comment"
201
+ };
202
+ }
203
+ function writeOutputs(result) {
204
+ const outputFile = process.env.GITHUB_OUTPUT;
205
+ function output(key, value) {
206
+ if (outputFile) {
207
+ if (value.includes("\n")) {
208
+ fs8.appendFileSync(outputFile, `${key}<<KODY_EOF
209
+ ${value}
210
+ KODY_EOF
211
+ `);
212
+ } else {
213
+ fs8.appendFileSync(outputFile, `${key}=${value}
214
+ `);
215
+ }
216
+ }
217
+ const display = value.includes("\n") ? value.split("\n")[0] + "..." : value;
218
+ console.log(`${key}=${display}`);
219
+ }
220
+ output("task_id", result.task_id);
221
+ output("mode", result.mode);
222
+ output("from_stage", result.from_stage);
223
+ output("issue_number", result.issue_number);
224
+ output("pr_number", result.pr_number);
225
+ output("feedback", result.feedback);
226
+ output("complexity", result.complexity);
227
+ output("ci_run_id", result.ci_run_id);
228
+ output("dry_run", result.dry_run ? "true" : "false");
229
+ output("valid", result.valid ? "true" : "false");
230
+ output("trigger_type", result.trigger_type);
231
+ }
232
+ function runCiParse() {
233
+ const result = parseCommentInputs();
234
+ writeOutputs(result);
235
+ }
236
+ var VALID_MODES;
237
+ var init_parse_inputs = __esm({
238
+ "src/ci/parse-inputs.ts"() {
239
+ "use strict";
240
+ VALID_MODES = [
241
+ "full",
242
+ "rerun",
243
+ "fix",
244
+ "fix-ci",
245
+ "status",
246
+ "approve",
247
+ "review",
248
+ "resolve",
249
+ "bootstrap"
250
+ ];
251
+ }
252
+ });
253
+
67
254
  // src/agent-runner.ts
68
255
  import { spawn, execFileSync as execFileSync6 } from "child_process";
69
256
  function writeStdin(child, prompt) {
@@ -395,11 +582,36 @@ var init_validators = __esm({
395
582
  });
396
583
 
397
584
  // src/config.ts
398
- import * as fs8 from "fs";
585
+ import * as fs9 from "fs";
399
586
  import * as path7 from "path";
587
+ function resolveStageConfig(config, stageName, modelTier) {
588
+ const stageOverride = config.agent.stages?.[stageName];
589
+ if (stageOverride) return stageOverride;
590
+ if (config.agent.default) return config.agent.default;
591
+ const model = config.agent.modelMap[modelTier];
592
+ if (!model) {
593
+ throw new Error(`No model configured for stage '${stageName}' (tier: ${modelTier}). Set agent.stages.${stageName} or agent.default in kody.config.json`);
594
+ }
595
+ return {
596
+ provider: config.agent.provider ?? "claude",
597
+ model
598
+ };
599
+ }
400
600
  function needsLitellmProxy(config) {
401
601
  return !!(config.agent.provider && config.agent.provider !== "anthropic");
402
602
  }
603
+ function stageNeedsProxy(stageConfig) {
604
+ return stageConfig.provider !== "claude" && stageConfig.provider !== "anthropic";
605
+ }
606
+ function anyStageNeedsProxy(config) {
607
+ if (config.agent.stages) {
608
+ for (const sc of Object.values(config.agent.stages)) {
609
+ if (stageNeedsProxy(sc)) return true;
610
+ }
611
+ }
612
+ if (config.agent.default && stageNeedsProxy(config.agent.default)) return true;
613
+ return needsLitellmProxy(config);
614
+ }
403
615
  function getLitellmUrl() {
404
616
  return LITELLM_DEFAULT_URL;
405
617
  }
@@ -414,9 +626,9 @@ function setConfigDir(dir) {
414
626
  function getProjectConfig() {
415
627
  if (_config) return _config;
416
628
  const configPath = path7.join(_configDir ?? process.cwd(), "kody.config.json");
417
- if (fs8.existsSync(configPath)) {
629
+ if (fs9.existsSync(configPath)) {
418
630
  try {
419
- const result = parseJsonSafe(fs8.readFileSync(configPath, "utf-8"));
631
+ const result = parseJsonSafe(fs9.readFileSync(configPath, "utf-8"));
420
632
  if (!result.ok) {
421
633
  logger.warn(`kody.config.json: ${result.error} \u2014 using defaults`);
422
634
  _config = { ...DEFAULT_CONFIG };
@@ -652,6 +864,17 @@ function commitAll(message, cwd) {
652
864
  logger.info(` Committed: ${hash} ${message}`);
653
865
  return { success: true, hash, message };
654
866
  }
867
+ function getDiffFiles(baseBranch, cwd) {
868
+ try {
869
+ const output = git(["diff", "--name-only", `origin/${baseBranch}...HEAD`], { cwd });
870
+ if (!output) return [];
871
+ return output.split("\n").filter((f) => f && !f.startsWith(".kody/"));
872
+ } catch (err) {
873
+ const msg = err instanceof Error ? err.message : String(err);
874
+ logger.warn(` Failed to get diff files: ${msg}`);
875
+ return [];
876
+ }
877
+ }
655
878
  function pushBranch(cwd) {
656
879
  try {
657
880
  git(["push", "-u", "origin", "HEAD"], { cwd, timeout: 12e4 });
@@ -730,6 +953,19 @@ function getIssue(issueNumber) {
730
953
  return null;
731
954
  }
732
955
  }
956
+ function getIssueComments(issueNumber) {
957
+ try {
958
+ const output = gh([
959
+ "api",
960
+ `repos/{owner}/{repo}/issues/${issueNumber}/comments`,
961
+ "--jq",
962
+ "[.[] | {body, created_at}]"
963
+ ]);
964
+ return output ? JSON.parse(output) : [];
965
+ } catch {
966
+ return [];
967
+ }
968
+ }
733
969
  function getIssueLabels(issueNumber) {
734
970
  try {
735
971
  const output = gh(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"]);
@@ -1059,14 +1295,14 @@ var init_github_api = __esm({
1059
1295
  });
1060
1296
 
1061
1297
  // src/pipeline/state.ts
1062
- import * as fs9 from "fs";
1298
+ import * as fs10 from "fs";
1063
1299
  import * as path8 from "path";
1064
1300
  function loadState(taskId, taskDir) {
1065
1301
  const p = path8.join(taskDir, "status.json");
1066
- if (!fs9.existsSync(p)) return null;
1302
+ if (!fs10.existsSync(p)) return null;
1067
1303
  try {
1068
1304
  const result = parseJsonSafe(
1069
- fs9.readFileSync(p, "utf-8"),
1305
+ fs10.readFileSync(p, "utf-8"),
1070
1306
  ["taskId", "state", "stages", "createdAt", "updatedAt"]
1071
1307
  );
1072
1308
  if (!result.ok) {
@@ -1086,8 +1322,8 @@ function writeState(state, taskDir) {
1086
1322
  };
1087
1323
  const target = path8.join(taskDir, "status.json");
1088
1324
  const tmp = target + ".tmp";
1089
- fs9.writeFileSync(tmp, JSON.stringify(updated, null, 2));
1090
- fs9.renameSync(tmp, target);
1325
+ fs10.writeFileSync(tmp, JSON.stringify(updated, null, 2));
1326
+ fs10.renameSync(tmp, target);
1091
1327
  return updated;
1092
1328
  }
1093
1329
  function initState(taskId) {
@@ -1128,16 +1364,16 @@ var init_complexity = __esm({
1128
1364
  });
1129
1365
 
1130
1366
  // src/memory.ts
1131
- import * as fs10 from "fs";
1367
+ import * as fs11 from "fs";
1132
1368
  import * as path9 from "path";
1133
1369
  function readProjectMemory(projectDir) {
1134
1370
  const memoryDir = path9.join(projectDir, ".kody", "memory");
1135
- if (!fs10.existsSync(memoryDir)) return "";
1136
- const files = fs10.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1371
+ if (!fs11.existsSync(memoryDir)) return "";
1372
+ const files = fs11.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1137
1373
  if (files.length === 0) return "";
1138
1374
  const sections = [];
1139
1375
  for (const file of files) {
1140
- const content = fs10.readFileSync(path9.join(memoryDir, file), "utf-8").trim();
1376
+ const content = fs11.readFileSync(path9.join(memoryDir, file), "utf-8").trim();
1141
1377
  if (content) {
1142
1378
  sections.push(`## ${file.replace(".md", "")}
1143
1379
  ${content}`);
@@ -1156,7 +1392,7 @@ var init_memory = __esm({
1156
1392
  });
1157
1393
 
1158
1394
  // src/context-tiers.ts
1159
- import * as fs11 from "fs";
1395
+ import * as fs12 from "fs";
1160
1396
  import * as path10 from "path";
1161
1397
  function estimateTokens(text) {
1162
1398
  return Math.ceil(text.length / 4);
@@ -1261,14 +1497,14 @@ function selectTier(tiered, tier) {
1261
1497
  }
1262
1498
  function readProjectMemoryTiered(projectDir, tier) {
1263
1499
  const memoryDir = path10.join(projectDir, ".kody", "memory");
1264
- if (!fs11.existsSync(memoryDir)) return "";
1265
- const files = fs11.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1500
+ if (!fs12.existsSync(memoryDir)) return "";
1501
+ const files = fs12.readdirSync(memoryDir).filter((f) => f.endsWith(".md")).sort();
1266
1502
  if (files.length === 0) return "";
1267
1503
  const tierLabel2 = tier === "L2" ? "full" : tier === "L1" ? "overview" : "abstract";
1268
1504
  const sections = [];
1269
1505
  for (const file of files) {
1270
1506
  const filePath = path10.join(memoryDir, file);
1271
- const content = fs11.readFileSync(filePath, "utf-8").trim();
1507
+ const content = fs12.readFileSync(filePath, "utf-8").trim();
1272
1508
  if (!content) continue;
1273
1509
  const tiered = getTieredContent(filePath, content);
1274
1510
  const selected = selectTier(tiered, tier);
@@ -1292,8 +1528,8 @@ function injectTaskContextTiered(prompt, taskId, taskDir, policy, feedback) {
1292
1528
  context += `Task Directory: ${taskDir}
1293
1529
  `;
1294
1530
  const taskMdPath = path10.join(taskDir, "task.md");
1295
- if (fs11.existsSync(taskMdPath)) {
1296
- const content = fs11.readFileSync(taskMdPath, "utf-8");
1531
+ if (fs12.existsSync(taskMdPath)) {
1532
+ const content = fs12.readFileSync(taskMdPath, "utf-8");
1297
1533
  const selected = selectContent(taskMdPath, content, policy.taskDescription);
1298
1534
  const label = tierLabel("Task Description", policy.taskDescription);
1299
1535
  context += `
@@ -1302,8 +1538,8 @@ ${selected}
1302
1538
  `;
1303
1539
  }
1304
1540
  const taskJsonPath = path10.join(taskDir, "task.json");
1305
- if (fs11.existsSync(taskJsonPath)) {
1306
- const content = fs11.readFileSync(taskJsonPath, "utf-8");
1541
+ if (fs12.existsSync(taskJsonPath)) {
1542
+ const content = fs12.readFileSync(taskJsonPath, "utf-8");
1307
1543
  if (policy.taskClassification === "L2") {
1308
1544
  try {
1309
1545
  const taskDef = JSON.parse(content.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, ""));
@@ -1330,8 +1566,8 @@ ${selected}
1330
1566
  }
1331
1567
  }
1332
1568
  const specPath = path10.join(taskDir, "spec.md");
1333
- if (fs11.existsSync(specPath)) {
1334
- const content = fs11.readFileSync(specPath, "utf-8");
1569
+ if (fs12.existsSync(specPath)) {
1570
+ const content = fs12.readFileSync(specPath, "utf-8");
1335
1571
  const selected = selectContent(specPath, content, policy.spec);
1336
1572
  const label = tierLabel("Spec", policy.spec);
1337
1573
  context += `
@@ -1340,8 +1576,8 @@ ${selected}
1340
1576
  `;
1341
1577
  }
1342
1578
  const planPath = path10.join(taskDir, "plan.md");
1343
- if (fs11.existsSync(planPath)) {
1344
- const content = fs11.readFileSync(planPath, "utf-8");
1579
+ if (fs12.existsSync(planPath)) {
1580
+ const content = fs12.readFileSync(planPath, "utf-8");
1345
1581
  const selected = selectContent(planPath, content, policy.plan);
1346
1582
  const label = tierLabel("Plan", policy.plan);
1347
1583
  context += `
@@ -1350,8 +1586,8 @@ ${selected}
1350
1586
  `;
1351
1587
  }
1352
1588
  const contextMdPath = path10.join(taskDir, "context.md");
1353
- if (fs11.existsSync(contextMdPath)) {
1354
- const content = fs11.readFileSync(contextMdPath, "utf-8");
1589
+ if (fs12.existsSync(contextMdPath)) {
1590
+ const content = fs12.readFileSync(contextMdPath, "utf-8");
1355
1591
  const selected = selectContent(contextMdPath, content, policy.accumulatedContext);
1356
1592
  const label = tierLabel("Previous Stage Context", policy.accumulatedContext);
1357
1593
  context += `
@@ -1483,13 +1719,13 @@ var init_mcp_config = __esm({
1483
1719
  });
1484
1720
 
1485
1721
  // src/context.ts
1486
- import * as fs12 from "fs";
1722
+ import * as fs13 from "fs";
1487
1723
  import * as path11 from "path";
1488
1724
  function readPromptFile(stageName, projectDir) {
1489
1725
  if (projectDir) {
1490
1726
  const stepFile = path11.join(projectDir, ".kody", "steps", `${stageName}.md`);
1491
- if (fs12.existsSync(stepFile)) {
1492
- return fs12.readFileSync(stepFile, "utf-8");
1727
+ if (fs13.existsSync(stepFile)) {
1728
+ return fs13.readFileSync(stepFile, "utf-8");
1493
1729
  }
1494
1730
  console.warn(` \u26A0 No step file at ${stepFile}, falling back to engine defaults. Run 'kody-engine-lite init --force' to generate step files.`);
1495
1731
  }
@@ -1499,8 +1735,8 @@ function readPromptFile(stageName, projectDir) {
1499
1735
  path11.resolve(scriptDir, "..", "..", "prompts", `${stageName}.md`)
1500
1736
  ];
1501
1737
  for (const candidate of candidates) {
1502
- if (fs12.existsSync(candidate)) {
1503
- return fs12.readFileSync(candidate, "utf-8");
1738
+ if (fs13.existsSync(candidate)) {
1739
+ return fs13.readFileSync(candidate, "utf-8");
1504
1740
  }
1505
1741
  }
1506
1742
  throw new Error(`Prompt file not found: tried ${candidates.join(", ")}`);
@@ -1513,17 +1749,17 @@ function injectTaskContext(prompt, taskId, taskDir, feedback) {
1513
1749
  context += `Task Directory: ${taskDir}
1514
1750
  `;
1515
1751
  const taskMdPath = path11.join(taskDir, "task.md");
1516
- if (fs12.existsSync(taskMdPath)) {
1517
- const taskMd = fs12.readFileSync(taskMdPath, "utf-8");
1752
+ if (fs13.existsSync(taskMdPath)) {
1753
+ const taskMd = fs13.readFileSync(taskMdPath, "utf-8");
1518
1754
  context += `
1519
1755
  ## Task Description
1520
1756
  ${taskMd}
1521
1757
  `;
1522
1758
  }
1523
1759
  const taskJsonPath = path11.join(taskDir, "task.json");
1524
- if (fs12.existsSync(taskJsonPath)) {
1760
+ if (fs13.existsSync(taskJsonPath)) {
1525
1761
  try {
1526
- const taskDef = JSON.parse(fs12.readFileSync(taskJsonPath, "utf-8"));
1762
+ const taskDef = JSON.parse(fs13.readFileSync(taskJsonPath, "utf-8"));
1527
1763
  context += `
1528
1764
  ## Task Classification
1529
1765
  `;
@@ -1537,8 +1773,8 @@ ${taskMd}
1537
1773
  }
1538
1774
  }
1539
1775
  const specPath = path11.join(taskDir, "spec.md");
1540
- if (fs12.existsSync(specPath)) {
1541
- const spec = fs12.readFileSync(specPath, "utf-8");
1776
+ if (fs13.existsSync(specPath)) {
1777
+ const spec = fs13.readFileSync(specPath, "utf-8");
1542
1778
  const truncated = spec.slice(0, MAX_TASK_CONTEXT_SPEC);
1543
1779
  context += `
1544
1780
  ## Spec Summary
@@ -1546,8 +1782,8 @@ ${truncated}${spec.length > MAX_TASK_CONTEXT_SPEC ? "\n..." : ""}
1546
1782
  `;
1547
1783
  }
1548
1784
  const planPath = path11.join(taskDir, "plan.md");
1549
- if (fs12.existsSync(planPath)) {
1550
- const plan = fs12.readFileSync(planPath, "utf-8");
1785
+ if (fs13.existsSync(planPath)) {
1786
+ const plan = fs13.readFileSync(planPath, "utf-8");
1551
1787
  const truncated = plan.slice(0, MAX_TASK_CONTEXT_PLAN);
1552
1788
  context += `
1553
1789
  ## Plan Summary
@@ -1555,8 +1791,8 @@ ${truncated}${plan.length > MAX_TASK_CONTEXT_PLAN ? "\n..." : ""}
1555
1791
  `;
1556
1792
  }
1557
1793
  const contextMdPath = path11.join(taskDir, "context.md");
1558
- if (fs12.existsSync(contextMdPath)) {
1559
- const accumulated = fs12.readFileSync(contextMdPath, "utf-8");
1794
+ if (fs13.existsSync(contextMdPath)) {
1795
+ const accumulated = fs13.readFileSync(contextMdPath, "utf-8");
1560
1796
  const truncated = accumulated.slice(-MAX_ACCUMULATED_CONTEXT);
1561
1797
  const prefix = accumulated.length > MAX_ACCUMULATED_CONTEXT ? "...(earlier context truncated)\n" : "";
1562
1798
  context += `
@@ -1572,12 +1808,22 @@ ${feedback}
1572
1808
  }
1573
1809
  return prompt.replace("{{TASK_CONTEXT}}", context);
1574
1810
  }
1811
+ function inferHasUIFromScope(scope) {
1812
+ return scope.some((filePath) => {
1813
+ const ext = path11.extname(filePath).toLowerCase();
1814
+ if (UI_EXTENSIONS.has(ext)) return true;
1815
+ const normalized = filePath.replace(/\\/g, "/");
1816
+ return UI_PATH_SEGMENTS.some((seg) => normalized.includes(seg));
1817
+ });
1818
+ }
1575
1819
  function taskHasUI(taskDir) {
1576
1820
  const taskJsonPath = path11.join(taskDir, "task.json");
1577
- if (!fs12.existsSync(taskJsonPath)) return true;
1821
+ if (!fs13.existsSync(taskJsonPath)) return true;
1578
1822
  try {
1579
- const taskDef = JSON.parse(fs12.readFileSync(taskJsonPath, "utf-8"));
1580
- return taskDef.hasUI !== false;
1823
+ const taskDef = JSON.parse(fs13.readFileSync(taskJsonPath, "utf-8"));
1824
+ const scope = Array.isArray(taskDef.scope) ? taskDef.scope : [];
1825
+ if (scope.length === 0) return true;
1826
+ return inferHasUIFromScope(scope);
1581
1827
  } catch {
1582
1828
  return true;
1583
1829
  }
@@ -1697,8 +1943,8 @@ ${prompt}` : prompt;
1697
1943
  if (isMcpEnabledForStage(stageName, config.mcp) && taskHasUI(taskDir)) {
1698
1944
  assembled = assembled + "\n\n" + getBrowserToolGuidance(stageName, taskDir);
1699
1945
  const qaGuidePath = path11.join(projectDir, ".kody", "qa-guide.md");
1700
- if (fs12.existsSync(qaGuidePath)) {
1701
- const qaGuide = fs12.readFileSync(qaGuidePath, "utf-8").trim();
1946
+ if (fs13.existsSync(qaGuidePath)) {
1947
+ const qaGuide = fs13.readFileSync(qaGuidePath, "utf-8").trim();
1702
1948
  assembled = assembled + "\n\n" + qaGuide;
1703
1949
  }
1704
1950
  }
@@ -1722,13 +1968,16 @@ ${prompt}` : prompt;
1722
1968
  }
1723
1969
  return assembled;
1724
1970
  }
1971
+ function escalateModelTier(currentTier) {
1972
+ return TIER_ESCALATION[currentTier] ?? "strong";
1973
+ }
1725
1974
  function resolveModel(modelTier, stageName) {
1726
1975
  const config = getProjectConfig();
1727
1976
  const mapped = config.agent.modelMap[modelTier];
1728
1977
  if (mapped) return mapped;
1729
1978
  return DEFAULT_MODEL_MAP[modelTier] ?? "sonnet";
1730
1979
  }
1731
- var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT;
1980
+ var DEFAULT_MODEL_MAP, MAX_TASK_CONTEXT_PLAN, MAX_TASK_CONTEXT_SPEC, MAX_ACCUMULATED_CONTEXT, UI_EXTENSIONS, UI_PATH_SEGMENTS, TIER_ESCALATION;
1732
1981
  var init_context = __esm({
1733
1982
  "src/context.ts"() {
1734
1983
  "use strict";
@@ -1744,6 +1993,29 @@ var init_context = __esm({
1744
1993
  MAX_TASK_CONTEXT_PLAN = 1500;
1745
1994
  MAX_TASK_CONTEXT_SPEC = 2e3;
1746
1995
  MAX_ACCUMULATED_CONTEXT = 4e3;
1996
+ UI_EXTENSIONS = /* @__PURE__ */ new Set([
1997
+ ".tsx",
1998
+ ".jsx",
1999
+ ".vue",
2000
+ ".svelte",
2001
+ ".css",
2002
+ ".scss",
2003
+ ".sass",
2004
+ ".less",
2005
+ ".html"
2006
+ ]);
2007
+ UI_PATH_SEGMENTS = [
2008
+ "/components/",
2009
+ "/pages/",
2010
+ "/layouts/",
2011
+ "/styles/",
2012
+ "/views/"
2013
+ ];
2014
+ TIER_ESCALATION = {
2015
+ cheap: "mid",
2016
+ mid: "strong",
2017
+ strong: "strong"
2018
+ };
1747
2019
  }
1748
2020
  });
1749
2021
 
@@ -1767,7 +2039,7 @@ var init_runner_selection = __esm({
1767
2039
  });
1768
2040
 
1769
2041
  // src/stages/agent.ts
1770
- import * as fs13 from "fs";
2042
+ import * as fs14 from "fs";
1771
2043
  import * as path12 from "path";
1772
2044
  function getSessionInfo(stageName, sessions) {
1773
2045
  const group = SESSION_GROUP[stageName];
@@ -1798,15 +2070,19 @@ async function executeAgentStage(ctx, def) {
1798
2070
  return { outcome: "completed", retries: 0 };
1799
2071
  }
1800
2072
  const prompt = buildFullPrompt(def.name, ctx.taskId, ctx.taskDir, ctx.projectDir, ctx.input.feedback);
1801
- const model = resolveModel(def.modelTier, def.name);
2073
+ let currentModelTier = def.modelTier;
1802
2074
  if (ctx.input.feedback && def.name === "build") {
1803
2075
  logger.info(` feedback: ${ctx.input.feedback.slice(0, 200)}${ctx.input.feedback.length > 200 ? "..." : ""}`);
1804
2076
  }
1805
2077
  const config = getProjectConfig();
2078
+ const sc = resolveStageConfig(config, def.name, def.modelTier);
2079
+ let model = sc.model;
2080
+ const useProxy = stageNeedsProxy(sc);
2081
+ const escalateEnabled = config.agent.escalateOnTimeout !== false;
1806
2082
  const runnerName = config.agent.stageRunners?.[def.name] ?? config.agent.defaultRunner ?? Object.keys(ctx.runners)[0] ?? "claude";
1807
- logger.info(` runner=${runnerName} model=${model} timeout=${def.timeout / 1e3}s`);
2083
+ logger.info(` runner=${runnerName} provider=${sc.provider} model=${model} timeout=${def.timeout / 1e3}s`);
1808
2084
  const extraEnv = {};
1809
- if (needsLitellmProxy(config)) {
2085
+ if (useProxy) {
1810
2086
  extraEnv.ANTHROPIC_BASE_URL = getLitellmUrl();
1811
2087
  }
1812
2088
  const sessions = ctx.sessions ?? {};
@@ -1820,37 +2096,58 @@ async function executeAgentStage(ctx, def) {
1820
2096
  logger.info(` MCP servers enabled for ${def.name}`);
1821
2097
  }
1822
2098
  const runner = getRunnerForStage(ctx, def.name);
1823
- const result = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
2099
+ const maxRetries = def.maxRetries ?? 0;
2100
+ let lastResult = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
1824
2101
  cwd: ctx.projectDir,
1825
2102
  env: extraEnv,
1826
2103
  ...sessionInfo,
1827
2104
  mcpConfigJson
1828
2105
  });
1829
- if (result.outcome !== "completed") {
1830
- return { outcome: result.outcome, error: result.error, retries: 0 };
2106
+ let retries = 0;
2107
+ while (lastResult.outcome !== "completed" && retries < maxRetries) {
2108
+ retries++;
2109
+ const isTimeout = lastResult.outcome === "timed_out";
2110
+ if (isTimeout && escalateEnabled) {
2111
+ const nextTier = escalateModelTier(currentModelTier);
2112
+ if (nextTier !== currentModelTier) {
2113
+ logger.info(` Escalating model from ${currentModelTier} to ${nextTier} after timeout`);
2114
+ currentModelTier = nextTier;
2115
+ model = resolveModel(currentModelTier, def.name);
2116
+ }
2117
+ }
2118
+ logger.info(` retry ${retries}/${maxRetries} with model=${model}`);
2119
+ lastResult = await runner.run(def.name, prompt, model, def.timeout, ctx.taskDir, {
2120
+ cwd: ctx.projectDir,
2121
+ env: extraEnv,
2122
+ mcpConfigJson
2123
+ });
2124
+ }
2125
+ if (lastResult.outcome !== "completed") {
2126
+ return { outcome: lastResult.outcome, error: lastResult.error, retries };
1831
2127
  }
2128
+ const result = lastResult;
1832
2129
  if (def.outputFile && result.output) {
1833
- fs13.writeFileSync(path12.join(ctx.taskDir, def.outputFile), result.output);
2130
+ fs14.writeFileSync(path12.join(ctx.taskDir, def.outputFile), result.output);
1834
2131
  }
1835
2132
  if (def.outputFile) {
1836
2133
  const outputPath = path12.join(ctx.taskDir, def.outputFile);
1837
- if (!fs13.existsSync(outputPath)) {
2134
+ if (!fs14.existsSync(outputPath)) {
1838
2135
  const ext = path12.extname(def.outputFile);
1839
2136
  const base = path12.basename(def.outputFile, ext);
1840
- const files = fs13.readdirSync(ctx.taskDir);
2137
+ const files = fs14.readdirSync(ctx.taskDir);
1841
2138
  const variant = files.find(
1842
2139
  (f) => f.startsWith(base + "-") && f.endsWith(ext)
1843
2140
  );
1844
2141
  if (variant) {
1845
- fs13.renameSync(path12.join(ctx.taskDir, variant), outputPath);
2142
+ fs14.renameSync(path12.join(ctx.taskDir, variant), outputPath);
1846
2143
  logger.info(` Renamed variant ${variant} \u2192 ${def.outputFile}`);
1847
2144
  }
1848
2145
  }
1849
2146
  }
1850
2147
  if (def.outputFile) {
1851
2148
  const outputPath = path12.join(ctx.taskDir, def.outputFile);
1852
- if (fs13.existsSync(outputPath)) {
1853
- const content = fs13.readFileSync(outputPath, "utf-8");
2149
+ if (fs14.existsSync(outputPath)) {
2150
+ const content = fs14.readFileSync(outputPath, "utf-8");
1854
2151
  const validation = validateStageOutput(def.name, content);
1855
2152
  if (!validation.valid) {
1856
2153
  if (def.name === "taskify") {
@@ -1864,7 +2161,7 @@ async function executeAgentStage(ctx, def) {
1864
2161
  const stripped = stripFences(retryResult.output);
1865
2162
  const retryValidation = validateTaskJson(stripped);
1866
2163
  if (retryValidation.valid) {
1867
- fs13.writeFileSync(outputPath, retryResult.output);
2164
+ fs14.writeFileSync(outputPath, retryResult.output);
1868
2165
  logger.info(` taskify retry produced valid JSON`);
1869
2166
  } else {
1870
2167
  logger.warn(` taskify retry still invalid: ${retryValidation.error}`);
@@ -1875,10 +2172,9 @@ async function executeAgentStage(ctx, def) {
1875
2172
  description: plainText.slice(0, 500),
1876
2173
  scope: [],
1877
2174
  risk_level: "low",
1878
- hasUI: true,
1879
2175
  questions: []
1880
2176
  }, null, 2);
1881
- fs13.writeFileSync(outputPath, fallback);
2177
+ fs14.writeFileSync(outputPath, fallback);
1882
2178
  logger.info(` taskify fallback: generated minimal task.json (risk_level=low)`);
1883
2179
  }
1884
2180
  }
@@ -1889,7 +2185,7 @@ async function executeAgentStage(ctx, def) {
1889
2185
  }
1890
2186
  }
1891
2187
  appendStageContext(ctx.taskDir, def.name, result.output);
1892
- return { outcome: "completed", outputFile: def.outputFile, retries: 0 };
2188
+ return { outcome: "completed", outputFile: def.outputFile, retries };
1893
2189
  }
1894
2190
  function appendStageContext(taskDir, stageName, output) {
1895
2191
  const contextPath = path12.join(taskDir, "context.md");
@@ -1905,7 +2201,7 @@ function appendStageContext(taskDir, stageName, output) {
1905
2201
  ### ${stageName} (${timestamp2})
1906
2202
  ${summary}
1907
2203
  `;
1908
- fs13.appendFileSync(contextPath, entry);
2204
+ fs14.appendFileSync(contextPath, entry);
1909
2205
  }
1910
2206
  var SESSION_GROUP;
1911
2207
  var init_agent = __esm({
@@ -2144,7 +2440,7 @@ Error context:
2144
2440
  });
2145
2441
 
2146
2442
  // src/stages/gate.ts
2147
- import * as fs14 from "fs";
2443
+ import * as fs15 from "fs";
2148
2444
  import * as path13 from "path";
2149
2445
  function executeGateStage(ctx, def) {
2150
2446
  if (ctx.input.dryRun) {
@@ -2188,7 +2484,7 @@ ${output}
2188
2484
  `);
2189
2485
  }
2190
2486
  }
2191
- fs14.writeFileSync(path13.join(ctx.taskDir, "verify.md"), lines.join(""));
2487
+ fs15.writeFileSync(path13.join(ctx.taskDir, "verify.md"), lines.join(""));
2192
2488
  return {
2193
2489
  outcome: verifyResult.pass ? "completed" : "failed",
2194
2490
  retries: 0
@@ -2203,7 +2499,7 @@ var init_gate = __esm({
2203
2499
  });
2204
2500
 
2205
2501
  // src/stages/verify.ts
2206
- import * as fs15 from "fs";
2502
+ import * as fs16 from "fs";
2207
2503
  import * as path14 from "path";
2208
2504
  import { execFileSync as execFileSync11 } from "child_process";
2209
2505
  async function executeVerifyWithAutofix(ctx, def) {
@@ -2216,7 +2512,7 @@ async function executeVerifyWithAutofix(ctx, def) {
2216
2512
  }
2217
2513
  if (attempt < maxAttempts) {
2218
2514
  const verifyPath = path14.join(ctx.taskDir, "verify.md");
2219
- const errorOutput = fs15.existsSync(verifyPath) ? fs15.readFileSync(verifyPath, "utf-8") : "Unknown error";
2515
+ const errorOutput = fs16.existsSync(verifyPath) ? fs16.readFileSync(verifyPath, "utf-8") : "Unknown error";
2220
2516
  const modifiedFiles = getModifiedFiles(ctx.projectDir);
2221
2517
  const defaultRunner = getRunnerForStage(ctx, "taskify");
2222
2518
  const diagConfig = getProjectConfig();
@@ -2312,13 +2608,13 @@ var init_verify = __esm({
2312
2608
  });
2313
2609
 
2314
2610
  // src/cli/task-resolution.ts
2315
- import * as fs16 from "fs";
2611
+ import * as fs17 from "fs";
2316
2612
  import * as path15 from "path";
2317
2613
  import { execFileSync as execFileSync12 } from "child_process";
2318
2614
  function findLatestTaskForIssue(issueNumber, projectDir) {
2319
2615
  const tasksDir = path15.join(projectDir, ".kody", "tasks");
2320
- if (!fs16.existsSync(tasksDir)) return null;
2321
- const allDirs = fs16.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2616
+ if (!fs17.existsSync(tasksDir)) return null;
2617
+ const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2322
2618
  const prefix = `${issueNumber}-`;
2323
2619
  const direct = allDirs.find((d) => d.startsWith(prefix));
2324
2620
  if (direct) return direct;
@@ -2345,14 +2641,38 @@ function generateTaskId() {
2345
2641
  const pad = (n) => String(n).padStart(2, "0");
2346
2642
  return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
2347
2643
  }
2644
+ function resolveTaskIdFromComments(issueNumber) {
2645
+ try {
2646
+ const comments = getIssueComments(issueNumber);
2647
+ const pattern = /pipeline started: `([^`]+)`/;
2648
+ let latestTaskId = null;
2649
+ for (const comment of comments) {
2650
+ const match = comment.body.match(pattern);
2651
+ if (match) {
2652
+ latestTaskId = match[1];
2653
+ }
2654
+ }
2655
+ return latestTaskId;
2656
+ } catch {
2657
+ return null;
2658
+ }
2659
+ }
2660
+ function resolveTaskIdForCommand(issueNumber, projectDir) {
2661
+ const fromTasks = findLatestTaskForIssue(issueNumber, projectDir);
2662
+ if (fromTasks) return fromTasks;
2663
+ const fromComments = resolveTaskIdFromComments(issueNumber);
2664
+ if (fromComments) return fromComments;
2665
+ return null;
2666
+ }
2348
2667
  var init_task_resolution = __esm({
2349
2668
  "src/cli/task-resolution.ts"() {
2350
2669
  "use strict";
2670
+ init_github_api();
2351
2671
  }
2352
2672
  });
2353
2673
 
2354
2674
  // src/review-standalone.ts
2355
- import * as fs17 from "fs";
2675
+ import * as fs18 from "fs";
2356
2676
  import * as path16 from "path";
2357
2677
  function resolveReviewTarget(input) {
2358
2678
  if (input.prs.length === 0) {
@@ -2378,16 +2698,34 @@ Or comment on the specific PR: \`@kody review\``
2378
2698
  async function runStandaloneReview(input) {
2379
2699
  const taskId = input.taskId ?? `review-${generateTaskId()}`;
2380
2700
  const taskDir = path16.join(input.projectDir, ".kody", "tasks", taskId);
2381
- fs17.mkdirSync(taskDir, { recursive: true });
2382
- const diffInstruction = input.baseBranch ? `
2701
+ fs18.mkdirSync(taskDir, { recursive: true });
2702
+ let diffInstruction = "";
2703
+ let filesChangedSection = "";
2704
+ if (input.baseBranch) {
2705
+ diffInstruction = `
2383
2706
 
2384
2707
  ## Diff Command
2385
2708
  Run: \`git diff origin/${input.baseBranch}...HEAD\` to see the PR changes.
2386
- Do NOT use bare \`git diff\` \u2014 it shows only uncommitted working tree changes, not the PR diff.` : "";
2709
+ Do NOT use bare \`git diff\` \u2014 it shows only uncommitted working tree changes, not the PR diff.`;
2710
+ const diffFiles = getDiffFiles(input.baseBranch, input.projectDir);
2711
+ if (diffFiles.length > 0) {
2712
+ logger.info(`[review] Review scope: git diff origin/${input.baseBranch}...HEAD (${diffFiles.length} files)`);
2713
+ const fileList = diffFiles.map((f) => `- ${f}`).join("\n");
2714
+ filesChangedSection = `
2715
+
2716
+ ## Files Changed
2717
+ Only review the following ${diffFiles.length} files (these are the files changed in this PR):
2718
+ ${fileList}`;
2719
+ } else {
2720
+ logger.info(`[review] Review scope: git diff origin/${input.baseBranch}...HEAD (0 files)`);
2721
+ }
2722
+ } else {
2723
+ logger.warn(`[review] No baseBranch provided \u2014 reviewing all files (no diff scope)`);
2724
+ }
2387
2725
  const taskContent = `# ${input.prTitle}
2388
2726
 
2389
- ${input.prBody ?? ""}${diffInstruction}`;
2390
- fs17.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
2727
+ ${input.prBody ?? ""}${diffInstruction}${filesChangedSection}`;
2728
+ fs18.writeFileSync(path16.join(taskDir, "task.md"), taskContent);
2391
2729
  const reviewDef = STAGES.find((s) => s.name === "review");
2392
2730
  const ctx = {
2393
2731
  taskId,
@@ -2411,8 +2749,8 @@ ${input.prBody ?? ""}${diffInstruction}`;
2411
2749
  }
2412
2750
  const reviewPath = path16.join(taskDir, "review.md");
2413
2751
  let reviewContent;
2414
- if (fs17.existsSync(reviewPath)) {
2415
- reviewContent = fs17.readFileSync(reviewPath, "utf-8");
2752
+ if (fs18.existsSync(reviewPath)) {
2753
+ reviewContent = fs18.readFileSync(reviewPath, "utf-8");
2416
2754
  }
2417
2755
  return {
2418
2756
  outcome: "completed",
@@ -2447,11 +2785,12 @@ var init_review_standalone = __esm({
2447
2785
  init_agent();
2448
2786
  init_task_resolution();
2449
2787
  init_logger();
2788
+ init_git_utils();
2450
2789
  }
2451
2790
  });
2452
2791
 
2453
2792
  // src/stages/review.ts
2454
- import * as fs18 from "fs";
2793
+ import * as fs19 from "fs";
2455
2794
  import * as path17 from "path";
2456
2795
  async function executeReviewWithFix(ctx, def) {
2457
2796
  if (ctx.input.dryRun) {
@@ -2467,10 +2806,10 @@ async function executeReviewWithFix(ctx, def) {
2467
2806
  return reviewResult;
2468
2807
  }
2469
2808
  const reviewFile = path17.join(ctx.taskDir, "review.md");
2470
- if (!fs18.existsSync(reviewFile)) {
2809
+ if (!fs19.existsSync(reviewFile)) {
2471
2810
  return { outcome: "failed", retries: iteration, error: "review.md not found" };
2472
2811
  }
2473
- const content = fs18.readFileSync(reviewFile, "utf-8");
2812
+ const content = fs19.readFileSync(reviewFile, "utf-8");
2474
2813
  if (detectReviewVerdict(content) !== "fail") {
2475
2814
  return { ...reviewResult, retries: iteration };
2476
2815
  }
@@ -2499,15 +2838,15 @@ var init_review = __esm({
2499
2838
  });
2500
2839
 
2501
2840
  // src/stages/ship.ts
2502
- import * as fs19 from "fs";
2841
+ import * as fs20 from "fs";
2503
2842
  import * as path18 from "path";
2504
2843
  import { execFileSync as execFileSync13 } from "child_process";
2505
2844
  function buildPrBody(ctx) {
2506
2845
  const sections = [];
2507
2846
  const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2508
- if (fs19.existsSync(taskJsonPath)) {
2847
+ if (fs20.existsSync(taskJsonPath)) {
2509
2848
  try {
2510
- const raw = fs19.readFileSync(taskJsonPath, "utf-8");
2849
+ const raw = fs20.readFileSync(taskJsonPath, "utf-8");
2511
2850
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2512
2851
  const task = JSON.parse(cleaned);
2513
2852
  if (task.description) {
@@ -2527,8 +2866,8 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
2527
2866
  }
2528
2867
  }
2529
2868
  const reviewPath = path18.join(ctx.taskDir, "review.md");
2530
- if (fs19.existsSync(reviewPath)) {
2531
- const review = fs19.readFileSync(reviewPath, "utf-8");
2869
+ if (fs20.existsSync(reviewPath)) {
2870
+ const review = fs20.readFileSync(reviewPath, "utf-8");
2532
2871
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
2533
2872
  if (summaryMatch) {
2534
2873
  const summary = summaryMatch[1].trim();
@@ -2546,13 +2885,13 @@ ${summary}`);
2546
2885
  }
2547
2886
  }
2548
2887
  const verifyPath = path18.join(ctx.taskDir, "verify.md");
2549
- if (fs19.existsSync(verifyPath)) {
2550
- const verify = fs19.readFileSync(verifyPath, "utf-8");
2888
+ if (fs20.existsSync(verifyPath)) {
2889
+ const verify = fs20.readFileSync(verifyPath, "utf-8");
2551
2890
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
2552
2891
  }
2553
2892
  const planPath = path18.join(ctx.taskDir, "plan.md");
2554
- if (fs19.existsSync(planPath)) {
2555
- const plan = fs19.readFileSync(planPath, "utf-8").trim();
2893
+ if (fs20.existsSync(planPath)) {
2894
+ const plan = fs20.readFileSync(planPath, "utf-8").trim();
2556
2895
  if (plan) {
2557
2896
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
2558
2897
  sections.push(`
@@ -2574,11 +2913,11 @@ Closes #${ctx.input.issueNumber}`);
2574
2913
  function executeShipStage(ctx, _def) {
2575
2914
  const shipPath = path18.join(ctx.taskDir, "ship.md");
2576
2915
  if (ctx.input.dryRun) {
2577
- fs19.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
2916
+ fs20.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
2578
2917
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2579
2918
  }
2580
2919
  if (ctx.input.local && !ctx.input.issueNumber) {
2581
- fs19.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
2920
+ fs20.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
2582
2921
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2583
2922
  }
2584
2923
  try {
@@ -2626,9 +2965,9 @@ function executeShipStage(ctx, _def) {
2626
2965
  };
2627
2966
  let prefix = "chore";
2628
2967
  const taskJsonPath = path18.join(ctx.taskDir, "task.json");
2629
- if (fs19.existsSync(taskJsonPath)) {
2968
+ if (fs20.existsSync(taskJsonPath)) {
2630
2969
  try {
2631
- const raw = fs19.readFileSync(taskJsonPath, "utf-8");
2970
+ const raw = fs20.readFileSync(taskJsonPath, "utf-8");
2632
2971
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2633
2972
  const task = JSON.parse(cleaned);
2634
2973
  prefix = TYPE_PREFIX[task.task_type] ?? "chore";
@@ -2636,17 +2975,17 @@ function executeShipStage(ctx, _def) {
2636
2975
  }
2637
2976
  }
2638
2977
  const taskMdPath = path18.join(ctx.taskDir, "task.md");
2639
- if (fs19.existsSync(taskMdPath)) {
2640
- const content = fs19.readFileSync(taskMdPath, "utf-8");
2978
+ if (fs20.existsSync(taskMdPath)) {
2979
+ const content = fs20.readFileSync(taskMdPath, "utf-8");
2641
2980
  const heading = content.split("\n").find((l) => l.startsWith("# "));
2642
2981
  if (heading) {
2643
2982
  title = `${prefix}: ${heading.replace(/^#\s*/, "").trim()}`.slice(0, 72);
2644
2983
  }
2645
2984
  }
2646
2985
  if (title === "Update") {
2647
- if (fs19.existsSync(taskJsonPath)) {
2986
+ if (fs20.existsSync(taskJsonPath)) {
2648
2987
  try {
2649
- const raw = fs19.readFileSync(taskJsonPath, "utf-8");
2988
+ const raw = fs20.readFileSync(taskJsonPath, "utf-8");
2650
2989
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2651
2990
  const task = JSON.parse(cleaned);
2652
2991
  if (task.title) title = `${prefix}: ${task.title}`.slice(0, 72);
@@ -2669,7 +3008,7 @@ function executeShipStage(ctx, _def) {
2669
3008
  } catch {
2670
3009
  }
2671
3010
  }
2672
- fs19.writeFileSync(shipPath, `# Ship
3011
+ fs20.writeFileSync(shipPath, `# Ship
2673
3012
 
2674
3013
  Updated existing PR: ${existingPr.url}
2675
3014
  PR #${existingPr.number}
@@ -2690,20 +3029,20 @@ PR #${existingPr.number}
2690
3029
  } catch {
2691
3030
  }
2692
3031
  }
2693
- fs19.writeFileSync(shipPath, `# Ship
3032
+ fs20.writeFileSync(shipPath, `# Ship
2694
3033
 
2695
3034
  PR created: ${pr.url}
2696
3035
  PR #${pr.number}
2697
3036
  `);
2698
3037
  } else {
2699
- fs19.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
3038
+ fs20.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
2700
3039
  }
2701
3040
  }
2702
3041
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2703
3042
  } catch (err) {
2704
3043
  const msg = err instanceof Error ? err.message : String(err);
2705
3044
  try {
2706
- fs19.writeFileSync(shipPath, `# Ship
3045
+ fs20.writeFileSync(shipPath, `# Ship
2707
3046
 
2708
3047
  Failed: ${msg}
2709
3048
  `);
@@ -2752,15 +3091,15 @@ var init_executor_registry = __esm({
2752
3091
  });
2753
3092
 
2754
3093
  // src/pipeline/questions.ts
2755
- import * as fs20 from "fs";
3094
+ import * as fs21 from "fs";
2756
3095
  import * as path19 from "path";
2757
3096
  function checkForQuestions(ctx, stageName) {
2758
3097
  if (ctx.input.local || !ctx.input.issueNumber) return false;
2759
3098
  try {
2760
3099
  if (stageName === "taskify") {
2761
3100
  const taskJsonPath = path19.join(ctx.taskDir, "task.json");
2762
- if (!fs20.existsSync(taskJsonPath)) return false;
2763
- const raw = fs20.readFileSync(taskJsonPath, "utf-8");
3101
+ if (!fs21.existsSync(taskJsonPath)) return false;
3102
+ const raw = fs21.readFileSync(taskJsonPath, "utf-8");
2764
3103
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2765
3104
  const taskJson = JSON.parse(cleaned);
2766
3105
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -2776,8 +3115,8 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
2776
3115
  }
2777
3116
  if (stageName === "plan") {
2778
3117
  const planPath = path19.join(ctx.taskDir, "plan.md");
2779
- if (!fs20.existsSync(planPath)) return false;
2780
- const plan = fs20.readFileSync(planPath, "utf-8");
3118
+ if (!fs21.existsSync(planPath)) return false;
3119
+ const plan = fs21.readFileSync(planPath, "utf-8");
2781
3120
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
2782
3121
  if (questionsMatch) {
2783
3122
  const questionsText = questionsMatch[1].trim();
@@ -2806,7 +3145,7 @@ var init_questions = __esm({
2806
3145
  });
2807
3146
 
2808
3147
  // src/pipeline/hooks.ts
2809
- import * as fs21 from "fs";
3148
+ import * as fs22 from "fs";
2810
3149
  import * as path20 from "path";
2811
3150
  function applyPreStageLabel(ctx, def) {
2812
3151
  if (!ctx.input.issueNumber || ctx.input.local) return;
@@ -2846,8 +3185,8 @@ function autoDetectComplexity(ctx, def) {
2846
3185
  }
2847
3186
  try {
2848
3187
  const taskJsonPath = path20.join(ctx.taskDir, "task.json");
2849
- if (!fs21.existsSync(taskJsonPath)) return null;
2850
- const raw = fs21.readFileSync(taskJsonPath, "utf-8");
3188
+ if (!fs22.existsSync(taskJsonPath)) return null;
3189
+ const raw = fs22.readFileSync(taskJsonPath, "utf-8");
2851
3190
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2852
3191
  const taskJson = JSON.parse(cleaned);
2853
3192
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -2878,7 +3217,7 @@ function checkRiskGate(ctx, def, state, complexity) {
2878
3217
  if (ctx.input.mode === "rerun") return null;
2879
3218
  if (!ctx.input.issueNumber) return null;
2880
3219
  const planPath = path20.join(ctx.taskDir, "plan.md");
2881
- const plan = fs21.existsSync(planPath) ? fs21.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
3220
+ const plan = fs22.existsSync(planPath) ? fs22.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
2882
3221
  try {
2883
3222
  postComment(
2884
3223
  ctx.input.issueNumber,
@@ -2945,7 +3284,7 @@ var init_hooks = __esm({
2945
3284
  });
2946
3285
 
2947
3286
  // src/learning/auto-learn.ts
2948
- import * as fs22 from "fs";
3287
+ import * as fs23 from "fs";
2949
3288
  import * as path21 from "path";
2950
3289
  function stripAnsi(str) {
2951
3290
  return str.replace(/\x1b\[[0-9;]*m/g, "");
@@ -2953,14 +3292,14 @@ function stripAnsi(str) {
2953
3292
  function autoLearn(ctx) {
2954
3293
  try {
2955
3294
  const memoryDir = path21.join(ctx.projectDir, ".kody", "memory");
2956
- if (!fs22.existsSync(memoryDir)) {
2957
- fs22.mkdirSync(memoryDir, { recursive: true });
3295
+ if (!fs23.existsSync(memoryDir)) {
3296
+ fs23.mkdirSync(memoryDir, { recursive: true });
2958
3297
  }
2959
3298
  const learnings = [];
2960
3299
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2961
3300
  const verifyPath = path21.join(ctx.taskDir, "verify.md");
2962
- if (fs22.existsSync(verifyPath)) {
2963
- const verify = stripAnsi(fs22.readFileSync(verifyPath, "utf-8"));
3301
+ if (fs23.existsSync(verifyPath)) {
3302
+ const verify = stripAnsi(fs23.readFileSync(verifyPath, "utf-8"));
2964
3303
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
2965
3304
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
2966
3305
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -2970,17 +3309,17 @@ function autoLearn(ctx) {
2970
3309
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
2971
3310
  }
2972
3311
  const reviewPath = path21.join(ctx.taskDir, "review.md");
2973
- if (fs22.existsSync(reviewPath)) {
2974
- const review = fs22.readFileSync(reviewPath, "utf-8");
3312
+ if (fs23.existsSync(reviewPath)) {
3313
+ const review = fs23.readFileSync(reviewPath, "utf-8");
2975
3314
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
2976
3315
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
2977
3316
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
2978
3317
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
2979
3318
  }
2980
3319
  const taskJsonPath = path21.join(ctx.taskDir, "task.json");
2981
- if (fs22.existsSync(taskJsonPath)) {
3320
+ if (fs23.existsSync(taskJsonPath)) {
2982
3321
  try {
2983
- const raw = stripAnsi(fs22.readFileSync(taskJsonPath, "utf-8"));
3322
+ const raw = stripAnsi(fs23.readFileSync(taskJsonPath, "utf-8"));
2984
3323
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2985
3324
  const task = JSON.parse(cleaned);
2986
3325
  if (task.scope && Array.isArray(task.scope)) {
@@ -2996,7 +3335,7 @@ function autoLearn(ctx) {
2996
3335
  ## Learned ${timestamp2} (task: ${ctx.taskId})
2997
3336
  ${learnings.join("\n")}
2998
3337
  `;
2999
- fs22.appendFileSync(conventionsPath, entry);
3338
+ fs23.appendFileSync(conventionsPath, entry);
3000
3339
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
3001
3340
  }
3002
3341
  autoLearnArchitecture(ctx.projectDir, memoryDir, timestamp2);
@@ -3005,7 +3344,7 @@ ${learnings.join("\n")}
3005
3344
  }
3006
3345
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3007
3346
  const archPath = path21.join(memoryDir, "architecture.md");
3008
- if (fs22.existsSync(archPath)) return;
3347
+ if (fs23.existsSync(archPath)) return;
3009
3348
  const detected = detectArchitectureBasic(projectDir);
3010
3349
  if (detected.length > 0) {
3011
3350
  const content = `# Architecture (auto-detected ${timestamp2})
@@ -3013,7 +3352,7 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
3013
3352
  ## Overview
3014
3353
  ${detected.join("\n")}
3015
3354
  `;
3016
- fs22.writeFileSync(archPath, content);
3355
+ fs23.writeFileSync(archPath, content);
3017
3356
  logger.info(`Auto-detected architecture (${detected.length} items)`);
3018
3357
  }
3019
3358
  }
@@ -3026,13 +3365,13 @@ var init_auto_learn = __esm({
3026
3365
  });
3027
3366
 
3028
3367
  // src/retrospective.ts
3029
- import * as fs23 from "fs";
3368
+ import * as fs24 from "fs";
3030
3369
  import * as path22 from "path";
3031
3370
  function readArtifact(taskDir, filename, maxChars) {
3032
3371
  const p = path22.join(taskDir, filename);
3033
- if (!fs23.existsSync(p)) return null;
3372
+ if (!fs24.existsSync(p)) return null;
3034
3373
  try {
3035
- const content = fs23.readFileSync(p, "utf-8");
3374
+ const content = fs24.readFileSync(p, "utf-8");
3036
3375
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
3037
3376
  } catch {
3038
3377
  return null;
@@ -3089,9 +3428,9 @@ function getLogPath(projectDir) {
3089
3428
  }
3090
3429
  function readPreviousRetrospectives(projectDir, limit = 10) {
3091
3430
  const logPath = getLogPath(projectDir);
3092
- if (!fs23.existsSync(logPath)) return [];
3431
+ if (!fs24.existsSync(logPath)) return [];
3093
3432
  try {
3094
- const content = fs23.readFileSync(logPath, "utf-8");
3433
+ const content = fs24.readFileSync(logPath, "utf-8");
3095
3434
  const lines = content.split("\n").filter(Boolean);
3096
3435
  const entries = [];
3097
3436
  const start = Math.max(0, lines.length - limit);
@@ -3119,10 +3458,10 @@ function formatPreviousEntries(entries) {
3119
3458
  function appendRetrospectiveEntry(projectDir, entry) {
3120
3459
  const logPath = getLogPath(projectDir);
3121
3460
  const dir = path22.dirname(logPath);
3122
- if (!fs23.existsSync(dir)) {
3123
- fs23.mkdirSync(dir, { recursive: true });
3461
+ if (!fs24.existsSync(dir)) {
3462
+ fs24.mkdirSync(dir, { recursive: true });
3124
3463
  }
3125
- fs23.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3464
+ fs24.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3126
3465
  }
3127
3466
  async function runRetrospective(ctx, state, pipelineStartTime) {
3128
3467
  if (ctx.input.dryRun) return;
@@ -3232,8 +3571,65 @@ If no pipeline flaw is detected, set "pipelineFlaw" to null.
3232
3571
  }
3233
3572
  });
3234
3573
 
3574
+ // src/pipeline/summary.ts
3575
+ function formatDuration(ms) {
3576
+ const totalSec = Math.round(ms / 1e3);
3577
+ if (totalSec < 60) return `${totalSec}s`;
3578
+ const min = Math.floor(totalSec / 60);
3579
+ const sec = totalSec % 60;
3580
+ return `${min}m ${sec}s`;
3581
+ }
3582
+ function stageDuration(stage) {
3583
+ if (!stage.startedAt) return "-";
3584
+ const start = new Date(stage.startedAt).getTime();
3585
+ const end = stage.completedAt ? new Date(stage.completedAt).getTime() : Date.now();
3586
+ if (isNaN(start) || isNaN(end)) return "-";
3587
+ return formatDuration(end - start);
3588
+ }
3589
+ function formatPipelineSummary(state, options) {
3590
+ const lines = [];
3591
+ lines.push(`## Pipeline Summary: \`${state.taskId}\``);
3592
+ lines.push("");
3593
+ lines.push("| Stage | Status | Duration | Retries |");
3594
+ lines.push("|-------|--------|----------|---------|");
3595
+ for (const def of STAGES) {
3596
+ const s = state.stages[def.name];
3597
+ if (!s) continue;
3598
+ const status = STATUS_ICONS[s.state] ?? s.state;
3599
+ const duration = stageDuration(s);
3600
+ const retries = s.retries ?? 0;
3601
+ lines.push(`| ${def.name} | ${status} | ${duration} | ${retries} |`);
3602
+ }
3603
+ const totalMs = new Date(state.updatedAt).getTime() - new Date(state.createdAt).getTime();
3604
+ const totalStr = isNaN(totalMs) || totalMs < 0 ? "-" : formatDuration(totalMs);
3605
+ lines.push("");
3606
+ const footerParts = [`**Total:** ${totalStr}`];
3607
+ if (options?.complexity) {
3608
+ footerParts.push(`**Complexity:** ${options.complexity}`);
3609
+ }
3610
+ if (options?.model) {
3611
+ footerParts.push(`**Model:** ${options.model}`);
3612
+ }
3613
+ lines.push(footerParts.join(" | "));
3614
+ return lines.join("\n");
3615
+ }
3616
+ var STATUS_ICONS;
3617
+ var init_summary = __esm({
3618
+ "src/pipeline/summary.ts"() {
3619
+ "use strict";
3620
+ init_definitions();
3621
+ STATUS_ICONS = {
3622
+ completed: "completed",
3623
+ failed: "failed",
3624
+ timeout: "timeout",
3625
+ running: "running",
3626
+ pending: "pending"
3627
+ };
3628
+ }
3629
+ });
3630
+
3235
3631
  // src/pipeline.ts
3236
- import * as fs24 from "fs";
3632
+ import * as fs25 from "fs";
3237
3633
  import * as path23 from "path";
3238
3634
  function ensureFeatureBranchIfNeeded(ctx) {
3239
3635
  if (ctx.input.dryRun) return;
@@ -3248,7 +3644,7 @@ function ensureFeatureBranchIfNeeded(ctx) {
3248
3644
  if (!ctx.input.issueNumber) return;
3249
3645
  try {
3250
3646
  const taskMdPath = path23.join(ctx.taskDir, "task.md");
3251
- const title = fs24.existsSync(taskMdPath) ? fs24.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
3647
+ const title = fs25.existsSync(taskMdPath) ? fs25.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
3252
3648
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
3253
3649
  syncWithDefault(ctx.projectDir);
3254
3650
  } catch (err) {
@@ -3263,9 +3659,9 @@ function ensureFeatureBranchIfNeeded(ctx) {
3263
3659
  }
3264
3660
  function acquireLock(taskDir) {
3265
3661
  const lockPath = path23.join(taskDir, ".lock");
3266
- if (fs24.existsSync(lockPath)) {
3662
+ if (fs25.existsSync(lockPath)) {
3267
3663
  try {
3268
- const pid = parseInt(fs24.readFileSync(lockPath, "utf-8").trim(), 10);
3664
+ const pid = parseInt(fs25.readFileSync(lockPath, "utf-8").trim(), 10);
3269
3665
  if (!isNaN(pid)) {
3270
3666
  try {
3271
3667
  process.kill(pid, 0);
@@ -3282,14 +3678,14 @@ function acquireLock(taskDir) {
3282
3678
  logger.warn(` Corrupt lock file \u2014 overwriting`);
3283
3679
  }
3284
3680
  try {
3285
- fs24.unlinkSync(lockPath);
3681
+ fs25.unlinkSync(lockPath);
3286
3682
  } catch {
3287
3683
  }
3288
3684
  }
3289
3685
  try {
3290
- const fd = fs24.openSync(lockPath, fs24.constants.O_WRONLY | fs24.constants.O_CREAT | fs24.constants.O_EXCL);
3291
- fs24.writeSync(fd, String(process.pid));
3292
- fs24.closeSync(fd);
3686
+ const fd = fs25.openSync(lockPath, fs25.constants.O_WRONLY | fs25.constants.O_CREAT | fs25.constants.O_EXCL);
3687
+ fs25.writeSync(fd, String(process.pid));
3688
+ fs25.closeSync(fd);
3293
3689
  } catch (err) {
3294
3690
  if (err.code === "EEXIST") {
3295
3691
  throw new Error("Pipeline already running (lock acquired by another process)");
@@ -3299,7 +3695,7 @@ function acquireLock(taskDir) {
3299
3695
  }
3300
3696
  function releaseLock(taskDir) {
3301
3697
  try {
3302
- fs24.unlinkSync(path23.join(taskDir, ".lock"));
3698
+ fs25.unlinkSync(path23.join(taskDir, ".lock"));
3303
3699
  } catch {
3304
3700
  }
3305
3701
  }
@@ -3449,6 +3845,24 @@ async function runPipelineInner(ctx) {
3449
3845
  await runRetrospective(ctx, state, pipelineStartTime).catch((err) => {
3450
3846
  logger.warn(` Retrospective failed: ${err instanceof Error ? err.message : String(err)}`);
3451
3847
  });
3848
+ if (ctx.input.issueNumber && !ctx.input.dryRun) {
3849
+ const config = getProjectConfig();
3850
+ const isCI3 = !!process.env.GITHUB_ACTIONS;
3851
+ const shouldPost = config.github.postSummary ?? (isCI3 ? true : false);
3852
+ if (shouldPost) {
3853
+ try {
3854
+ const summaryOpts = {};
3855
+ if (complexity) summaryOpts.complexity = complexity;
3856
+ const modelMap = config.agent?.modelMap;
3857
+ if (modelMap?.mid) summaryOpts.model = modelMap.mid;
3858
+ const summary = formatPipelineSummary(state, summaryOpts);
3859
+ postComment(ctx.input.issueNumber, summary);
3860
+ logger.info("Pipeline summary posted on issue");
3861
+ } catch (err) {
3862
+ logger.warn(` Failed to post pipeline summary: ${err instanceof Error ? err.message : String(err)}`);
3863
+ }
3864
+ }
3865
+ }
3452
3866
  return state;
3453
3867
  }
3454
3868
  function printStatus(taskId, taskDir) {
@@ -3483,12 +3897,14 @@ var init_pipeline = __esm({
3483
3897
  init_hooks();
3484
3898
  init_auto_learn();
3485
3899
  init_retrospective();
3900
+ init_summary();
3901
+ init_config();
3486
3902
  }
3487
3903
  });
3488
3904
 
3489
3905
  // src/preflight.ts
3490
3906
  import { execFileSync as execFileSync14 } from "child_process";
3491
- import * as fs25 from "fs";
3907
+ import * as fs26 from "fs";
3492
3908
  function check(name, fn) {
3493
3909
  try {
3494
3910
  const detail = fn() ?? void 0;
@@ -3541,7 +3957,7 @@ function runPreflight() {
3541
3957
  return v;
3542
3958
  }),
3543
3959
  check("package.json", () => {
3544
- if (!fs25.existsSync("package.json")) throw new Error("not found");
3960
+ if (!fs26.existsSync("package.json")) throw new Error("not found");
3545
3961
  })
3546
3962
  ];
3547
3963
  const failed = checks.filter((c) => !c.ok);
@@ -3618,7 +4034,7 @@ var init_args = __esm({
3618
4034
  });
3619
4035
 
3620
4036
  // src/cli/litellm.ts
3621
- import * as fs26 from "fs";
4037
+ import * as fs27 from "fs";
3622
4038
  import * as os from "os";
3623
4039
  import * as path24 from "path";
3624
4040
  import { execFileSync as execFileSync15 } from "child_process";
@@ -3676,13 +4092,40 @@ function generateLitellmConfig(provider, modelMap) {
3676
4092
  }
3677
4093
  return entries.join("\n") + "\n";
3678
4094
  }
4095
+ function generateLitellmConfigFromStages(defaultConfig, stages) {
4096
+ const proxyModels = [];
4097
+ if (defaultConfig && defaultConfig.provider !== "claude" && defaultConfig.provider !== "anthropic") {
4098
+ proxyModels.push(defaultConfig);
4099
+ }
4100
+ if (stages) {
4101
+ for (const sc of Object.values(stages)) {
4102
+ if (sc.provider !== "claude" && sc.provider !== "anthropic") {
4103
+ proxyModels.push(sc);
4104
+ }
4105
+ }
4106
+ }
4107
+ if (proxyModels.length === 0) return void 0;
4108
+ const entries = ["model_list:"];
4109
+ const seen = /* @__PURE__ */ new Set();
4110
+ for (const { provider, model } of proxyModels) {
4111
+ const key = `${provider}/${model}`;
4112
+ if (seen.has(key)) continue;
4113
+ seen.add(key);
4114
+ const apiKeyVar = providerApiKeyEnvVar(provider);
4115
+ entries.push(` - model_name: ${model}`);
4116
+ entries.push(` litellm_params:`);
4117
+ entries.push(` model: ${provider}/${model}`);
4118
+ entries.push(` api_key: os.environ/${apiKeyVar}`);
4119
+ }
4120
+ return entries.join("\n") + "\n";
4121
+ }
3679
4122
  async function tryStartLitellm(url, projectDir, generatedConfig) {
3680
4123
  if (!generatedConfig) {
3681
4124
  logger.warn("No provider configured in kody.config.json \u2014 cannot start LiteLLM proxy");
3682
4125
  return null;
3683
4126
  }
3684
4127
  const configPath = path24.join(os.tmpdir(), "kody-litellm-config.yaml");
3685
- fs26.writeFileSync(configPath, generatedConfig);
4128
+ fs27.writeFileSync(configPath, generatedConfig);
3686
4129
  const portMatch = url.match(/:(\d+)/);
3687
4130
  const port = portMatch ? portMatch[1] : "4000";
3688
4131
  let litellmFound = false;
@@ -3706,15 +4149,15 @@ async function tryStartLitellm(url, projectDir, generatedConfig) {
3706
4149
  try {
3707
4150
  execFileSync15("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
3708
4151
  cmd = "litellm";
3709
- args2 = ["--config", configPath, "--port", port];
4152
+ args2 = ["--config", configPath, "--port", port, "--no_db"];
3710
4153
  } catch {
3711
4154
  cmd = "python3";
3712
- args2 = ["-m", "litellm", "--config", configPath, "--port", port];
4155
+ args2 = ["-m", "litellm", "--config", configPath, "--port", port, "--no_db"];
3713
4156
  }
3714
4157
  const dotenvPath = path24.join(projectDir, ".env");
3715
4158
  const dotenvVars = {};
3716
- if (fs26.existsSync(dotenvPath)) {
3717
- for (const rawLine of fs26.readFileSync(dotenvPath, "utf-8").split("\n")) {
4159
+ if (fs27.existsSync(dotenvPath)) {
4160
+ for (const rawLine of fs27.readFileSync(dotenvPath, "utf-8").split("\n")) {
3718
4161
  const line = rawLine.trim();
3719
4162
  if (!line || line.startsWith("#")) continue;
3720
4163
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -3765,7 +4208,7 @@ var init_litellm = __esm({
3765
4208
  });
3766
4209
 
3767
4210
  // src/cli/task-state.ts
3768
- import * as fs27 from "fs";
4211
+ import * as fs28 from "fs";
3769
4212
  import * as path25 from "path";
3770
4213
  function resolveTaskAction(issueNumber, existingTaskId, existingState) {
3771
4214
  if (!existingTaskId || !existingState) {
@@ -3800,9 +4243,9 @@ function resolveForIssue(issueNumber, projectDir) {
3800
4243
  if (existingTaskId) {
3801
4244
  const statusPath = path25.join(projectDir, ".kody", "tasks", existingTaskId, "status.json");
3802
4245
  let existingState = null;
3803
- if (fs27.existsSync(statusPath)) {
4246
+ if (fs28.existsSync(statusPath)) {
3804
4247
  try {
3805
- existingState = JSON.parse(fs27.readFileSync(statusPath, "utf-8"));
4248
+ existingState = JSON.parse(fs28.readFileSync(statusPath, "utf-8"));
3806
4249
  } catch {
3807
4250
  }
3808
4251
  }
@@ -3959,10 +4402,10 @@ var init_resolve = __esm({
3959
4402
 
3960
4403
  // src/entry.ts
3961
4404
  var entry_exports = {};
3962
- import * as fs28 from "fs";
4405
+ import * as fs29 from "fs";
3963
4406
  import * as path26 from "path";
3964
4407
  async function ensureLitellmProxy(config, projectDir) {
3965
- if (!needsLitellmProxy(config)) return null;
4408
+ if (!anyStageNeedsProxy(config)) return null;
3966
4409
  const litellmUrl = getLitellmUrl();
3967
4410
  const proxyRunning = await checkLitellmHealth(litellmUrl);
3968
4411
  let litellmProcess = null;
@@ -3975,7 +4418,9 @@ async function ensureLitellmProxy(config, projectDir) {
3975
4418
  }
3976
4419
  }
3977
4420
  let generatedConfig;
3978
- if (config.agent.provider && config.agent.provider !== "anthropic") {
4421
+ if (config.agent.stages || config.agent.default) {
4422
+ generatedConfig = generateLitellmConfigFromStages(config.agent.default, config.agent.stages);
4423
+ } else if (config.agent.provider && config.agent.provider !== "anthropic") {
3979
4424
  generatedConfig = generateLitellmConfig(config.agent.provider, config.agent.modelMap);
3980
4425
  }
3981
4426
  litellmProcess = await tryStartLitellm(litellmUrl, projectDir, generatedConfig);
@@ -3986,9 +4431,8 @@ async function ensureLitellmProxy(config, projectDir) {
3986
4431
  } else {
3987
4432
  logger.info(`LiteLLM proxy already running at ${litellmUrl}`);
3988
4433
  }
3989
- process.env.ANTHROPIC_BASE_URL = litellmUrl;
3990
- logger.info(`ANTHROPIC_BASE_URL set to ${litellmUrl}`);
3991
- if (!process.env.ANTHROPIC_API_KEY || !process.env.ANTHROPIC_API_KEY.startsWith("sk-ant-")) {
4434
+ logger.info(`LiteLLM proxy available at ${litellmUrl}`);
4435
+ if (!process.env.ANTHROPIC_API_KEY) {
3992
4436
  process.env.ANTHROPIC_API_KEY = `sk-ant-api03-${"0".repeat(64)}`;
3993
4437
  }
3994
4438
  return litellmProcess;
@@ -4016,7 +4460,7 @@ async function main() {
4016
4460
  const input = parseArgs();
4017
4461
  const projectDir = input.cwd ? path26.resolve(input.cwd) : process.cwd();
4018
4462
  if (input.cwd) {
4019
- if (!fs28.existsSync(projectDir)) {
4463
+ if (!fs29.existsSync(projectDir)) {
4020
4464
  console.error(`--cwd path does not exist: ${projectDir}`);
4021
4465
  process.exit(1);
4022
4466
  }
@@ -4060,7 +4504,16 @@ async function main() {
4060
4504
  }
4061
4505
  let taskId = input.taskId;
4062
4506
  if (!taskId) {
4063
- if (isPRFix) {
4507
+ if ((input.command === "rerun" || input.command === "status") && input.issueNumber) {
4508
+ const resolved = resolveTaskIdForCommand(input.issueNumber, projectDir);
4509
+ if (resolved) {
4510
+ taskId = resolved;
4511
+ logger.info(`Auto-resolved task-id: ${taskId} (from issue #${input.issueNumber})`);
4512
+ } else {
4513
+ console.error(`No task found for issue #${input.issueNumber}. Provide --task-id explicitly.`);
4514
+ process.exit(1);
4515
+ }
4516
+ } else if (isPRFix) {
4064
4517
  taskId = `${input.command === "fix-ci" ? "fixci" : "fix"}-pr-${input.prNumber}-${generateTaskId()}`;
4065
4518
  } else if (input.issueNumber) {
4066
4519
  taskId = `${input.issueNumber}-${generateTaskId()}`;
@@ -4074,7 +4527,7 @@ async function main() {
4074
4527
  }
4075
4528
  }
4076
4529
  const taskDir = path26.join(projectDir, ".kody", "tasks", taskId);
4077
- fs28.mkdirSync(taskDir, { recursive: true });
4530
+ fs29.mkdirSync(taskDir, { recursive: true });
4078
4531
  if (input.command === "status") {
4079
4532
  printStatus(taskId, taskDir);
4080
4533
  return;
@@ -4190,31 +4643,31 @@ async function main() {
4190
4643
  logger.info("Preflight checks:");
4191
4644
  runPreflight();
4192
4645
  if (input.task) {
4193
- fs28.writeFileSync(path26.join(taskDir, "task.md"), input.task);
4646
+ fs29.writeFileSync(path26.join(taskDir, "task.md"), input.task);
4194
4647
  }
4195
4648
  const taskMdPath = path26.join(taskDir, "task.md");
4196
- if (!fs28.existsSync(taskMdPath) && isPRFix && input.prNumber) {
4649
+ if (!fs29.existsSync(taskMdPath) && isPRFix && input.prNumber) {
4197
4650
  logger.info(`Fetching PR #${input.prNumber} details as task context...`);
4198
4651
  const prDetails = getPRDetails(input.prNumber);
4199
4652
  if (prDetails) {
4200
4653
  const taskContent = `# ${prDetails.title}
4201
4654
 
4202
4655
  ${prDetails.body ?? ""}`;
4203
- fs28.writeFileSync(taskMdPath, taskContent);
4656
+ fs29.writeFileSync(taskMdPath, taskContent);
4204
4657
  logger.info(` Task loaded from PR #${input.prNumber}: ${prDetails.title}`);
4205
4658
  }
4206
- } else if (!fs28.existsSync(taskMdPath) && input.issueNumber) {
4659
+ } else if (!fs29.existsSync(taskMdPath) && input.issueNumber) {
4207
4660
  logger.info(`Fetching issue #${input.issueNumber} body as task...`);
4208
4661
  const issue = getIssue(input.issueNumber);
4209
4662
  if (issue) {
4210
4663
  const taskContent = `# ${issue.title}
4211
4664
 
4212
4665
  ${issue.body ?? ""}`;
4213
- fs28.writeFileSync(taskMdPath, taskContent);
4666
+ fs29.writeFileSync(taskMdPath, taskContent);
4214
4667
  logger.info(` Task loaded from issue #${input.issueNumber}: ${issue.title}`);
4215
4668
  }
4216
4669
  }
4217
- if (!fs28.existsSync(taskMdPath)) {
4670
+ if (!fs29.existsSync(taskMdPath)) {
4218
4671
  console.error("No task.md found. Provide --task, --issue-number, or ensure .kody/tasks/<id>/task.md exists.");
4219
4672
  process.exit(1);
4220
4673
  }
@@ -4352,7 +4805,7 @@ To rerun: \`@kody rerun ${taskId} --from <stage>\``
4352
4805
  }
4353
4806
  }
4354
4807
  const state = await runPipeline(ctx);
4355
- const files = fs28.readdirSync(taskDir);
4808
+ const files = fs29.readdirSync(taskDir);
4356
4809
  console.log(`
4357
4810
  Artifacts in ${taskDir}:`);
4358
4811
  for (const f of files) {
@@ -4416,7 +4869,7 @@ var init_entry = __esm({
4416
4869
  });
4417
4870
 
4418
4871
  // src/bin/cli.ts
4419
- import * as fs29 from "fs";
4872
+ import * as fs30 from "fs";
4420
4873
  import * as path27 from "path";
4421
4874
  import { fileURLToPath } from "url";
4422
4875
 
@@ -5592,7 +6045,7 @@ var __dirname = path27.dirname(fileURLToPath(import.meta.url));
5592
6045
  var PKG_ROOT = path27.resolve(__dirname, "..", "..");
5593
6046
  function getVersion() {
5594
6047
  const pkgPath = path27.join(PKG_ROOT, "package.json");
5595
- const pkg = JSON.parse(fs29.readFileSync(pkgPath, "utf-8"));
6048
+ const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf-8"));
5596
6049
  return pkg.version;
5597
6050
  }
5598
6051
  var args = process.argv.slice(2);
@@ -5601,6 +6054,8 @@ if (command === "init") {
5601
6054
  initCommand({ force: args.includes("--force") }, PKG_ROOT);
5602
6055
  } else if (command === "bootstrap") {
5603
6056
  bootstrapCommand({ force: args.includes("--force") }, PKG_ROOT);
6057
+ } else if (command === "ci-parse") {
6058
+ Promise.resolve().then(() => (init_parse_inputs(), parse_inputs_exports)).then(({ runCiParse: runCiParse2 }) => runCiParse2());
5604
6059
  } else if (command === "version" || command === "--version" || command === "-v") {
5605
6060
  console.log(getVersion());
5606
6061
  } else {