@kody-ade/kody-engine 0.2.0 → 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 +1637 -1353
- package/dist/executables/build/profile.json +4 -3
- package/dist/executables/types.ts +2 -6
- package/package.json +15 -12
- package/templates/kody2.yml +16 -8
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
|
}
|
|
@@ -1542,247 +1336,663 @@ function computeFailureReason(ctx) {
|
|
|
1542
1336
|
return "";
|
|
1543
1337
|
}
|
|
1544
1338
|
|
|
1545
|
-
// src/
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
const hasCommits = Boolean(ctx.data.hasCommitsAhead);
|
|
1553
|
-
const prUrl = ctx.output.prUrl;
|
|
1554
|
-
if (!commitResult?.committed && !hasCommits) {
|
|
1555
|
-
const reason = "no changes to commit";
|
|
1556
|
-
postWith(targetType, targetNumber, `\u26A0\uFE0F kody2 FAILED: ${reason}`, ctx.cwd);
|
|
1557
|
-
ctx.output.exitCode = 3;
|
|
1558
|
-
ctx.output.reason = reason;
|
|
1559
|
-
return;
|
|
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";
|
|
1560
1346
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
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
|
+
};
|
|
1683
|
+
|
|
1684
|
+
// src/scripts/postIssueComment.ts
|
|
1685
|
+
var postIssueComment2 = async (ctx) => {
|
|
1686
|
+
if (ctx.skipAgent && ctx.output.exitCode !== void 0) return;
|
|
1687
|
+
const targetType = ctx.data.commentTargetType;
|
|
1688
|
+
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
1689
|
+
if (!targetType || !targetNumber) return;
|
|
1690
|
+
const commitResult = ctx.data.commitResult;
|
|
1691
|
+
const hasCommits = Boolean(ctx.data.hasCommitsAhead);
|
|
1692
|
+
const prUrl = ctx.output.prUrl;
|
|
1693
|
+
if (!commitResult?.committed && !hasCommits) {
|
|
1694
|
+
const reason = "no changes to commit";
|
|
1695
|
+
postWith(targetType, targetNumber, `\u26A0\uFE0F kody2 FAILED: ${reason}`, ctx.cwd);
|
|
1696
|
+
ctx.output.exitCode = 3;
|
|
1697
|
+
ctx.output.reason = reason;
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
if (ctx.output.exitCode === 4 && ctx.data.prCrashReason) {
|
|
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
|
-
const failureReason = computeFailureReason2(ctx);
|
|
1567
|
-
const isFailure = failureReason.length > 0;
|
|
1568
|
-
const msg = isFailure ? `\u26A0\uFE0F kody2 FAILED: ${
|
|
1569
|
-
postWith(targetType, targetNumber, msg, ctx.cwd);
|
|
1570
|
-
let exitCode = 0;
|
|
1571
|
-
const agentDone = Boolean(ctx.data.agentDone);
|
|
1572
|
-
const verifyOk = ctx.data.verifyOk !== false;
|
|
1573
|
-
const misses = ctx.data.coverageMisses ?? [];
|
|
1574
|
-
if (!agentDone || misses.length > 0) exitCode = 1;
|
|
1575
|
-
else if (!verifyOk) exitCode = 2;
|
|
1576
|
-
ctx.output.exitCode = exitCode;
|
|
1577
|
-
ctx.output.reason = failureReason || void 0;
|
|
1705
|
+
const failureReason = computeFailureReason2(ctx);
|
|
1706
|
+
const isFailure = failureReason.length > 0;
|
|
1707
|
+
const msg = isFailure ? `\u26A0\uFE0F kody2 FAILED: ${truncate2(failureReason, 1500)}${prUrl ? ` \u2014 draft PR: ${prUrl}` : ""}` : `\u2705 kody2 PR opened: ${prUrl}`;
|
|
1708
|
+
postWith(targetType, targetNumber, msg, ctx.cwd);
|
|
1709
|
+
let exitCode = 0;
|
|
1710
|
+
const agentDone = Boolean(ctx.data.agentDone);
|
|
1711
|
+
const verifyOk = ctx.data.verifyOk !== false;
|
|
1712
|
+
const misses = ctx.data.coverageMisses ?? [];
|
|
1713
|
+
if (!agentDone || misses.length > 0) exitCode = 1;
|
|
1714
|
+
else if (!verifyOk) exitCode = 2;
|
|
1715
|
+
ctx.output.exitCode = exitCode;
|
|
1716
|
+
ctx.output.reason = failureReason || void 0;
|
|
1717
|
+
};
|
|
1718
|
+
function computeFailureReason2(ctx) {
|
|
1719
|
+
const misses = ctx.data.coverageMisses ?? [];
|
|
1720
|
+
if (misses.length > 0) return `missing tests: ${misses.map((m) => m.expectedTest).join(", ")}`;
|
|
1721
|
+
const agentDone = Boolean(ctx.data.agentDone);
|
|
1722
|
+
if (!agentDone) {
|
|
1723
|
+
return ctx.data.agentFailureReason || ctx.data.agentError || "agent did not emit DONE";
|
|
1724
|
+
}
|
|
1725
|
+
if (ctx.data.verifyOk === false) return ctx.data.verifyReason || "verify failed";
|
|
1726
|
+
return "";
|
|
1727
|
+
}
|
|
1728
|
+
function postWith(type, n, body, cwd) {
|
|
1729
|
+
try {
|
|
1730
|
+
if (type === "issue") postIssueComment(n, body, cwd);
|
|
1731
|
+
else postPrReviewComment(n, body, cwd);
|
|
1732
|
+
} catch {
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
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;
|
|
1747
|
+
}
|
|
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;
|
|
1762
|
+
}
|
|
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
|
+
);
|
|
1578
1786
|
};
|
|
1579
|
-
function
|
|
1580
|
-
const misses = ctx.data.coverageMisses ?? [];
|
|
1581
|
-
if (misses.length > 0) return `missing tests: ${misses.map((m) => m.expectedTest).join(", ")}`;
|
|
1582
|
-
const agentDone = Boolean(ctx.data.agentDone);
|
|
1583
|
-
if (!agentDone) {
|
|
1584
|
-
return ctx.data.agentFailureReason || ctx.data.agentError || "agent did not emit DONE";
|
|
1585
|
-
}
|
|
1586
|
-
if (ctx.data.verifyOk === false) return ctx.data.verifyReason || "verify failed";
|
|
1587
|
-
return "";
|
|
1588
|
-
}
|
|
1589
|
-
function postWith(type, n, body, cwd) {
|
|
1787
|
+
function getConflictedFiles(cwd) {
|
|
1590
1788
|
try {
|
|
1591
|
-
|
|
1592
|
-
|
|
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) : [];
|
|
1593
1795
|
} catch {
|
|
1796
|
+
return [];
|
|
1594
1797
|
}
|
|
1595
1798
|
}
|
|
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}
|
|
1596
1806
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
composePrompt
|
|
1606
|
-
};
|
|
1607
|
-
var postflightScripts = {
|
|
1608
|
-
parseAgentResult: parseAgentResult2,
|
|
1609
|
-
verify,
|
|
1610
|
-
checkCoverageWithRetry,
|
|
1611
|
-
commitAndPush: commitAndPush2,
|
|
1612
|
-
ensurePr: ensurePr2,
|
|
1613
|
-
postIssueComment: postIssueComment2
|
|
1614
|
-
};
|
|
1615
|
-
var allScriptNames = /* @__PURE__ */ new Set([
|
|
1616
|
-
...Object.keys(preflightScripts),
|
|
1617
|
-
...Object.keys(postflightScripts)
|
|
1618
|
-
]);
|
|
1619
|
-
|
|
1620
|
-
// src/agent.ts
|
|
1621
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1622
|
-
import * as fs7 from "fs";
|
|
1623
|
-
import * as path6 from "path";
|
|
1624
|
-
|
|
1625
|
-
// src/format.ts
|
|
1626
|
-
function renderEvent(msg, opts = {}) {
|
|
1627
|
-
if (opts.quiet) {
|
|
1628
|
-
if (msg.type === "result") return formatResult(msg);
|
|
1629
|
-
return null;
|
|
1630
|
-
}
|
|
1631
|
-
switch (msg.type) {
|
|
1632
|
-
case "system":
|
|
1633
|
-
return null;
|
|
1634
|
-
case "assistant":
|
|
1635
|
-
return formatAssistant(msg, opts);
|
|
1636
|
-
case "user":
|
|
1637
|
-
return formatUserToolResult(msg, opts);
|
|
1638
|
-
case "result":
|
|
1639
|
-
return formatResult(msg);
|
|
1640
|
-
default:
|
|
1641
|
-
return null;
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
function formatAssistant(msg, opts) {
|
|
1645
|
-
const content = msg.message?.content ?? [];
|
|
1646
|
-
const lines = [];
|
|
1647
|
-
for (const block of content) {
|
|
1648
|
-
if (block.type === "text") {
|
|
1649
|
-
const text = block.text.trim();
|
|
1650
|
-
if (text) lines.push(text);
|
|
1651
|
-
} else if (block.type === "tool_use") {
|
|
1652
|
-
const tu = block;
|
|
1653
|
-
lines.push(`\u2192 ${tu.name}${summarizeToolInput(tu.name, tu.input)}`);
|
|
1807
|
+
\`\`\`
|
|
1808
|
+
${content.slice(0, 6e3)}
|
|
1809
|
+
\`\`\`
|
|
1810
|
+
`;
|
|
1811
|
+
total += snippet.length;
|
|
1812
|
+
chunks.push(snippet);
|
|
1813
|
+
if (total >= maxBytes) break;
|
|
1814
|
+
} catch {
|
|
1654
1815
|
}
|
|
1655
1816
|
}
|
|
1656
|
-
return
|
|
1817
|
+
return chunks.join("\n");
|
|
1657
1818
|
}
|
|
1658
|
-
function
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
if (block.type === "tool_result") {
|
|
1663
|
-
const tr = block;
|
|
1664
|
-
const text = stringifyToolContent(tr.content);
|
|
1665
|
-
const lineCount = text.split("\n").length;
|
|
1666
|
-
const sizeBytes = text.length;
|
|
1667
|
-
const flag = tr.is_error ? " ERROR" : "";
|
|
1668
|
-
const summary = ` \u21B3${flag} ${lineCount} lines, ${formatBytes(sizeBytes)}`;
|
|
1669
|
-
if (opts.verbose) {
|
|
1670
|
-
lines.push(`${summary}
|
|
1671
|
-
${truncate2(text, 4e3)}`);
|
|
1672
|
-
} else {
|
|
1673
|
-
lines.push(summary);
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1819
|
+
function tryPostPr3(prNumber, body, cwd) {
|
|
1820
|
+
try {
|
|
1821
|
+
postPrReviewComment(prNumber, body, cwd);
|
|
1822
|
+
} catch {
|
|
1676
1823
|
}
|
|
1677
|
-
return lines.length > 0 ? lines.join("\n") : null;
|
|
1678
|
-
}
|
|
1679
|
-
function formatResult(msg) {
|
|
1680
|
-
const ok = msg.subtype === "success";
|
|
1681
|
-
const tag = ok ? "DONE" : `FAILED (${msg.subtype ?? "unknown"})`;
|
|
1682
|
-
const dur = msg.duration_ms ? ` ${(msg.duration_ms / 1e3).toFixed(1)}s` : "";
|
|
1683
|
-
const turns = msg.num_turns ? ` ${msg.num_turns} turns` : "";
|
|
1684
|
-
const cost = typeof msg.total_cost_usd === "number" ? ` $${msg.total_cost_usd.toFixed(4)}` : "";
|
|
1685
|
-
return `
|
|
1686
|
-
=== ${tag}${dur}${turns}${cost} ===`;
|
|
1687
1824
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
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,9 +2223,60 @@ function finish(out) {
|
|
|
2105
2223
|
}
|
|
2106
2224
|
|
|
2107
2225
|
// src/kody2-cli.ts
|
|
2108
|
-
import * as fs10 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";
|
|
2229
|
+
|
|
2230
|
+
// src/dispatch.ts
|
|
2231
|
+
import * as fs11 from "fs";
|
|
2232
|
+
function autoDispatch(explicit) {
|
|
2233
|
+
if (explicit?.mode && explicit.target) {
|
|
2234
|
+
return {
|
|
2235
|
+
mode: explicit.mode,
|
|
2236
|
+
target: explicit.target
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
2240
|
+
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
2241
|
+
if (!eventName || !eventPath || !fs11.existsSync(eventPath)) return null;
|
|
2242
|
+
let event = {};
|
|
2243
|
+
try {
|
|
2244
|
+
event = JSON.parse(fs11.readFileSync(eventPath, "utf-8"));
|
|
2245
|
+
} catch {
|
|
2246
|
+
return null;
|
|
2247
|
+
}
|
|
2248
|
+
if (eventName === "workflow_dispatch") {
|
|
2249
|
+
const n = parseInt(String(event.inputs?.issue_number ?? ""), 10);
|
|
2250
|
+
if (!Number.isNaN(n) && n > 0) return { mode: "run", target: n };
|
|
2251
|
+
return null;
|
|
2252
|
+
}
|
|
2253
|
+
if (eventName === "issue_comment") {
|
|
2254
|
+
const body = String(event.comment?.body ?? "").toLowerCase();
|
|
2255
|
+
const issueNum = Number(event.issue?.number ?? 0);
|
|
2256
|
+
const isPr = !!event.issue?.pull_request;
|
|
2257
|
+
if (!issueNum) return null;
|
|
2258
|
+
const afterTag = extractAfterTag(body);
|
|
2259
|
+
if (isPr) {
|
|
2260
|
+
if (/\bfix-ci\b/.test(afterTag)) return { mode: "fix-ci", target: issueNum };
|
|
2261
|
+
if (/\bresolve\b/.test(afterTag)) return { mode: "resolve", target: issueNum };
|
|
2262
|
+
const feedbackText = extractFeedback(afterTag);
|
|
2263
|
+
return { mode: "fix", target: issueNum, feedback: feedbackText };
|
|
2264
|
+
}
|
|
2265
|
+
return { mode: "run", target: issueNum };
|
|
2266
|
+
}
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
function extractAfterTag(body) {
|
|
2270
|
+
const idx = body.indexOf("@kody2");
|
|
2271
|
+
if (idx === -1) return body;
|
|
2272
|
+
return body.slice(idx + "@kody2".length).trim();
|
|
2273
|
+
}
|
|
2274
|
+
function extractFeedback(afterTag) {
|
|
2275
|
+
const cleaned = afterTag.replace(/^(fix|please|kindly)[\s:,.-]+/i, "").trim();
|
|
2276
|
+
return cleaned.length > 0 ? cleaned : void 0;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// src/kody2-cli.ts
|
|
2111
2280
|
var CI_HELP = `kody2 ci \u2014 minimal-YAML autonomous engineer (CI preflight + run)
|
|
2112
2281
|
|
|
2113
2282
|
Usage:
|
|
@@ -2192,9 +2361,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
2192
2361
|
return token;
|
|
2193
2362
|
}
|
|
2194
2363
|
function detectPackageManager(cwd) {
|
|
2195
|
-
if (
|
|
2196
|
-
if (
|
|
2197
|
-
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";
|
|
2198
2367
|
return "npm";
|
|
2199
2368
|
}
|
|
2200
2369
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -2274,13 +2443,13 @@ function postFailureTail(issueNumber, cwd, reason) {
|
|
|
2274
2443
|
const logPath = path9.join(cwd, ".kody2", "last-run.jsonl");
|
|
2275
2444
|
let tail = "";
|
|
2276
2445
|
try {
|
|
2277
|
-
if (
|
|
2278
|
-
const content =
|
|
2446
|
+
if (fs12.existsSync(logPath)) {
|
|
2447
|
+
const content = fs12.readFileSync(logPath, "utf-8");
|
|
2279
2448
|
tail = content.slice(-3e3);
|
|
2280
2449
|
}
|
|
2281
2450
|
} catch {
|
|
2282
2451
|
}
|
|
2283
|
-
const body = tail ? `\u26A0\uFE0F kody2 preflight failed: ${
|
|
2452
|
+
const body = tail ? `\u26A0\uFE0F kody2 preflight failed: ${truncate2(reason, 500)}
|
|
2284
2453
|
|
|
2285
2454
|
<details><summary>Last-run log tail</summary>
|
|
2286
2455
|
|
|
@@ -2288,7 +2457,7 @@ function postFailureTail(issueNumber, cwd, reason) {
|
|
|
2288
2457
|
${tail}
|
|
2289
2458
|
\`\`\`
|
|
2290
2459
|
|
|
2291
|
-
</details>` : `\u26A0\uFE0F kody2 preflight failed: ${
|
|
2460
|
+
</details>` : `\u26A0\uFE0F kody2 preflight failed: ${truncate2(reason, 1500)}`;
|
|
2292
2461
|
try {
|
|
2293
2462
|
postIssueComment(issueNumber, body, cwd);
|
|
2294
2463
|
} catch {
|
|
@@ -2300,15 +2469,26 @@ async function runCi(argv) {
|
|
|
2300
2469
|
return 0;
|
|
2301
2470
|
}
|
|
2302
2471
|
const args = parseCiArgs(argv);
|
|
2472
|
+
const autoFallback = !args.issueNumber ? autoDispatch() : null;
|
|
2473
|
+
if (!args.issueNumber && !autoFallback) {
|
|
2474
|
+
} else {
|
|
2475
|
+
args.errors = args.errors.filter((e) => !e.includes("--issue"));
|
|
2476
|
+
}
|
|
2303
2477
|
if (args.errors.length > 0 && !args.errors.includes("__HELP__")) {
|
|
2304
2478
|
for (const e of args.errors) process.stderr.write(`error: ${e}
|
|
2305
2479
|
`);
|
|
2306
|
-
process.stderr.write(
|
|
2480
|
+
process.stderr.write(`
|
|
2481
|
+
${CI_HELP}`);
|
|
2307
2482
|
return 64;
|
|
2308
2483
|
}
|
|
2309
2484
|
const cwd = args.cwd ? path9.resolve(args.cwd) : process.cwd();
|
|
2310
|
-
const
|
|
2311
|
-
|
|
2485
|
+
const dispatch = autoFallback ?? {
|
|
2486
|
+
mode: "run",
|
|
2487
|
+
target: args.issueNumber,
|
|
2488
|
+
feedback: void 0
|
|
2489
|
+
};
|
|
2490
|
+
const issueNumber = dispatch.target;
|
|
2491
|
+
process.stdout.write(`\u2192 kody2 preflight (cwd=${cwd}, mode=${dispatch.mode}, target=${issueNumber})
|
|
2312
2492
|
`);
|
|
2313
2493
|
try {
|
|
2314
2494
|
const n = unpackAllSecrets();
|
|
@@ -2345,11 +2525,17 @@ async function runCi(argv) {
|
|
|
2345
2525
|
postFailureTail(issueNumber, cwd, `preflight crashed: ${msg}`);
|
|
2346
2526
|
return 99;
|
|
2347
2527
|
}
|
|
2348
|
-
process.stdout.write(
|
|
2528
|
+
process.stdout.write(`\u2192 kody2: preflight done, handing off to kody2 ${dispatch.mode}
|
|
2529
|
+
|
|
2530
|
+
`);
|
|
2349
2531
|
try {
|
|
2350
2532
|
const config = loadConfig(cwd);
|
|
2533
|
+
const cliArgs = { mode: dispatch.mode };
|
|
2534
|
+
if (dispatch.mode === "run") cliArgs.issue = issueNumber;
|
|
2535
|
+
else cliArgs.pr = issueNumber;
|
|
2536
|
+
if (dispatch.feedback) cliArgs.feedback = dispatch.feedback;
|
|
2351
2537
|
const result = await runExecutable("build", {
|
|
2352
|
-
cliArgs
|
|
2538
|
+
cliArgs,
|
|
2353
2539
|
cwd,
|
|
2354
2540
|
config,
|
|
2355
2541
|
verbose: args.verbose,
|
|
@@ -2363,12 +2549,74 @@ async function runCi(argv) {
|
|
|
2363
2549
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2364
2550
|
process.stderr.write(`[kody2] run crashed: ${msg}
|
|
2365
2551
|
`);
|
|
2366
|
-
if (err instanceof Error && err.stack) process.stderr.write(err.stack
|
|
2552
|
+
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
2553
|
+
`);
|
|
2367
2554
|
postFailureTail(issueNumber, cwd, `run crashed: ${msg}`);
|
|
2368
2555
|
return 99;
|
|
2369
2556
|
}
|
|
2370
2557
|
}
|
|
2371
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
|
+
|
|
2372
2620
|
// src/entry.ts
|
|
2373
2621
|
var HELP_TEXT = `kody2 \u2014 single-session autonomous engineer
|
|
2374
2622
|
|
|
@@ -2385,10 +2633,10 @@ All commands dispatch to the Build executable with a specific mode. The
|
|
|
2385
2633
|
executable is defined by \`src/executables/build/profile.json\`.
|
|
2386
2634
|
|
|
2387
2635
|
Exit codes:
|
|
2388
|
-
0 success (PR opened, verify passed)
|
|
2636
|
+
0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
|
|
2389
2637
|
1 agent reported FAILED (draft PR opened)
|
|
2390
|
-
2 verify failed (draft PR opened)
|
|
2391
|
-
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)
|
|
2392
2640
|
4 PR creation failed
|
|
2393
2641
|
5 uncommitted changes on target branch
|
|
2394
2642
|
64 invalid CLI args
|
|
@@ -2408,7 +2656,18 @@ function parseArgs(argv) {
|
|
|
2408
2656
|
parseCommandArgs(cmd, argv.slice(1), result);
|
|
2409
2657
|
return result;
|
|
2410
2658
|
}
|
|
2411
|
-
|
|
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(", ")})`);
|
|
2412
2671
|
return result;
|
|
2413
2672
|
}
|
|
2414
2673
|
function parseCommandArgs(cmd, rest, result) {
|
|
@@ -2443,7 +2702,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2443
2702
|
if (args.errors.length > 0) {
|
|
2444
2703
|
for (const e of args.errors) process.stderr.write(`error: ${e}
|
|
2445
2704
|
`);
|
|
2446
|
-
process.stderr.write(
|
|
2705
|
+
process.stderr.write(`
|
|
2706
|
+
${HELP_TEXT}`);
|
|
2447
2707
|
return 64;
|
|
2448
2708
|
}
|
|
2449
2709
|
if (args.command === "help") {
|
|
@@ -2451,7 +2711,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2451
2711
|
return 0;
|
|
2452
2712
|
}
|
|
2453
2713
|
if (args.command === "version") {
|
|
2454
|
-
process.stdout.write(
|
|
2714
|
+
process.stdout.write(`kody2 ${package_default.version}
|
|
2715
|
+
`);
|
|
2455
2716
|
return 0;
|
|
2456
2717
|
}
|
|
2457
2718
|
if (args.command === "ci") {
|
|
@@ -2461,7 +2722,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2461
2722
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2462
2723
|
process.stderr.write(`[kody2] fatal: ${msg}
|
|
2463
2724
|
`);
|
|
2464
|
-
if (err instanceof Error && err.stack) process.stderr.write(err.stack
|
|
2725
|
+
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
2726
|
+
`);
|
|
2465
2727
|
return 99;
|
|
2466
2728
|
}
|
|
2467
2729
|
}
|
|
@@ -2477,6 +2739,27 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2477
2739
|
`);
|
|
2478
2740
|
return 99;
|
|
2479
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
|
+
}
|
|
2480
2763
|
const cliArgs = { mode: args.command };
|
|
2481
2764
|
if (args.issueNumber !== void 0) cliArgs.issue = args.issueNumber;
|
|
2482
2765
|
if (args.prNumber !== void 0) cliArgs.pr = args.prNumber;
|
|
@@ -2495,7 +2778,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2495
2778
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2496
2779
|
process.stderr.write(`[kody2] wrapper crashed: ${msg}
|
|
2497
2780
|
`);
|
|
2498
|
-
if (err instanceof Error && err.stack) process.stderr.write(err.stack
|
|
2781
|
+
if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
|
|
2782
|
+
`);
|
|
2499
2783
|
process.stdout.write(`PR_URL=FAILED: wrapper crashed: ${msg}
|
|
2500
2784
|
`);
|
|
2501
2785
|
return 99;
|