@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.
|
|
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
|
|
141
|
-
import * as
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
2382
|
+
const here = path9.dirname(new URL(import.meta.url).pathname);
|
|
2139
2383
|
const candidates = [
|
|
2140
|
-
|
|
2384
|
+
path9.join(here, "executables", profileName, "profile.json"),
|
|
2141
2385
|
// same-dir sibling (dev)
|
|
2142
|
-
|
|
2386
|
+
path9.join(here, "..", "executables", profileName, "profile.json"),
|
|
2143
2387
|
// up one (prod: dist/bin → dist/executables)
|
|
2144
|
-
|
|
2388
|
+
path9.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
2145
2389
|
// fallback
|
|
2146
2390
|
];
|
|
2147
2391
|
for (const c of candidates) {
|
|
2148
|
-
if (
|
|
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
|
|
2227
|
-
import * as
|
|
2228
|
-
import * as
|
|
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
|
|
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 || !
|
|
2485
|
+
if (!eventName || !eventPath || !fs12.existsSync(eventPath)) return null;
|
|
2242
2486
|
let event = {};
|
|
2243
2487
|
try {
|
|
2244
|
-
event = JSON.parse(
|
|
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
|
|
2364
|
-
if (
|
|
2365
|
-
if (
|
|
2366
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2677
|
+
execFileSync12("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
|
|
2434
2678
|
} catch {
|
|
2435
2679
|
}
|
|
2436
2680
|
try {
|
|
2437
|
-
|
|
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 =
|
|
2687
|
+
const logPath = path10.join(cwd, ".kody2", "last-run.jsonl");
|
|
2444
2688
|
let tail = "";
|
|
2445
2689
|
try {
|
|
2446
|
-
if (
|
|
2447
|
-
const content =
|
|
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 ?
|
|
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 ??
|
|
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
|
|
2561
|
-
import * as
|
|
2804
|
+
import * as fs14 from "fs";
|
|
2805
|
+
import * as path11 from "path";
|
|
2562
2806
|
function getExecutablesRoot() {
|
|
2563
|
-
const here =
|
|
2807
|
+
const here = path11.dirname(new URL(import.meta.url).pathname);
|
|
2564
2808
|
const candidates = [
|
|
2565
|
-
|
|
2809
|
+
path11.join(here, "executables"),
|
|
2566
2810
|
// dev: src/
|
|
2567
|
-
|
|
2811
|
+
path11.join(here, "..", "executables"),
|
|
2568
2812
|
// built: dist/bin → dist/executables
|
|
2569
|
-
|
|
2813
|
+
path11.join(here, "..", "src", "executables")
|
|
2570
2814
|
// fallback
|
|
2571
2815
|
];
|
|
2572
2816
|
for (const c of candidates) {
|
|
2573
|
-
if (
|
|
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 (!
|
|
2579
|
-
const entries =
|
|
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 =
|
|
2584
|
-
if (
|
|
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 =
|
|
2593
|
-
return
|
|
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
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
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
|
-
|
|
2985
|
+
process.stdout.write(`PR_URL=FAILED: config error: ${msg}
|
|
2739
2986
|
`);
|
|
2740
|
-
|
|
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
|
+
"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",
|