@kody-ade/kody-engine 0.2.4 → 0.2.6

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.
package/dist/bin/kody2.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.2.4",
6
+ version: "0.2.6",
7
7
  description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -81,7 +81,7 @@ function loadConfig(projectDir = process.cwd()) {
81
81
  throw new Error(`kody.config.json is invalid JSON: ${msg}`);
82
82
  }
83
83
  const quality = raw.quality ?? {};
84
- const git3 = raw.git ?? {};
84
+ const git4 = raw.git ?? {};
85
85
  const github = raw.github ?? {};
86
86
  const agent = raw.agent ?? {};
87
87
  if (!agent.model || typeof agent.model !== "string") {
@@ -97,7 +97,7 @@ function loadConfig(projectDir = process.cwd()) {
97
97
  testUnit: typeof quality.testUnit === "string" ? quality.testUnit : ""
98
98
  },
99
99
  git: {
100
- defaultBranch: typeof git3.defaultBranch === "string" ? git3.defaultBranch : "main"
100
+ defaultBranch: typeof git4.defaultBranch === "string" ? git4.defaultBranch : "main"
101
101
  },
102
102
  github: {
103
103
  owner: String(github.owner),
@@ -107,9 +107,23 @@ function loadConfig(projectDir = process.cwd()) {
107
107
  model: String(agent.model)
108
108
  },
109
109
  issueContext: parseIssueContext(raw.issueContext),
110
- testRequirements: parseTestRequirements(raw.testRequirements)
110
+ testRequirements: parseTestRequirements(raw.testRequirements),
111
+ release: parseReleaseConfig(raw.release)
111
112
  };
112
113
  }
114
+ function parseReleaseConfig(raw) {
115
+ if (!raw || typeof raw !== "object") return void 0;
116
+ const r = raw;
117
+ const out = {};
118
+ if (Array.isArray(r.versionFiles)) out.versionFiles = r.versionFiles.filter((f) => typeof f === "string");
119
+ if (typeof r.publishCommand === "string") out.publishCommand = r.publishCommand;
120
+ if (typeof r.notifyCommand === "string") out.notifyCommand = r.notifyCommand;
121
+ if (typeof r.e2eCommand === "string") out.e2eCommand = r.e2eCommand;
122
+ if (typeof r.draftRelease === "boolean") out.draftRelease = r.draftRelease;
123
+ if (typeof r.releaseBranch === "string") out.releaseBranch = r.releaseBranch;
124
+ if (typeof r.timeoutMs === "number" && r.timeoutMs > 0) out.timeoutMs = Math.floor(r.timeoutMs);
125
+ return Object.keys(out).length > 0 ? out : void 0;
126
+ }
113
127
  function parseIssueContext(raw) {
114
128
  if (!raw || typeof raw !== "object") return void 0;
115
129
  const r = raw;
@@ -137,8 +151,8 @@ function getAnthropicApiKeyOrDummy() {
137
151
  }
138
152
 
139
153
  // src/executor.ts
140
- import * as fs11 from "fs";
141
- import * as path9 from "path";
154
+ import * as fs12 from "fs";
155
+ import * as path10 from "path";
142
156
 
143
157
  // src/agent.ts
144
158
  import * as fs2 from "fs";
@@ -1898,8 +1912,359 @@ function postWith(type, n, body, cwd) {
1898
1912
  }
1899
1913
  }
1900
1914
 
1915
+ // src/scripts/postReviewResult.ts
1916
+ function detectVerdict(body) {
1917
+ const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
1918
+ if (!m) return "UNKNOWN";
1919
+ return m[1].toUpperCase();
1920
+ }
1921
+ var postReviewResult = async (ctx, _profile, agentResult) => {
1922
+ const prNumber = ctx.data.commentTargetNumber;
1923
+ if (!prNumber) {
1924
+ ctx.output.exitCode = 99;
1925
+ ctx.output.reason = "review postflight: no PR number in context";
1926
+ return;
1927
+ }
1928
+ if (!agentResult || agentResult.outcome !== "completed") {
1929
+ const reason = agentResult?.error ?? "agent did not complete";
1930
+ try {
1931
+ postPrReviewComment(prNumber, `\u26A0\uFE0F kody2 review FAILED: ${truncate2(reason, 1e3)}`, ctx.cwd);
1932
+ } catch {
1933
+ }
1934
+ ctx.output.exitCode = 1;
1935
+ ctx.output.reason = reason;
1936
+ return;
1937
+ }
1938
+ const reviewBody = agentResult.finalText.trim();
1939
+ if (!reviewBody) {
1940
+ try {
1941
+ postPrReviewComment(prNumber, `\u26A0\uFE0F kody2 review FAILED: agent produced no review body`, ctx.cwd);
1942
+ } catch {
1943
+ }
1944
+ ctx.output.exitCode = 1;
1945
+ ctx.output.reason = "empty review body";
1946
+ return;
1947
+ }
1948
+ try {
1949
+ postPrReviewComment(prNumber, reviewBody, ctx.cwd);
1950
+ } catch (err) {
1951
+ const msg = err instanceof Error ? err.message : String(err);
1952
+ ctx.output.exitCode = 4;
1953
+ ctx.output.reason = `failed to post review comment: ${msg}`;
1954
+ return;
1955
+ }
1956
+ const verdict = detectVerdict(reviewBody);
1957
+ ctx.data.reviewVerdict = verdict;
1958
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
1959
+ process.stdout.write(`
1960
+ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/pull/${prNumber} (verdict: ${verdict})
1961
+ `);
1962
+ };
1963
+
1964
+ // src/scripts/releaseFlow.ts
1965
+ import { execFileSync as execFileSync10, spawnSync } from "child_process";
1966
+ import * as fs10 from "fs";
1967
+ import * as path9 from "path";
1968
+ function bumpVersion(current, bump) {
1969
+ const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
1970
+ if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
1971
+ let [major, minor, patch] = [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
1972
+ if (bump === "major") {
1973
+ major++;
1974
+ minor = 0;
1975
+ patch = 0;
1976
+ } else if (bump === "minor") {
1977
+ minor++;
1978
+ patch = 0;
1979
+ } else patch++;
1980
+ return `${major}.${minor}.${patch}`;
1981
+ }
1982
+ function updateVersionInFile(file, newVersion, cwd) {
1983
+ const abs = path9.join(cwd, file);
1984
+ if (!fs10.existsSync(abs)) return false;
1985
+ const content = fs10.readFileSync(abs, "utf-8");
1986
+ const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
1987
+ if (updated === content) return false;
1988
+ fs10.writeFileSync(abs, updated);
1989
+ return true;
1990
+ }
1991
+ function generateChangelog(cwd, newVersion, lastTag) {
1992
+ const range = lastTag ? `${lastTag}..HEAD` : "HEAD";
1993
+ let log = "";
1994
+ try {
1995
+ log = execFileSync10("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
1996
+ cwd,
1997
+ encoding: "utf-8",
1998
+ stdio: ["ignore", "pipe", "pipe"]
1999
+ }).trim();
2000
+ } catch {
2001
+ }
2002
+ const commits = log.split("\n").filter((l) => l.length > 0).map((line) => {
2003
+ const [subject, sha] = line.split("||");
2004
+ return { subject: subject ?? "", sha: sha ?? "" };
2005
+ }).filter((c) => !/^chore:\s*release\s+v\d/i.test(c.subject));
2006
+ const groups = { feat: [], fix: [], perf: [], refactor: [], docs: [], chore: [], other: [] };
2007
+ for (const c of commits) {
2008
+ const m = c.subject.match(/^(\w+)(?:\(.*?\))?\s*:\s*(.+)$/);
2009
+ const type = m?.[1]?.toLowerCase() ?? "other";
2010
+ const msg = m?.[2] ?? c.subject;
2011
+ const bucket = groups[type] ?? groups.other;
2012
+ bucket.push(`- ${msg} (${c.sha})`);
2013
+ }
2014
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2015
+ const parts = [`## v${newVersion} \u2014 ${date}`, ""];
2016
+ const labels = [
2017
+ ["feat", "Features"],
2018
+ ["fix", "Fixes"],
2019
+ ["perf", "Performance"],
2020
+ ["refactor", "Refactoring"],
2021
+ ["docs", "Docs"],
2022
+ ["chore", "Chores"],
2023
+ ["other", "Other"]
2024
+ ];
2025
+ for (const [key, label] of labels) {
2026
+ const items = groups[key];
2027
+ if (!items || items.length === 0) continue;
2028
+ parts.push(`### ${label}`);
2029
+ parts.push(...items);
2030
+ parts.push("");
2031
+ }
2032
+ if (parts.length === 2) parts.push("_No notable commits since the last release._", "");
2033
+ return parts.join("\n");
2034
+ }
2035
+ function prependChangelog(cwd, entry) {
2036
+ const p = path9.join(cwd, "CHANGELOG.md");
2037
+ const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
2038
+ if (fs10.existsSync(p)) {
2039
+ const prior = fs10.readFileSync(p, "utf-8");
2040
+ if (/^#\s*Changelog\b/m.test(prior)) {
2041
+ const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
2042
+ fs10.writeFileSync(p, `${prior.slice(0, idx + 1)}
2043
+ ${entry}${prior.slice(idx + 1)}`);
2044
+ } else {
2045
+ fs10.writeFileSync(p, `${header}${entry}${prior}`);
2046
+ }
2047
+ } else {
2048
+ fs10.writeFileSync(p, `${header}${entry}`);
2049
+ }
2050
+ }
2051
+ function git3(args, cwd, timeout = 6e4) {
2052
+ return execFileSync10("git", args, {
2053
+ encoding: "utf-8",
2054
+ timeout,
2055
+ cwd,
2056
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2057
+ stdio: ["pipe", "pipe", "pipe"]
2058
+ }).trim();
2059
+ }
2060
+ function lastReleaseTag(cwd) {
2061
+ try {
2062
+ return git3(["describe", "--tags", "--abbrev=0", "--match", "v*"], cwd);
2063
+ } catch {
2064
+ return null;
2065
+ }
2066
+ }
2067
+ function runShell(cmd, cwd, timeoutMs) {
2068
+ const r = spawnSync(cmd, {
2069
+ cwd,
2070
+ shell: true,
2071
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
2072
+ encoding: "utf-8",
2073
+ timeout: timeoutMs
2074
+ });
2075
+ return { exitCode: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
2076
+ }
2077
+ var releaseFlow = async (ctx) => {
2078
+ const mode = ctx.args.mode ?? "prepare";
2079
+ const bump = ctx.args.bump ?? "patch";
2080
+ const dryRun = ctx.args["dry-run"] === true || ctx.args.dryRun === true;
2081
+ const cwd = ctx.cwd;
2082
+ const releaseCfg = ctx.config.release ?? {};
2083
+ const versionFiles = releaseCfg.versionFiles && releaseCfg.versionFiles.length > 0 ? releaseCfg.versionFiles : ["package.json"];
2084
+ const timeoutMs = releaseCfg.timeoutMs ?? 6e5;
2085
+ ctx.skipAgent = true;
2086
+ if (mode === "prepare") {
2087
+ await runPrepare({ cwd, bump, dryRun, versionFiles, ctx });
2088
+ return;
2089
+ }
2090
+ if (mode === "finalize") {
2091
+ await runFinalize({ cwd, dryRun, timeoutMs, releaseCfg, ctx });
2092
+ return;
2093
+ }
2094
+ ctx.output.exitCode = 64;
2095
+ ctx.output.reason = `release: unknown mode '${mode}'`;
2096
+ };
2097
+ async function runPrepare(args) {
2098
+ const { cwd, bump, dryRun, versionFiles, ctx } = args;
2099
+ const pkgPath = path9.join(cwd, "package.json");
2100
+ if (!fs10.existsSync(pkgPath)) {
2101
+ ctx.output.exitCode = 99;
2102
+ ctx.output.reason = "release prepare: package.json not found";
2103
+ return;
2104
+ }
2105
+ const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
2106
+ if (typeof pkg.version !== "string") {
2107
+ ctx.output.exitCode = 99;
2108
+ ctx.output.reason = "release prepare: package.json has no version";
2109
+ return;
2110
+ }
2111
+ const oldVersion = pkg.version;
2112
+ const newVersion = bumpVersion(oldVersion, bump);
2113
+ const tag = `v${newVersion}`;
2114
+ process.stdout.write(`\u2192 release prepare: ${oldVersion} \u2192 ${newVersion} (${bump})
2115
+ `);
2116
+ if (dryRun) {
2117
+ ctx.output.exitCode = 0;
2118
+ ctx.output.reason = `dry-run \u2014 would bump to ${newVersion}`;
2119
+ process.stdout.write(`RELEASE_PLAN=bump=${newVersion} tag=${tag}
2120
+ `);
2121
+ return;
2122
+ }
2123
+ const touched = [];
2124
+ for (const f of versionFiles) {
2125
+ if (updateVersionInFile(f, newVersion, cwd)) touched.push(f);
2126
+ }
2127
+ if (touched.length === 0) {
2128
+ ctx.output.exitCode = 1;
2129
+ ctx.output.reason = `release prepare: no version strings updated (files: ${versionFiles.join(", ")})`;
2130
+ return;
2131
+ }
2132
+ process.stdout.write(` wrote ${touched.join(", ")}
2133
+ `);
2134
+ const entry = generateChangelog(cwd, newVersion, lastReleaseTag(cwd));
2135
+ prependChangelog(cwd, entry);
2136
+ process.stdout.write(` wrote CHANGELOG.md
2137
+ `);
2138
+ const releaseBranch = `release/${tag}`;
2139
+ try {
2140
+ git3(["checkout", "-b", releaseBranch], cwd);
2141
+ for (const f of [...touched, "CHANGELOG.md"]) git3(["add", "--", f], cwd);
2142
+ git3(["commit", "--no-gpg-sign", "-m", `chore: release ${tag}`], cwd);
2143
+ git3(["push", "-u", "origin", releaseBranch], cwd, 12e4);
2144
+ } catch (err) {
2145
+ const msg = err instanceof Error ? err.message : String(err);
2146
+ ctx.output.exitCode = 4;
2147
+ ctx.output.reason = `release prepare: git commit/push failed: ${msg}`;
2148
+ return;
2149
+ }
2150
+ const base = ctx.config.git.defaultBranch;
2151
+ const title = `chore: release ${tag}`;
2152
+ const body = `Automated release PR opened by kody2.
2153
+
2154
+ ${entry}
2155
+
2156
+ Merge this and then run \`kody2 release --mode finalize\`.`;
2157
+ let prUrl = "";
2158
+ try {
2159
+ prUrl = gh(
2160
+ ["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"],
2161
+ { input: body, cwd }
2162
+ ).trim();
2163
+ } catch (err) {
2164
+ const msg = err instanceof Error ? err.message : String(err);
2165
+ ctx.output.exitCode = 4;
2166
+ ctx.output.reason = `release prepare: gh pr create failed: ${msg}`;
2167
+ return;
2168
+ }
2169
+ ctx.output.prUrl = prUrl;
2170
+ ctx.output.exitCode = 0;
2171
+ process.stdout.write(`RELEASE_PR=${prUrl}
2172
+ `);
2173
+ }
2174
+ async function runFinalize(args) {
2175
+ const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
2176
+ const pkgPath = path9.join(cwd, "package.json");
2177
+ const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
2178
+ if (typeof pkg.version !== "string") {
2179
+ ctx.output.exitCode = 99;
2180
+ ctx.output.reason = "release finalize: package.json has no version";
2181
+ return;
2182
+ }
2183
+ const version = pkg.version;
2184
+ const tag = `v${version}`;
2185
+ process.stdout.write(`\u2192 release finalize: ${tag}
2186
+ `);
2187
+ try {
2188
+ git3(["rev-parse", "--verify", tag], cwd);
2189
+ ctx.output.exitCode = 1;
2190
+ ctx.output.reason = `release finalize: tag ${tag} already exists`;
2191
+ return;
2192
+ } catch {
2193
+ }
2194
+ if (dryRun) {
2195
+ ctx.output.exitCode = 0;
2196
+ ctx.output.reason = `dry-run \u2014 would tag + publish ${tag}`;
2197
+ return;
2198
+ }
2199
+ if (releaseCfg.e2eCommand && releaseCfg.e2eCommand.trim().length > 0) {
2200
+ const cmd = releaseCfg.e2eCommand.replace(/\$VERSION/g, version);
2201
+ process.stdout.write(` E2E gate: ${cmd}
2202
+ `);
2203
+ const r = runShell(cmd, cwd, timeoutMs);
2204
+ if (r.exitCode !== 0) {
2205
+ ctx.output.exitCode = 2;
2206
+ ctx.output.reason = `release finalize: E2E gate failed (exit ${r.exitCode}): ${truncate2(r.stderr, 600)}`;
2207
+ return;
2208
+ }
2209
+ }
2210
+ try {
2211
+ git3(["tag", "-a", tag, "-m", `Release ${tag}`], cwd);
2212
+ git3(["push", "origin", tag], cwd, 12e4);
2213
+ } catch (err) {
2214
+ const msg = err instanceof Error ? err.message : String(err);
2215
+ ctx.output.exitCode = 4;
2216
+ ctx.output.reason = `release finalize: tag/push failed: ${msg}`;
2217
+ return;
2218
+ }
2219
+ let publishStatus = "skipped";
2220
+ if (releaseCfg.publishCommand && releaseCfg.publishCommand.trim().length > 0) {
2221
+ const cmd = releaseCfg.publishCommand.replace(/\$VERSION/g, version);
2222
+ process.stdout.write(` publish: ${cmd}
2223
+ `);
2224
+ const r = runShell(cmd, cwd, timeoutMs);
2225
+ publishStatus = r.exitCode === 0 ? "ok" : "failed";
2226
+ if (r.exitCode !== 0) {
2227
+ process.stderr.write(`[kody2 release] publishCommand exit ${r.exitCode}
2228
+ ${truncate2(r.stderr, 2e3)}
2229
+ `);
2230
+ }
2231
+ }
2232
+ let releaseUrl = "";
2233
+ try {
2234
+ const releaseArgs = [
2235
+ "release",
2236
+ "create",
2237
+ tag,
2238
+ "--title",
2239
+ tag,
2240
+ "--notes",
2241
+ `Release ${tag} \u2014 automated by kody2.`
2242
+ ];
2243
+ if (releaseCfg.draftRelease) releaseArgs.push("--draft");
2244
+ releaseUrl = gh(releaseArgs, { cwd }).trim();
2245
+ } catch (err) {
2246
+ process.stderr.write(`[kody2 release] gh release create failed: ${err instanceof Error ? err.message : String(err)}
2247
+ `);
2248
+ }
2249
+ if (releaseCfg.notifyCommand && releaseCfg.notifyCommand.trim().length > 0) {
2250
+ const cmd = releaseCfg.notifyCommand.replace(/\$VERSION/g, version);
2251
+ runShell(cmd, cwd, timeoutMs);
2252
+ }
2253
+ if (releaseUrl) ctx.output.prUrl = releaseUrl;
2254
+ if (publishStatus === "failed") {
2255
+ ctx.output.exitCode = 1;
2256
+ ctx.output.reason = `release finalize: tag + gh release created, but publishCommand failed`;
2257
+ return;
2258
+ }
2259
+ ctx.output.exitCode = 0;
2260
+ process.stdout.write(`RELEASE_TAG=${tag}
2261
+ `);
2262
+ if (releaseUrl) process.stdout.write(`RELEASE_URL=${releaseUrl}
2263
+ `);
2264
+ }
2265
+
1901
2266
  // src/scripts/resolveFlow.ts
1902
- import { execFileSync as execFileSync10 } from "child_process";
2267
+ import { execFileSync as execFileSync11 } from "child_process";
1903
2268
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
1904
2269
  var resolveFlow = async (ctx) => {
1905
2270
  const prNumber = ctx.args.pr;
@@ -1951,7 +2316,7 @@ var resolveFlow = async (ctx) => {
1951
2316
  };
1952
2317
  function getConflictedFiles(cwd) {
1953
2318
  try {
1954
- const out = execFileSync10("git", ["diff", "--name-only", "--diff-filter=U"], {
2319
+ const out = execFileSync11("git", ["diff", "--name-only", "--diff-filter=U"], {
1955
2320
  encoding: "utf-8",
1956
2321
  cwd,
1957
2322
  env: { ...process.env, HUSKY: "0" }
@@ -1966,7 +2331,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
1966
2331
  let total = 0;
1967
2332
  for (const f of files) {
1968
2333
  try {
1969
- const content = execFileSync10("cat", [f], { encoding: "utf-8", cwd }).toString();
2334
+ const content = execFileSync11("cat", [f], { encoding: "utf-8", cwd }).toString();
1970
2335
  const snippet = `### ${f}
1971
2336
 
1972
2337
  \`\`\`
@@ -1988,6 +2353,33 @@ function tryPostPr3(prNumber, body, cwd) {
1988
2353
  }
1989
2354
  }
1990
2355
 
2356
+ // src/scripts/reviewFlow.ts
2357
+ var reviewFlow = async (ctx) => {
2358
+ const prNumber = ctx.args.pr;
2359
+ const pr = getPr(prNumber, ctx.cwd);
2360
+ if (pr.state !== "OPEN") {
2361
+ ctx.output.exitCode = 1;
2362
+ ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
2363
+ ctx.skipAgent = true;
2364
+ return;
2365
+ }
2366
+ ctx.data.pr = pr;
2367
+ ctx.data.commentTargetType = "pr";
2368
+ ctx.data.commentTargetNumber = prNumber;
2369
+ checkoutPrBranch(prNumber, ctx.cwd);
2370
+ ctx.data.branch = getCurrentBranch(ctx.cwd);
2371
+ ctx.data.prDiff = getPrDiff(prNumber, ctx.cwd);
2372
+ const runUrl = getRunUrl();
2373
+ const runSuffix = runUrl ? `, run ${runUrl}` : "";
2374
+ tryPostPr4(prNumber, `\u{1F440} kody2 review started on PR #${prNumber}${runSuffix}`, ctx.cwd);
2375
+ };
2376
+ function tryPostPr4(prNumber, body, cwd) {
2377
+ try {
2378
+ postPrReviewComment(prNumber, body, cwd);
2379
+ } catch {
2380
+ }
2381
+ }
2382
+
1991
2383
  // src/scripts/runFlow.ts
1992
2384
  var runFlow = async (ctx) => {
1993
2385
  const issueNumber = ctx.args.issue;
@@ -2104,7 +2496,7 @@ var verify = async (ctx) => {
2104
2496
  };
2105
2497
 
2106
2498
  // src/scripts/writeRunSummary.ts
2107
- import * as fs10 from "fs";
2499
+ import * as fs11 from "fs";
2108
2500
  var writeRunSummary = async (ctx) => {
2109
2501
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
2110
2502
  if (!summaryPath) return;
@@ -2126,7 +2518,7 @@ var writeRunSummary = async (ctx) => {
2126
2518
  if (reason) lines.push(`- **Reason:** ${reason}`);
2127
2519
  lines.push("");
2128
2520
  try {
2129
- fs10.appendFileSync(summaryPath, `${lines.join("\n")}
2521
+ fs11.appendFileSync(summaryPath, `${lines.join("\n")}
2130
2522
  `);
2131
2523
  } catch {
2132
2524
  }
@@ -2138,7 +2530,9 @@ var preflightScripts = {
2138
2530
  fixFlow,
2139
2531
  fixCiFlow,
2140
2532
  resolveFlow,
2533
+ reviewFlow,
2141
2534
  initFlow,
2535
+ releaseFlow,
2142
2536
  loadConventions,
2143
2537
  loadCoverageRules,
2144
2538
  composePrompt
@@ -2150,6 +2544,7 @@ var postflightScripts = {
2150
2544
  commitAndPush: commitAndPush2,
2151
2545
  ensurePr: ensurePr2,
2152
2546
  postIssueComment: postIssueComment2,
2547
+ postReviewResult,
2153
2548
  writeRunSummary
2154
2549
  };
2155
2550
  var allScriptNames = /* @__PURE__ */ new Set([
@@ -2158,7 +2553,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
2158
2553
  ]);
2159
2554
 
2160
2555
  // src/tools.ts
2161
- import { execFileSync as execFileSync11 } from "child_process";
2556
+ import { execFileSync as execFileSync12 } from "child_process";
2162
2557
  function verifyCliTools(tools, cwd) {
2163
2558
  const out = [];
2164
2559
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -2174,24 +2569,24 @@ function firstRequiredFailure(results, tools) {
2174
2569
  }
2175
2570
  function verifyOne(tool, cwd) {
2176
2571
  const result = { name: tool.name, present: false, verified: false };
2177
- let present = runShell(tool.install.checkCommand, cwd);
2572
+ let present = runShell2(tool.install.checkCommand, cwd);
2178
2573
  if (!present && tool.install.installCommand) {
2179
- runShell(tool.install.installCommand, cwd, 12e4);
2180
- present = runShell(tool.install.checkCommand, cwd);
2574
+ runShell2(tool.install.installCommand, cwd, 12e4);
2575
+ present = runShell2(tool.install.checkCommand, cwd);
2181
2576
  }
2182
2577
  result.present = present;
2183
2578
  if (!present) {
2184
2579
  result.error = `tool "${tool.name}" not on PATH (check: ${tool.install.checkCommand})`;
2185
2580
  return result;
2186
2581
  }
2187
- const verified = runShell(tool.verify, cwd);
2582
+ const verified = runShell2(tool.verify, cwd);
2188
2583
  result.verified = verified;
2189
2584
  if (!verified) result.error = `tool "${tool.name}" failed verify: ${tool.verify}`;
2190
2585
  return result;
2191
2586
  }
2192
- function runShell(cmd, cwd, timeoutMs = 3e4) {
2587
+ function runShell2(cmd, cwd, timeoutMs = 3e4) {
2193
2588
  try {
2194
- execFileSync11("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
2589
+ execFileSync12("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
2195
2590
  return true;
2196
2591
  } catch {
2197
2592
  return false;
@@ -2242,7 +2637,7 @@ async function runExecutable(profileName, input) {
2242
2637
  data: {},
2243
2638
  output: { exitCode: 0 }
2244
2639
  };
2245
- const ndjsonDir = path9.join(input.cwd, ".kody2");
2640
+ const ndjsonDir = path10.join(input.cwd, ".kody2");
2246
2641
  const invokeAgent = async (prompt) => runAgent({
2247
2642
  prompt,
2248
2643
  model,
@@ -2301,17 +2696,17 @@ async function runExecutable(profileName, input) {
2301
2696
  }
2302
2697
  }
2303
2698
  function resolveProfilePath(profileName) {
2304
- const here = path9.dirname(new URL(import.meta.url).pathname);
2699
+ const here = path10.dirname(new URL(import.meta.url).pathname);
2305
2700
  const candidates = [
2306
- path9.join(here, "executables", profileName, "profile.json"),
2701
+ path10.join(here, "executables", profileName, "profile.json"),
2307
2702
  // same-dir sibling (dev)
2308
- path9.join(here, "..", "executables", profileName, "profile.json"),
2703
+ path10.join(here, "..", "executables", profileName, "profile.json"),
2309
2704
  // up one (prod: dist/bin → dist/executables)
2310
- path9.join(here, "..", "src", "executables", profileName, "profile.json")
2705
+ path10.join(here, "..", "src", "executables", profileName, "profile.json")
2311
2706
  // fallback
2312
2707
  ];
2313
2708
  for (const c of candidates) {
2314
- if (fs11.existsSync(c)) return c;
2709
+ if (fs12.existsSync(c)) return c;
2315
2710
  }
2316
2711
  return candidates[0];
2317
2712
  }
@@ -2389,12 +2784,12 @@ function finish(out) {
2389
2784
  }
2390
2785
 
2391
2786
  // src/kody2-cli.ts
2392
- import { execFileSync as execFileSync12 } from "child_process";
2393
- import * as fs13 from "fs";
2394
- import * as path10 from "path";
2787
+ import { execFileSync as execFileSync13 } from "child_process";
2788
+ import * as fs14 from "fs";
2789
+ import * as path11 from "path";
2395
2790
 
2396
2791
  // src/dispatch.ts
2397
- import * as fs12 from "fs";
2792
+ import * as fs13 from "fs";
2398
2793
  function autoDispatch(explicit) {
2399
2794
  if (explicit?.mode && explicit.target) {
2400
2795
  return {
@@ -2404,10 +2799,10 @@ function autoDispatch(explicit) {
2404
2799
  }
2405
2800
  const eventName = process.env.GITHUB_EVENT_NAME;
2406
2801
  const eventPath = process.env.GITHUB_EVENT_PATH;
2407
- if (!eventName || !eventPath || !fs12.existsSync(eventPath)) return null;
2802
+ if (!eventName || !eventPath || !fs13.existsSync(eventPath)) return null;
2408
2803
  let event = {};
2409
2804
  try {
2410
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
2805
+ event = JSON.parse(fs13.readFileSync(eventPath, "utf-8"));
2411
2806
  } catch {
2412
2807
  return null;
2413
2808
  }
@@ -2527,14 +2922,14 @@ function resolveAuthToken(env = process.env) {
2527
2922
  return token;
2528
2923
  }
2529
2924
  function detectPackageManager2(cwd) {
2530
- if (fs13.existsSync(path10.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2531
- if (fs13.existsSync(path10.join(cwd, "yarn.lock"))) return "yarn";
2532
- if (fs13.existsSync(path10.join(cwd, "bun.lockb"))) return "bun";
2925
+ if (fs14.existsSync(path11.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2926
+ if (fs14.existsSync(path11.join(cwd, "yarn.lock"))) return "yarn";
2927
+ if (fs14.existsSync(path11.join(cwd, "bun.lockb"))) return "bun";
2533
2928
  return "npm";
2534
2929
  }
2535
2930
  function shellOut(cmd, args, cwd, stream = true) {
2536
2931
  try {
2537
- execFileSync12(cmd, args, {
2932
+ execFileSync13(cmd, args, {
2538
2933
  cwd,
2539
2934
  stdio: stream ? "inherit" : "pipe",
2540
2935
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -2547,7 +2942,7 @@ function shellOut(cmd, args, cwd, stream = true) {
2547
2942
  }
2548
2943
  function isOnPath(bin) {
2549
2944
  try {
2550
- execFileSync12("which", [bin], { stdio: "pipe" });
2945
+ execFileSync13("which", [bin], { stdio: "pipe" });
2551
2946
  return true;
2552
2947
  } catch {
2553
2948
  return false;
@@ -2581,7 +2976,7 @@ function installLitellmIfNeeded(cwd) {
2581
2976
  } catch {
2582
2977
  }
2583
2978
  try {
2584
- execFileSync12("python3", ["-c", "import litellm"], { stdio: "pipe" });
2979
+ execFileSync13("python3", ["-c", "import litellm"], { stdio: "pipe" });
2585
2980
  process.stdout.write("\u2192 kody2: litellm already installed\n");
2586
2981
  return 0;
2587
2982
  } catch {
@@ -2591,26 +2986,26 @@ function installLitellmIfNeeded(cwd) {
2591
2986
  }
2592
2987
  function configureGitIdentity(cwd) {
2593
2988
  try {
2594
- const name = execFileSync12("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
2989
+ const name = execFileSync13("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
2595
2990
  if (name) return;
2596
2991
  } catch {
2597
2992
  }
2598
2993
  try {
2599
- execFileSync12("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
2994
+ execFileSync13("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
2600
2995
  } catch {
2601
2996
  }
2602
2997
  try {
2603
- execFileSync12("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
2998
+ execFileSync13("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
2604
2999
  } catch {
2605
3000
  }
2606
3001
  }
2607
3002
  function postFailureTail(issueNumber, cwd, reason) {
2608
3003
  if (!issueNumber) return;
2609
- const logPath = path10.join(cwd, ".kody2", "last-run.jsonl");
3004
+ const logPath = path11.join(cwd, ".kody2", "last-run.jsonl");
2610
3005
  let tail = "";
2611
3006
  try {
2612
- if (fs13.existsSync(logPath)) {
2613
- const content = fs13.readFileSync(logPath, "utf-8");
3007
+ if (fs14.existsSync(logPath)) {
3008
+ const content = fs14.readFileSync(logPath, "utf-8");
2614
3009
  tail = content.slice(-3e3);
2615
3010
  }
2616
3011
  } catch {
@@ -2647,7 +3042,7 @@ async function runCi(argv) {
2647
3042
  ${CI_HELP}`);
2648
3043
  return 64;
2649
3044
  }
2650
- const cwd = args.cwd ? path10.resolve(args.cwd) : process.cwd();
3045
+ const cwd = args.cwd ? path11.resolve(args.cwd) : process.cwd();
2651
3046
  const dispatch = autoFallback ?? {
2652
3047
  mode: "run",
2653
3048
  target: args.issueNumber,
@@ -2723,31 +3118,31 @@ ${CI_HELP}`);
2723
3118
  }
2724
3119
 
2725
3120
  // src/registry.ts
2726
- import * as fs14 from "fs";
2727
- import * as path11 from "path";
3121
+ import * as fs15 from "fs";
3122
+ import * as path12 from "path";
2728
3123
  function getExecutablesRoot() {
2729
- const here = path11.dirname(new URL(import.meta.url).pathname);
3124
+ const here = path12.dirname(new URL(import.meta.url).pathname);
2730
3125
  const candidates = [
2731
- path11.join(here, "executables"),
3126
+ path12.join(here, "executables"),
2732
3127
  // dev: src/
2733
- path11.join(here, "..", "executables"),
3128
+ path12.join(here, "..", "executables"),
2734
3129
  // built: dist/bin → dist/executables
2735
- path11.join(here, "..", "src", "executables")
3130
+ path12.join(here, "..", "src", "executables")
2736
3131
  // fallback
2737
3132
  ];
2738
3133
  for (const c of candidates) {
2739
- if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
3134
+ if (fs15.existsSync(c) && fs15.statSync(c).isDirectory()) return c;
2740
3135
  }
2741
3136
  return candidates[0];
2742
3137
  }
2743
3138
  function listExecutables(root = getExecutablesRoot()) {
2744
- if (!fs14.existsSync(root)) return [];
2745
- const entries = fs14.readdirSync(root, { withFileTypes: true });
3139
+ if (!fs15.existsSync(root)) return [];
3140
+ const entries = fs15.readdirSync(root, { withFileTypes: true });
2746
3141
  const out = [];
2747
3142
  for (const ent of entries) {
2748
3143
  if (!ent.isDirectory()) continue;
2749
- const profilePath = path11.join(root, ent.name, "profile.json");
2750
- if (fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile()) {
3144
+ const profilePath = path12.join(root, ent.name, "profile.json");
3145
+ if (fs15.existsSync(profilePath) && fs15.statSync(profilePath).isFile()) {
2751
3146
  out.push({ name: ent.name, profilePath });
2752
3147
  }
2753
3148
  }
@@ -2755,8 +3150,8 @@ function listExecutables(root = getExecutablesRoot()) {
2755
3150
  }
2756
3151
  function hasExecutable(name, root = getExecutablesRoot()) {
2757
3152
  if (!isSafeName(name)) return false;
2758
- const profilePath = path11.join(root, name, "profile.json");
2759
- return fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile();
3153
+ const profilePath = path12.join(root, name, "profile.json");
3154
+ return fs15.existsSync(profilePath) && fs15.statSync(profilePath).isFile();
2760
3155
  }
2761
3156
  function isSafeName(name) {
2762
3157
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "release",
3
+ "describe": "Version bump + changelog + release PR (prepare), or tag + publish + GH release (finalize). No agent.",
4
+
5
+ "inputs": [
6
+ {
7
+ "name": "mode",
8
+ "flag": "--mode",
9
+ "type": "enum",
10
+ "values": ["prepare", "finalize"],
11
+ "required": false,
12
+ "describe": "`prepare` (default): bump + changelog + release PR. `finalize`: E2E gate + tag + publish + GH release."
13
+ },
14
+ {
15
+ "name": "bump",
16
+ "flag": "--bump",
17
+ "type": "enum",
18
+ "values": ["patch", "minor", "major"],
19
+ "required": false,
20
+ "describe": "Version bump when mode=prepare (ignored in finalize). Default patch."
21
+ },
22
+ {
23
+ "name": "dry-run",
24
+ "flag": "--dry-run",
25
+ "type": "bool",
26
+ "required": false,
27
+ "describe": "Print plan without writing files, creating PRs, tagging, or publishing."
28
+ }
29
+ ],
30
+
31
+ "claudeCode": {
32
+ "model": "inherit",
33
+ "permissionMode": "acceptEdits",
34
+ "maxTurns": null,
35
+ "systemPromptAppend": null,
36
+ "tools": [],
37
+ "hooks": { "PreToolUse": [], "PostToolUse": [], "Stop": [] },
38
+ "skills": [],
39
+ "commands": [],
40
+ "subagents": [],
41
+ "plugins": [],
42
+ "mcpServers": []
43
+ },
44
+
45
+ "cliTools": [],
46
+
47
+ "scripts": {
48
+ "preflight": [
49
+ { "script": "releaseFlow" }
50
+ ],
51
+ "postflight": []
52
+ }
53
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "review",
3
+ "describe": "Read-only structured review of an open PR. Posts one comment, never commits.",
4
+
5
+ "inputs": [
6
+ {
7
+ "name": "pr",
8
+ "flag": "--pr",
9
+ "type": "int",
10
+ "required": true,
11
+ "describe": "GitHub PR number to review."
12
+ }
13
+ ],
14
+
15
+ "claudeCode": {
16
+ "model": "inherit",
17
+ "permissionMode": "default",
18
+ "maxTurns": null,
19
+ "systemPromptAppend": null,
20
+ "tools": ["Read", "Grep", "Glob", "Bash"],
21
+ "hooks": { "PreToolUse": [], "PostToolUse": [], "Stop": [] },
22
+ "skills": [],
23
+ "commands": [],
24
+ "subagents": [],
25
+ "plugins": [],
26
+ "mcpServers": []
27
+ },
28
+
29
+ "cliTools": [],
30
+
31
+ "scripts": {
32
+ "preflight": [
33
+ { "script": "reviewFlow" },
34
+ { "script": "loadConventions" },
35
+ { "script": "composePrompt" }
36
+ ],
37
+ "postflight": [
38
+ { "script": "postReviewResult" }
39
+ ]
40
+ }
41
+ }
@@ -0,0 +1,47 @@
1
+ You are Kody, a senior code reviewer. Review PR #{{pr.number}} carefully and post ONE structured review comment. Do NOT edit any files. Do NOT run any `git` or `gh` commands. Use Read / Grep / Glob / Bash only to inspect the diff and surrounding code.
2
+
3
+ # PR #{{pr.number}}: {{pr.title}}
4
+
5
+ Base: {{pr.baseRefName}} ← Head: {{pr.headRefName}}
6
+
7
+ {{pr.body}}
8
+
9
+ {{conventionsBlock}}
10
+
11
+ # Diff
12
+
13
+ ```diff
14
+ {{prDiff}}
15
+ ```
16
+
17
+ # Required output
18
+
19
+ Your FINAL message must be a markdown-formatted review comment, **structured exactly as below** — no preamble, no DONE / COMMIT_MSG / PR_SUMMARY markers. The entire final message IS the review comment and will be posted verbatim:
20
+
21
+ ```
22
+ ## Verdict: PASS | CONCERNS | FAIL
23
+
24
+ ### Summary
25
+ <2-3 sentences: what this PR does, is the approach sound>
26
+
27
+ ### Strengths
28
+ - <bullet>
29
+ - <bullet>
30
+
31
+ ### Concerns
32
+ - <bullet, or "None" if none>
33
+
34
+ ### Suggestions
35
+ - <bullet with file:line reference where possible>
36
+
37
+ ### Bottom line
38
+ <one sentence>
39
+ ```
40
+
41
+ # Rules
42
+
43
+ - No file edits. No `git`/`gh` invocations. Read-only investigation.
44
+ - Be specific: cite file paths and line numbers. No generic advice.
45
+ - Verdict **FAIL** only for clear correctness / security / regression risks.
46
+ - Verdict **CONCERNS** for style / clarity / test-coverage gaps that shouldn't block.
47
+ - Verdict **PASS** when the PR meets spec with no blocking issues.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "kody2 — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",