@kody-ade/kody-engine 0.2.3 → 0.2.5

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.3",
6
+ version: "0.2.5",
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",
@@ -137,8 +137,8 @@ function getAnthropicApiKeyOrDummy() {
137
137
  }
138
138
 
139
139
  // src/executor.ts
140
- import * as fs10 from "fs";
141
- import * as path8 from "path";
140
+ import * as fs11 from "fs";
141
+ import * as path9 from "path";
142
142
 
143
143
  // src/agent.ts
144
144
  import * as fs2 from "fs";
@@ -519,9 +519,6 @@ function parseClaudeCode(p, raw) {
519
519
  throw new ProfileError(p, `claudeCode.permissionMode must be one of default|acceptEdits|plan|bypassPermissions`);
520
520
  }
521
521
  const tools = Array.isArray(r.tools) ? r.tools : [];
522
- if (tools.length === 0) {
523
- throw new ProfileError(p, `claudeCode.tools must declare at least one SDK tool`);
524
- }
525
522
  const hooksRaw = r.hooks ?? {};
526
523
  const hooks = {
527
524
  PreToolUse: Array.isArray(hooksRaw.PreToolUse) ? hooksRaw.PreToolUse : [],
@@ -1651,6 +1648,174 @@ function tryPostPr2(prNumber, body, cwd) {
1651
1648
  }
1652
1649
  }
1653
1650
 
1651
+ // src/scripts/initFlow.ts
1652
+ import { execFileSync as execFileSync9 } from "child_process";
1653
+ import * as fs9 from "fs";
1654
+ import * as path8 from "path";
1655
+ function detectPackageManager(cwd) {
1656
+ if (fs9.existsSync(path8.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1657
+ if (fs9.existsSync(path8.join(cwd, "yarn.lock"))) return "yarn";
1658
+ if (fs9.existsSync(path8.join(cwd, "bun.lockb"))) return "bun";
1659
+ return "npm";
1660
+ }
1661
+ function qualityCommandsFor(pm) {
1662
+ return {
1663
+ typecheck: `${pm} tsc --noEmit`,
1664
+ lint: "",
1665
+ testUnit: `${pm} test`
1666
+ };
1667
+ }
1668
+ function detectOwnerRepo(cwd) {
1669
+ let url;
1670
+ try {
1671
+ url = execFileSync9("git", ["remote", "get-url", "origin"], {
1672
+ cwd,
1673
+ encoding: "utf-8",
1674
+ stdio: ["ignore", "pipe", "pipe"]
1675
+ }).trim();
1676
+ } catch {
1677
+ return null;
1678
+ }
1679
+ const m = url.match(/[:/]([^/:]+)\/([^/]+?)(?:\.git)?$/) ?? null;
1680
+ if (!m) return null;
1681
+ return { owner: m[1], repo: m[2] };
1682
+ }
1683
+ function makeConfig(pm, ownerRepo, defaultBranch) {
1684
+ return {
1685
+ $schema: "https://raw.githubusercontent.com/aharonyaircohen/kody-engine/main/kody.config.schema.json",
1686
+ quality: qualityCommandsFor(pm),
1687
+ git: { defaultBranch },
1688
+ github: {
1689
+ owner: ownerRepo?.owner ?? "OWNER",
1690
+ repo: ownerRepo?.repo ?? "REPO"
1691
+ },
1692
+ agent: {
1693
+ model: "minimax/MiniMax-M2.7-highspeed"
1694
+ }
1695
+ };
1696
+ }
1697
+ var WORKFLOW_TEMPLATE = `# Drop this file at .github/workflows/kody2.yml in your repo.
1698
+ #
1699
+ # Triggers: @kody2 comment on an issue or PR, or manual workflow_dispatch.
1700
+ # Everything else (install deps, set up LiteLLM, run the agent, open the PR)
1701
+ # is handled inside the @kody-ade/kody-engine package.
1702
+ #
1703
+ # Required repo secrets: at least one model provider key (e.g. MINIMAX_API_KEY,
1704
+ # ANTHROPIC_API_KEY). kody2 reads any *_API_KEY secret automatically via
1705
+ # toJSON(secrets) \u2014 no need to list them here.
1706
+ #
1707
+ # Recommended: KODY_TOKEN secret \u2014 a PAT or GitHub App token with repo
1708
+ # scope so kody2's pushes trigger downstream CI and PR-body edits succeed.
1709
+
1710
+ name: kody2
1711
+
1712
+ on:
1713
+ workflow_dispatch:
1714
+ inputs:
1715
+ issue_number:
1716
+ description: "GitHub issue number"
1717
+ required: true
1718
+ type: string
1719
+ issue_comment:
1720
+ types: [created]
1721
+
1722
+ jobs:
1723
+ run:
1724
+ if: >-
1725
+ \${{ github.event_name == 'workflow_dispatch' ||
1726
+ (github.event_name == 'issue_comment' &&
1727
+ !github.event.issue.pull_request &&
1728
+ contains(github.event.comment.body, '@kody2')) }}
1729
+ runs-on: ubuntu-latest
1730
+ timeout-minutes: 60
1731
+ permissions:
1732
+ issues: write
1733
+ pull-requests: write
1734
+ contents: write
1735
+ steps:
1736
+ - uses: actions/checkout@v4
1737
+ with:
1738
+ fetch-depth: 0
1739
+ token: \${{ secrets.KODY_TOKEN || github.token }}
1740
+
1741
+ - uses: actions/setup-node@v4
1742
+ with:
1743
+ node-version: 22
1744
+
1745
+ - uses: actions/setup-python@v5
1746
+ with:
1747
+ python-version: "3.12"
1748
+
1749
+ - env:
1750
+ ALL_SECRETS: \${{ toJSON(secrets) }}
1751
+ run: npx -y -p @kody-ade/kody-engine@latest kody2 ci --issue \${{ github.event.inputs.issue_number || github.event.issue.number }}
1752
+ `;
1753
+ function defaultBranchFromGit(cwd) {
1754
+ try {
1755
+ const ref = execFileSync9("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
1756
+ cwd,
1757
+ encoding: "utf-8",
1758
+ stdio: ["ignore", "pipe", "pipe"]
1759
+ }).trim();
1760
+ return ref.replace("refs/remotes/origin/", "");
1761
+ } catch {
1762
+ try {
1763
+ return execFileSync9("git", ["branch", "--show-current"], {
1764
+ cwd,
1765
+ encoding: "utf-8",
1766
+ stdio: ["ignore", "pipe", "pipe"]
1767
+ }).trim() || "main";
1768
+ } catch {
1769
+ return "main";
1770
+ }
1771
+ }
1772
+ }
1773
+ function performInit(cwd, force) {
1774
+ const wrote = [];
1775
+ const skipped = [];
1776
+ const pm = detectPackageManager(cwd);
1777
+ const ownerRepo = detectOwnerRepo(cwd);
1778
+ const defaultBranch = defaultBranchFromGit(cwd);
1779
+ const configPath = path8.join(cwd, "kody.config.json");
1780
+ if (fs9.existsSync(configPath) && !force) {
1781
+ skipped.push("kody.config.json");
1782
+ } else {
1783
+ const cfg = makeConfig(pm, ownerRepo, defaultBranch);
1784
+ fs9.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
1785
+ `);
1786
+ wrote.push("kody.config.json");
1787
+ }
1788
+ const workflowDir = path8.join(cwd, ".github", "workflows");
1789
+ const workflowPath = path8.join(workflowDir, "kody2.yml");
1790
+ if (fs9.existsSync(workflowPath) && !force) {
1791
+ skipped.push(".github/workflows/kody2.yml");
1792
+ } else {
1793
+ fs9.mkdirSync(workflowDir, { recursive: true });
1794
+ fs9.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
1795
+ wrote.push(".github/workflows/kody2.yml");
1796
+ }
1797
+ return { wrote, skipped };
1798
+ }
1799
+ var initFlow = async (ctx) => {
1800
+ const force = ctx.args.force === true;
1801
+ const cwd = ctx.cwd;
1802
+ const { wrote, skipped } = performInit(cwd, force);
1803
+ process.stdout.write("\u2192 kody2 init\n");
1804
+ for (const f of wrote) process.stdout.write(` wrote ${f}
1805
+ `);
1806
+ for (const f of skipped) process.stdout.write(` skipped ${f} (already exists; pass --force to overwrite)
1807
+ `);
1808
+ process.stdout.write(
1809
+ wrote.length > 0 ? `
1810
+ Done. Edit kody.config.json to pick your model, then push the workflow file.
1811
+ ` : `
1812
+ Nothing to do. All files already present. (Use --force to overwrite.)
1813
+ `
1814
+ );
1815
+ ctx.skipAgent = true;
1816
+ ctx.output.exitCode = 0;
1817
+ };
1818
+
1654
1819
  // src/scripts/loadConventions.ts
1655
1820
  var loadConventions = async (ctx) => {
1656
1821
  const conventions = loadProjectConventions(ctx.cwd);
@@ -1733,8 +1898,57 @@ function postWith(type, n, body, cwd) {
1733
1898
  }
1734
1899
  }
1735
1900
 
1901
+ // src/scripts/postReviewResult.ts
1902
+ function detectVerdict(body) {
1903
+ const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
1904
+ if (!m) return "UNKNOWN";
1905
+ return m[1].toUpperCase();
1906
+ }
1907
+ var postReviewResult = async (ctx, _profile, agentResult) => {
1908
+ const prNumber = ctx.data.commentTargetNumber;
1909
+ if (!prNumber) {
1910
+ ctx.output.exitCode = 99;
1911
+ ctx.output.reason = "review postflight: no PR number in context";
1912
+ return;
1913
+ }
1914
+ if (!agentResult || agentResult.outcome !== "completed") {
1915
+ const reason = agentResult?.error ?? "agent did not complete";
1916
+ try {
1917
+ postPrReviewComment(prNumber, `\u26A0\uFE0F kody2 review FAILED: ${truncate2(reason, 1e3)}`, ctx.cwd);
1918
+ } catch {
1919
+ }
1920
+ ctx.output.exitCode = 1;
1921
+ ctx.output.reason = reason;
1922
+ return;
1923
+ }
1924
+ const reviewBody = agentResult.finalText.trim();
1925
+ if (!reviewBody) {
1926
+ try {
1927
+ postPrReviewComment(prNumber, `\u26A0\uFE0F kody2 review FAILED: agent produced no review body`, ctx.cwd);
1928
+ } catch {
1929
+ }
1930
+ ctx.output.exitCode = 1;
1931
+ ctx.output.reason = "empty review body";
1932
+ return;
1933
+ }
1934
+ try {
1935
+ postPrReviewComment(prNumber, reviewBody, ctx.cwd);
1936
+ } catch (err) {
1937
+ const msg = err instanceof Error ? err.message : String(err);
1938
+ ctx.output.exitCode = 4;
1939
+ ctx.output.reason = `failed to post review comment: ${msg}`;
1940
+ return;
1941
+ }
1942
+ const verdict = detectVerdict(reviewBody);
1943
+ ctx.data.reviewVerdict = verdict;
1944
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
1945
+ process.stdout.write(`
1946
+ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/pull/${prNumber} (verdict: ${verdict})
1947
+ `);
1948
+ };
1949
+
1736
1950
  // src/scripts/resolveFlow.ts
1737
- import { execFileSync as execFileSync9 } from "child_process";
1951
+ import { execFileSync as execFileSync10 } from "child_process";
1738
1952
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
1739
1953
  var resolveFlow = async (ctx) => {
1740
1954
  const prNumber = ctx.args.pr;
@@ -1786,7 +2000,7 @@ var resolveFlow = async (ctx) => {
1786
2000
  };
1787
2001
  function getConflictedFiles(cwd) {
1788
2002
  try {
1789
- const out = execFileSync9("git", ["diff", "--name-only", "--diff-filter=U"], {
2003
+ const out = execFileSync10("git", ["diff", "--name-only", "--diff-filter=U"], {
1790
2004
  encoding: "utf-8",
1791
2005
  cwd,
1792
2006
  env: { ...process.env, HUSKY: "0" }
@@ -1801,7 +2015,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
1801
2015
  let total = 0;
1802
2016
  for (const f of files) {
1803
2017
  try {
1804
- const content = execFileSync9("cat", [f], { encoding: "utf-8", cwd }).toString();
2018
+ const content = execFileSync10("cat", [f], { encoding: "utf-8", cwd }).toString();
1805
2019
  const snippet = `### ${f}
1806
2020
 
1807
2021
  \`\`\`
@@ -1823,6 +2037,33 @@ function tryPostPr3(prNumber, body, cwd) {
1823
2037
  }
1824
2038
  }
1825
2039
 
2040
+ // src/scripts/reviewFlow.ts
2041
+ var reviewFlow = async (ctx) => {
2042
+ const prNumber = ctx.args.pr;
2043
+ const pr = getPr(prNumber, ctx.cwd);
2044
+ if (pr.state !== "OPEN") {
2045
+ ctx.output.exitCode = 1;
2046
+ ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
2047
+ ctx.skipAgent = true;
2048
+ return;
2049
+ }
2050
+ ctx.data.pr = pr;
2051
+ ctx.data.commentTargetType = "pr";
2052
+ ctx.data.commentTargetNumber = prNumber;
2053
+ checkoutPrBranch(prNumber, ctx.cwd);
2054
+ ctx.data.branch = getCurrentBranch(ctx.cwd);
2055
+ ctx.data.prDiff = getPrDiff(prNumber, ctx.cwd);
2056
+ const runUrl = getRunUrl();
2057
+ const runSuffix = runUrl ? `, run ${runUrl}` : "";
2058
+ tryPostPr4(prNumber, `\u{1F440} kody2 review started on PR #${prNumber}${runSuffix}`, ctx.cwd);
2059
+ };
2060
+ function tryPostPr4(prNumber, body, cwd) {
2061
+ try {
2062
+ postPrReviewComment(prNumber, body, cwd);
2063
+ } catch {
2064
+ }
2065
+ }
2066
+
1826
2067
  // src/scripts/runFlow.ts
1827
2068
  var runFlow = async (ctx) => {
1828
2069
  const issueNumber = ctx.args.issue;
@@ -1939,7 +2180,7 @@ var verify = async (ctx) => {
1939
2180
  };
1940
2181
 
1941
2182
  // src/scripts/writeRunSummary.ts
1942
- import * as fs9 from "fs";
2183
+ import * as fs10 from "fs";
1943
2184
  var writeRunSummary = async (ctx) => {
1944
2185
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
1945
2186
  if (!summaryPath) return;
@@ -1961,7 +2202,7 @@ var writeRunSummary = async (ctx) => {
1961
2202
  if (reason) lines.push(`- **Reason:** ${reason}`);
1962
2203
  lines.push("");
1963
2204
  try {
1964
- fs9.appendFileSync(summaryPath, `${lines.join("\n")}
2205
+ fs10.appendFileSync(summaryPath, `${lines.join("\n")}
1965
2206
  `);
1966
2207
  } catch {
1967
2208
  }
@@ -1973,6 +2214,8 @@ var preflightScripts = {
1973
2214
  fixFlow,
1974
2215
  fixCiFlow,
1975
2216
  resolveFlow,
2217
+ reviewFlow,
2218
+ initFlow,
1976
2219
  loadConventions,
1977
2220
  loadCoverageRules,
1978
2221
  composePrompt
@@ -1984,6 +2227,7 @@ var postflightScripts = {
1984
2227
  commitAndPush: commitAndPush2,
1985
2228
  ensurePr: ensurePr2,
1986
2229
  postIssueComment: postIssueComment2,
2230
+ postReviewResult,
1987
2231
  writeRunSummary
1988
2232
  };
1989
2233
  var allScriptNames = /* @__PURE__ */ new Set([
@@ -1992,7 +2236,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
1992
2236
  ]);
1993
2237
 
1994
2238
  // src/tools.ts
1995
- import { execFileSync as execFileSync10 } from "child_process";
2239
+ import { execFileSync as execFileSync11 } from "child_process";
1996
2240
  function verifyCliTools(tools, cwd) {
1997
2241
  const out = [];
1998
2242
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -2025,7 +2269,7 @@ function verifyOne(tool, cwd) {
2025
2269
  }
2026
2270
  function runShell(cmd, cwd, timeoutMs = 3e4) {
2027
2271
  try {
2028
- execFileSync10("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
2272
+ execFileSync11("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
2029
2273
  return true;
2030
2274
  } catch {
2031
2275
  return false;
@@ -2076,7 +2320,7 @@ async function runExecutable(profileName, input) {
2076
2320
  data: {},
2077
2321
  output: { exitCode: 0 }
2078
2322
  };
2079
- const ndjsonDir = path8.join(input.cwd, ".kody2");
2323
+ const ndjsonDir = path9.join(input.cwd, ".kody2");
2080
2324
  const invokeAgent = async (prompt) => runAgent({
2081
2325
  prompt,
2082
2326
  model,
@@ -2135,17 +2379,17 @@ async function runExecutable(profileName, input) {
2135
2379
  }
2136
2380
  }
2137
2381
  function resolveProfilePath(profileName) {
2138
- const here = path8.dirname(new URL(import.meta.url).pathname);
2382
+ const here = path9.dirname(new URL(import.meta.url).pathname);
2139
2383
  const candidates = [
2140
- path8.join(here, "executables", profileName, "profile.json"),
2384
+ path9.join(here, "executables", profileName, "profile.json"),
2141
2385
  // same-dir sibling (dev)
2142
- path8.join(here, "..", "executables", profileName, "profile.json"),
2386
+ path9.join(here, "..", "executables", profileName, "profile.json"),
2143
2387
  // up one (prod: dist/bin → dist/executables)
2144
- path8.join(here, "..", "src", "executables", profileName, "profile.json")
2388
+ path9.join(here, "..", "src", "executables", profileName, "profile.json")
2145
2389
  // fallback
2146
2390
  ];
2147
2391
  for (const c of candidates) {
2148
- if (fs10.existsSync(c)) return c;
2392
+ if (fs11.existsSync(c)) return c;
2149
2393
  }
2150
2394
  return candidates[0];
2151
2395
  }
@@ -2223,12 +2467,12 @@ function finish(out) {
2223
2467
  }
2224
2468
 
2225
2469
  // src/kody2-cli.ts
2226
- import { execFileSync as execFileSync11 } from "child_process";
2227
- import * as fs12 from "fs";
2228
- import * as path9 from "path";
2470
+ import { execFileSync as execFileSync12 } from "child_process";
2471
+ import * as fs13 from "fs";
2472
+ import * as path10 from "path";
2229
2473
 
2230
2474
  // src/dispatch.ts
2231
- import * as fs11 from "fs";
2475
+ import * as fs12 from "fs";
2232
2476
  function autoDispatch(explicit) {
2233
2477
  if (explicit?.mode && explicit.target) {
2234
2478
  return {
@@ -2238,10 +2482,10 @@ function autoDispatch(explicit) {
2238
2482
  }
2239
2483
  const eventName = process.env.GITHUB_EVENT_NAME;
2240
2484
  const eventPath = process.env.GITHUB_EVENT_PATH;
2241
- if (!eventName || !eventPath || !fs11.existsSync(eventPath)) return null;
2485
+ if (!eventName || !eventPath || !fs12.existsSync(eventPath)) return null;
2242
2486
  let event = {};
2243
2487
  try {
2244
- event = JSON.parse(fs11.readFileSync(eventPath, "utf-8"));
2488
+ event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
2245
2489
  } catch {
2246
2490
  return null;
2247
2491
  }
@@ -2360,15 +2604,15 @@ function resolveAuthToken(env = process.env) {
2360
2604
  if (token && !env.GH_TOKEN) env.GH_TOKEN = token;
2361
2605
  return token;
2362
2606
  }
2363
- function detectPackageManager(cwd) {
2364
- if (fs12.existsSync(path9.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2365
- if (fs12.existsSync(path9.join(cwd, "yarn.lock"))) return "yarn";
2366
- if (fs12.existsSync(path9.join(cwd, "bun.lockb"))) return "bun";
2607
+ function detectPackageManager2(cwd) {
2608
+ if (fs13.existsSync(path10.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2609
+ if (fs13.existsSync(path10.join(cwd, "yarn.lock"))) return "yarn";
2610
+ if (fs13.existsSync(path10.join(cwd, "bun.lockb"))) return "bun";
2367
2611
  return "npm";
2368
2612
  }
2369
2613
  function shellOut(cmd, args, cwd, stream = true) {
2370
2614
  try {
2371
- execFileSync11(cmd, args, {
2615
+ execFileSync12(cmd, args, {
2372
2616
  cwd,
2373
2617
  stdio: stream ? "inherit" : "pipe",
2374
2618
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -2381,7 +2625,7 @@ function shellOut(cmd, args, cwd, stream = true) {
2381
2625
  }
2382
2626
  function isOnPath(bin) {
2383
2627
  try {
2384
- execFileSync11("which", [bin], { stdio: "pipe" });
2628
+ execFileSync12("which", [bin], { stdio: "pipe" });
2385
2629
  return true;
2386
2630
  } catch {
2387
2631
  return false;
@@ -2415,7 +2659,7 @@ function installLitellmIfNeeded(cwd) {
2415
2659
  } catch {
2416
2660
  }
2417
2661
  try {
2418
- execFileSync11("python3", ["-c", "import litellm"], { stdio: "pipe" });
2662
+ execFileSync12("python3", ["-c", "import litellm"], { stdio: "pipe" });
2419
2663
  process.stdout.write("\u2192 kody2: litellm already installed\n");
2420
2664
  return 0;
2421
2665
  } catch {
@@ -2425,26 +2669,26 @@ function installLitellmIfNeeded(cwd) {
2425
2669
  }
2426
2670
  function configureGitIdentity(cwd) {
2427
2671
  try {
2428
- const name = execFileSync11("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
2672
+ const name = execFileSync12("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
2429
2673
  if (name) return;
2430
2674
  } catch {
2431
2675
  }
2432
2676
  try {
2433
- execFileSync11("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
2677
+ execFileSync12("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
2434
2678
  } catch {
2435
2679
  }
2436
2680
  try {
2437
- execFileSync11("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
2681
+ execFileSync12("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
2438
2682
  } catch {
2439
2683
  }
2440
2684
  }
2441
2685
  function postFailureTail(issueNumber, cwd, reason) {
2442
2686
  if (!issueNumber) return;
2443
- const logPath = path9.join(cwd, ".kody2", "last-run.jsonl");
2687
+ const logPath = path10.join(cwd, ".kody2", "last-run.jsonl");
2444
2688
  let tail = "";
2445
2689
  try {
2446
- if (fs12.existsSync(logPath)) {
2447
- const content = fs12.readFileSync(logPath, "utf-8");
2690
+ if (fs13.existsSync(logPath)) {
2691
+ const content = fs13.readFileSync(logPath, "utf-8");
2448
2692
  tail = content.slice(-3e3);
2449
2693
  }
2450
2694
  } catch {
@@ -2481,7 +2725,7 @@ async function runCi(argv) {
2481
2725
  ${CI_HELP}`);
2482
2726
  return 64;
2483
2727
  }
2484
- const cwd = args.cwd ? path9.resolve(args.cwd) : process.cwd();
2728
+ const cwd = args.cwd ? path10.resolve(args.cwd) : process.cwd();
2485
2729
  const dispatch = autoFallback ?? {
2486
2730
  mode: "run",
2487
2731
  target: args.issueNumber,
@@ -2496,7 +2740,7 @@ ${CI_HELP}`);
2496
2740
  `);
2497
2741
  resolveAuthToken();
2498
2742
  reactToTriggerComment(cwd);
2499
- const pm = args.packageManager ?? detectPackageManager(cwd);
2743
+ const pm = args.packageManager ?? detectPackageManager2(cwd);
2500
2744
  process.stdout.write(`\u2192 kody2: package manager = ${pm}
2501
2745
  `);
2502
2746
  if (!args.skipInstall) {
@@ -2557,31 +2801,31 @@ ${CI_HELP}`);
2557
2801
  }
2558
2802
 
2559
2803
  // src/registry.ts
2560
- import * as fs13 from "fs";
2561
- import * as path10 from "path";
2804
+ import * as fs14 from "fs";
2805
+ import * as path11 from "path";
2562
2806
  function getExecutablesRoot() {
2563
- const here = path10.dirname(new URL(import.meta.url).pathname);
2807
+ const here = path11.dirname(new URL(import.meta.url).pathname);
2564
2808
  const candidates = [
2565
- path10.join(here, "executables"),
2809
+ path11.join(here, "executables"),
2566
2810
  // dev: src/
2567
- path10.join(here, "..", "executables"),
2811
+ path11.join(here, "..", "executables"),
2568
2812
  // built: dist/bin → dist/executables
2569
- path10.join(here, "..", "src", "executables")
2813
+ path11.join(here, "..", "src", "executables")
2570
2814
  // fallback
2571
2815
  ];
2572
2816
  for (const c of candidates) {
2573
- if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
2817
+ if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
2574
2818
  }
2575
2819
  return candidates[0];
2576
2820
  }
2577
2821
  function listExecutables(root = getExecutablesRoot()) {
2578
- if (!fs13.existsSync(root)) return [];
2579
- const entries = fs13.readdirSync(root, { withFileTypes: true });
2822
+ if (!fs14.existsSync(root)) return [];
2823
+ const entries = fs14.readdirSync(root, { withFileTypes: true });
2580
2824
  const out = [];
2581
2825
  for (const ent of entries) {
2582
2826
  if (!ent.isDirectory()) continue;
2583
- const profilePath = path10.join(root, ent.name, "profile.json");
2584
- if (fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile()) {
2827
+ const profilePath = path11.join(root, ent.name, "profile.json");
2828
+ if (fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile()) {
2585
2829
  out.push({ name: ent.name, profilePath });
2586
2830
  }
2587
2831
  }
@@ -2589,8 +2833,8 @@ function listExecutables(root = getExecutablesRoot()) {
2589
2833
  }
2590
2834
  function hasExecutable(name, root = getExecutablesRoot()) {
2591
2835
  if (!isSafeName(name)) return false;
2592
- const profilePath = path10.join(root, name, "profile.json");
2593
- return fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile();
2836
+ const profilePath = path11.join(root, name, "profile.json");
2837
+ return fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile();
2594
2838
  }
2595
2839
  function isSafeName(name) {
2596
2840
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
@@ -2728,16 +2972,27 @@ ${HELP_TEXT}`);
2728
2972
  }
2729
2973
  }
2730
2974
  const cwd = args.cwd ?? process.cwd();
2975
+ const configlessCommands = /* @__PURE__ */ new Set(["init"]);
2976
+ const needsConfig = !(args.command === "__executable__" && configlessCommands.has(args.executableName ?? ""));
2731
2977
  let config;
2732
- try {
2733
- config = loadConfig(cwd);
2734
- } catch (err) {
2735
- const msg = err instanceof Error ? err.message : String(err);
2736
- process.stderr.write(`[kody2] config error: ${msg}
2978
+ if (needsConfig) {
2979
+ try {
2980
+ config = loadConfig(cwd);
2981
+ } catch (err) {
2982
+ const msg = err instanceof Error ? err.message : String(err);
2983
+ process.stderr.write(`[kody2] config error: ${msg}
2737
2984
  `);
2738
- process.stdout.write(`PR_URL=FAILED: config error: ${msg}
2985
+ process.stdout.write(`PR_URL=FAILED: config error: ${msg}
2739
2986
  `);
2740
- return 99;
2987
+ return 99;
2988
+ }
2989
+ } else {
2990
+ config = {
2991
+ quality: { typecheck: "", lint: "", testUnit: "" },
2992
+ git: { defaultBranch: "main" },
2993
+ github: { owner: "", repo: "" },
2994
+ agent: { model: "claude/claude-haiku-4-5-20251001" }
2995
+ };
2741
2996
  }
2742
2997
  if (args.command === "__executable__") {
2743
2998
  try {
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "init",
3
+ "describe": "Scaffold a consumer repo with kody.config.json and the @kody2 workflow. No agent.",
4
+
5
+ "inputs": [
6
+ {
7
+ "name": "force",
8
+ "flag": "--force",
9
+ "type": "bool",
10
+ "required": false,
11
+ "describe": "Overwrite existing files instead of skipping them."
12
+ }
13
+ ],
14
+
15
+ "claudeCode": {
16
+ "model": "inherit",
17
+ "permissionMode": "acceptEdits",
18
+ "maxTurns": null,
19
+ "systemPromptAppend": null,
20
+ "tools": [],
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": "initFlow" }
34
+ ],
35
+ "postflight": []
36
+ }
37
+ }
@@ -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.3",
3
+ "version": "0.2.5",
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",