@kody-ade/kody-engine 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/kody2.js +1529 -1312
- package/dist/executables/build/profile.json +4 -3
- package/dist/executables/types.ts +2 -6
- package/package.json +15 -12
- package/templates/kody2.yml +15 -6
package/dist/bin/kody2.js
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// package.json
|
|
4
|
+
var package_default = {
|
|
5
|
+
name: "@kody-ade/kody-engine",
|
|
6
|
+
version: "0.2.2",
|
|
7
|
+
description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
8
|
+
license: "MIT",
|
|
9
|
+
type: "module",
|
|
10
|
+
bin: {
|
|
11
|
+
kody2: "dist/bin/kody2.js"
|
|
12
|
+
},
|
|
13
|
+
files: [
|
|
14
|
+
"dist",
|
|
15
|
+
"templates",
|
|
16
|
+
"kody.config.schema.json"
|
|
17
|
+
],
|
|
18
|
+
scripts: {
|
|
19
|
+
kody2: "tsx bin/kody2.ts",
|
|
20
|
+
build: `tsup && node -e "require('fs').cpSync('src/executables', 'dist/executables', { recursive: true })"`,
|
|
21
|
+
test: "vitest run tests/unit tests/int --no-coverage",
|
|
22
|
+
"test:e2e": "vitest run tests/e2e --no-coverage",
|
|
23
|
+
"test:all": "vitest run tests --no-coverage",
|
|
24
|
+
typecheck: "tsc --noEmit",
|
|
25
|
+
lint: "biome check",
|
|
26
|
+
"lint:fix": "biome check --write",
|
|
27
|
+
format: "biome format --write",
|
|
28
|
+
prepublishOnly: "pnpm build"
|
|
29
|
+
},
|
|
30
|
+
dependencies: {
|
|
31
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.92"
|
|
32
|
+
},
|
|
33
|
+
devDependencies: {
|
|
34
|
+
"@biomejs/biome": "^2.4.12",
|
|
35
|
+
"@types/node": "^22.5.4",
|
|
36
|
+
tsup: "^8.5.1",
|
|
37
|
+
tsx: "^4.21.0",
|
|
38
|
+
typescript: "~5.7.0",
|
|
39
|
+
vitest: "^4.1.1"
|
|
40
|
+
},
|
|
41
|
+
engines: {
|
|
42
|
+
node: ">=22"
|
|
43
|
+
},
|
|
44
|
+
repository: {
|
|
45
|
+
type: "git",
|
|
46
|
+
url: "git+https://github.com/aharonyaircohen/kody-engine.git"
|
|
47
|
+
},
|
|
48
|
+
homepage: "https://github.com/aharonyaircohen/kody-engine",
|
|
49
|
+
bugs: "https://github.com/aharonyaircohen/kody-engine/issues"
|
|
50
|
+
};
|
|
51
|
+
|
|
3
52
|
// src/config.ts
|
|
4
53
|
import * as fs from "fs";
|
|
5
54
|
import * as path from "path";
|
|
@@ -8,9 +57,7 @@ var LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
|
|
|
8
57
|
function parseProviderModel(s) {
|
|
9
58
|
const slash = s.indexOf("/");
|
|
10
59
|
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
|
-
);
|
|
60
|
+
throw new Error(`Invalid model spec '${s}' \u2014 expected 'provider/model' (e.g. 'minimax/MiniMax-M2.7-highspeed')`);
|
|
14
61
|
}
|
|
15
62
|
return { provider: s.slice(0, slash), model: s.slice(slash + 1) };
|
|
16
63
|
}
|
|
@@ -68,7 +115,8 @@ function parseIssueContext(raw) {
|
|
|
68
115
|
const r = raw;
|
|
69
116
|
const out = {};
|
|
70
117
|
if (typeof r.commentLimit === "number" && r.commentLimit > 0) out.commentLimit = Math.floor(r.commentLimit);
|
|
71
|
-
if (typeof r.commentMaxBytes === "number" && r.commentMaxBytes > 0)
|
|
118
|
+
if (typeof r.commentMaxBytes === "number" && r.commentMaxBytes > 0)
|
|
119
|
+
out.commentMaxBytes = Math.floor(r.commentMaxBytes);
|
|
72
120
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
73
121
|
}
|
|
74
122
|
function parseTestRequirements(raw) {
|
|
@@ -89,12 +137,293 @@ function getAnthropicApiKeyOrDummy() {
|
|
|
89
137
|
}
|
|
90
138
|
|
|
91
139
|
// src/executor.ts
|
|
92
|
-
import * as
|
|
140
|
+
import * as fs10 from "fs";
|
|
93
141
|
import * as path8 from "path";
|
|
94
142
|
|
|
95
|
-
// src/
|
|
143
|
+
// src/agent.ts
|
|
96
144
|
import * as fs2 from "fs";
|
|
97
145
|
import * as path2 from "path";
|
|
146
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
147
|
+
|
|
148
|
+
// src/format.ts
|
|
149
|
+
function renderEvent(msg, opts = {}) {
|
|
150
|
+
if (opts.quiet) {
|
|
151
|
+
if (msg.type === "result") return formatResult(msg);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
switch (msg.type) {
|
|
155
|
+
case "system":
|
|
156
|
+
return null;
|
|
157
|
+
case "assistant":
|
|
158
|
+
return formatAssistant(msg, opts);
|
|
159
|
+
case "user":
|
|
160
|
+
return formatUserToolResult(msg, opts);
|
|
161
|
+
case "result":
|
|
162
|
+
return formatResult(msg);
|
|
163
|
+
default:
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function formatAssistant(msg, _opts) {
|
|
168
|
+
const content = msg.message?.content ?? [];
|
|
169
|
+
const lines = [];
|
|
170
|
+
for (const block of content) {
|
|
171
|
+
if (block.type === "text") {
|
|
172
|
+
const text = block.text.trim();
|
|
173
|
+
if (text) lines.push(text);
|
|
174
|
+
} else if (block.type === "tool_use") {
|
|
175
|
+
const tu = block;
|
|
176
|
+
lines.push(`\u2192 ${tu.name}${summarizeToolInput(tu.name, tu.input)}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
180
|
+
}
|
|
181
|
+
function formatUserToolResult(msg, opts) {
|
|
182
|
+
const content = msg.message?.content ?? [];
|
|
183
|
+
const lines = [];
|
|
184
|
+
for (const block of content) {
|
|
185
|
+
if (block.type === "tool_result") {
|
|
186
|
+
const tr = block;
|
|
187
|
+
const text = stringifyToolContent(tr.content);
|
|
188
|
+
const lineCount = text.split("\n").length;
|
|
189
|
+
const sizeBytes = text.length;
|
|
190
|
+
const flag = tr.is_error ? " ERROR" : "";
|
|
191
|
+
const summary = ` \u21B3${flag} ${lineCount} lines, ${formatBytes(sizeBytes)}`;
|
|
192
|
+
if (opts.verbose) {
|
|
193
|
+
lines.push(`${summary}
|
|
194
|
+
${truncate(text, 4e3)}`);
|
|
195
|
+
} else {
|
|
196
|
+
lines.push(summary);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
201
|
+
}
|
|
202
|
+
function formatResult(msg) {
|
|
203
|
+
const ok = msg.subtype === "success";
|
|
204
|
+
const tag = ok ? "DONE" : `FAILED (${msg.subtype ?? "unknown"})`;
|
|
205
|
+
const dur = msg.duration_ms ? ` ${(msg.duration_ms / 1e3).toFixed(1)}s` : "";
|
|
206
|
+
const turns = msg.num_turns ? ` ${msg.num_turns} turns` : "";
|
|
207
|
+
const cost = typeof msg.total_cost_usd === "number" ? ` $${msg.total_cost_usd.toFixed(4)}` : "";
|
|
208
|
+
return `
|
|
209
|
+
=== ${tag}${dur}${turns}${cost} ===`;
|
|
210
|
+
}
|
|
211
|
+
function summarizeToolInput(toolName, input = {}) {
|
|
212
|
+
if (toolName === "Bash" && typeof input.command === "string") {
|
|
213
|
+
const cmd = input.command.split("\n")[0];
|
|
214
|
+
return `: ${truncate(cmd, 120)}`;
|
|
215
|
+
}
|
|
216
|
+
if ((toolName === "Read" || toolName === "Edit" || toolName === "Write") && typeof input.file_path === "string") {
|
|
217
|
+
return ` ${input.file_path}`;
|
|
218
|
+
}
|
|
219
|
+
if ((toolName === "Glob" || toolName === "Grep") && typeof input.pattern === "string") {
|
|
220
|
+
return `: ${truncate(input.pattern, 80)}`;
|
|
221
|
+
}
|
|
222
|
+
return "";
|
|
223
|
+
}
|
|
224
|
+
function stringifyToolContent(content) {
|
|
225
|
+
if (typeof content === "string") return content;
|
|
226
|
+
if (Array.isArray(content)) {
|
|
227
|
+
return content.map((b) => {
|
|
228
|
+
if (b && typeof b === "object" && "text" in b && typeof b.text === "string") {
|
|
229
|
+
return b.text;
|
|
230
|
+
}
|
|
231
|
+
return JSON.stringify(b);
|
|
232
|
+
}).join("\n");
|
|
233
|
+
}
|
|
234
|
+
return JSON.stringify(content);
|
|
235
|
+
}
|
|
236
|
+
function truncate(s, max) {
|
|
237
|
+
if (s.length <= max) return s;
|
|
238
|
+
return `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
239
|
+
}
|
|
240
|
+
function formatBytes(bytes) {
|
|
241
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
242
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
243
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/agent.ts
|
|
247
|
+
var DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
|
|
248
|
+
async function runAgent(opts) {
|
|
249
|
+
const ndjsonDir = opts.ndjsonDir ?? path2.join(opts.cwd, ".kody2");
|
|
250
|
+
fs2.mkdirSync(ndjsonDir, { recursive: true });
|
|
251
|
+
const ndjsonPath = path2.join(ndjsonDir, "last-run.jsonl");
|
|
252
|
+
const fullLog = fs2.createWriteStream(ndjsonPath, { flags: "w" });
|
|
253
|
+
const env = {
|
|
254
|
+
...process.env,
|
|
255
|
+
SKIP_HOOKS: "1",
|
|
256
|
+
HUSKY: "0",
|
|
257
|
+
CI: process.env.CI ?? "1"
|
|
258
|
+
};
|
|
259
|
+
if (opts.litellmUrl) {
|
|
260
|
+
env.ANTHROPIC_BASE_URL = opts.litellmUrl;
|
|
261
|
+
env.ANTHROPIC_API_KEY = getAnthropicApiKeyOrDummy();
|
|
262
|
+
}
|
|
263
|
+
let finalText = "";
|
|
264
|
+
let outcome = "failed";
|
|
265
|
+
let errorMessage;
|
|
266
|
+
try {
|
|
267
|
+
const queryOptions = {
|
|
268
|
+
model: opts.model.model,
|
|
269
|
+
cwd: opts.cwd,
|
|
270
|
+
allowedTools: opts.allowedToolsOverride ?? DEFAULT_ALLOWED_TOOLS,
|
|
271
|
+
permissionMode: opts.permissionModeOverride ?? "acceptEdits",
|
|
272
|
+
env
|
|
273
|
+
};
|
|
274
|
+
if (opts.mcpServers && opts.mcpServers.length > 0) {
|
|
275
|
+
queryOptions.mcpServers = opts.mcpServers;
|
|
276
|
+
}
|
|
277
|
+
const result = query({
|
|
278
|
+
prompt: opts.prompt,
|
|
279
|
+
// biome-ignore lint/suspicious/noExplicitAny: SDK options type is narrow; mcpServers is runtime-passthrough.
|
|
280
|
+
options: queryOptions
|
|
281
|
+
});
|
|
282
|
+
for await (const msg of result) {
|
|
283
|
+
try {
|
|
284
|
+
fullLog.write(`${JSON.stringify(msg)}
|
|
285
|
+
`);
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
const line = renderEvent(msg, { verbose: opts.verbose, quiet: opts.quiet });
|
|
289
|
+
if (line) process.stdout.write(`${line}
|
|
290
|
+
`);
|
|
291
|
+
const m = msg;
|
|
292
|
+
if (m.type === "result") {
|
|
293
|
+
if (m.subtype === "success") {
|
|
294
|
+
outcome = "completed";
|
|
295
|
+
finalText = (typeof m.result === "string" ? m.result : "").trim();
|
|
296
|
+
} else {
|
|
297
|
+
outcome = "failed";
|
|
298
|
+
errorMessage = `result subtype: ${m.subtype ?? "unknown"}`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch (e) {
|
|
303
|
+
outcome = "failed";
|
|
304
|
+
errorMessage = e instanceof Error ? e.message : String(e);
|
|
305
|
+
} finally {
|
|
306
|
+
try {
|
|
307
|
+
fullLog.end();
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return { outcome, finalText, error: errorMessage, ndjsonPath };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/litellm.ts
|
|
315
|
+
import { execFileSync, spawn } from "child_process";
|
|
316
|
+
import * as fs3 from "fs";
|
|
317
|
+
import * as os from "os";
|
|
318
|
+
import * as path3 from "path";
|
|
319
|
+
async function checkLitellmHealth(url) {
|
|
320
|
+
try {
|
|
321
|
+
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
322
|
+
return response.ok;
|
|
323
|
+
} catch {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function generateLitellmConfigYaml(model) {
|
|
328
|
+
const apiKeyVar = providerApiKeyEnvVar(model.provider);
|
|
329
|
+
return [
|
|
330
|
+
"model_list:",
|
|
331
|
+
` - model_name: ${model.model}`,
|
|
332
|
+
` litellm_params:`,
|
|
333
|
+
` model: ${model.provider}/${model.model}`,
|
|
334
|
+
` api_key: os.environ/${apiKeyVar}`,
|
|
335
|
+
"",
|
|
336
|
+
"litellm_settings:",
|
|
337
|
+
" drop_params: true",
|
|
338
|
+
""
|
|
339
|
+
].join("\n");
|
|
340
|
+
}
|
|
341
|
+
async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL) {
|
|
342
|
+
if (!needsLitellmProxy(model)) return null;
|
|
343
|
+
if (await checkLitellmHealth(url)) {
|
|
344
|
+
return { url, kill: () => {
|
|
345
|
+
} };
|
|
346
|
+
}
|
|
347
|
+
let cmd = "litellm";
|
|
348
|
+
try {
|
|
349
|
+
execFileSync("which", ["litellm"], { timeout: 3e3, stdio: "pipe" });
|
|
350
|
+
} catch {
|
|
351
|
+
try {
|
|
352
|
+
execFileSync("python3", ["-c", "import litellm"], { timeout: 1e4, stdio: "pipe" });
|
|
353
|
+
cmd = "python3";
|
|
354
|
+
} catch {
|
|
355
|
+
throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const configPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.yaml`);
|
|
359
|
+
fs3.writeFileSync(configPath, generateLitellmConfigYaml(model));
|
|
360
|
+
const portMatch = url.match(/:(\d+)/);
|
|
361
|
+
const port = portMatch ? portMatch[1] : "4000";
|
|
362
|
+
const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
|
|
363
|
+
const dotenvVars = readDotenvApiKeys(projectDir);
|
|
364
|
+
const logPath = path3.join(os.tmpdir(), `kody2-litellm-${Date.now()}.log`);
|
|
365
|
+
const outFd = fs3.openSync(logPath, "w");
|
|
366
|
+
const child = spawn(cmd, args, {
|
|
367
|
+
stdio: ["ignore", outFd, outFd],
|
|
368
|
+
detached: true,
|
|
369
|
+
env: stripBlockingEnv({ ...process.env, ...dotenvVars })
|
|
370
|
+
});
|
|
371
|
+
fs3.closeSync(outFd);
|
|
372
|
+
for (let i = 0; i < 30; i++) {
|
|
373
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
374
|
+
if (await checkLitellmHealth(url)) {
|
|
375
|
+
return {
|
|
376
|
+
url,
|
|
377
|
+
kill: () => {
|
|
378
|
+
try {
|
|
379
|
+
child.kill();
|
|
380
|
+
} catch {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
let logTail = "";
|
|
387
|
+
try {
|
|
388
|
+
logTail = fs3.readFileSync(logPath, "utf-8").slice(-2e3);
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
child.kill();
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
throw new Error(`LiteLLM proxy failed to start within 60s. Log tail:
|
|
396
|
+
${logTail}`);
|
|
397
|
+
}
|
|
398
|
+
function readDotenvApiKeys(projectDir) {
|
|
399
|
+
const dotenvPath = path3.join(projectDir, ".env");
|
|
400
|
+
if (!fs3.existsSync(dotenvPath)) return {};
|
|
401
|
+
const result = {};
|
|
402
|
+
for (const rawLine of fs3.readFileSync(dotenvPath, "utf-8").split("\n")) {
|
|
403
|
+
const line = rawLine.trim();
|
|
404
|
+
if (!line || line.startsWith("#")) continue;
|
|
405
|
+
const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
|
|
406
|
+
if (!match) continue;
|
|
407
|
+
let value = match[2].trim();
|
|
408
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
409
|
+
value = value.slice(1, -1);
|
|
410
|
+
}
|
|
411
|
+
const commentIdx = value.indexOf(" #");
|
|
412
|
+
if (commentIdx !== -1) value = value.slice(0, commentIdx).trim();
|
|
413
|
+
if (value) result[match[1]] = value;
|
|
414
|
+
}
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
function stripBlockingEnv(env) {
|
|
418
|
+
const out = { ...env };
|
|
419
|
+
delete out.DATABASE_URL;
|
|
420
|
+
delete out.AI_BASE_URL;
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/profile.ts
|
|
425
|
+
import * as fs4 from "fs";
|
|
426
|
+
import * as path4 from "path";
|
|
98
427
|
var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
|
|
99
428
|
var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
|
|
100
429
|
var ProfileError = class extends Error {
|
|
@@ -107,12 +436,12 @@ var ProfileError = class extends Error {
|
|
|
107
436
|
profilePath;
|
|
108
437
|
};
|
|
109
438
|
function loadProfile(profilePath) {
|
|
110
|
-
if (!
|
|
439
|
+
if (!fs4.existsSync(profilePath)) {
|
|
111
440
|
throw new ProfileError(profilePath, "file not found");
|
|
112
441
|
}
|
|
113
442
|
let raw;
|
|
114
443
|
try {
|
|
115
|
-
raw = JSON.parse(
|
|
444
|
+
raw = JSON.parse(fs4.readFileSync(profilePath, "utf-8"));
|
|
116
445
|
} catch (err) {
|
|
117
446
|
throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
118
447
|
}
|
|
@@ -128,7 +457,7 @@ function loadProfile(profilePath) {
|
|
|
128
457
|
cliTools: parseCliTools(profilePath, r.cliTools),
|
|
129
458
|
scripts: parseScripts(profilePath, r.scripts),
|
|
130
459
|
outputContract: r.outputContract,
|
|
131
|
-
dir:
|
|
460
|
+
dir: path4.dirname(profilePath)
|
|
132
461
|
};
|
|
133
462
|
return profile;
|
|
134
463
|
}
|
|
@@ -270,637 +599,368 @@ function parseScriptList(p, key, raw) {
|
|
|
270
599
|
return out;
|
|
271
600
|
}
|
|
272
601
|
|
|
273
|
-
// src/
|
|
274
|
-
import { execFileSync } from "child_process";
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
602
|
+
// src/coverage.ts
|
|
603
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
604
|
+
function patternToRegex(pattern) {
|
|
605
|
+
let s = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
606
|
+
s = s.replace(/\*\*\//g, "\xA7S").replace(/\*\*/g, "\xA7A").replace(/\*/g, "[^/]*");
|
|
607
|
+
s = s.replace(/§S/g, "(?:.*/)?").replace(/§A/g, ".*");
|
|
608
|
+
return new RegExp(`^${s}$`);
|
|
278
609
|
}
|
|
279
|
-
function
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
};
|
|
610
|
+
function renderSiblingPath(file, requireSibling) {
|
|
611
|
+
const lastSlash = file.lastIndexOf("/");
|
|
612
|
+
const dir = lastSlash === -1 ? "" : file.slice(0, lastSlash + 1);
|
|
613
|
+
const base = lastSlash === -1 ? file : file.slice(lastSlash + 1);
|
|
614
|
+
const name = base.replace(/\.[^.]+$/, "");
|
|
615
|
+
const ext = base.match(/\.[^.]+$/)?.[0] ?? "";
|
|
616
|
+
const sibling = requireSibling.replace(/\{name\}/g, name).replace(/\{ext\}/g, ext);
|
|
617
|
+
return dir + sibling;
|
|
343
618
|
}
|
|
344
|
-
function
|
|
619
|
+
function safeGit(args, cwd) {
|
|
345
620
|
try {
|
|
346
|
-
return
|
|
347
|
-
} catch
|
|
348
|
-
process.stderr.write(`[kody2] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
349
|
-
`);
|
|
621
|
+
return execFileSync2("git", args, { encoding: "utf-8", cwd, env: { ...process.env, HUSKY: "0" } }).trim();
|
|
622
|
+
} catch {
|
|
350
623
|
return "";
|
|
351
624
|
}
|
|
352
625
|
}
|
|
353
|
-
function
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
}
|
|
626
|
+
function getAddedFiles(baseBranch, cwd) {
|
|
627
|
+
const committed = safeGit(["diff", "--name-only", "--diff-filter=A", `origin/${baseBranch}...HEAD`], cwd);
|
|
628
|
+
const untracked = safeGit(["ls-files", "--others", "--exclude-standard"], cwd);
|
|
629
|
+
const set = /* @__PURE__ */ new Set();
|
|
630
|
+
for (const f of committed.split("\n")) if (f) set.add(f);
|
|
631
|
+
for (const f of untracked.split("\n")) if (f) set.add(f);
|
|
632
|
+
return [...set];
|
|
370
633
|
}
|
|
371
|
-
function
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
634
|
+
function checkCoverage(addedFiles, requirements) {
|
|
635
|
+
if (requirements.length === 0) return [];
|
|
636
|
+
const addedSet = new Set(addedFiles);
|
|
637
|
+
const misses = [];
|
|
638
|
+
for (const file of addedFiles) {
|
|
639
|
+
if (/\.(test|spec)\./.test(file)) continue;
|
|
640
|
+
for (const req of requirements) {
|
|
641
|
+
const re = patternToRegex(req.pattern);
|
|
642
|
+
if (!re.test(file)) continue;
|
|
643
|
+
const expected = renderSiblingPath(file, req.requireSibling);
|
|
644
|
+
if (!addedSet.has(expected)) {
|
|
645
|
+
misses.push({ file, expectedTest: expected });
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
383
649
|
}
|
|
650
|
+
return misses;
|
|
384
651
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
}
|
|
652
|
+
function formatMissesForFeedback(misses) {
|
|
653
|
+
if (misses.length === 0) return "";
|
|
654
|
+
const lines = ["The following files were added without a sibling test file:"];
|
|
655
|
+
for (const m of misses) lines.push(`- \`${m.file}\` \u2192 expected \`${m.expectedTest}\``);
|
|
656
|
+
lines.push("");
|
|
657
|
+
lines.push(
|
|
658
|
+
"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."
|
|
659
|
+
);
|
|
660
|
+
return lines.join("\n");
|
|
401
661
|
}
|
|
402
662
|
|
|
403
|
-
// src/
|
|
404
|
-
import
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
}
|
|
663
|
+
// src/prompt.ts
|
|
664
|
+
import * as fs5 from "fs";
|
|
665
|
+
import * as path5 from "path";
|
|
666
|
+
var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
|
|
667
|
+
var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
668
|
+
function loadProjectConventions(projectDir) {
|
|
669
|
+
const out = [];
|
|
670
|
+
for (const rel of CONVENTION_FILES) {
|
|
671
|
+
const abs = path5.join(projectDir, rel);
|
|
672
|
+
if (!fs5.existsSync(abs)) continue;
|
|
673
|
+
let content;
|
|
462
674
|
try {
|
|
463
|
-
|
|
675
|
+
content = fs5.readFileSync(abs, "utf-8");
|
|
464
676
|
} catch {
|
|
677
|
+
continue;
|
|
465
678
|
}
|
|
466
|
-
|
|
679
|
+
const truncated = content.length > CONVENTIONS_PER_FILE_MAX_BYTES;
|
|
680
|
+
if (truncated) content = `${content.slice(0, CONVENTIONS_PER_FILE_MAX_BYTES)}
|
|
681
|
+
|
|
682
|
+
\u2026 (truncated)`;
|
|
683
|
+
out.push({ path: rel, content, truncated });
|
|
467
684
|
}
|
|
685
|
+
return out;
|
|
468
686
|
}
|
|
469
|
-
function
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
return {
|
|
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 {
|
|
687
|
+
function parseAgentResult(finalText) {
|
|
688
|
+
const text = (finalText || "").trim();
|
|
689
|
+
if (!text) return { done: false, commitMessage: "", prSummary: "", failureReason: "agent produced no final message" };
|
|
690
|
+
const failedMatch = text.match(/(?:^|\n)\s*FAILED\s*:\s*(.+?)\s*$/s);
|
|
691
|
+
if (failedMatch) {
|
|
692
|
+
return { done: false, commitMessage: "", prSummary: "", failureReason: failedMatch[1].trim() };
|
|
490
693
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
git(["checkout", branchName], cwd);
|
|
494
|
-
return { branch: branchName, created: false };
|
|
495
|
-
} catch {
|
|
694
|
+
if (!/(^|\n)\s*DONE\b/i.test(text)) {
|
|
695
|
+
return { done: false, commitMessage: "", prSummary: "", failureReason: "no DONE or FAILED marker in agent output" };
|
|
496
696
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
697
|
+
const commitMatch = text.match(/^[ \t]*COMMIT_MSG\s*:\s*(.+)$/im);
|
|
698
|
+
const commitMessage = commitMatch ? commitMatch[1].trim() : "";
|
|
699
|
+
const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
|
|
700
|
+
let prSummary = "";
|
|
701
|
+
if (summaryStart !== -1) {
|
|
702
|
+
const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
|
|
703
|
+
prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
|
|
501
704
|
}
|
|
502
|
-
return {
|
|
705
|
+
return { done: true, commitMessage, prSummary, failureReason: "" };
|
|
503
706
|
}
|
|
504
707
|
|
|
505
|
-
// src/
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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 {
|
|
708
|
+
// src/scripts/checkCoverageWithRetry.ts
|
|
709
|
+
var checkCoverageWithRetry = async (ctx) => {
|
|
710
|
+
const reqs = ctx.data.coverageRules ?? [];
|
|
711
|
+
if (reqs.length === 0) {
|
|
712
|
+
ctx.data.coverageMisses = [];
|
|
523
713
|
return;
|
|
524
714
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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 {
|
|
715
|
+
if (!ctx.data.agentDone) {
|
|
716
|
+
ctx.data.coverageMisses = [];
|
|
717
|
+
return;
|
|
550
718
|
}
|
|
551
|
-
|
|
719
|
+
const misses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
|
|
720
|
+
if (misses.length === 0) {
|
|
721
|
+
ctx.data.coverageMisses = [];
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const invoker = ctx.data.__invokeAgent;
|
|
725
|
+
const basePrompt = ctx.data.prompt;
|
|
726
|
+
if (!invoker || !basePrompt) {
|
|
727
|
+
ctx.data.coverageMisses = misses;
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
process.stderr.write(`[kody2] coverage check found ${misses.length} missing test(s); retrying agent once
|
|
731
|
+
`);
|
|
732
|
+
const retryPrompt = `${basePrompt}
|
|
552
733
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
734
|
+
# Coverage failure (retry)
|
|
735
|
+
${formatMissesForFeedback(misses)}`;
|
|
736
|
+
const retry = await invoker(retryPrompt);
|
|
737
|
+
const retryParsed = parseAgentResult(retry.finalText);
|
|
738
|
+
if (retry.outcome === "completed" && retryParsed.done) {
|
|
739
|
+
ctx.data.agentDone = true;
|
|
740
|
+
ctx.data.commitMessage = retryParsed.commitMessage || ctx.data.commitMessage;
|
|
741
|
+
ctx.data.prSummary = retryParsed.prSummary || ctx.data.prSummary;
|
|
742
|
+
}
|
|
743
|
+
const finalMisses = checkCoverage(getAddedFiles(ctx.config.git.defaultBranch, ctx.cwd), reqs);
|
|
744
|
+
ctx.data.coverageMisses = finalMisses;
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
// src/scripts/commitAndPush.ts
|
|
748
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
749
|
+
|
|
750
|
+
// src/commit.ts
|
|
751
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
752
|
+
import * as fs6 from "fs";
|
|
753
|
+
import * as path6 from "path";
|
|
754
|
+
var FORBIDDEN_PATH_PREFIXES = [
|
|
755
|
+
".kody/",
|
|
756
|
+
".kody-engine/",
|
|
757
|
+
".kody2/",
|
|
758
|
+
".kody-lean/",
|
|
759
|
+
// back-compat: stale runtime dir from kody-lean v0.5.x
|
|
760
|
+
"node_modules/",
|
|
761
|
+
"dist/",
|
|
762
|
+
"build/"
|
|
763
|
+
];
|
|
764
|
+
var FORBIDDEN_PATH_EXACT = /* @__PURE__ */ new Set([".env"]);
|
|
765
|
+
var FORBIDDEN_PATH_SUFFIXES = [".log"];
|
|
766
|
+
var CONVENTIONAL_PREFIXES = [
|
|
767
|
+
"feat:",
|
|
768
|
+
"fix:",
|
|
769
|
+
"chore:",
|
|
770
|
+
"docs:",
|
|
771
|
+
"refactor:",
|
|
772
|
+
"test:",
|
|
773
|
+
"perf:",
|
|
774
|
+
"ci:",
|
|
775
|
+
"style:",
|
|
776
|
+
"build:",
|
|
777
|
+
"revert:"
|
|
778
|
+
];
|
|
779
|
+
function git(args, cwd) {
|
|
560
780
|
try {
|
|
561
|
-
|
|
562
|
-
|
|
781
|
+
return execFileSync3("git", args, {
|
|
782
|
+
encoding: "utf-8",
|
|
783
|
+
timeout: 12e4,
|
|
784
|
+
cwd,
|
|
785
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
786
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
787
|
+
}).trim();
|
|
563
788
|
} catch (err) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
throw err;
|
|
789
|
+
const e = err;
|
|
790
|
+
const stderr = e.stderr?.toString().trim() ?? "";
|
|
791
|
+
const stdout = e.stdout?.toString().trim() ?? "";
|
|
792
|
+
const status = e.status ?? "?";
|
|
793
|
+
const detail = stderr || stdout || e.message || "(no output)";
|
|
794
|
+
throw new Error(`git ${args.join(" ")} (exit ${status}):
|
|
795
|
+
${detail}`);
|
|
572
796
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
tryPost(issueNumber, startMsg, ctx.cwd);
|
|
576
|
-
};
|
|
577
|
-
function tryPost(issueNumber, body, cwd) {
|
|
797
|
+
}
|
|
798
|
+
function tryGit(args, cwd) {
|
|
578
799
|
try {
|
|
579
|
-
|
|
800
|
+
git(args, cwd);
|
|
801
|
+
return true;
|
|
580
802
|
} catch {
|
|
803
|
+
return false;
|
|
581
804
|
}
|
|
582
805
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
ctx.output.exitCode = 1;
|
|
590
|
-
ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
|
|
591
|
-
ctx.skipAgent = true;
|
|
592
|
-
return;
|
|
806
|
+
function abortUnfinishedGitOps(cwd) {
|
|
807
|
+
const aborted = [];
|
|
808
|
+
const gitDir = path6.join(cwd ?? process.cwd(), ".git");
|
|
809
|
+
if (!fs6.existsSync(gitDir)) return aborted;
|
|
810
|
+
if (fs6.existsSync(path6.join(gitDir, "MERGE_HEAD"))) {
|
|
811
|
+
if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
|
|
593
812
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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;
|
|
813
|
+
if (fs6.existsSync(path6.join(gitDir, "CHERRY_PICK_HEAD"))) {
|
|
814
|
+
if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
|
|
815
|
+
}
|
|
816
|
+
if (fs6.existsSync(path6.join(gitDir, "REVERT_HEAD"))) {
|
|
817
|
+
if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
|
|
818
|
+
}
|
|
819
|
+
if (fs6.existsSync(path6.join(gitDir, "rebase-merge")) || fs6.existsSync(path6.join(gitDir, "rebase-apply"))) {
|
|
820
|
+
if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
|
|
606
821
|
}
|
|
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
822
|
try {
|
|
619
|
-
|
|
823
|
+
const unmerged = git(["diff", "--name-only", "--diff-filter=U"], cwd);
|
|
824
|
+
if (unmerged) {
|
|
825
|
+
tryGit(["reset", "--mixed", "HEAD"], cwd);
|
|
826
|
+
aborted.push("unmerged-paths-reset");
|
|
827
|
+
}
|
|
620
828
|
} catch {
|
|
621
829
|
}
|
|
830
|
+
return aborted;
|
|
622
831
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
832
|
+
function isForbiddenPath(p) {
|
|
833
|
+
if (FORBIDDEN_PATH_EXACT.has(p)) return true;
|
|
834
|
+
for (const pre of FORBIDDEN_PATH_PREFIXES) if (p.startsWith(pre)) return true;
|
|
835
|
+
for (const suf of FORBIDDEN_PATH_SUFFIXES) if (p.endsWith(suf)) return true;
|
|
836
|
+
return false;
|
|
629
837
|
}
|
|
630
|
-
function
|
|
631
|
-
const
|
|
632
|
-
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
633
|
-
return execFileSync4("gh", args, {
|
|
838
|
+
function listChangedFiles(cwd) {
|
|
839
|
+
const raw = execFileSync3("git", ["status", "--porcelain=v1", "-z"], {
|
|
634
840
|
encoding: "utf-8",
|
|
635
|
-
timeout: GH_TIMEOUT_MS,
|
|
636
841
|
cwd,
|
|
637
|
-
env,
|
|
638
|
-
stdio: ["
|
|
639
|
-
})
|
|
842
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
843
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
844
|
+
});
|
|
845
|
+
if (!raw) return [];
|
|
846
|
+
const entries = raw.split("\0").filter((e) => e.length > 0);
|
|
847
|
+
return entries.map((e) => e.slice(3)).filter(Boolean);
|
|
640
848
|
}
|
|
641
|
-
function
|
|
642
|
-
|
|
849
|
+
function normalizeCommitMessage(raw) {
|
|
850
|
+
const trimmed = raw.trim().replace(/^['"]|['"]$/g, "").trim();
|
|
851
|
+
if (!trimmed) return "chore: kody2 update";
|
|
852
|
+
const firstLine2 = trimmed.split("\n")[0];
|
|
853
|
+
for (const prefix of CONVENTIONAL_PREFIXES) {
|
|
854
|
+
if (firstLine2.toLowerCase().startsWith(prefix)) return trimmed;
|
|
855
|
+
}
|
|
856
|
+
return `chore: ${trimmed}`;
|
|
857
|
+
}
|
|
858
|
+
function commitAndPush(branch, agentMessage, cwd) {
|
|
859
|
+
const allChanged = listChangedFiles(cwd);
|
|
860
|
+
const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
|
|
861
|
+
const mergeHeadExists = fs6.existsSync(path6.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
|
|
862
|
+
if (allowedFiles.length === 0 && !mergeHeadExists) {
|
|
863
|
+
return { committed: false, pushed: false, sha: "", message: "" };
|
|
864
|
+
}
|
|
865
|
+
for (const f of allowedFiles) {
|
|
866
|
+
try {
|
|
867
|
+
git(["add", "--", f], cwd);
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const message = normalizeCommitMessage(agentMessage);
|
|
643
872
|
try {
|
|
644
|
-
|
|
645
|
-
|
|
873
|
+
git(["commit", "--no-gpg-sign", "-m", message], cwd);
|
|
874
|
+
} catch (err) {
|
|
875
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
876
|
+
if (/nothing to commit/i.test(msg)) {
|
|
877
|
+
return { committed: false, pushed: false, sha: "", message };
|
|
878
|
+
}
|
|
879
|
+
throw err;
|
|
880
|
+
}
|
|
881
|
+
const sha = git(["rev-parse", "HEAD"], cwd).slice(0, 7);
|
|
882
|
+
try {
|
|
883
|
+
git(["push", "-u", "origin", branch], cwd);
|
|
646
884
|
} catch {
|
|
647
|
-
|
|
885
|
+
git(["push", "--force-with-lease", "-u", "origin", branch], cwd);
|
|
648
886
|
}
|
|
649
|
-
|
|
887
|
+
return { committed: true, pushed: true, sha, message };
|
|
888
|
+
}
|
|
889
|
+
function hasCommitsAhead(branch, defaultBranch, cwd) {
|
|
650
890
|
try {
|
|
651
|
-
const out =
|
|
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);
|
|
891
|
+
const out = git(["rev-list", "--count", `origin/${defaultBranch}..${branch}`], cwd);
|
|
892
|
+
return parseInt(out, 10) > 0;
|
|
686
893
|
} catch {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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;
|
|
894
|
+
try {
|
|
895
|
+
const out = git(["rev-list", "--count", `${defaultBranch}..${branch}`], cwd);
|
|
896
|
+
return parseInt(out, 10) > 0;
|
|
897
|
+
} catch {
|
|
898
|
+
return false;
|
|
717
899
|
}
|
|
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
900
|
}
|
|
743
901
|
}
|
|
744
902
|
|
|
745
|
-
// src/scripts/
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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;
|
|
903
|
+
// src/scripts/commitAndPush.ts
|
|
904
|
+
var commitAndPush2 = async (ctx) => {
|
|
905
|
+
const branch = ctx.data.branch;
|
|
906
|
+
if (!branch) {
|
|
907
|
+
ctx.data.commitResult = { committed: false, pushed: false };
|
|
784
908
|
return;
|
|
785
909
|
}
|
|
786
|
-
ctx.
|
|
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) {
|
|
910
|
+
if (ctx.args.mode === "resolve") {
|
|
808
911
|
try {
|
|
809
|
-
|
|
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;
|
|
912
|
+
execFileSync4("git", ["add", "-A"], { cwd: ctx.cwd, env: { ...process.env, HUSKY: "0" }, stdio: "pipe" });
|
|
819
913
|
} catch {
|
|
820
914
|
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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;
|
|
915
|
+
} else {
|
|
916
|
+
const aborted = abortUnfinishedGitOps(ctx.cwd);
|
|
917
|
+
if (aborted.length > 0) {
|
|
918
|
+
process.stderr.write(`[kody2] cleaned up unfinished git ops: ${aborted.join(", ")}
|
|
919
|
+
`);
|
|
846
920
|
}
|
|
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
921
|
}
|
|
860
|
-
|
|
861
|
-
|
|
922
|
+
const fallbackMsg = defaultCommitMessage(ctx.args.mode, ctx.data);
|
|
923
|
+
const message = ctx.data.commitMessage || fallbackMsg;
|
|
924
|
+
try {
|
|
925
|
+
const result = commitAndPush(branch, message, ctx.cwd);
|
|
926
|
+
ctx.data.commitResult = result;
|
|
927
|
+
ctx.data.changedFiles = listChangedFiles(ctx.cwd).filter((f) => !isForbiddenPath(f));
|
|
928
|
+
} catch (err) {
|
|
929
|
+
ctx.data.commitCrash = err instanceof Error ? err.message : String(err);
|
|
930
|
+
ctx.data.commitResult = { committed: false, pushed: false };
|
|
862
931
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
932
|
+
ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
|
|
933
|
+
};
|
|
934
|
+
function defaultCommitMessage(mode, data) {
|
|
935
|
+
switch (mode) {
|
|
936
|
+
case "run":
|
|
937
|
+
return `chore: kody2 changes for #${data.commentTargetNumber}`;
|
|
938
|
+
case "fix":
|
|
939
|
+
return `chore(fix): kody2 fix for PR #${data.commentTargetNumber}`;
|
|
940
|
+
case "fix-ci":
|
|
941
|
+
return `fix(ci): kody2 fix-ci for PR #${data.commentTargetNumber}`;
|
|
942
|
+
case "resolve":
|
|
943
|
+
return `fix: resolve merge conflicts with ${data.baseBranch}`;
|
|
944
|
+
default:
|
|
945
|
+
return `chore: kody2 changes`;
|
|
870
946
|
}
|
|
871
|
-
return { done: true, commitMessage, prSummary, failureReason: "" };
|
|
872
947
|
}
|
|
873
948
|
|
|
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
949
|
// src/scripts/composePrompt.ts
|
|
890
|
-
import * as
|
|
891
|
-
import * as
|
|
950
|
+
import * as fs7 from "fs";
|
|
951
|
+
import * as path7 from "path";
|
|
892
952
|
var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
893
953
|
var composePrompt = async (ctx, profile) => {
|
|
894
954
|
const explicit = ctx.data.promptTemplate;
|
|
895
955
|
const mode = ctx.args.mode;
|
|
896
956
|
const candidates = [
|
|
897
|
-
explicit ?
|
|
898
|
-
mode ?
|
|
899
|
-
|
|
957
|
+
explicit ? path7.join(profile.dir, explicit) : null,
|
|
958
|
+
mode ? path7.join(profile.dir, "prompts", `${mode}.md`) : null,
|
|
959
|
+
path7.join(profile.dir, "prompt.md")
|
|
900
960
|
].filter(Boolean);
|
|
901
961
|
let templatePath = "";
|
|
902
962
|
for (const c of candidates) {
|
|
903
|
-
if (
|
|
963
|
+
if (fs7.existsSync(c)) {
|
|
904
964
|
templatePath = c;
|
|
905
965
|
break;
|
|
906
966
|
}
|
|
@@ -908,18 +968,20 @@ var composePrompt = async (ctx, profile) => {
|
|
|
908
968
|
if (!templatePath) {
|
|
909
969
|
throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
|
|
910
970
|
}
|
|
911
|
-
const template =
|
|
971
|
+
const template = fs7.readFileSync(templatePath, "utf-8");
|
|
912
972
|
const tokens = {
|
|
913
973
|
...stringifyAll(ctx.args, "args."),
|
|
914
974
|
...stringifyAll(ctx.data, ""),
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
975
|
+
conventionsBlock: formatConventions(ctx.data.conventions),
|
|
976
|
+
coverageBlock: formatCoverageBlock(
|
|
977
|
+
ctx.data.coverageRules
|
|
978
|
+
),
|
|
979
|
+
toolsUsage: formatToolsUsage(profile),
|
|
980
|
+
systemPromptAppend: profile.claudeCode.systemPromptAppend ?? "",
|
|
981
|
+
repoOwner: ctx.config.github.owner,
|
|
982
|
+
repoName: ctx.config.github.repo,
|
|
983
|
+
defaultBranch: ctx.config.git.defaultBranch,
|
|
984
|
+
branch: ctx.data.branch ?? ""
|
|
923
985
|
};
|
|
924
986
|
ctx.data.prompt = template.replace(MUSTACHE, (_, key) => tokens[key] ?? "");
|
|
925
987
|
};
|
|
@@ -944,10 +1006,7 @@ function stringifyAll(source, prefix) {
|
|
|
944
1006
|
}
|
|
945
1007
|
function formatConventions(conventions) {
|
|
946
1008
|
if (!conventions || conventions.length === 0) return "";
|
|
947
|
-
const lines = [
|
|
948
|
-
"# Project conventions (AUTHORITATIVE \u2014 follow these over patterns you infer from code)",
|
|
949
|
-
""
|
|
950
|
-
];
|
|
1009
|
+
const lines = ["# Project conventions (AUTHORITATIVE \u2014 follow these over patterns you infer from code)", ""];
|
|
951
1010
|
for (const c of conventions) {
|
|
952
1011
|
lines.push(`## ${c.path}${c.truncated ? " (truncated)" : ""}`);
|
|
953
1012
|
lines.push("");
|
|
@@ -973,452 +1032,188 @@ function formatCoverageBlock(reqs) {
|
|
|
973
1032
|
function formatToolsUsage(profile) {
|
|
974
1033
|
const entries = (profile.cliTools ?? []).filter((t) => t.usage.trim().length > 0);
|
|
975
1034
|
if (entries.length === 0) return "";
|
|
976
|
-
const lines = [
|
|
977
|
-
"# Available CLI tools",
|
|
978
|
-
""
|
|
979
|
-
];
|
|
1035
|
+
const lines = ["# Available CLI tools", ""];
|
|
980
1036
|
for (const t of entries) {
|
|
981
1037
|
lines.push(`## \`${t.name}\``);
|
|
982
1038
|
lines.push(t.usage);
|
|
983
1039
|
if (t.allowedUses.length > 0) {
|
|
984
|
-
lines.push(`Allowed sub-commands: ${t.allowedUses.map((u) =>
|
|
1040
|
+
lines.push(`Allowed sub-commands: ${t.allowedUses.map((u) => `\`${u}\``).join(", ")}`);
|
|
985
1041
|
}
|
|
986
1042
|
lines.push("");
|
|
987
1043
|
}
|
|
988
1044
|
return lines.join("\n");
|
|
989
1045
|
}
|
|
990
1046
|
|
|
991
|
-
// src/
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
+
// src/issue.ts
|
|
1048
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
1049
|
+
var API_TIMEOUT_MS = 3e4;
|
|
1050
|
+
function ghToken() {
|
|
1051
|
+
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
1047
1052
|
}
|
|
1048
|
-
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1053
|
+
function gh(args, options) {
|
|
1054
|
+
const token = ghToken();
|
|
1055
|
+
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
1056
|
+
return execFileSync5("gh", args, {
|
|
1057
|
+
encoding: "utf-8",
|
|
1058
|
+
timeout: API_TIMEOUT_MS,
|
|
1059
|
+
cwd: options?.cwd,
|
|
1060
|
+
env,
|
|
1061
|
+
input: options?.input,
|
|
1062
|
+
stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
|
|
1063
|
+
}).trim();
|
|
1064
|
+
}
|
|
1065
|
+
function getIssue(issueNumber, cwd) {
|
|
1066
|
+
const output = gh(["issue", "view", String(issueNumber), "--json", "number,title,body,comments"], { cwd });
|
|
1067
|
+
const parsed = JSON.parse(output);
|
|
1068
|
+
if (typeof parsed?.title !== "string") {
|
|
1069
|
+
throw new Error(`Issue #${issueNumber}: unexpected response shape`);
|
|
1059
1070
|
}
|
|
1060
|
-
return {
|
|
1071
|
+
return {
|
|
1072
|
+
number: parsed.number ?? issueNumber,
|
|
1073
|
+
title: parsed.title,
|
|
1074
|
+
body: parsed.body ?? "",
|
|
1075
|
+
comments: (parsed.comments ?? []).map((c) => ({
|
|
1076
|
+
body: c.body ?? "",
|
|
1077
|
+
author: c.author?.login ?? "unknown",
|
|
1078
|
+
createdAt: c.createdAt ?? ""
|
|
1079
|
+
}))
|
|
1080
|
+
};
|
|
1061
1081
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1082
|
+
function postIssueComment(issueNumber, body, cwd) {
|
|
1083
|
+
try {
|
|
1084
|
+
gh(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: body, cwd });
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
process.stderr.write(
|
|
1087
|
+
`[kody2] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
1088
|
+
`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1065
1091
|
}
|
|
1066
|
-
function
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1092
|
+
function truncate2(s, maxBytes) {
|
|
1093
|
+
if (s.length <= maxBytes) return s;
|
|
1094
|
+
return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
|
|
1095
|
+
}
|
|
1096
|
+
function getPr(prNumber, cwd) {
|
|
1097
|
+
const output = gh(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
|
|
1098
|
+
cwd
|
|
1099
|
+
});
|
|
1100
|
+
const parsed = JSON.parse(output);
|
|
1101
|
+
if (typeof parsed?.title !== "string") {
|
|
1102
|
+
throw new Error(`PR #${prNumber}: unexpected response shape`);
|
|
1074
1103
|
}
|
|
1075
|
-
return
|
|
1104
|
+
return {
|
|
1105
|
+
number: parsed.number ?? prNumber,
|
|
1106
|
+
title: parsed.title,
|
|
1107
|
+
body: parsed.body ?? "",
|
|
1108
|
+
headRefName: String(parsed.headRefName ?? ""),
|
|
1109
|
+
baseRefName: String(parsed.baseRefName ?? ""),
|
|
1110
|
+
state: String(parsed.state ?? "")
|
|
1111
|
+
};
|
|
1076
1112
|
}
|
|
1077
|
-
|
|
1078
|
-
// src/scripts/verify.ts
|
|
1079
|
-
var verify = async (ctx) => {
|
|
1113
|
+
function getPrDiff(prNumber, cwd) {
|
|
1080
1114
|
try {
|
|
1081
|
-
|
|
1082
|
-
ctx.data.verifyOk = result.ok;
|
|
1083
|
-
ctx.data.verifyReason = result.ok ? "" : summarizeFailure(result);
|
|
1115
|
+
return gh(["pr", "diff", String(prNumber)], { cwd });
|
|
1084
1116
|
} catch (err) {
|
|
1085
|
-
|
|
1086
|
-
|
|
1117
|
+
process.stderr.write(
|
|
1118
|
+
`[kody2] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
1119
|
+
`
|
|
1120
|
+
);
|
|
1121
|
+
return "";
|
|
1087
1122
|
}
|
|
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
1123
|
}
|
|
1098
|
-
function
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1124
|
+
function getPrReviews(prNumber, cwd) {
|
|
1125
|
+
try {
|
|
1126
|
+
const output = gh(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
|
|
1127
|
+
const parsed = JSON.parse(output);
|
|
1128
|
+
if (!Array.isArray(parsed?.reviews)) return [];
|
|
1129
|
+
return parsed.reviews.map(
|
|
1130
|
+
(r) => ({
|
|
1131
|
+
body: r.body ?? "",
|
|
1132
|
+
state: r.state ?? "",
|
|
1133
|
+
author: r.author?.login ?? "unknown",
|
|
1134
|
+
submittedAt: r.submittedAt ?? ""
|
|
1135
|
+
})
|
|
1136
|
+
);
|
|
1137
|
+
} catch {
|
|
1138
|
+
return [];
|
|
1139
|
+
}
|
|
1106
1140
|
}
|
|
1107
|
-
function
|
|
1141
|
+
function getPrComments(prNumber, cwd) {
|
|
1108
1142
|
try {
|
|
1109
|
-
|
|
1143
|
+
const output = gh(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
|
|
1144
|
+
const parsed = JSON.parse(output);
|
|
1145
|
+
if (!Array.isArray(parsed?.comments)) return [];
|
|
1146
|
+
return parsed.comments.map((c) => ({
|
|
1147
|
+
body: c.body ?? "",
|
|
1148
|
+
author: c.author?.login ?? "unknown",
|
|
1149
|
+
createdAt: c.createdAt ?? ""
|
|
1150
|
+
})).filter((c) => c.body.trim().length > 0);
|
|
1110
1151
|
} catch {
|
|
1111
|
-
return
|
|
1152
|
+
return [];
|
|
1112
1153
|
}
|
|
1113
1154
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1155
|
+
var KODY_COMMENT_PREFIXES = ["\u2699\uFE0F kody2", "\u2705 kody2", "\u26A0\uFE0F kody2", "\u2139\uFE0F kody2", "\u2192 kody2"];
|
|
1156
|
+
function getPrLatestReviewBody(prNumber, cwd) {
|
|
1157
|
+
const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
|
|
1158
|
+
const comments = getPrComments(prNumber, cwd).filter((c) => !KODY_COMMENT_PREFIXES.some((p) => c.body.startsWith(p))).map((c) => ({ body: c.body, at: c.createdAt }));
|
|
1159
|
+
const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
|
|
1160
|
+
if (all.length > 0) return all[0].body;
|
|
1161
|
+
const pr = getPr(prNumber, cwd);
|
|
1162
|
+
return pr.body;
|
|
1121
1163
|
}
|
|
1122
|
-
function
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
-
}
|
|
1164
|
+
function postPrReviewComment(prNumber, body, cwd) {
|
|
1165
|
+
try {
|
|
1166
|
+
gh(["pr", "comment", String(prNumber), "--body-file", "-"], { input: body, cwd });
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
process.stderr.write(
|
|
1169
|
+
`[kody2] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
1170
|
+
`
|
|
1171
|
+
);
|
|
1137
1172
|
}
|
|
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
1173
|
}
|
|
1148
1174
|
|
|
1149
|
-
// src/
|
|
1150
|
-
var
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1175
|
+
// src/pr.ts
|
|
1176
|
+
var TITLE_MAX = 72;
|
|
1177
|
+
function stripTitlePrefixes(raw) {
|
|
1178
|
+
let s = raw.trim();
|
|
1179
|
+
while (true) {
|
|
1180
|
+
const next = s.replace(/^(\[WIP\]\s*)?#\d+:\s*/, "");
|
|
1181
|
+
if (next === s) break;
|
|
1182
|
+
s = next;
|
|
1155
1183
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1184
|
+
return s;
|
|
1185
|
+
}
|
|
1186
|
+
function buildPrTitle(issueNumber, issueTitle, draft) {
|
|
1187
|
+
const prefix = draft ? "[WIP] " : "";
|
|
1188
|
+
const clean = stripTitlePrefixes(issueTitle);
|
|
1189
|
+
const base = `${prefix}#${issueNumber}: ${clean}`;
|
|
1190
|
+
if (base.length <= TITLE_MAX) return base;
|
|
1191
|
+
return `${base.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
1192
|
+
}
|
|
1193
|
+
function buildPrBody(opts) {
|
|
1194
|
+
const lines = [];
|
|
1195
|
+
if (opts.draft && opts.failureReason) {
|
|
1196
|
+
const headline = firstLine(opts.failureReason);
|
|
1197
|
+
lines.push(`> \u26A0\uFE0F Draft: ${headline}`);
|
|
1198
|
+
lines.push(`> The failures below may be **pre-existing in the repo** \u2014 verify before treating as PR-blocking.`);
|
|
1199
|
+
lines.push("");
|
|
1159
1200
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1201
|
+
lines.push("## Summary");
|
|
1202
|
+
lines.push("");
|
|
1203
|
+
if (opts.agentSummary?.trim()) {
|
|
1204
|
+
lines.push(opts.agentSummary.trim());
|
|
1205
|
+
} else {
|
|
1206
|
+
lines.push(`Implementation of issue #${opts.issueNumber} \u2014 ${opts.issueTitle}`);
|
|
1207
|
+
lines.push("");
|
|
1208
|
+
lines.push("_(agent did not supply PR_SUMMARY)_");
|
|
1164
1209
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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("");
|
|
1210
|
+
lines.push("");
|
|
1211
|
+
if (opts.changedFiles.length > 0) {
|
|
1212
|
+
lines.push("## Changes");
|
|
1213
|
+
lines.push("");
|
|
1214
|
+
for (const f of opts.changedFiles.slice(0, 50)) lines.push(`- \`${f}\``);
|
|
1215
|
+
if (opts.changedFiles.length > 50) lines.push(`- \u2026 and ${opts.changedFiles.length - 50} more`);
|
|
1216
|
+
lines.push("");
|
|
1422
1217
|
}
|
|
1423
1218
|
lines.push(`Closes #${opts.issueNumber}`);
|
|
1424
1219
|
lines.push("");
|
|
@@ -1427,7 +1222,7 @@ function buildPrBody(opts) {
|
|
|
1427
1222
|
lines.push("<summary>Verify output (click to expand)</summary>");
|
|
1428
1223
|
lines.push("");
|
|
1429
1224
|
lines.push("```");
|
|
1430
|
-
lines.push(
|
|
1225
|
+
lines.push(truncate2(opts.failureReason, 6e3));
|
|
1431
1226
|
lines.push("```");
|
|
1432
1227
|
lines.push("");
|
|
1433
1228
|
lines.push("</details>");
|
|
@@ -1441,7 +1236,7 @@ function firstLine(s) {
|
|
|
1441
1236
|
const trimmed = s.trim();
|
|
1442
1237
|
const nl = trimmed.indexOf("\n");
|
|
1443
1238
|
const head = nl === -1 ? trimmed : trimmed.slice(0, nl);
|
|
1444
|
-
return head.length > 200 ? head.slice(0, 197)
|
|
1239
|
+
return head.length > 200 ? `${head.slice(0, 197)}\u2026` : head;
|
|
1445
1240
|
}
|
|
1446
1241
|
function findExistingPr(branch, cwd) {
|
|
1447
1242
|
try {
|
|
@@ -1461,13 +1256,12 @@ function ensurePr(opts) {
|
|
|
1461
1256
|
const existing = findExistingPr(opts.branch, opts.cwd);
|
|
1462
1257
|
if (existing) {
|
|
1463
1258
|
try {
|
|
1464
|
-
gh(
|
|
1465
|
-
["pr", "edit", String(existing.number), "--body-file", "-"],
|
|
1466
|
-
{ input: body, cwd: opts.cwd }
|
|
1467
|
-
);
|
|
1259
|
+
gh(["pr", "edit", String(existing.number), "--body-file", "-"], { input: body, cwd: opts.cwd });
|
|
1468
1260
|
} catch (err) {
|
|
1469
|
-
process.stderr.write(
|
|
1470
|
-
`)
|
|
1261
|
+
process.stderr.write(
|
|
1262
|
+
`[kody2] failed to update PR #${existing.number}: ${err instanceof Error ? err.message : String(err)}
|
|
1263
|
+
`
|
|
1264
|
+
);
|
|
1471
1265
|
}
|
|
1472
1266
|
return { url: existing.url, number: existing.number, draft: opts.draft, action: "updated" };
|
|
1473
1267
|
}
|
|
@@ -1538,9 +1332,354 @@ function computeFailureReason(ctx) {
|
|
|
1538
1332
|
if (!agentDone) {
|
|
1539
1333
|
return ctx.data.agentFailureReason || ctx.data.agentError || ctx.data.commitCrash || "agent did not emit DONE";
|
|
1540
1334
|
}
|
|
1541
|
-
if (ctx.data.verifyOk === false) return ctx.data.verifyReason || "verify failed";
|
|
1542
|
-
return "";
|
|
1543
|
-
}
|
|
1335
|
+
if (ctx.data.verifyOk === false) return ctx.data.verifyReason || "verify failed";
|
|
1336
|
+
return "";
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/branch.ts
|
|
1340
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
1341
|
+
var UncommittedChangesError = class extends Error {
|
|
1342
|
+
constructor(branch) {
|
|
1343
|
+
super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
|
|
1344
|
+
this.branch = branch;
|
|
1345
|
+
this.name = "UncommittedChangesError";
|
|
1346
|
+
}
|
|
1347
|
+
branch;
|
|
1348
|
+
};
|
|
1349
|
+
function git2(args, cwd) {
|
|
1350
|
+
return execFileSync6("git", args, {
|
|
1351
|
+
encoding: "utf-8",
|
|
1352
|
+
timeout: 3e4,
|
|
1353
|
+
cwd,
|
|
1354
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
|
|
1355
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1356
|
+
}).trim();
|
|
1357
|
+
}
|
|
1358
|
+
function deriveBranchName(issueNumber, title) {
|
|
1359
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50).replace(/-$/, "");
|
|
1360
|
+
return slug ? `${issueNumber}-${slug}` : `${issueNumber}`;
|
|
1361
|
+
}
|
|
1362
|
+
function getCurrentBranch(cwd) {
|
|
1363
|
+
return git2(["branch", "--show-current"], cwd);
|
|
1364
|
+
}
|
|
1365
|
+
function hasUncommittedChanges(cwd) {
|
|
1366
|
+
return git2(["status", "--porcelain", "--untracked-files=no"], cwd).length > 0;
|
|
1367
|
+
}
|
|
1368
|
+
function checkoutPrBranch(prNumber, cwd) {
|
|
1369
|
+
const env = {
|
|
1370
|
+
...process.env,
|
|
1371
|
+
HUSKY: "0",
|
|
1372
|
+
SKIP_HOOKS: "1",
|
|
1373
|
+
GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
|
|
1374
|
+
};
|
|
1375
|
+
execFileSync6("gh", ["pr", "checkout", String(prNumber)], {
|
|
1376
|
+
cwd,
|
|
1377
|
+
env,
|
|
1378
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1379
|
+
timeout: 6e4
|
|
1380
|
+
});
|
|
1381
|
+
return getCurrentBranch(cwd);
|
|
1382
|
+
}
|
|
1383
|
+
function mergeBase(baseBranch, cwd) {
|
|
1384
|
+
try {
|
|
1385
|
+
git2(["fetch", "origin", baseBranch], cwd);
|
|
1386
|
+
} catch {
|
|
1387
|
+
return "error";
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
git2(["merge", `origin/${baseBranch}`, "--no-edit", "--no-ff"], cwd);
|
|
1391
|
+
return "clean";
|
|
1392
|
+
} catch {
|
|
1393
|
+
try {
|
|
1394
|
+
const unmerged = git2(["diff", "--name-only", "--diff-filter=U"], cwd);
|
|
1395
|
+
if (unmerged.length > 0) return "conflict";
|
|
1396
|
+
} catch {
|
|
1397
|
+
}
|
|
1398
|
+
try {
|
|
1399
|
+
git2(["merge", "--abort"], cwd);
|
|
1400
|
+
} catch {
|
|
1401
|
+
}
|
|
1402
|
+
return "error";
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
1406
|
+
const branchName = deriveBranchName(issueNumber, title);
|
|
1407
|
+
const current = getCurrentBranch(cwd);
|
|
1408
|
+
if (current === branchName) {
|
|
1409
|
+
if (hasUncommittedChanges(cwd)) throw new UncommittedChangesError(branchName);
|
|
1410
|
+
return { branch: branchName, created: false };
|
|
1411
|
+
}
|
|
1412
|
+
if (hasUncommittedChanges(cwd)) throw new UncommittedChangesError(current || "(detached)");
|
|
1413
|
+
try {
|
|
1414
|
+
git2(["fetch", "origin"], cwd);
|
|
1415
|
+
} catch {
|
|
1416
|
+
}
|
|
1417
|
+
try {
|
|
1418
|
+
git2(["rev-parse", "--verify", `origin/${branchName}`], cwd);
|
|
1419
|
+
git2(["checkout", branchName], cwd);
|
|
1420
|
+
try {
|
|
1421
|
+
git2(["pull", "origin", branchName], cwd);
|
|
1422
|
+
} catch {
|
|
1423
|
+
}
|
|
1424
|
+
return { branch: branchName, created: false };
|
|
1425
|
+
} catch {
|
|
1426
|
+
}
|
|
1427
|
+
try {
|
|
1428
|
+
git2(["rev-parse", "--verify", branchName], cwd);
|
|
1429
|
+
git2(["checkout", branchName], cwd);
|
|
1430
|
+
return { branch: branchName, created: false };
|
|
1431
|
+
} catch {
|
|
1432
|
+
}
|
|
1433
|
+
try {
|
|
1434
|
+
git2(["checkout", "-b", branchName, `origin/${defaultBranch}`], cwd);
|
|
1435
|
+
} catch {
|
|
1436
|
+
git2(["checkout", "-b", branchName], cwd);
|
|
1437
|
+
}
|
|
1438
|
+
return { branch: branchName, created: true };
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/gha.ts
|
|
1442
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
1443
|
+
import * as fs8 from "fs";
|
|
1444
|
+
function getRunUrl() {
|
|
1445
|
+
const server = process.env.GITHUB_SERVER_URL;
|
|
1446
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
1447
|
+
const runId = process.env.GITHUB_RUN_ID;
|
|
1448
|
+
if (!server || !repo || !runId) return "";
|
|
1449
|
+
return `${server}/${repo}/actions/runs/${runId}`;
|
|
1450
|
+
}
|
|
1451
|
+
function reactToTriggerComment(cwd) {
|
|
1452
|
+
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
1453
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
1454
|
+
if (!eventPath || !fs8.existsSync(eventPath)) return;
|
|
1455
|
+
let event = null;
|
|
1456
|
+
try {
|
|
1457
|
+
event = JSON.parse(fs8.readFileSync(eventPath, "utf-8"));
|
|
1458
|
+
} catch {
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const commentId = event?.comment?.id;
|
|
1462
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
1463
|
+
if (!commentId || !repo) return;
|
|
1464
|
+
const token = process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
1465
|
+
try {
|
|
1466
|
+
execFileSync7(
|
|
1467
|
+
"gh",
|
|
1468
|
+
[
|
|
1469
|
+
"api",
|
|
1470
|
+
"-X",
|
|
1471
|
+
"POST",
|
|
1472
|
+
"-H",
|
|
1473
|
+
"Accept: application/vnd.github+json",
|
|
1474
|
+
`/repos/${repo}/issues/comments/${commentId}/reactions`,
|
|
1475
|
+
"-f",
|
|
1476
|
+
"content=eyes"
|
|
1477
|
+
],
|
|
1478
|
+
{
|
|
1479
|
+
cwd,
|
|
1480
|
+
env: { ...process.env, GH_TOKEN: token ?? process.env.GH_TOKEN ?? "" },
|
|
1481
|
+
stdio: "pipe",
|
|
1482
|
+
timeout: 15e3
|
|
1483
|
+
}
|
|
1484
|
+
);
|
|
1485
|
+
} catch {
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/workflow.ts
|
|
1490
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
1491
|
+
var GH_TIMEOUT_MS = 3e4;
|
|
1492
|
+
function ghToken2() {
|
|
1493
|
+
return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
|
|
1494
|
+
}
|
|
1495
|
+
function gh2(args, cwd) {
|
|
1496
|
+
const token = ghToken2();
|
|
1497
|
+
const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
|
|
1498
|
+
return execFileSync8("gh", args, {
|
|
1499
|
+
encoding: "utf-8",
|
|
1500
|
+
timeout: GH_TIMEOUT_MS,
|
|
1501
|
+
cwd,
|
|
1502
|
+
env,
|
|
1503
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1504
|
+
}).trim();
|
|
1505
|
+
}
|
|
1506
|
+
function getLatestFailedRunForPr(prNumber, cwd) {
|
|
1507
|
+
let headBranch;
|
|
1508
|
+
try {
|
|
1509
|
+
const out = gh2(["pr", "view", String(prNumber), "--json", "headRefName"], cwd);
|
|
1510
|
+
headBranch = JSON.parse(out).headRefName;
|
|
1511
|
+
} catch {
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
if (!headBranch) return null;
|
|
1515
|
+
try {
|
|
1516
|
+
const out = gh2(
|
|
1517
|
+
[
|
|
1518
|
+
"run",
|
|
1519
|
+
"list",
|
|
1520
|
+
"--branch",
|
|
1521
|
+
headBranch,
|
|
1522
|
+
"--status",
|
|
1523
|
+
"failure",
|
|
1524
|
+
"--limit",
|
|
1525
|
+
"1",
|
|
1526
|
+
"--json",
|
|
1527
|
+
"databaseId,workflowName,headBranch,conclusion,url,createdAt"
|
|
1528
|
+
],
|
|
1529
|
+
cwd
|
|
1530
|
+
);
|
|
1531
|
+
const parsed = JSON.parse(out);
|
|
1532
|
+
if (!Array.isArray(parsed) || parsed.length === 0) return null;
|
|
1533
|
+
const r = parsed[0];
|
|
1534
|
+
return {
|
|
1535
|
+
id: String(r.databaseId ?? ""),
|
|
1536
|
+
workflowName: r.workflowName ?? "",
|
|
1537
|
+
headBranch: r.headBranch ?? headBranch,
|
|
1538
|
+
conclusion: r.conclusion ?? "failure",
|
|
1539
|
+
url: r.url ?? "",
|
|
1540
|
+
createdAt: r.createdAt ?? ""
|
|
1541
|
+
};
|
|
1542
|
+
} catch {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
function getFailedRunLogTail(runId, maxBytes, cwd) {
|
|
1547
|
+
try {
|
|
1548
|
+
const raw = gh2(["run", "view", String(runId), "--log-failed"], cwd);
|
|
1549
|
+
if (raw.length <= maxBytes) return raw;
|
|
1550
|
+
return raw.slice(-maxBytes);
|
|
1551
|
+
} catch {
|
|
1552
|
+
return "";
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/scripts/fixCiFlow.ts
|
|
1557
|
+
var LOG_MAX_BYTES = 3e4;
|
|
1558
|
+
var fixCiFlow = async (ctx) => {
|
|
1559
|
+
const prNumber = ctx.args.pr;
|
|
1560
|
+
const pr = getPr(prNumber, ctx.cwd);
|
|
1561
|
+
if (pr.state !== "OPEN") {
|
|
1562
|
+
ctx.output.exitCode = 1;
|
|
1563
|
+
ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
|
|
1564
|
+
ctx.skipAgent = true;
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
ctx.data.pr = pr;
|
|
1568
|
+
ctx.data.commentTargetType = "pr";
|
|
1569
|
+
ctx.data.commentTargetNumber = prNumber;
|
|
1570
|
+
checkoutPrBranch(prNumber, ctx.cwd);
|
|
1571
|
+
ctx.data.branch = getCurrentBranch(ctx.cwd);
|
|
1572
|
+
let runId = ctx.args.runId;
|
|
1573
|
+
let workflowName = "";
|
|
1574
|
+
let failedRunUrl = "";
|
|
1575
|
+
if (!runId) {
|
|
1576
|
+
const run = getLatestFailedRunForPr(prNumber, ctx.cwd);
|
|
1577
|
+
if (!run) {
|
|
1578
|
+
ctx.output.exitCode = 1;
|
|
1579
|
+
ctx.output.reason = `no failed workflow run found on PR #${prNumber}'s branch`;
|
|
1580
|
+
ctx.skipAgent = true;
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
runId = run.id;
|
|
1584
|
+
workflowName = run.workflowName;
|
|
1585
|
+
failedRunUrl = run.url;
|
|
1586
|
+
}
|
|
1587
|
+
const logTail = getFailedRunLogTail(runId, LOG_MAX_BYTES, ctx.cwd);
|
|
1588
|
+
if (!logTail) {
|
|
1589
|
+
ctx.output.exitCode = 1;
|
|
1590
|
+
ctx.output.reason = `failed to fetch log tail for run ${runId}`;
|
|
1591
|
+
ctx.skipAgent = true;
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
ctx.data.failedRunId = runId;
|
|
1595
|
+
ctx.data.failedWorkflowName = workflowName;
|
|
1596
|
+
ctx.data.failedRunUrl = failedRunUrl;
|
|
1597
|
+
ctx.data.failedLogTail = logTail;
|
|
1598
|
+
ctx.data.prDiff = getPrDiff(prNumber, ctx.cwd);
|
|
1599
|
+
const runUrl = getRunUrl();
|
|
1600
|
+
const runSuffix = runUrl ? `, kody2 run ${runUrl}` : "";
|
|
1601
|
+
tryPostPr(
|
|
1602
|
+
prNumber,
|
|
1603
|
+
`\u2699\uFE0F kody2 fix-ci started on \`${ctx.data.branch}\`${runSuffix} \u2014 analyzing workflow run ${runId}`,
|
|
1604
|
+
ctx.cwd
|
|
1605
|
+
);
|
|
1606
|
+
};
|
|
1607
|
+
function tryPostPr(prNumber, body, cwd) {
|
|
1608
|
+
try {
|
|
1609
|
+
postPrReviewComment(prNumber, body, cwd);
|
|
1610
|
+
} catch {
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// src/scripts/fixFlow.ts
|
|
1615
|
+
var fixFlow = async (ctx) => {
|
|
1616
|
+
const prNumber = ctx.args.pr;
|
|
1617
|
+
const pr = getPr(prNumber, ctx.cwd);
|
|
1618
|
+
if (pr.state !== "OPEN") {
|
|
1619
|
+
ctx.output.exitCode = 1;
|
|
1620
|
+
ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
|
|
1621
|
+
ctx.skipAgent = true;
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
ctx.data.pr = pr;
|
|
1625
|
+
ctx.data.commentTargetType = "pr";
|
|
1626
|
+
ctx.data.commentTargetNumber = prNumber;
|
|
1627
|
+
checkoutPrBranch(prNumber, ctx.cwd);
|
|
1628
|
+
ctx.data.branch = getCurrentBranch(ctx.cwd);
|
|
1629
|
+
const inlineFeedback = ctx.args.feedback?.trim();
|
|
1630
|
+
const feedback = inlineFeedback || getPrLatestReviewBody(prNumber, ctx.cwd);
|
|
1631
|
+
if (!feedback.trim()) {
|
|
1632
|
+
ctx.output.exitCode = 1;
|
|
1633
|
+
ctx.output.reason = "no --feedback provided and no review/body text found on PR";
|
|
1634
|
+
ctx.skipAgent = true;
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
ctx.data.feedback = feedback;
|
|
1638
|
+
ctx.data.prDiff = getPrDiff(prNumber, ctx.cwd);
|
|
1639
|
+
const runUrl = getRunUrl();
|
|
1640
|
+
const runSuffix = runUrl ? `, run ${runUrl}` : "";
|
|
1641
|
+
tryPostPr2(
|
|
1642
|
+
prNumber,
|
|
1643
|
+
`\u2699\uFE0F kody2 fix started on \`${ctx.data.branch}\`${runSuffix} \u2014 applying feedback (${truncate2(feedback.replace(/\n/g, " "), 200)})`,
|
|
1644
|
+
ctx.cwd
|
|
1645
|
+
);
|
|
1646
|
+
};
|
|
1647
|
+
function tryPostPr2(prNumber, body, cwd) {
|
|
1648
|
+
try {
|
|
1649
|
+
postPrReviewComment(prNumber, body, cwd);
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// src/scripts/loadConventions.ts
|
|
1655
|
+
var loadConventions = async (ctx) => {
|
|
1656
|
+
const conventions = loadProjectConventions(ctx.cwd);
|
|
1657
|
+
ctx.data.conventions = conventions;
|
|
1658
|
+
if (conventions.length > 0) {
|
|
1659
|
+
process.stderr.write(`[kody2] loaded conventions: ${conventions.map((c) => c.path).join(", ")}
|
|
1660
|
+
`);
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
// src/scripts/loadCoverageRules.ts
|
|
1665
|
+
var loadCoverageRules = async (ctx) => {
|
|
1666
|
+
ctx.data.coverageRules = ctx.config.testRequirements ?? [];
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
// src/scripts/parseAgentResult.ts
|
|
1670
|
+
var parseAgentResult2 = async (ctx, _profile, agentResult) => {
|
|
1671
|
+
if (!agentResult) {
|
|
1672
|
+
ctx.data.agentDone = false;
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
const parsed = parseAgentResult(agentResult.finalText);
|
|
1676
|
+
ctx.data.agentDone = parsed.done;
|
|
1677
|
+
ctx.data.commitMessage = parsed.commitMessage;
|
|
1678
|
+
ctx.data.prSummary = parsed.prSummary;
|
|
1679
|
+
ctx.data.agentFailureReason = parsed.failureReason;
|
|
1680
|
+
ctx.data.agentOutcome = agentResult.outcome;
|
|
1681
|
+
ctx.data.agentError = agentResult.error;
|
|
1682
|
+
};
|
|
1544
1683
|
|
|
1545
1684
|
// src/scripts/postIssueComment.ts
|
|
1546
1685
|
var postIssueComment2 = async (ctx) => {
|
|
@@ -1559,13 +1698,13 @@ var postIssueComment2 = async (ctx) => {
|
|
|
1559
1698
|
return;
|
|
1560
1699
|
}
|
|
1561
1700
|
if (ctx.output.exitCode === 4 && ctx.data.prCrashReason) {
|
|
1562
|
-
postWith(targetType, targetNumber, `\u26A0\uFE0F kody2 FAILED: ${
|
|
1701
|
+
postWith(targetType, targetNumber, `\u26A0\uFE0F kody2 FAILED: ${truncate2(ctx.data.prCrashReason, 1500)}`, ctx.cwd);
|
|
1563
1702
|
ctx.output.reason = ctx.data.prCrashReason;
|
|
1564
1703
|
return;
|
|
1565
1704
|
}
|
|
1566
1705
|
const failureReason = computeFailureReason2(ctx);
|
|
1567
1706
|
const isFailure = failureReason.length > 0;
|
|
1568
|
-
const msg = isFailure ? `\u26A0\uFE0F kody2 FAILED: ${
|
|
1707
|
+
const msg = isFailure ? `\u26A0\uFE0F kody2 FAILED: ${truncate2(failureReason, 1500)}${prUrl ? ` \u2014 draft PR: ${prUrl}` : ""}` : `\u2705 kody2 PR opened: ${prUrl}`;
|
|
1569
1708
|
postWith(targetType, targetNumber, msg, ctx.cwd);
|
|
1570
1709
|
let exitCode = 0;
|
|
1571
1710
|
const agentDone = Boolean(ctx.data.agentDone);
|
|
@@ -1594,195 +1733,266 @@ function postWith(type, n, body, cwd) {
|
|
|
1594
1733
|
}
|
|
1595
1734
|
}
|
|
1596
1735
|
|
|
1597
|
-
// src/scripts/
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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;
|
|
1736
|
+
// src/scripts/resolveFlow.ts
|
|
1737
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
1738
|
+
var CONFLICT_DIFF_MAX_BYTES = 4e4;
|
|
1739
|
+
var resolveFlow = async (ctx) => {
|
|
1740
|
+
const prNumber = ctx.args.pr;
|
|
1741
|
+
const pr = getPr(prNumber, ctx.cwd);
|
|
1742
|
+
if (pr.state !== "OPEN") {
|
|
1743
|
+
ctx.output.exitCode = 1;
|
|
1744
|
+
ctx.output.reason = `PR #${prNumber} is not OPEN (state: ${pr.state})`;
|
|
1745
|
+
ctx.skipAgent = true;
|
|
1746
|
+
return;
|
|
1630
1747
|
}
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1748
|
+
ctx.data.pr = pr;
|
|
1749
|
+
ctx.data.commentTargetType = "pr";
|
|
1750
|
+
ctx.data.commentTargetNumber = prNumber;
|
|
1751
|
+
checkoutPrBranch(prNumber, ctx.cwd);
|
|
1752
|
+
ctx.data.branch = getCurrentBranch(ctx.cwd);
|
|
1753
|
+
const baseBranch = pr.baseRefName || ctx.config.git.defaultBranch;
|
|
1754
|
+
ctx.data.baseBranch = baseBranch;
|
|
1755
|
+
const mergeStatus = mergeBase(baseBranch, ctx.cwd);
|
|
1756
|
+
if (mergeStatus === "clean") {
|
|
1757
|
+
ctx.output.exitCode = 0;
|
|
1758
|
+
ctx.output.reason = `already up to date with origin/${baseBranch} \u2014 nothing to resolve`;
|
|
1759
|
+
ctx.skipAgent = true;
|
|
1760
|
+
tryPostPr3(prNumber, `\u2139\uFE0F kody2 resolve: ${ctx.output.reason}`, ctx.cwd);
|
|
1761
|
+
return;
|
|
1642
1762
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1763
|
+
if (mergeStatus === "error") {
|
|
1764
|
+
ctx.output.exitCode = 99;
|
|
1765
|
+
ctx.output.reason = `failed to merge origin/${baseBranch} (non-conflict error); see runner log`;
|
|
1766
|
+
ctx.skipAgent = true;
|
|
1767
|
+
tryPostPr3(prNumber, `\u26A0\uFE0F kody2 resolve FAILED: ${ctx.output.reason}`, ctx.cwd);
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const conflictedFiles = getConflictedFiles(ctx.cwd);
|
|
1771
|
+
if (conflictedFiles.length === 0) {
|
|
1772
|
+
ctx.output.exitCode = 99;
|
|
1773
|
+
ctx.output.reason = "merge reported conflict but no unmerged paths detected";
|
|
1774
|
+
ctx.skipAgent = true;
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
ctx.data.conflictedFiles = conflictedFiles;
|
|
1778
|
+
ctx.data.conflictMarkersPreview = getConflictMarkersPreview(conflictedFiles, ctx.cwd);
|
|
1779
|
+
const runUrl = getRunUrl();
|
|
1780
|
+
const runSuffix = runUrl ? `, run ${runUrl}` : "";
|
|
1781
|
+
tryPostPr3(
|
|
1782
|
+
prNumber,
|
|
1783
|
+
`\u2699\uFE0F kody2 resolve started on \`${ctx.data.branch}\`${runSuffix} \u2014 ${conflictedFiles.length} conflicted file(s)`,
|
|
1784
|
+
ctx.cwd
|
|
1785
|
+
);
|
|
1786
|
+
};
|
|
1787
|
+
function getConflictedFiles(cwd) {
|
|
1788
|
+
try {
|
|
1789
|
+
const out = execFileSync9("git", ["diff", "--name-only", "--diff-filter=U"], {
|
|
1790
|
+
encoding: "utf-8",
|
|
1791
|
+
cwd,
|
|
1792
|
+
env: { ...process.env, HUSKY: "0" }
|
|
1793
|
+
}).trim();
|
|
1794
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
1795
|
+
} catch {
|
|
1796
|
+
return [];
|
|
1655
1797
|
}
|
|
1656
|
-
return lines.length > 0 ? lines.join("\n") : null;
|
|
1657
1798
|
}
|
|
1658
|
-
function
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1661
|
-
for (const
|
|
1662
|
-
|
|
1663
|
-
const
|
|
1664
|
-
const
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
}
|
|
1799
|
+
function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTES) {
|
|
1800
|
+
const chunks = [];
|
|
1801
|
+
let total = 0;
|
|
1802
|
+
for (const f of files) {
|
|
1803
|
+
try {
|
|
1804
|
+
const content = execFileSync9("cat", [f], { encoding: "utf-8", cwd }).toString();
|
|
1805
|
+
const snippet = `### ${f}
|
|
1806
|
+
|
|
1807
|
+
\`\`\`
|
|
1808
|
+
${content.slice(0, 6e3)}
|
|
1809
|
+
\`\`\`
|
|
1810
|
+
`;
|
|
1811
|
+
total += snippet.length;
|
|
1812
|
+
chunks.push(snippet);
|
|
1813
|
+
if (total >= maxBytes) break;
|
|
1814
|
+
} catch {
|
|
1675
1815
|
}
|
|
1676
1816
|
}
|
|
1677
|
-
return
|
|
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} ===`;
|
|
1817
|
+
return chunks.join("\n");
|
|
1687
1818
|
}
|
|
1688
|
-
function
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1819
|
+
function tryPostPr3(prNumber, body, cwd) {
|
|
1820
|
+
try {
|
|
1821
|
+
postPrReviewComment(prNumber, body, cwd);
|
|
1822
|
+
} catch {
|
|
1692
1823
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/scripts/runFlow.ts
|
|
1827
|
+
var runFlow = async (ctx) => {
|
|
1828
|
+
const issueNumber = ctx.args.issue;
|
|
1829
|
+
const issue = getIssue(issueNumber, ctx.cwd);
|
|
1830
|
+
ctx.data.issue = issue;
|
|
1831
|
+
ctx.data.commentTargetType = "issue";
|
|
1832
|
+
ctx.data.commentTargetNumber = issueNumber;
|
|
1833
|
+
try {
|
|
1834
|
+
const branchInfo = ensureFeatureBranch(issueNumber, issue.title, ctx.config.git.defaultBranch, ctx.cwd);
|
|
1835
|
+
ctx.data.branch = branchInfo.branch;
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
if (err instanceof UncommittedChangesError) {
|
|
1838
|
+
ctx.output.exitCode = 5;
|
|
1839
|
+
ctx.output.reason = err.message;
|
|
1840
|
+
ctx.skipAgent = true;
|
|
1841
|
+
tryPost(issueNumber, `\u26A0\uFE0F kody2 refused to start: ${err.message}`, ctx.cwd);
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
throw err;
|
|
1695
1845
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1846
|
+
const runUrl = getRunUrl();
|
|
1847
|
+
const startMsg = runUrl ? `\u2699\uFE0F kody2 started \u2014 branch \`${ctx.data.branch}\`, run ${runUrl}` : `\u2699\uFE0F kody2 started \u2014 branch \`${ctx.data.branch}\``;
|
|
1848
|
+
tryPost(issueNumber, startMsg, ctx.cwd);
|
|
1849
|
+
};
|
|
1850
|
+
function tryPost(issueNumber, body, cwd) {
|
|
1851
|
+
try {
|
|
1852
|
+
postIssueComment(issueNumber, body, cwd);
|
|
1853
|
+
} catch {
|
|
1698
1854
|
}
|
|
1699
|
-
return "";
|
|
1700
1855
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1856
|
+
|
|
1857
|
+
// src/verify.ts
|
|
1858
|
+
import { spawn as spawn2 } from "child_process";
|
|
1859
|
+
var TAIL_CHARS = 4e3;
|
|
1860
|
+
var COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1861
|
+
function runCommand(command, cwd) {
|
|
1862
|
+
return new Promise((resolve2) => {
|
|
1863
|
+
const start = Date.now();
|
|
1864
|
+
const child = spawn2(command, {
|
|
1865
|
+
cwd,
|
|
1866
|
+
shell: true,
|
|
1867
|
+
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
|
|
1868
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1869
|
+
});
|
|
1870
|
+
const buffers = [];
|
|
1871
|
+
let totalSize = 0;
|
|
1872
|
+
const collect = (chunk) => {
|
|
1873
|
+
buffers.push(chunk);
|
|
1874
|
+
totalSize += chunk.length;
|
|
1875
|
+
while (totalSize > TAIL_CHARS * 4 && buffers.length > 1) {
|
|
1876
|
+
totalSize -= buffers[0].length;
|
|
1877
|
+
buffers.shift();
|
|
1707
1878
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1879
|
+
};
|
|
1880
|
+
child.stdout?.on("data", collect);
|
|
1881
|
+
child.stderr?.on("data", collect);
|
|
1882
|
+
const timer = setTimeout(() => {
|
|
1883
|
+
child.kill("SIGTERM");
|
|
1884
|
+
setTimeout(() => {
|
|
1885
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
1886
|
+
}, 5e3);
|
|
1887
|
+
}, COMMAND_TIMEOUT_MS);
|
|
1888
|
+
child.on("exit", (code) => {
|
|
1889
|
+
clearTimeout(timer);
|
|
1890
|
+
const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
|
|
1891
|
+
resolve2({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
|
|
1892
|
+
});
|
|
1893
|
+
child.on("error", (err) => {
|
|
1894
|
+
clearTimeout(timer);
|
|
1895
|
+
resolve2({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
|
|
1896
|
+
});
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
async function verifyAll(config, cwd) {
|
|
1900
|
+
const commands = [];
|
|
1901
|
+
if (config.quality.typecheck) commands.push({ name: "typecheck", cmd: config.quality.typecheck });
|
|
1902
|
+
if (config.quality.testUnit) commands.push({ name: "test", cmd: config.quality.testUnit });
|
|
1903
|
+
if (config.quality.lint) commands.push({ name: "lint", cmd: config.quality.lint });
|
|
1904
|
+
const failed = [];
|
|
1905
|
+
const details = {};
|
|
1906
|
+
for (const { name, cmd } of commands) {
|
|
1907
|
+
const result = await runCommand(cmd, cwd);
|
|
1908
|
+
details[name] = result;
|
|
1909
|
+
if (result.exitCode !== 0) failed.push(name);
|
|
1710
1910
|
}
|
|
1711
|
-
return
|
|
1911
|
+
return { ok: failed.length === 0, failed, details };
|
|
1712
1912
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
return s.
|
|
1913
|
+
var ANSI_RE = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
1914
|
+
function stripAnsi(s) {
|
|
1915
|
+
return s.replace(ANSI_RE, "");
|
|
1716
1916
|
}
|
|
1717
|
-
function
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1917
|
+
function summarizeFailure(result) {
|
|
1918
|
+
const lines = [`verify failed: ${result.failed.join(", ")}`];
|
|
1919
|
+
for (const name of result.failed) {
|
|
1920
|
+
const d = result.details[name];
|
|
1921
|
+
if (!d) continue;
|
|
1922
|
+
lines.push(`
|
|
1923
|
+
--- ${name} (exit ${d.exitCode}, ${(d.durationMs / 1e3).toFixed(1)}s) ---`);
|
|
1924
|
+
lines.push(stripAnsi(d.tail));
|
|
1925
|
+
}
|
|
1926
|
+
return lines.join("\n");
|
|
1721
1927
|
}
|
|
1722
1928
|
|
|
1723
|
-
// src/
|
|
1724
|
-
var
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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();
|
|
1929
|
+
// src/scripts/verify.ts
|
|
1930
|
+
var verify = async (ctx) => {
|
|
1931
|
+
try {
|
|
1932
|
+
const result = await verifyAll(ctx.config, ctx.cwd);
|
|
1933
|
+
ctx.data.verifyOk = result.ok;
|
|
1934
|
+
ctx.data.verifyReason = result.ok ? "" : summarizeFailure(result);
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
ctx.data.verifyOk = false;
|
|
1937
|
+
ctx.data.verifyReason = `verify crashed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1739
1938
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1939
|
+
};
|
|
1940
|
+
|
|
1941
|
+
// src/scripts/writeRunSummary.ts
|
|
1942
|
+
import * as fs9 from "fs";
|
|
1943
|
+
var writeRunSummary = async (ctx) => {
|
|
1944
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
1945
|
+
if (!summaryPath) return;
|
|
1946
|
+
const mode = ctx.args.mode;
|
|
1947
|
+
const issue = ctx.args.issue;
|
|
1948
|
+
const pr = ctx.args.pr;
|
|
1949
|
+
const target = issue ? `issue #${issue}` : pr ? `PR #${pr}` : "(unknown)";
|
|
1950
|
+
const prUrl = ctx.output.prUrl;
|
|
1951
|
+
const exitCode = ctx.output.exitCode ?? 0;
|
|
1952
|
+
const reason = ctx.output.reason;
|
|
1953
|
+
const status = exitCode === 0 ? "\u2705 success" : exitCode === 3 ? "\u23ED\uFE0F no-op" : "\u26A0\uFE0F failed";
|
|
1954
|
+
const lines = [];
|
|
1955
|
+
lines.push(`## kody2 run \u2014 ${status}`);
|
|
1956
|
+
lines.push("");
|
|
1957
|
+
lines.push(`- **Mode:** \`${mode ?? "?"}\``);
|
|
1958
|
+
lines.push(`- **Target:** ${target}`);
|
|
1959
|
+
if (prUrl) lines.push(`- **PR:** ${prUrl}`);
|
|
1960
|
+
lines.push(`- **Exit code:** ${exitCode}`);
|
|
1961
|
+
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
1962
|
+
lines.push("");
|
|
1743
1963
|
try {
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
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
|
-
}
|
|
1964
|
+
fs9.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
1965
|
+
`);
|
|
1966
|
+
} catch {
|
|
1780
1967
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
// src/scripts/index.ts
|
|
1971
|
+
var preflightScripts = {
|
|
1972
|
+
runFlow,
|
|
1973
|
+
fixFlow,
|
|
1974
|
+
fixCiFlow,
|
|
1975
|
+
resolveFlow,
|
|
1976
|
+
loadConventions,
|
|
1977
|
+
loadCoverageRules,
|
|
1978
|
+
composePrompt
|
|
1979
|
+
};
|
|
1980
|
+
var postflightScripts = {
|
|
1981
|
+
parseAgentResult: parseAgentResult2,
|
|
1982
|
+
verify,
|
|
1983
|
+
checkCoverageWithRetry,
|
|
1984
|
+
commitAndPush: commitAndPush2,
|
|
1985
|
+
ensurePr: ensurePr2,
|
|
1986
|
+
postIssueComment: postIssueComment2,
|
|
1987
|
+
writeRunSummary
|
|
1988
|
+
};
|
|
1989
|
+
var allScriptNames = /* @__PURE__ */ new Set([
|
|
1990
|
+
...Object.keys(preflightScripts),
|
|
1991
|
+
...Object.keys(postflightScripts)
|
|
1992
|
+
]);
|
|
1783
1993
|
|
|
1784
1994
|
// src/tools.ts
|
|
1785
|
-
import { execFileSync as
|
|
1995
|
+
import { execFileSync as execFileSync10 } from "child_process";
|
|
1786
1996
|
function verifyCliTools(tools, cwd) {
|
|
1787
1997
|
const out = [];
|
|
1788
1998
|
for (const t of tools) out.push(verifyOne(t, cwd));
|
|
@@ -1815,121 +2025,13 @@ function verifyOne(tool, cwd) {
|
|
|
1815
2025
|
}
|
|
1816
2026
|
function runShell(cmd, cwd, timeoutMs = 3e4) {
|
|
1817
2027
|
try {
|
|
1818
|
-
|
|
2028
|
+
execFileSync10("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
|
|
1819
2029
|
return true;
|
|
1820
2030
|
} catch {
|
|
1821
2031
|
return false;
|
|
1822
2032
|
}
|
|
1823
2033
|
}
|
|
1824
2034
|
|
|
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
2035
|
// src/executor.ts
|
|
1934
2036
|
async function runExecutable(profileName, input) {
|
|
1935
2037
|
const profilePath = resolveProfilePath(profileName);
|
|
@@ -1960,7 +2062,10 @@ async function runExecutable(profileName, input) {
|
|
|
1960
2062
|
try {
|
|
1961
2063
|
litellm = await startLitellmIfNeeded(model, input.cwd);
|
|
1962
2064
|
} catch (err) {
|
|
1963
|
-
return finish({
|
|
2065
|
+
return finish({
|
|
2066
|
+
exitCode: 99,
|
|
2067
|
+
reason: `litellm startup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2068
|
+
});
|
|
1964
2069
|
}
|
|
1965
2070
|
const ctx = {
|
|
1966
2071
|
args,
|
|
@@ -1981,7 +2086,8 @@ async function runExecutable(profileName, input) {
|
|
|
1981
2086
|
quiet: input.quiet,
|
|
1982
2087
|
ndjsonDir,
|
|
1983
2088
|
allowedToolsOverride: profile.claudeCode.tools,
|
|
1984
|
-
permissionModeOverride: profile.claudeCode.permissionMode
|
|
2089
|
+
permissionModeOverride: profile.claudeCode.permissionMode,
|
|
2090
|
+
mcpServers: profile.claudeCode.mcpServers
|
|
1985
2091
|
});
|
|
1986
2092
|
ctx.data.__invokeAgent = invokeAgent;
|
|
1987
2093
|
try {
|
|
@@ -2006,9 +2112,21 @@ async function runExecutable(profileName, input) {
|
|
|
2006
2112
|
if (!shouldRun(entry, ctx)) continue;
|
|
2007
2113
|
const fn = postflightScripts[entry.script];
|
|
2008
2114
|
if (!fn) return finish({ exitCode: 99, reason: `postflight script not registered: ${entry.script}` });
|
|
2009
|
-
|
|
2115
|
+
try {
|
|
2116
|
+
await fn(ctx, profile, agentResult);
|
|
2117
|
+
} catch (err) {
|
|
2118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2119
|
+
process.stderr.write(`[kody2] postflight script "${entry.script}" crashed: ${msg}
|
|
2120
|
+
`);
|
|
2121
|
+
if (!ctx.output.reason) ctx.output.reason = `postflight ${entry.script} crashed: ${msg}`;
|
|
2122
|
+
if (ctx.output.exitCode === 0) ctx.output.exitCode = 99;
|
|
2123
|
+
}
|
|
2010
2124
|
}
|
|
2011
|
-
return finish(
|
|
2125
|
+
return finish({
|
|
2126
|
+
exitCode: ctx.output.exitCode ?? 0,
|
|
2127
|
+
prUrl: ctx.output.prUrl,
|
|
2128
|
+
reason: ctx.output.reason
|
|
2129
|
+
});
|
|
2012
2130
|
} finally {
|
|
2013
2131
|
try {
|
|
2014
2132
|
litellm?.kill();
|
|
@@ -2027,7 +2145,7 @@ function resolveProfilePath(profileName) {
|
|
|
2027
2145
|
// fallback
|
|
2028
2146
|
];
|
|
2029
2147
|
for (const c of candidates) {
|
|
2030
|
-
if (
|
|
2148
|
+
if (fs10.existsSync(c)) return c;
|
|
2031
2149
|
}
|
|
2032
2150
|
return candidates[0];
|
|
2033
2151
|
}
|
|
@@ -2105,12 +2223,12 @@ function finish(out) {
|
|
|
2105
2223
|
}
|
|
2106
2224
|
|
|
2107
2225
|
// src/kody2-cli.ts
|
|
2108
|
-
import * as fs11 from "fs";
|
|
2109
|
-
import * as path9 from "path";
|
|
2110
2226
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
2227
|
+
import * as fs12 from "fs";
|
|
2228
|
+
import * as path9 from "path";
|
|
2111
2229
|
|
|
2112
2230
|
// src/dispatch.ts
|
|
2113
|
-
import * as
|
|
2231
|
+
import * as fs11 from "fs";
|
|
2114
2232
|
function autoDispatch(explicit) {
|
|
2115
2233
|
if (explicit?.mode && explicit.target) {
|
|
2116
2234
|
return {
|
|
@@ -2120,10 +2238,10 @@ function autoDispatch(explicit) {
|
|
|
2120
2238
|
}
|
|
2121
2239
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
2122
2240
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
2123
|
-
if (!eventName || !eventPath || !
|
|
2241
|
+
if (!eventName || !eventPath || !fs11.existsSync(eventPath)) return null;
|
|
2124
2242
|
let event = {};
|
|
2125
2243
|
try {
|
|
2126
|
-
event = JSON.parse(
|
|
2244
|
+
event = JSON.parse(fs11.readFileSync(eventPath, "utf-8"));
|
|
2127
2245
|
} catch {
|
|
2128
2246
|
return null;
|
|
2129
2247
|
}
|
|
@@ -2243,9 +2361,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
2243
2361
|
return token;
|
|
2244
2362
|
}
|
|
2245
2363
|
function detectPackageManager(cwd) {
|
|
2246
|
-
if (
|
|
2247
|
-
if (
|
|
2248
|
-
if (
|
|
2364
|
+
if (fs12.existsSync(path9.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
2365
|
+
if (fs12.existsSync(path9.join(cwd, "yarn.lock"))) return "yarn";
|
|
2366
|
+
if (fs12.existsSync(path9.join(cwd, "bun.lockb"))) return "bun";
|
|
2249
2367
|
return "npm";
|
|
2250
2368
|
}
|
|
2251
2369
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -2325,13 +2443,13 @@ function postFailureTail(issueNumber, cwd, reason) {
|
|
|
2325
2443
|
const logPath = path9.join(cwd, ".kody2", "last-run.jsonl");
|
|
2326
2444
|
let tail = "";
|
|
2327
2445
|
try {
|
|
2328
|
-
if (
|
|
2329
|
-
const content =
|
|
2446
|
+
if (fs12.existsSync(logPath)) {
|
|
2447
|
+
const content = fs12.readFileSync(logPath, "utf-8");
|
|
2330
2448
|
tail = content.slice(-3e3);
|
|
2331
2449
|
}
|
|
2332
2450
|
} catch {
|
|
2333
2451
|
}
|
|
2334
|
-
const body = tail ? `\u26A0\uFE0F kody2 preflight failed: ${
|
|
2452
|
+
const body = tail ? `\u26A0\uFE0F kody2 preflight failed: ${truncate2(reason, 500)}
|
|
2335
2453
|
|
|
2336
2454
|
<details><summary>Last-run log tail</summary>
|
|
2337
2455
|
|
|
@@ -2339,7 +2457,7 @@ function postFailureTail(issueNumber, cwd, reason) {
|
|
|
2339
2457
|
${tail}
|
|
2340
2458
|
\`\`\`
|
|
2341
2459
|
|
|
2342
|
-
</details>` : `\u26A0\uFE0F kody2 preflight failed: ${
|
|
2460
|
+
</details>` : `\u26A0\uFE0F kody2 preflight failed: ${truncate2(reason, 1500)}`;
|
|
2343
2461
|
try {
|
|
2344
2462
|
postIssueComment(issueNumber, body, cwd);
|
|
2345
2463
|
} catch {
|
|
@@ -2359,7 +2477,8 @@ async function runCi(argv) {
|
|
|
2359
2477
|
if (args.errors.length > 0 && !args.errors.includes("__HELP__")) {
|
|
2360
2478
|
for (const e of args.errors) process.stderr.write(`error: ${e}
|
|
2361
2479
|
`);
|
|
2362
|
-
process.stderr.write(
|
|
2480
|
+
process.stderr.write(`
|
|
2481
|
+
${CI_HELP}`);
|
|
2363
2482
|
return 64;
|
|
2364
2483
|
}
|
|
2365
2484
|
const cwd = args.cwd ? path9.resolve(args.cwd) : process.cwd();
|
|
@@ -2430,12 +2549,74 @@ async function runCi(argv) {
|
|
|
2430
2549
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2431
2550
|
process.stderr.write(`[kody2] run crashed: ${msg}
|
|
2432
2551
|
`);
|
|
2433
|
-
if (err instanceof Error && err.stack) process.stderr.write(err.stack
|
|
2552
|
+
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
2553
|
+
`);
|
|
2434
2554
|
postFailureTail(issueNumber, cwd, `run crashed: ${msg}`);
|
|
2435
2555
|
return 99;
|
|
2436
2556
|
}
|
|
2437
2557
|
}
|
|
2438
2558
|
|
|
2559
|
+
// src/registry.ts
|
|
2560
|
+
import * as fs13 from "fs";
|
|
2561
|
+
import * as path10 from "path";
|
|
2562
|
+
function getExecutablesRoot() {
|
|
2563
|
+
const here = path10.dirname(new URL(import.meta.url).pathname);
|
|
2564
|
+
const candidates = [
|
|
2565
|
+
path10.join(here, "executables"),
|
|
2566
|
+
// dev: src/
|
|
2567
|
+
path10.join(here, "..", "executables"),
|
|
2568
|
+
// built: dist/bin → dist/executables
|
|
2569
|
+
path10.join(here, "..", "src", "executables")
|
|
2570
|
+
// fallback
|
|
2571
|
+
];
|
|
2572
|
+
for (const c of candidates) {
|
|
2573
|
+
if (fs13.existsSync(c) && fs13.statSync(c).isDirectory()) return c;
|
|
2574
|
+
}
|
|
2575
|
+
return candidates[0];
|
|
2576
|
+
}
|
|
2577
|
+
function listExecutables(root = getExecutablesRoot()) {
|
|
2578
|
+
if (!fs13.existsSync(root)) return [];
|
|
2579
|
+
const entries = fs13.readdirSync(root, { withFileTypes: true });
|
|
2580
|
+
const out = [];
|
|
2581
|
+
for (const ent of entries) {
|
|
2582
|
+
if (!ent.isDirectory()) continue;
|
|
2583
|
+
const profilePath = path10.join(root, ent.name, "profile.json");
|
|
2584
|
+
if (fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile()) {
|
|
2585
|
+
out.push({ name: ent.name, profilePath });
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
2589
|
+
}
|
|
2590
|
+
function hasExecutable(name, root = getExecutablesRoot()) {
|
|
2591
|
+
if (!isSafeName(name)) return false;
|
|
2592
|
+
const profilePath = path10.join(root, name, "profile.json");
|
|
2593
|
+
return fs13.existsSync(profilePath) && fs13.statSync(profilePath).isFile();
|
|
2594
|
+
}
|
|
2595
|
+
function isSafeName(name) {
|
|
2596
|
+
return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
|
|
2597
|
+
}
|
|
2598
|
+
function parseGenericFlags(argv) {
|
|
2599
|
+
const args = {};
|
|
2600
|
+
const positional = [];
|
|
2601
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2602
|
+
const arg = argv[i];
|
|
2603
|
+
if (!arg.startsWith("--")) {
|
|
2604
|
+
positional.push(arg);
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
const key = arg.slice(2);
|
|
2608
|
+
const next = argv[i + 1];
|
|
2609
|
+
if (next !== void 0 && !next.startsWith("--")) {
|
|
2610
|
+
args[key] = next;
|
|
2611
|
+
i++;
|
|
2612
|
+
} else {
|
|
2613
|
+
args[key] = true;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
if (positional.length > 0) args._ = positional;
|
|
2617
|
+
return args;
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2439
2620
|
// src/entry.ts
|
|
2440
2621
|
var HELP_TEXT = `kody2 \u2014 single-session autonomous engineer
|
|
2441
2622
|
|
|
@@ -2452,10 +2633,10 @@ All commands dispatch to the Build executable with a specific mode. The
|
|
|
2452
2633
|
executable is defined by \`src/executables/build/profile.json\`.
|
|
2453
2634
|
|
|
2454
2635
|
Exit codes:
|
|
2455
|
-
0 success (PR opened, verify passed)
|
|
2636
|
+
0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
|
|
2456
2637
|
1 agent reported FAILED (draft PR opened)
|
|
2457
|
-
2 verify failed (draft PR opened)
|
|
2458
|
-
3 no commits to ship
|
|
2638
|
+
2 verify failed (draft PR opened) \u2014 skipped in resolve mode
|
|
2639
|
+
3 no commits to ship (also the resolve clean-merge short-circuit)
|
|
2459
2640
|
4 PR creation failed
|
|
2460
2641
|
5 uncommitted changes on target branch
|
|
2461
2642
|
64 invalid CLI args
|
|
@@ -2475,7 +2656,18 @@ function parseArgs(argv) {
|
|
|
2475
2656
|
parseCommandArgs(cmd, argv.slice(1), result);
|
|
2476
2657
|
return result;
|
|
2477
2658
|
}
|
|
2478
|
-
|
|
2659
|
+
if (hasExecutable(cmd)) {
|
|
2660
|
+
result.command = "__executable__";
|
|
2661
|
+
result.executableName = cmd;
|
|
2662
|
+
result.cliArgs = parseGenericFlags(argv.slice(1));
|
|
2663
|
+
if (typeof result.cliArgs.cwd === "string") result.cwd = result.cliArgs.cwd;
|
|
2664
|
+
if (result.cliArgs.verbose === true) result.verbose = true;
|
|
2665
|
+
if (result.cliArgs.quiet === true) result.quiet = true;
|
|
2666
|
+
return result;
|
|
2667
|
+
}
|
|
2668
|
+
const discovered = listExecutables().map((e) => e.name).filter((n) => n !== "build");
|
|
2669
|
+
const available = ["run", "fix", "fix-ci", "resolve", "ci", "help", "version", ...discovered];
|
|
2670
|
+
result.errors.push(`unknown command: ${cmd} (available: ${available.join(", ")})`);
|
|
2479
2671
|
return result;
|
|
2480
2672
|
}
|
|
2481
2673
|
function parseCommandArgs(cmd, rest, result) {
|
|
@@ -2510,7 +2702,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2510
2702
|
if (args.errors.length > 0) {
|
|
2511
2703
|
for (const e of args.errors) process.stderr.write(`error: ${e}
|
|
2512
2704
|
`);
|
|
2513
|
-
process.stderr.write(
|
|
2705
|
+
process.stderr.write(`
|
|
2706
|
+
${HELP_TEXT}`);
|
|
2514
2707
|
return 64;
|
|
2515
2708
|
}
|
|
2516
2709
|
if (args.command === "help") {
|
|
@@ -2518,7 +2711,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2518
2711
|
return 0;
|
|
2519
2712
|
}
|
|
2520
2713
|
if (args.command === "version") {
|
|
2521
|
-
process.stdout.write(
|
|
2714
|
+
process.stdout.write(`kody2 ${package_default.version}
|
|
2715
|
+
`);
|
|
2522
2716
|
return 0;
|
|
2523
2717
|
}
|
|
2524
2718
|
if (args.command === "ci") {
|
|
@@ -2528,7 +2722,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2528
2722
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2529
2723
|
process.stderr.write(`[kody2] fatal: ${msg}
|
|
2530
2724
|
`);
|
|
2531
|
-
if (err instanceof Error && err.stack) process.stderr.write(err.stack
|
|
2725
|
+
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
2726
|
+
`);
|
|
2532
2727
|
return 99;
|
|
2533
2728
|
}
|
|
2534
2729
|
}
|
|
@@ -2544,6 +2739,27 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2544
2739
|
`);
|
|
2545
2740
|
return 99;
|
|
2546
2741
|
}
|
|
2742
|
+
if (args.command === "__executable__") {
|
|
2743
|
+
try {
|
|
2744
|
+
const result = await runExecutable(args.executableName, {
|
|
2745
|
+
cliArgs: args.cliArgs ?? {},
|
|
2746
|
+
cwd,
|
|
2747
|
+
config,
|
|
2748
|
+
verbose: args.verbose,
|
|
2749
|
+
quiet: args.quiet
|
|
2750
|
+
});
|
|
2751
|
+
return result.exitCode;
|
|
2752
|
+
} catch (err) {
|
|
2753
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2754
|
+
process.stderr.write(`[kody2] ${args.executableName} crashed: ${msg}
|
|
2755
|
+
`);
|
|
2756
|
+
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
2757
|
+
`);
|
|
2758
|
+
process.stdout.write(`PR_URL=FAILED: ${args.executableName} crashed: ${msg}
|
|
2759
|
+
`);
|
|
2760
|
+
return 99;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2547
2763
|
const cliArgs = { mode: args.command };
|
|
2548
2764
|
if (args.issueNumber !== void 0) cliArgs.issue = args.issueNumber;
|
|
2549
2765
|
if (args.prNumber !== void 0) cliArgs.pr = args.prNumber;
|
|
@@ -2562,7 +2778,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2562
2778
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2563
2779
|
process.stderr.write(`[kody2] wrapper crashed: ${msg}
|
|
2564
2780
|
`);
|
|
2565
|
-
if (err instanceof Error && err.stack) process.stderr.write(err.stack
|
|
2781
|
+
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
2782
|
+
`);
|
|
2566
2783
|
process.stdout.write(`PR_URL=FAILED: wrapper crashed: ${msg}
|
|
2567
2784
|
`);
|
|
2568
2785
|
return 99;
|