@kody-ade/kody-engine 0.1.7 → 0.2.0
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/LICENSE +21 -0
- package/README.md +28 -61
- package/dist/bin/kody2.js +2512 -0
- package/dist/executables/build/profile.json +83 -0
- package/dist/executables/build/prompts/fix-ci.md +42 -0
- package/dist/executables/build/prompts/fix.md +40 -0
- package/dist/executables/build/prompts/resolve.md +34 -0
- package/dist/executables/build/prompts/run.md +31 -0
- package/dist/executables/types.ts +154 -0
- package/kody.config.schema.json +406 -0
- package/package.json +23 -28
- package/templates/kody2.yml +57 -0
- package/dist/bin/cli.mjs +0 -10781
- package/dist/bin/cli.mjs.map +0 -1
- package/opencode/agents/admin-expert.md +0 -73
- package/opencode/agents/advisor.md +0 -128
- package/opencode/agents/architect.md +0 -193
- package/opencode/agents/autofix.md +0 -103
- package/opencode/agents/build-delegation-test.md +0 -93
- package/opencode/agents/build-delegation.md +0 -98
- package/opencode/agents/build-manager.md +0 -212
- package/opencode/agents/build.md +0 -266
- package/opencode/agents/clarify.md +0 -84
- package/opencode/agents/code-reviewer.md +0 -42
- package/opencode/agents/commit.md +0 -27
- package/opencode/agents/docs.md +0 -123
- package/opencode/agents/domain/admin-expert.md +0 -43
- package/opencode/agents/domain/llm-expert.md +0 -55
- package/opencode/agents/domain/payload-expert.md +0 -67
- package/opencode/agents/domain/security-auditor.md +0 -62
- package/opencode/agents/domain/ui-expert.md +0 -43
- package/opencode/agents/domain/web-expert.md +0 -45
- package/opencode/agents/e2e-test-writer.md +0 -156
- package/opencode/agents/fix.md +0 -158
- package/opencode/agents/gap.md +0 -206
- package/opencode/agents/kody-expert.md +0 -173
- package/opencode/agents/llm-expert.md +0 -90
- package/opencode/agents/neuron.md +0 -12
- package/opencode/agents/payload-expert.md +0 -32
- package/opencode/agents/plan-gap.md +0 -132
- package/opencode/agents/pr.md +0 -25
- package/opencode/agents/review.md +0 -163
- package/opencode/agents/security-auditor.md +0 -33
- package/opencode/agents/taskify.md +0 -344
- package/opencode/agents/test-writer.md +0 -261
- package/opencode/agents/test.md +0 -142
- package/opencode/agents/verify.md +0 -30
- package/opencode/agents/web-expert.md +0 -63
- package/opencode/docs/BROWSER_AUTOMATION.md +0 -64
- package/opencode/docs/PIPELINE.md +0 -210
- package/opencode/opencode.json +0 -98
- package/templates/kody.yml +0 -312
|
@@ -0,0 +1,2512 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
var LITELLM_DEFAULT_PORT = 4e3;
|
|
7
|
+
var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
|
|
8
|
+
function parseProviderModel(s) {
|
|
9
|
+
const slash = s.indexOf("/");
|
|
10
|
+
if (slash <= 0 || slash === s.length - 1) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Invalid model spec '${s}' \u2014 expected 'provider/model' (e.g. 'minimax/MiniMax-M2.7-highspeed')`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return { provider: s.slice(0, slash), model: s.slice(slash + 1) };
|
|
16
|
+
}
|
|
17
|
+
function providerApiKeyEnvVar(provider) {
|
|
18
|
+
if (provider === "anthropic" || provider === "claude") return "ANTHROPIC_API_KEY";
|
|
19
|
+
return `${provider.toUpperCase()}_API_KEY`;
|
|
20
|
+
}
|
|
21
|
+
function needsLitellmProxy(model) {
|
|
22
|
+
return model.provider !== "claude" && model.provider !== "anthropic";
|
|
23
|
+
}
|
|
24
|
+
function loadConfig(projectDir = process.cwd()) {
|
|
25
|
+
const configPath = path.join(projectDir, "kody.config.json");
|
|
26
|
+
if (!fs.existsSync(configPath)) {
|
|
27
|
+
throw new Error(`kody.config.json not found at ${configPath}`);
|
|
28
|
+
}
|
|
29
|
+
let raw;
|
|
30
|
+
try {
|
|
31
|
+
raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
34
|
+
throw new Error(`kody.config.json is invalid JSON: ${msg}`);
|
|
35
|
+
}
|
|
36
|
+
const quality = raw.quality ?? {};
|
|
37
|
+
const git3 = raw.git ?? {};
|
|
38
|
+
const github = raw.github ?? {};
|
|
39
|
+
const agent = raw.agent ?? {};
|
|
40
|
+
if (!agent.model || typeof agent.model !== "string") {
|
|
41
|
+
throw new Error(`kody.config.json: agent.model is required (e.g. "minimax/MiniMax-M2.7-highspeed")`);
|
|
42
|
+
}
|
|
43
|
+
if (!github.owner || !github.repo) {
|
|
44
|
+
throw new Error(`kody.config.json: github.owner and github.repo are required`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
quality: {
|
|
48
|
+
typecheck: typeof quality.typecheck === "string" ? quality.typecheck : "",
|
|
49
|
+
lint: typeof quality.lint === "string" ? quality.lint : "",
|
|
50
|
+
testUnit: typeof quality.testUnit === "string" ? quality.testUnit : ""
|
|
51
|
+
},
|
|
52
|
+
git: {
|
|
53
|
+
defaultBranch: typeof git3.defaultBranch === "string" ? git3.defaultBranch : "main"
|
|
54
|
+
},
|
|
55
|
+
github: {
|
|
56
|
+
owner: String(github.owner),
|
|
57
|
+
repo: String(github.repo)
|
|
58
|
+
},
|
|
59
|
+
agent: {
|
|
60
|
+
model: String(agent.model)
|
|
61
|
+
},
|
|
62
|
+
issueContext: parseIssueContext(raw.issueContext),
|
|
63
|
+
testRequirements: parseTestRequirements(raw.testRequirements)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function parseIssueContext(raw) {
|
|
67
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
68
|
+
const r = raw;
|
|
69
|
+
const out = {};
|
|
70
|
+
if (typeof r.commentLimit === "number" && r.commentLimit > 0) out.commentLimit = Math.floor(r.commentLimit);
|
|
71
|
+
if (typeof r.commentMaxBytes === "number" && r.commentMaxBytes > 0) out.commentMaxBytes = Math.floor(r.commentMaxBytes);
|
|
72
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
73
|
+
}
|
|
74
|
+
function parseTestRequirements(raw) {
|
|
75
|
+
if (!Array.isArray(raw)) return void 0;
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const item of raw) {
|
|
78
|
+
if (item && typeof item === "object" && typeof item.pattern === "string" && typeof item.requireSibling === "string") {
|
|
79
|
+
out.push({
|
|
80
|
+
pattern: item.pattern,
|
|
81
|
+
requireSibling: item.requireSibling
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out.length > 0 ? out : void 0;
|
|
86
|
+
}
|
|
87
|
+
function getAnthropicApiKeyOrDummy() {
|
|
88
|
+
return process.env.ANTHROPIC_API_KEY || `sk-ant-api03-${"0".repeat(64)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/executor.ts
|
|
92
|
+
import * as fs9 from "fs";
|
|
93
|
+
import * as path8 from "path";
|
|
94
|
+
|
|
95
|
+
// src/profile.ts
|
|
96
|
+
import * as fs2 from "fs";
|
|
97
|
+
import * as path2 from "path";
|
|
98
|
+
var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
|
|
99
|
+
var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
|
|
100
|
+
var ProfileError = class extends Error {
|
|
101
|
+
constructor(profilePath, message) {
|
|
102
|
+
super(`Invalid profile at ${profilePath}:
|
|
103
|
+
${message}`);
|
|
104
|
+
this.profilePath = profilePath;
|
|
105
|
+
this.name = "ProfileError";
|
|
106
|
+
}
|
|
107
|
+
profilePath;
|
|
108
|
+
};
|
|
109
|
+
function loadProfile(profilePath) {
|
|
110
|
+
if (!fs2.existsSync(profilePath)) {
|
|
111
|
+
throw new ProfileError(profilePath, "file not found");
|
|
112
|
+
}
|
|
113
|
+
let raw;
|
|
114
|
+
try {
|
|
115
|
+
raw = JSON.parse(fs2.readFileSync(profilePath, "utf-8"));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
118
|
+
}
|
|
119
|
+
if (!raw || typeof raw !== "object") {
|
|
120
|
+
throw new ProfileError(profilePath, "profile must be a JSON object");
|
|
121
|
+
}
|
|
122
|
+
const r = raw;
|
|
123
|
+
const profile = {
|
|
124
|
+
name: requireString(profilePath, r, "name"),
|
|
125
|
+
describe: typeof r.describe === "string" ? r.describe : "",
|
|
126
|
+
inputs: parseInputs(profilePath, r.inputs),
|
|
127
|
+
claudeCode: parseClaudeCode(profilePath, r.claudeCode),
|
|
128
|
+
cliTools: parseCliTools(profilePath, r.cliTools),
|
|
129
|
+
scripts: parseScripts(profilePath, r.scripts),
|
|
130
|
+
outputContract: r.outputContract,
|
|
131
|
+
dir: path2.dirname(profilePath)
|
|
132
|
+
};
|
|
133
|
+
return profile;
|
|
134
|
+
}
|
|
135
|
+
function validateScriptReferences(profile, registeredScripts) {
|
|
136
|
+
const missing = [];
|
|
137
|
+
for (const e of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
|
|
138
|
+
if (!registeredScripts.has(e.script)) missing.push(e.script);
|
|
139
|
+
}
|
|
140
|
+
return missing;
|
|
141
|
+
}
|
|
142
|
+
function requireString(p, r, key) {
|
|
143
|
+
const v = r[key];
|
|
144
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
145
|
+
throw new ProfileError(p, `"${key}" must be a non-empty string`);
|
|
146
|
+
}
|
|
147
|
+
return v;
|
|
148
|
+
}
|
|
149
|
+
function parseInputs(p, raw) {
|
|
150
|
+
if (!Array.isArray(raw)) throw new ProfileError(p, `"inputs" must be an array`);
|
|
151
|
+
const out = [];
|
|
152
|
+
for (const [i, item] of raw.entries()) {
|
|
153
|
+
if (!item || typeof item !== "object") {
|
|
154
|
+
throw new ProfileError(p, `inputs[${i}] must be an object`);
|
|
155
|
+
}
|
|
156
|
+
const r = item;
|
|
157
|
+
const name = requireString(p, r, "name");
|
|
158
|
+
const flag = requireString(p, r, "flag");
|
|
159
|
+
const type = requireString(p, r, "type");
|
|
160
|
+
if (!VALID_INPUT_TYPES.has(type)) {
|
|
161
|
+
throw new ProfileError(p, `inputs[${i}].type must be one of int|string|bool|enum`);
|
|
162
|
+
}
|
|
163
|
+
const spec = {
|
|
164
|
+
name,
|
|
165
|
+
flag,
|
|
166
|
+
type,
|
|
167
|
+
describe: typeof r.describe === "string" ? r.describe : ""
|
|
168
|
+
};
|
|
169
|
+
if (type === "enum") {
|
|
170
|
+
if (!Array.isArray(r.values) || r.values.length === 0) {
|
|
171
|
+
throw new ProfileError(p, `inputs[${i}] (enum) requires non-empty "values" array`);
|
|
172
|
+
}
|
|
173
|
+
spec.values = r.values;
|
|
174
|
+
}
|
|
175
|
+
if (typeof r.required === "boolean") spec.required = r.required;
|
|
176
|
+
if (r.requiredWhen && typeof r.requiredWhen === "object") {
|
|
177
|
+
spec.requiredWhen = r.requiredWhen;
|
|
178
|
+
}
|
|
179
|
+
out.push(spec);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
function parseClaudeCode(p, raw) {
|
|
184
|
+
if (!raw || typeof raw !== "object") {
|
|
185
|
+
throw new ProfileError(p, `"claudeCode" must be an object`);
|
|
186
|
+
}
|
|
187
|
+
const r = raw;
|
|
188
|
+
const permissionMode = typeof r.permissionMode === "string" ? r.permissionMode : "acceptEdits";
|
|
189
|
+
if (!VALID_PERMISSION_MODES.has(permissionMode)) {
|
|
190
|
+
throw new ProfileError(p, `claudeCode.permissionMode must be one of default|acceptEdits|plan|bypassPermissions`);
|
|
191
|
+
}
|
|
192
|
+
const tools = Array.isArray(r.tools) ? r.tools : [];
|
|
193
|
+
if (tools.length === 0) {
|
|
194
|
+
throw new ProfileError(p, `claudeCode.tools must declare at least one SDK tool`);
|
|
195
|
+
}
|
|
196
|
+
const hooksRaw = r.hooks ?? {};
|
|
197
|
+
const hooks = {
|
|
198
|
+
PreToolUse: Array.isArray(hooksRaw.PreToolUse) ? hooksRaw.PreToolUse : [],
|
|
199
|
+
PostToolUse: Array.isArray(hooksRaw.PostToolUse) ? hooksRaw.PostToolUse : [],
|
|
200
|
+
Stop: Array.isArray(hooksRaw.Stop) ? hooksRaw.Stop : []
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
model: typeof r.model === "string" ? r.model : "inherit",
|
|
204
|
+
permissionMode,
|
|
205
|
+
maxTurns: typeof r.maxTurns === "number" ? r.maxTurns : null,
|
|
206
|
+
systemPromptAppend: typeof r.systemPromptAppend === "string" ? r.systemPromptAppend : null,
|
|
207
|
+
tools,
|
|
208
|
+
hooks,
|
|
209
|
+
skills: Array.isArray(r.skills) ? r.skills : [],
|
|
210
|
+
commands: Array.isArray(r.commands) ? r.commands : [],
|
|
211
|
+
subagents: Array.isArray(r.subagents) ? r.subagents : [],
|
|
212
|
+
plugins: Array.isArray(r.plugins) ? r.plugins : [],
|
|
213
|
+
mcpServers: Array.isArray(r.mcpServers) ? r.mcpServers : []
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function parseCliTools(p, raw) {
|
|
217
|
+
if (raw === void 0 || raw === null) return [];
|
|
218
|
+
if (!Array.isArray(raw)) throw new ProfileError(p, `"cliTools" must be an array or absent`);
|
|
219
|
+
const out = [];
|
|
220
|
+
for (const [i, item] of raw.entries()) {
|
|
221
|
+
if (!item || typeof item !== "object") {
|
|
222
|
+
throw new ProfileError(p, `cliTools[${i}] must be an object`);
|
|
223
|
+
}
|
|
224
|
+
const r = item;
|
|
225
|
+
const install = r.install;
|
|
226
|
+
if (!install || typeof install !== "object") {
|
|
227
|
+
throw new ProfileError(p, `cliTools[${i}].install must be an object`);
|
|
228
|
+
}
|
|
229
|
+
out.push({
|
|
230
|
+
name: requireString(p, r, "name"),
|
|
231
|
+
install: {
|
|
232
|
+
required: Boolean(install.required),
|
|
233
|
+
checkCommand: requireString(p, install, "checkCommand"),
|
|
234
|
+
installCommand: typeof install.installCommand === "string" ? install.installCommand : void 0
|
|
235
|
+
},
|
|
236
|
+
verify: requireString(p, r, "verify"),
|
|
237
|
+
usage: typeof r.usage === "string" ? r.usage : "",
|
|
238
|
+
allowedUses: Array.isArray(r.allowedUses) ? r.allowedUses : []
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
function parseScripts(p, raw) {
|
|
244
|
+
if (!raw || typeof raw !== "object") {
|
|
245
|
+
throw new ProfileError(p, `"scripts" must be an object with preflight and postflight arrays`);
|
|
246
|
+
}
|
|
247
|
+
const r = raw;
|
|
248
|
+
return {
|
|
249
|
+
preflight: parseScriptList(p, "preflight", r.preflight),
|
|
250
|
+
postflight: parseScriptList(p, "postflight", r.postflight)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function parseScriptList(p, key, raw) {
|
|
254
|
+
if (!Array.isArray(raw)) {
|
|
255
|
+
throw new ProfileError(p, `scripts.${key} must be an array`);
|
|
256
|
+
}
|
|
257
|
+
const out = [];
|
|
258
|
+
for (const [i, item] of raw.entries()) {
|
|
259
|
+
if (!item || typeof item !== "object") {
|
|
260
|
+
throw new ProfileError(p, `scripts.${key}[${i}] must be an object like { script, runWhen? }`);
|
|
261
|
+
}
|
|
262
|
+
const r = item;
|
|
263
|
+
const script = requireString(p, r, "script");
|
|
264
|
+
const entry = { script };
|
|
265
|
+
if (r.runWhen && typeof r.runWhen === "object") {
|
|
266
|
+
entry.runWhen = r.runWhen;
|
|
267
|
+
}
|
|
268
|
+
out.push(entry);
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/issue.ts
|
|
274
|
+
import { execFileSync } from "child_process";
|
|
275
|
+
var API_TIMEOUT_MS = 3e4;
|
|
276
|
+
function ghToken() {
|
|
277
|
+
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
278
|
+
}
|
|
279
|
+
function gh(args, options) {
|
|
280
|
+
const token = ghToken();
|
|
281
|
+
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
282
|
+
return execFileSync("gh", args, {
|
|
283
|
+
encoding: "utf-8",
|
|
284
|
+
timeout: API_TIMEOUT_MS,
|
|
285
|
+
cwd: options?.cwd,
|
|
286
|
+
env,
|
|
287
|
+
input: options?.input,
|
|
288
|
+
stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
289
|
+
}).trim();
|
|
290
|
+
}
|
|
291
|
+
function getIssue(issueNumber, cwd) {
|
|
292
|
+
const output = gh(
|
|
293
|
+
["issue", "view", String(issueNumber), "--json", "number,title,body,comments"],
|
|
294
|
+
{ cwd }
|
|
295
|
+
);
|
|
296
|
+
const parsed = JSON.parse(output);
|
|
297
|
+
if (typeof parsed?.title !== "string") {
|
|
298
|
+
throw new Error(`Issue #${issueNumber}: unexpected response shape`);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
number: parsed.number ?? issueNumber,
|
|
302
|
+
title: parsed.title,
|
|
303
|
+
body: parsed.body ?? "",
|
|
304
|
+
comments: (parsed.comments ?? []).map((c) => ({
|
|
305
|
+
body: c.body ?? "",
|
|
306
|
+
author: c.author?.login ?? "unknown",
|
|
307
|
+
createdAt: c.createdAt ?? ""
|
|
308
|
+
}))
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function postIssueComment(issueNumber, body, cwd) {
|
|
312
|
+
try {
|
|
313
|
+
gh(
|
|
314
|
+
["issue", "comment", String(issueNumber), "--body-file", "-"],
|
|
315
|
+
{ input: body, cwd }
|
|
316
|
+
);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
process.stderr.write(`[kody2] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
319
|
+
`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function truncate(s, maxBytes) {
|
|
323
|
+
if (s.length <= maxBytes) return s;
|
|
324
|
+
return s.slice(0, maxBytes) + `\u2026 (+${s.length - maxBytes} chars)`;
|
|
325
|
+
}
|
|
326
|
+
function getPr(prNumber, cwd) {
|
|
327
|
+
const output = gh(
|
|
328
|
+
["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"],
|
|
329
|
+
{ cwd }
|
|
330
|
+
);
|
|
331
|
+
const parsed = JSON.parse(output);
|
|
332
|
+
if (typeof parsed?.title !== "string") {
|
|
333
|
+
throw new Error(`PR #${prNumber}: unexpected response shape`);
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
number: parsed.number ?? prNumber,
|
|
337
|
+
title: parsed.title,
|
|
338
|
+
body: parsed.body ?? "",
|
|
339
|
+
headRefName: String(parsed.headRefName ?? ""),
|
|
340
|
+
baseRefName: String(parsed.baseRefName ?? ""),
|
|
341
|
+
state: String(parsed.state ?? "")
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function getPrDiff(prNumber, cwd) {
|
|
345
|
+
try {
|
|
346
|
+
return gh(["pr", "diff", String(prNumber)], { cwd });
|
|
347
|
+
} catch (err) {
|
|
348
|
+
process.stderr.write(`[kody2] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
349
|
+
`);
|
|
350
|
+
return "";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function getPrReviews(prNumber, cwd) {
|
|
354
|
+
try {
|
|
355
|
+
const output = gh(
|
|
356
|
+
["pr", "view", String(prNumber), "--json", "reviews"],
|
|
357
|
+
{ cwd }
|
|
358
|
+
);
|
|
359
|
+
const parsed = JSON.parse(output);
|
|
360
|
+
if (!Array.isArray(parsed?.reviews)) return [];
|
|
361
|
+
return parsed.reviews.map((r) => ({
|
|
362
|
+
body: r.body ?? "",
|
|
363
|
+
state: r.state ?? "",
|
|
364
|
+
author: r.author?.login ?? "unknown",
|
|
365
|
+
submittedAt: r.submittedAt ?? ""
|
|
366
|
+
}));
|
|
367
|
+
} catch {
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function getPrComments(prNumber, cwd) {
|
|
372
|
+
try {
|
|
373
|
+
const output = gh(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
|
|
374
|
+
const parsed = JSON.parse(output);
|
|
375
|
+
if (!Array.isArray(parsed?.comments)) return [];
|
|
376
|
+
return parsed.comments.map((c) => ({
|
|
377
|
+
body: c.body ?? "",
|
|
378
|
+
author: c.author?.login ?? "unknown",
|
|
379
|
+
createdAt: c.createdAt ?? ""
|
|
380
|
+
})).filter((c) => c.body.trim().length > 0);
|
|
381
|
+
} catch {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
var KODY_COMMENT_PREFIXES = ["\u2699\uFE0F kody2", "\u2705 kody2", "\u26A0\uFE0F kody2", "\u2139\uFE0F kody2", "\u2192 kody2"];
|
|
386
|
+
function getPrLatestReviewBody(prNumber, cwd) {
|
|
387
|
+
const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
|
|
388
|
+
const comments = getPrComments(prNumber, cwd).filter((c) => !KODY_COMMENT_PREFIXES.some((p) => c.body.startsWith(p))).map((c) => ({ body: c.body, at: c.createdAt }));
|
|
389
|
+
const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
|
|
390
|
+
if (all.length > 0) return all[0].body;
|
|
391
|
+
const pr = getPr(prNumber, cwd);
|
|
392
|
+
return pr.body;
|
|
393
|
+
}
|
|
394
|
+
function postPrReviewComment(prNumber, body, cwd) {
|
|
395
|
+
try {
|
|
396
|
+
gh(["pr", "comment", String(prNumber), "--body-file", "-"], { input: body, cwd });
|
|
397
|
+
} catch (err) {
|
|
398
|
+
process.stderr.write(`[kody2] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
399
|
+
`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/branch.ts
|
|
404
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
405
|
+
var UncommittedChangesError = class extends Error {
|
|
406
|
+
constructor(branch) {
|
|
407
|
+
super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
|
|
408
|
+
this.branch = branch;
|
|
409
|
+
this.name = "UncommittedChangesError";
|
|
410
|
+
}
|
|
411
|
+
branch;
|
|
412
|
+
};
|
|
413
|
+
function git(args, cwd) {
|
|
414
|
+
return execFileSync2("git", args, {
|
|
415
|
+
encoding: "utf-8",
|
|
416
|
+
timeout: 3e4,
|
|
417
|
+
cwd,
|
|
418
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
419
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
420
|
+
}).trim();
|
|
421
|
+
}
|
|
422
|
+
function deriveBranchName(issueNumber, title) {
|
|
423
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50).replace(/-$/, "");
|
|
424
|
+
return slug ? `${issueNumber}-${slug}` : `${issueNumber}`;
|
|
425
|
+
}
|
|
426
|
+
function getCurrentBranch(cwd) {
|
|
427
|
+
return git(["branch", "--show-current"], cwd);
|
|
428
|
+
}
|
|
429
|
+
function hasUncommittedChanges(cwd) {
|
|
430
|
+
return git(["status", "--porcelain", "--untracked-files=no"], cwd).length > 0;
|
|
431
|
+
}
|
|
432
|
+
function checkoutPrBranch(prNumber, cwd) {
|
|
433
|
+
const env = {
|
|
434
|
+
...process.env,
|
|
435
|
+
HUSKY: "0",
|
|
436
|
+
SKIP_HOOKS: "1",
|
|
437
|
+
GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
|
|
438
|
+
};
|
|
439
|
+
execFileSync2("gh", ["pr", "checkout", String(prNumber)], {
|
|
440
|
+
cwd,
|
|
441
|
+
env,
|
|
442
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
443
|
+
timeout: 6e4
|
|
444
|
+
});
|
|
445
|
+
return getCurrentBranch(cwd);
|
|
446
|
+
}
|
|
447
|
+
function mergeBase(baseBranch, cwd) {
|
|
448
|
+
try {
|
|
449
|
+
git(["fetch", "origin", baseBranch], cwd);
|
|
450
|
+
} catch {
|
|
451
|
+
return "error";
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
git(["merge", `origin/${baseBranch}`, "--no-edit", "--no-ff"], cwd);
|
|
455
|
+
return "clean";
|
|
456
|
+
} catch {
|
|
457
|
+
try {
|
|
458
|
+
const unmerged = git(["diff", "--name-only", "--diff-filter=U"], cwd);
|
|
459
|
+
if (unmerged.length > 0) return "conflict";
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
git(["merge", "--abort"], cwd);
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
return "error";
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
470
|
+
const branchName = deriveBranchName(issueNumber, title);
|
|
471
|
+
const current = getCurrentBranch(cwd);
|
|
472
|
+
if (current === branchName) {
|
|
473
|
+
if (hasUncommittedChanges(cwd)) throw new UncommittedChangesError(branchName);
|
|
474
|
+
return { branch: branchName, created: false };
|
|
475
|
+
}
|
|
476
|
+
if (hasUncommittedChanges(cwd)) throw new UncommittedChangesError(current || "(detached)");
|
|
477
|
+
try {
|
|
478
|
+
git(["fetch", "origin"], cwd);
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
git(["rev-parse", "--verify", `origin/${branchName}`], cwd);
|
|
483
|
+
git(["checkout", branchName], cwd);
|
|
484
|
+
try {
|
|
485
|
+
git(["pull", "origin", branchName], cwd);
|
|
486
|
+
} catch {
|
|
487
|
+
}
|
|
488
|
+
return { branch: branchName, created: false };
|
|
489
|
+
} catch {
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
git(["rev-parse", "--verify", branchName], cwd);
|
|
493
|
+
git(["checkout", branchName], cwd);
|
|
494
|
+
return { branch: branchName, created: false };
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
git(["checkout", "-b", branchName, `origin/${defaultBranch}`], cwd);
|
|
499
|
+
} catch {
|
|
500
|
+
git(["checkout", "-b", branchName], cwd);
|
|
501
|
+
}
|
|
502
|
+
return { branch: branchName, created: true };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/gha.ts
|
|
506
|
+
import * as fs3 from "fs";
|
|
507
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
508
|
+
function getRunUrl() {
|
|
509
|
+
const server = process.env.GITHUB_SERVER_URL;
|
|
510
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
511
|
+
const runId = process.env.GITHUB_RUN_ID;
|
|
512
|
+
if (!server || !repo || !runId) return "";
|
|
513
|
+
return `${server}/${repo}/actions/runs/${runId}`;
|
|
514
|
+
}
|
|
515
|
+
function reactToTriggerComment(cwd) {
|
|
516
|
+
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
517
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
518
|
+
if (!eventPath || !fs3.existsSync(eventPath)) return;
|
|
519
|
+
let event = null;
|
|
520
|
+
try {
|
|
521
|
+
event = JSON.parse(fs3.readFileSync(eventPath, "utf-8"));
|
|
522
|
+
} catch {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const commentId = event?.comment?.id;
|
|
526
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
527
|
+
if (!commentId || !repo) return;
|
|
528
|
+
const token = process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
529
|
+
try {
|
|
530
|
+
execFileSync3(
|
|
531
|
+
"gh",
|
|
532
|
+
[
|
|
533
|
+
"api",
|
|
534
|
+
"-X",
|
|
535
|
+
"POST",
|
|
536
|
+
"-H",
|
|
537
|
+
"Accept: application/vnd.github+json",
|
|
538
|
+
`/repos/${repo}/issues/comments/${commentId}/reactions`,
|
|
539
|
+
"-f",
|
|
540
|
+
"content=eyes"
|
|
541
|
+
],
|
|
542
|
+
{
|
|
543
|
+
cwd,
|
|
544
|
+
env: { ...process.env, GH_TOKEN: token ?? process.env.GH_TOKEN ?? "" },
|
|
545
|
+
stdio: "pipe",
|
|
546
|
+
timeout: 15e3
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/scripts/runFlow.ts
|
|
554
|
+
var runFlow = async (ctx) => {
|
|
555
|
+
const issueNumber = ctx.args.issue;
|
|
556
|
+
const issue = getIssue(issueNumber, ctx.cwd);
|
|
557
|
+
ctx.data.issue = issue;
|
|
558
|
+
ctx.data.commentTargetType = "issue";
|
|
559
|
+
ctx.data.commentTargetNumber = issueNumber;
|
|
560
|
+
try {
|
|
561
|
+
const branchInfo = ensureFeatureBranch(issueNumber, issue.title, ctx.config.git.defaultBranch, ctx.cwd);
|
|
562
|
+
ctx.data.branch = branchInfo.branch;
|
|
563
|
+
} catch (err) {
|
|
564
|
+
if (err instanceof UncommittedChangesError) {
|
|
565
|
+
ctx.output.exitCode = 5;
|
|
566
|
+
ctx.output.reason = err.message;
|
|
567
|
+
ctx.skipAgent = true;
|
|
568
|
+
tryPost(issueNumber, `\u26A0\uFE0F kody2 refused to start: ${err.message}`, ctx.cwd);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
throw err;
|
|
572
|
+
}
|
|
573
|
+
const runUrl = getRunUrl();
|
|
574
|
+
const startMsg = runUrl ? `\u2699\uFE0F kody2 started \u2014 branch \`${ctx.data.branch}\`, run ${runUrl}` : `\u2699\uFE0F kody2 started \u2014 branch \`${ctx.data.branch}\``;
|
|
575
|
+
tryPost(issueNumber, startMsg, ctx.cwd);
|
|
576
|
+
};
|
|
577
|
+
function tryPost(issueNumber, body, cwd) {
|
|
578
|
+
try {
|
|
579
|
+
postIssueComment(issueNumber, body, cwd);
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/scripts/fixFlow.ts
|
|
585
|
+
var fixFlow = async (ctx) => {
|
|
586
|
+
const prNumber = ctx.args.pr;
|
|
587
|
+
const pr = getPr(prNumber, ctx.cwd);
|
|
588
|
+
if (pr.state !== "OPEN") {
|
|
589
|
+
ctx.output.exitCode = 1;
|
|
590
|
+
ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
|
|
591
|
+
ctx.skipAgent = true;
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
ctx.data.pr = pr;
|
|
595
|
+
ctx.data.commentTargetType = "pr";
|
|
596
|
+
ctx.data.commentTargetNumber = prNumber;
|
|
597
|
+
checkoutPrBranch(prNumber, ctx.cwd);
|
|
598
|
+
ctx.data.branch = getCurrentBranch(ctx.cwd);
|
|
599
|
+
const inlineFeedback = ctx.args.feedback?.trim();
|
|
600
|
+
const feedback = inlineFeedback || getPrLatestReviewBody(prNumber, ctx.cwd);
|
|
601
|
+
if (!feedback.trim()) {
|
|
602
|
+
ctx.output.exitCode = 1;
|
|
603
|
+
ctx.output.reason = "no --feedback provided and no review/body text found on PR";
|
|
604
|
+
ctx.skipAgent = true;
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
ctx.data.feedback = feedback;
|
|
608
|
+
ctx.data.prDiff = getPrDiff(prNumber, ctx.cwd);
|
|
609
|
+
const runUrl = getRunUrl();
|
|
610
|
+
const runSuffix = runUrl ? `, run ${runUrl}` : "";
|
|
611
|
+
tryPostPr(
|
|
612
|
+
prNumber,
|
|
613
|
+
`\u2699\uFE0F kody2 fix started on \`${ctx.data.branch}\`${runSuffix} \u2014 applying feedback (${truncate(feedback.replace(/\n/g, " "), 200)})`,
|
|
614
|
+
ctx.cwd
|
|
615
|
+
);
|
|
616
|
+
};
|
|
617
|
+
function tryPostPr(prNumber, body, cwd) {
|
|
618
|
+
try {
|
|
619
|
+
postPrReviewComment(prNumber, body, cwd);
|
|
620
|
+
} catch {
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/workflow.ts
|
|
625
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
626
|
+
var GH_TIMEOUT_MS = 3e4;
|
|
627
|
+
function ghToken2() {
|
|
628
|
+
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
629
|
+
}
|
|
630
|
+
function gh2(args, cwd) {
|
|
631
|
+
const token = ghToken2();
|
|
632
|
+
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
633
|
+
return execFileSync4("gh", args, {
|
|
634
|
+
encoding: "utf-8",
|
|
635
|
+
timeout: GH_TIMEOUT_MS,
|
|
636
|
+
cwd,
|
|
637
|
+
env,
|
|
638
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
639
|
+
}).trim();
|
|
640
|
+
}
|
|
641
|
+
function getLatestFailedRunForPr(prNumber, cwd) {
|
|
642
|
+
let headBranch;
|
|
643
|
+
try {
|
|
644
|
+
const out = gh2(["pr", "view", String(prNumber), "--json", "headRefName"], cwd);
|
|
645
|
+
headBranch = JSON.parse(out).headRefName;
|
|
646
|
+
} catch {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
if (!headBranch) return null;
|
|
650
|
+
try {
|
|
651
|
+
const out = gh2(
|
|
652
|
+
[
|
|
653
|
+
"run",
|
|
654
|
+
"list",
|
|
655
|
+
"--branch",
|
|
656
|
+
headBranch,
|
|
657
|
+
"--status",
|
|
658
|
+
"failure",
|
|
659
|
+
"--limit",
|
|
660
|
+
"1",
|
|
661
|
+
"--json",
|
|
662
|
+
"databaseId,workflowName,headBranch,conclusion,url,createdAt"
|
|
663
|
+
],
|
|
664
|
+
cwd
|
|
665
|
+
);
|
|
666
|
+
const parsed = JSON.parse(out);
|
|
667
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return null;
|
|
668
|
+
const r = parsed[0];
|
|
669
|
+
return {
|
|
670
|
+
id: String(r.databaseId ?? ""),
|
|
671
|
+
workflowName: r.workflowName ?? "",
|
|
672
|
+
headBranch: r.headBranch ?? headBranch,
|
|
673
|
+
conclusion: r.conclusion ?? "failure",
|
|
674
|
+
url: r.url ?? "",
|
|
675
|
+
createdAt: r.createdAt ?? ""
|
|
676
|
+
};
|
|
677
|
+
} catch {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function getFailedRunLogTail(runId, maxBytes, cwd) {
|
|
682
|
+
try {
|
|
683
|
+
const raw = gh2(["run", "view", String(runId), "--log-failed"], cwd);
|
|
684
|
+
if (raw.length <= maxBytes) return raw;
|
|
685
|
+
return raw.slice(-maxBytes);
|
|
686
|
+
} catch {
|
|
687
|
+
return "";
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/scripts/fixCiFlow.ts
|
|
692
|
+
var LOG_MAX_BYTES = 3e4;
|
|
693
|
+
var fixCiFlow = async (ctx) => {
|
|
694
|
+
const prNumber = ctx.args.pr;
|
|
695
|
+
const pr = getPr(prNumber, ctx.cwd);
|
|
696
|
+
if (pr.state !== "OPEN") {
|
|
697
|
+
ctx.output.exitCode = 1;
|
|
698
|
+
ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
|
|
699
|
+
ctx.skipAgent = true;
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
ctx.data.pr = pr;
|
|
703
|
+
ctx.data.commentTargetType = "pr";
|
|
704
|
+
ctx.data.commentTargetNumber = prNumber;
|
|
705
|
+
checkoutPrBranch(prNumber, ctx.cwd);
|
|
706
|
+
ctx.data.branch = getCurrentBranch(ctx.cwd);
|
|
707
|
+
let runId = ctx.args.runId;
|
|
708
|
+
let workflowName = "";
|
|
709
|
+
let failedRunUrl = "";
|
|
710
|
+
if (!runId) {
|
|
711
|
+
const run = getLatestFailedRunForPr(prNumber, ctx.cwd);
|
|
712
|
+
if (!run) {
|
|
713
|
+
ctx.output.exitCode = 1;
|
|
714
|
+
ctx.output.reason = `no failed workflow run found on PR #${prNumber}'s branch`;
|
|
715
|
+
ctx.skipAgent = true;
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
runId = run.id;
|
|
719
|
+
workflowName = run.workflowName;
|
|
720
|
+
failedRunUrl = run.url;
|
|
721
|
+
}
|
|
722
|
+
const logTail = getFailedRunLogTail(runId, LOG_MAX_BYTES, ctx.cwd);
|
|
723
|
+
if (!logTail) {
|
|
724
|
+
ctx.output.exitCode = 1;
|
|
725
|
+
ctx.output.reason = `failed to fetch log tail for run ${runId}`;
|
|
726
|
+
ctx.skipAgent = true;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
ctx.data.failedRunId = runId;
|
|
730
|
+
ctx.data.failedWorkflowName = workflowName;
|
|
731
|
+
ctx.data.failedRunUrl = failedRunUrl;
|
|
732
|
+
ctx.data.failedLogTail = logTail;
|
|
733
|
+
ctx.data.prDiff = getPrDiff(prNumber, ctx.cwd);
|
|
734
|
+
const runUrl = getRunUrl();
|
|
735
|
+
const runSuffix = runUrl ? `, kody2 run ${runUrl}` : "";
|
|
736
|
+
tryPostPr2(prNumber, `\u2699\uFE0F kody2 fix-ci started on \`${ctx.data.branch}\`${runSuffix} \u2014 analyzing workflow run ${runId}`, ctx.cwd);
|
|
737
|
+
};
|
|
738
|
+
function tryPostPr2(prNumber, body, cwd) {
|
|
739
|
+
try {
|
|
740
|
+
postPrReviewComment(prNumber, body, cwd);
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/scripts/resolveFlow.ts
|
|
746
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
747
|
+
var CONFLICT_DIFF_MAX_BYTES = 4e4;
|
|
748
|
+
var resolveFlow = async (ctx) => {
|
|
749
|
+
const prNumber = ctx.args.pr;
|
|
750
|
+
const pr = getPr(prNumber, ctx.cwd);
|
|
751
|
+
if (pr.state !== "OPEN") {
|
|
752
|
+
ctx.output.exitCode = 1;
|
|
753
|
+
ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
|
|
754
|
+
ctx.skipAgent = true;
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
ctx.data.pr = pr;
|
|
758
|
+
ctx.data.commentTargetType = "pr";
|
|
759
|
+
ctx.data.commentTargetNumber = prNumber;
|
|
760
|
+
checkoutPrBranch(prNumber, ctx.cwd);
|
|
761
|
+
ctx.data.branch = getCurrentBranch(ctx.cwd);
|
|
762
|
+
const baseBranch = pr.baseRefName || ctx.config.git.defaultBranch;
|
|
763
|
+
ctx.data.baseBranch = baseBranch;
|
|
764
|
+
const mergeStatus = mergeBase(baseBranch, ctx.cwd);
|
|
765
|
+
if (mergeStatus === "clean") {
|
|
766
|
+
ctx.output.exitCode = 0;
|
|
767
|
+
ctx.output.reason = `already up to date with origin/${baseBranch} \u2014 nothing to resolve`;
|
|
768
|
+
ctx.skipAgent = true;
|
|
769
|
+
tryPostPr3(prNumber, `\u2139\uFE0F kody2 resolve: ${ctx.output.reason}`, ctx.cwd);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (mergeStatus === "error") {
|
|
773
|
+
ctx.output.exitCode = 99;
|
|
774
|
+
ctx.output.reason = `failed to merge origin/${baseBranch} (non-conflict error); see runner log`;
|
|
775
|
+
ctx.skipAgent = true;
|
|
776
|
+
tryPostPr3(prNumber, `\u26A0\uFE0F kody2 resolve FAILED: ${ctx.output.reason}`, ctx.cwd);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const conflictedFiles = getConflictedFiles(ctx.cwd);
|
|
780
|
+
if (conflictedFiles.length === 0) {
|
|
781
|
+
ctx.output.exitCode = 99;
|
|
782
|
+
ctx.output.reason = "merge reported conflict but no unmerged paths detected";
|
|
783
|
+
ctx.skipAgent = true;
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
ctx.data.conflictedFiles = conflictedFiles;
|
|
787
|
+
ctx.data.conflictMarkersPreview = getConflictMarkersPreview(conflictedFiles, ctx.cwd);
|
|
788
|
+
const runUrl = getRunUrl();
|
|
789
|
+
const runSuffix = runUrl ? `, run ${runUrl}` : "";
|
|
790
|
+
tryPostPr3(prNumber, `\u2699\uFE0F kody2 resolve started on \`${ctx.data.branch}\`${runSuffix} \u2014 ${conflictedFiles.length} conflicted file(s)`, ctx.cwd);
|
|
791
|
+
};
|
|
792
|
+
function getConflictedFiles(cwd) {
|
|
793
|
+
try {
|
|
794
|
+
const out = execFileSync5("git", ["diff", "--name-only", "--diff-filter=U"], {
|
|
795
|
+
encoding: "utf-8",
|
|
796
|
+
cwd,
|
|
797
|
+
env: { ...process.env, HUSKY: "0" }
|
|
798
|
+
}).trim();
|
|
799
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
800
|
+
} catch {
|
|
801
|
+
return [];
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTES) {
|
|
805
|
+
const chunks = [];
|
|
806
|
+
let total = 0;
|
|
807
|
+
for (const f of files) {
|
|
808
|
+
try {
|
|
809
|
+
const content = execFileSync5("cat", [f], { encoding: "utf-8", cwd }).toString();
|
|
810
|
+
const snippet = `### ${f}
|
|
811
|
+
|
|
812
|
+
\`\`\`
|
|
813
|
+
${content.slice(0, 6e3)}
|
|
814
|
+
\`\`\`
|
|
815
|
+
`;
|
|
816
|
+
total += snippet.length;
|
|
817
|
+
chunks.push(snippet);
|
|
818
|
+
if (total >= maxBytes) break;
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return chunks.join("\n");
|
|
823
|
+
}
|
|
824
|
+
function tryPostPr3(prNumber, body, cwd) {
|
|
825
|
+
try {
|
|
826
|
+
postPrReviewComment(prNumber, body, cwd);
|
|
827
|
+
} catch {
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/prompt.ts
|
|
832
|
+
import * as fs4 from "fs";
|
|
833
|
+
import * as path3 from "path";
|
|
834
|
+
var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
|
|
835
|
+
var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
836
|
+
function loadProjectConventions(projectDir) {
|
|
837
|
+
const out = [];
|
|
838
|
+
for (const rel of CONVENTION_FILES) {
|
|
839
|
+
const abs = path3.join(projectDir, rel);
|
|
840
|
+
if (!fs4.existsSync(abs)) continue;
|
|
841
|
+
let content;
|
|
842
|
+
try {
|
|
843
|
+
content = fs4.readFileSync(abs, "utf-8");
|
|
844
|
+
} catch {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const truncated = content.length > CONVENTIONS_PER_FILE_MAX_BYTES;
|
|
848
|
+
if (truncated) content = content.slice(0, CONVENTIONS_PER_FILE_MAX_BYTES) + "\n\n\u2026 (truncated)";
|
|
849
|
+
out.push({ path: rel, content, truncated });
|
|
850
|
+
}
|
|
851
|
+
return out;
|
|
852
|
+
}
|
|
853
|
+
function parseAgentResult(finalText) {
|
|
854
|
+
const text = (finalText || "").trim();
|
|
855
|
+
if (!text) return { done: false, commitMessage: "", prSummary: "", failureReason: "agent produced no final message" };
|
|
856
|
+
const failedMatch = text.match(/(?:^|\n)\s*FAILED\s*:\s*(.+?)\s*$/s);
|
|
857
|
+
if (failedMatch) {
|
|
858
|
+
return { done: false, commitMessage: "", prSummary: "", failureReason: failedMatch[1].trim() };
|
|
859
|
+
}
|
|
860
|
+
if (!/(^|\n)\s*DONE\b/i.test(text)) {
|
|
861
|
+
return { done: false, commitMessage: "", prSummary: "", failureReason: "no DONE or FAILED marker in agent output" };
|
|
862
|
+
}
|
|
863
|
+
const commitMatch = text.match(/^[ \t]*COMMIT_MSG\s*:\s*(.+)$/im);
|
|
864
|
+
const commitMessage = commitMatch ? commitMatch[1].trim() : "";
|
|
865
|
+
const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
|
|
866
|
+
let prSummary = "";
|
|
867
|
+
if (summaryStart !== -1) {
|
|
868
|
+
const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
|
|
869
|
+
prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
|
|
870
|
+
}
|
|
871
|
+
return { done: true, commitMessage, prSummary, failureReason: "" };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// src/scripts/loadConventions.ts
|
|
875
|
+
var loadConventions = async (ctx) => {
|
|
876
|
+
const conventions = loadProjectConventions(ctx.cwd);
|
|
877
|
+
ctx.data.conventions = conventions;
|
|
878
|
+
if (conventions.length > 0) {
|
|
879
|
+
process.stderr.write(`[kody2] loaded conventions: ${conventions.map((c) => c.path).join(", ")}
|
|
880
|
+
`);
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
// src/scripts/loadCoverageRules.ts
|
|
885
|
+
var loadCoverageRules = async (ctx) => {
|
|
886
|
+
ctx.data.coverageRules = ctx.config.testRequirements ?? [];
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// src/scripts/composePrompt.ts
|
|
890
|
+
import * as fs5 from "fs";
|
|
891
|
+
import * as path4 from "path";
|
|
892
|
+
var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
893
|
+
var composePrompt = async (ctx, profile) => {
|
|
894
|
+
const explicit = ctx.data.promptTemplate;
|
|
895
|
+
const mode = ctx.args.mode;
|
|
896
|
+
const candidates = [
|
|
897
|
+
explicit ? path4.join(profile.dir, explicit) : null,
|
|
898
|
+
mode ? path4.join(profile.dir, "prompts", `${mode}.md`) : null,
|
|
899
|
+
path4.join(profile.dir, "prompt.md")
|
|
900
|
+
].filter(Boolean);
|
|
901
|
+
let templatePath = "";
|
|
902
|
+
for (const c of candidates) {
|
|
903
|
+
if (fs5.existsSync(c)) {
|
|
904
|
+
templatePath = c;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (!templatePath) {
|
|
909
|
+
throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
|
|
910
|
+
}
|
|
911
|
+
const template = fs5.readFileSync(templatePath, "utf-8");
|
|
912
|
+
const tokens = {
|
|
913
|
+
...stringifyAll(ctx.args, "args."),
|
|
914
|
+
...stringifyAll(ctx.data, ""),
|
|
915
|
+
"conventionsBlock": formatConventions(ctx.data.conventions),
|
|
916
|
+
"coverageBlock": formatCoverageBlock(ctx.data.coverageRules),
|
|
917
|
+
"toolsUsage": formatToolsUsage(profile),
|
|
918
|
+
"systemPromptAppend": profile.claudeCode.systemPromptAppend ?? "",
|
|
919
|
+
"repoOwner": ctx.config.github.owner,
|
|
920
|
+
"repoName": ctx.config.github.repo,
|
|
921
|
+
"defaultBranch": ctx.config.git.defaultBranch,
|
|
922
|
+
"branch": ctx.data.branch ?? ""
|
|
923
|
+
};
|
|
924
|
+
ctx.data.prompt = template.replace(MUSTACHE, (_, key) => tokens[key] ?? "");
|
|
925
|
+
};
|
|
926
|
+
function stringifyAll(source, prefix) {
|
|
927
|
+
const out = {};
|
|
928
|
+
for (const [k, v] of Object.entries(source)) {
|
|
929
|
+
const key = prefix + k;
|
|
930
|
+
if (v === null || v === void 0) continue;
|
|
931
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
932
|
+
out[key] = String(v);
|
|
933
|
+
} else if (Array.isArray(v)) {
|
|
934
|
+
out[key] = v.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join("\n");
|
|
935
|
+
} else if (typeof v === "object") {
|
|
936
|
+
for (const [k2, v2] of Object.entries(v)) {
|
|
937
|
+
if (typeof v2 === "string" || typeof v2 === "number" || typeof v2 === "boolean") {
|
|
938
|
+
out[`${key}.${k2}`] = String(v2);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
function formatConventions(conventions) {
|
|
946
|
+
if (!conventions || conventions.length === 0) return "";
|
|
947
|
+
const lines = [
|
|
948
|
+
"# Project conventions (AUTHORITATIVE \u2014 follow these over patterns you infer from code)",
|
|
949
|
+
""
|
|
950
|
+
];
|
|
951
|
+
for (const c of conventions) {
|
|
952
|
+
lines.push(`## ${c.path}${c.truncated ? " (truncated)" : ""}`);
|
|
953
|
+
lines.push("");
|
|
954
|
+
lines.push("```");
|
|
955
|
+
lines.push(c.content);
|
|
956
|
+
lines.push("```");
|
|
957
|
+
lines.push("");
|
|
958
|
+
}
|
|
959
|
+
return lines.join("\n");
|
|
960
|
+
}
|
|
961
|
+
function formatCoverageBlock(reqs) {
|
|
962
|
+
if (!reqs || reqs.length === 0) return "";
|
|
963
|
+
const lines = [
|
|
964
|
+
"# Test coverage requirements (ENFORCED)",
|
|
965
|
+
"",
|
|
966
|
+
"Every newly added file matching one of these patterns MUST be accompanied by a sibling test file in the same commit. The wrapper checks this after you finish; if any sibling test is missing, the run will fail and the issue will be re-invoked with the gap as feedback.",
|
|
967
|
+
""
|
|
968
|
+
];
|
|
969
|
+
for (const r of reqs) lines.push(`- new \`${r.pattern}\` \u2192 must include sibling \`${r.requireSibling}\``);
|
|
970
|
+
lines.push("");
|
|
971
|
+
return lines.join("\n");
|
|
972
|
+
}
|
|
973
|
+
function formatToolsUsage(profile) {
|
|
974
|
+
const entries = (profile.cliTools ?? []).filter((t) => t.usage.trim().length > 0);
|
|
975
|
+
if (entries.length === 0) return "";
|
|
976
|
+
const lines = [
|
|
977
|
+
"# Available CLI tools",
|
|
978
|
+
""
|
|
979
|
+
];
|
|
980
|
+
for (const t of entries) {
|
|
981
|
+
lines.push(`## \`${t.name}\``);
|
|
982
|
+
lines.push(t.usage);
|
|
983
|
+
if (t.allowedUses.length > 0) {
|
|
984
|
+
lines.push(`Allowed sub-commands: ${t.allowedUses.map((u) => "`" + u + "`").join(", ")}`);
|
|
985
|
+
}
|
|
986
|
+
lines.push("");
|
|
987
|
+
}
|
|
988
|
+
return lines.join("\n");
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// src/scripts/parseAgentResult.ts
|
|
992
|
+
var parseAgentResult2 = async (ctx, _profile, agentResult) => {
|
|
993
|
+
if (!agentResult) {
|
|
994
|
+
ctx.data.agentDone = false;
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const parsed = parseAgentResult(agentResult.finalText);
|
|
998
|
+
ctx.data.agentDone = parsed.done;
|
|
999
|
+
ctx.data.commitMessage = parsed.commitMessage;
|
|
1000
|
+
ctx.data.prSummary = parsed.prSummary;
|
|
1001
|
+
ctx.data.agentFailureReason = parsed.failureReason;
|
|
1002
|
+
ctx.data.agentOutcome = agentResult.outcome;
|
|
1003
|
+
ctx.data.agentError = agentResult.error;
|
|
1004
|
+
};
|
|
1005
|
+
|
|
1006
|
+
// src/verify.ts
|
|
1007
|
+
import { spawn } from "child_process";
|
|
1008
|
+
var TAIL_CHARS = 4e3;
|
|
1009
|
+
var COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1010
|
+
function runCommand(command, cwd) {
|
|
1011
|
+
return new Promise((resolve2) => {
|
|
1012
|
+
const start = Date.now();
|
|
1013
|
+
const child = spawn(command, {
|
|
1014
|
+
cwd,
|
|
1015
|
+
shell: true,
|
|
1016
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
|
|
1017
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1018
|
+
});
|
|
1019
|
+
const buffers = [];
|
|
1020
|
+
let totalSize = 0;
|
|
1021
|
+
const collect = (chunk) => {
|
|
1022
|
+
buffers.push(chunk);
|
|
1023
|
+
totalSize += chunk.length;
|
|
1024
|
+
while (totalSize > TAIL_CHARS * 4 && buffers.length > 1) {
|
|
1025
|
+
totalSize -= buffers[0].length;
|
|
1026
|
+
buffers.shift();
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
child.stdout?.on("data", collect);
|
|
1030
|
+
child.stderr?.on("data", collect);
|
|
1031
|
+
const timer = setTimeout(() => {
|
|
1032
|
+
child.kill("SIGTERM");
|
|
1033
|
+
setTimeout(() => {
|
|
1034
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
1035
|
+
}, 5e3);
|
|
1036
|
+
}, COMMAND_TIMEOUT_MS);
|
|
1037
|
+
child.on("exit", (code) => {
|
|
1038
|
+
clearTimeout(timer);
|
|
1039
|
+
const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
|
|
1040
|
+
resolve2({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
|
|
1041
|
+
});
|
|
1042
|
+
child.on("error", (err) => {
|
|
1043
|
+
clearTimeout(timer);
|
|
1044
|
+
resolve2({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
async function verifyAll(config, cwd) {
|
|
1049
|
+
const commands = [];
|
|
1050
|
+
if (config.quality.typecheck) commands.push({ name: "typecheck", cmd: config.quality.typecheck });
|
|
1051
|
+
if (config.quality.testUnit) commands.push({ name: "test", cmd: config.quality.testUnit });
|
|
1052
|
+
if (config.quality.lint) commands.push({ name: "lint", cmd: config.quality.lint });
|
|
1053
|
+
const failed = [];
|
|
1054
|
+
const details = {};
|
|
1055
|
+
for (const { name, cmd } of commands) {
|
|
1056
|
+
const result = await runCommand(cmd, cwd);
|
|
1057
|
+
details[name] = result;
|
|
1058
|
+
if (result.exitCode !== 0) failed.push(name);
|
|
1059
|
+
}
|
|
1060
|
+
return { ok: failed.length === 0, failed, details };
|
|
1061
|
+
}
|
|
1062
|
+
var ANSI_RE = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
1063
|
+
function stripAnsi(s) {
|
|
1064
|
+
return s.replace(ANSI_RE, "");
|
|
1065
|
+
}
|
|
1066
|
+
function summarizeFailure(result) {
|
|
1067
|
+
const lines = [`verify failed: ${result.failed.join(", ")}`];
|
|
1068
|
+
for (const name of result.failed) {
|
|
1069
|
+
const d = result.details[name];
|
|
1070
|
+
if (!d) continue;
|
|
1071
|
+
lines.push(`
|
|
1072
|
+
--- ${name} (exit ${d.exitCode}, ${(d.durationMs / 1e3).toFixed(1)}s) ---`);
|
|
1073
|
+
lines.push(stripAnsi(d.tail));
|
|
1074
|
+
}
|
|
1075
|
+
return lines.join("\n");
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/scripts/verify.ts
|
|
1079
|
+
var verify = async (ctx) => {
|
|
1080
|
+
try {
|
|
1081
|
+
const result = await verifyAll(ctx.config, ctx.cwd);
|
|
1082
|
+
ctx.data.verifyOk = result.ok;
|
|
1083
|
+
ctx.data.verifyReason = result.ok ? "" : summarizeFailure(result);
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
ctx.data.verifyOk = false;
|
|
1086
|
+
ctx.data.verifyReason = `verify crashed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// src/coverage.ts
|
|
1091
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
1092
|
+
function patternToRegex(pattern) {
|
|
1093
|
+
let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
1094
|
+
s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
|
|
1095
|
+
s = s.replace(/§S/g, "(?:.*/)?").replace(/§A/g, ".*");
|
|
1096
|
+
return new RegExp(`^${s}$`);
|
|
1097
|
+
}
|
|
1098
|
+
function renderSiblingPath(file, requireSibling) {
|
|
1099
|
+
const lastSlash = file.lastIndexOf("/");
|
|
1100
|
+
const dir = lastSlash === -1 ? "" : file.slice(0, lastSlash + 1);
|
|
1101
|
+
const base = lastSlash === -1 ? file : file.slice(lastSlash + 1);
|
|
1102
|
+
const name = base.replace(/\.[^.]+$/, "");
|
|
1103
|
+
const ext = base.match(/\.[^.]+$/)?.[0] ?? "";
|
|
1104
|
+
const sibling = requireSibling.replace(/\{name\}/g, name).replace(/\{ext\}/g, ext);
|
|
1105
|
+
return dir + sibling;
|
|
1106
|
+
}
|
|
1107
|
+
function safeGit(args, cwd) {
|
|
1108
|
+
try {
|
|
1109
|
+
return execFileSync6("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
|
|
1110
|
+
} catch {
|
|
1111
|
+
return "";
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
function getAddedFiles(baseBranch, cwd) {
|
|
1115
|
+
const committed = safeGit(["diff", "--name-only", "--diff-filter=A", `origin/${baseBranch}...HEAD`], cwd);
|
|
1116
|
+
const untracked = safeGit(["ls-files", "--others", "--exclude-standard"], cwd);
|
|
1117
|
+
const set = /* @__PURE__ */ new Set();
|
|
1118
|
+
for (const f of committed.split("\n")) if (f) set.add(f);
|
|
1119
|
+
for (const f of untracked.split("\n")) if (f) set.add(f);
|
|
1120
|
+
return [...set];
|
|
1121
|
+
}
|
|
1122
|
+
function checkCoverage(addedFiles, requirements) {
|
|
1123
|
+
if (requirements.length === 0) return [];
|
|
1124
|
+
const addedSet = new Set(addedFiles);
|
|
1125
|
+
const misses = [];
|
|
1126
|
+
for (const file of addedFiles) {
|
|
1127
|
+
if (/\.(test|spec)\./.test(file)) continue;
|
|
1128
|
+
for (const req of requirements) {
|
|
1129
|
+
const re = patternToRegex(req.pattern);
|
|
1130
|
+
if (!re.test(file)) continue;
|
|
1131
|
+
const expected = renderSiblingPath(file, req.requireSibling);
|
|
1132
|
+
if (!addedSet.has(expected)) {
|
|
1133
|
+
misses.push({ file, expectedTest: expected });
|
|
1134
|
+
}
|
|
1135
|
+
break;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return misses;
|
|
1139
|
+
}
|
|
1140
|
+
function formatMissesForFeedback(misses) {
|
|
1141
|
+
if (misses.length === 0) return "";
|
|
1142
|
+
const lines = ["The following files were added without a sibling test file:"];
|
|
1143
|
+
for (const m of misses) lines.push(`- \`${m.file}\` \u2192 expected \`${m.expectedTest}\``);
|
|
1144
|
+
lines.push("");
|
|
1145
|
+
lines.push("Add the missing test files. Each should cover the new file's public API with at least a happy path and one failure path. Then re-emit DONE / COMMIT_MSG / PR_SUMMARY.");
|
|
1146
|
+
return lines.join("\n");
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/scripts/checkCoverageWithRetry.ts
|
|
1150
|
+
var checkCoverageWithRetry = async (ctx) => {
|
|
1151
|
+
const reqs = ctx.data.coverageRules ?? [];
|
|
1152
|
+
if (reqs.length === 0) {
|
|
1153
|
+
ctx.data.coverageMisses = [];
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (!ctx.data.agentDone) {
|
|
1157
|
+
ctx.data.coverageMisses = [];
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
const misses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
|
|
1161
|
+
if (misses.length === 0) {
|
|
1162
|
+
ctx.data.coverageMisses = [];
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const invoker = ctx.data.__invokeAgent;
|
|
1166
|
+
const basePrompt = ctx.data.prompt;
|
|
1167
|
+
if (!invoker || !basePrompt) {
|
|
1168
|
+
ctx.data.coverageMisses = misses;
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
process.stderr.write(`[kody2] coverage check found ${misses.length} missing test(s); retrying agent once
|
|
1172
|
+
`);
|
|
1173
|
+
const retryPrompt = `${basePrompt}
|
|
1174
|
+
|
|
1175
|
+
# Coverage failure (retry)
|
|
1176
|
+
${formatMissesForFeedback(misses)}`;
|
|
1177
|
+
const retry = await invoker(retryPrompt);
|
|
1178
|
+
const retryParsed = parseAgentResult(retry.finalText);
|
|
1179
|
+
if (retry.outcome === "completed" && retryParsed.done) {
|
|
1180
|
+
ctx.data.agentDone = true;
|
|
1181
|
+
ctx.data.commitMessage = retryParsed.commitMessage || ctx.data.commitMessage;
|
|
1182
|
+
ctx.data.prSummary = retryParsed.prSummary || ctx.data.prSummary;
|
|
1183
|
+
}
|
|
1184
|
+
const finalMisses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
|
|
1185
|
+
ctx.data.coverageMisses = finalMisses;
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
// src/scripts/commitAndPush.ts
|
|
1189
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
1190
|
+
|
|
1191
|
+
// src/commit.ts
|
|
1192
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
1193
|
+
import * as fs6 from "fs";
|
|
1194
|
+
import * as path5 from "path";
|
|
1195
|
+
var FORBIDDEN_PATH_PREFIXES = [
|
|
1196
|
+
".kody/",
|
|
1197
|
+
".kody-engine/",
|
|
1198
|
+
".kody2/",
|
|
1199
|
+
".kody-lean/",
|
|
1200
|
+
// back-compat: stale runtime dir from kody-lean v0.5.x
|
|
1201
|
+
"node_modules/",
|
|
1202
|
+
"dist/",
|
|
1203
|
+
"build/"
|
|
1204
|
+
];
|
|
1205
|
+
var FORBIDDEN_PATH_EXACT = /* @__PURE__ */ new Set([".env"]);
|
|
1206
|
+
var FORBIDDEN_PATH_SUFFIXES = [".log"];
|
|
1207
|
+
var CONVENTIONAL_PREFIXES = [
|
|
1208
|
+
"feat:",
|
|
1209
|
+
"fix:",
|
|
1210
|
+
"chore:",
|
|
1211
|
+
"docs:",
|
|
1212
|
+
"refactor:",
|
|
1213
|
+
"test:",
|
|
1214
|
+
"perf:",
|
|
1215
|
+
"ci:",
|
|
1216
|
+
"style:",
|
|
1217
|
+
"build:",
|
|
1218
|
+
"revert:"
|
|
1219
|
+
];
|
|
1220
|
+
function git2(args, cwd) {
|
|
1221
|
+
try {
|
|
1222
|
+
return execFileSync7("git", args, {
|
|
1223
|
+
encoding: "utf-8",
|
|
1224
|
+
timeout: 12e4,
|
|
1225
|
+
cwd,
|
|
1226
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
1227
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1228
|
+
}).trim();
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
const e = err;
|
|
1231
|
+
const stderr = e.stderr?.toString().trim() ?? "";
|
|
1232
|
+
const stdout = e.stdout?.toString().trim() ?? "";
|
|
1233
|
+
const status = e.status ?? "?";
|
|
1234
|
+
const detail = stderr || stdout || e.message || "(no output)";
|
|
1235
|
+
throw new Error(`git ${args.join(" ")} (exit ${status}):
|
|
1236
|
+
${detail}`);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function tryGit(args, cwd) {
|
|
1240
|
+
try {
|
|
1241
|
+
git2(args, cwd);
|
|
1242
|
+
return true;
|
|
1243
|
+
} catch {
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
function abortUnfinishedGitOps(cwd) {
|
|
1248
|
+
const aborted = [];
|
|
1249
|
+
const gitDir = path5.join(cwd ?? process.cwd(), ".git");
|
|
1250
|
+
if (!fs6.existsSync(gitDir)) return aborted;
|
|
1251
|
+
if (fs6.existsSync(path5.join(gitDir, "MERGE_HEAD"))) {
|
|
1252
|
+
if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
|
|
1253
|
+
}
|
|
1254
|
+
if (fs6.existsSync(path5.join(gitDir, "CHERRY_PICK_HEAD"))) {
|
|
1255
|
+
if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
|
|
1256
|
+
}
|
|
1257
|
+
if (fs6.existsSync(path5.join(gitDir, "REVERT_HEAD"))) {
|
|
1258
|
+
if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
|
|
1259
|
+
}
|
|
1260
|
+
if (fs6.existsSync(path5.join(gitDir, "rebase-merge")) || fs6.existsSync(path5.join(gitDir, "rebase-apply"))) {
|
|
1261
|
+
if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
|
|
1262
|
+
}
|
|
1263
|
+
try {
|
|
1264
|
+
const unmerged = git2(["diff", "--name-only", "--diff-filter=U"], cwd);
|
|
1265
|
+
if (unmerged) {
|
|
1266
|
+
tryGit(["reset", "--mixed", "HEAD"], cwd);
|
|
1267
|
+
aborted.push("unmerged-paths-reset");
|
|
1268
|
+
}
|
|
1269
|
+
} catch {
|
|
1270
|
+
}
|
|
1271
|
+
return aborted;
|
|
1272
|
+
}
|
|
1273
|
+
function isForbiddenPath(p) {
|
|
1274
|
+
if (FORBIDDEN_PATH_EXACT.has(p)) return true;
|
|
1275
|
+
for (const pre of FORBIDDEN_PATH_PREFIXES) if (p.startsWith(pre)) return true;
|
|
1276
|
+
for (const suf of FORBIDDEN_PATH_SUFFIXES) if (p.endsWith(suf)) return true;
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
function listChangedFiles(cwd) {
|
|
1280
|
+
const raw = execFileSync7("git", ["status", "--porcelain=v1", "-z"], {
|
|
1281
|
+
encoding: "utf-8",
|
|
1282
|
+
cwd,
|
|
1283
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
1284
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1285
|
+
});
|
|
1286
|
+
if (!raw) return [];
|
|
1287
|
+
const entries = raw.split("\0").filter((e) => e.length > 0);
|
|
1288
|
+
return entries.map((e) => e.slice(3)).filter(Boolean);
|
|
1289
|
+
}
|
|
1290
|
+
function normalizeCommitMessage(raw) {
|
|
1291
|
+
const trimmed = raw.trim().replace(/^['"]|['"]$/g, "").trim();
|
|
1292
|
+
if (!trimmed) return "chore: kody2 update";
|
|
1293
|
+
const firstLine2 = trimmed.split("\n")[0];
|
|
1294
|
+
for (const prefix of CONVENTIONAL_PREFIXES) {
|
|
1295
|
+
if (firstLine2.toLowerCase().startsWith(prefix)) return trimmed;
|
|
1296
|
+
}
|
|
1297
|
+
return `chore: ${trimmed}`;
|
|
1298
|
+
}
|
|
1299
|
+
function commitAndPush(branch, agentMessage, cwd) {
|
|
1300
|
+
const allChanged = listChangedFiles(cwd);
|
|
1301
|
+
const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
|
|
1302
|
+
const mergeHeadExists = fs6.existsSync(path5.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
|
|
1303
|
+
if (allowedFiles.length === 0 && !mergeHeadExists) {
|
|
1304
|
+
return { committed: false, pushed: false, sha: "", message: "" };
|
|
1305
|
+
}
|
|
1306
|
+
for (const f of allowedFiles) {
|
|
1307
|
+
try {
|
|
1308
|
+
git2(["add", "--", f], cwd);
|
|
1309
|
+
} catch {
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
const message = normalizeCommitMessage(agentMessage);
|
|
1313
|
+
try {
|
|
1314
|
+
git2(["commit", "--no-gpg-sign", "-m", message], cwd);
|
|
1315
|
+
} catch (err) {
|
|
1316
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1317
|
+
if (/nothing to commit/i.test(msg)) {
|
|
1318
|
+
return { committed: false, pushed: false, sha: "", message };
|
|
1319
|
+
}
|
|
1320
|
+
throw err;
|
|
1321
|
+
}
|
|
1322
|
+
const sha = git2(["rev-parse", "HEAD"], cwd).slice(0, 7);
|
|
1323
|
+
try {
|
|
1324
|
+
git2(["push", "-u", "origin", branch], cwd);
|
|
1325
|
+
} catch {
|
|
1326
|
+
git2(["push", "--force-with-lease", "-u", "origin", branch], cwd);
|
|
1327
|
+
}
|
|
1328
|
+
return { committed: true, pushed: true, sha, message };
|
|
1329
|
+
}
|
|
1330
|
+
function hasCommitsAhead(branch, defaultBranch, cwd) {
|
|
1331
|
+
try {
|
|
1332
|
+
const out = git2(["rev-list", "--count", `origin/${defaultBranch}..${branch}`], cwd);
|
|
1333
|
+
return parseInt(out, 10) > 0;
|
|
1334
|
+
} catch {
|
|
1335
|
+
try {
|
|
1336
|
+
const out = git2(["rev-list", "--count", `${defaultBranch}..${branch}`], cwd);
|
|
1337
|
+
return parseInt(out, 10) > 0;
|
|
1338
|
+
} catch {
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/scripts/commitAndPush.ts
|
|
1345
|
+
var commitAndPush2 = async (ctx) => {
|
|
1346
|
+
const branch = ctx.data.branch;
|
|
1347
|
+
if (!branch) {
|
|
1348
|
+
ctx.data.commitResult = { committed: false, pushed: false };
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (ctx.args.mode === "resolve") {
|
|
1352
|
+
try {
|
|
1353
|
+
execFileSync8("git", ["add", "-A"], { cwd: ctx.cwd, env: { ...process.env, HUSKY: "0" }, stdio: "pipe" });
|
|
1354
|
+
} catch {
|
|
1355
|
+
}
|
|
1356
|
+
} else {
|
|
1357
|
+
const aborted = abortUnfinishedGitOps(ctx.cwd);
|
|
1358
|
+
if (aborted.length > 0) {
|
|
1359
|
+
process.stderr.write(`[kody2] cleaned up unfinished git ops: ${aborted.join(", ")}
|
|
1360
|
+
`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
const fallbackMsg = defaultCommitMessage(ctx.args.mode, ctx.data);
|
|
1364
|
+
const message = ctx.data.commitMessage || fallbackMsg;
|
|
1365
|
+
try {
|
|
1366
|
+
const result = commitAndPush(branch, message, ctx.cwd);
|
|
1367
|
+
ctx.data.commitResult = result;
|
|
1368
|
+
ctx.data.changedFiles = listChangedFiles(ctx.cwd).filter((f) => !isForbiddenPath(f));
|
|
1369
|
+
} catch (err) {
|
|
1370
|
+
ctx.data.commitCrash = err instanceof Error ? err.message : String(err);
|
|
1371
|
+
ctx.data.commitResult = { committed: false, pushed: false };
|
|
1372
|
+
}
|
|
1373
|
+
ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
|
|
1374
|
+
};
|
|
1375
|
+
function defaultCommitMessage(mode, data) {
|
|
1376
|
+
switch (mode) {
|
|
1377
|
+
case "run":
|
|
1378
|
+
return `chore: kody2 changes for #${data.commentTargetNumber}`;
|
|
1379
|
+
case "fix":
|
|
1380
|
+
return `chore(fix): kody2 fix for PR #${data.commentTargetNumber}`;
|
|
1381
|
+
case "fix-ci":
|
|
1382
|
+
return `fix(ci): kody2 fix-ci for PR #${data.commentTargetNumber}`;
|
|
1383
|
+
case "resolve":
|
|
1384
|
+
return `fix: resolve merge conflicts with ${data.baseBranch}`;
|
|
1385
|
+
default:
|
|
1386
|
+
return `chore: kody2 changes`;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/pr.ts
|
|
1391
|
+
var TITLE_MAX = 72;
|
|
1392
|
+
function buildPrTitle(issueNumber, issueTitle, draft) {
|
|
1393
|
+
const prefix = draft ? "[WIP] " : "";
|
|
1394
|
+
const base = `${prefix}#${issueNumber}: ${issueTitle}`;
|
|
1395
|
+
if (base.length <= TITLE_MAX) return base;
|
|
1396
|
+
return base.slice(0, TITLE_MAX - 1) + "\u2026";
|
|
1397
|
+
}
|
|
1398
|
+
function buildPrBody(opts) {
|
|
1399
|
+
const lines = [];
|
|
1400
|
+
if (opts.draft && opts.failureReason) {
|
|
1401
|
+
const headline = firstLine(opts.failureReason);
|
|
1402
|
+
lines.push(`> \u26A0\uFE0F Draft: ${headline}`);
|
|
1403
|
+
lines.push(`> The failures below may be **pre-existing in the repo** \u2014 verify before treating as PR-blocking.`);
|
|
1404
|
+
lines.push("");
|
|
1405
|
+
}
|
|
1406
|
+
lines.push("## Summary");
|
|
1407
|
+
lines.push("");
|
|
1408
|
+
if (opts.agentSummary && opts.agentSummary.trim()) {
|
|
1409
|
+
lines.push(opts.agentSummary.trim());
|
|
1410
|
+
} else {
|
|
1411
|
+
lines.push(`Implementation of issue #${opts.issueNumber} \u2014 ${opts.issueTitle}`);
|
|
1412
|
+
lines.push("");
|
|
1413
|
+
lines.push("_(agent did not supply PR_SUMMARY)_");
|
|
1414
|
+
}
|
|
1415
|
+
lines.push("");
|
|
1416
|
+
if (opts.changedFiles.length > 0) {
|
|
1417
|
+
lines.push("## Changes");
|
|
1418
|
+
lines.push("");
|
|
1419
|
+
for (const f of opts.changedFiles.slice(0, 50)) lines.push(`- \`${f}\``);
|
|
1420
|
+
if (opts.changedFiles.length > 50) lines.push(`- \u2026 and ${opts.changedFiles.length - 50} more`);
|
|
1421
|
+
lines.push("");
|
|
1422
|
+
}
|
|
1423
|
+
lines.push(`Closes #${opts.issueNumber}`);
|
|
1424
|
+
lines.push("");
|
|
1425
|
+
if (opts.draft && opts.failureReason) {
|
|
1426
|
+
lines.push("<details>");
|
|
1427
|
+
lines.push("<summary>Verify output (click to expand)</summary>");
|
|
1428
|
+
lines.push("");
|
|
1429
|
+
lines.push("```");
|
|
1430
|
+
lines.push(truncate(opts.failureReason, 6e3));
|
|
1431
|
+
lines.push("```");
|
|
1432
|
+
lines.push("");
|
|
1433
|
+
lines.push("</details>");
|
|
1434
|
+
lines.push("");
|
|
1435
|
+
}
|
|
1436
|
+
lines.push("---");
|
|
1437
|
+
lines.push("_Opened by kody2 (single-session autonomous run)._ ");
|
|
1438
|
+
return lines.join("\n");
|
|
1439
|
+
}
|
|
1440
|
+
function firstLine(s) {
|
|
1441
|
+
const trimmed = s.trim();
|
|
1442
|
+
const nl = trimmed.indexOf("\n");
|
|
1443
|
+
const head = nl === -1 ? trimmed : trimmed.slice(0, nl);
|
|
1444
|
+
return head.length > 200 ? head.slice(0, 197) + "\u2026" : head;
|
|
1445
|
+
}
|
|
1446
|
+
function findExistingPr(branch, cwd) {
|
|
1447
|
+
try {
|
|
1448
|
+
const output = gh(["pr", "view", branch, "--json", "number,url"], { cwd });
|
|
1449
|
+
const parsed = JSON.parse(output);
|
|
1450
|
+
if (typeof parsed?.number === "number" && typeof parsed?.url === "string") {
|
|
1451
|
+
return { number: parsed.number, url: parsed.url };
|
|
1452
|
+
}
|
|
1453
|
+
return null;
|
|
1454
|
+
} catch {
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
function ensurePr(opts) {
|
|
1459
|
+
const title = buildPrTitle(opts.issueNumber, opts.issueTitle, opts.draft);
|
|
1460
|
+
const body = buildPrBody(opts);
|
|
1461
|
+
const existing = findExistingPr(opts.branch, opts.cwd);
|
|
1462
|
+
if (existing) {
|
|
1463
|
+
try {
|
|
1464
|
+
gh(
|
|
1465
|
+
["pr", "edit", String(existing.number), "--body-file", "-"],
|
|
1466
|
+
{ input: body, cwd: opts.cwd }
|
|
1467
|
+
);
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
process.stderr.write(`[kody2] failed to update PR #${existing.number}: ${err instanceof Error ? err.message : String(err)}
|
|
1470
|
+
`);
|
|
1471
|
+
}
|
|
1472
|
+
return { url: existing.url, number: existing.number, draft: opts.draft, action: "updated" };
|
|
1473
|
+
}
|
|
1474
|
+
const args = [
|
|
1475
|
+
"pr",
|
|
1476
|
+
"create",
|
|
1477
|
+
"--head",
|
|
1478
|
+
opts.branch,
|
|
1479
|
+
"--base",
|
|
1480
|
+
opts.defaultBranch,
|
|
1481
|
+
"--title",
|
|
1482
|
+
title,
|
|
1483
|
+
"--body-file",
|
|
1484
|
+
"-"
|
|
1485
|
+
];
|
|
1486
|
+
if (opts.draft) args.push("--draft");
|
|
1487
|
+
const output = gh(args, { input: body, cwd: opts.cwd });
|
|
1488
|
+
const url = output.trim();
|
|
1489
|
+
const match = url.match(/\/pull\/(\d+)$/);
|
|
1490
|
+
const number = match ? parseInt(match[1], 10) : 0;
|
|
1491
|
+
return { url, number, draft: opts.draft, action: "created" };
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/scripts/ensurePr.ts
|
|
1495
|
+
var ensurePr2 = async (ctx) => {
|
|
1496
|
+
if (ctx.skipAgent && ctx.output.exitCode !== void 0 && ctx.output.exitCode !== 0) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
const commitResult = ctx.data.commitResult;
|
|
1500
|
+
const hasCommits = Boolean(ctx.data.hasCommitsAhead);
|
|
1501
|
+
if (!commitResult?.committed && !hasCommits) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const branch = ctx.data.branch;
|
|
1505
|
+
if (!branch) return;
|
|
1506
|
+
const failureReason = computeFailureReason(ctx);
|
|
1507
|
+
const isFailure = failureReason.length > 0;
|
|
1508
|
+
const changedFiles = ctx.data.changedFiles ?? [];
|
|
1509
|
+
const issue = ctx.data.issue;
|
|
1510
|
+
const pr = ctx.data.pr;
|
|
1511
|
+
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
1512
|
+
const title = issue?.title ?? pr?.title ?? `kody2 changes`;
|
|
1513
|
+
try {
|
|
1514
|
+
const result = ensurePr({
|
|
1515
|
+
branch,
|
|
1516
|
+
defaultBranch: ctx.config.git.defaultBranch,
|
|
1517
|
+
issueNumber: targetNumber,
|
|
1518
|
+
issueTitle: title,
|
|
1519
|
+
draft: isFailure,
|
|
1520
|
+
failureReason: isFailure ? failureReason : void 0,
|
|
1521
|
+
changedFiles,
|
|
1522
|
+
agentSummary: ctx.data.prSummary,
|
|
1523
|
+
cwd: ctx.cwd
|
|
1524
|
+
});
|
|
1525
|
+
ctx.output.prUrl = result.url;
|
|
1526
|
+
ctx.data.prResult = result;
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
const reason = `PR creation failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1529
|
+
ctx.data.prCrashReason = reason;
|
|
1530
|
+
ctx.output.exitCode = 4;
|
|
1531
|
+
ctx.output.reason = reason;
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
function computeFailureReason(ctx) {
|
|
1535
|
+
const misses = ctx.data.coverageMisses ?? [];
|
|
1536
|
+
if (misses.length > 0) return `missing tests: ${misses.map((m) => m.expectedTest).join(", ")}`;
|
|
1537
|
+
const agentDone = Boolean(ctx.data.agentDone);
|
|
1538
|
+
if (!agentDone) {
|
|
1539
|
+
return ctx.data.agentFailureReason || ctx.data.agentError || ctx.data.commitCrash || "agent did not emit DONE";
|
|
1540
|
+
}
|
|
1541
|
+
if (ctx.data.verifyOk === false) return ctx.data.verifyReason || "verify failed";
|
|
1542
|
+
return "";
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/scripts/postIssueComment.ts
|
|
1546
|
+
var postIssueComment2 = async (ctx) => {
|
|
1547
|
+
if (ctx.skipAgent && ctx.output.exitCode !== void 0) return;
|
|
1548
|
+
const targetType = ctx.data.commentTargetType;
|
|
1549
|
+
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
1550
|
+
if (!targetType || !targetNumber) return;
|
|
1551
|
+
const commitResult = ctx.data.commitResult;
|
|
1552
|
+
const hasCommits = Boolean(ctx.data.hasCommitsAhead);
|
|
1553
|
+
const prUrl = ctx.output.prUrl;
|
|
1554
|
+
if (!commitResult?.committed && !hasCommits) {
|
|
1555
|
+
const reason = "no changes to commit";
|
|
1556
|
+
postWith(targetType, targetNumber, `\u26A0\uFE0F kody2 FAILED: ${reason}`, ctx.cwd);
|
|
1557
|
+
ctx.output.exitCode = 3;
|
|
1558
|
+
ctx.output.reason = reason;
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (ctx.output.exitCode === 4 && ctx.data.prCrashReason) {
|
|
1562
|
+
postWith(targetType, targetNumber, `\u26A0\uFE0F kody2 FAILED: ${truncate(ctx.data.prCrashReason, 1500)}`, ctx.cwd);
|
|
1563
|
+
ctx.output.reason = ctx.data.prCrashReason;
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const failureReason = computeFailureReason2(ctx);
|
|
1567
|
+
const isFailure = failureReason.length > 0;
|
|
1568
|
+
const msg = isFailure ? `\u26A0\uFE0F kody2 FAILED: ${truncate(failureReason, 1500)}${prUrl ? ` \u2014 draft PR: ${prUrl}` : ""}` : `\u2705 kody2 PR opened: ${prUrl}`;
|
|
1569
|
+
postWith(targetType, targetNumber, msg, ctx.cwd);
|
|
1570
|
+
let exitCode = 0;
|
|
1571
|
+
const agentDone = Boolean(ctx.data.agentDone);
|
|
1572
|
+
const verifyOk = ctx.data.verifyOk !== false;
|
|
1573
|
+
const misses = ctx.data.coverageMisses ?? [];
|
|
1574
|
+
if (!agentDone || misses.length > 0) exitCode = 1;
|
|
1575
|
+
else if (!verifyOk) exitCode = 2;
|
|
1576
|
+
ctx.output.exitCode = exitCode;
|
|
1577
|
+
ctx.output.reason = failureReason || void 0;
|
|
1578
|
+
};
|
|
1579
|
+
function computeFailureReason2(ctx) {
|
|
1580
|
+
const misses = ctx.data.coverageMisses ?? [];
|
|
1581
|
+
if (misses.length > 0) return `missing tests: ${misses.map((m) => m.expectedTest).join(", ")}`;
|
|
1582
|
+
const agentDone = Boolean(ctx.data.agentDone);
|
|
1583
|
+
if (!agentDone) {
|
|
1584
|
+
return ctx.data.agentFailureReason || ctx.data.agentError || "agent did not emit DONE";
|
|
1585
|
+
}
|
|
1586
|
+
if (ctx.data.verifyOk === false) return ctx.data.verifyReason || "verify failed";
|
|
1587
|
+
return "";
|
|
1588
|
+
}
|
|
1589
|
+
function postWith(type, n, body, cwd) {
|
|
1590
|
+
try {
|
|
1591
|
+
if (type === "issue") postIssueComment(n, body, cwd);
|
|
1592
|
+
else postPrReviewComment(n, body, cwd);
|
|
1593
|
+
} catch {
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// src/scripts/index.ts
|
|
1598
|
+
var preflightScripts = {
|
|
1599
|
+
runFlow,
|
|
1600
|
+
fixFlow,
|
|
1601
|
+
fixCiFlow,
|
|
1602
|
+
resolveFlow,
|
|
1603
|
+
loadConventions,
|
|
1604
|
+
loadCoverageRules,
|
|
1605
|
+
composePrompt
|
|
1606
|
+
};
|
|
1607
|
+
var postflightScripts = {
|
|
1608
|
+
parseAgentResult: parseAgentResult2,
|
|
1609
|
+
verify,
|
|
1610
|
+
checkCoverageWithRetry,
|
|
1611
|
+
commitAndPush: commitAndPush2,
|
|
1612
|
+
ensurePr: ensurePr2,
|
|
1613
|
+
postIssueComment: postIssueComment2
|
|
1614
|
+
};
|
|
1615
|
+
var allScriptNames = /* @__PURE__ */ new Set([
|
|
1616
|
+
...Object.keys(preflightScripts),
|
|
1617
|
+
...Object.keys(postflightScripts)
|
|
1618
|
+
]);
|
|
1619
|
+
|
|
1620
|
+
// src/agent.ts
|
|
1621
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1622
|
+
import * as fs7 from "fs";
|
|
1623
|
+
import * as path6 from "path";
|
|
1624
|
+
|
|
1625
|
+
// src/format.ts
|
|
1626
|
+
function renderEvent(msg, opts = {}) {
|
|
1627
|
+
if (opts.quiet) {
|
|
1628
|
+
if (msg.type === "result") return formatResult(msg);
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
switch (msg.type) {
|
|
1632
|
+
case "system":
|
|
1633
|
+
return null;
|
|
1634
|
+
case "assistant":
|
|
1635
|
+
return formatAssistant(msg, opts);
|
|
1636
|
+
case "user":
|
|
1637
|
+
return formatUserToolResult(msg, opts);
|
|
1638
|
+
case "result":
|
|
1639
|
+
return formatResult(msg);
|
|
1640
|
+
default:
|
|
1641
|
+
return null;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
function formatAssistant(msg, opts) {
|
|
1645
|
+
const content = msg.message?.content ?? [];
|
|
1646
|
+
const lines = [];
|
|
1647
|
+
for (const block of content) {
|
|
1648
|
+
if (block.type === "text") {
|
|
1649
|
+
const text = block.text.trim();
|
|
1650
|
+
if (text) lines.push(text);
|
|
1651
|
+
} else if (block.type === "tool_use") {
|
|
1652
|
+
const tu = block;
|
|
1653
|
+
lines.push(`\u2192 ${tu.name}${summarizeToolInput(tu.name, tu.input)}`);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
1657
|
+
}
|
|
1658
|
+
function formatUserToolResult(msg, opts) {
|
|
1659
|
+
const content = msg.message?.content ?? [];
|
|
1660
|
+
const lines = [];
|
|
1661
|
+
for (const block of content) {
|
|
1662
|
+
if (block.type === "tool_result") {
|
|
1663
|
+
const tr = block;
|
|
1664
|
+
const text = stringifyToolContent(tr.content);
|
|
1665
|
+
const lineCount = text.split("\n").length;
|
|
1666
|
+
const sizeBytes = text.length;
|
|
1667
|
+
const flag = tr.is_error ? " ERROR" : "";
|
|
1668
|
+
const summary = ` \u21B3${flag} ${lineCount} lines, ${formatBytes(sizeBytes)}`;
|
|
1669
|
+
if (opts.verbose) {
|
|
1670
|
+
lines.push(`${summary}
|
|
1671
|
+
${truncate2(text, 4e3)}`);
|
|
1672
|
+
} else {
|
|
1673
|
+
lines.push(summary);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
1678
|
+
}
|
|
1679
|
+
function formatResult(msg) {
|
|
1680
|
+
const ok = msg.subtype === "success";
|
|
1681
|
+
const tag = ok ? "DONE" : `FAILED (${msg.subtype ?? "unknown"})`;
|
|
1682
|
+
const dur = msg.duration_ms ? ` ${(msg.duration_ms / 1e3).toFixed(1)}s` : "";
|
|
1683
|
+
const turns = msg.num_turns ? ` ${msg.num_turns} turns` : "";
|
|
1684
|
+
const cost = typeof msg.total_cost_usd === "number" ? ` $${msg.total_cost_usd.toFixed(4)}` : "";
|
|
1685
|
+
return `
|
|
1686
|
+
=== ${tag}${dur}${turns}${cost} ===`;
|
|
1687
|
+
}
|
|
1688
|
+
function summarizeToolInput(toolName, input = {}) {
|
|
1689
|
+
if (toolName === "Bash" && typeof input.command === "string") {
|
|
1690
|
+
const cmd = input.command.split("\n")[0];
|
|
1691
|
+
return `: ${truncate2(cmd, 120)}`;
|
|
1692
|
+
}
|
|
1693
|
+
if ((toolName === "Read" || toolName === "Edit" || toolName === "Write") && typeof input.file_path === "string") {
|
|
1694
|
+
return ` ${input.file_path}`;
|
|
1695
|
+
}
|
|
1696
|
+
if ((toolName === "Glob" || toolName === "Grep") && typeof input.pattern === "string") {
|
|
1697
|
+
return `: ${truncate2(input.pattern, 80)}`;
|
|
1698
|
+
}
|
|
1699
|
+
return "";
|
|
1700
|
+
}
|
|
1701
|
+
function stringifyToolContent(content) {
|
|
1702
|
+
if (typeof content === "string") return content;
|
|
1703
|
+
if (Array.isArray(content)) {
|
|
1704
|
+
return content.map((b) => {
|
|
1705
|
+
if (b && typeof b === "object" && "text" in b && typeof b.text === "string") {
|
|
1706
|
+
return b.text;
|
|
1707
|
+
}
|
|
1708
|
+
return JSON.stringify(b);
|
|
1709
|
+
}).join("\n");
|
|
1710
|
+
}
|
|
1711
|
+
return JSON.stringify(content);
|
|
1712
|
+
}
|
|
1713
|
+
function truncate2(s, max) {
|
|
1714
|
+
if (s.length <= max) return s;
|
|
1715
|
+
return s.slice(0, max) + `\u2026 (+${s.length - max} chars)`;
|
|
1716
|
+
}
|
|
1717
|
+
function formatBytes(bytes) {
|
|
1718
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
1719
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
1720
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// src/agent.ts
|
|
1724
|
+
var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
|
|
1725
|
+
async function runAgent(opts) {
|
|
1726
|
+
const ndjsonDir = opts.ndjsonDir ?? path6.join(opts.cwd, ".kody2");
|
|
1727
|
+
fs7.mkdirSync(ndjsonDir, { recursive: true });
|
|
1728
|
+
const ndjsonPath = path6.join(ndjsonDir, "last-run.jsonl");
|
|
1729
|
+
const fullLog = fs7.createWriteStream(ndjsonPath, { flags: "w" });
|
|
1730
|
+
const env = {
|
|
1731
|
+
...process.env,
|
|
1732
|
+
SKIP_HOOKS: "1",
|
|
1733
|
+
HUSKY: "0",
|
|
1734
|
+
CI: process.env.CI ?? "1"
|
|
1735
|
+
};
|
|
1736
|
+
if (opts.litellmUrl) {
|
|
1737
|
+
env.ANTHROPIC_BASE_URL = opts.litellmUrl;
|
|
1738
|
+
env.ANTHROPIC_API_KEY = getAnthropicApiKeyOrDummy();
|
|
1739
|
+
}
|
|
1740
|
+
let finalText = "";
|
|
1741
|
+
let outcome = "failed";
|
|
1742
|
+
let errorMessage;
|
|
1743
|
+
try {
|
|
1744
|
+
const result = query({
|
|
1745
|
+
prompt: opts.prompt,
|
|
1746
|
+
options: {
|
|
1747
|
+
model: opts.model.model,
|
|
1748
|
+
cwd: opts.cwd,
|
|
1749
|
+
allowedTools: opts.allowedToolsOverride ?? DEFAULT_ALLOWED_TOOLS,
|
|
1750
|
+
permissionMode: opts.permissionModeOverride ?? "acceptEdits",
|
|
1751
|
+
env
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
for await (const msg of result) {
|
|
1755
|
+
try {
|
|
1756
|
+
fullLog.write(JSON.stringify(msg) + "\n");
|
|
1757
|
+
} catch {
|
|
1758
|
+
}
|
|
1759
|
+
const line = renderEvent(msg, { verbose: opts.verbose, quiet: opts.quiet });
|
|
1760
|
+
if (line) process.stdout.write(line + "\n");
|
|
1761
|
+
const m = msg;
|
|
1762
|
+
if (m.type === "result") {
|
|
1763
|
+
if (m.subtype === "success") {
|
|
1764
|
+
outcome = "completed";
|
|
1765
|
+
finalText = (typeof m.result === "string" ? m.result : "").trim();
|
|
1766
|
+
} else {
|
|
1767
|
+
outcome = "failed";
|
|
1768
|
+
errorMessage = `result subtype: ${m.subtype ?? "unknown"}`;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
} catch (e) {
|
|
1773
|
+
outcome = "failed";
|
|
1774
|
+
errorMessage = e instanceof Error ? e.message : String(e);
|
|
1775
|
+
} finally {
|
|
1776
|
+
try {
|
|
1777
|
+
fullLog.end();
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return { outcome, finalText, error: errorMessage, ndjsonPath };
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// src/tools.ts
|
|
1785
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
1786
|
+
function verifyCliTools(tools, cwd) {
|
|
1787
|
+
const out = [];
|
|
1788
|
+
for (const t of tools) out.push(verifyOne(t, cwd));
|
|
1789
|
+
return out;
|
|
1790
|
+
}
|
|
1791
|
+
function firstRequiredFailure(results, tools) {
|
|
1792
|
+
for (const t of tools) {
|
|
1793
|
+
const r = results.find((x) => x.name === t.name);
|
|
1794
|
+
if (!r) continue;
|
|
1795
|
+
if (t.install.required && (!r.present || !r.verified)) return r;
|
|
1796
|
+
}
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
function verifyOne(tool, cwd) {
|
|
1800
|
+
const result = { name: tool.name, present: false, verified: false };
|
|
1801
|
+
let present = runShell(tool.install.checkCommand, cwd);
|
|
1802
|
+
if (!present && tool.install.installCommand) {
|
|
1803
|
+
runShell(tool.install.installCommand, cwd, 12e4);
|
|
1804
|
+
present = runShell(tool.install.checkCommand, cwd);
|
|
1805
|
+
}
|
|
1806
|
+
result.present = present;
|
|
1807
|
+
if (!present) {
|
|
1808
|
+
result.error = `tool "${tool.name}" not on PATH (check: ${tool.install.checkCommand})`;
|
|
1809
|
+
return result;
|
|
1810
|
+
}
|
|
1811
|
+
const verified = runShell(tool.verify, cwd);
|
|
1812
|
+
result.verified = verified;
|
|
1813
|
+
if (!verified) result.error = `tool "${tool.name}" failed verify: ${tool.verify}`;
|
|
1814
|
+
return result;
|
|
1815
|
+
}
|
|
1816
|
+
function runShell(cmd, cwd, timeoutMs = 3e4) {
|
|
1817
|
+
try {
|
|
1818
|
+
execFileSync9("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
|
|
1819
|
+
return true;
|
|
1820
|
+
} catch {
|
|
1821
|
+
return false;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// src/litellm.ts
|
|
1826
|
+
import * as fs8 from "fs";
|
|
1827
|
+
import * as os from "os";
|
|
1828
|
+
import * as path7 from "path";
|
|
1829
|
+
import { execFileSync as execFileSync10, spawn as spawn2 } from "child_process";
|
|
1830
|
+
async function checkLitellmHealth(url) {
|
|
1831
|
+
try {
|
|
1832
|
+
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
1833
|
+
return response.ok;
|
|
1834
|
+
} catch {
|
|
1835
|
+
return false;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
function generateLitellmConfigYaml(model) {
|
|
1839
|
+
const apiKeyVar = providerApiKeyEnvVar(model.provider);
|
|
1840
|
+
return [
|
|
1841
|
+
"model_list:",
|
|
1842
|
+
` - model_name: ${model.model}`,
|
|
1843
|
+
` litellm_params:`,
|
|
1844
|
+
` model: ${model.provider}/${model.model}`,
|
|
1845
|
+
` api_key: os.environ/${apiKeyVar}`,
|
|
1846
|
+
"",
|
|
1847
|
+
"litellm_settings:",
|
|
1848
|
+
" drop_params: true",
|
|
1849
|
+
""
|
|
1850
|
+
].join("\n");
|
|
1851
|
+
}
|
|
1852
|
+
async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL) {
|
|
1853
|
+
if (!needsLitellmProxy(model)) return null;
|
|
1854
|
+
if (await checkLitellmHealth(url)) {
|
|
1855
|
+
return { url, kill: () => {
|
|
1856
|
+
} };
|
|
1857
|
+
}
|
|
1858
|
+
let cmd = "litellm";
|
|
1859
|
+
let args;
|
|
1860
|
+
try {
|
|
1861
|
+
execFileSync10("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
1862
|
+
} catch {
|
|
1863
|
+
try {
|
|
1864
|
+
execFileSync10("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
1865
|
+
cmd = "python3";
|
|
1866
|
+
} catch {
|
|
1867
|
+
throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
const configPath = path7.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
|
|
1871
|
+
fs8.writeFileSync(configPath, generateLitellmConfigYaml(model));
|
|
1872
|
+
const portMatch = url.match(/:(\d+)/);
|
|
1873
|
+
const port = portMatch ? portMatch[1] : "4000";
|
|
1874
|
+
args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
|
|
1875
|
+
const dotenvVars = readDotenvApiKeys(projectDir);
|
|
1876
|
+
const logPath = path7.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
|
|
1877
|
+
const outFd = fs8.openSync(logPath, "w");
|
|
1878
|
+
const child = spawn2(cmd, args, {
|
|
1879
|
+
stdio: ["ignore", outFd, outFd],
|
|
1880
|
+
detached: true,
|
|
1881
|
+
env: stripBlockingEnv({ ...process.env, ...dotenvVars })
|
|
1882
|
+
});
|
|
1883
|
+
fs8.closeSync(outFd);
|
|
1884
|
+
for (let i = 0; i < 30; i++) {
|
|
1885
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1886
|
+
if (await checkLitellmHealth(url)) {
|
|
1887
|
+
return { url, kill: () => {
|
|
1888
|
+
try {
|
|
1889
|
+
child.kill();
|
|
1890
|
+
} catch {
|
|
1891
|
+
}
|
|
1892
|
+
} };
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
let logTail = "";
|
|
1896
|
+
try {
|
|
1897
|
+
logTail = fs8.readFileSync(logPath, "utf-8").slice(-2e3);
|
|
1898
|
+
} catch {
|
|
1899
|
+
}
|
|
1900
|
+
try {
|
|
1901
|
+
child.kill();
|
|
1902
|
+
} catch {
|
|
1903
|
+
}
|
|
1904
|
+
throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
|
|
1905
|
+
${logTail}`);
|
|
1906
|
+
}
|
|
1907
|
+
function readDotenvApiKeys(projectDir) {
|
|
1908
|
+
const dotenvPath = path7.join(projectDir, ".env");
|
|
1909
|
+
if (!fs8.existsSync(dotenvPath)) return {};
|
|
1910
|
+
const result = {};
|
|
1911
|
+
for (const rawLine of fs8.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
1912
|
+
const line = rawLine.trim();
|
|
1913
|
+
if (!line || line.startsWith("#")) continue;
|
|
1914
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
1915
|
+
if (!match) continue;
|
|
1916
|
+
let value = match[2].trim();
|
|
1917
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1918
|
+
value = value.slice(1, -1);
|
|
1919
|
+
}
|
|
1920
|
+
const commentIdx = value.indexOf(" #");
|
|
1921
|
+
if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
|
|
1922
|
+
if (value) result[match[1]] = value;
|
|
1923
|
+
}
|
|
1924
|
+
return result;
|
|
1925
|
+
}
|
|
1926
|
+
function stripBlockingEnv(env) {
|
|
1927
|
+
const out = { ...env };
|
|
1928
|
+
delete out.DATABASE_URL;
|
|
1929
|
+
delete out.AI_BASE_URL;
|
|
1930
|
+
return out;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// src/executor.ts
|
|
1934
|
+
async function runExecutable(profileName, input) {
|
|
1935
|
+
const profilePath = resolveProfilePath(profileName);
|
|
1936
|
+
const profile = loadProfile(profilePath);
|
|
1937
|
+
const missing = validateScriptReferences(profile, allScriptNames);
|
|
1938
|
+
if (missing.length > 0) {
|
|
1939
|
+
return finish({ exitCode: 99, reason: `profile references unknown scripts: ${missing.join(", ")}` });
|
|
1940
|
+
}
|
|
1941
|
+
let args;
|
|
1942
|
+
try {
|
|
1943
|
+
args = validateInputs(profile.inputs, input.cliArgs);
|
|
1944
|
+
} catch (err) {
|
|
1945
|
+
return finish({ exitCode: 64, reason: err instanceof Error ? err.message : String(err) });
|
|
1946
|
+
}
|
|
1947
|
+
const toolResults = verifyCliTools(profile.cliTools, input.cwd);
|
|
1948
|
+
const firstFail = firstRequiredFailure(toolResults, profile.cliTools);
|
|
1949
|
+
if (firstFail) {
|
|
1950
|
+
return finish({ exitCode: 99, reason: `required CLI tool check failed: ${firstFail.error}` });
|
|
1951
|
+
}
|
|
1952
|
+
const modelSpec = profile.claudeCode.model === "inherit" ? input.config.agent.model : profile.claudeCode.model;
|
|
1953
|
+
let model;
|
|
1954
|
+
try {
|
|
1955
|
+
model = parseProviderModel(modelSpec);
|
|
1956
|
+
} catch (err) {
|
|
1957
|
+
return finish({ exitCode: 99, reason: `agent.model invalid: ${err instanceof Error ? err.message : String(err)}` });
|
|
1958
|
+
}
|
|
1959
|
+
let litellm = null;
|
|
1960
|
+
try {
|
|
1961
|
+
litellm = await startLitellmIfNeeded(model, input.cwd);
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
return finish({ exitCode: 99, reason: `litellm startup failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1964
|
+
}
|
|
1965
|
+
const ctx = {
|
|
1966
|
+
args,
|
|
1967
|
+
cwd: input.cwd,
|
|
1968
|
+
config: input.config,
|
|
1969
|
+
verbose: input.verbose,
|
|
1970
|
+
quiet: input.quiet,
|
|
1971
|
+
data: {},
|
|
1972
|
+
output: { exitCode: 0 }
|
|
1973
|
+
};
|
|
1974
|
+
const ndjsonDir = path8.join(input.cwd, ".kody2");
|
|
1975
|
+
const invokeAgent = async (prompt) => runAgent({
|
|
1976
|
+
prompt,
|
|
1977
|
+
model,
|
|
1978
|
+
cwd: input.cwd,
|
|
1979
|
+
litellmUrl: litellm?.url ?? null,
|
|
1980
|
+
verbose: input.verbose,
|
|
1981
|
+
quiet: input.quiet,
|
|
1982
|
+
ndjsonDir,
|
|
1983
|
+
allowedToolsOverride: profile.claudeCode.tools,
|
|
1984
|
+
permissionModeOverride: profile.claudeCode.permissionMode
|
|
1985
|
+
});
|
|
1986
|
+
ctx.data.__invokeAgent = invokeAgent;
|
|
1987
|
+
try {
|
|
1988
|
+
for (const entry of profile.scripts.preflight) {
|
|
1989
|
+
if (!shouldRun(entry, ctx)) continue;
|
|
1990
|
+
const fn = preflightScripts[entry.script];
|
|
1991
|
+
if (!fn) return finish({ exitCode: 99, reason: `preflight script not registered: ${entry.script}` });
|
|
1992
|
+
await fn(ctx, profile);
|
|
1993
|
+
if (ctx.skipAgent && ctx.output.exitCode !== void 0 && ctx.output.exitCode !== 0) {
|
|
1994
|
+
return finish(ctx.output);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
let agentResult = null;
|
|
1998
|
+
if (!ctx.skipAgent) {
|
|
1999
|
+
const prompt = ctx.data.prompt;
|
|
2000
|
+
if (!prompt) {
|
|
2001
|
+
return finish({ exitCode: 99, reason: "composePrompt did not produce a prompt (ctx.data.prompt missing)" });
|
|
2002
|
+
}
|
|
2003
|
+
agentResult = await invokeAgent(prompt);
|
|
2004
|
+
}
|
|
2005
|
+
for (const entry of profile.scripts.postflight) {
|
|
2006
|
+
if (!shouldRun(entry, ctx)) continue;
|
|
2007
|
+
const fn = postflightScripts[entry.script];
|
|
2008
|
+
if (!fn) return finish({ exitCode: 99, reason: `postflight script not registered: ${entry.script}` });
|
|
2009
|
+
await fn(ctx, profile, agentResult);
|
|
2010
|
+
}
|
|
2011
|
+
return finish(ctx.output);
|
|
2012
|
+
} finally {
|
|
2013
|
+
try {
|
|
2014
|
+
litellm?.kill();
|
|
2015
|
+
} catch {
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
function resolveProfilePath(profileName) {
|
|
2020
|
+
const here = path8.dirname(new URL(import.meta.url).pathname);
|
|
2021
|
+
const candidates = [
|
|
2022
|
+
path8.join(here, "executables", profileName, "profile.json"),
|
|
2023
|
+
// same-dir sibling (dev)
|
|
2024
|
+
path8.join(here, "..", "executables", profileName, "profile.json"),
|
|
2025
|
+
// up one (prod: dist/bin → dist/executables)
|
|
2026
|
+
path8.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
2027
|
+
// fallback
|
|
2028
|
+
];
|
|
2029
|
+
for (const c of candidates) {
|
|
2030
|
+
if (fs9.existsSync(c)) return c;
|
|
2031
|
+
}
|
|
2032
|
+
return candidates[0];
|
|
2033
|
+
}
|
|
2034
|
+
function validateInputs(specs, raw) {
|
|
2035
|
+
const out = {};
|
|
2036
|
+
for (const spec of specs) {
|
|
2037
|
+
const v = raw[spec.name];
|
|
2038
|
+
if (v === void 0 || v === null) continue;
|
|
2039
|
+
out[spec.name] = coerce(spec, v);
|
|
2040
|
+
}
|
|
2041
|
+
for (const spec of specs) {
|
|
2042
|
+
const present = out[spec.name] !== void 0;
|
|
2043
|
+
if (present) continue;
|
|
2044
|
+
const isRequired = spec.required === true || satisfiesRequiredWhen(spec.requiredWhen, out);
|
|
2045
|
+
if (isRequired) {
|
|
2046
|
+
throw new Error(`required input missing: ${spec.flag} (${spec.name})`);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
return out;
|
|
2050
|
+
}
|
|
2051
|
+
function coerce(spec, v) {
|
|
2052
|
+
switch (spec.type) {
|
|
2053
|
+
case "int": {
|
|
2054
|
+
const n = typeof v === "number" ? v : parseInt(String(v), 10);
|
|
2055
|
+
if (Number.isNaN(n)) throw new Error(`${spec.flag} must be an integer`);
|
|
2056
|
+
return n;
|
|
2057
|
+
}
|
|
2058
|
+
case "bool": {
|
|
2059
|
+
if (typeof v === "boolean") return v;
|
|
2060
|
+
const s = String(v).toLowerCase();
|
|
2061
|
+
return s === "true" || s === "1" || s === "yes";
|
|
2062
|
+
}
|
|
2063
|
+
case "enum": {
|
|
2064
|
+
const s = String(v);
|
|
2065
|
+
if (!spec.values?.includes(s)) throw new Error(`${spec.flag} must be one of: ${spec.values?.join("|")}`);
|
|
2066
|
+
return s;
|
|
2067
|
+
}
|
|
2068
|
+
default:
|
|
2069
|
+
return String(v);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
function satisfiesRequiredWhen(rw, current) {
|
|
2073
|
+
if (!rw) return false;
|
|
2074
|
+
for (const [key, want] of Object.entries(rw)) {
|
|
2075
|
+
const actual = String(current[key] ?? "");
|
|
2076
|
+
const wanted = Array.isArray(want) ? want.map(String) : [String(want)];
|
|
2077
|
+
if (wanted.includes(actual)) return true;
|
|
2078
|
+
}
|
|
2079
|
+
return false;
|
|
2080
|
+
}
|
|
2081
|
+
function shouldRun(entry, ctx) {
|
|
2082
|
+
if (!entry.runWhen) return true;
|
|
2083
|
+
for (const [key, want] of Object.entries(entry.runWhen)) {
|
|
2084
|
+
const actual = resolveDottedPath(ctx, key);
|
|
2085
|
+
const wanted = Array.isArray(want) ? want : [want];
|
|
2086
|
+
if (!wanted.map(String).includes(String(actual))) return false;
|
|
2087
|
+
}
|
|
2088
|
+
return true;
|
|
2089
|
+
}
|
|
2090
|
+
function resolveDottedPath(root, key) {
|
|
2091
|
+
const parts = key.split(".");
|
|
2092
|
+
let cur = root;
|
|
2093
|
+
for (const p of parts) {
|
|
2094
|
+
if (cur === null || cur === void 0) return void 0;
|
|
2095
|
+
cur = cur[p];
|
|
2096
|
+
}
|
|
2097
|
+
return cur;
|
|
2098
|
+
}
|
|
2099
|
+
function finish(out) {
|
|
2100
|
+
if (out.prUrl) process.stdout.write(`PR_URL=${out.prUrl}
|
|
2101
|
+
`);
|
|
2102
|
+
else if (out.reason) process.stdout.write(`PR_URL=FAILED: ${out.reason}
|
|
2103
|
+
`);
|
|
2104
|
+
return out;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// src/kody2-cli.ts
|
|
2108
|
+
import * as fs10 from "fs";
|
|
2109
|
+
import * as path9 from "path";
|
|
2110
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
2111
|
+
var CI_HELP = `kody2 ci \u2014 minimal-YAML autonomous engineer (CI preflight + run)
|
|
2112
|
+
|
|
2113
|
+
Usage:
|
|
2114
|
+
kody2 ci --issue <N> [--cwd <path>] [--verbose|--quiet]
|
|
2115
|
+
[--skip-install] [--skip-litellm] [--package-manager pnpm|yarn|bun|npm]
|
|
2116
|
+
|
|
2117
|
+
Options:
|
|
2118
|
+
--issue <N> GitHub issue number to work on (required)
|
|
2119
|
+
--cwd <path> Project directory (default: cwd)
|
|
2120
|
+
--verbose Print full tool output
|
|
2121
|
+
--quiet Print only errors and final PR_URL
|
|
2122
|
+
--skip-install Skip dependency install (pre-warmed runners)
|
|
2123
|
+
--skip-litellm Skip LiteLLM proxy install (Anthropic-direct)
|
|
2124
|
+
--package-manager Override package-manager auto-detect
|
|
2125
|
+
|
|
2126
|
+
Environment:
|
|
2127
|
+
ALL_SECRETS JSON blob of all GitHub secrets (auto-populated in CI)
|
|
2128
|
+
KODY_TOKEN|GH_TOKEN|GITHUB_TOKEN|GH_PAT auth token for gh/git operations
|
|
2129
|
+
|
|
2130
|
+
Exit codes (inherited from kody2 run):
|
|
2131
|
+
0 success (PR opened, verify passed)
|
|
2132
|
+
1 agent reported FAILED (draft PR opened)
|
|
2133
|
+
2 verify failed (draft PR opened)
|
|
2134
|
+
3 no commits to ship
|
|
2135
|
+
4 PR creation failed
|
|
2136
|
+
5 uncommitted changes on target branch
|
|
2137
|
+
99 wrapper crashed
|
|
2138
|
+
`;
|
|
2139
|
+
function parseCiArgs(argv) {
|
|
2140
|
+
const result = { errors: [] };
|
|
2141
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2142
|
+
const arg = argv[i];
|
|
2143
|
+
if (arg === "--issue") {
|
|
2144
|
+
const n = parseInt(argv[++i] ?? "", 10);
|
|
2145
|
+
if (Number.isNaN(n) || n <= 0) result.errors.push("--issue requires a positive integer");
|
|
2146
|
+
else result.issueNumber = n;
|
|
2147
|
+
} else if (arg === "--cwd") {
|
|
2148
|
+
result.cwd = argv[++i];
|
|
2149
|
+
} else if (arg === "--verbose") result.verbose = true;
|
|
2150
|
+
else if (arg === "--quiet") result.quiet = true;
|
|
2151
|
+
else if (arg === "--skip-install") result.skipInstall = true;
|
|
2152
|
+
else if (arg === "--skip-litellm") result.skipLitellm = true;
|
|
2153
|
+
else if (arg === "--package-manager") {
|
|
2154
|
+
const v = argv[++i];
|
|
2155
|
+
if (v === "pnpm" || v === "yarn" || v === "bun" || v === "npm") result.packageManager = v;
|
|
2156
|
+
else result.errors.push(`--package-manager must be one of pnpm|yarn|bun|npm (got: ${v})`);
|
|
2157
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
2158
|
+
result.errors.push("__HELP__");
|
|
2159
|
+
} else if (arg?.startsWith("--")) {
|
|
2160
|
+
result.errors.push(`unknown arg: ${arg}`);
|
|
2161
|
+
} else if (arg) {
|
|
2162
|
+
result.errors.push(`unexpected positional: ${arg}`);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
if (!result.issueNumber && !result.errors.includes("__HELP__")) {
|
|
2166
|
+
result.errors.push("--issue <N> is required");
|
|
2167
|
+
}
|
|
2168
|
+
return result;
|
|
2169
|
+
}
|
|
2170
|
+
function unpackAllSecrets(env = process.env) {
|
|
2171
|
+
const raw = env.ALL_SECRETS;
|
|
2172
|
+
if (!raw) return 0;
|
|
2173
|
+
let parsed;
|
|
2174
|
+
try {
|
|
2175
|
+
parsed = JSON.parse(raw);
|
|
2176
|
+
} catch {
|
|
2177
|
+
return 0;
|
|
2178
|
+
}
|
|
2179
|
+
if (!parsed || typeof parsed !== "object") return 0;
|
|
2180
|
+
let count = 0;
|
|
2181
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
2182
|
+
if (typeof v !== "string" || !v) continue;
|
|
2183
|
+
if (env[k] !== void 0) continue;
|
|
2184
|
+
env[k] = v;
|
|
2185
|
+
count++;
|
|
2186
|
+
}
|
|
2187
|
+
return count;
|
|
2188
|
+
}
|
|
2189
|
+
function resolveAuthToken(env = process.env) {
|
|
2190
|
+
const token = env.KODY_TOKEN || env.GH_TOKEN || env.GITHUB_TOKEN || env.GH_PAT;
|
|
2191
|
+
if (token && !env.GH_TOKEN) env.GH_TOKEN = token;
|
|
2192
|
+
return token;
|
|
2193
|
+
}
|
|
2194
|
+
function detectPackageManager(cwd) {
|
|
2195
|
+
if (fs10.existsSync(path9.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2196
|
+
if (fs10.existsSync(path9.join(cwd, "yarn.lock"))) return "yarn";
|
|
2197
|
+
if (fs10.existsSync(path9.join(cwd, "bun.lockb"))) return "bun";
|
|
2198
|
+
return "npm";
|
|
2199
|
+
}
|
|
2200
|
+
function shellOut(cmd, args, cwd, stream = true) {
|
|
2201
|
+
try {
|
|
2202
|
+
execFileSync11(cmd, args, {
|
|
2203
|
+
cwd,
|
|
2204
|
+
stdio: stream ? "inherit" : "pipe",
|
|
2205
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
|
|
2206
|
+
});
|
|
2207
|
+
return 0;
|
|
2208
|
+
} catch (err) {
|
|
2209
|
+
const e = err;
|
|
2210
|
+
return e.status ?? 1;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
function isOnPath(bin) {
|
|
2214
|
+
try {
|
|
2215
|
+
execFileSync11("which", [bin], { stdio: "pipe" });
|
|
2216
|
+
return true;
|
|
2217
|
+
} catch {
|
|
2218
|
+
return false;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
function ensurePackageManagerInstalled(pm, cwd) {
|
|
2222
|
+
if (pm === "npm" || isOnPath(pm)) return 0;
|
|
2223
|
+
process.stdout.write(`\u2192 kody2: ${pm} not on PATH \u2014 installing via npm install -g ${pm}
|
|
2224
|
+
`);
|
|
2225
|
+
return shellOut("npm", ["install", "-g", pm], cwd);
|
|
2226
|
+
}
|
|
2227
|
+
function installDeps(pm, cwd) {
|
|
2228
|
+
const ensureCode = ensurePackageManagerInstalled(pm, cwd);
|
|
2229
|
+
if (ensureCode !== 0) return ensureCode;
|
|
2230
|
+
const args = {
|
|
2231
|
+
pnpm: ["install", "--frozen-lockfile"],
|
|
2232
|
+
yarn: ["install", "--frozen-lockfile"],
|
|
2233
|
+
bun: ["install", "--frozen-lockfile"],
|
|
2234
|
+
npm: ["ci"]
|
|
2235
|
+
};
|
|
2236
|
+
return shellOut(pm, args[pm], cwd);
|
|
2237
|
+
}
|
|
2238
|
+
function installLitellmIfNeeded(cwd) {
|
|
2239
|
+
try {
|
|
2240
|
+
const cfg = loadConfig(cwd);
|
|
2241
|
+
const model = parseProviderModel(cfg.agent.model);
|
|
2242
|
+
if (!needsLitellmProxy(model)) {
|
|
2243
|
+
process.stdout.write("\u2192 kody2: provider is anthropic/claude, skipping LiteLLM install\n");
|
|
2244
|
+
return 0;
|
|
2245
|
+
}
|
|
2246
|
+
} catch {
|
|
2247
|
+
}
|
|
2248
|
+
try {
|
|
2249
|
+
execFileSync11("python3", ["-c", "import litellm"], { stdio: "pipe" });
|
|
2250
|
+
process.stdout.write("\u2192 kody2: litellm already installed\n");
|
|
2251
|
+
return 0;
|
|
2252
|
+
} catch {
|
|
2253
|
+
}
|
|
2254
|
+
process.stdout.write("\u2192 kody2: installing litellm (pip install 'litellm[proxy]')\n");
|
|
2255
|
+
return shellOut("pip", ["install", "litellm[proxy]"], cwd);
|
|
2256
|
+
}
|
|
2257
|
+
function configureGitIdentity(cwd) {
|
|
2258
|
+
try {
|
|
2259
|
+
const name = execFileSync11("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
|
|
2260
|
+
if (name) return;
|
|
2261
|
+
} catch {
|
|
2262
|
+
}
|
|
2263
|
+
try {
|
|
2264
|
+
execFileSync11("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
|
|
2265
|
+
} catch {
|
|
2266
|
+
}
|
|
2267
|
+
try {
|
|
2268
|
+
execFileSync11("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
|
|
2269
|
+
} catch {
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
function postFailureTail(issueNumber, cwd, reason) {
|
|
2273
|
+
if (!issueNumber) return;
|
|
2274
|
+
const logPath = path9.join(cwd, ".kody2", "last-run.jsonl");
|
|
2275
|
+
let tail = "";
|
|
2276
|
+
try {
|
|
2277
|
+
if (fs10.existsSync(logPath)) {
|
|
2278
|
+
const content = fs10.readFileSync(logPath, "utf-8");
|
|
2279
|
+
tail = content.slice(-3e3);
|
|
2280
|
+
}
|
|
2281
|
+
} catch {
|
|
2282
|
+
}
|
|
2283
|
+
const body = tail ? `\u26A0\uFE0F kody2 preflight failed: ${truncate(reason, 500)}
|
|
2284
|
+
|
|
2285
|
+
<details><summary>Last-run log tail</summary>
|
|
2286
|
+
|
|
2287
|
+
\`\`\`
|
|
2288
|
+
${tail}
|
|
2289
|
+
\`\`\`
|
|
2290
|
+
|
|
2291
|
+
</details>` : `\u26A0\uFE0F kody2 preflight failed: ${truncate(reason, 1500)}`;
|
|
2292
|
+
try {
|
|
2293
|
+
postIssueComment(issueNumber, body, cwd);
|
|
2294
|
+
} catch {
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
async function runCi(argv) {
|
|
2298
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
2299
|
+
process.stdout.write(CI_HELP);
|
|
2300
|
+
return 0;
|
|
2301
|
+
}
|
|
2302
|
+
const args = parseCiArgs(argv);
|
|
2303
|
+
if (args.errors.length > 0 && !args.errors.includes("__HELP__")) {
|
|
2304
|
+
for (const e of args.errors) process.stderr.write(`error: ${e}
|
|
2305
|
+
`);
|
|
2306
|
+
process.stderr.write("\n" + CI_HELP);
|
|
2307
|
+
return 64;
|
|
2308
|
+
}
|
|
2309
|
+
const cwd = args.cwd ? path9.resolve(args.cwd) : process.cwd();
|
|
2310
|
+
const issueNumber = args.issueNumber;
|
|
2311
|
+
process.stdout.write(`\u2192 kody2 preflight (cwd=${cwd}, issue=${issueNumber})
|
|
2312
|
+
`);
|
|
2313
|
+
try {
|
|
2314
|
+
const n = unpackAllSecrets();
|
|
2315
|
+
if (n > 0) process.stdout.write(`\u2192 kody2: unpacked ${n} secret(s) from ALL_SECRETS
|
|
2316
|
+
`);
|
|
2317
|
+
resolveAuthToken();
|
|
2318
|
+
reactToTriggerComment(cwd);
|
|
2319
|
+
const pm = args.packageManager ?? detectPackageManager(cwd);
|
|
2320
|
+
process.stdout.write(`\u2192 kody2: package manager = ${pm}
|
|
2321
|
+
`);
|
|
2322
|
+
if (!args.skipInstall) {
|
|
2323
|
+
const code = installDeps(pm, cwd);
|
|
2324
|
+
if (code !== 0) {
|
|
2325
|
+
postFailureTail(issueNumber, cwd, `dependency install failed (${pm}, exit ${code})`);
|
|
2326
|
+
return 99;
|
|
2327
|
+
}
|
|
2328
|
+
} else {
|
|
2329
|
+
process.stdout.write("\u2192 kody2: skipping dep install (--skip-install)\n");
|
|
2330
|
+
}
|
|
2331
|
+
if (!args.skipLitellm) {
|
|
2332
|
+
const code = installLitellmIfNeeded(cwd);
|
|
2333
|
+
if (code !== 0) {
|
|
2334
|
+
postFailureTail(issueNumber, cwd, `litellm install failed (exit ${code})`);
|
|
2335
|
+
return 99;
|
|
2336
|
+
}
|
|
2337
|
+
} else {
|
|
2338
|
+
process.stdout.write("\u2192 kody2: skipping LiteLLM install (--skip-litellm)\n");
|
|
2339
|
+
}
|
|
2340
|
+
configureGitIdentity(cwd);
|
|
2341
|
+
} catch (err) {
|
|
2342
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2343
|
+
process.stderr.write(`[kody2] preflight crashed: ${msg}
|
|
2344
|
+
`);
|
|
2345
|
+
postFailureTail(issueNumber, cwd, `preflight crashed: ${msg}`);
|
|
2346
|
+
return 99;
|
|
2347
|
+
}
|
|
2348
|
+
process.stdout.write("\u2192 kody2: preflight done, handing off to kody2 run\n\n");
|
|
2349
|
+
try {
|
|
2350
|
+
const config = loadConfig(cwd);
|
|
2351
|
+
const result = await runExecutable("build", {
|
|
2352
|
+
cliArgs: { mode: "run", issue: issueNumber },
|
|
2353
|
+
cwd,
|
|
2354
|
+
config,
|
|
2355
|
+
verbose: args.verbose,
|
|
2356
|
+
quiet: args.quiet
|
|
2357
|
+
});
|
|
2358
|
+
if (result.exitCode !== 0 && result.exitCode !== 1 && result.exitCode !== 2) {
|
|
2359
|
+
postFailureTail(issueNumber, cwd, result.reason || `exit ${result.exitCode}`);
|
|
2360
|
+
}
|
|
2361
|
+
return result.exitCode;
|
|
2362
|
+
} catch (err) {
|
|
2363
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2364
|
+
process.stderr.write(`[kody2] run crashed: ${msg}
|
|
2365
|
+
`);
|
|
2366
|
+
if (err instanceof Error && err.stack) process.stderr.write(err.stack + "\n");
|
|
2367
|
+
postFailureTail(issueNumber, cwd, `run crashed: ${msg}`);
|
|
2368
|
+
return 99;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// src/entry.ts
|
|
2373
|
+
var HELP_TEXT = `kody2 \u2014 single-session autonomous engineer
|
|
2374
|
+
|
|
2375
|
+
Usage:
|
|
2376
|
+
kody2 run --issue <N> [--cwd <path>] [--verbose|--quiet] [--dry-run]
|
|
2377
|
+
kody2 ci --issue <N> [preflight flags \u2014 see: kody2 ci --help]
|
|
2378
|
+
kody2 fix --pr <N> [--feedback "..."] [--cwd <path>] [--verbose|--quiet]
|
|
2379
|
+
kody2 fix-ci --pr <N> [--run-id <ID>] [--cwd <path>] [--verbose|--quiet]
|
|
2380
|
+
kody2 resolve --pr <N> [--cwd <path>] [--verbose|--quiet]
|
|
2381
|
+
kody2 help
|
|
2382
|
+
kody2 version
|
|
2383
|
+
|
|
2384
|
+
All commands dispatch to the Build executable with a specific mode. The
|
|
2385
|
+
executable is defined by \`src/executables/build/profile.json\`.
|
|
2386
|
+
|
|
2387
|
+
Exit codes:
|
|
2388
|
+
0 success (PR opened, verify passed)
|
|
2389
|
+
1 agent reported FAILED (draft PR opened)
|
|
2390
|
+
2 verify failed (draft PR opened)
|
|
2391
|
+
3 no commits to ship
|
|
2392
|
+
4 PR creation failed
|
|
2393
|
+
5 uncommitted changes on target branch
|
|
2394
|
+
64 invalid CLI args
|
|
2395
|
+
99 wrapper crashed
|
|
2396
|
+
`;
|
|
2397
|
+
function parseArgs(argv) {
|
|
2398
|
+
const result = { command: "help", errors: [] };
|
|
2399
|
+
if (argv.length === 0) return result;
|
|
2400
|
+
const cmd = argv[0];
|
|
2401
|
+
if (cmd === "help" || cmd === "--help" || cmd === "-h") return { ...result, command: "help" };
|
|
2402
|
+
if (cmd === "version" || cmd === "--version" || cmd === "-v") return { ...result, command: "version" };
|
|
2403
|
+
if (cmd === "ci") {
|
|
2404
|
+
return { ...result, command: "ci", ciArgv: argv.slice(1) };
|
|
2405
|
+
}
|
|
2406
|
+
if (cmd === "run" || cmd === "fix" || cmd === "fix-ci" || cmd === "resolve") {
|
|
2407
|
+
result.command = cmd;
|
|
2408
|
+
parseCommandArgs(cmd, argv.slice(1), result);
|
|
2409
|
+
return result;
|
|
2410
|
+
}
|
|
2411
|
+
result.errors.push(`unknown command: ${cmd}`);
|
|
2412
|
+
return result;
|
|
2413
|
+
}
|
|
2414
|
+
function parseCommandArgs(cmd, rest, result) {
|
|
2415
|
+
for (let i = 0; i < rest.length; i++) {
|
|
2416
|
+
const arg = rest[i];
|
|
2417
|
+
if (arg === "--issue") {
|
|
2418
|
+
const n = parseInt(rest[++i] ?? "", 10);
|
|
2419
|
+
if (Number.isNaN(n) || n <= 0) result.errors.push("--issue requires a positive integer");
|
|
2420
|
+
else result.issueNumber = n;
|
|
2421
|
+
} else if (arg === "--pr") {
|
|
2422
|
+
const n = parseInt(rest[++i] ?? "", 10);
|
|
2423
|
+
if (Number.isNaN(n) || n <= 0) result.errors.push("--pr requires a positive integer");
|
|
2424
|
+
else result.prNumber = n;
|
|
2425
|
+
} else if (arg === "--feedback") {
|
|
2426
|
+
result.feedback = rest[++i];
|
|
2427
|
+
} else if (arg === "--run-id") {
|
|
2428
|
+
result.runId = rest[++i];
|
|
2429
|
+
} else if (arg === "--cwd") {
|
|
2430
|
+
result.cwd = rest[++i];
|
|
2431
|
+
} else if (arg === "--verbose") result.verbose = true;
|
|
2432
|
+
else if (arg === "--quiet") result.quiet = true;
|
|
2433
|
+
else if (arg === "--dry-run") result.dryRun = true;
|
|
2434
|
+
else result.errors.push(`unknown arg: ${arg}`);
|
|
2435
|
+
}
|
|
2436
|
+
if (cmd === "run" && !result.issueNumber) result.errors.push("--issue <N> is required for run");
|
|
2437
|
+
if (cmd === "fix" && !result.prNumber) result.errors.push("--pr <N> is required for fix");
|
|
2438
|
+
if (cmd === "fix-ci" && !result.prNumber) result.errors.push("--pr <N> is required for fix-ci");
|
|
2439
|
+
if (cmd === "resolve" && !result.prNumber) result.errors.push("--pr <N> is required for resolve");
|
|
2440
|
+
}
|
|
2441
|
+
async function main(argv = process.argv.slice(2)) {
|
|
2442
|
+
const args = parseArgs(argv);
|
|
2443
|
+
if (args.errors.length > 0) {
|
|
2444
|
+
for (const e of args.errors) process.stderr.write(`error: ${e}
|
|
2445
|
+
`);
|
|
2446
|
+
process.stderr.write("\n" + HELP_TEXT);
|
|
2447
|
+
return 64;
|
|
2448
|
+
}
|
|
2449
|
+
if (args.command === "help") {
|
|
2450
|
+
process.stdout.write(HELP_TEXT);
|
|
2451
|
+
return 0;
|
|
2452
|
+
}
|
|
2453
|
+
if (args.command === "version") {
|
|
2454
|
+
process.stdout.write("kody2 0.2.0\n");
|
|
2455
|
+
return 0;
|
|
2456
|
+
}
|
|
2457
|
+
if (args.command === "ci") {
|
|
2458
|
+
try {
|
|
2459
|
+
return await runCi(args.ciArgv ?? []);
|
|
2460
|
+
} catch (err) {
|
|
2461
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2462
|
+
process.stderr.write(`[kody2] fatal: ${msg}
|
|
2463
|
+
`);
|
|
2464
|
+
if (err instanceof Error && err.stack) process.stderr.write(err.stack + "\n");
|
|
2465
|
+
return 99;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
const cwd = args.cwd ?? process.cwd();
|
|
2469
|
+
let config;
|
|
2470
|
+
try {
|
|
2471
|
+
config = loadConfig(cwd);
|
|
2472
|
+
} catch (err) {
|
|
2473
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2474
|
+
process.stderr.write(`[kody2] config error: ${msg}
|
|
2475
|
+
`);
|
|
2476
|
+
process.stdout.write(`PR_URL=FAILED: config error: ${msg}
|
|
2477
|
+
`);
|
|
2478
|
+
return 99;
|
|
2479
|
+
}
|
|
2480
|
+
const cliArgs = { mode: args.command };
|
|
2481
|
+
if (args.issueNumber !== void 0) cliArgs.issue = args.issueNumber;
|
|
2482
|
+
if (args.prNumber !== void 0) cliArgs.pr = args.prNumber;
|
|
2483
|
+
if (args.feedback !== void 0) cliArgs.feedback = args.feedback;
|
|
2484
|
+
if (args.runId !== void 0) cliArgs.runId = args.runId;
|
|
2485
|
+
try {
|
|
2486
|
+
const result = await runExecutable("build", {
|
|
2487
|
+
cliArgs,
|
|
2488
|
+
cwd,
|
|
2489
|
+
config,
|
|
2490
|
+
verbose: args.verbose,
|
|
2491
|
+
quiet: args.quiet
|
|
2492
|
+
});
|
|
2493
|
+
return result.exitCode;
|
|
2494
|
+
} catch (err) {
|
|
2495
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2496
|
+
process.stderr.write(`[kody2] wrapper crashed: ${msg}
|
|
2497
|
+
`);
|
|
2498
|
+
if (err instanceof Error && err.stack) process.stderr.write(err.stack + "\n");
|
|
2499
|
+
process.stdout.write(`PR_URL=FAILED: wrapper crashed: ${msg}
|
|
2500
|
+
`);
|
|
2501
|
+
return 99;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// bin/kody2.ts
|
|
2506
|
+
main().then((code) => {
|
|
2507
|
+
process.exit(code);
|
|
2508
|
+
}).catch((err) => {
|
|
2509
|
+
process.stderr.write(`[kody2] fatal: ${err instanceof Error ? err.message : String(err)}
|
|
2510
|
+
`);
|
|
2511
|
+
process.exit(99);
|
|
2512
|
+
});
|