@kody-ade/kody-engine-lite 0.1.81 → 0.1.83

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 (2) hide show
  1. package/dist/bin/cli.js +301 -279
  2. package/package.json +1 -1
package/dist/bin/cli.js CHANGED
@@ -137,7 +137,7 @@ var init_agent_runner = __esm({
137
137
  "src/agent-runner.ts"() {
138
138
  "use strict";
139
139
  SIGKILL_GRACE_MS = 5e3;
140
- STDERR_TAIL_CHARS = 500;
140
+ STDERR_TAIL_CHARS = 2e3;
141
141
  RUNNER_FACTORIES = {
142
142
  "claude-code": createClaudeCodeRunner
143
143
  };
@@ -282,7 +282,11 @@ function getProjectConfig() {
282
282
  quality: { ...DEFAULT_CONFIG.quality, ...raw.quality },
283
283
  git: { ...DEFAULT_CONFIG.git, ...raw.git },
284
284
  github: { ...DEFAULT_CONFIG.github, ...raw.github },
285
- agent: { ...DEFAULT_CONFIG.agent, ...raw.agent },
285
+ agent: {
286
+ ...DEFAULT_CONFIG.agent,
287
+ ...raw.agent,
288
+ modelMap: { ...DEFAULT_CONFIG.agent.modelMap, ...raw.agent?.modelMap }
289
+ },
286
290
  contextTiers: raw.contextTiers ? { ...DEFAULT_CONFIG.contextTiers, ...raw.contextTiers } : DEFAULT_CONFIG.contextTiers,
287
291
  mcp: raw.mcp ? { enabled: false, servers: {}, stages: ["build", "verify", "review", "review-fix"], ...raw.mcp } : void 0
288
292
  };
@@ -713,8 +717,10 @@ function submitPRReview(prNumber, body, event) {
713
717
  { input: body }
714
718
  );
715
719
  logger.info(` PR review submitted on #${prNumber}: ${event}`);
720
+ return true;
716
721
  } catch (err) {
717
722
  logger.warn(` Failed to submit PR review: ${err}`);
723
+ return false;
718
724
  }
719
725
  }
720
726
  function getCIFailureLogs(runId, maxLength = 8e3) {
@@ -1868,8 +1874,8 @@ async function diagnoseFailure(stageName, errorOutput, modifiedFiles, runner, mo
1868
1874
  `Stage: ${stageName}`,
1869
1875
  ``,
1870
1876
  `Error output:`,
1871
- errorOutput.slice(-2e3),
1872
- // Last 2000 chars of error
1877
+ errorOutput.slice(-5e3),
1878
+ // Last 5000 chars of error for accurate diagnosis
1873
1879
  ``,
1874
1880
  modifiedFiles.length > 0 ? `Files modified by build stage:
1875
1881
  ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (build may not have run yet)."
@@ -1916,13 +1922,21 @@ ${modifiedFiles.map((f) => `- ${f}`).join("\n")}` : "No files were modified (bui
1916
1922
  }
1917
1923
  function getModifiedFiles(projectDir) {
1918
1924
  try {
1919
- const output = execFileSync5("git", ["diff", "--name-only", "HEAD~1"], {
1925
+ const staged = execFileSync5("git", ["diff", "--name-only", "--cached"], {
1926
+ encoding: "utf-8",
1927
+ cwd: projectDir,
1928
+ timeout: 5e3,
1929
+ stdio: ["pipe", "pipe", "pipe"]
1930
+ }).trim();
1931
+ const unstaged = execFileSync5("git", ["diff", "--name-only"], {
1920
1932
  encoding: "utf-8",
1921
1933
  cwd: projectDir,
1922
1934
  timeout: 5e3,
1923
1935
  stdio: ["pipe", "pipe", "pipe"]
1924
1936
  }).trim();
1925
- return output ? output.split("\n").filter(Boolean) : [];
1937
+ const all = `${staged}
1938
+ ${unstaged}`.split("\n").filter(Boolean);
1939
+ return [...new Set(all)];
1926
1940
  } catch {
1927
1941
  return [];
1928
1942
  }
@@ -2122,55 +2136,198 @@ var init_verify = __esm({
2122
2136
  }
2123
2137
  });
2124
2138
 
2125
- // src/stages/review.ts
2139
+ // src/cli/task-resolution.ts
2126
2140
  import * as fs9 from "fs";
2127
2141
  import * as path9 from "path";
2142
+ import { execFileSync as execFileSync7 } from "child_process";
2143
+ function findLatestTaskForIssue(issueNumber, projectDir) {
2144
+ const tasksDir = path9.join(projectDir, ".kody", "tasks");
2145
+ if (!fs9.existsSync(tasksDir)) return null;
2146
+ const allDirs = fs9.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
2147
+ const prefix = `${issueNumber}-`;
2148
+ const direct = allDirs.find((d) => d.startsWith(prefix));
2149
+ if (direct) return direct;
2150
+ try {
2151
+ const branch = execFileSync7("git", ["branch", "--show-current"], {
2152
+ encoding: "utf-8",
2153
+ cwd: projectDir,
2154
+ timeout: 5e3,
2155
+ stdio: ["pipe", "pipe", "pipe"]
2156
+ }).trim();
2157
+ const branchIssueMatch = branch.match(/^(\d+)-/);
2158
+ if (branchIssueMatch) {
2159
+ const branchIssueNum = branchIssueMatch[1];
2160
+ const branchPrefix = `${branchIssueNum}-`;
2161
+ const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
2162
+ if (fromBranch) return fromBranch;
2163
+ }
2164
+ } catch {
2165
+ }
2166
+ return null;
2167
+ }
2168
+ function generateTaskId() {
2169
+ const now = /* @__PURE__ */ new Date();
2170
+ const pad = (n) => String(n).padStart(2, "0");
2171
+ return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
2172
+ }
2173
+ var init_task_resolution = __esm({
2174
+ "src/cli/task-resolution.ts"() {
2175
+ "use strict";
2176
+ }
2177
+ });
2178
+
2179
+ // src/review-standalone.ts
2180
+ import * as fs10 from "fs";
2181
+ import * as path10 from "path";
2182
+ function resolveReviewTarget(input) {
2183
+ if (input.prs.length === 0) {
2184
+ return {
2185
+ action: "none",
2186
+ message: `Issue #${input.issueNumber} has no open PRs. Nothing to review.`
2187
+ };
2188
+ }
2189
+ if (input.prs.length === 1) {
2190
+ return { action: "review", prNumber: input.prs[0].number };
2191
+ }
2192
+ const prList = input.prs.map((pr) => ` - #${pr.number}: ${pr.title}`).join("\n");
2193
+ return {
2194
+ action: "pick",
2195
+ prs: input.prs,
2196
+ message: `\u26A0\uFE0F Issue #${input.issueNumber} has ${input.prs.length} open PRs:
2197
+ ${prList}
2198
+
2199
+ Run: \`pnpm kody review --pr-number <n>\`
2200
+ Or comment on the specific PR: \`@kody review\``
2201
+ };
2202
+ }
2203
+ async function runStandaloneReview(input) {
2204
+ const taskId = input.taskId ?? `review-${generateTaskId()}`;
2205
+ const taskDir = path10.join(input.projectDir, ".kody", "tasks", taskId);
2206
+ fs10.mkdirSync(taskDir, { recursive: true });
2207
+ const taskContent = `# ${input.prTitle}
2208
+
2209
+ ${input.prBody ?? ""}`;
2210
+ fs10.writeFileSync(path10.join(taskDir, "task.md"), taskContent);
2211
+ const reviewDef = STAGES.find((s) => s.name === "review");
2212
+ const ctx = {
2213
+ taskId,
2214
+ taskDir,
2215
+ projectDir: input.projectDir,
2216
+ runners: input.runners,
2217
+ sessions: {},
2218
+ input: {
2219
+ mode: "full",
2220
+ local: input.local
2221
+ }
2222
+ };
2223
+ logger.info(`[review] standalone review for: ${input.prTitle}`);
2224
+ const result = await executeAgentStage(ctx, reviewDef);
2225
+ if (result.outcome !== "completed") {
2226
+ return {
2227
+ outcome: "failed",
2228
+ taskDir,
2229
+ error: result.error ?? "Review stage failed"
2230
+ };
2231
+ }
2232
+ const reviewPath = path10.join(taskDir, "review.md");
2233
+ let reviewContent;
2234
+ if (fs10.existsSync(reviewPath)) {
2235
+ reviewContent = fs10.readFileSync(reviewPath, "utf-8");
2236
+ }
2237
+ return {
2238
+ outcome: "completed",
2239
+ reviewContent,
2240
+ taskDir
2241
+ };
2242
+ }
2243
+ function detectReviewVerdict(reviewContent) {
2244
+ const verdictMatch = reviewContent.match(/##\s*Verdict:\s*(PASS|FAIL)/i);
2245
+ if (verdictMatch) {
2246
+ return verdictMatch[1].toLowerCase();
2247
+ }
2248
+ const hasCritical = /###\s*Critical\s*\n(?!None\.)/i.test(reviewContent);
2249
+ const hasMajor = /###\s*Major\s*\n(?!None\.)/i.test(reviewContent);
2250
+ if (hasCritical || hasMajor) return "fail";
2251
+ return "pass";
2252
+ }
2253
+ function formatReviewComment(reviewContent, taskId) {
2254
+ const verdict = detectReviewVerdict(reviewContent);
2255
+ const cta = verdict === "fail" ? "\n\n> To fix these issues, comment: `@kody fix`\n> The review findings will be used automatically as context." : "";
2256
+ return `## \u{1F50D} Kody Review (\`${taskId}\`)
2257
+
2258
+ ${reviewContent}${cta}
2259
+
2260
+ ---
2261
+ \u{1F916} Generated by Kody`;
2262
+ }
2263
+ var init_review_standalone = __esm({
2264
+ "src/review-standalone.ts"() {
2265
+ "use strict";
2266
+ init_definitions();
2267
+ init_agent();
2268
+ init_task_resolution();
2269
+ init_logger();
2270
+ }
2271
+ });
2272
+
2273
+ // src/stages/review.ts
2274
+ import * as fs11 from "fs";
2275
+ import * as path11 from "path";
2128
2276
  async function executeReviewWithFix(ctx, def) {
2129
2277
  if (ctx.input.dryRun) {
2130
2278
  return { outcome: "completed", retries: 0 };
2131
2279
  }
2132
2280
  const reviewDef = STAGES.find((s) => s.name === "review");
2133
2281
  const reviewFixDef = STAGES.find((s) => s.name === "review-fix");
2134
- const reviewResult = await executeAgentStage(ctx, reviewDef);
2135
- if (reviewResult.outcome !== "completed") {
2136
- return reviewResult;
2137
- }
2138
- const reviewFile = path9.join(ctx.taskDir, "review.md");
2139
- if (!fs9.existsSync(reviewFile)) {
2140
- return { outcome: "failed", retries: 0, error: "review.md not found" };
2141
- }
2142
- const content = fs9.readFileSync(reviewFile, "utf-8");
2143
- const hasIssues = /\bfail\b/i.test(content) && !/pass/i.test(content);
2144
- if (!hasIssues) {
2145
- return reviewResult;
2146
- }
2147
- logger.info(` review found issues, running review-fix...`);
2148
- const fixResult = await executeAgentStage(ctx, reviewFixDef);
2149
- if (fixResult.outcome !== "completed") {
2150
- return fixResult;
2282
+ for (let iteration = 0; iteration <= MAX_REVIEW_FIX_ITERATIONS; iteration++) {
2283
+ const label = iteration === 0 ? "initial review" : `review after fix #${iteration}`;
2284
+ logger.info(` running ${label}...`);
2285
+ const reviewResult = await executeAgentStage(ctx, reviewDef);
2286
+ if (reviewResult.outcome !== "completed") {
2287
+ return reviewResult;
2288
+ }
2289
+ const reviewFile = path11.join(ctx.taskDir, "review.md");
2290
+ if (!fs11.existsSync(reviewFile)) {
2291
+ return { outcome: "failed", retries: iteration, error: "review.md not found" };
2292
+ }
2293
+ const content = fs11.readFileSync(reviewFile, "utf-8");
2294
+ if (detectReviewVerdict(content) !== "fail") {
2295
+ return { ...reviewResult, retries: iteration };
2296
+ }
2297
+ if (iteration === MAX_REVIEW_FIX_ITERATIONS) {
2298
+ logger.warn(` review still failing after ${MAX_REVIEW_FIX_ITERATIONS} fix attempts`);
2299
+ return { ...reviewResult, retries: iteration };
2300
+ }
2301
+ logger.info(` review found issues (iteration ${iteration + 1}/${MAX_REVIEW_FIX_ITERATIONS}), running review-fix...`);
2302
+ const fixResult = await executeAgentStage(ctx, reviewFixDef);
2303
+ if (fixResult.outcome !== "completed") {
2304
+ return fixResult;
2305
+ }
2151
2306
  }
2152
- logger.info(` re-running review after fix...`);
2153
- return executeAgentStage(ctx, reviewDef);
2307
+ return { outcome: "failed", retries: MAX_REVIEW_FIX_ITERATIONS, error: "Unexpected review-fix loop exit" };
2154
2308
  }
2309
+ var MAX_REVIEW_FIX_ITERATIONS;
2155
2310
  var init_review = __esm({
2156
2311
  "src/stages/review.ts"() {
2157
2312
  "use strict";
2158
2313
  init_definitions();
2159
2314
  init_logger();
2160
2315
  init_agent();
2316
+ init_review_standalone();
2317
+ MAX_REVIEW_FIX_ITERATIONS = 2;
2161
2318
  }
2162
2319
  });
2163
2320
 
2164
2321
  // src/stages/ship.ts
2165
- import * as fs10 from "fs";
2166
- import * as path10 from "path";
2167
- import { execFileSync as execFileSync7 } from "child_process";
2322
+ import * as fs12 from "fs";
2323
+ import * as path12 from "path";
2324
+ import { execFileSync as execFileSync8 } from "child_process";
2168
2325
  function buildPrBody(ctx) {
2169
2326
  const sections = [];
2170
- const taskJsonPath = path10.join(ctx.taskDir, "task.json");
2171
- if (fs10.existsSync(taskJsonPath)) {
2327
+ const taskJsonPath = path12.join(ctx.taskDir, "task.json");
2328
+ if (fs12.existsSync(taskJsonPath)) {
2172
2329
  try {
2173
- const raw = fs10.readFileSync(taskJsonPath, "utf-8");
2330
+ const raw = fs12.readFileSync(taskJsonPath, "utf-8");
2174
2331
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2175
2332
  const task = JSON.parse(cleaned);
2176
2333
  if (task.description) {
@@ -2189,9 +2346,9 @@ ${task.scope.map((s) => `- \`${s}\``).join("\n")}`);
2189
2346
  } catch {
2190
2347
  }
2191
2348
  }
2192
- const reviewPath = path10.join(ctx.taskDir, "review.md");
2193
- if (fs10.existsSync(reviewPath)) {
2194
- const review = fs10.readFileSync(reviewPath, "utf-8");
2349
+ const reviewPath = path12.join(ctx.taskDir, "review.md");
2350
+ if (fs12.existsSync(reviewPath)) {
2351
+ const review = fs12.readFileSync(reviewPath, "utf-8");
2195
2352
  const summaryMatch = review.match(/## Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
2196
2353
  if (summaryMatch) {
2197
2354
  const summary = summaryMatch[1].trim();
@@ -2208,14 +2365,14 @@ ${summary}`);
2208
2365
  **Review:** ${verdictMatch[1].toUpperCase() === "PASS" ? "\u2705 PASS" : "\u274C FAIL"}`);
2209
2366
  }
2210
2367
  }
2211
- const verifyPath = path10.join(ctx.taskDir, "verify.md");
2212
- if (fs10.existsSync(verifyPath)) {
2213
- const verify = fs10.readFileSync(verifyPath, "utf-8");
2368
+ const verifyPath = path12.join(ctx.taskDir, "verify.md");
2369
+ if (fs12.existsSync(verifyPath)) {
2370
+ const verify = fs12.readFileSync(verifyPath, "utf-8");
2214
2371
  if (/PASS/i.test(verify)) sections.push(`**Verify:** \u2705 typecheck + tests + lint passed`);
2215
2372
  }
2216
- const planPath = path10.join(ctx.taskDir, "plan.md");
2217
- if (fs10.existsSync(planPath)) {
2218
- const plan = fs10.readFileSync(planPath, "utf-8").trim();
2373
+ const planPath = path12.join(ctx.taskDir, "plan.md");
2374
+ if (fs12.existsSync(planPath)) {
2375
+ const plan = fs12.readFileSync(planPath, "utf-8").trim();
2219
2376
  if (plan) {
2220
2377
  const truncated = plan.length > 800 ? plan.slice(0, 800) + "\n..." : plan;
2221
2378
  sections.push(`
@@ -2235,25 +2392,25 @@ Closes #${ctx.input.issueNumber}`);
2235
2392
  return sections.join("\n");
2236
2393
  }
2237
2394
  function executeShipStage(ctx, _def) {
2238
- const shipPath = path10.join(ctx.taskDir, "ship.md");
2395
+ const shipPath = path12.join(ctx.taskDir, "ship.md");
2239
2396
  if (ctx.input.dryRun) {
2240
- fs10.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
2397
+ fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 dry run.\n");
2241
2398
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2242
2399
  }
2243
2400
  if (ctx.input.local && !ctx.input.issueNumber) {
2244
- fs10.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
2401
+ fs12.writeFileSync(shipPath, "# Ship\n\nShip stage skipped \u2014 local mode, no issue number.\n");
2245
2402
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2246
2403
  }
2247
2404
  try {
2248
2405
  const head = getCurrentBranch(ctx.projectDir);
2249
2406
  const base = getDefaultBranch(ctx.projectDir);
2250
2407
  try {
2251
- execFileSync7("git", ["add", ctx.taskDir], {
2408
+ execFileSync8("git", ["add", ctx.taskDir], {
2252
2409
  cwd: ctx.projectDir,
2253
2410
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2254
2411
  stdio: "pipe"
2255
2412
  });
2256
- execFileSync7("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
2413
+ execFileSync8("git", ["commit", "--no-gpg-sign", "-m", `chore: add kody task artifacts [skip ci]`], {
2257
2414
  cwd: ctx.projectDir,
2258
2415
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2259
2416
  stdio: "pipe"
@@ -2267,7 +2424,7 @@ function executeShipStage(ctx, _def) {
2267
2424
  let repo = config.github?.repo;
2268
2425
  if (!owner || !repo) {
2269
2426
  try {
2270
- const remoteUrl = execFileSync7("git", ["remote", "get-url", "origin"], {
2427
+ const remoteUrl = execFileSync8("git", ["remote", "get-url", "origin"], {
2271
2428
  encoding: "utf-8",
2272
2429
  cwd: ctx.projectDir
2273
2430
  }).trim();
@@ -2287,10 +2444,10 @@ function executeShipStage(ctx, _def) {
2287
2444
  docs: "docs",
2288
2445
  chore: "chore"
2289
2446
  };
2290
- const taskJsonPath = path10.join(ctx.taskDir, "task.json");
2291
- if (fs10.existsSync(taskJsonPath)) {
2447
+ const taskJsonPath = path12.join(ctx.taskDir, "task.json");
2448
+ if (fs12.existsSync(taskJsonPath)) {
2292
2449
  try {
2293
- const raw = fs10.readFileSync(taskJsonPath, "utf-8");
2450
+ const raw = fs12.readFileSync(taskJsonPath, "utf-8");
2294
2451
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2295
2452
  const task = JSON.parse(cleaned);
2296
2453
  const prefix = TYPE_PREFIX[task.task_type] ?? "chore";
@@ -2300,9 +2457,9 @@ function executeShipStage(ctx, _def) {
2300
2457
  }
2301
2458
  }
2302
2459
  if (title === "Update") {
2303
- const taskMdPath = path10.join(ctx.taskDir, "task.md");
2304
- if (fs10.existsSync(taskMdPath)) {
2305
- const content = fs10.readFileSync(taskMdPath, "utf-8");
2460
+ const taskMdPath = path12.join(ctx.taskDir, "task.md");
2461
+ if (fs12.existsSync(taskMdPath)) {
2462
+ const content = fs12.readFileSync(taskMdPath, "utf-8");
2306
2463
  const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#") && !l.startsWith("*"));
2307
2464
  if (firstLine) title = `chore: ${firstLine.trim()}`.slice(0, 72);
2308
2465
  }
@@ -2322,7 +2479,7 @@ function executeShipStage(ctx, _def) {
2322
2479
  } catch {
2323
2480
  }
2324
2481
  }
2325
- fs10.writeFileSync(shipPath, `# Ship
2482
+ fs12.writeFileSync(shipPath, `# Ship
2326
2483
 
2327
2484
  Updated existing PR: ${existingPr.url}
2328
2485
  PR #${existingPr.number}
@@ -2343,19 +2500,19 @@ PR #${existingPr.number}
2343
2500
  } catch {
2344
2501
  }
2345
2502
  }
2346
- fs10.writeFileSync(shipPath, `# Ship
2503
+ fs12.writeFileSync(shipPath, `# Ship
2347
2504
 
2348
2505
  PR created: ${pr.url}
2349
2506
  PR #${pr.number}
2350
2507
  `);
2351
2508
  } else {
2352
- fs10.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
2509
+ fs12.writeFileSync(shipPath, "# Ship\n\nPushed branch but failed to create PR.\n");
2353
2510
  }
2354
2511
  }
2355
2512
  return { outcome: "completed", outputFile: "ship.md", retries: 0 };
2356
2513
  } catch (err) {
2357
2514
  const msg = err instanceof Error ? err.message : String(err);
2358
- fs10.writeFileSync(shipPath, `# Ship
2515
+ fs12.writeFileSync(shipPath, `# Ship
2359
2516
 
2360
2517
  Failed: ${msg}
2361
2518
  `);
@@ -2401,15 +2558,15 @@ var init_executor_registry = __esm({
2401
2558
  });
2402
2559
 
2403
2560
  // src/pipeline/questions.ts
2404
- import * as fs11 from "fs";
2405
- import * as path11 from "path";
2561
+ import * as fs13 from "fs";
2562
+ import * as path13 from "path";
2406
2563
  function checkForQuestions(ctx, stageName) {
2407
2564
  if (ctx.input.local || !ctx.input.issueNumber) return false;
2408
2565
  try {
2409
2566
  if (stageName === "taskify") {
2410
- const taskJsonPath = path11.join(ctx.taskDir, "task.json");
2411
- if (!fs11.existsSync(taskJsonPath)) return false;
2412
- const raw = fs11.readFileSync(taskJsonPath, "utf-8");
2567
+ const taskJsonPath = path13.join(ctx.taskDir, "task.json");
2568
+ if (!fs13.existsSync(taskJsonPath)) return false;
2569
+ const raw = fs13.readFileSync(taskJsonPath, "utf-8");
2413
2570
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2414
2571
  const taskJson = JSON.parse(cleaned);
2415
2572
  if (taskJson.questions && Array.isArray(taskJson.questions) && taskJson.questions.length > 0) {
@@ -2424,9 +2581,9 @@ Reply with \`@kody approve\` and your answers in the comment body.`;
2424
2581
  }
2425
2582
  }
2426
2583
  if (stageName === "plan") {
2427
- const planPath = path11.join(ctx.taskDir, "plan.md");
2428
- if (!fs11.existsSync(planPath)) return false;
2429
- const plan = fs11.readFileSync(planPath, "utf-8");
2584
+ const planPath = path13.join(ctx.taskDir, "plan.md");
2585
+ if (!fs13.existsSync(planPath)) return false;
2586
+ const plan = fs13.readFileSync(planPath, "utf-8");
2430
2587
  const questionsMatch = plan.match(/## Questions\s*\n([\s\S]*?)(?=\n## |\n*$)/);
2431
2588
  if (questionsMatch) {
2432
2589
  const questionsText = questionsMatch[1].trim();
@@ -2455,8 +2612,8 @@ var init_questions = __esm({
2455
2612
  });
2456
2613
 
2457
2614
  // src/pipeline/hooks.ts
2458
- import * as fs12 from "fs";
2459
- import * as path12 from "path";
2615
+ import * as fs14 from "fs";
2616
+ import * as path14 from "path";
2460
2617
  function applyPreStageLabel(ctx, def) {
2461
2618
  if (!ctx.input.issueNumber || ctx.input.local) return;
2462
2619
  if (def.name === "build") setLifecycleLabel(ctx.input.issueNumber, "building");
@@ -2482,9 +2639,9 @@ function autoDetectComplexity(ctx, def) {
2482
2639
  if (def.name !== "taskify") return null;
2483
2640
  if (ctx.input.complexity) return null;
2484
2641
  try {
2485
- const taskJsonPath = path12.join(ctx.taskDir, "task.json");
2486
- if (!fs12.existsSync(taskJsonPath)) return null;
2487
- const raw = fs12.readFileSync(taskJsonPath, "utf-8");
2642
+ const taskJsonPath = path14.join(ctx.taskDir, "task.json");
2643
+ if (!fs14.existsSync(taskJsonPath)) return null;
2644
+ const raw = fs14.readFileSync(taskJsonPath, "utf-8");
2488
2645
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2489
2646
  const taskJson = JSON.parse(cleaned);
2490
2647
  if (!taskJson.risk_level || !isValidComplexity(taskJson.risk_level)) return null;
@@ -2514,8 +2671,8 @@ function checkRiskGate(ctx, def, state, complexity) {
2514
2671
  if (ctx.input.dryRun || ctx.input.local) return null;
2515
2672
  if (ctx.input.mode === "rerun") return null;
2516
2673
  if (!ctx.input.issueNumber) return null;
2517
- const planPath = path12.join(ctx.taskDir, "plan.md");
2518
- const plan = fs12.existsSync(planPath) ? fs12.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
2674
+ const planPath = path14.join(ctx.taskDir, "plan.md");
2675
+ const plan = fs14.existsSync(planPath) ? fs14.readFileSync(planPath, "utf-8").slice(0, 1500) : "(plan not available)";
2519
2676
  try {
2520
2677
  postComment(
2521
2678
  ctx.input.issueNumber,
@@ -2582,22 +2739,22 @@ var init_hooks = __esm({
2582
2739
  });
2583
2740
 
2584
2741
  // src/learning/auto-learn.ts
2585
- import * as fs13 from "fs";
2586
- import * as path13 from "path";
2742
+ import * as fs15 from "fs";
2743
+ import * as path15 from "path";
2587
2744
  function stripAnsi(str) {
2588
2745
  return str.replace(/\x1b\[[0-9;]*m/g, "");
2589
2746
  }
2590
2747
  function autoLearn(ctx) {
2591
2748
  try {
2592
- const memoryDir = path13.join(ctx.projectDir, ".kody", "memory");
2593
- if (!fs13.existsSync(memoryDir)) {
2594
- fs13.mkdirSync(memoryDir, { recursive: true });
2749
+ const memoryDir = path15.join(ctx.projectDir, ".kody", "memory");
2750
+ if (!fs15.existsSync(memoryDir)) {
2751
+ fs15.mkdirSync(memoryDir, { recursive: true });
2595
2752
  }
2596
2753
  const learnings = [];
2597
2754
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2598
- const verifyPath = path13.join(ctx.taskDir, "verify.md");
2599
- if (fs13.existsSync(verifyPath)) {
2600
- const verify = stripAnsi(fs13.readFileSync(verifyPath, "utf-8"));
2755
+ const verifyPath = path15.join(ctx.taskDir, "verify.md");
2756
+ if (fs15.existsSync(verifyPath)) {
2757
+ const verify = stripAnsi(fs15.readFileSync(verifyPath, "utf-8"));
2601
2758
  if (/vitest/i.test(verify)) learnings.push("- Uses vitest for testing");
2602
2759
  if (/jest/i.test(verify)) learnings.push("- Uses jest for testing");
2603
2760
  if (/eslint/i.test(verify)) learnings.push("- Uses eslint for linting");
@@ -2606,18 +2763,18 @@ function autoLearn(ctx) {
2606
2763
  if (/jsdom/i.test(verify)) learnings.push("- Test environment: jsdom");
2607
2764
  if (/node/i.test(verify) && /environment/i.test(verify)) learnings.push("- Test environment: node");
2608
2765
  }
2609
- const reviewPath = path13.join(ctx.taskDir, "review.md");
2610
- if (fs13.existsSync(reviewPath)) {
2611
- const review = fs13.readFileSync(reviewPath, "utf-8");
2766
+ const reviewPath = path15.join(ctx.taskDir, "review.md");
2767
+ if (fs15.existsSync(reviewPath)) {
2768
+ const review = fs15.readFileSync(reviewPath, "utf-8");
2612
2769
  if (/\.js extension/i.test(review)) learnings.push("- Imports use .js extensions (ESM)");
2613
2770
  if (/barrel export/i.test(review)) learnings.push("- Uses barrel exports (index.ts)");
2614
2771
  if (/timezone/i.test(review)) learnings.push("- Timezone handling is a concern in this codebase");
2615
2772
  if (/UTC/i.test(review)) learnings.push("- Date operations should consider UTC vs local time");
2616
2773
  }
2617
- const taskJsonPath = path13.join(ctx.taskDir, "task.json");
2618
- if (fs13.existsSync(taskJsonPath)) {
2774
+ const taskJsonPath = path15.join(ctx.taskDir, "task.json");
2775
+ if (fs15.existsSync(taskJsonPath)) {
2619
2776
  try {
2620
- const raw = stripAnsi(fs13.readFileSync(taskJsonPath, "utf-8"));
2777
+ const raw = stripAnsi(fs15.readFileSync(taskJsonPath, "utf-8"));
2621
2778
  const cleaned = raw.replace(/^```json\s*\n?/m, "").replace(/\n?```\s*$/m, "");
2622
2779
  const task = JSON.parse(cleaned);
2623
2780
  if (task.scope && Array.isArray(task.scope)) {
@@ -2628,13 +2785,13 @@ function autoLearn(ctx) {
2628
2785
  }
2629
2786
  }
2630
2787
  if (learnings.length > 0) {
2631
- const conventionsPath = path13.join(memoryDir, "conventions.md");
2788
+ const conventionsPath = path15.join(memoryDir, "conventions.md");
2632
2789
  const entry = `
2633
2790
  ## Learned ${timestamp2} (task: ${ctx.taskId})
2634
2791
  ${learnings.join("\n")}
2635
2792
  `;
2636
- fs13.appendFileSync(conventionsPath, entry);
2637
- invalidateCache(conventionsPath, path13.join(memoryDir, ".tiers"));
2793
+ fs15.appendFileSync(conventionsPath, entry);
2794
+ invalidateCache(conventionsPath, path15.join(memoryDir, ".tiers"));
2638
2795
  logger.info(`Auto-learned ${learnings.length} convention(s)`);
2639
2796
  }
2640
2797
  autoLearnDecisions(ctx.taskDir, memoryDir, ctx.taskId, timestamp2);
@@ -2643,13 +2800,13 @@ ${learnings.join("\n")}
2643
2800
  }
2644
2801
  }
2645
2802
  function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
2646
- const archPath = path13.join(memoryDir, "architecture.md");
2647
- if (fs13.existsSync(archPath)) return;
2803
+ const archPath = path15.join(memoryDir, "architecture.md");
2804
+ if (fs15.existsSync(archPath)) return;
2648
2805
  const detected = [];
2649
- const pkgPath = path13.join(projectDir, "package.json");
2650
- if (fs13.existsSync(pkgPath)) {
2806
+ const pkgPath = path15.join(projectDir, "package.json");
2807
+ if (fs15.existsSync(pkgPath)) {
2651
2808
  try {
2652
- const pkg = JSON.parse(fs13.readFileSync(pkgPath, "utf-8"));
2809
+ const pkg = JSON.parse(fs15.readFileSync(pkgPath, "utf-8"));
2653
2810
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2654
2811
  if (allDeps.next) detected.push(`- Framework: Next.js ${allDeps.next}`);
2655
2812
  else if (allDeps.react) detected.push(`- Framework: React ${allDeps.react}`);
@@ -2665,15 +2822,15 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
2665
2822
  if (allDeps.payload || allDeps["@payloadcms/next"]) detected.push(`- CMS: Payload CMS`);
2666
2823
  if (pkg.type === "module") detected.push("- Module system: ESM");
2667
2824
  else detected.push("- Module system: CommonJS");
2668
- if (fs13.existsSync(path13.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
2669
- else if (fs13.existsSync(path13.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
2670
- else if (fs13.existsSync(path13.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
2825
+ if (fs15.existsSync(path15.join(projectDir, "pnpm-lock.yaml"))) detected.push("- Package manager: pnpm");
2826
+ else if (fs15.existsSync(path15.join(projectDir, "yarn.lock"))) detected.push("- Package manager: yarn");
2827
+ else if (fs15.existsSync(path15.join(projectDir, "package-lock.json"))) detected.push("- Package manager: npm");
2671
2828
  } catch {
2672
2829
  }
2673
2830
  }
2674
2831
  const topDirs = [];
2675
2832
  try {
2676
- const entries = fs13.readdirSync(projectDir, { withFileTypes: true });
2833
+ const entries = fs15.readdirSync(projectDir, { withFileTypes: true });
2677
2834
  for (const entry of entries) {
2678
2835
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
2679
2836
  topDirs.push(entry.name);
@@ -2682,10 +2839,10 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
2682
2839
  if (topDirs.length > 0) detected.push(`- Top-level directories: ${topDirs.join(", ")}`);
2683
2840
  } catch {
2684
2841
  }
2685
- const srcDir = path13.join(projectDir, "src");
2686
- if (fs13.existsSync(srcDir)) {
2842
+ const srcDir = path15.join(projectDir, "src");
2843
+ if (fs15.existsSync(srcDir)) {
2687
2844
  try {
2688
- const srcEntries = fs13.readdirSync(srcDir, { withFileTypes: true });
2845
+ const srcEntries = fs15.readdirSync(srcDir, { withFileTypes: true });
2689
2846
  const srcDirs = srcEntries.filter((e) => e.isDirectory()).map((e) => e.name);
2690
2847
  if (srcDirs.length > 0) detected.push(`- src/ structure: ${srcDirs.join(", ")}`);
2691
2848
  } catch {
@@ -2697,15 +2854,15 @@ function autoLearnArchitecture(projectDir, memoryDir, timestamp2) {
2697
2854
  ## Overview
2698
2855
  ${detected.join("\n")}
2699
2856
  `;
2700
- fs13.writeFileSync(archPath, content);
2701
- invalidateCache(archPath, path13.join(memoryDir, ".tiers"));
2857
+ fs15.writeFileSync(archPath, content);
2858
+ invalidateCache(archPath, path15.join(memoryDir, ".tiers"));
2702
2859
  logger.info(`Auto-detected architecture (${detected.length} items)`);
2703
2860
  }
2704
2861
  }
2705
2862
  function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
2706
- const reviewPath = path13.join(taskDir, "review.md");
2707
- if (!fs13.existsSync(reviewPath)) return;
2708
- const review = fs13.readFileSync(reviewPath, "utf-8");
2863
+ const reviewPath = path15.join(taskDir, "review.md");
2864
+ if (!fs15.existsSync(reviewPath)) return;
2865
+ const review = fs15.readFileSync(reviewPath, "utf-8");
2709
2866
  const decisions = [];
2710
2867
  const existingPatternRe = /(?:use|follow|reuse|match|adopt)\s+(?:the\s+)?existing\s+(.+?)(?:\.|$)/gim;
2711
2868
  for (const match of review.matchAll(existingPatternRe)) {
@@ -2724,10 +2881,10 @@ function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
2724
2881
  decisions.push(`- Avoid ${match[1].trim()} for ${match[2].trim()}`);
2725
2882
  }
2726
2883
  if (decisions.length === 0) return;
2727
- const decisionsPath = path13.join(memoryDir, "decisions.md");
2884
+ const decisionsPath = path15.join(memoryDir, "decisions.md");
2728
2885
  let existing = "";
2729
- if (fs13.existsSync(decisionsPath)) {
2730
- existing = fs13.readFileSync(decisionsPath, "utf-8");
2886
+ if (fs15.existsSync(decisionsPath)) {
2887
+ existing = fs15.readFileSync(decisionsPath, "utf-8");
2731
2888
  } else {
2732
2889
  existing = "# Architectural Decisions\n\nDecisions extracted from code reviews. The planning agent MUST follow these.\n";
2733
2890
  }
@@ -2737,8 +2894,8 @@ function autoLearnDecisions(taskDir, memoryDir, taskId, timestamp2) {
2737
2894
  ## From task ${taskId} (${timestamp2})
2738
2895
  ${newDecisions.join("\n")}
2739
2896
  `;
2740
- fs13.appendFileSync(decisionsPath, existing ? entry : existing + entry);
2741
- invalidateCache(decisionsPath, path13.join(memoryDir, ".tiers"));
2897
+ fs15.appendFileSync(decisionsPath, existing ? entry : existing + entry);
2898
+ invalidateCache(decisionsPath, path15.join(memoryDir, ".tiers"));
2742
2899
  logger.info(`Auto-learned ${newDecisions.length} architectural decision(s)`);
2743
2900
  }
2744
2901
  var init_auto_learn = __esm({
@@ -2750,13 +2907,13 @@ var init_auto_learn = __esm({
2750
2907
  });
2751
2908
 
2752
2909
  // src/retrospective.ts
2753
- import * as fs14 from "fs";
2754
- import * as path14 from "path";
2910
+ import * as fs16 from "fs";
2911
+ import * as path16 from "path";
2755
2912
  function readArtifact(taskDir, filename, maxChars) {
2756
- const p = path14.join(taskDir, filename);
2757
- if (!fs14.existsSync(p)) return null;
2913
+ const p = path16.join(taskDir, filename);
2914
+ if (!fs16.existsSync(p)) return null;
2758
2915
  try {
2759
- const content = fs14.readFileSync(p, "utf-8");
2916
+ const content = fs16.readFileSync(p, "utf-8");
2760
2917
  return content.length > maxChars ? content.slice(0, maxChars) + "\n...(truncated)" : content;
2761
2918
  } catch {
2762
2919
  return null;
@@ -2809,13 +2966,13 @@ function collectRunContext(ctx, state, pipelineStartTime) {
2809
2966
  return lines.join("\n");
2810
2967
  }
2811
2968
  function getLogPath(projectDir) {
2812
- return path14.join(projectDir, ".kody", "memory", "observer-log.jsonl");
2969
+ return path16.join(projectDir, ".kody", "memory", "observer-log.jsonl");
2813
2970
  }
2814
2971
  function readPreviousRetrospectives(projectDir, limit = 10) {
2815
2972
  const logPath = getLogPath(projectDir);
2816
- if (!fs14.existsSync(logPath)) return [];
2973
+ if (!fs16.existsSync(logPath)) return [];
2817
2974
  try {
2818
- const content = fs14.readFileSync(logPath, "utf-8");
2975
+ const content = fs16.readFileSync(logPath, "utf-8");
2819
2976
  const lines = content.split("\n").filter(Boolean);
2820
2977
  const entries = [];
2821
2978
  const start = Math.max(0, lines.length - limit);
@@ -2842,11 +2999,11 @@ function formatPreviousEntries(entries) {
2842
2999
  }
2843
3000
  function appendRetrospectiveEntry(projectDir, entry) {
2844
3001
  const logPath = getLogPath(projectDir);
2845
- const dir = path14.dirname(logPath);
2846
- if (!fs14.existsSync(dir)) {
2847
- fs14.mkdirSync(dir, { recursive: true });
3002
+ const dir = path16.dirname(logPath);
3003
+ if (!fs16.existsSync(dir)) {
3004
+ fs16.mkdirSync(dir, { recursive: true });
2848
3005
  }
2849
- fs14.appendFileSync(logPath, JSON.stringify(entry) + "\n");
3006
+ fs16.appendFileSync(logPath, JSON.stringify(entry) + "\n");
2850
3007
  }
2851
3008
  async function runRetrospective(ctx, state, pipelineStartTime) {
2852
3009
  if (ctx.input.dryRun) return;
@@ -2957,8 +3114,8 @@ If no pipeline flaw is detected, set "pipelineFlaw" to null.
2957
3114
  });
2958
3115
 
2959
3116
  // src/pipeline.ts
2960
- import * as fs15 from "fs";
2961
- import * as path15 from "path";
3117
+ import * as fs17 from "fs";
3118
+ import * as path17 from "path";
2962
3119
  function ensureFeatureBranchIfNeeded(ctx) {
2963
3120
  if (ctx.input.dryRun) return;
2964
3121
  if (ctx.input.prNumber) {
@@ -2971,8 +3128,8 @@ function ensureFeatureBranchIfNeeded(ctx) {
2971
3128
  }
2972
3129
  if (!ctx.input.issueNumber) return;
2973
3130
  try {
2974
- const taskMdPath = path15.join(ctx.taskDir, "task.md");
2975
- const title = fs15.existsSync(taskMdPath) ? fs15.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
3131
+ const taskMdPath = path17.join(ctx.taskDir, "task.md");
3132
+ const title = fs17.existsSync(taskMdPath) ? fs17.readFileSync(taskMdPath, "utf-8").split("\n")[0].slice(0, 50) : ctx.taskId;
2976
3133
  ensureFeatureBranch(ctx.input.issueNumber, title, ctx.projectDir);
2977
3134
  syncWithDefault(ctx.projectDir);
2978
3135
  } catch (err) {
@@ -2980,10 +3137,10 @@ function ensureFeatureBranchIfNeeded(ctx) {
2980
3137
  }
2981
3138
  }
2982
3139
  function acquireLock(taskDir) {
2983
- const lockPath = path15.join(taskDir, ".lock");
2984
- if (fs15.existsSync(lockPath)) {
3140
+ const lockPath = path17.join(taskDir, ".lock");
3141
+ if (fs17.existsSync(lockPath)) {
2985
3142
  try {
2986
- const pid = parseInt(fs15.readFileSync(lockPath, "utf-8").trim(), 10);
3143
+ const pid = parseInt(fs17.readFileSync(lockPath, "utf-8").trim(), 10);
2987
3144
  try {
2988
3145
  process.kill(pid, 0);
2989
3146
  throw new Error(`Pipeline already running (PID ${pid})`);
@@ -2994,11 +3151,11 @@ function acquireLock(taskDir) {
2994
3151
  if (e instanceof Error && e.message.startsWith("Pipeline already")) throw e;
2995
3152
  }
2996
3153
  }
2997
- fs15.writeFileSync(lockPath, String(process.pid));
3154
+ fs17.writeFileSync(lockPath, String(process.pid));
2998
3155
  }
2999
3156
  function releaseLock(taskDir) {
3000
3157
  try {
3001
- fs15.unlinkSync(path15.join(taskDir, ".lock"));
3158
+ fs17.unlinkSync(path17.join(taskDir, ".lock"));
3002
3159
  } catch {
3003
3160
  }
3004
3161
  }
@@ -3166,8 +3323,8 @@ var init_pipeline = __esm({
3166
3323
  });
3167
3324
 
3168
3325
  // src/preflight.ts
3169
- import { execFileSync as execFileSync8 } from "child_process";
3170
- import * as fs16 from "fs";
3326
+ import { execFileSync as execFileSync9 } from "child_process";
3327
+ import * as fs18 from "fs";
3171
3328
  function check(name, fn) {
3172
3329
  try {
3173
3330
  const detail = fn() ?? void 0;
@@ -3179,7 +3336,7 @@ function check(name, fn) {
3179
3336
  function runPreflight() {
3180
3337
  const checks = [
3181
3338
  check("claude CLI", () => {
3182
- const v = execFileSync8("claude", ["--version"], {
3339
+ const v = execFileSync9("claude", ["--version"], {
3183
3340
  encoding: "utf-8",
3184
3341
  timeout: 1e4,
3185
3342
  stdio: ["pipe", "pipe", "pipe"]
@@ -3187,14 +3344,14 @@ function runPreflight() {
3187
3344
  return v;
3188
3345
  }),
3189
3346
  check("git repo", () => {
3190
- execFileSync8("git", ["rev-parse", "--is-inside-work-tree"], {
3347
+ execFileSync9("git", ["rev-parse", "--is-inside-work-tree"], {
3191
3348
  encoding: "utf-8",
3192
3349
  timeout: 5e3,
3193
3350
  stdio: ["pipe", "pipe", "pipe"]
3194
3351
  });
3195
3352
  }),
3196
3353
  check("pnpm", () => {
3197
- const v = execFileSync8("pnpm", ["--version"], {
3354
+ const v = execFileSync9("pnpm", ["--version"], {
3198
3355
  encoding: "utf-8",
3199
3356
  timeout: 5e3,
3200
3357
  stdio: ["pipe", "pipe", "pipe"]
@@ -3202,7 +3359,7 @@ function runPreflight() {
3202
3359
  return v;
3203
3360
  }),
3204
3361
  check("node >= 18", () => {
3205
- const v = execFileSync8("node", ["--version"], {
3362
+ const v = execFileSync9("node", ["--version"], {
3206
3363
  encoding: "utf-8",
3207
3364
  timeout: 5e3,
3208
3365
  stdio: ["pipe", "pipe", "pipe"]
@@ -3212,7 +3369,7 @@ function runPreflight() {
3212
3369
  return v;
3213
3370
  }),
3214
3371
  check("gh CLI", () => {
3215
- const v = execFileSync8("gh", ["--version"], {
3372
+ const v = execFileSync9("gh", ["--version"], {
3216
3373
  encoding: "utf-8",
3217
3374
  timeout: 5e3,
3218
3375
  stdio: ["pipe", "pipe", "pipe"]
@@ -3220,7 +3377,7 @@ function runPreflight() {
3220
3377
  return v;
3221
3378
  }),
3222
3379
  check("package.json", () => {
3223
- if (!fs16.existsSync("package.json")) throw new Error("not found");
3380
+ if (!fs18.existsSync("package.json")) throw new Error("not found");
3224
3381
  })
3225
3382
  ];
3226
3383
  const failed = checks.filter((c) => !c.ok);
@@ -3240,140 +3397,6 @@ var init_preflight = __esm({
3240
3397
  }
3241
3398
  });
3242
3399
 
3243
- // src/cli/task-resolution.ts
3244
- import * as fs17 from "fs";
3245
- import * as path16 from "path";
3246
- import { execFileSync as execFileSync9 } from "child_process";
3247
- function findLatestTaskForIssue(issueNumber, projectDir) {
3248
- const tasksDir = path16.join(projectDir, ".kody", "tasks");
3249
- if (!fs17.existsSync(tasksDir)) return null;
3250
- const allDirs = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
3251
- const prefix = `${issueNumber}-`;
3252
- const direct = allDirs.find((d) => d.startsWith(prefix));
3253
- if (direct) return direct;
3254
- try {
3255
- const branch = execFileSync9("git", ["branch", "--show-current"], {
3256
- encoding: "utf-8",
3257
- cwd: projectDir,
3258
- timeout: 5e3,
3259
- stdio: ["pipe", "pipe", "pipe"]
3260
- }).trim();
3261
- const branchIssueMatch = branch.match(/^(\d+)-/);
3262
- if (branchIssueMatch) {
3263
- const branchIssueNum = branchIssueMatch[1];
3264
- const branchPrefix = `${branchIssueNum}-`;
3265
- const fromBranch = allDirs.find((d) => d.startsWith(branchPrefix));
3266
- if (fromBranch) return fromBranch;
3267
- }
3268
- } catch {
3269
- }
3270
- return null;
3271
- }
3272
- function generateTaskId() {
3273
- const now = /* @__PURE__ */ new Date();
3274
- const pad = (n) => String(n).padStart(2, "0");
3275
- return `${String(now.getFullYear()).slice(2)}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
3276
- }
3277
- var init_task_resolution = __esm({
3278
- "src/cli/task-resolution.ts"() {
3279
- "use strict";
3280
- }
3281
- });
3282
-
3283
- // src/review-standalone.ts
3284
- import * as fs18 from "fs";
3285
- import * as path17 from "path";
3286
- function resolveReviewTarget(input) {
3287
- if (input.prs.length === 0) {
3288
- return {
3289
- action: "none",
3290
- message: `Issue #${input.issueNumber} has no open PRs. Nothing to review.`
3291
- };
3292
- }
3293
- if (input.prs.length === 1) {
3294
- return { action: "review", prNumber: input.prs[0].number };
3295
- }
3296
- const prList = input.prs.map((pr) => ` - #${pr.number}: ${pr.title}`).join("\n");
3297
- return {
3298
- action: "pick",
3299
- prs: input.prs,
3300
- message: `\u26A0\uFE0F Issue #${input.issueNumber} has ${input.prs.length} open PRs:
3301
- ${prList}
3302
-
3303
- Run: \`pnpm kody review --pr-number <n>\`
3304
- Or comment on the specific PR: \`@kody review\``
3305
- };
3306
- }
3307
- async function runStandaloneReview(input) {
3308
- const taskId = input.taskId ?? `review-${generateTaskId()}`;
3309
- const taskDir = path17.join(input.projectDir, ".kody", "tasks", taskId);
3310
- fs18.mkdirSync(taskDir, { recursive: true });
3311
- const taskContent = `# ${input.prTitle}
3312
-
3313
- ${input.prBody ?? ""}`;
3314
- fs18.writeFileSync(path17.join(taskDir, "task.md"), taskContent);
3315
- const reviewDef = STAGES.find((s) => s.name === "review");
3316
- const ctx = {
3317
- taskId,
3318
- taskDir,
3319
- projectDir: input.projectDir,
3320
- runners: input.runners,
3321
- sessions: {},
3322
- input: {
3323
- mode: "full",
3324
- local: input.local
3325
- }
3326
- };
3327
- logger.info(`[review] standalone review for: ${input.prTitle}`);
3328
- const result = await executeAgentStage(ctx, reviewDef);
3329
- if (result.outcome !== "completed") {
3330
- return {
3331
- outcome: "failed",
3332
- taskDir,
3333
- error: result.error ?? "Review stage failed"
3334
- };
3335
- }
3336
- const reviewPath = path17.join(taskDir, "review.md");
3337
- let reviewContent;
3338
- if (fs18.existsSync(reviewPath)) {
3339
- reviewContent = fs18.readFileSync(reviewPath, "utf-8");
3340
- }
3341
- return {
3342
- outcome: "completed",
3343
- reviewContent,
3344
- taskDir
3345
- };
3346
- }
3347
- function detectReviewVerdict(reviewContent) {
3348
- const verdictMatch = reviewContent.match(/##\s*Verdict:\s*(PASS|FAIL)/i);
3349
- if (verdictMatch) {
3350
- return verdictMatch[1].toLowerCase();
3351
- }
3352
- const hasCritical = /###\s*Critical\s*\n(?!None\.)/i.test(reviewContent);
3353
- const hasMajor = /###\s*Major\s*\n(?!None\.)/i.test(reviewContent);
3354
- if (hasCritical || hasMajor) return "fail";
3355
- return "pass";
3356
- }
3357
- function formatReviewComment(reviewContent, taskId) {
3358
- const verdict = detectReviewVerdict(reviewContent);
3359
- const cta = verdict === "fail" ? "\n\n> To fix these issues, comment: `@kody fix`\n> The review findings will be used automatically as context." : "";
3360
- return `## \u{1F50D} Kody Review (\`${taskId}\`)
3361
-
3362
- ${reviewContent}${cta}
3363
-
3364
- ---
3365
- \u{1F916} Generated by Kody`;
3366
- }
3367
- var init_review_standalone = __esm({
3368
- "src/review-standalone.ts"() {
3369
- "use strict";
3370
- init_definitions();
3371
- init_agent();
3372
- init_task_resolution();
3373
- init_logger();
3374
- }
3375
- });
3376
-
3377
3400
  // src/cli/args.ts
3378
3401
  function getArg(args2, flag) {
3379
3402
  const idx = args2.indexOf(flag);
@@ -3938,12 +3961,11 @@ async function main() {
3938
3961
  console.log(result.reviewContent);
3939
3962
  if (!input.local && prNumber) {
3940
3963
  const comment = formatReviewComment(result.reviewContent, taskId);
3941
- postPRComment(prNumber, comment);
3942
3964
  const verdict = detectReviewVerdict(result.reviewContent);
3943
- if (verdict === "fail") {
3944
- submitPRReview(prNumber, comment, "request-changes");
3945
- } else {
3946
- submitPRReview(prNumber, comment, "approve");
3965
+ const event = verdict === "fail" ? "request-changes" : "approve";
3966
+ const posted = submitPRReview(prNumber, comment, event);
3967
+ if (!posted) {
3968
+ postPRComment(prNumber, comment);
3947
3969
  }
3948
3970
  }
3949
3971
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine-lite",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
4
4
  "description": "Autonomous SDLC pipeline: Kody orchestration + Claude Code + LiteLLM",
5
5
  "license": "MIT",
6
6
  "type": "module",